Vanessa 5 tahun lalu
induk
melakukan
e2fc34d18f

+ 2 - 7
demo/index.js

@@ -50,15 +50,14 @@ if (window.innerWidth < 768) {
 }
 
 window.vditor = new Vditor('vditor', {
-  // _lutePath: `http://192.168.0.107:9090/lute.min.js?${new Date().getTime()}`,
-  _lutePath: 'src/js/lute/lute.min.js',
+  _lutePath: `http://192.168.0.107:9090/lute.min.js?${new Date().getTime()}`,
+  // _lutePath: 'src/js/lute/lute.min.js',
   toolbar,
   mode: 'wysiwyg',
   height: window.innerHeight + 100,
   outline: true,
   debugger: true,
   typewriterMode: true,
-  theme: 'dark',
   placeholder: 'Hello, Vditor!',
   preview: {
     markdown: {
@@ -66,10 +65,6 @@ window.vditor = new Vditor('vditor', {
       mark: true,
       footnotes: true,
     },
-    theme: {
-      current: 'dark',
-      path: 'http://localhost:9000/src/css/content-theme',
-    },
     math: {
       engine: 'MathJax',
     },

+ 10 - 0
src/assets/scss/_reset.scss

@@ -495,5 +495,15 @@
 
   &-toc {
     margin-bottom: 16px;
+    user-select: text;
+    color: $blurColor;
+
+    ul {
+      list-style: none !important;
+    }
+
+    span {
+      cursor: pointer;
+    }
   }
 }

+ 2 - 1
src/ts/ir/input.ts

@@ -1,5 +1,5 @@
 import {Constants} from "../constants";
-import {isHeadingMD, isHrMD, renderToc} from "../util/fixBrowserBehavior";
+import {isHeadingMD, isHrMD} from "../util/fixBrowserBehavior";
 import {
     getTopList,
     hasClosestBlock, hasClosestByAttribute,
@@ -9,6 +9,7 @@ import {hasClosestByTag} from "../util/hasClosestByHeadings";
 import {log} from "../util/log";
 import {processCodeRender} from "../util/processCode";
 import {getSelectPosition, setRangeByWbr} from "../util/selection";
+import {renderToc} from "../util/toc";
 import {processAfterRender} from "./process";
 
 export const input = (vditor: IVditor, range: Range, ignoreSpace = false, event?: InputEvent) => {

+ 6 - 8
src/ts/ir/processKeydown.ts

@@ -23,6 +23,7 @@ import {matchHotKey} from "../util/hotKey";
 import {getEditorRange, getSelectPosition, setSelectionFocus} from "../util/selection";
 import {expandMarker} from "./expandMarker";
 import {processAfterRender, processHeading} from "./process";
+import {keydownToc} from "../util/toc";
 
 export const processKeydown = (vditor: IVditor, event: KeyboardEvent) => {
     vditor.ir.composingLock = event.isComposing;
@@ -49,7 +50,7 @@ export const processKeydown = (vditor: IVditor, event: KeyboardEvent) => {
 
     // 仅处理以下快捷键操作
     if (event.key !== "Enter" && event.key !== "Tab" && event.key !== "Backspace" && event.key.indexOf("Arrow") === -1
-        && !isCtrl(event) && event.key !== "Escape") {
+        && !isCtrl(event) && event.key !== "Escape" && event.key !== "Delete") {
         return false;
     }
 
@@ -82,13 +83,6 @@ export const processKeydown = (vditor: IVditor, event: KeyboardEvent) => {
     if (fixBlockquote(vditor, range, event, pElement)) {
         return true;
     }
-    // toc 前无元素,插入空块
-    if (pElement && pElement.previousElementSibling &&
-        pElement.previousElementSibling.classList.contains("vditor-toc")) {
-        if (insertBeforeBlock(vditor, event, range, pElement, pElement.previousElementSibling as HTMLElement)) {
-            return true;
-        }
-    }
     // 代码块
     const preRenderElement = hasClosestByClassName(startContainer, "vditor-ir__marker--pre");
     if (preRenderElement && preRenderElement.tagName === "PRE") {
@@ -235,5 +229,9 @@ export const processKeydown = (vditor: IVditor, event: KeyboardEvent) => {
     }
     fixCursorDownInlineMath(range, event.key);
 
+    if (blockElement && keydownToc(blockElement, vditor, event, range)) {
+        event.preventDefault();
+        return true;
+    }
     return false;
 };

+ 50 - 31
src/ts/markdown/mathRender.ts

@@ -42,6 +42,10 @@ export const mathRender = (element: HTMLElement, options?: { cdn?: string, math?
         addStyle(`${options.cdn}/dist/js/katex/katex.min.css`, "vditorKatexStyle");
         addScript(`${options.cdn}/dist/js/katex/katex.min.js`, "vditorKatexScript").then(() => {
             mathElements.forEach((mathElement) => {
+                if (mathElement.parentElement.classList.contains("vditor-wysiwyg__pre") ||
+                    mathElement.parentElement.classList.contains("vditor-ir__marker--pre")) {
+                    return;
+                }
                 if (mathElement.getAttribute("data-math")) {
                     return;
                 }
@@ -68,6 +72,18 @@ export const mathRender = (element: HTMLElement, options?: { cdn?: string, math?
             });
         });
     } else if (options.math.engine === "MathJax") {
+        const chainAsync = (fns: any) => {
+            if (fns.length === 0) {
+                return;
+            }
+            let curr = 0;
+            const last = fns[fns.length - 1];
+            const next = () => {
+                const fn = fns[curr++];
+                fn === last ? fn() : fn(next);
+            };
+            next();
+        };
         if (!window.MathJax) {
             window.MathJax = {
                 loader: {
@@ -80,40 +96,43 @@ export const mathRender = (element: HTMLElement, options?: { cdn?: string, math?
         }
         // 循环加载会抛异常
         addScriptSync(`${options.cdn}/dist/js/mathjax/tex-svg.js`, "vditorMathJaxScript");
-        const renderMath = () => {
-            mathElements.forEach((mathElement) => {
-                if (mathElement.getAttribute("data-math")) {
-                    return;
-                }
-                const math = code160to32(mathElement.textContent);
+        const renderMath = (mathElement: Element, next?: () => void) => {
+            const math = code160to32(mathElement.textContent).trim();
+            const mathOptions = window.MathJax.getMetricsFor(mathElement);
+            mathOptions.display = mathElement.tagName === "DIV";
+            window.MathJax.tex2svgPromise(math, mathOptions).then((node: Element) => {
+                mathElement.innerHTML = "";
                 mathElement.setAttribute("data-math", math);
-                if (!math) {
-                    return;
+                mathElement.append(node);
+                window.MathJax.startup.document.clear();
+                window.MathJax.startup.document.updateDocument();
+                const errorTextElement = node.querySelector('[data-mml-node="merror"]');
+                if (errorTextElement && errorTextElement.textContent.trim() !== "") {
+                    mathElement.innerHTML = errorTextElement.textContent.trim();
+                    mathElement.className = "vditor-reset--error";
+                }
+                if (next) {
+                    next();
                 }
-                window.MathJax.texReset();
-                const mathOptions = window.MathJax.getMetricsFor(mathElement);
-                mathOptions.display = mathElement.tagName === "DIV";
-                window.MathJax.tex2svgPromise(math, mathOptions).then((node: HTMLElement) => {
-                    mathElement.innerHTML = "";
-                    mathElement.append(node);
-                    window.MathJax.startup.document.clear();
-                    window.MathJax.startup.document.updateDocument();
-
-                    const errorTextElement = mathElement.querySelector('[data-mml-node="merror"]');
-                    if (errorTextElement && errorTextElement.textContent.trim() !== "") {
-                        mathElement.innerHTML = errorTextElement.textContent.trim();
-                        mathElement.className = "vditor-reset--error";
-                    }
-                });
             });
         };
-        // 初次加载需等到 js 文件执行完成
-        if (!window.MathJax.texReset) {
-            setTimeout(() => {
-                renderMath();
-            });
-        } else {
-            renderMath();
-        }
+        window.MathJax.startup.promise.then(() => {
+            const chains: any [] = [];
+            for (let i = 0; i < mathElements.length; i++) {
+                const mathElement = mathElements[i];
+                if (!mathElement.parentElement.classList.contains("vditor-wysiwyg__pre") &&
+                    !mathElement.parentElement.classList.contains("vditor-ir__marker--pre") &&
+                    !mathElement.getAttribute("data-math") && code160to32(mathElement.textContent).trim()) {
+                    chains.push((next: () => void) => {
+                        if (i === mathElements.length - 1) {
+                            renderMath(mathElement);
+                        } else {
+                            renderMath(mathElement, next);
+                        }
+                    });
+                }
+            }
+            chainAsync(chains);
+        });
     }
 };

+ 14 - 1
src/ts/toolbar/EditMode.ts

@@ -2,6 +2,7 @@ import {Constants} from "../constants";
 import {i18n} from "../i18n";
 import {processAfterRender} from "../ir/process";
 import {getMarkdown} from "../markdown/getMarkdown";
+import {mathRender} from "../markdown/mathRender";
 import {processAfterRender as processSVAfterRender, processSpinVditorSVDOM} from "../sv/process";
 import {setPadding, setTypewriterPosition} from "../ui/initUI";
 import {getEventName, updateHotkeyTip} from "../util/compatibility";
@@ -63,6 +64,12 @@ export const setEditMode = (vditor: IVditor, type: string, event: Event | string
         vditor.ir.element.querySelectorAll(".vditor-ir__preview[data-render='2']").forEach((item: HTMLElement) => {
             processCodeRender(item, vditor);
         });
+        vditor.ir.element.querySelectorAll(".vditor-toc").forEach((item: HTMLElement) => {
+            mathRender(item, {
+                cdn: vditor.options.cdn,
+                math: vditor.options.preview.math,
+            });
+        });
     } else if (type === "wysiwyg") {
         hideToolbar(vditor.toolbar.elements, ["both"]);
         showToolbar(vditor.toolbar.elements, ["outdent", "indent", "outline", "insert-before", "insert-after"]);
@@ -78,6 +85,12 @@ export const setEditMode = (vditor: IVditor, type: string, event: Event | string
             enableHint: false,
             enableInput: false,
         });
+        vditor.wysiwyg.element.querySelectorAll(".vditor-toc").forEach((item: HTMLElement) => {
+            mathRender(item, {
+                cdn: vditor.options.cdn,
+                math: vditor.options.preview.math,
+            });
+        });
         vditor.wysiwyg.popover.style.display = "none";
     } else if (type === "sv") {
         showToolbar(vditor.toolbar.elements, ["both"]);
@@ -90,7 +103,7 @@ export const setEditMode = (vditor: IVditor, type: string, event: Event | string
             vditor.sv.element.style.display = "block";
         }
         vditor.currentMode = "sv";
-        let svHTML =  processSpinVditorSVDOM(markdownText, vditor);
+        let svHTML = processSpinVditorSVDOM(markdownText, vditor);
         if (svHTML === "<div data-block='0'></div>") {
             // https://github.com/Vanessa219/vditor/issues/654 SV 模式 Placeholder 显示问题
             svHTML = "";

+ 1 - 28
src/ts/util/fixBrowserBehavior.ts

@@ -4,7 +4,6 @@ import {processAfterRender} from "../ir/process";
 import {processAfterRender as processSVAfterRender, processPaste} from "../sv/process";
 import {uploadFiles} from "../upload";
 import {setHeaders} from "../upload/setHeaders";
-import {processCodeRender, processPasteCode} from "../util/processCode";
 import {afterRenderEvent} from "../wysiwyg/afterRenderEvent";
 import {input} from "../wysiwyg/input";
 import {isCtrl, isFirefox} from "./compatibility";
@@ -17,9 +16,9 @@ import {
     hasClosestByMatchTag,
 } from "./hasClosest";
 import {getLastNode} from "./hasClosest";
-import {hasClosestByHeadings} from "./hasClosestByHeadings";
 import {highlightToolbar} from "./highlightToolbar";
 import {matchHotKey} from "./hotKey";
+import {processCodeRender, processPasteCode} from "./processCode";
 import {
     getEditorRange,
     getSelectPosition,
@@ -430,32 +429,6 @@ export const isHeadingMD = (text: string) => {
     return false;
 };
 
-export const isToC = (text: string) => {
-    return text.trim().toLowerCase() === "[toc]";
-};
-
-export const renderToc = (vditor: IVditor) => {
-    const editorElement = vditor[vditor.currentMode].element;
-    vditor.outline.render(vditor);
-    const tocElement = editorElement.querySelector('[data-type="toc-block"]');
-    if (!tocElement) {
-        return;
-    }
-    let tocHTML = "";
-    Array.from(editorElement.children).forEach((item: HTMLElement) => {
-        if (hasClosestByHeadings(item)) {
-            const headingNo = parseInt(item.tagName.substring(1), 10);
-            const space = new Array((headingNo - 1) * 2).fill("&emsp;").join("");
-            if (vditor.currentMode === "ir") {
-                tocHTML += `${space}<span data-type="toc-h">${item.textContent.substring(headingNo + 1).trim()}</span><br>`;
-            } else {
-                tocHTML += `${space}<span data-type="toc-h">${item.textContent.trim()}</span><br>`;
-            }
-        }
-    });
-    tocElement.innerHTML = tocHTML || "[ToC]";
-};
-
 export const execAfterRender = (vditor: IVditor, options = {
     enableAddUndoStack: true,
     enableHint: false,

+ 93 - 0
src/ts/util/toc.ts

@@ -0,0 +1,93 @@
+import {mathRender} from "../markdown/mathRender";
+import {hasClosestByClassName, hasClosestByMatchTag} from "./hasClosest";
+import {hasClosestByHeadings} from "./hasClosestByHeadings";
+import {getSelectPosition} from "./selection";
+import {execAfterRender, insertAfterBlock, insertBeforeBlock} from "./fixBrowserBehavior";
+
+export const renderToc = (vditor: IVditor) => {
+    const editorElement = vditor[vditor.currentMode].element;
+    let tocHTML = "";
+    Array.from(editorElement.children).forEach((item: HTMLElement) => {
+        if (hasClosestByHeadings(item)) {
+            tocHTML += item.outerHTML;
+        }
+    });
+    if (tocHTML === "") {
+        tocHTML = "[ToC]";
+    } else {
+        const tempElement = document.createElement("div");
+        if (vditor.currentMode === "wysiwyg") {
+            tempElement.innerHTML = vditor.lute.SpinVditorDOM("<p>[ToC]</p>" + tocHTML);
+        } else if (vditor.currentMode === "ir") {
+            tempElement.innerHTML = vditor.lute.SpinVditorIRDOM("<p>[ToC]</p>" + tocHTML);
+        }
+        tocHTML = tempElement.firstElementChild.innerHTML;
+    }
+    editorElement.querySelectorAll('[data-type="toc-block"]').forEach((item: HTMLElement) => {
+        item.innerHTML = tocHTML;
+        mathRender(item, {
+            cdn: vditor.options.cdn,
+            math: vditor.options.preview.math,
+        });
+    });
+};
+
+export const clickToc = (event: MouseEvent & { target: HTMLElement }, vditor: IVditor) => {
+    // TOC 点击
+    const spanElement = hasClosestByMatchTag(event.target, "SPAN");
+    if (spanElement && hasClosestByClassName(spanElement, "vditor-toc")) {
+        const headingElement = vditor[vditor.currentMode].element.querySelector("#" + spanElement.getAttribute("data-target-id")) as HTMLElement;
+        if (headingElement) {
+            if (vditor.options.height === "auto") {
+                let windowScrollY = headingElement.offsetTop + vditor.element.offsetTop;
+                if (!vditor.options.toolbarConfig.pin) {
+                    windowScrollY += vditor.toolbar.element.offsetHeight;
+                }
+                window.scrollTo(window.scrollX, windowScrollY);
+            } else {
+                if (vditor.element.offsetTop < window.scrollY) {
+                    window.scrollTo(window.scrollX, vditor.element.offsetTop);
+                }
+                vditor[vditor.currentMode].element.scrollTop = headingElement.offsetTop;
+            }
+        }
+        return;
+    }
+};
+
+export const keydownToc = (blockElement: HTMLElement, vditor: IVditor, event: KeyboardEvent, range: Range) => {
+    // toc 前无元素,插入空块
+    if (blockElement.previousElementSibling &&
+        blockElement.previousElementSibling.classList.contains("vditor-toc")) {
+        if (event.key === "Backspace" && getSelectPosition(blockElement, vditor[vditor.currentMode].element, range).start === 0) {
+            blockElement.previousElementSibling.remove();
+            execAfterRender(vditor);
+            return true;
+        }
+        if (insertBeforeBlock(vditor, event, range, blockElement, blockElement.previousElementSibling as HTMLElement)) {
+            return true;
+        }
+    }
+    // toc 后无元素,插入空块
+    if (blockElement.nextElementSibling &&
+        blockElement.nextElementSibling.classList.contains("vditor-toc")) {
+        if (event.key === "Delete" &&
+            getSelectPosition(blockElement, vditor[vditor.currentMode].element, range).start >= blockElement.textContent.trimRight().length) {
+            blockElement.nextElementSibling.remove();
+            execAfterRender(vditor);
+            return true;
+        }
+        if (insertAfterBlock(vditor, event, range, blockElement, blockElement.nextElementSibling as HTMLElement)) {
+            return true;
+        }
+    }
+    // toc 删除
+    if (event.key === "Backspace" || event.key === "Delete") {
+        const tocElement = hasClosestByClassName(range.startContainer, "vditor-toc");
+        if (tocElement) {
+            tocElement.remove();
+            execAfterRender(vditor);
+            return true;
+        }
+    }
+}

+ 1 - 9
src/ts/wysiwyg/highlightToolbarWYSIWYG.ts

@@ -126,15 +126,7 @@ export const highlightToolbarWYSIWYG = (vditor: IVditor) => {
         }
 
         // toc popover
-        let tocElement = hasClosestByClassName(typeElement, "vditor-toc") as HTMLElement;
-        if (!tocElement) {
-            const blockElement = hasClosestByAttribute(typeElement, "data-block", "0");
-            if (blockElement) {
-                if (blockElement.previousElementSibling?.classList.contains("vditor-toc")) {
-                    tocElement = blockElement.previousElementSibling as HTMLElement;
-                }
-            }
-        }
+        const tocElement = hasClosestByClassName(typeElement, "vditor-toc") as HTMLElement;
         if (tocElement) {
             vditor.wysiwyg.popover.innerHTML = "";
             genClose(tocElement, vditor);

+ 4 - 1
src/ts/wysiwyg/index.ts

@@ -11,7 +11,7 @@ import {
     scrollCenter,
     selectEvent,
 } from "../util/editorCommonEvent";
-import {isHeadingMD, isHrMD, paste, renderToc} from "../util/fixBrowserBehavior";
+import {isHeadingMD, isHrMD, paste} from "../util/fixBrowserBehavior";
 import {
     hasClosestBlock, hasClosestByAttribute,
     hasClosestByClassName, hasClosestByMatchTag,
@@ -23,6 +23,7 @@ import {
     getSelectPosition,
     setRangeByWbr,
 } from "../util/selection";
+import {clickToc, renderToc} from "../util/toc";
 import {afterRenderEvent} from "./afterRenderEvent";
 import {genImagePopover, genLinkRefPopover, highlightToolbarWYSIWYG} from "./highlightToolbarWYSIWYG";
 import {getRenderElementNextNode, modifyPre} from "./inlineTag";
@@ -433,6 +434,8 @@ class WYSIWYG {
             if (previewElement) {
                 showCode(previewElement, vditor);
             }
+
+            clickToc(event, vditor);
         });
 
         this.element.addEventListener("keyup", (event: KeyboardEvent & { target: HTMLElement }) => {

+ 3 - 3
src/ts/wysiwyg/input.ts

@@ -1,4 +1,3 @@
-import {isToC, renderToc} from "../util/fixBrowserBehavior";
 import {
     getTopList,
     hasClosestBlock, hasClosestByAttribute, hasTopClosestByTag,
@@ -7,6 +6,7 @@ import {hasClosestByHeadings, hasClosestByTag} from "../util/hasClosestByHeading
 import {log} from "../util/log";
 import {processCodeRender} from "../util/processCode";
 import {setRangeByWbr} from "../util/selection";
+import {renderToc} from "../util/toc";
 import {afterRenderEvent} from "./afterRenderEvent";
 import {previoueIsEmptyA} from "./inlineTag";
 
@@ -64,8 +64,8 @@ export const input = (vditor: IVditor, range: Range, event?: InputEvent) => {
         });
 
         let html = "";
-        if (blockElement.getAttribute("data-type") === "link-ref-defs-block" || isToC(blockElement.innerText)) {
-            // 修改链接引用或 ToC
+        if (blockElement.getAttribute("data-type") === "link-ref-defs-block") {
+            // 修改链接引用
             blockElement = vditor.wysiwyg.element;
         }
 

+ 8 - 1
src/ts/wysiwyg/processKeydown.ts

@@ -24,6 +24,7 @@ import {afterRenderEvent} from "./afterRenderEvent";
 import {nextIsCode} from "./inlineTag";
 import {removeHeading, setHeading} from "./setHeading";
 import {showCode} from "./showCode";
+import {keydownToc} from "../util/toc";
 
 export const processKeydown = (vditor: IVditor, event: KeyboardEvent) => {
     // Chrome firefox 触发 compositionend 机制不一致 https://github.com/Vanessa219/vditor/issues/188
@@ -51,7 +52,7 @@ export const processKeydown = (vditor: IVditor, event: KeyboardEvent) => {
 
     // 仅处理以下快捷键操作
     if (event.key !== "Enter" && event.key !== "Tab" && event.key !== "Backspace" && event.key.indexOf("Arrow") === -1
-        && !isCtrl(event) && event.key !== "Escape") {
+        && !isCtrl(event) && event.key !== "Escape" && event.key !== "Delete") {
         return false;
     }
 
@@ -336,6 +337,12 @@ export const processKeydown = (vditor: IVditor, event: KeyboardEvent) => {
             range.setStartAfter(nextElement);
         }
     }
+
+    if (blockElement && keydownToc(blockElement, vditor, event, range)) {
+        event.preventDefault();
+        return true;
+    }
+
     return false;
 };
 

+ 1 - 1
src/ts/wysiwyg/setHeading.ts

@@ -1,6 +1,6 @@
-import {renderToc} from "../util/fixBrowserBehavior";
 import {hasClosestBlock} from "../util/hasClosest";
 import {getEditorRange, setRangeByWbr} from "../util/selection";
+import {renderToc} from "../util/toc";
 
 export const setHeading = (vditor: IVditor, tagName: string) => {
     const range = getEditorRange(vditor.wysiwyg.element);