diff --git a/packages/mini-markdown-editor/src/EditorWrapper.tsx b/packages/mini-markdown-editor/src/EditorWrapper.tsx index d6b111e5053ab8979211aa6279cb5e2c9d4ba417..62e1d59c8b0914551861249bcdf1e7ba22574bac 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 1c40b9b99903320e96680f4a716a4cd99dd4eb6f..53735a8136aaf38b3d75062b1da5f7b2116360fa 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 3304bb597e3ce54feb07027982a3bba563f3e834..dd0315433ce7a8b95a1bbfbd559d13686746079a 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 0000000000000000000000000000000000000000..1fb41171eddeae52714265e0a9717c3f6ed9861f --- /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 0000000000000000000000000000000000000000..5d0ab77e3b34ca1bad6d7707f5b0f44463ad93e6 --- /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]); +};