diff --git a/packages/mini-markdown-editor/package.json b/packages/mini-markdown-editor/package.json index 73661ebabfc031112302396d5f34cb589721f7c0..2c6695531a3bd6af5548f61c33404279d0c0ee5a 100644 --- a/packages/mini-markdown-editor/package.json +++ b/packages/mini-markdown-editor/package.json @@ -39,6 +39,7 @@ "highlight.js": "^11.11.1", "html2pdf.js": "^0.10.2", "nanoid": "^5.0.9", + "immer": "^10.1.1", "react-hotkeys-hook": "^4.6.1", "zustand": "^5.0.3" }, diff --git a/packages/mini-markdown-editor/src/App.tsx b/packages/mini-markdown-editor/src/App.tsx index 2e3fc3941ecf694d752442b989e74fae747507b4..31eeb4d4141085302a22891e1a71392153882de2 100644 --- a/packages/mini-markdown-editor/src/App.tsx +++ b/packages/mini-markdown-editor/src/App.tsx @@ -8,6 +8,8 @@ import "highlight.js/styles/atom-one-dark.css"; // import { ViewUpdate } from "./types/code-mirror"; import { EditorView } from "@uiw/react-codemirror"; import { EditorRef } from "./types/ref"; +import { toolbarConfig } from "./config/toolbar"; +import OlIcon from "@/assets/images/ol.svg?raw"; const AppWrapper = styled.div` width: 100%; @@ -71,6 +73,30 @@ const App: FC = () => { } }, []); + // 添加abc工具 + useEffect(() => { + try { + toolbarConfig.addToolbar({ + type: "abc", + title: "我是测试abc", + icon: OlIcon, + description: "我是描述abc", + hotkey: { + command: "Mod-p", + description: "控制台输出def", + handle: () => { + console.log("我是快捷键输出def"); + }, + }, + onClick: () => { + console.log("我是输出abc"); + }, + }); + } catch (error) { + console.error(error); + } + }, []); + return ( @@ -143,6 +169,7 @@ const App: FC = () => { 获取预览区实例 ((config, ref) => { } + editor={} preview={} /> diff --git a/packages/mini-markdown-editor/src/common/hotkeys.ts b/packages/mini-markdown-editor/src/common/hotkeys.ts index cde6d7f1f7e17a0ec4329502b80a0ed056269dd6..92bcb5e9d85a0f374344b7d8625e76891c37f909 100644 --- a/packages/mini-markdown-editor/src/common/hotkeys.ts +++ b/packages/mini-markdown-editor/src/common/hotkeys.ts @@ -30,10 +30,10 @@ export class Hotkey { // Actions static readonly SAVE = new Hotkey("mod+s", "Save"); - private constructor( + constructor( public readonly command: Command, public readonly description: Description, - public readonly handle?: void | (() => void), + public readonly handle?: () => void, ) { Hotkey.validateCommand(command); } @@ -104,4 +104,13 @@ export class Hotkey { throw new Error(`This is must!: ${command}`); } } + + // 生成配置对象的方法 + public toConfig() { + return { + command: this.codeMirrorCommand, + description: this.description, + handle: this.handle, + }; + } } diff --git a/packages/mini-markdown-editor/src/components/Editor/index.tsx b/packages/mini-markdown-editor/src/components/Editor/index.tsx index 741f4dad1c30344a869e6c86e1d7308a37214ea9..314121c5cdac4dc1a27de4df1cef1342311bb1b8 100644 --- a/packages/mini-markdown-editor/src/components/Editor/index.tsx +++ b/packages/mini-markdown-editor/src/components/Editor/index.tsx @@ -7,7 +7,8 @@ import { handleEditorScroll } from "@/utils/handle-scroll"; import { usePersistEditorContent } from "@/hooks/use-persist-editor-content"; import { ConfigContext } from "../providers/config-provider"; import { createEditorExtensions } from "@/extensions/codemirror"; -import { Callback } from "@/types/global-config"; +import { Callback, GlobalConfig } from "@/types/global-config"; +import { filterContextProps } from "@/utils/filter-context-props"; const ScrollWrapper = styled.div<{ $lineNumbers?: boolean; @@ -40,7 +41,13 @@ const ScrollWrapper = styled.div<{ } `; -const Editor: FC<{ isSyncScroll: boolean }> = ({ isSyncScroll }) => { +interface EditorProps extends GlobalConfig { + className?: string; + style?: React.CSSProperties; + isSyncScroll: boolean; +} + +const Editor: FC = (props) => { const { content, setContent, @@ -103,11 +110,15 @@ const Editor: FC<{ isSyncScroll: boolean }> = ({ isSyncScroll }) => { scroll: () => { if (scrollWrapper !== "editor") return; const view = editorViewRef.current; - if (!(view && previewView && isSyncScroll)) return; + if (!(view && previewView && props.isSyncScroll)) return; handleEditorScroll({ editorView: view, previewView }); }, }); + const { className, style, ...CodeMirrorProps } = useMemo(() => { + return filterContextProps(props); + }, [props]); + const handleMouseEnter = () => { setScrollWrapper("editor"); }; @@ -137,7 +148,7 @@ const Editor: FC<{ isSyncScroll: boolean }> = ({ isSyncScroll }) => { return ( = ({ isSyncScroll }) => { defaultKeymap: true, }} autoFocus={true} - style={{ height: "100%" }} + style={{ height: "100%", ...(style || {}) }} onChange={handleChange} onMouseEnter={handleMouseEnter} + {...CodeMirrorProps} /> ); diff --git a/packages/mini-markdown-editor/src/config/base.ts b/packages/mini-markdown-editor/src/config/base.ts new file mode 100644 index 0000000000000000000000000000000000000000..26934c5b50937386d751252c082631d435ff97d8 --- /dev/null +++ b/packages/mini-markdown-editor/src/config/base.ts @@ -0,0 +1,127 @@ +import { EventEmitter } from "events"; + +export interface EventCallback { + (...args: any[]): void; +} + +interface BaseConfig { + name?: string; + // 最大监听器数量 + maxListeners?: number; +} + +export abstract class BaseClass { + protected readonly config: BaseConfig; + protected readonly eventEmitter: EventEmitter; + protected readonly logger: Console; + protected isDestroyed: boolean = false; + + constructor(config: BaseConfig = {}) { + this.config = { + ...config, + }; + + this.eventEmitter = new EventEmitter(); + this.eventEmitter.setMaxListeners(this.config.maxListeners!); + this.logger = console; + } + + // 错误日志 + protected error(...args: unknown[]): void { + this.logger.error(`[${this.config.name}]`, ...args); + } + + /** + * 添加事件监听器 + * @param event 事件名称 + * @param listener 监听器回调函数 + */ + public on(event: string, listener: EventCallback): void { + if (typeof listener !== "function") { + throw new TypeError("Listener must be a function"); + } + this.eventEmitter.on(event, listener); + } + + /** + * 一次性事件监听器 + * @param event 事件名称 + * @param listener 监听器回调函数 + */ + public once(event: string, listener: EventCallback): void { + if (typeof listener !== "function") { + throw new TypeError("Listener must be a function"); + } + this.eventEmitter.once(event, listener); + } + + /** + * 移除事件监听器 + * @param event 事件名称 + * @param listener 监听器回调函数 + */ + public off(event: string, listener: EventCallback): void { + if (typeof listener !== "function") { + throw new TypeError("Listener must be a function"); + } + this.eventEmitter.off(event, listener); + } + + /** + * 触发事件 + * @param event 事件名称 + * @param args 事件参数 + */ + protected emit(event: string, ...args: unknown[]): void { + this.eventEmitter.emit(event, ...args); + } + + public isDestroy(): boolean { + return this.isDestroyed; + } + + /** + * 获取当前注册的所有事件 + */ + public getEvents(): string[] { + return this.eventEmitter.eventNames() as string[]; + } + + /** + * 获取特定事件的监听器数量 + * @param event 事件名称 + */ + public listenerCount(event: string): number { + return this.eventEmitter.listenerCount(event); + } + + // 销毁实例 + public destroy(): void { + if (this.isDestroyed) { + return; + } + + try { + // 触发销毁前事件 + this.emit("beforeDestroy"); + + // 移除所有事件监听器 + this.eventEmitter.removeAllListeners(); + + // 标记为已销毁 + this.isDestroyed = true; + + // 触发销毁完成事件 + this.emit("destroyed"); + } catch (error) { + this.error("Error during destruction:", error); + } + } + + protected checkDestroyed(): void { + if (this.isDestroyed) { + this.logger.error("ERROR ISDESTOYED"); + // TODO: 后续操作 + } + } +} diff --git a/packages/mini-markdown-editor/src/config/global.ts b/packages/mini-markdown-editor/src/config/global.ts index 9b90b351804b52a93d46443c8347f49552bd36ef..ed2a041e3186ce0a311030016a4b87bc64e1da89 100644 --- a/packages/mini-markdown-editor/src/config/global.ts +++ b/packages/mini-markdown-editor/src/config/global.ts @@ -1,10 +1,15 @@ -import { GlobalConfig } from "@/types/global-config"; +import { GlobalContextConfig } from "@/types/global-config"; // 默认配置 -export const defaultGlobalConfig: GlobalConfig = { +export const defaultGlobalConfig: GlobalContextConfig = { status: true, // 底部状态栏 theme: "light", // 主题 local: true, // 是否开启本地存储 lineNumbers: false, // 是否显示行号 enableShortcuts: true, // 是否开启快捷键 + onSave: () => {}, // 保存触发 + onChange: () => {}, // 内容改变触发 + onUpload: () => {}, // 上传图片触发 + onDragUpload: () => {}, // 拖拽上传图片触发 + onPatseUpload: () => {}, // 粘贴上传图片触发 }; diff --git a/packages/mini-markdown-editor/src/config/toolbar/base.tsx b/packages/mini-markdown-editor/src/config/toolbar/base.tsx index 9be28135b0f91a0853cccd9c619c10432b4ab490..12f1cba22e1e42b71bf11d25222e0d95c32e9027 100644 --- a/packages/mini-markdown-editor/src/config/toolbar/base.tsx +++ b/packages/mini-markdown-editor/src/config/toolbar/base.tsx @@ -25,7 +25,7 @@ import Save from "@/components/Toolbar/Save"; // 快捷键描述 import { Hotkey } from "@/common/hotkeys"; -export const toolbar: ToolbarItem[] = [ +export const defaultToolbar: ToolbarItem[] = [ { type: "heading", icon: HeadingIcon, @@ -34,31 +34,37 @@ export const toolbar: ToolbarItem[] = [ { title: "H1 一级标题", type: "heading-1", + hotkey: Hotkey.TITLE.FIRST.toConfig(), onClick: () => InsertTextEvent("heading-1"), }, { title: "H2 二级标题", type: "heading-2", + hotkey: Hotkey.TITLE.SECOND.toConfig(), onClick: () => InsertTextEvent("heading-2"), }, { title: "H3 三级标题", type: "heading-3", + hotkey: Hotkey.TITLE.THIRD.toConfig(), onClick: () => InsertTextEvent("heading-3"), }, { title: "H4 四级标题", type: "heading-4", + hotkey: Hotkey.TITLE.FOURTH.toConfig(), onClick: () => InsertTextEvent("heading-4"), }, { title: "H5 五级标题", type: "heading-5", + hotkey: Hotkey.TITLE.FIFTH.toConfig(), onClick: () => InsertTextEvent("heading-5"), }, { title: "H6 六级标题", type: "heading-6", + hotkey: Hotkey.TITLE.SIXTH.toConfig(), onClick: () => InsertTextEvent("heading-6"), }, ], @@ -68,6 +74,7 @@ export const toolbar: ToolbarItem[] = [ icon: BoldIcon, title: "加粗", description: Hotkey.BOLD.readableCommand, + hotkey: Hotkey.BOLD.toConfig(), onClick: () => InsertTextEvent("bold"), }, { @@ -75,6 +82,7 @@ export const toolbar: ToolbarItem[] = [ icon: ItalicIcon, title: "斜体", description: Hotkey.ITALIC.readableCommand, + hotkey: Hotkey.ITALIC.toConfig(), onClick: () => InsertTextEvent("italic"), }, { @@ -82,6 +90,7 @@ export const toolbar: ToolbarItem[] = [ icon: UnderlineIcon, title: "下划线", description: Hotkey.UNDERLINE.readableCommand, + hotkey: Hotkey.UNDERLINE.toConfig(), onClick: () => InsertTextEvent("underline"), }, { @@ -89,6 +98,7 @@ export const toolbar: ToolbarItem[] = [ icon: DeleteIcon, title: "删除线", description: Hotkey.DELETE.readableCommand, + hotkey: Hotkey.DELETE.toConfig(), onClick: () => InsertTextEvent("delete"), }, { @@ -99,6 +109,7 @@ export const toolbar: ToolbarItem[] = [ icon: BlockquoteIcon, title: "引用", description: Hotkey.BLOCKQUOTE.readableCommand, + hotkey: Hotkey.BLOCKQUOTE.toConfig(), onClick: () => InsertTextEvent("blockquote"), }, { @@ -106,6 +117,7 @@ export const toolbar: ToolbarItem[] = [ icon: UlIcon, title: "无序列表", description: Hotkey.UNORDERED_LIST.readableCommand, + hotkey: Hotkey.UNORDERED_LIST.toConfig(), onClick: () => InsertTextEvent("ul"), }, { @@ -113,6 +125,7 @@ export const toolbar: ToolbarItem[] = [ icon: OlIcon, title: "有序列表", description: Hotkey.ORDERED_LIST.readableCommand, + hotkey: Hotkey.ORDERED_LIST.toConfig(), onClick: () => InsertTextEvent("ol"), }, { @@ -120,6 +133,7 @@ export const toolbar: ToolbarItem[] = [ icon: InlineCodeIcon, title: "行内代码", description: Hotkey.INLINE_CODE.readableCommand, + hotkey: Hotkey.INLINE_CODE.toConfig(), onClick: () => InsertTextEvent("inlinecode"), }, { @@ -127,6 +141,7 @@ export const toolbar: ToolbarItem[] = [ icon: CodeIcon, title: "代码块", description: Hotkey.CODE_BLOCK.readableCommand, + hotkey: Hotkey.CODE_BLOCK.toConfig(), onClick: () => InsertTextEvent("code"), }, { @@ -134,6 +149,7 @@ export const toolbar: ToolbarItem[] = [ icon: LinkIcon, title: "链接", description: Hotkey.LINK.readableCommand, + hotkey: Hotkey.LINK.toConfig(), onClick: () => InsertTextEvent("link"), }, { @@ -144,6 +160,7 @@ export const toolbar: ToolbarItem[] = [ { title: "添加链接", type: "image-link", + hotkey: Hotkey.LINK.toConfig(), onClick: () => InsertTextEvent("image-link"), }, { @@ -157,6 +174,7 @@ export const toolbar: ToolbarItem[] = [ icon: TableIcon, title: "表格", description: Hotkey.TABLE.readableCommand, + hotkey: Hotkey.TABLE.toConfig(), onClick: () => InsertTextEvent("table"), }, { @@ -185,10 +203,12 @@ export const toolbar: ToolbarItem[] = [ }, { type: "fullscreen", + hotkey: Hotkey.FULL_SCREEN.toConfig(), component: , }, { type: "save", + hotkey: Hotkey.SAVE.toConfig(), component: , }, { diff --git a/packages/mini-markdown-editor/src/config/toolbar/index.ts b/packages/mini-markdown-editor/src/config/toolbar/index.ts index b317d538eab117c9fd9a739a38827f1f4195609f..042e76e60872d17d1d36a91aaa2bc7055e719c8d 100644 --- a/packages/mini-markdown-editor/src/config/toolbar/index.ts +++ b/packages/mini-markdown-editor/src/config/toolbar/index.ts @@ -1,16 +1,24 @@ import type { ToolbarItem, ToolbarType } from "@/types/toolbar"; -import { toolbar } from "./base"; +import { defaultToolbar } from "./base"; +import { produce } from "immer"; +import { BaseClass } from "../base"; -class ToolbarConfig { +class ToolbarConfig extends BaseClass { private toolbars: ToolbarItem[]; + private readonly defaultToolbars: ToolbarItem[]; - constructor() { + constructor(initialToolbars: ToolbarItem[]) { + super({ + name: "toolbarConfig", + maxListeners: 10, + }); + this.defaultToolbars = [...initialToolbars]; this.toolbars = this.initToolbars(); } - // 初始化工具栏内容 + // 初始化默认工具栏内容 private initToolbars(): ToolbarItem[] { - return toolbar; + return [...this.defaultToolbars]; } // 获取所有工具栏项 @@ -18,31 +26,63 @@ class ToolbarConfig { return this.toolbars; } + // 获取指定类型的工具栏项 + private getToolbarByType(type: ToolbarType): ToolbarItem | undefined { + return this.toolbars.find((toolbar) => toolbar.type === type); + } + + // 添加指定工具栏项 public addToolbar(toolbarItem: ToolbarItem): void { - this.toolbars.push(toolbarItem); + // 过滤掉重复的工具栏项 + if (this.getToolbarByType(toolbarItem.type)) { + throw new Error(`Toolbar type ${toolbarItem.type} already exists`); + } + this.toolbars = produce(this.toolbars, (draft) => { + draft.push(toolbarItem); + }); } + // 移除指定工具栏项 public removeToolbar(type: ToolbarType): void { - const index = this.toolbars.findIndex((toolbar) => toolbar.type === type); - if (index !== -1) { - this.toolbars.splice(index, 1); - } + this.toolbars = produce(this.toolbars, (draft) => { + const index = draft.findIndex((toolbar) => toolbar.type === type); + if (index !== -1) { + draft.splice(index, 1); + } + }); } + // 更新工具栏 public updateToolbar(type: ToolbarType, newConfig: Partial): void { - const index = this.toolbars.findIndex((toolbar) => toolbar.type === type); - if (index !== -1) { - this.toolbars[index] = { ...this.toolbars[index], ...newConfig }; - } + this.toolbars = produce(this.toolbars, (draft) => { + const index = draft.findIndex((toolbar) => toolbar.type === type); + if (index !== -1) { + draft[index] = { ...draft[index], ...newConfig }; + } + }); } // TODO: 添加重新排序工具栏顺序(例如拖拽排序??)的方法 public reorderToolbars(newOrder: ToolbarType[]): void { const toolbarMap = new Map(this.toolbars.map((toolbar) => [toolbar.type, toolbar])); - this.toolbars = newOrder - .map((type) => toolbarMap.get(type)) - .filter((toolbar): toolbar is ToolbarItem => toolbar !== undefined); + this.toolbars = produce(this.toolbars, () => + newOrder + .map((type) => toolbarMap.get(type)) + .filter((toolbar): toolbar is ToolbarItem => toolbar !== undefined), + ); + } + + public reset(): void { + this.toolbars = this.initToolbars(); + } + + public enableToolbar(type: ToolbarType): void { + this.updateToolbar(type, { disabled: false }); + } + + public disableToolbar(type: ToolbarType): void { + this.updateToolbar(type, { disabled: true }); } } -export const toolbarConfig = new ToolbarConfig(); +export const toolbarConfig = new ToolbarConfig(defaultToolbar); diff --git a/packages/mini-markdown-editor/src/extensions/codemirror/hotkeys.ts b/packages/mini-markdown-editor/src/extensions/codemirror/hotkeys.ts index 99fce7a1d0ee52f15d74d31348b73708388c954c..25ae6f77e3566329ced1c860625728eff1837805 100644 --- a/packages/mini-markdown-editor/src/extensions/codemirror/hotkeys.ts +++ b/packages/mini-markdown-editor/src/extensions/codemirror/hotkeys.ts @@ -1,179 +1,14 @@ import { Extension } from "@codemirror/state"; import { keymap } from "@codemirror/view"; -import { Hotkey } from "@/common/hotkeys"; -import { InsertTextEvent } from "@/config/toolbar/event"; -import { ToolbarType } from "@/types/toolbar"; -import { useToolbarStore } from "@/store/toolbar"; - -// 定义默认快捷键支持 -const KEYMAP = { - // Text - [Hotkey.TITLE.FIRST.codeMirrorCommand]: { - run: () => { - Hotkey.TITLE.FIRST.handle?.(); - InsertTextEvent(Hotkey.TITLE.FIRST.description as ToolbarType); - return true; - }, - preventDefault: true, - }, - [Hotkey.TITLE.SECOND.codeMirrorCommand]: { - run: () => { - Hotkey.TITLE.SECOND.handle?.(); - InsertTextEvent(Hotkey.TITLE.SECOND.description as ToolbarType); - return true; - }, - preventDefault: true, - }, - [Hotkey.TITLE.THIRD.codeMirrorCommand]: { - run: () => { - Hotkey.TITLE.THIRD.handle?.(); - InsertTextEvent(Hotkey.TITLE.THIRD.description as ToolbarType); - return true; - }, - preventDefault: true, - }, - [Hotkey.TITLE.FOURTH.codeMirrorCommand]: { - run: () => { - Hotkey.TITLE.FOURTH.handle?.(); - InsertTextEvent(Hotkey.TITLE.FOURTH.description as ToolbarType); - return true; - }, - preventDefault: true, - }, - [Hotkey.TITLE.FIFTH.codeMirrorCommand]: { - run: () => { - Hotkey.TITLE.FIFTH.handle?.(); - InsertTextEvent(Hotkey.TITLE.FIFTH.description as ToolbarType); - return true; - }, - preventDefault: true, - }, - [Hotkey.TITLE.SIXTH.codeMirrorCommand]: { - run: () => { - Hotkey.TITLE.SIXTH.handle?.(); - InsertTextEvent(Hotkey.TITLE.SIXTH.description as ToolbarType); - return true; - }, - preventDefault: true, - }, - [Hotkey.BOLD.codeMirrorCommand]: { - run: () => { - Hotkey.BOLD.handle?.(); - InsertTextEvent(Hotkey.BOLD.description as ToolbarType); - return true; - }, - preventDefault: true, - }, - [Hotkey.ITALIC.codeMirrorCommand]: { - run: () => { - Hotkey.ITALIC.handle?.(); - InsertTextEvent(Hotkey.ITALIC.description as ToolbarType); - return true; - }, - preventDefault: true, - }, - [Hotkey.UNDERLINE.codeMirrorCommand]: { - run: () => { - Hotkey.UNDERLINE.handle?.(); - InsertTextEvent(Hotkey.UNDERLINE.description as ToolbarType); - return true; - }, - preventDefault: true, - }, - [Hotkey.DELETE.codeMirrorCommand]: { - run: () => { - Hotkey.DELETE.handle?.(); - InsertTextEvent(Hotkey.DELETE.description as ToolbarType); - return true; - }, - preventDefault: true, - }, - [Hotkey.BLOCKQUOTE.codeMirrorCommand]: { - run: () => { - Hotkey.BLOCKQUOTE.handle?.(); - InsertTextEvent(Hotkey.BLOCKQUOTE.description as ToolbarType); - return true; - }, - preventDefault: true, - }, - [Hotkey.UNORDERED_LIST.codeMirrorCommand]: { - run: () => { - Hotkey.UNORDERED_LIST.handle?.(); - InsertTextEvent(Hotkey.UNORDERED_LIST.description as ToolbarType); - return true; - }, - preventDefault: true, - }, - [Hotkey.ORDERED_LIST.codeMirrorCommand]: { - run: () => { - Hotkey.ORDERED_LIST.handle?.(); - InsertTextEvent(Hotkey.ORDERED_LIST.description as ToolbarType); - return true; - }, - preventDefault: true, - }, - [Hotkey.INLINE_CODE.codeMirrorCommand]: { - run: () => { - Hotkey.INLINE_CODE.handle?.(); - InsertTextEvent(Hotkey.INLINE_CODE.description as ToolbarType); - return true; - }, - preventDefault: true, - }, - [Hotkey.CODE_BLOCK.codeMirrorCommand]: { - run: () => { - Hotkey.CODE_BLOCK.handle?.(); - InsertTextEvent(Hotkey.CODE_BLOCK.description as ToolbarType); - return true; - }, - preventDefault: true, - }, - [Hotkey.LINK.codeMirrorCommand]: { - run: () => { - Hotkey.LINK.handle?.(); - InsertTextEvent(Hotkey.LINK.description as ToolbarType); - return true; - }, - preventDefault: true, - }, - [Hotkey.TABLE.codeMirrorCommand]: { - run: () => { - Hotkey.TABLE.handle?.(); - InsertTextEvent(Hotkey.TABLE.description as ToolbarType); - return true; - }, - preventDefault: true, - }, - - // Actions - [Hotkey.FULL_SCREEN.codeMirrorCommand]: { - run: () => { - Hotkey.FULL_SCREEN.handle?.(); - const currentState = useToolbarStore.getState(); - useToolbarStore.setState({ - isFullScreen: !currentState.isFullScreen, - }); - return true; - }, - preventDefault: true, - }, - [Hotkey.SAVE.codeMirrorCommand]: { - run: () => { - Hotkey.SAVE.handle?.(); - // TODO: 添加保存事件 - return true; - }, - preventDefault: true, - }, -}; +import { handleHotkeys } from "@/utils/handle-hotkeys"; +import { toolbarConfig } from "@/config/toolbar"; export function createHotkeysExtension(): Extension { + const KEY_MAP = handleHotkeys(toolbarConfig.getAllToolbars()); return keymap.of([ - ...Object.entries(KEYMAP).map(([key, value]) => ({ + ...Object.entries(KEY_MAP).map(([key, value]) => ({ key, ...value, })), - - //! 留空 暴露外部其他内部快捷键 ]); } diff --git a/packages/mini-markdown-editor/src/index.tsx b/packages/mini-markdown-editor/src/index.tsx index 0dccd3db0bdd719784ab4a561e9ae0f52e4fd687..ccf7db29dcc7534782ee7a3492f5f8daebd94cbf 100644 --- a/packages/mini-markdown-editor/src/index.tsx +++ b/packages/mini-markdown-editor/src/index.tsx @@ -5,6 +5,9 @@ import Editor from "./EditorWrapper"; // 导出组件 export { Editor }; +// 导出配置 +export { toolbarConfig as ToolbarManager } from "@/config/toolbar"; + // 导出 ts 类型 export * from "@/types/code-mirror"; export * from "@/types/global-config"; diff --git a/packages/mini-markdown-editor/src/types/global-config.ts b/packages/mini-markdown-editor/src/types/global-config.ts index 6b5a582902b60b07ffe79fc2ec4cfea3a1a0f5ad..1e7e00a0e8c3b760cbef4c518a82ee79b1688389 100644 --- a/packages/mini-markdown-editor/src/types/global-config.ts +++ b/packages/mini-markdown-editor/src/types/global-config.ts @@ -1,7 +1,7 @@ -import { EditorView, ViewUpdate } from "@uiw/react-codemirror"; +import { EditorView, ReactCodeMirrorProps, ViewUpdate } from "@uiw/react-codemirror"; import { ToolbarType } from "./toolbar"; -export interface GlobalConfig { +export interface GlobalConfig extends ReactCodeMirrorProps { /** * 编辑器内容 * @type {string} @@ -66,3 +66,20 @@ export interface GlobalConfig { } export type Callback = (param: { url: string; alt?: string }) => void; + +export type GlobalContextConfig = Pick< + GlobalConfig, + | "theme" + | "initialValue" + | "toolbar" + | "status" + | "local" + | "lineNumbers" + | "enableShortcuts" + | "onUpload" + | "onDragUpload" + | "onPatseUpload" + | "onSave" + | "onChange" +>; +export type EditorConfig = Omit; diff --git a/packages/mini-markdown-editor/src/types/toolbar.ts b/packages/mini-markdown-editor/src/types/toolbar.ts index 01823b6f7839085f78c37ca0978323494e397f0a..a9172541af8f7720d2a6dedbeaa2c24af883f3c8 100644 --- a/packages/mini-markdown-editor/src/types/toolbar.ts +++ b/packages/mini-markdown-editor/src/types/toolbar.ts @@ -1,59 +1,124 @@ -export interface ToolbarItem { +import { Hotkey } from "@/common/hotkeys"; + +// 基础工具栏类型 +export enum BaseToolbarType { + HEADING = "heading", + HEADING_1 = "heading-1", + HEADING_2 = "heading-2", + HEADING_3 = "heading-3", + HEADING_4 = "heading-4", + HEADING_5 = "heading-5", + HEADING_6 = "heading-6", + BOLD = "bold", + ITALIC = "italic", + UNDERLINE = "underline", + DELETE = "delete", + LINE = "line", + BLOCKQUOTE = "blockquote", + UL = "ul", + OL = "ol", + INLINECODE = "inlinecode", + CODE = "code", + LINK = "link", + IMAGE = "image", + IMAGE_LINK = "image-link", + IMAGE_UPLOAD = "image-upload", + TABLE = "table", + UNDO = "undo", + REDO = "redo", + FULLSCREEN = "fullscreen", + WRITE = "write", + PREVIEW = "preview", + CONTENTS = "contents", + HELP = "help", + OUTPUT = "output", + EMOJI = "emoji", + SAVE = "save", +} + +// 允许用户扩展的工具栏类型 +export type ExtendedToolbarType = string; + +// 合并基础类型和扩展类型 +export type ToolbarType = BaseToolbarType | ExtendedToolbarType; + +// 基础工具栏项接口 +export interface BaseToolbarItem { type: ToolbarType; icon?: string; title?: string; description?: string; - list?: ToolbarItemListItem[]; disabled?: boolean; visible?: boolean; onClick?: () => void; component?: React.ReactNode; } +// 工具栏列表项接口 export interface ToolbarItemListItem { title: string; type: string; + hotkey?: { + command: string; + description: string; + handle?: () => void; + }; onClick?: (...args: any[]) => void | (() => void); } +// 完整的工具栏项接口 +export interface ToolbarItem extends BaseToolbarItem { + list?: ToolbarItemListItem[]; + hotkey?: { + command: string; + description: string; + handle?: () => void; + }; +} + +// 工具栏上下文值接口 export interface ToolbarContextValues { toolbars: ToolbarItem[]; addToolbar?: (toolbarItem: ToolbarItem) => void; removeToolbar?: (type: ToolbarType) => void; - updateToolbar?: (type: ToolbarType, newConfig: ToolbarItem) => void; + updateToolbar?: (type: ToolbarType, newConfig: Partial) => void; reorderToolbars?: (newOrder: ToolbarType[]) => void; } -export type ToolbarType = - | "heading" - | "heading-1" - | "heading-2" - | "heading-3" - | "heading-4" - | "heading-5" - | "heading-6" - | "bold" - | "italic" - | "underline" - | "delete" - | "line" - | "blockquote" - | "ul" - | "ol" - | "inlinecode" - | "code" - | "link" - | "image" - | "image-link" - | "image-upload" - | "table" - | "undo" - | "redo" - | "fullscreen" - | "write" - | "preview" - | "contents" - | "help" - | "output" - | "emoji" - | "save"; +// 类型保护函数 +export const isBaseToolbarType = (type: ToolbarType): type is BaseToolbarType => { + return Object.values(BaseToolbarType).includes(type as BaseToolbarType); +}; + +// 工具栏配置验证 +export interface ToolbarValidationResult { + isValid: boolean; + errors: string[]; +} + +// 工具栏配置验证函数 +// 更新验证函数以包含快捷键验证 +export const validateToolbarItem = (item: ToolbarItem): ToolbarValidationResult => { + const errors: string[] = []; + + if (!item.type) { + errors.push("Toolbar item must have a type"); + } + + if (item.list && !Array.isArray(item.list)) { + errors.push("Toolbar item list must be an array"); + } + + if (item.hotkey) { + try { + new Hotkey(item.hotkey.command, item.hotkey.description || "", item.hotkey.handle); + } catch (e) { + errors.push(`Invalid hotkey configuration: ${(e as Error).message}`); + } + } + + return { + isValid: errors.length === 0, + errors, + }; +}; diff --git a/packages/mini-markdown-editor/src/utils/filter-context-props.ts b/packages/mini-markdown-editor/src/utils/filter-context-props.ts new file mode 100644 index 0000000000000000000000000000000000000000..f59f98fcdf67844dbd9d968c4b383c489f727761 --- /dev/null +++ b/packages/mini-markdown-editor/src/utils/filter-context-props.ts @@ -0,0 +1,25 @@ +import { defaultGlobalConfig } from "@/config/global"; +import { GlobalConfig, GlobalContextConfig } from "@/types/global-config"; + +type GlobalContextKeys = keyof GlobalContextConfig | "isSyncScroll"; + +export const filterContextProps = (props: GlobalConfig): Omit => { + // 快速查找 + const contextKeys = new Set( + Object.keys(defaultGlobalConfig) as Array, + ); + + contextKeys.add("isSyncScroll" as GlobalContextKeys); + + const { className, style, ...restProps } = props; + + const filteredProps = Object.fromEntries( + Object.entries(restProps).filter(([key]) => !contextKeys.has(key as GlobalContextKeys)), + ); + + return { + ...filteredProps, + className, + style, + } as Omit; +}; diff --git a/packages/mini-markdown-editor/src/utils/handle-hotkeys.ts b/packages/mini-markdown-editor/src/utils/handle-hotkeys.ts new file mode 100644 index 0000000000000000000000000000000000000000..280cfea7d6681d035eb058f024fe38c622e226ce --- /dev/null +++ b/packages/mini-markdown-editor/src/utils/handle-hotkeys.ts @@ -0,0 +1,108 @@ +import { ToolbarItem, ToolbarType } from "@/types/toolbar"; +import { BaseToolbarType } from "@/types/toolbar"; +import { useToolbarStore } from "@/store/toolbar"; +import { InsertTextEvent } from "@/config/toolbar/event"; + +// 定义处理器类型 +interface HotkeyHandler { + run: () => boolean; + preventDefault: boolean; +} + +// 定义热键类型 +interface HotkeyType { + command: string; + description: string; + handle?: () => void; +} + +// 创建文本插入类型的处理器 +export const createInsertTextHandler = (hotkey: HotkeyType): HotkeyHandler => ({ + run: () => { + InsertTextEvent(hotkey.description as ToolbarType); + hotkey.handle?.(); + return true; + }, + preventDefault: true, +}); + +// 创建全屏切换处理器 +export const createFullScreenHandler = (hotkey: HotkeyType): HotkeyHandler => ({ + run: () => { + const currentState = useToolbarStore.getState(); + useToolbarStore.setState({ + isFullScreen: !currentState.isFullScreen, + }); + hotkey.handle?.(); + return true; + }, + preventDefault: true, +}); + +// 创建保存处理器 +export const createSaveHandler = (hotkey: HotkeyType): HotkeyHandler => ({ + run: () => { + hotkey.handle?.(); + // TODO: 添加保存事件 + return true; + }, + preventDefault: true, +}); + +// 创建自定义处理器 +export const createCustomHandler = (hotkey: HotkeyType): HotkeyHandler => ({ + run: () => { + hotkey.handle?.(); + return true; + }, + preventDefault: true, +}); + +// 判断是否为基础工具栏类型(判断是否为新添加的工具) +const isBaseToolbarType = (type: string): type is BaseToolbarType => { + return Object.values(BaseToolbarType).includes(type as BaseToolbarType); +}; + +// 创建对应类型的处理器 +const createHandler = (item: ToolbarItem): HotkeyHandler | null => { + if (!item.hotkey) return null; + + switch (item.type) { + case BaseToolbarType.FULLSCREEN: + return createFullScreenHandler(item.hotkey); + case BaseToolbarType.SAVE: + return createSaveHandler(item.hotkey); + default: + if (isBaseToolbarType(item.type)) { + return createInsertTextHandler(item.hotkey); + } + // 对于自定义类型,使用createCustomHandler + return createCustomHandler(item.hotkey); + } +}; + +// 处理单个工具栏项 +const processToolbarItem = (result: Record, item: ToolbarItem): void => { + const handler = createHandler(item); + if (item.hotkey && handler) { + result[item.hotkey.command] = handler; + } + + // 处理子列表 + if (item.list?.length) { + item.list.forEach((listItem) => { + if (listItem.hotkey) { + const listItemHandler = createHandler(listItem); + if (listItemHandler) { + result[listItem.hotkey.command] = listItemHandler; + } + } + }); + } +}; + +export const handleHotkeys = (toolbar: ToolbarItem[]): Record => { + const result: Record = {}; + toolbar.forEach((item) => processToolbarItem(result, item)); + return result; +};