diff --git a/package.json b/package.json
index d47ee0d52245e1bd9a8a9cee99d71420ad770f36..e1d189d300684c7c45b3a9b457472b3c2961f80b 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"build:ast:play": "pnpm -F @mini-markdown/ast-parser build:play",
"dev:editor": "pnpm -F @mini-markdown/editor dev",
"build:editor": "pnpm -F @mini-markdown/editor build",
+ "dev:play": "pnpm -F @mini-markdown/play dev",
"prepare": "husky",
"lint": "node ./scripts/zx-lint.js",
"prettier": "prettier --write ."
diff --git a/packages/mini-markdown-editor/src/App.tsx b/packages/mini-markdown-editor/src/App.tsx
index 6548f265f5b7d81f7e5da0f155f703cade24dd0a..e9684add0bac8caa3481847f7d8b552d78cfb9ed 100644
--- a/packages/mini-markdown-editor/src/App.tsx
+++ b/packages/mini-markdown-editor/src/App.tsx
@@ -6,6 +6,7 @@ import { Button, message } from "antd";
// 可根据需要引入不同的主题
import "highlight.js/styles/atom-one-dark.css";
import { ViewUpdate } from "./types/code-mirror";
+import { EditorView } from "@uiw/react-codemirror";
const AppWrapper = styled.div`
width: 100%;
@@ -43,6 +44,24 @@ const App: FC = () => {
console.log(val, view);
};
+ const handleSave = (content: string, view: EditorView) => {
+ console.log(content, view);
+ };
+
+ const handlePatseUpload = async (file: File, callback: Callback) => {
+ await new Promise((resolve) => {
+ setTimeout(() => {
+ console.log("settimeout 上传成功", file);
+ resolve({});
+ }, 1500);
+ });
+ callback({
+ url: "https://www.baidu.com/img/flexible/logo/pc/result@2.png",
+ alt: "123",
+ });
+ message.success("上传成功");
+ };
+
return (
@@ -53,6 +72,9 @@ const App: FC = () => {
lineNumbers={true}
theme={theme as "light" | "dark"}
onChange={handleChange}
+ onSave={handleSave}
+ onDragUpload={handlePatseUpload}
+ onPatseUpload={handlePatseUpload}
/>
);
diff --git a/packages/mini-markdown-editor/src/components/Editor/index.tsx b/packages/mini-markdown-editor/src/components/Editor/index.tsx
index 3e4a41ef1b12110b94baccf91c97b92c18591a75..741f4dad1c30344a869e6c86e1d7308a37214ea9 100644
--- a/packages/mini-markdown-editor/src/components/Editor/index.tsx
+++ b/packages/mini-markdown-editor/src/components/Editor/index.tsx
@@ -7,6 +7,7 @@ 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";
const ScrollWrapper = styled.div<{
$lineNumbers?: boolean;
@@ -82,7 +83,8 @@ const Editor: FC<{ isSyncScroll: boolean }> = ({ isSyncScroll }) => {
setEditorViewInstance(view);
};
- const { onChange } = useContext(ConfigContext);
+ const { theme, lineNumbers, enableShortcuts, onChange, onDragUpload, onPatseUpload } =
+ useContext(ConfigContext);
const handleChange = (val: string, editView: ViewUpdate) => {
// 更新store
@@ -110,14 +112,28 @@ const Editor: FC<{ isSyncScroll: boolean }> = ({ isSyncScroll }) => {
setScrollWrapper("editor");
};
+ // 拖拽上传
+ const handleDragUpload = (file: File, Callback: Callback) => {
+ onDragUpload?.(file, Callback);
+ };
+ // 粘贴上传
+ const handlePatseUpload = (file: File, Callback: Callback) => {
+ onPatseUpload?.(file, Callback);
+ };
+
// 创建编辑器扩展
const extensions = useMemo(
- () => createEditorExtensions({ scrollWrapper, eventExt }),
+ () =>
+ createEditorExtensions({
+ enableShortcuts,
+ scrollWrapper,
+ eventExt,
+ onDragUpload: handleDragUpload,
+ onPasteUpload: handlePatseUpload,
+ }),
[scrollWrapper],
);
- const { theme, lineNumbers } = useContext(ConfigContext);
-
return (
{
+ const { content, editorView } = useEditorContentStore();
+ const saveContent = useSaveContent();
+ const { onSave } = useContext(ConfigContext);
+
+ const handleSave = () => {
+ if (content) {
+ saveContent(content);
+ // onSave回调
+ if (onSave) {
+ onSave(content, editorView!);
+ }
+ }
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default Save;
diff --git a/packages/mini-markdown-editor/src/config/global.ts b/packages/mini-markdown-editor/src/config/global.ts
index 40a8264f0b5802c59e4dc37dbdbd20c746474587..9b90b351804b52a93d46443c8347f49552bd36ef 100644
--- a/packages/mini-markdown-editor/src/config/global.ts
+++ b/packages/mini-markdown-editor/src/config/global.ts
@@ -6,4 +6,5 @@ export const defaultGlobalConfig: GlobalConfig = {
theme: "light", // 主题
local: true, // 是否开启本地存储
lineNumbers: false, // 是否显示行号
+ enableShortcuts: true, // 是否开启快捷键
};
diff --git a/packages/mini-markdown-editor/src/config/toolbar/base.tsx b/packages/mini-markdown-editor/src/config/toolbar/base.tsx
index 5c2e3ec4e0ff4d61fe5a9ddbacc03cba0eb0a6eb..9be28135b0f91a0853cccd9c619c10432b4ab490 100644
--- a/packages/mini-markdown-editor/src/config/toolbar/base.tsx
+++ b/packages/mini-markdown-editor/src/config/toolbar/base.tsx
@@ -21,6 +21,7 @@ import Upload from "@/components/Toolbar/Upload";
import FullScreen from "@/components/Toolbar/FullScreen";
import { Contents, Read, Write, Help, Output } from "@/components/Toolbar/ShowLayout";
import Emoji from "@/components/Toolbar/Emoji";
+import Save from "@/components/Toolbar/Save";
// 快捷键描述
import { Hotkey } from "@/common/hotkeys";
@@ -186,6 +187,10 @@ export const toolbar: ToolbarItem[] = [
type: "fullscreen",
component: ,
},
+ {
+ type: "save",
+ component: ,
+ },
{
type: "write",
component: ,
diff --git a/packages/mini-markdown-editor/src/config/toolbar/event.ts b/packages/mini-markdown-editor/src/config/toolbar/event.ts
index dafb4ce8758786dcf048fb8a3699da0c4216aea8..c6c9694d79c89ed344d38a6c2b31528b48dcc6da 100644
--- a/packages/mini-markdown-editor/src/config/toolbar/event.ts
+++ b/packages/mini-markdown-editor/src/config/toolbar/event.ts
@@ -10,7 +10,7 @@ export const InsertTextEvent = (type: ToolbarType) => {
// 上传图片
export const InsertImageEvent = (url: string, alt: string) => {
- const content = ``;
+ const content = `\n`;
const selection = {
anchor: 2,
head: 2 + alt.length,
diff --git a/packages/mini-markdown-editor/src/extensions/codemirror/event.d.ts b/packages/mini-markdown-editor/src/extensions/codemirror/event.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f30d80b7410d3fc14621197c0b7b57b2778de0e1
--- /dev/null
+++ b/packages/mini-markdown-editor/src/extensions/codemirror/event.d.ts
@@ -0,0 +1,13 @@
+import { Callback } from "@/types/global-config";
+import { EditorView, ViewPlugin } from "@uiw/react-codemirror";
+
+export interface EventOptions {
+ scrollWrapper: string;
+ eventExt?: ViewPlugin<{
+ dom?: HTMLElement;
+ view: EditorView;
+ destroy(): void;
+ }>;
+ onDragUpload?: (file: File, callback: Callback) => void;
+ onPasteUpload?: (file: File, callback: Callback) => void;
+}
diff --git a/packages/mini-markdown-editor/src/extensions/codemirror/event.ts b/packages/mini-markdown-editor/src/extensions/codemirror/event.ts
index 4d13bead206a025838ca227355953b503e1ebb94..3d06db00fa4929b1e713b4c7b7e468000cc26f53 100644
--- a/packages/mini-markdown-editor/src/extensions/codemirror/event.ts
+++ b/packages/mini-markdown-editor/src/extensions/codemirror/event.ts
@@ -1,38 +1,31 @@
import { EditorView, ViewPlugin } from "@uiw/react-codemirror";
import { nanoid } from "nanoid";
-
-interface EventOptions {
- scrollWrapper: string;
- eventExt?: ViewPlugin<{
- dom?: HTMLElement;
- view: EditorView;
- destroy(): void;
- }>;
-}
+import type { EventOptions } from "./event.d";
+import { Callback } from "@/types/global-config";
// 实例属性
// 提供销毁方法,同时有效避免全局 Url 的变量污染
class ImageHandler {
private currentObjectURL: string | null = null;
- handleImageFile(file: File, view: EditorView) {
- // TODO: 限制图片大小(可以外露)
- // if (file.size > 5 * 1024 * 1024) {
- // console.warn("图片大小不能超过5MB!");
- // return;
- // }
-
+ handleImageFile(
+ file: File,
+ view: EditorView,
+ uploadCallback?: (file: File, callback: Callback) => void,
+ ) {
if (this.currentObjectURL) {
URL.revokeObjectURL(this.currentObjectURL);
}
- const imageUrl = URL.createObjectURL(file);
- this.currentObjectURL = imageUrl;
- //* 生成随机八位 alt
+ // 创建临时URL
+ const temporaryUrl = URL.createObjectURL(file);
+ this.currentObjectURL = temporaryUrl;
const imageAlt = nanoid(8);
+ // 插入临时图片用作预览
const selection = view.state.selection.main;
- const content = ``;
+ const content = `\n`;
+ const insertPos = selection.from;
view.dispatch({
changes: {
@@ -40,7 +33,45 @@ class ImageHandler {
to: selection.to,
insert: content,
},
+ selection: {
+ anchor: selection.from + content.length,
+ head: selection.from + content.length,
+ },
});
+
+ // 处理上传后的信息
+ //! 此回调函数内部提供返回的url,作为新图片的地址
+ //! 可选传递alt参数,作为图片的描述
+ const handleCallback: Callback = (param: { url: string; alt?: string }) => {
+ try {
+ const newContent = `\n`;
+
+ // 计算需要替换的范围
+ const doc = view.state.doc;
+ const searchStr = content;
+ const searchPos = doc.slice(insertPos, insertPos + searchStr.length).toString();
+ if (searchPos === searchStr) {
+ view.dispatch({
+ changes: {
+ from: insertPos,
+ to: insertPos + searchStr.length,
+ insert: newContent,
+ },
+ });
+ }
+
+ // 清理临时URL
+ URL.revokeObjectURL(temporaryUrl);
+ this.currentObjectURL = null;
+ } catch (err) {
+ console.error("Failed to replace image URL:", err);
+ }
+ };
+
+ // 执行上传回调
+ if (uploadCallback) {
+ uploadCallback(file, handleCallback);
+ }
}
destroy() {
@@ -51,7 +82,7 @@ class ImageHandler {
}
// 创建拖拽插件
-const createDropPhotoExtension = () => {
+const createDropPhotoExtension = (onDragUpload?: EventOptions["onDragUpload"]) => {
return ViewPlugin.fromClass(
class {
private handler: ImageHandler;
@@ -69,7 +100,7 @@ const createDropPhotoExtension = () => {
const files = e.dataTransfer?.files;
if (!files?.[0]?.type.startsWith("image/")) return;
- this.handler.handleImageFile(files[0], view);
+ this.handler.handleImageFile(files[0], view, onDragUpload);
};
this.view.dom.addEventListener("dragover", this.onDragOver);
@@ -87,7 +118,7 @@ const createDropPhotoExtension = () => {
};
// 创建粘贴插件
-const createPastePhotoExtension = () => {
+const createPastePhotoExtension = (onPasteUpload?: EventOptions["onPasteUpload"]) => {
return ViewPlugin.fromClass(
class {
private handler: ImageHandler;
@@ -107,8 +138,7 @@ const createPastePhotoExtension = () => {
e.preventDefault();
const file = item.getAsFile();
if (file) {
- this.handler.handleImageFile(file, view);
- //! 只处理第一个图片
+ this.handler.handleImageFile(file, view, onPasteUpload);
break;
}
}
@@ -132,7 +162,9 @@ export const createEventExtension = (eventOptions: EventOptions): any => {
return [];
}
- return [eventOptions.eventExt, createDropPhotoExtension(), createPastePhotoExtension()].filter(
- Boolean,
- );
+ return [
+ eventOptions.eventExt,
+ createDropPhotoExtension(eventOptions.onDragUpload),
+ createPastePhotoExtension(eventOptions.onPasteUpload),
+ ].filter(Boolean);
};
diff --git a/packages/mini-markdown-editor/src/extensions/codemirror/index.ts b/packages/mini-markdown-editor/src/extensions/codemirror/index.ts
index 1247396e9df633681c6ab7bed2c917d4b18894c8..f092db47f83c94d12d94a92e7b66f801e8fd9750 100644
--- a/packages/mini-markdown-editor/src/extensions/codemirror/index.ts
+++ b/packages/mini-markdown-editor/src/extensions/codemirror/index.ts
@@ -1,28 +1,36 @@
import { Extension } from "@codemirror/state";
-import { EditorView, ViewPlugin } from "@uiw/react-codemirror";
+import { EditorView } from "@uiw/react-codemirror";
import { history } from "@codemirror/commands";
import { createMarkdownExtension } from "./markdown";
import { createHotkeysExtension } from "./hotkeys";
import { createEventExtension } from "./event";
+import type { EventOptions } from "./event.d";
-interface ExtensionOptions {
- scrollWrapper?: string;
- eventExt?: ViewPlugin<{
- dom?: HTMLElement;
- view: EditorView;
- destroy(): void;
- }>;
+interface ExtensionOptions extends EventOptions {
+ enableShortcuts?: boolean;
}
-export const createEditorExtensions = (options: ExtensionOptions = {}): Extension[] => {
- const { scrollWrapper = "editor", eventExt } = options;
+export const createEditorExtensions = (options: ExtensionOptions): Extension[] => {
+ const {
+ scrollWrapper = "editor",
+ eventExt,
+ enableShortcuts,
+ onDragUpload,
+ onPasteUpload,
+ } = options;
- return [
- createHotkeysExtension(),
+ // 创建基础扩展数组
+ const extensions: Extension[] = [
createMarkdownExtension(),
- createEventExtension({ scrollWrapper, eventExt }),
+ createEventExtension({ scrollWrapper, eventExt, onDragUpload, onPasteUpload }),
history(),
- //? 自动换行,可以考虑添加配置项
EditorView.lineWrapping,
];
+
+ // 是否开启快捷键支持
+ if (enableShortcuts) {
+ extensions.push(createHotkeysExtension());
+ }
+
+ return extensions;
};
diff --git a/packages/mini-markdown-editor/src/hooks/use-persist-editor-content.ts b/packages/mini-markdown-editor/src/hooks/use-persist-editor-content.ts
index 280b942ab1ad736738227df09e02df407492c94f..d44a836e8d204f1c6e43bc1ab7921a17956bb2ea 100644
--- a/packages/mini-markdown-editor/src/hooks/use-persist-editor-content.ts
+++ b/packages/mini-markdown-editor/src/hooks/use-persist-editor-content.ts
@@ -20,7 +20,6 @@ export const usePersistEditorContent = () => {
// 获取内容
const getContent = (): string => {
- if (!local) return "";
return localStorage.getItem(EDITOR_CONTENT_KEY) ?? "";
};
diff --git a/packages/mini-markdown-editor/src/hooks/use-save-content.ts b/packages/mini-markdown-editor/src/hooks/use-save-content.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d47fab8e5f338ee3bdbad34b7079015200ebc7ec
--- /dev/null
+++ b/packages/mini-markdown-editor/src/hooks/use-save-content.ts
@@ -0,0 +1,15 @@
+import { EDITOR_CONTENT_KEY } from "@/common";
+import { safeLocalStorage } from "@/utils/storage";
+import { useDebounceFn } from "ahooks";
+
+export const useSaveContent = () => {
+ const localStorage = safeLocalStorage();
+ const { run: saveContent } = useDebounceFn(
+ (content: string) => {
+ localStorage.setItem(EDITOR_CONTENT_KEY, content);
+ },
+ { wait: 300 },
+ );
+
+ return saveContent;
+};
diff --git a/packages/mini-markdown-editor/src/types/global-config.ts b/packages/mini-markdown-editor/src/types/global-config.ts
index d6ede9510e977ceb208d5b7ba48d952927dd4873..d6e626dd9db0271784c58c403e8a492938913e02 100644
--- a/packages/mini-markdown-editor/src/types/global-config.ts
+++ b/packages/mini-markdown-editor/src/types/global-config.ts
@@ -1,4 +1,4 @@
-import { ViewUpdate } from "@uiw/react-codemirror";
+import { EditorView, ViewUpdate } from "@uiw/react-codemirror";
import { ToolbarType } from "./toolbar";
export interface GlobalConfig {
@@ -28,6 +28,11 @@ export interface GlobalConfig {
* @type {boolean}
*/
lineNumbers?: boolean;
+ /**
+ * 是否开启快捷键支持
+ * @type {boolean}
+ */
+ enableShortcuts?: boolean;
/**
* 改变编辑器内容时触发
* @type {(value: string, editorView: ViewUpdate) => void}
@@ -43,11 +48,16 @@ export interface GlobalConfig {
* @type {(file: File, callback: Callback) => void}
*/
onDragUpload?: (file: File, callback: Callback) => void;
+ /**
+ * 粘贴上传图片时触发
+ * @type {(file: File, callback: Callback) => void}
+ */
+ onPatseUpload?: (file: File, callback: Callback) => void;
/**
* 保存触发
- * @type {(value: string, editorView: ViewUpdate) => void}
+ * @type {(value: string, editorView: EditorView) => void}
*/
- onSave?: (value: string, editorView: ViewUpdate) => void;
+ onSave?: (value: string, editorView: EditorView) => void;
}
export type Callback = (param: { url: string; alt?: string }) => void;
diff --git a/packages/mini-markdown-editor/src/types/toolbar.ts b/packages/mini-markdown-editor/src/types/toolbar.ts
index 7f2ca1e67b098b6079c33eb3cbb965cde1c7ea09..01823b6f7839085f78c37ca0978323494e397f0a 100644
--- a/packages/mini-markdown-editor/src/types/toolbar.ts
+++ b/packages/mini-markdown-editor/src/types/toolbar.ts
@@ -55,4 +55,5 @@ export type ToolbarType =
| "contents"
| "help"
| "output"
- | "emoji";
+ | "emoji"
+ | "save";
diff --git a/packages/mini-markdown-play/src/pages/ui-test.tsx b/packages/mini-markdown-play/src/pages/ui-test.tsx
index 7a307fab3526b0640087148ad454c818a925f01c..661c76fde3f2943bbd273ae6e67daa1573099d68 100644
--- a/packages/mini-markdown-play/src/pages/ui-test.tsx
+++ b/packages/mini-markdown-play/src/pages/ui-test.tsx
@@ -39,6 +39,20 @@ const App: FC = () => {
console.log(val, view);
};
+ const handlePatseUpload = async (file: File, callback: Callback) => {
+ await new Promise((resolve) => {
+ setTimeout(() => {
+ console.log("settimeout 上传成功", file);
+ resolve({});
+ }, 1500);
+ });
+ callback({
+ url: "https://www.baidu.com/img/flexible/logo/pc/result@2.png",
+ alt: "123",
+ });
+ message.success("上传成功");
+ };
+
return (
@@ -48,6 +62,9 @@ const App: FC = () => {
local={true}
theme={theme}
onChange={handleChange}
+ enableShortcuts={true}
+ onDragUpload={handlePatseUpload}
+ onPatseUpload={handlePatseUpload}
/>
);