From fe92ecf3eeb962bada36c0a55bf62edec4a50897 Mon Sep 17 00:00:00 2001 From: tabzzz Date: Mon, 27 Jan 2025 03:06:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(mini-markdown-editor):=20=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=BF=AB=E6=8D=B7=E9=94=AE=E6=94=AF=E6=8C=81=EF=BC=8C?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=BF=AB=E6=8D=B7=E9=94=AE=E7=9A=84=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E6=B3=A8=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/EditorWrapper.tsx | 49 +++--- .../src/common/hotkeys.ts | 16 +- .../src/components/Editor/index.tsx | 22 ++- .../components/providers/hotkeys-provider.tsx | 141 ++++++++++++++++++ .../src/hooks/use-editor-shortcuts.ts | 18 +++ 5 files changed, 214 insertions(+), 32 deletions(-) create mode 100644 packages/mini-markdown-editor/src/components/providers/hotkeys-provider.tsx create mode 100644 packages/mini-markdown-editor/src/hooks/use-editor-shortcuts.ts diff --git a/packages/mini-markdown-editor/src/EditorWrapper.tsx b/packages/mini-markdown-editor/src/EditorWrapper.tsx index d6b111e..62e1d59 100644 --- a/packages/mini-markdown-editor/src/EditorWrapper.tsx +++ b/packages/mini-markdown-editor/src/EditorWrapper.tsx @@ -8,6 +8,7 @@ import Status from "@/components/Status"; import { Row, Col } from "antd"; import { ToolbarProvider } from "@/components/providers/toolbar-provider"; import { ConfigProvider } from "@/components/providers/config-provider"; +import { HotkeysProvider } from "@/components/providers/hotkeys-provider"; import { GlobalConfig } from "./types/global-config"; import { useToolbarStore } from "./store/toolbar"; @@ -75,29 +76,31 @@ const EditorWrapper: FC = (config) => { return ( - - {/* 工具栏 */} - - - {/* 内容区域 */} - - - - {/* 编辑区 */} - - - - {/* 渲染区 */} - - - - {/* 渲染区 */} - 123 - - - {/* 分割线 */} - - + + + {/* 工具栏 */} + + + {/* 内容区域 */} + + + + {/* 编辑区 */} + + + + {/* 渲染区 */} + + + + {/* 渲染区 */} + 123 + + + {/* 分割线 */} + + + {/* 底部状态栏 */} {config.status ? : null} diff --git a/packages/mini-markdown-editor/src/common/hotkeys.ts b/packages/mini-markdown-editor/src/common/hotkeys.ts index 1c40b9b..53735a8 100644 --- a/packages/mini-markdown-editor/src/common/hotkeys.ts +++ b/packages/mini-markdown-editor/src/common/hotkeys.ts @@ -3,7 +3,7 @@ type Command = string; type Description = string; export class Hotkey { // Tools - static readonly Title = { + static readonly TITLE = { FIRST: new Hotkey("mod+1", "heaing-1"), SECOND: new Hotkey("mod+2", "heaing-2"), THIRD: new Hotkey("mod+3", "heaing-3"), @@ -35,12 +35,20 @@ export class Hotkey { Hotkey.validateCommand(command); } + // 检测是否为 Mac 系统 + private static readonly isMac = (() => { + if ("userAgentData" in navigator) { + return (navigator as any).userAgentData?.platform === "macOS"; + } + return /Mac|iPod|iPhone|iPad/.test(navigator.userAgent); + })(); + // 键值映射表 private static readonly KEY_MAPPING = { - mod: process.platform === "darwin" ? "⌘" : "Ctrl", + mod: Hotkey.isMac ? "⌘" : "Ctrl", shift: "⇧", - alt: process.platform === "darwin" ? "⌥" : "Alt", - ctrl: process.platform === "darwin" ? "⌃" : "Ctrl", + alt: Hotkey.isMac ? "⌥" : "Alt", + ctrl: Hotkey.isMac ? "⌃" : "Ctrl", equal: "=", minus: "-", plus: "+", diff --git a/packages/mini-markdown-editor/src/components/Editor/index.tsx b/packages/mini-markdown-editor/src/components/Editor/index.tsx index 3304bb5..dd03154 100644 --- a/packages/mini-markdown-editor/src/components/Editor/index.tsx +++ b/packages/mini-markdown-editor/src/components/Editor/index.tsx @@ -1,4 +1,4 @@ -import { FC, useCallback, useEffect, useRef } from "react"; +import { FC, useCallback, useContext, useEffect, useMemo, useRef } from "react"; import styled from "styled-components"; import CodeMirror, { type EditorView, ViewUpdate } from "@uiw/react-codemirror"; import { markdown, markdownLanguage } from "@codemirror/lang-markdown"; @@ -8,6 +8,8 @@ import { useEditorContentStore } from "@/store/editor"; import { handleEditorScroll } from "@/utils/handle-scroll"; import { safeLocalStorage } from "@/utils/storage"; import { EDITOR_CONTENT_KEY, SYNC_SCROLL_STATUS } from "@/common"; +import { useEditorShortcuts } from "@/hooks/use-editor-shortcuts"; +import { HotkeysContext } from "../providers/hotkeys-provider"; const ScrollWrapper = styled.div` width: 100%; @@ -57,6 +59,7 @@ const Editor: FC = () => { }, [editorViewRef], ); + useEditorShortcuts(); // 处理重加载后的光标位置 useEffect(() => { @@ -95,16 +98,24 @@ const Editor: FC = () => { setScrollWrapper("editor"); }; + const { createKeymapExtension } = useContext(HotkeysContext); + // 创建扩展数组 + const extensions = useMemo( + () => [ + markdown({ base: markdownLanguage, codeLanguages: languages }), + scrollWrapper === "editor" ? eventExt : [], + createKeymapExtension!(), + ], + [scrollWrapper, createKeymapExtension], + ); + return ( { highlightActiveLineGutter: false, searchKeymap: false, autocompletion: false, + defaultKeymap: false, }} autoFocus={true} style={{ height: "100%" }} diff --git a/packages/mini-markdown-editor/src/components/providers/hotkeys-provider.tsx b/packages/mini-markdown-editor/src/components/providers/hotkeys-provider.tsx new file mode 100644 index 0000000..1fb4117 --- /dev/null +++ b/packages/mini-markdown-editor/src/components/providers/hotkeys-provider.tsx @@ -0,0 +1,141 @@ +import React, { createContext, useCallback, useMemo } from "react"; +import { useHotkeys } from "react-hotkeys-hook"; +import { Hotkey } from "@/common/hotkeys"; +import { keymap } from "@codemirror/view"; +import type { KeyBinding } from "@codemirror/view"; +import { Extension } from "@uiw/react-codemirror"; + +interface HotkeysContextValue { + registerHandler?: (hotkey: Hotkey, handler: () => void) => void; + unregisterHandler?: (hotkey: Hotkey) => void; + isEnabled?: boolean; + setEnabled?: (enabled: boolean) => void; + createKeymapExtension?: () => Extension; + handleKeyEvent?: (event: KeyboardEvent) => boolean; +} + +export const HotkeysContext = createContext({ + registerHandler: () => {}, + unregisterHandler: () => {}, +}); + +interface HotkeysProviderProps { + children: React.ReactNode; + // 是否启用快捷键 + enabled?: boolean; +} + +// --- +// 将 Hotkey 命令转换为 CodeMirror 键映射格式(蜜汁标准) +// TODO: 可以更简洁一些 +function convertToCodeMirrorKey(command: string): string { + return command + .split("+") + .map((key) => { + switch (key) { + case "mod": + return "Mod"; + case "shift": + return "Shift"; + case "alt": + return "Alt"; + case "ctrl": + return "Ctrl"; + default: + return key.charAt(0).toUpperCase() + key.slice(1); + } + }) + .join("-"); +} + +// TODO: 用开关控制热键是否启用 +export function HotkeysProvider({ children, enabled = true }: HotkeysProviderProps) { + const [handlers] = React.useState(new Map void>()); + const [isEnabled, setEnabled] = React.useState(enabled); + + // 注册快捷键 + const registerHandler = useCallback( + (hotkey: Hotkey, handler: () => void) => { + handlers.set(hotkey.command, handler); + }, + [handlers], + ); + + // 取消注册(用于生命周期后期的销毁) + const unregisterHandler = useCallback( + (hotkey: Hotkey) => { + handlers.delete(hotkey.command); + }, + [handlers], + ); + + // 统一的事件处理函数 + const handleKeyEvent = useCallback( + (event: KeyboardEvent): boolean => { + if (!isEnabled) return false; + + const pressedKeys = []; + if (event.metaKey || event.ctrlKey) pressedKeys.push("mod"); + if (event.shiftKey) pressedKeys.push("shift"); + if (event.altKey) pressedKeys.push("alt"); + pressedKeys.push(event.key.toLowerCase()); + + const command = pressedKeys.join("+"); + // 测试使用 + // console.log("Pressed command:", command); + + const handler = handlers.get(command); + if (handler) { + event.preventDefault(); + handler(); + return true; + } + return false; + }, + [handlers, isEnabled], + ); + + // 创建 CodeMirror 扩展 + const createKeymapExtension = useCallback(() => { + const keyBindings: KeyBinding[] = []; + handlers.forEach((handler, command) => { + // 转换映射格式 + const cmKey = convertToCodeMirrorKey(command); + keyBindings.push({ + key: cmKey, + run: () => { + if (!isEnabled) return false; + handler(); + return true; + }, + preventDefault: true, + }); + }); + return keymap.of(keyBindings); + }, [handlers, isEnabled]); + + // 全局热键处理 + useHotkeys( + "*", + (event: KeyboardEvent) => { + handleKeyEvent(event); + }, + { + enableOnFormTags: true, + }, + ); + + const value = useMemo( + () => ({ + registerHandler, + unregisterHandler, + isEnabled, + setEnabled, + createKeymapExtension, + handleKeyEvent, + }), + [registerHandler, unregisterHandler, isEnabled, createKeymapExtension, handleKeyEvent], + ); + + return {children}; +} diff --git a/packages/mini-markdown-editor/src/hooks/use-editor-shortcuts.ts b/packages/mini-markdown-editor/src/hooks/use-editor-shortcuts.ts new file mode 100644 index 0000000..5d0ab77 --- /dev/null +++ b/packages/mini-markdown-editor/src/hooks/use-editor-shortcuts.ts @@ -0,0 +1,18 @@ +import { Hotkey } from "@/common/hotkeys"; +import { HotkeysContext } from "@/components/providers/hotkeys-provider"; +import { useContext, useEffect } from "react"; + +export const useEditorShortcuts = () => { + const { registerHandler, unregisterHandler } = useContext(HotkeysContext); + + // 方法全部在这里添加 + //! 这里未定义的则按照默认方法执行 + useEffect(() => { + registerHandler!(Hotkey.TITLE.FIRST, () => {}); + + return () => { + unregisterHandler!(Hotkey.TITLE.FIRST); + unregisterHandler!(Hotkey.TITLE.THIRD); + }; + }, [registerHandler, unregisterHandler]); +}; -- Gitee