From f20b7aa61c525af873d583dfd87c8143591b65e5 Mon Sep 17 00:00:00 2001 From: klarkxy <278370456@qq.com> Date: Fri, 10 Apr 2026 16:18:01 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20=E6=96=B0=E5=A2=9E=20WYSIWY?= =?UTF-8?q?G=20=E7=BC=96=E8=BE=91=E5=99=A8=E5=8A=9F=E8=83=BD=E5=92=8C?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E5=B7=A5=E5=85=B7=E6=A0=8F=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + web/admin/kindeditor.php | 4 +- web/bsadmin/kindeditor.php | 4 +- web/include/vditor-adapter.js | 24 +- web/include/vditor/LICENSE | 21 + web/include/vditor/README.md | 680 ++++++++ web/include/vditor/package.json | 76 + .../vditor/src/assets/less/_content.less | 279 +++ web/include/vditor/src/assets/less/_hint.less | 67 + web/include/vditor/src/assets/less/_ir.less | 275 +++ .../vditor/src/assets/less/_panel.less | 110 ++ .../vditor/src/assets/less/_reset.less | 610 +++++++ web/include/vditor/src/assets/less/_sv.less | 132 ++ .../vditor/src/assets/less/_toolbar.less | 180 ++ .../vditor/src/assets/less/_tooltipped.less | 171 ++ .../vditor/src/assets/less/_wysiwyg.less | 195 +++ web/include/vditor/src/assets/less/index.less | 88 + web/include/vditor/src/index.ts | 553 ++++++ web/include/vditor/src/method.ts | 68 + web/include/vditor/src/ts/constants.ts | 69 + web/include/vditor/src/ts/devtools/index.ts | 83 + web/include/vditor/src/ts/export/index.ts | 83 + web/include/vditor/src/ts/hint/index.ts | 276 +++ web/include/vditor/src/ts/ir/expandMarker.ts | 79 + .../vditor/src/ts/ir/highlightToolbarIR.ts | 97 ++ web/include/vditor/src/ts/ir/index.ts | 265 +++ web/include/vditor/src/ts/ir/input.ts | 246 +++ web/include/vditor/src/ts/ir/process.ts | 222 +++ .../vditor/src/ts/ir/processKeydown.ts | 238 +++ .../vditor/src/ts/markdown/abcRender.ts | 27 + .../vditor/src/ts/markdown/adapterRender.ts | 37 + .../vditor/src/ts/markdown/anchorRender.ts | 19 + .../vditor/src/ts/markdown/chartRender.ts | 38 + .../vditor/src/ts/markdown/codeRender.ts | 60 + .../vditor/src/ts/markdown/flowchartRender.ts | 25 + web/include/vditor/src/ts/markdown/getHTML.ts | 11 + .../vditor/src/ts/markdown/getMarkdown.ts | 12 + .../vditor/src/ts/markdown/graphvizRender.ts | 49 + .../vditor/src/ts/markdown/highlightRender.ts | 90 + .../src/ts/markdown/lazyLoadImageRender.ts | 56 + .../vditor/src/ts/markdown/markmapRender.ts | 57 + .../vditor/src/ts/markdown/mathRender.ts | 148 ++ .../vditor/src/ts/markdown/mediaRender.ts | 105 ++ .../vditor/src/ts/markdown/mermaidRender.ts | 60 + .../vditor/src/ts/markdown/mindmapRender.ts | 74 + .../vditor/src/ts/markdown/outlineRender.ts | 110 ++ .../vditor/src/ts/markdown/plantumlRender.ts | 32 + .../vditor/src/ts/markdown/previewRender.ts | 167 ++ web/include/vditor/src/ts/markdown/setLute.ts | 24 + .../vditor/src/ts/markdown/speechRender.ts | 107 ++ web/include/vditor/src/ts/outline/index.ts | 45 + web/include/vditor/src/ts/preview/image.ts | 50 + web/include/vditor/src/ts/preview/index.ts | 285 ++++ web/include/vditor/src/ts/resize/index.ts | 57 + .../vditor/src/ts/sv/combineFootnote.ts | 27 + web/include/vditor/src/ts/sv/index.ts | 125 ++ web/include/vditor/src/ts/sv/inputEvent.ts | 188 +++ web/include/vditor/src/ts/sv/process.ts | 215 +++ .../vditor/src/ts/sv/processKeydown.ts | 202 +++ web/include/vditor/src/ts/tip/index.ts | 39 + web/include/vditor/src/ts/toolbar/Both.ts | 28 + web/include/vditor/src/ts/toolbar/Br.ts | 8 + .../vditor/src/ts/toolbar/CodeTheme.ts | 36 + .../vditor/src/ts/toolbar/ContentTheme.ts | 35 + web/include/vditor/src/ts/toolbar/Counter.ts | 40 + web/include/vditor/src/ts/toolbar/Custom.ts | 17 + web/include/vditor/src/ts/toolbar/Devtools.ts | 28 + web/include/vditor/src/ts/toolbar/Divider.ts | 8 + web/include/vditor/src/ts/toolbar/EditMode.ts | 195 +++ web/include/vditor/src/ts/toolbar/Emoji.ts | 74 + web/include/vditor/src/ts/toolbar/Export.ts | 41 + .../vditor/src/ts/toolbar/Fullscreen.ts | 66 + web/include/vditor/src/ts/toolbar/Headings.ts | 73 + web/include/vditor/src/ts/toolbar/Help.ts | 30 + web/include/vditor/src/ts/toolbar/Indent.ts | 25 + web/include/vditor/src/ts/toolbar/Info.ts | 43 + .../vditor/src/ts/toolbar/InsertAfter.ts | 18 + .../vditor/src/ts/toolbar/InsertBefore.ts | 18 + web/include/vditor/src/ts/toolbar/MenuItem.ts | 54 + web/include/vditor/src/ts/toolbar/Outdent.ts | 24 + web/include/vditor/src/ts/toolbar/Outline.ts | 21 + web/include/vditor/src/ts/toolbar/Preview.ts | 55 + web/include/vditor/src/ts/toolbar/Record.ts | 60 + web/include/vditor/src/ts/toolbar/Redo.ts | 18 + web/include/vditor/src/ts/toolbar/Undo.ts | 18 + web/include/vditor/src/ts/toolbar/Upload.ts | 41 + web/include/vditor/src/ts/toolbar/index.ts | 192 +++ .../vditor/src/ts/toolbar/setToolbar.ts | 120 ++ web/include/vditor/src/ts/ui/initUI.ts | 180 ++ web/include/vditor/src/ts/ui/setCodeTheme.ts | 16 + .../vditor/src/ts/ui/setContentTheme.ts | 15 + .../vditor/src/ts/ui/setPreviewMode.ts | 31 + web/include/vditor/src/ts/ui/setTheme.ts | 7 + web/include/vditor/src/ts/undo/index.ts | 260 +++ .../vditor/src/ts/upload/getElement.ts | 10 + web/include/vditor/src/ts/upload/index.ts | 260 +++ .../vditor/src/ts/upload/setHeaders.ts | 10 + web/include/vditor/src/ts/util/Options.ts | 454 +++++ web/include/vditor/src/ts/util/RecordMedia.ts | 170 ++ web/include/vditor/src/ts/util/addScript.ts | 43 + web/include/vditor/src/ts/util/addStyle.ts | 10 + web/include/vditor/src/ts/util/code160to32.ts | 4 + .../vditor/src/ts/util/compatibility.ts | 67 + .../vditor/src/ts/util/editorCommonEvent.ts | 261 +++ .../vditor/src/ts/util/fixBrowserBehavior.ts | 1501 +++++++++++++++++ web/include/vditor/src/ts/util/function.ts | 15 + .../vditor/src/ts/util/getSelectText.ts | 8 + web/include/vditor/src/ts/util/hasClosest.ts | 154 ++ .../src/ts/util/hasClosestByHeadings.ts | 27 + .../vditor/src/ts/util/highlightToolbar.ts | 10 + web/include/vditor/src/ts/util/hotKey.ts | 54 + web/include/vditor/src/ts/util/log.ts | 6 + web/include/vditor/src/ts/util/merge.ts | 18 + web/include/vditor/src/ts/util/processCode.ts | 98 ++ web/include/vditor/src/ts/util/selection.ts | 275 +++ web/include/vditor/src/ts/util/toc.ts | 83 + .../vditor/src/ts/wysiwyg/afterRenderEvent.ts | 41 + .../src/ts/wysiwyg/highlightToolbarWYSIWYG.ts | 1139 +++++++++++++ web/include/vditor/src/ts/wysiwyg/index.ts | 575 +++++++ .../vditor/src/ts/wysiwyg/inlineTag.ts | 129 ++ web/include/vditor/src/ts/wysiwyg/input.ts | 212 +++ .../vditor/src/ts/wysiwyg/processKeydown.ts | 363 ++++ .../vditor/src/ts/wysiwyg/renderDomByMd.ts | 18 + .../vditor/src/ts/wysiwyg/setHeading.ts | 43 + web/include/vditor/src/ts/wysiwyg/showCode.ts | 32 + .../vditor/src/ts/wysiwyg/toolbarEvent.ts | 333 ++++ web/swadmin/kindeditor.php | 4 +- web/template/syzoj/problem.php | 72 - 128 files changed, 16048 insertions(+), 84 deletions(-) create mode 100644 web/include/vditor/LICENSE create mode 100644 web/include/vditor/README.md create mode 100644 web/include/vditor/package.json create mode 100644 web/include/vditor/src/assets/less/_content.less create mode 100644 web/include/vditor/src/assets/less/_hint.less create mode 100644 web/include/vditor/src/assets/less/_ir.less create mode 100644 web/include/vditor/src/assets/less/_panel.less create mode 100644 web/include/vditor/src/assets/less/_reset.less create mode 100644 web/include/vditor/src/assets/less/_sv.less create mode 100644 web/include/vditor/src/assets/less/_toolbar.less create mode 100644 web/include/vditor/src/assets/less/_tooltipped.less create mode 100644 web/include/vditor/src/assets/less/_wysiwyg.less create mode 100644 web/include/vditor/src/assets/less/index.less create mode 100644 web/include/vditor/src/index.ts create mode 100644 web/include/vditor/src/method.ts create mode 100644 web/include/vditor/src/ts/constants.ts create mode 100644 web/include/vditor/src/ts/devtools/index.ts create mode 100644 web/include/vditor/src/ts/export/index.ts create mode 100644 web/include/vditor/src/ts/hint/index.ts create mode 100644 web/include/vditor/src/ts/ir/expandMarker.ts create mode 100644 web/include/vditor/src/ts/ir/highlightToolbarIR.ts create mode 100644 web/include/vditor/src/ts/ir/index.ts create mode 100644 web/include/vditor/src/ts/ir/input.ts create mode 100644 web/include/vditor/src/ts/ir/process.ts create mode 100644 web/include/vditor/src/ts/ir/processKeydown.ts create mode 100644 web/include/vditor/src/ts/markdown/abcRender.ts create mode 100644 web/include/vditor/src/ts/markdown/adapterRender.ts create mode 100644 web/include/vditor/src/ts/markdown/anchorRender.ts create mode 100644 web/include/vditor/src/ts/markdown/chartRender.ts create mode 100644 web/include/vditor/src/ts/markdown/codeRender.ts create mode 100644 web/include/vditor/src/ts/markdown/flowchartRender.ts create mode 100644 web/include/vditor/src/ts/markdown/getHTML.ts create mode 100644 web/include/vditor/src/ts/markdown/getMarkdown.ts create mode 100644 web/include/vditor/src/ts/markdown/graphvizRender.ts create mode 100644 web/include/vditor/src/ts/markdown/highlightRender.ts create mode 100644 web/include/vditor/src/ts/markdown/lazyLoadImageRender.ts create mode 100644 web/include/vditor/src/ts/markdown/markmapRender.ts create mode 100644 web/include/vditor/src/ts/markdown/mathRender.ts create mode 100644 web/include/vditor/src/ts/markdown/mediaRender.ts create mode 100644 web/include/vditor/src/ts/markdown/mermaidRender.ts create mode 100644 web/include/vditor/src/ts/markdown/mindmapRender.ts create mode 100644 web/include/vditor/src/ts/markdown/outlineRender.ts create mode 100644 web/include/vditor/src/ts/markdown/plantumlRender.ts create mode 100644 web/include/vditor/src/ts/markdown/previewRender.ts create mode 100644 web/include/vditor/src/ts/markdown/setLute.ts create mode 100644 web/include/vditor/src/ts/markdown/speechRender.ts create mode 100644 web/include/vditor/src/ts/outline/index.ts create mode 100644 web/include/vditor/src/ts/preview/image.ts create mode 100644 web/include/vditor/src/ts/preview/index.ts create mode 100644 web/include/vditor/src/ts/resize/index.ts create mode 100644 web/include/vditor/src/ts/sv/combineFootnote.ts create mode 100644 web/include/vditor/src/ts/sv/index.ts create mode 100644 web/include/vditor/src/ts/sv/inputEvent.ts create mode 100644 web/include/vditor/src/ts/sv/process.ts create mode 100644 web/include/vditor/src/ts/sv/processKeydown.ts create mode 100644 web/include/vditor/src/ts/tip/index.ts create mode 100644 web/include/vditor/src/ts/toolbar/Both.ts create mode 100644 web/include/vditor/src/ts/toolbar/Br.ts create mode 100644 web/include/vditor/src/ts/toolbar/CodeTheme.ts create mode 100644 web/include/vditor/src/ts/toolbar/ContentTheme.ts create mode 100644 web/include/vditor/src/ts/toolbar/Counter.ts create mode 100644 web/include/vditor/src/ts/toolbar/Custom.ts create mode 100644 web/include/vditor/src/ts/toolbar/Devtools.ts create mode 100644 web/include/vditor/src/ts/toolbar/Divider.ts create mode 100644 web/include/vditor/src/ts/toolbar/EditMode.ts create mode 100644 web/include/vditor/src/ts/toolbar/Emoji.ts create mode 100644 web/include/vditor/src/ts/toolbar/Export.ts create mode 100644 web/include/vditor/src/ts/toolbar/Fullscreen.ts create mode 100644 web/include/vditor/src/ts/toolbar/Headings.ts create mode 100644 web/include/vditor/src/ts/toolbar/Help.ts create mode 100644 web/include/vditor/src/ts/toolbar/Indent.ts create mode 100644 web/include/vditor/src/ts/toolbar/Info.ts create mode 100644 web/include/vditor/src/ts/toolbar/InsertAfter.ts create mode 100644 web/include/vditor/src/ts/toolbar/InsertBefore.ts create mode 100644 web/include/vditor/src/ts/toolbar/MenuItem.ts create mode 100644 web/include/vditor/src/ts/toolbar/Outdent.ts create mode 100644 web/include/vditor/src/ts/toolbar/Outline.ts create mode 100644 web/include/vditor/src/ts/toolbar/Preview.ts create mode 100644 web/include/vditor/src/ts/toolbar/Record.ts create mode 100644 web/include/vditor/src/ts/toolbar/Redo.ts create mode 100644 web/include/vditor/src/ts/toolbar/Undo.ts create mode 100644 web/include/vditor/src/ts/toolbar/Upload.ts create mode 100644 web/include/vditor/src/ts/toolbar/index.ts create mode 100644 web/include/vditor/src/ts/toolbar/setToolbar.ts create mode 100644 web/include/vditor/src/ts/ui/initUI.ts create mode 100644 web/include/vditor/src/ts/ui/setCodeTheme.ts create mode 100644 web/include/vditor/src/ts/ui/setContentTheme.ts create mode 100644 web/include/vditor/src/ts/ui/setPreviewMode.ts create mode 100644 web/include/vditor/src/ts/ui/setTheme.ts create mode 100644 web/include/vditor/src/ts/undo/index.ts create mode 100644 web/include/vditor/src/ts/upload/getElement.ts create mode 100644 web/include/vditor/src/ts/upload/index.ts create mode 100644 web/include/vditor/src/ts/upload/setHeaders.ts create mode 100644 web/include/vditor/src/ts/util/Options.ts create mode 100644 web/include/vditor/src/ts/util/RecordMedia.ts create mode 100644 web/include/vditor/src/ts/util/addScript.ts create mode 100644 web/include/vditor/src/ts/util/addStyle.ts create mode 100644 web/include/vditor/src/ts/util/code160to32.ts create mode 100644 web/include/vditor/src/ts/util/compatibility.ts create mode 100644 web/include/vditor/src/ts/util/editorCommonEvent.ts create mode 100644 web/include/vditor/src/ts/util/fixBrowserBehavior.ts create mode 100644 web/include/vditor/src/ts/util/function.ts create mode 100644 web/include/vditor/src/ts/util/getSelectText.ts create mode 100644 web/include/vditor/src/ts/util/hasClosest.ts create mode 100644 web/include/vditor/src/ts/util/hasClosestByHeadings.ts create mode 100644 web/include/vditor/src/ts/util/highlightToolbar.ts create mode 100644 web/include/vditor/src/ts/util/hotKey.ts create mode 100644 web/include/vditor/src/ts/util/log.ts create mode 100644 web/include/vditor/src/ts/util/merge.ts create mode 100644 web/include/vditor/src/ts/util/processCode.ts create mode 100644 web/include/vditor/src/ts/util/selection.ts create mode 100644 web/include/vditor/src/ts/util/toc.ts create mode 100644 web/include/vditor/src/ts/wysiwyg/afterRenderEvent.ts create mode 100644 web/include/vditor/src/ts/wysiwyg/highlightToolbarWYSIWYG.ts create mode 100644 web/include/vditor/src/ts/wysiwyg/index.ts create mode 100644 web/include/vditor/src/ts/wysiwyg/inlineTag.ts create mode 100644 web/include/vditor/src/ts/wysiwyg/input.ts create mode 100644 web/include/vditor/src/ts/wysiwyg/processKeydown.ts create mode 100644 web/include/vditor/src/ts/wysiwyg/renderDomByMd.ts create mode 100644 web/include/vditor/src/ts/wysiwyg/setHeading.ts create mode 100644 web/include/vditor/src/ts/wysiwyg/showCode.ts create mode 100644 web/include/vditor/src/ts/wysiwyg/toolbarEvent.ts diff --git a/.gitignore b/.gitignore index 33ed58b..d805f47 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ core/judged/judgehub core/judge_client/judge_client core/judge_client/judge_client.http +node_modules +dist \ No newline at end of file diff --git a/web/admin/kindeditor.php b/web/admin/kindeditor.php index ced2656..e4ae463 100644 --- a/web/admin/kindeditor.php +++ b/web/admin/kindeditor.php @@ -6,9 +6,9 @@ selector: 'textarea.kindeditor', uploadUrl: '../kindeditor/php/upload_json.php', accept: '*/*', - mode: 'sv', + mode: 'wysiwyg', preview: { - mode: 'both', + mode: 'editor', delay: 0, actions: [], markdown: { diff --git a/web/bsadmin/kindeditor.php b/web/bsadmin/kindeditor.php index 62cb78e..b00361a 100644 --- a/web/bsadmin/kindeditor.php +++ b/web/bsadmin/kindeditor.php @@ -6,10 +6,10 @@ selector: 'textarea.kindeditor', uploadUrl: '../kindeditor/php/upload_json.php', accept: '*/*', - mode: 'sv', + mode: 'wysiwyg', ctrlEnterFormName: 'example', preview: { - mode: 'both', + mode: 'editor', delay: 0, actions: [], markdown: { diff --git a/web/include/vditor-adapter.js b/web/include/vditor-adapter.js index 8e4fc26..8fb5711 100644 --- a/web/include/vditor-adapter.js +++ b/web/include/vditor-adapter.js @@ -6,10 +6,25 @@ return; } - var DEFAULT_CDN = global.HUSTOJ_VDITOR_CDN || 'https://cdn.jsdelivr.net/npm/vditor@3.10.6'; + var currentScriptRef = documentRef.currentScript || (function () { + var scripts = documentRef.getElementsByTagName('script'); + return scripts.length > 0 ? scripts[scripts.length - 1] : null; + })(); + var DEFAULT_CDN = global.HUSTOJ_VDITOR_CDN || getLocalCdnBase(); + global.HUSTOJ_VDITOR_CDN = DEFAULT_CDN; var assetsPromise = null; var styleInjected = false; + function getLocalCdnBase() { + var scriptSrc = currentScriptRef && currentScriptRef.src; + + if (!scriptSrc) { + return 'include/vditor'; + } + + return scriptSrc.replace(/\/vditor-adapter\.js(?:[?#].*)?$/, '/vditor'); + } + function injectAdapterStyle() { var style; @@ -85,6 +100,7 @@ function ensureAssets(options) { var cdn = (options && options.cdn) || DEFAULT_CDN; + global.HUSTOJ_VDITOR_CDN = cdn; if (global.Vditor) { injectAdapterStyle(); return Promise.resolve(global.Vditor); @@ -95,7 +111,7 @@ ensureStyleTag(cdn + '/dist/index.css'); injectAdapterStyle(); - assetsPromise = ensureScript(cdn + '/dist/index.min.js').then(function () { + assetsPromise = ensureScript(cdn + '/dist/index.js').then(function () { return global.Vditor; }); return assetsPromise; @@ -447,10 +463,6 @@ '|', 'undo', 'redo', - '|', - 'edit-mode', - 'both', - 'preview', 'fullscreen', 'outline' ]; diff --git a/web/include/vditor/LICENSE b/web/include/vditor/LICENSE new file mode 100644 index 0000000..b28e5ea --- /dev/null +++ b/web/include/vditor/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 B3log 开源, b3log.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/web/include/vditor/README.md b/web/include/vditor/README.md new file mode 100644 index 0000000..b193430 --- /dev/null +++ b/web/include/vditor/README.md @@ -0,0 +1,680 @@ +

+Vditor + +
+易于使用的 Markdown 编辑器,为适配不同的应用场景而生 +

+ +npm bundle size +
+ + +

+ + + + +

+ +

+English  |  Demo +

+ +## 💡 简介 + +[Vditor](https://b3log.org/vditor) 是一款浏览器端的 Markdown 编辑器,支持所见即所得、即时渲染(类似 Typora)和分屏预览模式。它使用 TypeScript 实现,支持原生 JavaScript 以及 Vue、React、Angular 和 Svelte 等框架。 + +欢迎到 [Vditor 官方讨论区](https://ld246.com/tag/vditor)了解更多。同时也欢迎关注 B3log 开源社区微信公众号 `B3log开源`: + +![b3logos.jpg](https://b3logfile.com/file/2020/08/b3logos-032af045.jpg) + +## 🗺️ 背景 + +随着 Markdown 排版方式的普及,越来越多的应用开始集成 Markdown 编辑器。目前主流可集成的 Markdown 编辑器现状如下: + +* 有的仅支持分屏预览,即编辑区和预览区分离 +* 有的同时支持所见即所得和分屏预览,但所见即所得模式下不能完整支持 Markdown 语法排版 +* 几乎没有类似 Typora 的即时渲染 + +而这三点恰好对应了三种应用场景: + +* 分屏预览:适配传统的 Markdown 使用场景,适合大屏下编辑排版 +* 所见即所得:对不熟悉 Markdown 的用户友好,熟悉 Markdown 的用户也可以无缝使用 +* 即时渲染:理论上这是最为优雅的 Markdown 编辑方式,让熟悉 Markdown 的用户能够更专注于内容创作 + +所以,一个能够**适配应用场景**的 Markdown 编辑器至关重要,它需要考虑到: + +* 传统 Markdown 用户的使用场景,提供分屏预览 +* 富文本编辑用户的使用场景,提供所见即所得 +* 高阶 Markdown 用户的使用场景,提供即时渲染 + +Vditor 在这些方面做了努力,希望能为现代化的通用 Markdown 编辑领域做出一些贡献。 + +## ✨ 特性 + +* 支持三种编辑模式:所见即所得(wysiwyg)、即时渲染(ir)、分屏预览(sv) +* 支持大纲、数学公式、脑图、图表、流程图、甘特图、时序图、五线谱、[多媒体](https://ld246.com/article/1589813914768)、语音阅读、标题锚点、代码高亮及复制、graphviz 渲染、[plantuml](https://plantuml.com)UML图 +* 导出、图片懒加载、任务列表、多平台预览、多主题切换、复制到微信公众号/知乎功能 +* 实现 CommonMark 和 GFM 规范,可对 Markdown 进行格式化和语法树查看,并支持[10+项](https://ld246.com/article/1549638745630#options-preview-markdown)配置 +* 工具栏包含 36+ 项操作,除支持扩展外还可对每一项中的[快捷键](https://ld246.com/article/1582778815353)、提示、提示位置、图标、点击事件、类名、子工具栏进行自定义 +* 表情/at/话题等自动补全扩展 +* 可使用拖拽、剪切板粘贴上传,显示实时上传进度,支持 CORS 跨域上传 +* 实时保存内容,防止意外丢失 +* 录音支持,用户可直接发布语音 +* 粘贴 HTML 自动转换为 Markdown,如粘贴中包含外链图片可通过指定接口上传到服务器 +* 支持主窗口大小拖拽、字符计数 +* 多主题支持,内置黑白绿三套主题 +* 多语言支持,内置中、英、韩文本地化 +* 支持主流浏览器,对移动端友好 + +![editor.png](https://b3logfile.com/file/2020/07/editor-b304aa97.png) + +![preview.png](https://b3logfile.com/file/2020/05/preview-80846f66.png) + +## 🔮 编辑模式 + +### 所见即所得(WYSIWYG) + +*所见即所得*模式对不熟悉 Markdown 的用户较为友好,熟悉 Markdown 的话也可以无缝使用。 + +![vditor-wysiwyg](https://b3logfile.com/file/2020/07/wysiwyg-4f216b9b.gif) + +### 即时渲染(IR) + +*即时渲染*模式对熟悉 Typora 的用户应该不会感到陌生,理论上这是最优雅的 Markdown 编辑方式。 + +![vditor-ir](https://b3logfile.com/file/2020/07/ir-67cd956c.gif) + +### 分屏预览(SV) + +传统的*分屏预览*模式适合大屏下的 Markdown 编辑。 + +![vditor-sv](https://b3logfile.com/file/2020/07/sv-595dcb28.gif) + +## 🍱 语法支持 + +* 所有 CommonMark 语法:分隔线、ATX 标题、Setext 标题、缩进代码块、围栏代码块、HTML 块、链接引用定义、段落、块引用、列表、反斜杠转义、HTML 实体、行级代码、强调、加粗、链接、图片、行级 HTML、硬换行、软换行和纯文本。 +* 所有 GFM 语法:表格、任务列表项、删除线、自动链接、XSS 过滤 +* 常用 Markdown 扩展语法:脚注、ToC、自定义标题 ID +* 图表语法 + * 流程图、时序图、甘特图,通过 Mermaid 支持 + * Graphviz + * 折线图、饼图、脑图等,通过 ECharts 支持 +* 五线谱:通过 abc.js 支持 +* 数学公式:数学公式块、行级数学公式,通过 MathJax 和 KaTeX 支持 +* YAML Front Matter +* 中文语境优化 + * 中西文之间插入空格 + * 术语拼写修正 + * 中文后跟英文逗号句号等标点替换为中文对应标点 + +以上大部分特性可以通过开关配置是否启用,开发者可根据自己的应用场景选择搭配。 + +## 🗃 案例 + +* [Sym](https://github.com/88250/symphony) 一款用 Java 实现的现代化社区(论坛/BBS/社交网络/博客)平台 +* [Solo](https://github.com/88250/solo) & [Pipe](https://github.com/88250/pipe) B3log 分布式社区的博客端节点,欢迎加入下一代社区网络 +* [Tditor](https://tditor.com) 基于React、Vditor、Springboot, 一款打造极致文字创作体验的在线Markdown编辑平台 +* [Arya](https://github.com/nicejade/markdown-online-editor) 基于 Vue、Vditor,所构建的在线 Markdown 编辑器 +* [更多案例](https://github.com/Vanessa219/vditor/network/dependents?package_id=UGFja2FnZS0zMTY2Mzg4MzE%3D) + +## 🛠️ 使用文档 + +### CommonJS + +* 安装依赖 + +```shell +npm install vditor --save +``` + +* 在代码中引入并初始化对象,可参考 [index.js](https://github.com/Vanessa219/vditor/blob/master/demo/index.js) + +```ts +import Vditor from 'vditor' +import "~vditor/src/assets/less/index" + +const vditor = new Vditor(id, {options...}) +``` + +### HTML script + +* 在 HTML 中插入 CSS 和 JavaScript,可参考 [demo](https://b3log.org/vditor/demo/index.html) + +```html + + + +``` + +### 示例代码 + +* [官方示例](https://b3log.org/vditor/demo/index.html) / [示例源码](https://github.com/Vanessa219/b3log-index/tree/master/src/vditor) +* [CommonJS Editor](https://github.com/Vanessa219/vditor/blob/master/demo/index.js) +* [CommonJS Render](https://github.com/Vanessa219/vditor/blob/master/demo/render.js) +* [在Svelte中使用](https://github.com/HerbertHe/svelte-vditor-demo) + +### 主题 + +#### 编辑器主题 + +编辑器所展现的外观。内置classic,dark 2 套主题。 + +* 编辑器初始化时可通过 `options.theme` 设置内置主题 +* 初始化完成后可通过 `setTheme` 更新编辑器主题 +* 可通过修改 [index.less](https://github.com/Vanessa219/vditor/blob/master/src/assets/less/index.less) 中的变量对主题颜色进行定制 +* 可参考现有结构和类名在原有基础上进行修改 + +#### 内容主题 + +Markdown 输出的 HTML 所展现的外观。内置 ant-design, light,dark,wechat 4 套主题。支持内容主题扩展接口。 + +* 需在显示元素上添加 `class="vditor-reset"` +* 编辑器初始化时可通过 `options.preview.theme` 设置内置或自己开发的主题列表 +* 内容渲染初始化时可通过 `IPreviewOptions.theme` 设置内置或自己开发的主题 +* 初始化完成后可通过 `setTheme` 或 `setContentTheme` 更新内容主题 + +#### 代码主题 + +代码块所展现的外观。内置 github 等 36 套主题。 + +* 编辑器初始化时可通过 `options.preview.hljs` 对代码块样式、行号、是否启用进行设置 +* 内容渲染初始化时可通过 `IPreviewOptions.hljs` 对代码块样式、行号、是否启用进行设置 +* 初始化完成后可通过 `setTheme` 或 `setCodeTheme` 更新代码主题 + +### API + +#### id + +可填入元素 `id` 或元素自身 `HTMLElement` + +⚠️:当填入元素自身的 `HTMLElement` 时需设置 `options.cache.id` 或将 `options.cache.enable` 设置为 `false` + +#### options + +| | 说明 | 默认值 | +| - | - | - | +| i18n | 多语言,参见 ITips | - | +| undoDelay | 历史记录间隔 | - | +| after | 编辑器异步渲染完成后的回调方法 | - | +| height | 编辑器总高度 | 'auto' | +| minHeight | 编辑区域最小高度 | - | +| width | 编辑器总宽度,支持 % | 'auto' | +| placeholder | 输入区域为空时的提示 | '' | +| lang | 语言种类:en_US, fr_FR, pt_BR, ja_JP, ko_KR, ru_RU, sv_SE, zh_CN, zh_TW | 'zh_CN' | +| input(value: string) | 输入后触发 | - | +| focus(value: string) | 聚焦后触发 | - | +| blur(value: string) | 失焦后触发 | - | +| keydown(event: KeyboardEvent) | 按下后触发 | - | +| esc(value: string) | esc 按下后触发 | - | +| ctrlEnter(value: string) | ⌘/ctrl+enter 按下后触发 | - | +| select(value: string) | 编辑器中选中文字后触发 | - | +| tab | tab 键操作字符串,支持 `\t` 及任意字符串 | - | +| typewriterMode | 是否启用打字机模式 | false | +| cdn | 配置自建 CDN 地址 | `https://unpkg.com/vditor@${VDITOR_VERSION}` | +| mode | 可选模式:sv, ir, wysiwyg | 'ir' | +| debugger | 是否显示日志 | false | +| value | 编辑器初始化值 | '' | +| theme | 主题:classic, dark | 'classic' | +| icon | 图标风格:ant, material | 'ant' | +| customRenders: {language: string, render: (element: HTMLElement, vditor: IVditor) => void}[] | 自定义渲染器 | [] | + +#### options.toolbar + +* 工具栏,可使用 name 进行简写: `toolbar: ['emoji', 'br', 'bold', '|', 'line']` 。默认值参见 [src/ts/util/Options.ts](https://github.com/Vanessa219/vditor/blob/master/src/ts/util/Options.ts) +* name 可枚举为: `emoji` , `headings` , `bold` , `italic` , `strike` , `|` , `line` , `quote` , `list` , `ordered-list` , `check` ,`outdent` ,`indent` , `code` , `inline-code` , `insert-after` , `insert-before` ,`undo` , `redo` , `upload` , `link` , `table` , `record` , `edit-mode` , `both` , `preview` , `fullscreen` , `outline` , `code-theme` , `content-theme` , `export`, `devtools` , `info` , `help` , `br` +* 当 `name` 不在枚举中时,可以添加自定义按钮,格式如下: + +```js +new Vditor('vditor', { + toolbar: [ + { + hotkey: '⇧⌘S', + name: 'sponsor', + tipPosition: 's', + tip: '成为赞助者', + className: 'right', + icon: '', + click () {alert('捐赠地址:https://ld246.com/sponsor')}, + }], +}) +``` + +| | 说明 | 默认值 | +| - | - | - | +| name | 唯一标示 | - | +| icon | svg 图标 | - | +| tip | 提示 | - | +| tipPosition | 提示位置:'n', 'ne', 'nw', 's', 'se', 'sw', 'w', 'e' | - | +| hotkey | 快捷键,格式为⇧⌘//⌥⌘| - | +| suffix | 插入编辑器中的后缀 | - | +| prefix | 插入编辑器中的前缀 | - | +| click(event: Event, vditor: IVditor) | 自定义按钮点击时触发的事件 | - | +| className | 样式名 | '' | +| toolbar?: Array | 子菜单 | - | + +#### options.toolbarConfig + +| | 说明 | 默认值 | +| - | - | - | +| hide | 是否隐藏工具栏 | false | +| pin | 是否固定工具栏 | false | + +#### options.counter + +| | 说明 | 默认值 | +| - | - | - | +| enable | 是否启用计数器 | false | +| after(length: number, counter: options.counter): void | 字数统计回调 | - | +| max | 允许输入的最大值 | - | +| type | 统计类型:'markdown', 'text' | 'markdown' | + +#### options.cache + +| | 说明 | 默认值 | +| - | - | - | +| enable | 是否使用 localStorage 进行缓存 | true | +| id | 缓存 key,第一个参数为元素且启用缓存时**必填** | - | +| after(html: string): string | 缓存后的回调 | - | + +#### options.comment + +⚠️:仅支持 wysiwyg 模式 + +| | 说明 | 默认值 | +| - | - | - | +| enable | 是否启用评论模式 | false | +| add(id: string, text: string, commentsData: ICommentsData[]) | 添加评论回调 | - | +| remove(ids: string[]) | 删除评论回调 | - | +| scroll(top: number) | 滚动回调 | - | +| adjustTop(commentsData: ICommentsData[]) | 文档修改时,适配评论高度 | - | + +#### options.preview + +| | 说明 | 默认值 | +| - | - | - | +| delay | 预览 debounce 毫秒间隔 | 1000 | +| maxWidth | 预览区域最大宽度 | 800 | +| mode | 显示模式:both, editor | 'both' | +| url | md 解析请求 | - | +| parse(element: HTMLElement) | 预览回调 | - | +| transform(html: string): string | 渲染之前回调 | - | + +#### options.preview.hljs + +| | 说明 | 默认值 | +| - | - | - | +| defaultLang | 未指定语言时默认使用该语言 | '' | +| enable | 是否启用代码高亮 | true | +| style | 可选值参见[Chroma](https://xyproto.github.io/splash/docs/longer/all.html) | `github` | +| lineNumber | 是否启用行号 | false | +| langs | 自定义指定语言 | [CODE_LANGUAGES](https://github.com/Vanessa219/vditor/blob/53ca8f9a0e511b37b5dae7c6b15eb933e9e02ccd/src/ts/constants.ts#L20) | +| renderMenu(code: HTMLElement, copy: HTMLElement) | 渲染菜单按钮 | - | + +#### options.preview.markdown + +| | 说明 | 默认值 | +| - | - | - | +| autoSpace | 自动空格 | false | +| gfmAutoLink | 自动链接 | true | +| fixTermTypo | 自动矫正术语 | false | +| toc | 插入目录 | false | +| footnotes | 脚注 | true | +| codeBlockPreview | wysiwyg 和 ir 模式下是否对代码块进行渲染 | true | +| mathBlockPreview | wysiwyg 和 ir 模式下是否对数学公式进行渲染 | true | +| paragraphBeginningSpace | 段落开头空两个 | false | +| sanitize | 是否启用过滤 XSS | true | +| listStyle | 为列表添加 data-style 属性 | false | +| linkBase | 链接相对路径前缀 | '' | +| linkPrefix | 链接强制前缀 | '' | +| mark | 启用 mark 标记 | false | + +#### options.preview.theme + +| | 说明 | 默认值 | +| - | - | - | +| current | 当前主题 | "light" | +| list | 可选主题列表 | { "ant-design": "Ant Design", dark: "Dark", light: "Light", wechat: "WeChat" } | +| path | 主题样式地址 | `https://unpkg.com/vditor@${VDITOR_VERSION}/dist/css/content-theme` | + +#### options.preview.math + +| | 说明 | 默认值 | +| - | - | - | +| inlineDigit | 内联数学公式起始 $ 后是否允许数字 | false | +| macros | 使用 MathJax 渲染时传入的宏定义 | {} | +| engine | 数学公式渲染引擎:KaTeX, MathJax | 'KaTeX' | +| mathJaxOptions | 数学公式渲染引擎为 MathJax 时的参数 | - | + +#### options.preview.actions?: Array + +默认值为 ["desktop", "tablet", "mobile", "mp-wechat", "zhihu"]。 +可从默认值中挑选进行配置,也可使用以下字段进行自定制开发。 + +| | 说明 | 默认值 | +| - | - | - | +| key | 按钮唯一标识,不能为空 | - | +| text | 按钮文字 | - | +| tooltip | 提示 | - | +| className | 按钮类名 | - | +| click(key: string) | 按钮点击回调事件 | - | + +#### options.preview.render.media + +| | 说明 | 默认值 | +|--------|-----------|------| +| enable | 是否启用多媒体渲染 | true | + +#### options.image + +| | 说明 | 默认值 | +| - | - | - | +| isPreview | 是否预览图片 | true | +| preview(bom: Element) => void | 图片预览处理 | - | + +#### options.link + +| | 说明 | 默认值 | +| - | - | - | +| isOpen | 是否打开链接地址 | true | +| click(bom: Element) => void | 点击链接事件 | - | + +#### options.hint + +| | 说明 | 默认值 | +| - | - | - | +| parse | 是否进行 md 解析 | true | +| delay | 提示 debounce 毫秒间隔 | 200 | +| emoji | 默认表情,可从[lute/emoji_map](https://github.com/88250/lute/blob/master/parse/emoji_map.go) 中选取,也可自定义 | { '+1': '👍', '-1': '👎', 'heart': '❤️', 'cold_sweat': '😰' } | +| emojiTail | 常用表情提示 | - | +| emojiPath | 表情图片地址 | `https://unpkg.com/vditor@${VDITOR_VERSION}/dist/images/emoji` | +| extend: IHintExtend[] | 对 @/话题等关键字自动补全的扩展 | [] | + +```ts +interface IHintData { + html: string; + value: string; +} + +interface IHintExtend { + key: string; + + hint?(value: string): IHintData[] | Promise; +} +``` + +#### options.upload + +* 文件上传的数据结构如下。后端返回的数据结构不一致时,可使用 `format` 进行转换。 + +```js +// POST data +xhr.send(formData); // formData = FormData.append("file[]", File) +// return data +{ + "msg": "", + "code": 0, + "data": { + "errFiles": ['filename', 'filename2'], + "succMap": { + "filename3": "filepath3", + "filename3": "filepath3" + } + } +} +``` + +* 为了防止站外图片失效, `linkToImgUrl` 可将剪贴板中的站外图片地址传到服务器端进行保存处理,其数据结构如下: + +```js +// POST data +xhr.send(JSON.stringify({url: src})); // src 为站外图片地址 +// return data +{ + msg: '', + code: 0, + data : { + originalURL: '', + url: '' + } +} +``` + +* `success`,`format`,`error` 不会同时触发,具体调用情况如下: + +```js +if (xhr.status === 200) { + if (vditor.options.upload.success) { + vditor.options.upload.success(editorElement, xhr.responseText); + } else { + let responseText = xhr.responseText; + if (vditor.options.upload.format) { + responseText = vditor.options.upload.format(files as File [], xhr.responseText); + } + genUploadedLabel(responseText, vditor); + } +} else { + if (vditor.options.upload.error) { + vditor.options.upload.error(xhr.responseText); + } else { + vditor.tip.show(xhr.responseText); + } +} +``` + +| | 说明 | 默认值 | +| - | - | - | +| url | 上传 url,为空则不会触发上传相关事件 | '' | +| max | 上传文件最大 Byte | 10 * 1024 * 1024 | +| linkToImgUrl | 剪切板中包含图片地址时,使用此 url 重新上传 | '' | +| linkToImgCallback(responseText: string) | 图片地址上传回调 | - | +| linkToImgFormat(responseText: string): string | 对图片地址上传的返回值进行格式化 | - | +| success(editor: HTMLPreElement, msg: string) | 上传成功回调 | - | +| error(msg: string) | 上传失败回调 | - | +| token | CORS 上传验证,头为 X-Upload-Token | - | +| withCredentials | 跨站点访问控制 | false | +| headers | 请求头设置 | - | +| filename(name: string): string | 文件名安全处理 | name => name.replace(/\W/g, '') | +| accept | 文件上传类型,同[input accept](https://www.w3schools.com/tags/att_input_accept.asp) | - | +| validate(files: File[]) => string \| boolean | 校验,成功时返回 true 否则返回错误信息 | - | +| handler(files: File[]) => string \| null \| Promise \| Promise | 自定义上传,当发生错误时返回错误信息 | - | +| format(files: File[], responseText: string): string | 对服务端返回的数据进行转换,以满足内置的数据结构 | - | +| file(files: File[]): File[] \| Promise | 将上传的文件处理后再返回 | - | +| setHeaders(): { [key: string]: string } | 上传前使用返回值设置头 | - | +| extraData: { [key: string]: string \| Blob } | 为 FormData 添加额外的参数 | - | +| multiple | 上传文件是否为多个 | true | +| fieldName | 上传字段名称 | 'file[]' | +| renderLinkDest?(vditor: IVditor, node: ILuteNode, entering: boolean): [string, number] | 处理剪贴板中的图片地址 | '' | + +#### options.resize + +| | 说明 | 默认值 | +| - | - | - | +| enable | 是否支持大小拖拽 | false | +| position | 拖拽栏位置:'top', 'bottom' | 'bottom' | +| after(height: number) | 拖拽结束的回调 | - | + +#### options.classes + +| | 说明 | 默认值 | +| - | - | - | +| preview | 预览元素上的 className | '' | + +#### options.fullscreen + +| | 说明 | 默认值 | +| - | - | - | +| index | 全屏层级 | 90 | + +#### options.outline + +| | 说明 | 默认值 | +| - | - | - | +| enable | 初始化是否展现大纲 | false | +| position | 大纲位置:'left', 'right' | 'left' | + +#### methods + +| | 说明 | +| - | - | +| exportJSON(markdown: string) | 根据 Markdown 获取对应 JSON | +| getValue() | 获取 Markdown 内容 | +| getHTML() | 获取 HTML 内容 | +| insertValue(value: string, render = true) | 在焦点处插入内容,并默认进行 Markdown 渲染 | +| focus() | 聚焦到编辑器 | +| blur() | 让编辑器失焦 | +| disabled() | 禁用编辑器 | +| enable() | 解除编辑器禁用 | +| getSelection(): string | 返回选中的字符串 | +| setValue(markdown: string, clearStack = false) | 设置编辑器内容且选中清空历史栈 | +| clearStack() | 清空撤销和重做记录栈| +| renderPreview(value?: string) | 设置预览区域内容 | +| getCursorPosition():{top: number, left: number} | 获取焦点位置 | +| deleteValue() | 删除选中内容 | +| updateValue(value: string) | 更新选中内容 | +| isUploading() | 上传是否还在进行中 | +| clearCache() | 清除缓存 | +| disabledCache() | 禁用缓存 | +| enableCache() | 启用缓存 | +| html2md(value: string) | HTML 转 md | +| tip(text: string, time: number) | 消息提示。time 为 0 将一直显示 | +| setPreviewMode(mode: "both" \| "editor") | 设置预览模式 | +| setTheme(theme: "dark" \| "classic", contentTheme?: string, codeTheme?: string, contentThemePath?: string) | 设置主题、内容主题及代码块风格 | +| getCurrentMode(): string | 获取编辑器当前编辑模式 | +| destroy() | 销毁编辑器 | +| getCommentIds(): {id: string, top: number}[] | 获取所有评论 | +| hlCommentIds(ids: string[]) | 高亮评论 | +| unHlCommentIds(ids: string[]) | 取消评论高亮 | +| removeCommentIds(removeIds: string[]) | 删除评论 | +| updateToolbarConfig(config: {hide?: boolean, pin?: boolean}) | 更新工具栏配置 | + +#### static methods + +* 不需要进行编辑操作时,仅需引入 [`method.min.js`](https://unpkg.com/vditor/dist/) 后如下直接调用 + +```js +Vditor.mermaidRender(document) +``` + +```js +import VditorPreview from 'vditor/dist/method.min' +VditorPreview.mermaidRender(document) +``` + +* 需要对页面中的 Markdown 进行渲染时可直接调用 `preview` 方法,参数如下: + +```ts +previewElement: HTMLDivElement, // 使用该元素进行渲染 +markdown: string, // 需要渲染的 markdown 原文 +options?: IPreviewOptions { + mode: "dark" | "light"; + anchor?: number; // 为标题添加锚点 0:不渲染;1:渲染于标题前;2:渲染于标题后,默认 0 + customEmoji?: { [key: string]: string }; // 自定义 emoji,默认为 {} + lang?: (keyof II18nLang); // 语言,默认为 'zh_CN' + emojiPath?: string; // 表情图片路径 + hljs?: IHljs; // 参见 options.preview.hljs + speech?: { // 对选中后的内容进行阅读 + enable?: boolean, + }; + math?: IMath; // 数学公式渲染配置 + cdn?: string; // 自建 CDN 地址 + transform?(html: string): string; // 在渲染前进行的回调方法 + after?(); // 渲染完成后的回调 + lazyLoadImage?: string; // 设置为 Loading 图片地址后将启用图片的懒加载 + markdown?: options.preview.markdown; + theme?: options.preview.theme; + render?: options.preview.render; + renderers?: ILuteRender; // 自定义渲染 https://ld246.com/article/1588412297062 +} +``` + +* ⚠️ `method.min.js` 和 `index.min.js` 不可同时引入 + +| | 说明 | +| - | - | +| previewImage(oldImgElement: HTMLImageElement, lang: keyof II18n = "zh_CN", theme = "classic") | 点击图片预览 | +| mermaidRender(element: HTMLElement, cdn = options.cdn, theme = options.theme) | 流程图/时序图/甘特图 | +| flowchartRender(element: HTMLElement, cdn = options.cdn) | flowchart 渲染 | +| codeRender(element: HTMLElement, option?: IHljs) | 为 element 中的代码块添加复制按钮 | +| chartRender(element: (HTMLElement \| Document) = document, cdn = options.cdn, theme = options.theme) | 图表渲染 | +| mindmapRender(element: (HTMLElement \| Document) = document, cdn = options.cdn, theme = options.theme) | 脑图渲染 | +| plantumlRender(element: (HTMLElement \| Document) = document, cdn = options.cdn) | plantuml 渲染 | +| abcRender(element: (HTMLElement \| Document) = document, cdn = options.cdn) | 五线谱渲染 | +| md2html(mdText: string, options?: IPreviewOptions): Promise\ | Markdown 文本转换为 HTML,该方法需使用[异步编程](https://ld246.com/article/1546828434083?r=Vanessa#toc_h3_1) | +| preview(previewElement: HTMLDivElement, markdown: string, options?: IPreviewOptions) | 页面 Markdown 文章渲染 | +| highlightRender(hljsOption?: IHljs, element?: HTMLElement \| Document, cdn = options.cdn) | 为 element 中的代码块进行高亮渲染 | +| mediaRender(element: HTMLElement) | 为[特定链接](https://ld246.com/article/1589813914768)分别渲染为视频、音频、嵌入的 iframe | +| mathRender(element: HTMLElement, options?: {cdn?: string, math?: IMath}) | 对数学公式进行渲染 | +| speechRender(element: HTMLElement, lang?: (keyof II18nLang)) | 对选中的文字进行阅读 | +| graphvizRender(element: HTMLElement, cdn?: string) | 对 graphviz 进行渲染 | +| outlineRender(contentElement: HTMLElement, targetElement: Element) | 对大纲进行渲染 | +| lazyLoadImageRender(element: (HTMLElement \| Document) = document) | 对启用懒加载的图片进行渲染 | +| setCodeTheme(codeTheme: string, cdn = options.cdn) | 设置代码主题,codeTheme 参见 options.preview.hljs.style | +| setContentTheme(contentTheme: string, path: string) | 设置内容主题,contentTheme 参见 options.preview.theme.list | + +## 🏗 开发文档 + +### 原理相关 + +* [关于所见即所得 Markdown 编辑器的讨论](https://ld246.com/article/1579414663700) +* [Vditor 实现 Markdown 所见即所得](https://ld246.com/article/1577370404903) +* [Lute 一款对中文语境优化的 Markdown 引擎,支持 Go 和 JavaScript](https://ld246.com/article/1567047822949) + +### 环境 + +1. 安装 [node](https://nodejs.org/) LTS 版本 +2. [下载](https://github.com/Vanessa219/vditor/archive/master.zip)最新代码并解压 +3. 根目录运行 `npm install` +4. `npm run start` 启动本地服务器,打开 http://localhost:9000 +5. 修改代码 +6. `npm run build` 打包代码到 dist 目录 + +### CDN 切换 + +由于使用了按需加载的机制,默认 CDN 为 [https://unpkg.com/vditor](https://unpkg.com/vditor)@版本号 + +如果代码有修改或需要使用自建 CDN 的话,可按以下步骤进行操作: + +* 初始化时,需对 `options` 及 `IPreviewOptions` 中的 `cdn`,`emojiPath`, `themes` 进行配置 +* `highlightRender` , `mathRender` , `abcRender` , `chartRender` , `mermaidRender`, `flowchartRender`,`mindmapRender`,`graphvizRender`,`setCodeTheme`,`setContentTheme` 方法中需添加 cdn 参数 +* 将 build 成功的 dist 目录或 [jsDelivr](https://www.jsdelivr.com/package/npm/vditor?path=dist) 中的 dist 目录拷贝至正确的位置 + +### 升级 + +版本升级时请**仔细阅读** [CHANGELOG](https://github.com/Vanessa219/vditor/blob/master/CHANGELOG.md) 中的**升级**部分 + +## Ⓜ️ Markdown 使用指南 + +* [基础语法](https://ld246.com/article/1583129520165) +* [扩展语法](https://ld246.com/article/1583305480675) +* [速查手册](https://ld246.com/article/1583308420519) + +## 🏘️ 社区 + +* [官网](https://b3log.org/vditor) +* [讨论区](https://ld246.com/tag/vditor) +* [报告问题](https://github.com/Vanessa219/vditor/issues/new) + +## 📄 授权 + +Vditor 使用 [MIT](https://opensource.org/licenses/MIT) 开源协议。 + +## 🙏 鸣谢 + +* [Lute](https://github.com/88250/lute):🎼 一款结构化的 Markdown 引擎,支持 Go 和 JavaScript +* [highlight.js](https://github.com/highlightjs/highlight.js):JavaScript syntax highlighter +* [mermaid](https://github.com/knsv/mermaid):Generation of diagram and flowchart from text in a similar manner as Markdown +* [incubator-echarts](https://github.com/apache/incubator-echarts):A powerful, interactive charting and visualization library for browser +* [abcjs](https://github.com/paulrosen/abcjs):JavaScript library for rendering standard music notation in a browser + +## 📽️ 历史 + +我们在开发 [Sym](https://github.com/88250/symphony) 的初期是直接使用 WYSIWYG 富文本编辑器的。那时候基于 HTML 的编辑器非常流行,项目中引用起来也很方便,也符合用户当时的使用习惯。 + +后来,Markdown 的崛起逐步改变了大家的排版方式。再加上我们其他几个项目都是面向程序员用户的,所以迁移到 md 上也是大势所趋。我们选择了 [CodeMirror](https://github.com/codemirror/CodeMirror),这是一款优秀的编辑器,它对开发者提供了丰富的编程接口,对各种浏览器的兼容性也比较好。 + +再后来,随着我们项目业务需求方面的沉淀,使用 CodeMirror 有时候会感到比较“笨重”。比如要实现 @自动完成用户名列表、插入 Emoji、上传文件等就需要比较深入的二次开发,而这些业务需求恰恰是很多项目场景共有且必备的。 + +终于,我们决定开始在 Sym 中自己实现编辑器。随着几个版本的迭代,Sym 的编辑器也日趋成熟。在我们运营的社区[链滴](https://ld246.com)上陆续有人问我们是否能将编辑器单独抽离出来提供给大家使用。与此同时,我们的前端主程 [V](https://ld246.com/member/Vanessa) 同学对于维护分散在各个项目中的编辑器也感到有点力不从心,外加对 TypeScript 的好感,所以就决定使用 ts 来实现一个全新的浏览器端 md 编辑器。 + +于是,Vditor 就这样诞生了。 diff --git a/web/include/vditor/package.json b/web/include/vditor/package.json new file mode 100644 index 0000000..705a2af --- /dev/null +++ b/web/include/vditor/package.json @@ -0,0 +1,76 @@ +{ + "name": "vditor", + "version": "3.10.6", + "description": "♏ 易于使用的 Markdown 编辑器,为适配不同的应用场景而生", + "author": "Vanessa (http://vanessa.b3log.org)", + "homepage": "https://b3log.org/vditor", + "jsdelivr": "dist/index.min.js", + "main": "dist/index.js", + "funding": "https://ld246.com/sponsor", + "files": [ + "dist/*", + "src/index.ts", + "src/method.ts", + "src/ts/*", + "src/assets/*" + ], + "dependencies": { + "diff-match-patch": "^1.0.5" + }, + "types": "dist/index.d.ts", + "devDependencies": { + "@babel/core": "^7.25.2", + "@babel/preset-env": "^7.14.2", + "@types/diff-match-patch": "^1.0.32", + "@types/jest": "^26.0.23", + "@types/node": "^17.0.19", + "@types/puppeteer": "^2.1.5", + "@typescript-eslint/eslint-plugin": "^5.12.0", + "@typescript-eslint/parser": "^5.12.0", + "autoprefixer": "^10.4.2", + "babel-loader": "^8.2.2", + "clean-webpack-plugin": "^4.0.0-alpha.0", + "copy-webpack-plugin": "^6.4.1", + "css-loader": "^5.2.4", + "eslint": "^8.9.0", + "eslint-plugin-jest": "^26.1.1", + "eslint-plugin-prettier": "^4.0.0", + "file-loader": "^6.2.0", + "html-webpack-plugin": "^5.3.1", + "identity-obj-proxy": "^3.0.0", + "jest": "^26.6.3", + "less": "^4.1.2", + "less-loader": "^10.2.0", + "mini-css-extract-plugin": "^2.6.0", + "postcss": "^8.4.12", + "postcss-loader": "^5.2.0", + "prettier": "^2.5.1", + "puppeteer": "^23.0.2", + "style-loader": "^1.3.0", + "terser-webpack-plugin": "^5.3.0", + "ts-jest": "^26.5.6", + "ts-loader": "^6.2.2", + "typescript": "^4.9.5", + "webpack": "^5.66.0", + "webpack-bundle-analyzer": "^4.10.2", + "webpack-cli": "^4.9.1", + "webpack-dev-server": "^4.1.0" + }, + "license": "MIT", + "repository": "git://github.com/Vanessa219/vditor.git", + "bugs": { + "url": "https://github.com/Vanessa219/vditor/issues" + }, + "scripts": { + "lint": "eslint --fix --ext src/**/*.ts", + "test:watch": "jest --watch", + "test": "jest --coverage", + "start": "webpack serve --config webpack.start.js", + "build": "webpack" + }, + "keywords": [ + "editor", + "markdown", + "b3log" + ] +} diff --git a/web/include/vditor/src/assets/less/_content.less b/web/include/vditor/src/assets/less/_content.less new file mode 100644 index 0000000..77f995d --- /dev/null +++ b/web/include/vditor/src/assets/less/_content.less @@ -0,0 +1,279 @@ +@keyframes slideInDown { + from { + transform: translate3d(0, -100%, 0); + visibility: visible; + } + + to { + transform: translate3d(0, 0, 0); + } +} + +.vditor { + display: flex; + flex-direction: column; + border: 1px solid var(--border-color); + border-radius: 3px; + box-sizing: border-box; + font-family: @font-family-base; + + &--fullscreen { + position: fixed; + top: 0; + width: 100% !important; + left: 0; + height: 100vh !important; + z-index: 90; + border-radius: 0; + } + + &-content { + display: flex; + min-height: 60px; + flex: 1; + min-width: 1px; + position: relative; + } + + &-preview { + flex: 1; + min-width: 1px; + overflow: auto; + margin-left: -1px; + border-left: 1px solid var(--border-color); + box-sizing: border-box; + border-radius: 0 0 3px 0; + background-color: var(--textarea-background-color); + + &::-webkit-scrollbar { + display: none; + } + &__action { + text-align: center; + padding: 10px; + background-color: var(--toolbar-background-color); + + button { + background-color: var(--toolbar-background-color); + color: var(--toolbar-icon-color); + line-height: 20px; + border: 0; + margin: 0 10px; + cursor: pointer; + padding: 0 7px; + font-size: 12px; + + &.vditor-preview__action--current, + &:hover { + color: var(--toolbar-icon-hover-color); + background-color: var(--toolbar-background-color); + } + + &:focus { + outline: none; + } + + svg { + fill: currentColor; + height: 15px; + width: 15px; + vertical-align: middle; + } + } + } + + & > .vditor-reset { + padding: 10px; + margin: 0 auto; + } + + img:not(.emoji) { + cursor: pointer; + } + } + + &-devtools { + display: none; + background-color: var(--textarea-background-color); + overflow: auto; + flex: 1; + min-width: 1px; + box-shadow: inset 1px 0 var(--border-color); + box-sizing: border-box; + border-radius: 0 0 3px 0; + padding: 10px; + } + + &-counter { + padding: 3px; + color: var(--toolbar-icon-color); + background-color: var(--count-background-color); + border-radius: 3px; + font-size: 12px; + user-select: none; + float: right; + margin: 8px 3px 0 0; + + &--error { + color: @errorColor; + background-color: rgba(@errorColor, 0.1); + } + } + + &-resize { + padding: 3px 0; + cursor: row-resize; + user-select: none; + position: absolute; + width: 100%; + + &--top { + top: -3px; + } + + &--bottom { + bottom: -3px; + } + + & > div { + height: 3px; + background-color: var(--resize-background-color); + transition: @transition; + } + + &:hover, + &--selected { + & > div { + background-color: var(--resize-hover-background-color); + } + + svg { + color: var(--resize-hover-icon-color); + } + } + + svg { + fill: currentColor; + stroke-width: 0; + stroke: currentColor; + width: 13px; + height: 3px; + display: block; + margin: 0 auto; + color: var(--resize-icon-color); + } + } + + &-upload { + position: absolute; + height: 3px; + left: 0; + top: -2px; + transition: @transition; + background-color: @blurColor; + } + + &-tip { + position: absolute; + font-size: 12px; + top: 10px; + animation-duration: 0.15s; + animation-fill-mode: both; + left: 50%; + z-index: 5; + + &--show { + display: block; + animation-name: slideInDown; + } + + &__content { + text-align: left; + display: inline-block; + line-height: 16px; + padding: 3px 10px; + border-radius: 3px; + background: var(--toolbar-background-color); + position: relative; + margin-left: -50%; + color: var(--toolbar-icon-color); + max-width: 100%; + box-shadow: var(--panel-shadow); + + ul { + margin: 2px 0; + padding: 0 0 0 18px; + } + + a { + color: @blurColor; + } + } + + &__close { + position: absolute; + color: var(--toolbar-icon-color); + top: -7px; + right: -15px; + font-weight: bold; + cursor: pointer; + + &:hover { + color: var(--toolbar-icon-hover-color); + } + } + } + + &-img { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + flex-direction: column; + z-index: 100; + + &__bar { + border-bottom: 1px solid var(--border-color); + background-color: var(--toolbar-background-color); + text-align: center; + height: 36px; + box-sizing: border-box; + display: flex; + align-items: center; + justify-content: center; + } + + &__btn { + display: flex; + align-items: center; + cursor: pointer; + margin-left: 24px; + user-select: none; + color: var(--toolbar-icon-color); + + &:hover { + color: var(--toolbar-icon-hover-color); + } + + svg { + height: 14px; + width: 14px; + margin-right: 8px; + fill: currentColor; + } + } + + &__img { + flex: 1; + background-color: var(--textarea-background-color); + overflow: auto; + cursor: zoom-out; + + img { + max-width: none; + } + } + } +} diff --git a/web/include/vditor/src/assets/less/_hint.less b/web/include/vditor/src/assets/less/_hint.less new file mode 100644 index 0000000..d9fbbb8 --- /dev/null +++ b/web/include/vditor/src/assets/less/_hint.less @@ -0,0 +1,67 @@ +.vditor-hint { + background-color: var(--panel-background-color); + position: absolute; + box-shadow: var(--panel-shadow); + border-radius: 3px; + padding: 5px 0; + z-index: 4; + line-height: 20px; + list-style: none; + font-size: 12px; + margin: 0; + max-width: 250px; + min-width: 80px; + display: none; + + .vditor-hint { + margin-top: -31px; + left: 100%; + right: auto; + + &.vditor-panel--left { + right: 100%; + left: auto; + } + } + + button { + color: var(--toolbar-icon-color); + display: block; + padding: 3px 10px; + border: 0; + border-radius: 0; + line-height: 20px; + width: 100%; + box-sizing: border-box; + text-align: left; + margin: 0; + background-color: transparent; + cursor: pointer; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + + &:focus { + outline: none; + } + } + + &--current, + button:not(.vditor-menu--disabled):hover { + background-color: var(--toolbar-background-color) !important; + color: var(--toolbar-icon-hover-color) !important; + } + + &__emoji { + font-size: 16px; + float: left; + margin-right: 3px; + } + + img { + height: 20px; + width: 20px; + float: left; + margin-right: 3px; + } +} diff --git a/web/include/vditor/src/assets/less/_ir.less b/web/include/vditor/src/assets/less/_ir.less new file mode 100644 index 0000000..c6c4a7c --- /dev/null +++ b/web/include/vditor/src/assets/less/_ir.less @@ -0,0 +1,275 @@ +.vditor-ir { + box-sizing: border-box; + flex: 1; + min-width: 1px; + position: relative; + width: 100%; + + &__node { + &[data-type="code-block"]:before, + &[data-type="code-block"]:after, + &[data-type="yaml-front-matter"]:before, + &[data-type="yaml-front-matter"]:after, + &[data-type="math-block"]:before, + &[data-type="math-block"]:after { + content: ' '; + color: var(--second-color); + } + + &:not(.vditor-ir__node--expand) .vditor-ir__marker { + padding: 0 !important; + } + + &:not(.vditor-ir__node--expand)[data-type="a"] { + cursor: pointer; + } + + &[data-type="link-ref"], + &[data-type="footnotes-ref"] { + color: @blurColor; + } + + &[data-type="html-block"] { + margin-bottom: 1em; + } + + .vditor-ir__marker { + width: 0; + overflow: hidden; + display: inline-block; + height: 0; + transition: @transition; + } + + &--hidden { + .vditor-ir__marker { + visibility: hidden; + } + } + + &--expand { + .vditor-ir__marker { + color: var(--second-color); + display: inline; + height: auto; + width: auto; + + &--hide { + display: none; + } + + &--heading { + color: var(--ir-heading-color); + } + + &--bi { + color: var(--ir-bi-color); + } + + &--link { + color: var(--ir-link-color); + } + + &--title { + color: var(--ir-title-color); + } + + &--bracket { + color: var(--ir-bracket-color); + text-decoration: underline; + } + + &--paren { + color: var(--ir-paren-color); + } + + &--info { + color: var(--ir-heading-color); + } + + &--pre code { + color: var(--textarea-text-color); + height: auto; + text-align: left; + } + } + + &[data-type="code-block"]:before, + &[data-type="code-block"]:after { + content: '```'; + } + + &[data-type="yaml-front-matter"]:before, + &[data-type="yaml-front-matter"]:after { + content: '---'; + } + + &[data-type="math-block"]:before, + &[data-type="math-block"]:after { + content: '$$'; + } + } + + span[data-type="code-block-open-marker"], + span[data-type="code-block-close-marker"], + span[data-type="yaml-front-matter-open-marker"], + span[data-type="yaml-front-matter-close-marker"], + span[data-type="math-block-open-marker"], + span[data-type="math-block-close-marker"] { + display: none; + } + } + + &__preview { + cursor: pointer; + white-space: initial; + min-height: 27px; + } + + &__link { + color: var(--ir-bracket-color); + text-decoration: underline; + } + + pre.vditor-reset { + background-color: var(--panel-background-color); + margin: 0; + white-space: pre-wrap; + height: 100%; + box-sizing: border-box; + + &[contenteditable="false"] { + opacity: 0.3; + cursor: not-allowed; + } + + &:empty::before { + content: attr(placeholder); + color: var(--second-color); + } + + &:focus { + outline: none; + background-color: var(--textarea-background-color); + } + + &:after { + content: ""; + height: var(--editor-bottom); + display: block; + } + + pre { + margin: 0; + } + } + + hr { + display: inline-block; + margin: 12px 0; + width: 100%; + } + + blockquote:empty::before, + pre > code:empty::before, + p:empty::before, + h1:empty::after, + h2:empty::after, + h3:empty::after, + h4:empty::after, + h5:empty::after, + h6:empty::after { + content: ' '; + } + + // block title + & .vditor-reset > h1:before, + & .vditor-reset > h2:before, + & .vditor-reset > h3:before, + & .vditor-reset > h4:before, + & .vditor-reset > h5:before, + & .vditor-reset > h6:before, + div[data-type="link-ref-defs-block"]:before, + div[data-type="footnotes-block"]:before, + .vditor-toc:before { + float: left; + padding-right: 4px; + margin-left: -29px; + content: 'H1'; + font-size: 0.85rem; + font-weight: normal; + color: var(--second-color); + } + + & .vditor-reset > h2:before { + content: 'H2'; + } + + & .vditor-reset > h3:before { + content: 'H3'; + } + + & .vditor-reset > h4:before { + content: 'H4'; + } + + & .vditor-reset > h5:before { + content: 'H5'; + } + + & .vditor-reset > h6:before { + content: 'H6'; + } + + div[data-type="link-ref-defs-block"] { + &:before { + content: '"A"'; + } + + color: var(--blockquote-color); + } + + div[data-type="footnotes-block"] { + &:before { + content: '^F'; + } + + border-top: 2px solid var(--heading-border-color); + padding-top: 24px; + margin-top: 24px; + + & > div[data-type="footnotes-def"] { + & > ul, & > ol, & > p, & > blockquote, & > pre, & > table, & > hr { + margin-left: 8px; + } + } + } + + + .vditor-toc { + &:before { + content: "ToC"; + } + + span[data-type="toc-h"] { + color: @blurColor; + text-decoration: underline; + } + } +} + +@media screen and (max-width: @max-width) { + .vditor-ir { + h1:before, + h2:before, + h3:before, + h4:before, + h5:before, + h6:before, + div[data-type="link-ref-defs-block"]:before, + div[data-type="footnotes-block"]:before, + .vditor-toc:before { + content: none; + } + } +} diff --git a/web/include/vditor/src/assets/less/_panel.less b/web/include/vditor/src/assets/less/_panel.less new file mode 100644 index 0000000..3304d12 --- /dev/null +++ b/web/include/vditor/src/assets/less/_panel.less @@ -0,0 +1,110 @@ +/** + * panel. + * + * @author Liyuan Li + * @version 1.0.0.0, Jan 22, 2020 + */ + +@keyframes scale-in { + 0% { + opacity: 0; + transform: scale(0.5); + } + 100% { + opacity: 1; + transform: scale(1); + } +} + +.vditor { + &-panel { + background-color: var(--panel-background-color); + position: absolute; + box-shadow: var(--panel-shadow); + border-radius: 3px; + padding: 5px; + z-index: 3; + font-size: 14px; + display: none; + user-select: none; + max-width: 320px; + min-width: 80px; + animation-duration: 0.15s; + animation-name: scale-in; + animation-timing-function: cubic-bezier(0.2, 0, 0.13, 1.5); + color: var(--toolbar-icon-color); + + &--none { + padding: 0; + animation: none; + min-width: auto; + max-width: none; + white-space: nowrap; + opacity: 0.86; + } + + &--arrow:before { + position: absolute; + width: 0; + height: 0; + pointer-events: none; + content: " "; + border: 7px solid transparent; + top: -14px; + left: 5px; + border-bottom-color: var(--panel-background-color); + } + + &--left { + right: 0; + + &.vditor-panel--arrow:before { + right: 5px; + left: auto; + } + } + } + + &-input { + border: 0; + padding: 3px 5px; + background-color: var(--panel-background-color); + font-size: 12px; + color: var(--textarea-text-color); + + &:focus { + background-color: var(--toolbar-background-color); + outline: none; + } + } + + &-icon { + color: var(--toolbar-icon-color); + cursor: pointer; + float: left; + padding: 4px 5px; + height: 21px; + width: 23px; + background-color: transparent; + border: 0; + box-sizing: border-box; + + &:hover, + &--current { + color: var(--toolbar-icon-hover-color); + background-color: transparent; + } + + &:focus { + outline: none; + } + + svg { + height: 13px !important; + width: 13px !important; + float: left; + fill: currentColor; + pointer-events: none; + } + } +} diff --git a/web/include/vditor/src/assets/less/_reset.less b/web/include/vditor/src/assets/less/_reset.less new file mode 100644 index 0000000..a9778e5 --- /dev/null +++ b/web/include/vditor/src/assets/less/_reset.less @@ -0,0 +1,610 @@ +.vditor { + &-reset { + &--anchor { + padding-left: 20px; + } + + &--error { + color: @errorColor; + font-size: 12px; + display: block; + line-height: 16px; + } + + color: #24292e; + font-variant-ligatures: no-common-ligatures; + font-family: @font-family-base; + word-wrap: break-word; + overflow: auto; + line-height: 1.5; + font-size: 16px; + word-break: break-word; + + ul ul ul { + list-style-type: square; + } + + ul ul { + list-style-type: circle; + } + + ul { + list-style-type: disc; + } + + ul, + ol { + padding-left: 2em; + margin-top: 0; + margin-bottom: 16px; + } + + li + li { + margin-top: 0.25em; + } + + li p { + margin-top: 16px; + } + + audio { + max-width: 100%; + + &:focus { + outline: none; + } + } + + video { + max-height: 90vh; + max-width: 100%; + } + + img { + max-width: 100%; + } + + img.emoji { + cursor: auto; + max-width: 20px; + vertical-align: sub; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + margin-top: 24px; + margin-bottom: 16px; + font-weight: 600; + line-height: 1.25; + + &:hover .vditor-anchor svg { + visibility: visible; + } + } + + h1 { + font-size: 1.75em; + } + + h2 { + font-size: 1.55em; + } + + h3 { + font-size: 1.38em; + } + + h4 { + font-size: 1.25em; + } + + h5 { + font-size: 1.13em; + } + + h6 { + font-size: 1em; + } + + hr { + height: 2px; + padding: 0; + margin: 24px 0; + background-color: #eaecef; + border: 0; + } + + p { + margin-top: 0; + margin-bottom: 16px; + } + + blockquote { + padding: 0 1em; + color: #6a737d; + border-left: 0.25em solid #eaecef; + margin: 0 0 16px 0; + + & > :first-child { + margin-top: 0; + } + + & > :last-child { + margin-bottom: 0; + } + } + + ins > iframe { + border: 0; + } + + iframe { + border: 1px solid #d1d5da; + max-width: 100%; + box-sizing: border-box; + + &.iframe__video { + min-width: 80%; + min-height: 36vh; + } + } + + table { + border-collapse: collapse; + empty-cells: show; + margin-bottom: 16px; + overflow: auto; + border-spacing: 0; + display: block; + word-break: keep-all; + width: 100%; + + tr { + background-color: #fafbfc; + border-top: 1px solid #c6cbd1; + } + + td, + th { + padding: 6px 13px; + border: 1px solid #dfe2e5; + word-break: normal; + white-space: nowrap; + + &:first-child::after { + content: ""; + display: inline-block; + vertical-align: top; + min-height: 24px; + } + } + + th { + font-weight: 600; + } + + tbody tr:nth-child(2n) { + background-color: #fff; + } + } + + code:not(.hljs):not(.highlight-chroma) { + padding: 0.2em 0.4em; + margin: 0; + font-size: 85%; + border-radius: 3px; + font-family: @font-family-code; + word-break: break-word; + background-size: 20px 20px; + white-space: pre-wrap; + } + + pre { + margin: 1em 0; + + & > code { + margin: 0; + font-size: 85%; + padding: 0.5em; + border-radius: 5px; + display: block; + overflow: auto; + white-space: pre; + font-family: @font-family-code; + background-size: 20px 20px; + background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAA8AgMAAABHkjHhAAAACVBMVEWAgIBaWlo+Pj7rTFvWAAAAA3RSTlMHCAw+VhR4AAAA+klEQVQoz4WSMW7EQAhFPxKWNh2FCx+HkaZI6RRb5DYbyVfIJXLKDCFoMbaTKSw/8ZnPAPjaH2xgZcUNUDADD7D9LtDBCLZ45fbkvo/30K8yeI64pPwl6znd/3n/Oe93P3ho9qeh72btTFzqkz0rsJle8Zr81OLEwZ1dv/713uWqvu2pl+k0fy7MWtj9r/tN5q/02z89qa/L4Dc2LvM93kezPfXlME/O86EbY/V9GB9ePX8G1/6W+/9h1dq/HGfTfzT3j/xNo7522Bfnqe5jO/fvhVthlfk434v3iO9zG/UOphyPeinPl1J8Gtaa7xPTa/Dk+RIs4deMvwGvcGsmsCvJ0AAAAABJRU5ErkJggg==); + word-break: initial; + word-wrap: normal; + } + + &:hover div.vditor-copy { + display: block; + } + } + + .language-math, + .language-echarts, + .language-mindmap, + .language-plantuml, + .language-mermaid, + .language-markmap, + .language-abc, + .language-flowchart, + .language-graphviz { + margin-bottom: 16px; + } + + .language-math mjx-container:focus { + outline: none; + cursor: context-menu; + } + + .language-echarts, + .language-mindmap { + overflow: hidden; + height: 420px; + } + + .language-mermaid, + .language-markmap, + .language-flowchart, + .language-graphviz { + text-align: center; + } + + .language-graphviz parsererror { + overflow: auto; + } + + kbd { + display: inline-block; + padding: 3px 5px; + font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace; + line-height: 10px; + color: #24292e; + vertical-align: middle; + background-color: #fafbfc; + border: solid 1px #d1d5da; + border-radius: 3px; + box-shadow: inset 0 -1px 0 #d1d5da; + } + + summary { + cursor: pointer; + + &:focus { + outline: none; + } + } + + svg { + height: auto; + width: auto; + stroke-width: initial; + } + + p:last-child, + blockquote:last-child, + pre:last-child, + ul:last-child, + ol:last-child, + hr:last-child { + margin-bottom: 0; + } + } + + &-comment { + border-bottom: 2px solid rgb(248, 230, 171); + + &--focus, + &--hover { + background-color: rgb(250, 241, 209); + border-bottom: 2px solid rgb(255, 198, 10); + + .vditor-comment { + border-bottom: 2px solid rgb(255, 198, 10); + } + } + } + + &-task { + list-style: none !important; + word-break: break-all; + + input { + margin: 0 0.2em 0.25em -1.6em; + font-size: 12px; + vertical-align: middle; + } + } + + &-copy { + position: relative; + display: none; + z-index: 1; + + textarea { + position: absolute; + left: -100000px; + height: 10px; + } + + span { + cursor: pointer; + position: absolute; + right: 15px; + top: 0.5em; + } + + svg { + color: #586069; + height: 14px; + width: 14px !important; + display: block; + fill: currentColor; + } + } + + &-linenumber { + padding-left: 4em !important; + position: relative; + + &__rows { + position: absolute; + pointer-events: none; + top: 0.5em; + left: 0; + width: 3em; /* works for line-numbers below 1000 lines */ + user-select: none; + counter-reset: linenumber; + + & > span { + pointer-events: none; + display: block; + + &::before { + counter-increment: linenumber; + content: counter(linenumber); + color: rgba(158, 150, 150, 0.38); + display: block; + padding-right: 1em; + text-align: right; + } + } + } + } + + &-speech { + position: absolute; + display: none; + background-color: #f6f8fa; + border: 1px solid #d1d5da; + border-radius: 3px; + padding: 3px; + cursor: pointer; + color: #586069; + + &:hover, + &--current { + color: #4285f4; + } + + svg { + height: 14px; + width: 14px; + fill: currentColor; + display: block; + stroke-width: 0; + stroke: currentColor; + } + } + + &-anchor { + margin-left: 5px; + + &--left { + float: left; + padding-right: 4px; + margin-left: -20px; + } + + svg { + visibility: hidden; + } + + &:hover { + svg { + visibility: visible; + } + } + + &:focus { + outline: none; + } + } + + &-linkcard { + margin: 31px auto 16px; + transition: @transition; + cursor: pointer; + max-width: 768px; + padding: 0 10px; + + a { + border-radius: 3px; + background-color: #f6f8fa; + overflow: hidden; + max-height: 250px; + display: flex; + text-decoration: none; + flex-wrap: wrap-reverse; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + + &:hover { + box-shadow: 0 0 3px rgba(0, 0, 0, 0.13), 0 3px 6px rgba(0, 0, 0, 0.26); + text-decoration: none; + } + + &:visited .vditor-linkcard__abstract { + color: rgba(88, 96, 105, 0.36); + } + } + + &__info { + padding: 10px; + min-width: 200px; + box-sizing: border-box; + flex: 1; + } + + &__title { + font-size: 14px; + font-weight: 400; + color: #24292e; + display: flex; + align-items: center; + + img { + cursor: pointer; + height: 20px; + width: 20px; + border-radius: 3px; + flex-shrink: 0; + margin-right: 5px; + } + } + + &__abstract { + word-wrap: break-word; + word-break: break-all; + -webkit-line-clamp: 2; + overflow: hidden; + text-overflow: ellipsis; + -webkit-box-orient: vertical; + display: -webkit-box; + font-size: 13px; + color: #586069; + margin: 5px 0; + } + + &__site { + font-size: 12px; + color: #4285f4; + } + + &__image { + background-size: cover; + background-repeat: no-repeat; + background-position: center center; + max-width: 250px; + min-width: 126px; + cursor: pointer; + background-color: rgba(88, 96, 105, 0.36); + } + } + + &-footnotes__goto-ref { + text-decoration: none; + } + + &-toc { + margin-bottom: 16px; + user-select: text; + color: @blurColor; + + .vditor-outline__action { + display: none; + } + + ul { + list-style: none !important; + padding-left: 1em; + } + + & > ul { + padding-left: 0; + } + + span { + cursor: pointer; + } + + li > span > svg { + width: 0; + height: 0; + } + } + + &-outline { + width: 250px; + border-right: 1px solid var(--border-color); + background-color: var(--panel-background-color); + display: none; + overflow: auto; + + &--right { + border-right: 0; + border-left: 1px solid var(--border-color); + } + + &::-webkit-scrollbar { + display: none; + } + + ul { + list-style: none !important; + padding-left: 1em; + margin: 0; + } + + &__content > ul { + padding-left: 0; + } + + li > span { + display: flex; + align-items: center; + padding: 5px 10px; + cursor: pointer; + color: var(--textarea-text-color); + + & > svg { + height: 10px; + width: 10px; + flex-shrink: 0; + } + + &:hover { + color: var(--toolbar-icon-hover-color); + } + + & > span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + + &__title { + border-bottom: 1px dashed var(--border-color); + padding: 5px 10px; + color: var(--toolbar-icon-color); + font-size: 12px; + } + + &__action { + transition: @transition; + fill: currentColor; + margin-right: 5px; + flex-shrink: 0; + + &--close { + transform: rotate(-90deg); + } + } + } +} diff --git a/web/include/vditor/src/assets/less/_sv.less b/web/include/vditor/src/assets/less/_sv.less new file mode 100644 index 0000000..4440b6a --- /dev/null +++ b/web/include/vditor/src/assets/less/_sv.less @@ -0,0 +1,132 @@ +.vditor-sv { + font-family: @font-family-base; + margin: 0 1px 0 0; + overflow: auto; + width: 100%; + flex: 1; + min-width: 1px; + border: 0; + resize: none; + padding: 10px 9px 10px 10px; + box-sizing: border-box; + background-color: var(--panel-background-color); + outline: 0 none; + font-size: 16px; + line-height: 22px; + color: var(--textarea-text-color); + border-radius: 0 0 3px 3px; + font-variant-ligatures: no-common-ligatures; + white-space: pre-wrap; + word-break: break-word; + word-wrap: break-word; + + &[contenteditable="false"] { + opacity: 0.3; + cursor: not-allowed; + } + + &:empty::before { + content: attr(placeholder); + color: var(--second-color); + } + + &:focus { + background-color: var(--textarea-background-color); + } + + &:after { + content: ""; + height: var(--editor-bottom); + display: block; + } + + span[data-type="newline"] + span[data-type="text"]:empty { + display: inherit; + } + + .sup { + vertical-align: super; + font-size: smaller; + } + + .strong { + font-weight: bold; + } + + .em { + font-style: italic; + } + + .s { + text-decoration: line-through; + } + + .mark:not(.vditor-sv__marker) { + background-color: yellow; + color: black; + } + + .h1 { + font-size: 1.75em; + line-height: 44px; + } + + .h2 { + font-size: 1.55em; + line-height: 38px; + } + + .h3 { + font-size: 1.38em; + line-height: 27px; + } + + .h4 { + font-size: 1.25em; + line-height: 25px; + } + + .h5 { + font-size: 1.13em; + } + + .h6 { + font-size: 1em; + } + + &__marker { + color: var(--second-color); + + &--heading { + color: var(--ir-heading-color); + } + + &--bi { + color: var(--ir-bi-color); + } + + &--link { + color: var(--ir-link-color); + } + + &--title { + color: var(--ir-title-color); + } + + &--bracket { + color: var(--ir-bracket-color); + } + + &--paren { + color: var(--ir-paren-color); + } + + &--info { + color: var(--ir-heading-color); + } + + &--strong { + font-weight: bold; + } + } +} diff --git a/web/include/vditor/src/assets/less/_toolbar.less b/web/include/vditor/src/assets/less/_toolbar.less new file mode 100644 index 0000000..0b60da5 --- /dev/null +++ b/web/include/vditor/src/assets/less/_toolbar.less @@ -0,0 +1,180 @@ +.vditor { + &-toolbar { + background-color: var(--toolbar-background-color); + border-bottom: 1px solid var(--border-color); + padding: 0 5px; + line-height: 1; + + &--pin { + position: sticky; + top: 0; + z-index: 1; + } + + &--hide { + transition: @transition; + height: 5px; + overflow: hidden; + + &:hover { + background-color: var(--toolbar-background-color); + height: auto; + overflow: visible; + } + } + + &__item { + float: left; + position: relative; + + .vditor-tooltipped { + color: var(--toolbar-icon-color); + border: 0; + padding: 10px 5px; + background-color: transparent; + height: var(--toolbar-height); + width: 25px; + box-sizing: border-box; + font-size: 0; + + &:focus { + outline: none; + } + + &:focus { + cursor: pointer; + color: var(--toolbar-icon-hover-color); + } + } + + svg { + fill: currentColor; + display: inline-block; + stroke-width: 0; + stroke: currentColor; + width: 15px; + height: 15px; + } + + input { + position: absolute; + width: 25px; + height: var(--toolbar-height); + top: 0; + left: 0; + cursor: pointer; + opacity: .001; + overflow: hidden; + } + } + + &__divider { + float: left; + height: calc(var(--toolbar-height) - (var(--toolbar-divider-margin-top) * 2)); + border-left: 1px solid var(--second-color); + margin: var(--toolbar-divider-margin-top) 8px; + } + + &__br { + width: 100%; + padding: 0 !important; + height: 0 !important; + } + } + + &-menu { + &--current { + color: var(--toolbar-icon-hover-color) !important; + } + + &--disabled { + color: var(--second-color) !important; + cursor: not-allowed !important; + } + } + + &-emojis { + display: inline-block; + overflow: auto; + + &::-webkit-scrollbar { + display: none; + } + + &__tip { + flex: 1; + min-width: 1px; + width: 200px; + margin-right: 10px; + color: var(--toolbar-icon-color); + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + + &__tail { + margin-top: 5px; + font-size: 12px; + color: var(--toolbar-icon-color); + display: flex; + + a { + text-decoration: none; + color: var(--toolbar-icon-color); + + &:hover { + color: var(--toolbar-icon-hover-color); + } + } + } + + button { + cursor: pointer; + border-radius: 3px; + float: left; + height: 30px; + width: 30px; + text-align: center; + line-height: 26px; + padding: 3px; + box-sizing: border-box; + font-size: 16px; + transition: @transition; + border: 0; + margin: 0; + background-color: transparent; + overflow: hidden; + + &:focus { + outline: none; + } + + &:hover .vditor-emojis__icon { + display: inline-block; + transform: scale(1.2); + } + } + + img { + height: 20px; + width: 20px; + float: left; + margin: 3px 0 0 3px; + } + } +} + +@media screen and (max-width: @max-width) { + .vditor-toolbar__item { + padding: 0 12px; + } + .vditor-panel--left.vditor-panel--arrow:before { + right: 17px; + } +} + +@media(hover: hover) and (pointer: fine) { + .vditor-toolbar__item .vditor-tooltipped:hover { + color: var(--toolbar-icon-hover-color); + } +} diff --git a/web/include/vditor/src/assets/less/_tooltipped.less b/web/include/vditor/src/assets/less/_tooltipped.less new file mode 100644 index 0000000..087a880 --- /dev/null +++ b/web/include/vditor/src/assets/less/_tooltipped.less @@ -0,0 +1,171 @@ +/** + * tool tip. + * + * @author Liyuan Li + * @version 0.1.0.0, Jan 25, 2019 + */ +@keyframes tooltip-appear { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +.vditor-tooltipped { + position: relative; + cursor: pointer; + + &::after { + position: absolute; + z-index: 1000000; + display: none; + padding: 5px 8px; + font-size: 11px; + font-weight: normal; + -webkit-font-smoothing: subpixel-antialiased; + color: #fff; + text-align: center; + text-decoration: none; + text-shadow: none; + text-transform: none; + letter-spacing: normal; + word-wrap: break-word; + white-space: pre; + pointer-events: none; + content: attr(aria-label); + background: #3b3e43; + border-radius: 3px; + line-height: 16px; + opacity: 0; + } + + &::before { + position: absolute; + z-index: 1000001; + display: none; + width: 0; + height: 0; + color: #3b3e43; + pointer-events: none; + content: ""; + border: 5px solid transparent; + opacity: 0; + } + + &--hover::before, + &--hover::after, + &:hover::before, + &:hover::after, + &:active::before, + &:active::after, + &:focus::before, + &:focus::after { + display: inline-block; + text-decoration: none; + animation-name: tooltip-appear; + animation-duration: 0.15s; + animation-fill-mode: forwards; + animation-timing-function: ease-in; + } + + &__s::after, + &__se::after, + &__sw::after { + top: 100%; + right: 50%; + margin-top: 5px; + } + + &__s::before, + &__se::before, + &__sw::before { + top: auto; + right: 50%; + bottom: -5px; + margin-right: -5px; + border-bottom-color: #3b3e43; + } + + &__se::after { + right: auto; + left: 50%; + margin-left: -15px; + } + + &__sw::after { + margin-right: -15px; + } + + &__n::after, + &__ne::after, + &__nw::after { + right: 50%; + bottom: 100%; + margin-bottom: 5px; + } + + &__n::before, + &__ne::before, + &__nw::before { + top: -5px; + right: 50%; + bottom: auto; + margin-right: -5px; + border-top-color: #3b3e43; + } + + &__ne::after { + right: auto; + left: 50%; + margin-left: -15px; + } + + &__nw::after { + margin-right: -15px; + } + + &__s::after, + &__n::after { + transform: translateX(50%); + } + + &__w::after { + right: 100%; + bottom: 50%; + margin-right: 5px; + transform: translateY(50%); + } + + &__w::before { + top: 50%; + bottom: 50%; + left: -5px; + margin-top: -5px; + border-left-color: #3b3e43; + } + + &__e::after { + bottom: 50%; + left: 100%; + margin-left: 5px; + transform: translateY(50%); + } + + &__e::before { + top: 50%; + right: -5px; + bottom: 50%; + margin-top: -5px; + border-right-color: #3b3e43; + } +} + +@media screen and (max-width: @max-width) { + .vditor-tooltipped:before, + .vditor-tooltipped:after { + content: none; + } +} diff --git a/web/include/vditor/src/assets/less/_wysiwyg.less b/web/include/vditor/src/assets/less/_wysiwyg.less new file mode 100644 index 0000000..782501e --- /dev/null +++ b/web/include/vditor/src/assets/less/_wysiwyg.less @@ -0,0 +1,195 @@ +.vditor-wysiwyg { + box-sizing: border-box; + flex: 1; + position: relative; + width: 100%; + min-width: 1px; + + pre.vditor-reset { + background-color: var(--panel-background-color); + margin: 0; + white-space: pre-wrap; + height: 100%; + box-sizing: border-box; + + &[contenteditable="false"] { + opacity: 0.3; + cursor: not-allowed; + } + + &:empty::before { + content: attr(placeholder); + color: var(--second-color); + } + + &:focus { + outline: none; + background-color: var(--textarea-background-color); + } + + &:after { + content: ""; + height: var(--editor-bottom); + display: block; + } + } + + blockquote:empty::before, + pre > code:empty::before, + p:empty::before, + h1:empty::after, + h2:empty::after, + h3:empty::after, + h4:empty::after, + h5:empty::after, + h6:empty::after { + content: ' '; + } + + code[data-marker="`"] { + padding-left: 0 !important; + padding-right: 0 !important; + } + + &__block { + pre:first-child { + margin-bottom: -1em; + + code { + height: auto; + color: var(--textarea-text-color); + height: auto; + text-align: left; + } + } + + pre:last-child { + margin-bottom: 1em; + } + } + + &__preview { + cursor: pointer; + white-space: initial; + min-height: 27px; + } + + & > .vditor-reset > h1:before, + & > .vditor-reset > h2:before, + & > .vditor-reset > h3:before, + & > .vditor-reset > h4:before, + & > .vditor-reset > h5:before, + & > .vditor-reset > h6:before, + div.vditor-wysiwyg__block:before, + div[data-type="link-ref-defs-block"]:before, + div[data-type="footnotes-block"]:before, + .vditor-toc:before { + float: left; + padding-right: 4px; + margin-left: -29px; + content: 'H1'; + font-size: 0.85rem; + font-weight: normal; + color: var(--second-color); + } + + & > .vditor-reset > h2:before { + content: 'H2'; + } + + & > .vditor-reset > h3:before { + content: 'H3'; + } + + & > .vditor-reset > h4:before { + content: 'H4'; + } + + & > .vditor-reset > h5:before { + content: 'H5'; + } + + & > .vditor-reset > h6:before { + content: 'H6'; + } + + div[data-type="link-ref-defs-block"]:before { + content: '"A"'; + } + + div[data-type="footnotes-block"]:before { + content: '^F'; + } + + div.vditor-wysiwyg__block:before { + content: ""; + } + + div.vditor-wysiwyg__block[data-type="yaml-front-matter"]:before { + content: "F"; + } + + div.vditor-wysiwyg__block[data-type="math-block"]:before { + content: "$$"; + } + + .vditor-toc:before { + content: "ToC"; + } + + hr { + display: inline-block; + margin: 12px 0; + width: 100%; + } + + details { + white-space: initial; + } + + a { + cursor: pointer; + } + + span[data-type="backslash"] > span { + display: none; + color: var(--second-color); + } + + span[data-type="link-ref"], + sup[data-type="footnotes-ref"] { + color: @blurColor; + } + + span[data-type="toc-h"] { + color: @blurColor; + text-decoration: underline; + } + + div[data-type="footnotes-block"] { + border-top: 2px solid var(--heading-border-color); + padding-top: 24px; + margin-top: 24px; + } + + div[data-type="link-ref-defs-block"] { + color: var(--blockquote-color); + } +} + +@media screen and (max-width: @max-width) { + .vditor-wysiwyg { + h1:before, + h2:before, + h3:before, + h4:before, + h5:before, + h6:before, + div.vditor-wysiwyg__block:before, + div[data-type="link-ref-defs-block"]:before, + div[data-type="footnotes-block"]:before, + .vditor-toc:before { + content: none; + } + } +} diff --git a/web/include/vditor/src/assets/less/index.less b/web/include/vditor/src/assets/less/index.less new file mode 100644 index 0000000..0f42005 --- /dev/null +++ b/web/include/vditor/src/assets/less/index.less @@ -0,0 +1,88 @@ +@errorColor: #d23f31; +@blurColor: #4285f4; + +@font-family-base: "Helvetica Neue", "Luxi Sans", "DejaVu Sans", + "Hiragino Sans GB", "Microsoft Yahei", sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", "Noto Color Emoji", "Segoe UI Symbol", "Android Emoji", + "EmojiSymbols"; +@font-family-code: mononoki, Consolas, "Liberation Mono", Menlo, Courier, + monospace, "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", + "Segoe UI Symbol", "Android Emoji", "EmojiSymbols"; + +@transition: all 0.15s ease-in-out; + +@max-width: 520px; + +.vditor { + --border-color: #d1d5da; + --second-color: rgba(88, 96, 105, 0.36); + + --panel-background-color: #fff; + --panel-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); + + --toolbar-background-color: #f6f8fa; + --toolbar-icon-color: #586069; + --toolbar-icon-hover-color: @blurColor; + --toolbar-height: 35px; + --toolbar-divider-margin-top: 8px; + + --textarea-background-color: #fafbfc; + --textarea-text-color: #24292e; + + --resize-icon-color: var(--toolbar-icon-color); + --resize-background-color: var(--toolbar-background-color); + --resize-hover-icon-color: var(--panel-background-color); + --resize-hover-background-color: var(--toolbar-icon-hover-color); + + --count-background-color: rgba(27, 31, 35, 0.05); + --heading-border-color: #eaecef; + --blockquote-color: #6a737d; + + --ir-heading-color: #660e7a; + --ir-title-color: #808080; + --ir-bi-color: #0033b3; + --ir-link-color: #008000; + --ir-bracket-color: #0000ff; + --ir-paren-color: #008000; + + &--dark { + --border-color: #141414; + --second-color: rgba(185, 185, 185, 0.36); + + --panel-background-color: #24292e; + --panel-shadow: 0 1px 2px rgba(255, 255, 255, 0.2); + + --toolbar-background-color: #1d2125; + --toolbar-icon-color: #b9b9b9; + --toolbar-icon-hover-color: #fff; + + --textarea-background-color: #2f363d; + --textarea-text-color: #d1d5da; + + --resize-icon-color: var(--border-color); + --resize-background-color: var(--second-color); + --resize-hover-icon-color: var(--toolbar-icon-hover-color); + --resize-hover-background-color: rgba(185, 185, 185, 0.86); + + --count-background-color: rgba(@blurColor, 0.36); + --heading-border-color: var(--textarea-text-color); + --blockquote-color: var(--toolbar-icon-color); + + --ir-heading-color: #9876aa; + --ir-title-color: #808080; + --ir-bi-color: #cc7832; + --ir-link-color: #ffc66d; + --ir-bracket-color: #287bde; + --ir-paren-color: #6a8759; + } +} + +@import "_tooltipped"; +@import "_panel"; +@import "_toolbar"; +@import "_content"; +@import "_hint"; +@import "_reset"; +@import "_wysiwyg"; +@import "_ir"; +@import "_sv"; diff --git a/web/include/vditor/src/index.ts b/web/include/vditor/src/index.ts new file mode 100644 index 0000000..574f9e2 --- /dev/null +++ b/web/include/vditor/src/index.ts @@ -0,0 +1,553 @@ +import "./assets/less/index.less"; +import VditorMethod from "./method"; +import {Constants, VDITOR_VERSION} from "./ts/constants"; +import {DevTools} from "./ts/devtools/index"; +import {Hint} from "./ts/hint/index"; +import {IR} from "./ts/ir/index"; +import {input as irInput} from "./ts/ir/input"; +import {processAfterRender} from "./ts/ir/process"; +import {getHTML} from "./ts/markdown/getHTML"; +import {getMarkdown} from "./ts/markdown/getMarkdown"; +import {setLute} from "./ts/markdown/setLute"; +import {Outline} from "./ts/outline/index"; +import {Preview} from "./ts/preview/index"; +import {Resize} from "./ts/resize/index"; +import {Editor} from "./ts/sv/index"; +import {inputEvent} from "./ts/sv/inputEvent"; +import {processAfterRender as processSVAfterRender, processPaste} from "./ts/sv/process"; +import {Tip} from "./ts/tip/index"; +import {Toolbar} from "./ts/toolbar/index"; +import {disableToolbar, hidePanel} from "./ts/toolbar/setToolbar"; +import {enableToolbar} from "./ts/toolbar/setToolbar"; +import {initUI, UIUnbindListener} from "./ts/ui/initUI"; +import {setCodeTheme} from "./ts/ui/setCodeTheme"; +import {setContentTheme} from "./ts/ui/setContentTheme"; +import {setPreviewMode} from "./ts/ui/setPreviewMode"; +import {setTheme} from "./ts/ui/setTheme"; +import {Undo} from "./ts/undo/index"; +import {Upload} from "./ts/upload/index"; +import {addScript, addScriptSync} from "./ts/util/addScript"; +import {getSelectText} from "./ts/util/getSelectText"; +import {Options} from "./ts/util/Options"; +import {processCodeRender} from "./ts/util/processCode"; +import {getCursorPosition, getEditorRange, insertHTML} from "./ts/util/selection"; +import {afterRenderEvent} from "./ts/wysiwyg/afterRenderEvent"; +import {WYSIWYG} from "./ts/wysiwyg/index"; +import {input} from "./ts/wysiwyg/input"; +import {renderDomByMd} from "./ts/wysiwyg/renderDomByMd"; +import {execAfterRender} from "./ts/util/fixBrowserBehavior"; +import {accessLocalStorage} from "./ts/util/compatibility"; + +class Vditor extends VditorMethod { + public readonly version: string; + public vditor: IVditor; + + /** + * @param id 要挂载 Vditor 的元素或者元素 ID。 + * @param options Vditor 参数 + */ + constructor(id: string | HTMLElement, options?: IOptions) { + super(); + this.version = VDITOR_VERSION; + + if (typeof id === "string") { + if (!options) { + options = { + cache: { + id: `vditor${id}`, + }, + }; + } else if (!options.cache) { + options.cache = {id: `vditor${id}`}; + } else if (!options.cache.id) { + options.cache.id = `vditor${id}`; + } + if (!document.getElementById(id)) { + this.showErrorTip(`Failed to get element by id: ${id}`); + return; + } + id = document.getElementById(id); + } + + const getOptions = new Options(options); + const mergedOptions = getOptions.merge(); + + // 支持自定义国际化 + if (!mergedOptions.i18n) { + if (!["en_US", "fr_FR", "pt_BR", "ja_JP", "ko_KR", "ru_RU", "sv_SE", "zh_CN", "zh_TW"].includes(mergedOptions.lang)) { + throw new Error( + "options.lang error, see https://ld246.com/article/1549638745630#options", + ); + } else { + const i18nScriptPrefix = "vditorI18nScript"; + const i18nScriptID = i18nScriptPrefix + mergedOptions.lang; + document.querySelectorAll(`head script[id^="${i18nScriptPrefix}"]`).forEach((el) => { + if (el.id !== i18nScriptID) { + document.head.removeChild(el); + } + }); + addScript(`${mergedOptions.cdn}/dist/js/i18n/${mergedOptions.lang}.js`, i18nScriptID).then(() => { + this.init(id as HTMLElement, mergedOptions); + }).catch(error => { + this.showErrorTip(`GET ${mergedOptions.cdn}/dist/js/i18n/${mergedOptions.lang}.js net::ERR_ABORTED 404 (Not Found)`); + }); + } + } else { + window.VditorI18n = mergedOptions.i18n; + this.init(id, mergedOptions); + } + } + + private showErrorTip(error: string) { + const tip = new Tip(); + document.body.appendChild(tip.element); + tip.show(error, 0) + } + + public updateToolbarConfig(options: IToolbarConfig) { + this.vditor.toolbar.updateConfig(this.vditor, options); + } + + /** 设置主题 */ + public setTheme( + theme: "dark" | "classic", + contentTheme?: string, + codeTheme?: string, + contentThemePath?: string, + ) { + this.vditor.options.theme = theme; + setTheme(this.vditor); + if (contentTheme) { + this.vditor.options.preview.theme.current = contentTheme; + setContentTheme(contentTheme, contentThemePath || this.vditor.options.preview.theme.path); + } + if (codeTheme) { + this.vditor.options.preview.hljs.style = codeTheme; + setCodeTheme(codeTheme, this.vditor.options.cdn); + } + } + + /** 获取 Markdown 内容 */ + public getValue() { + return getMarkdown(this.vditor); + } + + /** 获取编辑器当前编辑模式 */ + public getCurrentMode() { + return this.vditor.currentMode; + } + + /** 聚焦到编辑器 */ + public focus() { + if (this.vditor.currentMode === "sv") { + this.vditor.sv.element.focus(); + } else if (this.vditor.currentMode === "wysiwyg") { + this.vditor.wysiwyg.element.focus(); + } else if (this.vditor.currentMode === "ir") { + this.vditor.ir.element.focus(); + } + } + + /** 让编辑器失焦 */ + public blur() { + if (this.vditor.currentMode === "sv") { + this.vditor.sv.element.blur(); + } else if (this.vditor.currentMode === "wysiwyg") { + this.vditor.wysiwyg.element.blur(); + } else if (this.vditor.currentMode === "ir") { + this.vditor.ir.element.blur(); + } + } + + /** 禁用编辑器 */ + public disabled() { + hidePanel(this.vditor, ["subToolbar", "hint", "popover"]); + disableToolbar( + this.vditor.toolbar.elements, + Constants.EDIT_TOOLBARS.concat(["undo", "redo", "fullscreen", "edit-mode"]), + ); + this.vditor[this.vditor.currentMode].element.setAttribute( + "contenteditable", + "false", + ); + } + + /** 解除编辑器禁用 */ + public enable() { + enableToolbar( + this.vditor.toolbar.elements, + Constants.EDIT_TOOLBARS.concat(["undo", "redo", "fullscreen", "edit-mode"]), + ); + this.vditor.undo.resetIcon(this.vditor); + this.vditor[this.vditor.currentMode].element.setAttribute("contenteditable", "true"); + } + + /** 返回选中的字符串 */ + public getSelection() { + if (this.vditor.currentMode === "wysiwyg") { + return getSelectText(this.vditor.wysiwyg.element); + } else if (this.vditor.currentMode === "sv") { + return getSelectText(this.vditor.sv.element); + } else if (this.vditor.currentMode === "ir") { + return getSelectText(this.vditor.ir.element); + } + } + + /** 设置预览区域内容 */ + public renderPreview(value?: string) { + this.vditor.preview.render(this.vditor, value); + } + + /** 获取焦点位置 */ + public getCursorPosition() { + return getCursorPosition(this.vditor[this.vditor.currentMode].element); + } + + /** 上传是否还在进行中 */ + public isUploading() { + return this.vditor.upload.isUploading; + } + + /** 清除缓存 */ + public clearCache() { + if (this.vditor.options.cache.enable && accessLocalStorage()) { + localStorage.removeItem(this.vditor.options.cache.id); + } + } + + /** 禁用缓存 */ + public disabledCache() { + this.vditor.options.cache.enable = false; + } + + /** 启用缓存 */ + public enableCache() { + if (!this.vditor.options.cache.id) { + throw new Error( + "need options.cache.id, see https://ld246.com/article/1549638745630#options", + ); + } + this.vditor.options.cache.enable = true; + } + + /** HTML 转 md */ + public html2md(value: string) { + return this.vditor.lute.HTML2Md(value); + } + + /** markdown 转 JSON 输出 */ + public exportJSON(value: string) { + return this.vditor.lute.RenderJSON(value); + } + + /** 获取 HTML */ + public getHTML() { + return getHTML(this.vditor); + } + + /** 消息提示。time 为 0 将一直显示 */ + public tip(text: string, time?: number) { + this.vditor.tip.show(text, time); + } + + /** 设置预览模式 */ + public setPreviewMode(mode: "both" | "editor") { + setPreviewMode(mode, this.vditor); + } + + /** 删除选中内容 */ + public deleteValue() { + if (window.getSelection().isCollapsed) { + return; + } + document.execCommand("delete", false); + } + + /** 更新选中内容 */ + public updateValue(value: string) { + document.execCommand("insertHTML", false, value); + } + + /** 在焦点处插入内容,并默认进行 Markdown 渲染 */ + public insertValue(value: string, render = true) { + const range = getEditorRange(this.vditor); + range.collapse(true); + // https://github.com/Vanessa219/vditor/issues/716 + // https://github.com/Vanessa219/vditor/issues/917 + const tmpElement = document.createElement("template"); + tmpElement.innerHTML = value; + range.insertNode(tmpElement.content.cloneNode(true)); + range.collapse(false); + if (this.vditor.currentMode === "sv") { + this.vditor.sv.preventInput = true; + if (render) { + inputEvent(this.vditor); + } + } else if (this.vditor.currentMode === "wysiwyg") { + // 由于 https://github.com/Vanessa219/vditor/issues/1566 不能使用 this.vditor.wysiwyg.preventInput = true; + if (render) { + input(this.vditor, getSelection().getRangeAt(0)); + } + } else if (this.vditor.currentMode === "ir") { + this.vditor.ir.preventInput = true; + if (render) { + irInput(this.vditor, getSelection().getRangeAt(0), true); + } + } + } + + /** 在焦点处插入 Markdown */ + public insertMD(md: string) { + // https://github.com/Vanessa219/vditor/issues/1640 + if (this.vditor.currentMode === "ir") { + insertHTML(this.vditor.lute.Md2VditorIRDOM(md), this.vditor); + } else if (this.vditor.currentMode === "wysiwyg") { + insertHTML(this.vditor.lute.Md2VditorDOM(md), this.vditor); + } else { + processPaste(this.vditor, md); + } + this.vditor.outline.render(this.vditor); + execAfterRender(this.vditor); + } + + /** 设置编辑器内容 */ + public setValue(markdown: string, clearStack = false) { + if (this.vditor.currentMode === "sv") { + this.vditor.sv.element.innerHTML = `
${this.vditor.lute.SpinVditorSVDOM(markdown)}
`; + processSVAfterRender(this.vditor, { + enableAddUndoStack: true, + enableHint: false, + enableInput: false, + }); + } else if (this.vditor.currentMode === "wysiwyg") { + renderDomByMd(this.vditor, markdown, { + enableAddUndoStack: true, + enableHint: false, + enableInput: false, + }); + } else { + this.vditor.ir.element.innerHTML = this.vditor.lute.Md2VditorIRDOM(markdown); + this.vditor.ir.element + .querySelectorAll(".vditor-ir__preview[data-render='2']") + .forEach((item: HTMLElement) => { + processCodeRender(item, this.vditor); + }); + processAfterRender(this.vditor, { + enableAddUndoStack: true, + enableHint: false, + enableInput: false, + }); + } + + this.vditor.outline.render(this.vditor); + + if (!markdown) { + hidePanel(this.vditor, ["emoji", "headings", "submenu", "hint"]); + if (this.vditor.wysiwyg.popover) { + this.vditor.wysiwyg.popover.style.display = "none"; + } + this.clearCache(); + } + if (clearStack) { + this.clearStack(); + } + } + + /** 清空 undo & redo 栈 */ + public clearStack() { + this.vditor.undo.clearStack(this.vditor); + this.vditor.undo.addToUndoStack(this.vditor); + } + + /** 销毁编辑器 */ + public destroy() { + this.vditor.element.innerHTML = this.vditor.originalInnerHTML; + this.vditor.element.classList.remove("vditor"); + this.vditor.element.removeAttribute("style"); + const iconScript = document.getElementById("vditorIconScript") + if (iconScript) { + iconScript.remove(); + } + this.clearCache(); + + UIUnbindListener(); + this.vditor.wysiwyg.unbindListener(); + } + + /** 获取评论 ID */ + public getCommentIds() { + if (this.vditor.currentMode !== "wysiwyg") { + return []; + } + return this.vditor.wysiwyg.getComments(this.vditor, true); + } + + /** 高亮评论 */ + public hlCommentIds(ids: string[]) { + if (this.vditor.currentMode !== "wysiwyg") { + return; + } + const hlItem = (item: Element) => { + item.classList.remove("vditor-comment--hover"); + ids.forEach((id) => { + if (item.getAttribute("data-cmtids").indexOf(id) > -1) { + item.classList.add("vditor-comment--hover"); + } + }); + }; + this.vditor.wysiwyg.element + .querySelectorAll(".vditor-comment") + .forEach((item) => { + hlItem(item); + }); + if (this.vditor.preview.element.style.display !== "none") { + this.vditor.preview.element + .querySelectorAll(".vditor-comment") + .forEach((item) => { + hlItem(item); + }); + } + } + + /** 取消评论高亮 */ + public unHlCommentIds(ids: string[]) { + if (this.vditor.currentMode !== "wysiwyg") { + return; + } + const unHlItem = (item: Element) => { + ids.forEach((id) => { + if (item.getAttribute("data-cmtids").indexOf(id) > -1) { + item.classList.remove("vditor-comment--hover"); + } + }); + }; + this.vditor.wysiwyg.element + .querySelectorAll(".vditor-comment") + .forEach((item) => { + unHlItem(item); + }); + if (this.vditor.preview.element.style.display !== "none") { + this.vditor.preview.element + .querySelectorAll(".vditor-comment") + .forEach((item) => { + unHlItem(item); + }); + } + } + + /** 删除评论 */ + public removeCommentIds(removeIds: string[]) { + if (this.vditor.currentMode !== "wysiwyg") { + return; + } + + const removeItem = (item: Element, removeId: string) => { + const ids = item.getAttribute("data-cmtids").split(" "); + ids.find((id, index) => { + if (id === removeId) { + ids.splice(index, 1); + return true; + } + }); + if (ids.length === 0) { + item.outerHTML = item.innerHTML; + getEditorRange(this.vditor).collapse(true); + } else { + item.setAttribute("data-cmtids", ids.join(" ")); + } + }; + removeIds.forEach((removeId) => { + this.vditor.wysiwyg.element + .querySelectorAll(".vditor-comment") + .forEach((item) => { + removeItem(item, removeId); + }); + if (this.vditor.preview.element.style.display !== "none") { + this.vditor.preview.element + .querySelectorAll(".vditor-comment") + .forEach((item) => { + removeItem(item, removeId); + }); + } + }); + afterRenderEvent(this.vditor, { + enableAddUndoStack: true, + enableHint: false, + enableInput: false, + }); + } + + private init(id: HTMLElement, mergedOptions: IOptions) { + this.vditor = { + currentMode: mergedOptions.mode, + element: id, + hint: new Hint(mergedOptions.hint.extend), + lute: undefined, + options: mergedOptions, + originalInnerHTML: id.innerHTML, + outline: new Outline(window.VditorI18n.outline), + tip: new Tip(), + }; + + this.vditor.sv = new Editor(this.vditor); + this.vditor.undo = new Undo(); + this.vditor.wysiwyg = new WYSIWYG(this.vditor); + this.vditor.ir = new IR(this.vditor); + this.vditor.toolbar = new Toolbar(this.vditor); + + if (mergedOptions.resize.enable) { + this.vditor.resize = new Resize(this.vditor); + } + + if (this.vditor.toolbar.elements.devtools) { + this.vditor.devtools = new DevTools(); + } + + if (mergedOptions.upload.url || mergedOptions.upload.handler) { + this.vditor.upload = new Upload(); + } + + addScript( + mergedOptions._lutePath || + `${mergedOptions.cdn}/dist/js/lute/lute.min.js`, + "vditorLuteScript", + ).then(() => { + this.vditor.lute = setLute({ + autoSpace: this.vditor.options.preview.markdown.autoSpace, + gfmAutoLink: this.vditor.options.preview.markdown.gfmAutoLink, + codeBlockPreview: this.vditor.options.preview.markdown + .codeBlockPreview, + emojiSite: this.vditor.options.hint.emojiPath, + emojis: this.vditor.options.hint.emoji, + fixTermTypo: this.vditor.options.preview.markdown.fixTermTypo, + footnotes: this.vditor.options.preview.markdown.footnotes, + headingAnchor: false, + inlineMathDigit: this.vditor.options.preview.math.inlineDigit, + linkBase: this.vditor.options.preview.markdown.linkBase, + linkPrefix: this.vditor.options.preview.markdown.linkPrefix, + listStyle: this.vditor.options.preview.markdown.listStyle, + mark: this.vditor.options.preview.markdown.mark, + mathBlockPreview: this.vditor.options.preview.markdown + .mathBlockPreview, + paragraphBeginningSpace: this.vditor.options.preview.markdown + .paragraphBeginningSpace, + sanitize: this.vditor.options.preview.markdown.sanitize, + toc: this.vditor.options.preview.markdown.toc, + }); + + this.vditor.preview = new Preview(this.vditor); + + initUI(this.vditor); + + if (mergedOptions.after) { + mergedOptions.after(); + } + if (mergedOptions.icon) { + // 防止初始化 2 个编辑器时加载 2 次 + addScriptSync(`${mergedOptions.cdn}/dist/js/icons/${mergedOptions.icon}.js`, "vditorIconScript"); + } + }); + } +} + +export default Vditor; diff --git a/web/include/vditor/src/method.ts b/web/include/vditor/src/method.ts new file mode 100644 index 0000000..8e1ff30 --- /dev/null +++ b/web/include/vditor/src/method.ts @@ -0,0 +1,68 @@ +import {abcRender} from "./ts/markdown/abcRender"; +import * as adapterRender from "./ts/markdown/adapterRender"; +import {chartRender} from "./ts/markdown/chartRender"; +import {codeRender} from "./ts/markdown/codeRender"; +import {flowchartRender} from "./ts/markdown/flowchartRender"; +import {graphvizRender} from "./ts/markdown/graphvizRender"; +import {highlightRender} from "./ts/markdown/highlightRender"; +import {lazyLoadImageRender} from "./ts/markdown/lazyLoadImageRender"; +import {mathRender} from "./ts/markdown/mathRender"; +import {mediaRender} from "./ts/markdown/mediaRender"; +import {mermaidRender} from "./ts/markdown/mermaidRender"; +import {markmapRender} from "./ts/markdown/markmapRender"; +import {mindmapRender} from "./ts/markdown/mindmapRender"; +import {outlineRender} from "./ts/markdown/outlineRender"; +import {plantumlRender} from "./ts/markdown/plantumlRender"; +import {md2html, previewRender} from "./ts/markdown/previewRender"; +import {speechRender} from "./ts/markdown/speechRender"; +import {previewImage} from "./ts/preview/image"; +import {setCodeTheme} from "./ts/ui/setCodeTheme"; +import {setContentTheme} from "./ts/ui/setContentTheme"; + +class Vditor { + + /** 点击图片放大 */ + public static adapterRender = adapterRender; + /** 点击图片放大 */ + public static previewImage = previewImage; + /** 为 element 中的代码块添加复制按钮 */ + public static codeRender = codeRender; + /** 对 graphviz 进行渲染 */ + public static graphvizRender = graphvizRender; + /** 为 element 中的代码块进行高亮渲染 */ + public static highlightRender = highlightRender; + /** 对数学公式进行渲染 */ + public static mathRender = mathRender; + /** 流程图/时序图/甘特图渲染 */ + public static mermaidRender = mermaidRender; + /** 支持markdown的思维导图 */ + public static markmapRender = markmapRender; + /** flowchart.js 渲染 */ + public static flowchartRender = flowchartRender; + /** 图表渲染 */ + public static chartRender = chartRender; + /** 五线谱渲染 */ + public static abcRender = abcRender; + /** 脑图渲染 */ + public static mindmapRender = mindmapRender; + /** plantuml渲染 */ + public static plantumlRender = plantumlRender; + /** 大纲渲染 */ + public static outlineRender = outlineRender; + /** 为[特定链接](https://github.com/Vanessa219/vditor/issues/7)分别渲染为视频、音频、嵌入的 iframe */ + public static mediaRender = mediaRender; + /** 对选中的文字进行阅读 */ + public static speechRender = speechRender; + /** 对图片进行懒加载 */ + public static lazyLoadImageRender = lazyLoadImageRender; + /** Markdown 文本转换为 HTML,该方法需使用[异步编程](https://ld246.com/article/1546828434083?r=Vaness) */ + public static md2html = md2html; + /** 页面 Markdown 文章渲染 */ + public static preview = previewRender; + /** 设置代码主题 */ + public static setCodeTheme = setCodeTheme; + /** 设置内容主题 */ + public static setContentTheme = setContentTheme; +} + +export default Vditor; diff --git a/web/include/vditor/src/ts/constants.ts b/web/include/vditor/src/ts/constants.ts new file mode 100644 index 0000000..b918db7 --- /dev/null +++ b/web/include/vditor/src/ts/constants.ts @@ -0,0 +1,69 @@ +declare const VDITOR_VERSION: string; + +const _VDITOR_VERSION = VDITOR_VERSION; + +export {_VDITOR_VERSION as VDITOR_VERSION}; + +export abstract class Constants { + public static readonly ZWSP: string = "\u200b"; + public static readonly DROP_EDITOR: string = "application/editor"; + public static readonly MOBILE_WIDTH: number = 520; + public static readonly CLASS_MENU_DISABLED: string = "vditor-menu--disabled"; + public static readonly EDIT_TOOLBARS: string[] = ["emoji", "headings", "bold", "italic", "strike", "link", "list", + "ordered-list", "outdent", "indent", "check", "line", "quote", "code", "inline-code", "insert-after", + "insert-before", "upload", "record", "table"]; + public static readonly CODE_THEME: string[] = ["abap", "algol", "algol_nu", "arduino", "autumn", "borland", "bw", + "colorful", "dracula", "emacs", "friendly", "fruity", "github", "igor", "lovelace", "manni", "monokai", + "monokailight", "murphy", "native", "paraiso-dark", "paraiso-light", "pastie", "perldoc", "pygments", + "rainbow_dash", "rrt", "solarized-dark", "solarized-dark256", "solarized-light", "swapoff", "tango", "trac", + "vim", "vs", "xcode", "ant-design"]; + public static readonly CODE_LANGUAGES: string[] = ["mermaid", "echarts", "mindmap", "plantuml", "abc", "graphviz", "flowchart", "apache", + "js", "ts", "html","markmap", + // common + "properties", "apache", "bash", "c", "csharp", "cpp", "css", "coffeescript", "diff", "go", "xml", "http", + "json", "java", "javascript", "kotlin", "less", "lua", "makefile", "markdown", "nginx", "objectivec", "php", + "php-template", "perl", "plaintext", "python", "python-repl", "r", "ruby", "rust", "scss", "sql", "shell", + "swift", "ini", "typescript", "vbnet", "yaml", + "ada", "clojure", "dart", "erb", "fortran", "gradle", "haskell", "julia", "julia-repl", "lisp", "matlab", + "pgsql", "powershell", "sql_more", "stata", "cmake", "mathematica", + // ext + "solidity", "yul" + ]; + public static readonly CDN = `https://unpkg.com/vditor@${VDITOR_VERSION}`; + public static readonly MARKDOWN_OPTIONS = { + autoSpace: false, + gfmAutoLink: true, + codeBlockPreview: true, + fixTermTypo: false, + footnotes: true, + linkBase: "", + linkPrefix: "", + listStyle: false, + mark: false, + mathBlockPreview: true, + paragraphBeginningSpace: false, + sanitize: true, + toc: false, + }; + public static readonly HLJS_OPTIONS = { + enable: true, + lineNumber: false, + defaultLang: "", + style: "github", + }; + public static readonly MATH_OPTIONS: IMath = { + engine: "KaTeX", + inlineDigit: false, + macros: {}, + }; + public static readonly THEME_OPTIONS = { + current: "light", + list: { + "ant-design": "Ant Design", + "dark": "Dark", + "light": "Light", + "wechat": "WeChat", + }, + path: `${Constants.CDN}/dist/css/content-theme`, + }; +} diff --git a/web/include/vditor/src/ts/devtools/index.ts b/web/include/vditor/src/ts/devtools/index.ts new file mode 100644 index 0000000..48cab72 --- /dev/null +++ b/web/include/vditor/src/ts/devtools/index.ts @@ -0,0 +1,83 @@ +import {getMarkdown} from "../markdown/getMarkdown"; +import {addScript} from "../util/addScript"; + +declare const echarts: { + init(element: HTMLElement): IEChart; +}; + +export class DevTools { + public element: HTMLDivElement; + private ASTChart: IEChart; + + constructor() { + this.element = document.createElement("div"); + this.element.className = "vditor-devtools"; + this.element.innerHTML = '
'; + } + + public renderEchart(vditor: IVditor) { + if (vditor.devtools.element.style.display !== "block") { + return; + } + + addScript(`${vditor.options.cdn}/dist/js/echarts/echarts.min.js?v=5.5.1`, "vditorEchartsScript").then(() => { + if (!this.ASTChart) { + this.ASTChart = echarts.init(vditor.devtools.element.lastElementChild as HTMLDivElement); + } + try { + (this.element.lastElementChild as HTMLElement).style.display = "block"; + this.element.firstElementChild.innerHTML = ""; + this.ASTChart.setOption({ + series: [ + { + data: JSON.parse(vditor.lute.RenderEChartsJSON(getMarkdown(vditor))), + initialTreeDepth: -1, + label: { + align: "left", + backgroundColor: "rgba(68, 77, 86, .68)", + borderRadius: 3, + color: "#d1d5da", + fontSize: 12, + lineHeight: 12, + offset: [9, 12], + padding: [2, 4, 2, 4], + position: "top", + verticalAlign: "middle", + }, + lineStyle: { + color: "#4285f4", + type: "curve", + width: 1, + }, + orient: "vertical", + roam: true, + type: "tree", + }, + ], + toolbox: { + bottom: 25, + emphasis: { + iconStyle: { + color: "#4285f4", + }, + }, + feature: { + restore: { + show: true, + }, + saveAsImage: { + show: true, + }, + }, + right: 15, + show: true, + }, + }); + this.ASTChart.resize(); + } catch (e) { + (this.element.lastElementChild as HTMLElement).style.display = "none"; + this.element.firstElementChild.innerHTML = e; + } + }); + } +} diff --git a/web/include/vditor/src/ts/export/index.ts b/web/include/vditor/src/ts/export/index.ts new file mode 100644 index 0000000..986b543 --- /dev/null +++ b/web/include/vditor/src/ts/export/index.ts @@ -0,0 +1,83 @@ +import {getHTML} from "../markdown/getHTML"; +import {getMarkdown} from "../markdown/getMarkdown"; + +export const download = (vditor: IVditor, content: string, filename: string) => { + const aElement = document.createElement("a"); + if ("download" in aElement) { + aElement.download = filename; + aElement.style.display = "none"; + aElement.href = URL.createObjectURL(new Blob([content])); + + document.body.appendChild(aElement); + aElement.click(); + aElement.remove(); + } else { + vditor.tip.show(window.VditorI18n.downloadTip, 0); + } +}; + +export const exportMarkdown = (vditor: IVditor) => { + const content = getMarkdown(vditor); + download(vditor, content, content.substr(0, 10) + ".md"); +}; + +export const exportPDF = (vditor: IVditor) => { + vditor.tip.show(window.VditorI18n.generate, 3800); + const iframe = document.querySelector("#vditorExportIframe") as HTMLIFrameElement; + iframe.contentDocument.open(); + iframe.contentDocument.write(` + +
+`); + iframe.contentDocument.close(); + setTimeout(() => { + iframe.contentWindow.postMessage(getMarkdown(vditor), "*"); + }, 200); +}; + +export const exportHTML = (vditor: IVditor) => { + const content = getHTML(vditor); + const html = ` + + +
${content}
+ +`; + download(vditor, html, content.substr(0, 10) + ".html"); +}; diff --git a/web/include/vditor/src/ts/hint/index.ts b/web/include/vditor/src/ts/hint/index.ts new file mode 100644 index 0000000..20b64c4 --- /dev/null +++ b/web/include/vditor/src/ts/hint/index.ts @@ -0,0 +1,276 @@ +import {Constants} from "../constants"; +import {processAfterRender} from "../ir/process"; +import {code160to32} from "../util/code160to32"; +import {isCtrl} from "../util/compatibility"; +import {execAfterRender} from "../util/fixBrowserBehavior"; +import {hasClosestByAttribute, hasClosestByClassName} from "../util/hasClosest"; +import {processCodeRender} from "../util/processCode"; +import {getCursorPosition, insertHTML, setSelectionFocus} from "../util/selection"; + +export class Hint { + public timeId: number; + public element: HTMLDivElement; + public recentLanguage: string; + private splitChar = ""; + private lastIndex = -1; + + constructor(hintExtends: IHintExtend[]) { + this.timeId = -1; + this.element = document.createElement("div"); + this.element.className = "vditor-hint"; + this.recentLanguage = ""; + hintExtends.push({key: ":"}); + } + + public render(vditor: IVditor) { + if (!window.getSelection().focusNode) { + return; + } + let currentLineValue: string; + const range = getSelection().getRangeAt(0); + currentLineValue = range.startContainer.textContent.substring(0, range.startOffset) || ""; + + const key = this.getKey(currentLineValue, vditor.options.hint.extend); + + if (typeof key === "undefined") { + this.element.style.display = "none"; + clearTimeout(this.timeId); + } else { + if (this.splitChar === ":") { + const emojiHint = key === "" ? vditor.options.hint.emoji : vditor.lute.GetEmojis(); + const matchEmojiData: IHintData[] = []; + Object.keys(emojiHint).forEach((keyName) => { + if (keyName.indexOf(key.toLowerCase()) === 0) { + if (emojiHint[keyName].indexOf(".") > -1) { + matchEmojiData.push({ + html: ` :${keyName}:`, + value: `:${keyName}:`, + }); + } else { + matchEmojiData.push({ + html: `${emojiHint[keyName]}${keyName}`, + value: emojiHint[keyName], + }); + } + } + }); + this.genHTML(matchEmojiData, key, vditor); + } else { + vditor.options.hint.extend.forEach((item) => { + if (item.key === this.splitChar) { + clearTimeout(this.timeId); + this.timeId = window.setTimeout(async () => { + this.genHTML(await item.hint(key), key, vditor); + }, vditor.options.hint.delay); + } + }); + } + } + } + + public genHTML(data: IHintData[], key: string, vditor: IVditor) { + if (data.length === 0) { + this.element.style.display = "none"; + return; + } + + const editorElement = vditor[vditor.currentMode].element; + const textareaPosition = getCursorPosition(editorElement); + const x = textareaPosition.left + + (vditor.options.outline.position === "left" ? vditor.outline.element.offsetWidth : 0); + const y = textareaPosition.top; + let hintsHTML = ""; + + data.forEach((hintData, i) => { + if (i > 7) { + return; + } + // process high light + let html = hintData.html; + if (key !== "") { + const lastIndex = html.lastIndexOf(">") + 1; + let replaceHtml = html.substr(lastIndex); + const replaceIndex = replaceHtml.toLowerCase().indexOf(key.toLowerCase()); + if (replaceIndex > -1) { + replaceHtml = replaceHtml.substring(0, replaceIndex) + "" + + replaceHtml.substring(replaceIndex, replaceIndex + key.length) + "" + + replaceHtml.substring(replaceIndex + key.length); + html = html.substr(0, lastIndex) + replaceHtml; + } + } + hintsHTML += ``; + }); + + this.element.innerHTML = hintsHTML; + const lineHeight = parseInt(document.defaultView.getComputedStyle(editorElement, null) + .getPropertyValue("line-height"), 10); + this.element.style.top = `${y + (lineHeight || 22)}px`; + this.element.style.left = `${x}px`; + this.element.style.display = "block"; + this.element.style.right = "auto"; + + this.element.querySelectorAll("button").forEach((element) => { + element.addEventListener("click", (event) => { + this.fillEmoji(element, vditor); + event.preventDefault(); + }); + }); + // hint 展现在上部 + if (this.element.getBoundingClientRect().bottom > window.innerHeight) { + this.element.style.top = `${y - this.element.offsetHeight}px`; + } + if (this.element.getBoundingClientRect().right > window.innerWidth) { + this.element.style.left = "auto"; + this.element.style.right = "0"; + } + } + + public fillEmoji = (element: HTMLElement, vditor: IVditor) => { + this.element.style.display = "none"; + + const value = decodeURIComponent(element.getAttribute("data-value")); + const range: Range = window.getSelection().getRangeAt(0); + + // 代码提示 + if (vditor.currentMode === "ir") { + const preBeforeElement = hasClosestByAttribute(range.startContainer, "data-type", "code-block-info"); + if (preBeforeElement) { + preBeforeElement.textContent = Constants.ZWSP + value.trimRight(); + range.selectNodeContents(preBeforeElement); + range.collapse(false); + processAfterRender(vditor); + preBeforeElement.parentElement.querySelectorAll("code").forEach((item) => { + item.className = "language-" + value.trimRight(); + }); + processCodeRender(preBeforeElement.parentElement.querySelector(".vditor-ir__preview"), vditor); + this.recentLanguage = value.trimRight(); + return; + } + } + if (vditor.currentMode === "wysiwyg" && range.startContainer.nodeType !== 3 ) { + const startContainer = range.startContainer as HTMLElement; + let inputElement: HTMLInputElement; + if (startContainer.classList.contains("vditor-input")) { + inputElement = startContainer as HTMLInputElement; + } else { + inputElement = startContainer.firstElementChild as HTMLInputElement; + } + if (inputElement && inputElement.classList.contains("vditor-input")) { + inputElement.value = value.trimRight(); + range.selectNodeContents(inputElement); + range.collapse(false); + // {detail: 1}用于标识这个自定义事件是在编程语言选择后触发的 + // 用于在鼠标选择语言后,自动聚焦到代码输入框 + inputElement.dispatchEvent(new CustomEvent("input", {detail: 1})); + this.recentLanguage = value.trimRight(); + return; + } + } + + range.setStart(range.startContainer, this.lastIndex); + range.deleteContents(); + + if (vditor.options.hint.parse) { + if (vditor.currentMode === "sv") { + insertHTML(vditor.lute.SpinVditorSVDOM(value), vditor); + } else if (vditor.currentMode === "wysiwyg") { + insertHTML(vditor.lute.SpinVditorDOM(value), vditor); + } else { + insertHTML(vditor.lute.SpinVditorIRDOM(value), vditor); + } + } else { + insertHTML(value, vditor); + } + if (this.splitChar === ":" && value.indexOf(":") > -1 && vditor.currentMode !== "sv") { + range.insertNode(document.createTextNode(" ")); + } + range.collapse(false); + setSelectionFocus(range); + + if (vditor.currentMode === "wysiwyg") { + const preElement = hasClosestByClassName(range.startContainer, "vditor-wysiwyg__block"); + if (preElement && preElement.lastElementChild.classList.contains("vditor-wysiwyg__preview")) { + preElement.lastElementChild.innerHTML = preElement.firstElementChild.innerHTML; + processCodeRender(preElement.lastElementChild as HTMLElement, vditor); + } + } else if (vditor.currentMode === "ir") { + const preElement = hasClosestByClassName(range.startContainer, "vditor-ir__marker--pre"); + if (preElement && preElement.nextElementSibling.classList.contains("vditor-ir__preview")) { + preElement.nextElementSibling.innerHTML = preElement.innerHTML; + processCodeRender(preElement.nextElementSibling as HTMLElement, vditor); + } + } + execAfterRender(vditor); + } + + public select(event: KeyboardEvent, vditor: IVditor) { + + if (this.element.querySelectorAll("button").length === 0 || + this.element.style.display === "none") { + return false; + } + + const currentHintElement: HTMLElement = this.element.querySelector(".vditor-hint--current"); + + if (event.key === "ArrowDown") { + event.preventDefault(); + event.stopPropagation(); + currentHintElement.removeAttribute("class"); + if (!currentHintElement.nextElementSibling) { + this.element.children[0].className = "vditor-hint--current"; + } else { + currentHintElement.nextElementSibling.className = "vditor-hint--current"; + } + return true; + } else if (event.key === "ArrowUp") { + event.preventDefault(); + event.stopPropagation(); + currentHintElement.removeAttribute("class"); + if (!currentHintElement.previousElementSibling) { + const length = this.element.children.length; + this.element.children[length - 1].className = "vditor-hint--current"; + } else { + currentHintElement.previousElementSibling.className = "vditor-hint--current"; + } + return true; + } else if (!isCtrl(event) && !event.shiftKey && !event.altKey && event.key === "Enter" && !event.isComposing) { + event.preventDefault(); + event.stopPropagation(); + this.fillEmoji(currentHintElement, vditor); + return true; + } + return false; + } + + private getKey(currentLineValue: string, extend: IHintExtend[]) { + this.lastIndex = -1; + this.splitChar = ""; + extend.forEach((item) => { + const currentLastIndex = currentLineValue.lastIndexOf(item.key); + if (this.lastIndex < currentLastIndex) { + this.splitChar = item.key; + this.lastIndex = currentLastIndex; + } + }); + + let key; + if (this.lastIndex === -1) { + return key; + } + const lineArray = currentLineValue.split(this.splitChar); + const lastItem = lineArray[lineArray.length - 1]; + const maxLength = 32; + if (lineArray.length > 1 && lastItem.trim() === lastItem) { + if (lineArray.length === 2 && lineArray[0] === "" && lineArray[1].length < maxLength) { + key = lineArray[1]; + } else { + const preChar = lineArray[lineArray.length - 2].slice(-1); + if (code160to32(preChar) === " " && lastItem.length < maxLength) { + key = lastItem; + } + } + } + return key; + } +} diff --git a/web/include/vditor/src/ts/ir/expandMarker.ts b/web/include/vditor/src/ts/ir/expandMarker.ts new file mode 100644 index 0000000..188b8f3 --- /dev/null +++ b/web/include/vditor/src/ts/ir/expandMarker.ts @@ -0,0 +1,79 @@ +import {hasClosestByClassName, hasTopClosestByClassName} from "../util/hasClosest"; +import {setSelectionFocus} from "../util/selection"; + +const nextIsNode = (range: Range) => { + const startContainer = range.startContainer; + if (startContainer.nodeType === 3 && startContainer.nodeValue.length !== range.startOffset) { + return false; + } + + let nextNode: HTMLElement = startContainer.nextSibling as HTMLElement; + + while (nextNode && nextNode.textContent === "") { + nextNode = nextNode.nextSibling as HTMLElement; + } + + if (!nextNode) { + // *em*|**string** + const markerElement = hasClosestByClassName(startContainer, "vditor-ir__marker"); + if (markerElement && !markerElement.nextSibling) { + const parentNextNode = startContainer.parentElement.parentElement.nextSibling as HTMLElement; + if (parentNextNode && parentNextNode.nodeType !== 3 && + parentNextNode.classList.contains("vditor-ir__node")) { + return parentNextNode; + } + } + return false; + } else if (nextNode && nextNode.nodeType !== 3 && nextNode.classList.contains("vditor-ir__node") && + !nextNode.getAttribute("data-block")) { + // test|*em* + return nextNode; + } + + return false; +}; + +const previousIsNode = (range: Range) => { + const startContainer = range.startContainer; + const previousNode = startContainer.previousSibling as HTMLElement; + if (startContainer.nodeType === 3 && range.startOffset === 0 && previousNode && previousNode.nodeType !== 3 && + // *em*|text + previousNode.classList.contains("vditor-ir__node") && !previousNode.getAttribute("data-block")) { + return previousNode; + } + return false; +}; + +export const expandMarker = (range: Range, vditor: IVditor) => { + vditor.ir.element.querySelectorAll(".vditor-ir__node--expand").forEach((item) => { + item.classList.remove("vditor-ir__node--expand"); + }); + + const nodeElement = hasTopClosestByClassName(range.startContainer, "vditor-ir__node"); + const nodeElementEnd = !range.collapsed && hasTopClosestByClassName(range.endContainer, "vditor-ir__node"); + // 选中文本为同一个 nodeElement 内时,需要展开 + if (!range.collapsed && (!nodeElement || nodeElement !== nodeElementEnd)) { + return; + } + + if (nodeElement) { + nodeElement.classList.add("vditor-ir__node--expand"); + nodeElement.classList.remove("vditor-ir__node--hidden"); + // https://github.com/Vanessa219/vditor/issues/615 safari中光标位置跳动 + setSelectionFocus(range); + } + + const nextNode = nextIsNode(range); + if (nextNode) { + nextNode.classList.add("vditor-ir__node--expand"); + nextNode.classList.remove("vditor-ir__node--hidden"); + return; + } + + const previousNode = previousIsNode(range); + if (previousNode) { + previousNode.classList.add("vditor-ir__node--expand"); + previousNode.classList.remove("vditor-ir__node--hidden"); + return; + } +}; diff --git a/web/include/vditor/src/ts/ir/highlightToolbarIR.ts b/web/include/vditor/src/ts/ir/highlightToolbarIR.ts new file mode 100644 index 0000000..184f906 --- /dev/null +++ b/web/include/vditor/src/ts/ir/highlightToolbarIR.ts @@ -0,0 +1,97 @@ +import {Constants} from "../constants"; +import {disableToolbar, enableToolbar, removeCurrentToolbar, setCurrentToolbar} from "../toolbar/setToolbar"; +import {hasClosestByAttribute, hasClosestByMatchTag} from "../util/hasClosest"; +import {hasClosestByHeadings} from "../util/hasClosestByHeadings"; +import {getEditorRange, selectIsEditor} from "../util/selection"; + +export const highlightToolbarIR = (vditor: IVditor) => { + clearTimeout(vditor[vditor.currentMode].hlToolbarTimeoutId); + vditor[vditor.currentMode].hlToolbarTimeoutId = window.setTimeout(() => { + if (vditor[vditor.currentMode].element.getAttribute("contenteditable") === "false") { + return; + } + if (!selectIsEditor(vditor[vditor.currentMode].element)) { + return; + } + + removeCurrentToolbar(vditor.toolbar.elements, Constants.EDIT_TOOLBARS); + enableToolbar(vditor.toolbar.elements, Constants.EDIT_TOOLBARS); + + const range = getEditorRange(vditor); + let typeElement = range.startContainer as HTMLElement; + if (range.startContainer.nodeType === 3) { + typeElement = range.startContainer.parentElement; + } + if (typeElement.classList.contains("vditor-reset")) { + typeElement = typeElement.childNodes[range.startOffset] as HTMLElement; + } + + const headingElement = vditor.currentMode === "sv" ? + hasClosestByAttribute(typeElement, "data-type", "heading") : hasClosestByHeadings(typeElement); + if (headingElement) { + setCurrentToolbar(vditor.toolbar.elements, ["headings"]); + } + + const quoteElement = + vditor.currentMode === "sv" ? hasClosestByAttribute(typeElement, "data-type", "blockquote") : + hasClosestByMatchTag(typeElement, "BLOCKQUOTE"); + if (quoteElement) { + setCurrentToolbar(vditor.toolbar.elements, ["quote"]); + } + + const strongElement = hasClosestByAttribute(typeElement, "data-type", "strong"); + if (strongElement) { + setCurrentToolbar(vditor.toolbar.elements, ["bold"]); + } + + const emElement = hasClosestByAttribute(typeElement, "data-type", "em"); + if (emElement) { + setCurrentToolbar(vditor.toolbar.elements, ["italic"]); + } + + const sElement = hasClosestByAttribute(typeElement, "data-type", "s"); + if (sElement) { + setCurrentToolbar(vditor.toolbar.elements, ["strike"]); + } + + const aElement = hasClosestByAttribute(typeElement, "data-type", "a"); + if (aElement) { + setCurrentToolbar(vditor.toolbar.elements, ["link"]); + } + + const liElement = hasClosestByMatchTag(typeElement, "LI"); + if (liElement) { + if (liElement.classList.contains("vditor-task")) { + setCurrentToolbar(vditor.toolbar.elements, ["check"]); + } else if (liElement.parentElement.tagName === "OL") { + setCurrentToolbar(vditor.toolbar.elements, ["ordered-list"]); + } else if (liElement.parentElement.tagName === "UL") { + setCurrentToolbar(vditor.toolbar.elements, ["list"]); + } + enableToolbar(vditor.toolbar.elements, ["outdent", "indent"]); + } else { + disableToolbar(vditor.toolbar.elements, ["outdent", "indent"]); + } + + const codeBlockElement = hasClosestByAttribute(typeElement, "data-type", "code-block"); + if (codeBlockElement) { + disableToolbar(vditor.toolbar.elements, ["headings", "bold", "italic", "strike", "line", "quote", + "list", "ordered-list", "check", "code", "inline-code", "upload", "link", "table", "record"]); + setCurrentToolbar(vditor.toolbar.elements, ["code"]); + } + + const codeElement = hasClosestByAttribute(typeElement, "data-type", "code"); + if (codeElement) { + disableToolbar(vditor.toolbar.elements, ["headings", "bold", "italic", "strike", "line", "quote", + "list", "ordered-list", "check", "code", "upload", "link", "table", "record"]); + setCurrentToolbar(vditor.toolbar.elements, ["inline-code"]); + } + + const tableElement = hasClosestByAttribute(typeElement, "data-type", "table"); + if (tableElement) { + disableToolbar(vditor.toolbar.elements, ["headings", "list", "ordered-list", "check", "line", + "quote", "code", "table"]); + } + + }, 200); +}; diff --git a/web/include/vditor/src/ts/ir/index.ts b/web/include/vditor/src/ts/ir/index.ts new file mode 100644 index 0000000..14618c6 --- /dev/null +++ b/web/include/vditor/src/ts/ir/index.ts @@ -0,0 +1,265 @@ +import {Constants} from "../constants"; +import {isCtrl, isFirefox} from "../util/compatibility"; +import { + blurEvent, + copyEvent, cutEvent, dblclickEvent, + dropEvent, + focusEvent, + hotkeyEvent, + scrollCenter, + selectEvent, +} from "../util/editorCommonEvent"; +import {paste} from "../util/fixBrowserBehavior"; +import {hasClosestByAttribute, hasClosestByClassName} from "../util/hasClosest"; +import { + getEditorRange, setRangeByWbr, + setSelectionFocus, +} from "../util/selection"; +import {clickToc} from "../util/toc"; +import {expandMarker} from "./expandMarker"; +import {highlightToolbarIR} from "./highlightToolbarIR"; +import {input} from "./input"; +import {processAfterRender, processHint} from "./process"; +import {hidePanel} from "../toolbar/setToolbar"; + +class IR { + public range: Range; + public element: HTMLPreElement; + public processTimeoutId: number; + public hlToolbarTimeoutId: number; + public composingLock: boolean = false; + public preventInput: boolean; + + constructor(vditor: IVditor) { + const divElement = document.createElement("div"); + divElement.className = "vditor-ir"; + + divElement.innerHTML = `
`;
+
+        this.element = divElement.firstElementChild as HTMLPreElement;
+
+        this.bindEvent(vditor);
+
+        focusEvent(vditor, this.element);
+        dblclickEvent(vditor, this.element);
+        blurEvent(vditor, this.element);
+        hotkeyEvent(vditor, this.element);
+        selectEvent(vditor, this.element);
+        dropEvent(vditor, this.element);
+        copyEvent(vditor, this.element, this.copy);
+        cutEvent(vditor, this.element, this.copy);
+    }
+
+    private copy(event: ClipboardEvent, vditor: IVditor) {
+        const range = getSelection().getRangeAt(0);
+        if (range.toString() === "") {
+            return;
+        }
+        event.stopPropagation();
+        event.preventDefault();
+
+        const tempElement = document.createElement("div");
+        tempElement.appendChild(range.cloneContents());
+
+        event.clipboardData.setData("text/plain", vditor.lute.VditorIRDOM2Md(tempElement.innerHTML).trim());
+        event.clipboardData.setData("text/html", "");
+    }
+
+    private bindEvent(vditor: IVditor) {
+        this.element.addEventListener("paste", (event: ClipboardEvent & { target: HTMLElement }) => {
+            paste(vditor, event, {
+                pasteCode: (code: string) => {
+                    document.execCommand("insertHTML", false, code);
+                },
+            });
+        });
+
+        this.element.addEventListener("scroll", () => {
+            hidePanel(vditor, ["hint"]);
+        });
+
+        this.element.addEventListener("compositionstart", (event: InputEvent) => {
+            this.composingLock = true;
+        });
+
+        this.element.addEventListener("compositionend", (event: InputEvent) => {
+            if (!isFirefox()) {
+                input(vditor, getSelection().getRangeAt(0).cloneRange());
+            }
+            this.composingLock = false;
+        });
+
+        this.element.addEventListener("input", (event: InputEvent) => {
+            if (event.inputType === "deleteByDrag" || event.inputType === "insertFromDrop") {
+                // https://github.com/Vanessa219/vditor/issues/801 编辑器内容拖拽问题
+                return;
+            }
+            if (this.preventInput) {
+                this.preventInput = false;
+                processAfterRender(vditor, {
+                    enableAddUndoStack: true,
+                    enableHint: true,
+                    enableInput: true,
+                });
+                return;
+            }
+            if (this.composingLock || event.data === "‘" || event.data === "“" || event.data === "《") {
+                return;
+            }
+            input(vditor, getSelection().getRangeAt(0).cloneRange(), false, event);
+        });
+
+        this.element.addEventListener("click", (event: MouseEvent & { target: HTMLInputElement }) => {
+            if (event.target.tagName === "INPUT") {
+                if (event.target.checked) {
+                    event.target.setAttribute("checked", "checked");
+                } else {
+                    event.target.removeAttribute("checked");
+                }
+                this.preventInput = true;
+                processAfterRender(vditor);
+                return;
+            }
+
+            const range = getEditorRange(vditor);
+
+            // 点击后光标落于预览区
+            let previewElement = hasClosestByClassName(event.target, "vditor-ir__preview");
+            if (!previewElement) {
+                previewElement = hasClosestByClassName(
+                    range.startContainer, "vditor-ir__preview");
+            }
+            if (previewElement) {
+                if (previewElement.previousElementSibling.firstElementChild) {
+                    range.selectNodeContents(previewElement.previousElementSibling.firstElementChild);
+                } else {
+                    // 行内数学公式
+                    range.selectNodeContents(previewElement.previousElementSibling);
+                }
+                range.collapse(true);
+                setSelectionFocus(range);
+                scrollCenter(vditor);
+            }
+
+            // 点击图片光标选中图片地址
+            if (event.target.tagName === "IMG") {
+                const linkElement =
+                    event.target.parentElement.querySelector(".vditor-ir__marker--link");
+                if (linkElement) {
+                    range.selectNode(linkElement);
+                    setSelectionFocus(range);
+                }
+            }
+            // 打开链接
+            const aElement = hasClosestByAttribute(event.target, "data-type", "a");
+            if (aElement && (!aElement.classList.contains("vditor-ir__node--expand"))) {
+                if (vditor.options.link.click) {
+                    vditor.options.link.click(aElement.querySelector(":scope > .vditor-ir__marker--link"));
+                } else if (vditor.options.link.isOpen) {
+                    window.open(aElement.querySelector(":scope > .vditor-ir__marker--link").textContent);
+                }
+                return;
+            }
+
+            if (event.target.isEqualNode(this.element) && this.element.lastElementChild && range.collapsed) {
+                const lastRect = this.element.lastElementChild.getBoundingClientRect();
+                if (event.y > lastRect.top + lastRect.height) {
+                    if (this.element.lastElementChild.tagName === "P" &&
+                        this.element.lastElementChild.textContent.trim().replace(Constants.ZWSP, "") === "") {
+                        range.selectNodeContents(this.element.lastElementChild);
+                        range.collapse(false);
+                    } else {
+                        this.element.insertAdjacentHTML("beforeend",
+                            `

${Constants.ZWSP}

`); + setRangeByWbr(this.element, range); + } + } + } + + if (range.toString() === "") { + expandMarker(range, vditor); + } else { + // https://github.com/Vanessa219/vditor/pull/681 当点击选中区域时 eventTarget 与 range 不一致,需延迟等待 range 发生变化 + setTimeout(() => { + expandMarker(getEditorRange(vditor), vditor); + }); + } + clickToc(event, vditor); + highlightToolbarIR(vditor); + }); + + this.element.addEventListener("keyup", (event) => { + if (event.isComposing || isCtrl(event)) { + return; + } + if (event.key === "Enter") { + scrollCenter(vditor); + } + highlightToolbarIR(vditor); + if ((event.key === "Backspace" || event.key === "Delete") && + vditor.ir.element.innerHTML !== "" && vditor.ir.element.childNodes.length === 1 && + vditor.ir.element.firstElementChild && vditor.ir.element.firstElementChild.tagName === "P" + && vditor.ir.element.firstElementChild.childElementCount === 0 + && (vditor.ir.element.textContent === "" || vditor.ir.element.textContent === "\n")) { + // 为空时显示 placeholder + vditor.ir.element.innerHTML = ""; + return; + } + const range = getEditorRange(vditor); + if (event.key === "Backspace") { + // firefox headings https://github.com/Vanessa219/vditor/issues/211 + if (isFirefox() && range.startContainer.textContent === "\n" && range.startOffset === 1) { + range.startContainer.textContent = ""; + expandMarker(range, vditor); + } + // 数学公式前是空块,空块前是 table,在空块前删除,数学公式会多一个 br + this.element.querySelectorAll(".language-math").forEach((item) => { + const brElement = item.querySelector("br"); + if (brElement) { + brElement.remove(); + } + }); + } else if (event.key.indexOf("Arrow") > -1) { + if (event.key === "ArrowLeft" || event.key === "ArrowRight") { + processHint(vditor); + } + expandMarker(range, vditor); + } else if (event.keyCode === 229 && event.code === "" && event.key === "Unidentified") { + // https://github.com/Vanessa219/vditor/issues/508 IR 删除到节点需展开 + expandMarker(range, vditor); + } + + const previewRenderElement = hasClosestByClassName(range.startContainer, "vditor-ir__preview"); + + if (previewRenderElement) { + if (event.key === "ArrowUp" || event.key === "ArrowLeft") { + if (previewRenderElement.previousElementSibling.firstElementChild) { + range.selectNodeContents(previewRenderElement.previousElementSibling.firstElementChild); + } else { + // 行内数学公式/html entity + range.selectNodeContents(previewRenderElement.previousElementSibling); + } + range.collapse(false); + event.preventDefault(); + return true; + } + if (previewRenderElement.tagName === "SPAN" && + (event.key === "ArrowDown" || event.key === "ArrowRight")) { + if (previewRenderElement.parentElement.getAttribute("data-type") === "html-entity") { + // html entity + previewRenderElement.parentElement.insertAdjacentText("afterend", Constants.ZWSP); + range.setStart(previewRenderElement.parentElement.nextSibling, 1); + } else { + range.selectNodeContents(previewRenderElement.parentElement.lastElementChild); + } + range.collapse(false); + event.preventDefault(); + return true; + } + } + }); + } +} + +export {IR}; diff --git a/web/include/vditor/src/ts/ir/input.ts b/web/include/vditor/src/ts/ir/input.ts new file mode 100644 index 0000000..d64f4b7 --- /dev/null +++ b/web/include/vditor/src/ts/ir/input.ts @@ -0,0 +1,246 @@ +import {Constants} from "../constants"; +import {isHeadingMD, isHrMD} from "../util/fixBrowserBehavior"; +import { + getTopList, + hasClosestBlock, hasClosestByAttribute, + hasClosestByClassName, +} from "../util/hasClosest"; +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"; +import {getMarkdown} from "../markdown/getMarkdown"; + +export const input = (vditor: IVditor, range: Range, ignoreSpace = false, event?: InputEvent) => { + let blockElement = hasClosestBlock(range.startContainer); + // 前后可以输入空格 + if (blockElement && !ignoreSpace && blockElement.getAttribute("data-type") !== "code-block") { + if ((isHrMD(blockElement.innerHTML) && blockElement.previousElementSibling) || + isHeadingMD(blockElement.innerHTML)) { + return; + } + + // 前后空格处理 + const startOffset = getSelectPosition(blockElement, vditor.ir.element, range).start; + + // 开始可以输入空格 + let startSpace = true; + for (let i = startOffset - 1; + // 软换行后有空格 + i > blockElement.textContent.substr(0, startOffset).lastIndexOf("\n"); + i--) { + if (blockElement.textContent.charAt(i) !== " " && + // 多个 tab 前删除不形成代码块 https://github.com/Vanessa219/vditor/issues/162 1 + blockElement.textContent.charAt(i) !== "\t") { + startSpace = false; + break; + } + } + if (startOffset === 0) { + startSpace = false; + } + + // 结尾可以输入空格 + let endSpace = true; + for (let i = startOffset - 1; i < blockElement.textContent.length; i++) { + if (blockElement.textContent.charAt(i) !== " " && blockElement.textContent.charAt(i) !== "\n") { + endSpace = false; + break; + } + } + + if (startSpace) { + if (typeof vditor.options.input === "function") { + vditor.options.input(getMarkdown(vditor)); + } + return; + } + + // https://github.com/Vanessa219/vditor/issues/729 + if (endSpace && /^#{1,6} $/.test(blockElement.textContent)) { + endSpace = false; + } + + if (endSpace) { + const markerElement = hasClosestByClassName(range.startContainer, "vditor-ir__marker"); + if (markerElement) { + // inline marker space https://github.com/Vanessa219/vditor/issues/239 + } else { + const previousNode = range.startContainer.previousSibling as HTMLElement; + if (previousNode && previousNode.nodeType !== 3 && previousNode.classList.contains("vditor-ir__node--expand")) { + // FireFox https://github.com/Vanessa219/vditor/issues/239 + previousNode.classList.remove("vditor-ir__node--expand"); + } + if (typeof vditor.options.input === "function") { + vditor.options.input(getMarkdown(vditor)); + } + return; + } + } + } + + vditor.ir.element.querySelectorAll(".vditor-ir__node--expand").forEach((item) => { + item.classList.remove("vditor-ir__node--expand"); + }); + + if (!blockElement) { + // 使用顶级块元素,应使用 innerHTML + blockElement = vditor.ir.element; + } + + // document.exeComment insertHTML 会插入 wbr + if (!blockElement.querySelector("wbr")) { + const previewRenderElement = hasClosestByClassName(range.startContainer, "vditor-ir__preview"); + if (previewRenderElement) { + previewRenderElement.previousElementSibling.insertAdjacentHTML("beforeend", ""); + } else { + range.insertNode(document.createElement("wbr")); + } + } + + // 清除浏览器自带的样式 + blockElement.querySelectorAll("[style]").forEach((item) => { + item.removeAttribute("style"); + }); + + if (blockElement.getAttribute("data-type") === "link-ref-defs-block") { + // 修改链接引用 + blockElement = vditor.ir.element; + } + + const isIRElement = blockElement.isEqualNode(vditor.ir.element); + const footnoteElement = hasClosestByAttribute(blockElement, "data-type", "footnotes-block"); + let html = ""; + if (!isIRElement) { + const blockquoteElement = hasClosestByTag(range.startContainer, "BLOCKQUOTE"); + // 列表需要到最顶层 + const topListElement = getTopList(range.startContainer); + if (topListElement) { + blockElement = topListElement; + } + + // 应到引用层,否则 > --- 会解析为 front-matter;列表中有 blockquote 则解析 blockquote;blockquote 中有列表则解析列表 + if (blockquoteElement && (!topListElement || (topListElement && !blockquoteElement.contains(topListElement)))) { + blockElement = blockquoteElement; + } + + // 修改脚注 + if (footnoteElement) { + blockElement = footnoteElement; + } + + html = blockElement.outerHTML; + + if (blockElement.tagName === "UL" || blockElement.tagName === "OL") { + // 如果为列表的话,需要把上下的列表都重绘 + const listPrevElement = blockElement.previousElementSibling; + const listNextElement = blockElement.nextElementSibling; + if (listPrevElement && (listPrevElement.tagName === "UL" || listPrevElement.tagName === "OL")) { + html = listPrevElement.outerHTML + html; + listPrevElement.remove(); + } + if (listNextElement && (listNextElement.tagName === "UL" || listNextElement.tagName === "OL")) { + html = html + listNextElement.outerHTML; + listNextElement.remove(); + } + // firefox 列表回车不会产生新的 list item https://github.com/Vanessa219/vditor/issues/194 + html = html.replace("

", "

  • "); + } else if (blockElement.previousElementSibling && + blockElement.previousElementSibling.textContent.replace(Constants.ZWSP, "") !== "" && + event && event.inputType === "insertParagraph") { + // 换行时需要处理上一段落 + html = blockElement.previousElementSibling.outerHTML + html; + blockElement.previousElementSibling.remove(); + } + if (!blockElement.innerText.startsWith("```")) { + // 添加链接引用 + vditor.ir.element.querySelectorAll("[data-type='link-ref-defs-block']").forEach((item) => { + if (item && !(blockElement as HTMLElement).isEqualNode(item)) { + html += item.outerHTML; + item.remove(); + } + }); + + // 添加脚注 + vditor.ir.element.querySelectorAll("[data-type='footnotes-block']").forEach((item) => { + if (item && !(blockElement as HTMLElement).isEqualNode(item)) { + html += item.outerHTML; + item.remove(); + } + }); + } + } else { + html = blockElement.innerHTML; + } + + log("SpinVditorIRDOM", html, "argument", vditor.options.debugger); + html = vditor.lute.SpinVditorIRDOM(html); + log("SpinVditorIRDOM", html, "result", vditor.options.debugger); + + if (isIRElement) { + blockElement.innerHTML = html; + } else { + blockElement.outerHTML = html; + + // 更新正文中的 tip + if (footnoteElement) { + const footnoteItemElement = hasClosestByAttribute(vditor.ir.element.querySelector("wbr"), + "data-type", "footnotes-def"); + if (footnoteItemElement) { + const footnoteItemText = footnoteItemElement.textContent; + const marker = footnoteItemText.substring(1, footnoteItemText.indexOf("]:")); + const footnoteRefElement = vditor.ir.element.querySelector(`sup[data-type="footnotes-ref"][data-footnotes-label="${marker}"]`); + if (footnoteRefElement) { + footnoteRefElement.setAttribute("aria-label", + footnoteItemText.substr(marker.length + 3).trim().substr(0, 24)); + } + } + } + } + + // linkref 合并及添加 + let firstLinkRefDefElement: HTMLElement; + const allLinkRefDefsElement = vditor.ir.element.querySelectorAll("[data-type='link-ref-defs-block']"); + allLinkRefDefsElement.forEach((item: HTMLElement, index) => { + if (index === 0) { + firstLinkRefDefElement = item; + } else { + firstLinkRefDefElement.insertAdjacentHTML("beforeend", item.innerHTML); + item.remove(); + } + }); + if (allLinkRefDefsElement.length > 0) { + vditor.ir.element.insertAdjacentElement("beforeend", allLinkRefDefsElement[0]); + } + + // 脚注合并后添加的末尾 + let firstFootnoteElement: HTMLElement; + const allFootnoteElement = vditor.ir.element.querySelectorAll("[data-type='footnotes-block']"); + allFootnoteElement.forEach((item: HTMLElement, index) => { + if (index === 0) { + firstFootnoteElement = item; + } else { + firstFootnoteElement.insertAdjacentHTML("beforeend", item.innerHTML); + item.remove(); + } + }); + if (allFootnoteElement.length > 0) { + vditor.ir.element.insertAdjacentElement("beforeend", allFootnoteElement[0]); + } + + setRangeByWbr(vditor.ir.element, range); + + vditor.ir.element.querySelectorAll(".vditor-ir__preview[data-render='2']").forEach((item: HTMLElement) => { + processCodeRender(item, vditor); + }); + + renderToc(vditor); + + processAfterRender(vditor, { + enableAddUndoStack: true, + enableHint: true, + enableInput: true, + }); +}; diff --git a/web/include/vditor/src/ts/ir/process.ts b/web/include/vditor/src/ts/ir/process.ts new file mode 100644 index 0000000..59512e9 --- /dev/null +++ b/web/include/vditor/src/ts/ir/process.ts @@ -0,0 +1,222 @@ +import {Constants} from "../constants"; +import {getMarkdown} from "../markdown/getMarkdown"; +import {removeCurrentToolbar} from "../toolbar/setToolbar"; +import {accessLocalStorage} from "../util/compatibility"; +import {listToggle} from "../util/fixBrowserBehavior"; +import {hasClosestBlock, hasClosestByAttribute, hasClosestByClassName, hasClosestByMatchTag} from "../util/hasClosest"; +import {getEditorRange, getSelectPosition, setRangeByWbr, setSelectionFocus} from "../util/selection"; +import {highlightToolbarIR} from "./highlightToolbarIR"; +import {input} from "./input"; + +export const processHint = (vditor: IVditor) => { + vditor.hint.render(vditor); + const startContainer = getEditorRange(vditor).startContainer; + // 代码块语言提示 + const preBeforeElement = hasClosestByAttribute(startContainer, "data-type", "code-block-info"); + if (preBeforeElement) { + if (preBeforeElement.textContent.replace(Constants.ZWSP, "") === "" && vditor.hint.recentLanguage) { + preBeforeElement.textContent = Constants.ZWSP + vditor.hint.recentLanguage; + const range = getEditorRange(vditor); + range.selectNodeContents(preBeforeElement); + } else { + const matchLangData: IHintData[] = []; + const key = + preBeforeElement.textContent.substring(0, getSelectPosition(preBeforeElement, vditor.ir.element).start) + .replace(Constants.ZWSP, ""); + (vditor.options.preview.hljs.langs || Constants.CODE_LANGUAGES).forEach((keyName) => { + if (keyName.indexOf(key.toLowerCase()) > -1) { + matchLangData.push({ + html: keyName, + value: keyName, + }); + } + }); + vditor.hint.genHTML(matchLangData, key, vditor); + } + } +}; + +export const processAfterRender = (vditor: IVditor, options = { + enableAddUndoStack: true, + enableHint: false, + enableInput: true, +}) => { + if (options.enableHint) { + processHint(vditor); + } + + clearTimeout(vditor.ir.processTimeoutId); + vditor.ir.processTimeoutId = window.setTimeout(() => { + if (vditor.ir.composingLock) { + return; + } + const text = getMarkdown(vditor); + if (typeof vditor.options.input === "function" && options.enableInput) { + vditor.options.input(text); + } + + if (vditor.options.counter.enable) { + vditor.counter.render(vditor, text); + } + + if (vditor.options.cache.enable && accessLocalStorage()) { + localStorage.setItem(vditor.options.cache.id, text); + if (vditor.options.cache.after) { + vditor.options.cache.after(text); + } + } + + if (vditor.devtools) { + vditor.devtools.renderEchart(vditor); + } + + if (options.enableAddUndoStack) { + vditor.undo.addToUndoStack(vditor); + } + }, vditor.options.undoDelay); +}; + +export const processHeading = (vditor: IVditor, value: string) => { + const range = getEditorRange(vditor); + const headingElement = hasClosestBlock(range.startContainer) || range.startContainer as HTMLElement; + if (headingElement) { + const headingMarkerElement = headingElement.querySelector(".vditor-ir__marker--heading"); + if (headingMarkerElement) { + headingMarkerElement.innerHTML = value; + } else { + headingElement.insertAdjacentText("afterbegin", value); + range.selectNodeContents(headingElement); + range.collapse(false); + } + input(vditor, range.cloneRange()); + highlightToolbarIR(vditor); + } +}; + +const removeInline = (range: Range, vditor: IVditor, type: string) => { + const inlineElement = hasClosestByAttribute(range.startContainer, "data-type", type) as HTMLElement; + if (inlineElement) { + inlineElement.firstElementChild.remove(); + inlineElement.lastElementChild.remove(); + range.insertNode(document.createElement("wbr")); + const tempElement = document.createElement("div"); + tempElement.innerHTML = vditor.lute.SpinVditorIRDOM(inlineElement.outerHTML); + inlineElement.outerHTML = tempElement.firstElementChild.innerHTML.trim(); + } +}; + +export const processToolbar = (vditor: IVditor, actionBtn: Element, prefix: string, suffix: string) => { + const range = getEditorRange(vditor); + const commandName = actionBtn.getAttribute("data-type"); + let typeElement = range.startContainer as HTMLElement; + if (typeElement.nodeType === 3) { + typeElement = typeElement.parentElement; + } + let useHighlight = true; + // 移除 + if (actionBtn.classList.contains("vditor-menu--current")) { + if (commandName === "quote") { + const quoteElement = hasClosestByMatchTag(typeElement, "BLOCKQUOTE"); + if (quoteElement) { + range.insertNode(document.createElement("wbr")); + quoteElement.outerHTML = quoteElement.innerHTML.trim() === "" ? + `

    ${quoteElement.innerHTML}

    ` : quoteElement.innerHTML; + } + } else if (commandName === "link") { + const aElement = hasClosestByAttribute(range.startContainer, "data-type", "a") as HTMLElement; + if (aElement) { + const aTextElement = hasClosestByClassName(range.startContainer, "vditor-ir__link"); + if (aTextElement) { + range.insertNode(document.createElement("wbr")); + aElement.outerHTML = aTextElement.innerHTML; + } else { + aElement.outerHTML = aElement.querySelector(".vditor-ir__link").innerHTML + ""; + } + } + } else if (commandName === "italic") { + removeInline(range, vditor, "em"); + } else if (commandName === "bold") { + removeInline(range, vditor, "strong"); + } else if (commandName === "strike") { + removeInline(range, vditor, "s"); + } else if (commandName === "inline-code") { + removeInline(range, vditor, "code"); + } else if (commandName === "check" || commandName === "list" || commandName === "ordered-list") { + listToggle(vditor, range, commandName); + useHighlight = false; + actionBtn.classList.remove("vditor-menu--current"); + } + } else { + // 添加 + if (vditor.ir.element.childNodes.length === 0) { + vditor.ir.element.innerHTML = '

    '; + setRangeByWbr(vditor.ir.element, range); + } + const blockElement = hasClosestBlock(range.startContainer); + if (commandName === "line") { + if (blockElement) { + const hrHTML = '

    \n

    '; + if (blockElement.innerHTML.trim() === "") { + blockElement.outerHTML = hrHTML; + } else { + blockElement.insertAdjacentHTML("afterend", hrHTML); + } + } + } else if (commandName === "quote") { + if (blockElement) { + range.insertNode(document.createElement("wbr")); + blockElement.outerHTML = `
    ${blockElement.outerHTML}
    `; + useHighlight = false; + actionBtn.classList.add("vditor-menu--current"); + } + } else if (commandName === "link") { + let html; + if (range.toString() === "") { + html = `${prefix}${suffix}`; + } else { + html = `${prefix}${range.toString()}${suffix.replace(")", ")")}`; + } + document.execCommand("insertHTML", false, html); + useHighlight = false; + actionBtn.classList.add("vditor-menu--current"); + } else if (commandName === "italic" || commandName === "bold" || commandName === "strike" + || commandName === "inline-code" || commandName === "code" || commandName === "table") { + let html; + if (range.toString() === "") { + html = `${prefix}${suffix}`; + } else { + if (commandName === "code") { + html = `${prefix}\n${range.toString()}${suffix}`; + } else if (commandName === "table") { + html = `${prefix}${range.toString()}${suffix}`; + } else { + html = `${prefix}${range.toString()}${suffix}`; + } + range.deleteContents(); + } + if (commandName === "table" || commandName === "code") { + html = "\n" + html + "\n\n"; + } + + const spanElement = document.createElement("span"); + spanElement.innerHTML = html; + range.insertNode(spanElement); + input(vditor, range); + + if (commandName === "table") { + range.selectNodeContents(getSelection().getRangeAt(0).startContainer.parentElement); + setSelectionFocus(range); + } + } else if (commandName === "check" || commandName === "list" || commandName === "ordered-list") { + listToggle(vditor, range, commandName, false); + useHighlight = false; + removeCurrentToolbar(vditor.toolbar.elements, ["check", "list", "ordered-list"]); + actionBtn.classList.add("vditor-menu--current"); + } + } + setRangeByWbr(vditor.ir.element, range); + processAfterRender(vditor); + if (useHighlight) { + highlightToolbarIR(vditor); + } +}; diff --git a/web/include/vditor/src/ts/ir/processKeydown.ts b/web/include/vditor/src/ts/ir/processKeydown.ts new file mode 100644 index 0000000..47e4452 --- /dev/null +++ b/web/include/vditor/src/ts/ir/processKeydown.ts @@ -0,0 +1,238 @@ +import {Constants} from "../constants"; +import {hidePanel} from "../toolbar/setToolbar"; +import {isCtrl} from "../util/compatibility"; +import { + fixBlockquote, fixCJKPosition, + fixCodeBlock, fixCursorDownInlineMath, + fixDelete, fixFirefoxArrowUpTable, fixGSKeyBackspace, fixHR, + fixList, + fixMarkdown, + fixTab, + fixTable, + fixTask, + insertAfterBlock, insertBeforeBlock, isFirstCell, isLastCell, +} from "../util/fixBrowserBehavior"; +import { + hasClosestBlock, + hasClosestByAttribute, + hasClosestByClassName, + hasClosestByMatchTag, +} from "../util/hasClosest"; +import {hasClosestByHeadings} from "../util/hasClosestByHeadings"; +import {matchHotKey} from "../util/hotKey"; +import {getEditorRange, getSelectPosition, setSelectionFocus} from "../util/selection"; +import {keydownToc} from "../util/toc"; +import {expandMarker} from "./expandMarker"; +import {processAfterRender, processHeading} from "./process"; + +export const processKeydown = (vditor: IVditor, event: KeyboardEvent) => { + vditor.ir.composingLock = event.isComposing; + if (event.isComposing) { + return false; + } + + // 添加第一次记录 undo 的光标 + if (event.key.indexOf("Arrow") === -1 && event.key !== "Meta" && event.key !== "Control" && event.key !== "Alt" && + event.key !== "Shift" && event.key !== "CapsLock" && event.key !== "Escape" && !/^F\d{1,2}$/.test(event.key)) { + vditor.undo.recordFirstPosition(vditor, event); + } + + const range = getEditorRange(vditor); + const startContainer = range.startContainer; + + if (!fixGSKeyBackspace(event, vditor, startContainer)) { + return false; + } + + fixCJKPosition(range, vditor, event); + + fixHR(range); + + // 仅处理以下快捷键操作 + if (event.key !== "Enter" && event.key !== "Tab" && event.key !== "Backspace" && event.key.indexOf("Arrow") === -1 + && !isCtrl(event) && event.key !== "Escape" && event.key !== "Delete") { + return false; + } + + // 斜体、粗体、内联代码块中换行 + const newlineElement = hasClosestByAttribute(startContainer, "data-newline", "1"); + if (!isCtrl(event) && !event.altKey && !event.shiftKey && event.key === "Enter" && newlineElement + && range.startOffset < newlineElement.textContent.length) { + const beforeMarkerElement = newlineElement.previousElementSibling; + if (beforeMarkerElement) { + range.insertNode(document.createTextNode(beforeMarkerElement.textContent)); + range.collapse(false); + } + const afterMarkerElement = newlineElement.nextSibling; + if (afterMarkerElement) { + range.insertNode(document.createTextNode(afterMarkerElement.textContent)); + range.collapse(true); + } + } + + const pElement = hasClosestByMatchTag(startContainer, "P"); + // md 处理 + if (fixMarkdown(event, vditor, pElement, range)) { + return true; + } + // li + if (fixList(range, vditor, pElement, event)) { + return true; + } + // blockquote + if (fixBlockquote(vditor, range, event, pElement)) { + return true; + } + // 代码块 + const preRenderElement = hasClosestByClassName(startContainer, "vditor-ir__marker--pre"); + if (preRenderElement && preRenderElement.tagName === "PRE") { + const codeRenderElement = preRenderElement.firstChild as HTMLElement; + if (fixCodeBlock(vditor, event, preRenderElement, range)) { + return true; + } + // 数学公式上无元素,按上或左将添加新块 + if ((codeRenderElement.getAttribute("data-type") === "math-block" + || codeRenderElement.getAttribute("data-type") === "html-block") && + insertBeforeBlock(vditor, event, range, codeRenderElement, preRenderElement.parentElement)) { + return true; + } + + // 代码块下无元素或者为代码块/table 元素,添加空块 + if (insertAfterBlock(vditor, event, range, codeRenderElement, preRenderElement.parentElement)) { + return true; + } + } + // 代码块语言 + const preBeforeElement = hasClosestByAttribute(startContainer, "data-type", "code-block-info"); + if (preBeforeElement) { + if (event.key === "Enter" || event.key === "Tab") { + range.selectNodeContents(preBeforeElement.nextElementSibling.firstChild); + range.collapse(true); + event.preventDefault(); + hidePanel(vditor, ["hint"]); + return true; + } + + if (event.key === "Backspace") { + const start = getSelectPosition(preBeforeElement, vditor.ir.element).start; + if (start === 1) { // 删除零宽空格 + range.setStart(startContainer, 0); + } + if (start === 2) { // 删除时清空自动补全语言 + vditor.hint.recentLanguage = ""; + } + } + if (insertBeforeBlock(vditor, event, range, preBeforeElement, preBeforeElement.parentElement)) { + // 上无元素,按上或左将添加新块 + hidePanel(vditor, ["hint"]); + return true; + } + } + + // table + const cellElement = hasClosestByMatchTag(startContainer, "TD") || + hasClosestByMatchTag(startContainer, "TH"); + if (event.key.indexOf("Arrow") > -1 && cellElement) { + const tableElement = isFirstCell(cellElement); + if (tableElement && insertBeforeBlock(vditor, event, range, cellElement, tableElement)) { + return true; + } + + const table2Element = isLastCell(cellElement); + if (table2Element && insertAfterBlock(vditor, event, range, cellElement, table2Element)) { + return true; + } + } + if (fixTable(vditor, event, range)) { + return true; + } + + // task list + if (fixTask(vditor, range, event)) { + return true; + } + + // tab + if (fixTab(vditor, range, event)) { + return true; + } + + const headingElement = hasClosestByHeadings(startContainer); + if (headingElement) { + // enter++: 标题变大 + if (matchHotKey("⌘=", event)) { + const headingMarkerElement = headingElement.querySelector(".vditor-ir__marker--heading"); + if (headingMarkerElement && headingMarkerElement.textContent.trim().length > 1) { + processHeading(vditor, headingMarkerElement.textContent.substr(1)); + } + event.preventDefault(); + return true; + } + + // enter++: 标题变小 + if (matchHotKey("⌘-", event)) { + const headingMarkerElement = headingElement.querySelector(".vditor-ir__marker--heading"); + if (headingMarkerElement && headingMarkerElement.textContent.trim().length < 6) { + processHeading(vditor, headingMarkerElement.textContent.trim() + "# "); + } + event.preventDefault(); + return true; + } + } + const blockElement = hasClosestBlock(startContainer); + if (event.key === "Backspace" && !isCtrl(event) && !event.shiftKey && !event.altKey && range.toString() === "") { + if (fixDelete(vditor, range, event, pElement)) { + return true; + } + + if (blockElement && blockElement.previousElementSibling + && blockElement.tagName !== "UL" && blockElement.tagName !== "OL" + && (blockElement.previousElementSibling.getAttribute("data-type") === "code-block" || + blockElement.previousElementSibling.getAttribute("data-type") === "math-block")) { + const rangeStart = getSelectPosition(blockElement, vditor.ir.element, range).start; + if (rangeStart === 0 || (rangeStart === 1 && blockElement.innerText.startsWith(Constants.ZWSP))) { + // 当前块删除后光标落于代码渲染块上,当前块会被删除,因此需要阻止事件,不能和 keyup 中的代码块处理合并 + range.selectNodeContents(blockElement.previousElementSibling.querySelector(".vditor-ir__marker--pre code")); + range.collapse(false); + expandMarker(range, vditor); + if (blockElement.textContent.trim().replace(Constants.ZWSP, "") === "") { + // 当前块为空且不是最后一个时,需要删除 + blockElement.remove(); + processAfterRender(vditor); + } + event.preventDefault(); + return true; + } + } + + // 光标位于标题前,marker 后 + if (headingElement) { + const headingLength = headingElement.firstElementChild.textContent.length; + if (getSelectPosition(headingElement, vditor.ir.element).start === headingLength && headingLength !== 0) { + range.setStart(headingElement.firstElementChild.firstChild, headingLength - 1); + range.collapse(true); + setSelectionFocus(range); + } + } + } + + if ((event.key === "ArrowUp" || event.key === "ArrowDown") && blockElement) { + // https://github.com/Vanessa219/vditor/issues/358 + blockElement.querySelectorAll(".vditor-ir__node").forEach((item: HTMLElement) => { + if (!item.contains(startContainer)) { + item.classList.add("vditor-ir__node--hidden"); + } + }); + + if (fixFirefoxArrowUpTable(event, blockElement, range)) { + return true; + } + } + fixCursorDownInlineMath(range, event.key); + + if (blockElement && keydownToc(blockElement, vditor, event, range)) { + event.preventDefault(); + return true; + } + return false; +}; diff --git a/web/include/vditor/src/ts/markdown/abcRender.ts b/web/include/vditor/src/ts/markdown/abcRender.ts new file mode 100644 index 0000000..577891c --- /dev/null +++ b/web/include/vditor/src/ts/markdown/abcRender.ts @@ -0,0 +1,27 @@ +import {Constants} from "../constants"; +import {addScript} from "../util/addScript"; +import {abcRenderAdapter} from "./adapterRender"; + +declare const ABCJS: { + renderAbc(element: HTMLElement, text: string): void; +}; + +export const abcRender = (element: (HTMLElement | Document) = document, cdn = Constants.CDN) => { + const abcElements = abcRenderAdapter.getElements(element); + if (abcElements.length > 0) { + addScript(`${cdn}/dist/js/abcjs/abcjs_basic.min.js`, "vditorAbcjsScript").then(() => { + abcElements.forEach((item: HTMLDivElement) => { + if (item.parentElement.classList.contains("vditor-wysiwyg__pre") || + item.parentElement.classList.contains("vditor-ir__marker--pre")) { + return; + } + if (item.getAttribute("data-processed") === "true") { + return; + } + ABCJS.renderAbc(item, abcRenderAdapter.getCode(item).trim()); + item.style.overflowX = "auto"; + item.setAttribute("data-processed", "true"); + }); + }); + } +}; diff --git a/web/include/vditor/src/ts/markdown/adapterRender.ts b/web/include/vditor/src/ts/markdown/adapterRender.ts new file mode 100644 index 0000000..640538b --- /dev/null +++ b/web/include/vditor/src/ts/markdown/adapterRender.ts @@ -0,0 +1,37 @@ +export const mathRenderAdapter = { + getCode: (mathElement: Element) => mathElement.textContent, + getElements: (element: HTMLElement) => element.querySelectorAll(".language-math"), +}; +export const mermaidRenderAdapter = { + /** 不仅要返回code,并且需要将 code 设置为 el 的 innerHTML */ + getCode: (el: Element) => el.textContent, + getElements: (element: HTMLElement) => element.querySelectorAll(".language-mermaid"), +}; +export const markmapRenderAdapter = { + getCode: (el: Element) => el.textContent, + getElements: (element: HTMLElement) => element.querySelectorAll(".language-markmap"), +}; +export const mindmapRenderAdapter = { + getCode: (el: Element) => el.getAttribute("data-code"), + getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-mindmap"), +}; +export const chartRenderAdapter = { + getCode: (el: HTMLElement) => el.innerText, + getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-echarts"), +}; +export const abcRenderAdapter = { + getCode: (el: Element) => el.textContent, + getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-abc"), +}; +export const graphvizRenderAdapter = { + getCode: (el: Element) => el.textContent, + getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-graphviz"), +}; +export const flowchartRenderAdapter = { + getCode: (el: Element) => el.textContent, + getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-flowchart"), +}; +export const plantumlRenderAdapter = { + getCode: (el: Element) => el.textContent, + getElements: (el: HTMLElement | Document) => el.querySelectorAll(".language-plantuml"), +}; diff --git a/web/include/vditor/src/ts/markdown/anchorRender.ts b/web/include/vditor/src/ts/markdown/anchorRender.ts new file mode 100644 index 0000000..3deaf91 --- /dev/null +++ b/web/include/vditor/src/ts/markdown/anchorRender.ts @@ -0,0 +1,19 @@ +export const anchorRender = (type: number) => { + document.querySelectorAll(".vditor-anchor").forEach((anchor: HTMLLinkElement) => { + if (type === 1) { + anchor.classList.add("vditor-anchor--left"); + } + anchor.onclick = () => { + const id = anchor.getAttribute("href").substr(1); + const top = document.getElementById("vditorAnchor-" + id).offsetTop; + document.querySelector("html").scrollTop = top; + }; + }); + + window.onhashchange = () => { + const element = document.getElementById("vditorAnchor-" + decodeURIComponent(window.location.hash.substr(1))); + if (element) { + document.querySelector("html").scrollTop = element.offsetTop; + } + }; +}; diff --git a/web/include/vditor/src/ts/markdown/chartRender.ts b/web/include/vditor/src/ts/markdown/chartRender.ts new file mode 100644 index 0000000..bd6be9f --- /dev/null +++ b/web/include/vditor/src/ts/markdown/chartRender.ts @@ -0,0 +1,38 @@ +import {Constants} from "../constants"; +import {addScript} from "../util/addScript"; +import {chartRenderAdapter} from "./adapterRender"; +import {looseJsonParse} from "../util/function"; + +declare const echarts: { + init(element: HTMLElement, theme?: string): IEChart; +}; + +export const chartRender = (element: (HTMLElement | Document) = document, cdn = Constants.CDN, theme: string) => { + const echartsElements = chartRenderAdapter.getElements(element); + if (echartsElements.length > 0) { + addScript(`${cdn}/dist/js/echarts/echarts.min.js?v=5.5.1`, "vditorEchartsScript").then(() => { + echartsElements.forEach(async (e: HTMLDivElement) => { + if (e.parentElement.classList.contains("vditor-wysiwyg__pre") || + e.parentElement.classList.contains("vditor-ir__marker--pre")) { + return; + } + + const text = chartRenderAdapter.getCode(e).trim(); + if (!text) { + return; + } + try { + if (e.getAttribute("data-processed") === "true") { + return; + } + const option = await looseJsonParse(text); + echarts.init(e, theme === "dark" ? "dark" : undefined).setOption(option); + e.setAttribute("data-processed", "true"); + } catch (error) { + e.className = "vditor-reset--error"; + e.innerHTML = `echarts render error:
    ${error}`; + } + }); + }); + } +}; diff --git a/web/include/vditor/src/ts/markdown/codeRender.ts b/web/include/vditor/src/ts/markdown/codeRender.ts new file mode 100644 index 0000000..6ca2967 --- /dev/null +++ b/web/include/vditor/src/ts/markdown/codeRender.ts @@ -0,0 +1,60 @@ +import {code160to32} from "../util/code160to32"; +import {Constants} from "../constants"; + +export const codeRender = (element: HTMLElement, option?: IHljs) => { + Array.from(element.querySelectorAll("pre > code")).filter((e, index) => { + if (e.parentElement.classList.contains("vditor-wysiwyg__pre") || + e.parentElement.classList.contains("vditor-ir__marker--pre")) { + return false; + } + if (e.classList.contains("language-mermaid") || e.classList.contains("language-flowchart") || + e.classList.contains("language-echarts") || e.classList.contains("language-mindmap") || + e.classList.contains("language-plantuml") || e.classList.contains("language-markmap") || + e.classList.contains("language-abc") || e.classList.contains("language-graphviz") || + e.classList.contains("language-math")) { + return false; + } + + if (e.style.maxHeight.indexOf("px") > -1) { + return false; + } + + // 避免预览区在渲染后由于代码块过多产生性能问题 https://github.com/b3log/vditor/issues/67 + if (element.classList.contains("vditor-preview") && index > 5) { + return false; + } + return true; + }).forEach((e) => { + let codeText = e.innerText; + if (e.classList.contains("highlight-chroma")) { + const codeElement = e.cloneNode(true) as HTMLElement; + codeElement.querySelectorAll(".highlight-ln").forEach((item: HTMLElement) => { + item.remove(); + }); + codeText = codeElement.innerText; + } else if (codeText.endsWith("\n")) { + codeText = codeText.substr(0, codeText.length - 1) + } + let iconHTML = ''; + if (!document.getElementById("vditorIconScript")) { + iconHTML = ''; + } + + const divElement = document.createElement("div"); + divElement.className = "vditor-copy"; + divElement.innerHTML = `${iconHTML}`; + const textarea = document.createElement("textarea"); + textarea.value = code160to32(codeText); + divElement.insertAdjacentElement("afterbegin", textarea); + if (option && option.renderMenu) { + option.renderMenu(e, divElement); + } + e.before(divElement); + e.style.maxHeight = (window.outerHeight - 40) + "px"; + // https://github.com/Vanessa219/vditor/issues/1356 + e.insertAdjacentHTML("afterend", `${Constants.ZWSP}`) + }); +}; diff --git a/web/include/vditor/src/ts/markdown/flowchartRender.ts b/web/include/vditor/src/ts/markdown/flowchartRender.ts new file mode 100644 index 0000000..e0b1b4a --- /dev/null +++ b/web/include/vditor/src/ts/markdown/flowchartRender.ts @@ -0,0 +1,25 @@ +import {Constants} from "../constants"; +import {addScript} from "../util/addScript"; +import {flowchartRenderAdapter} from "./adapterRender"; + +declare const flowchart: { + parse(text: string): { drawSVG: (type: HTMLElement) => void }; +}; + +export const flowchartRender = (element: HTMLElement, cdn = Constants.CDN) => { + const flowchartElements = flowchartRenderAdapter.getElements(element); + if (flowchartElements.length === 0) { + return; + } + addScript(`${cdn}/dist/js/flowchart.js/flowchart.min.js`, "vditorFlowchartScript").then(() => { + flowchartElements.forEach((item: HTMLElement) => { + if (item.getAttribute("data-processed") === "true") { + return; + } + const flowchartObj = flowchart.parse(flowchartRenderAdapter.getCode(item)); + item.innerHTML = ""; + flowchartObj.drawSVG(item); + item.setAttribute("data-processed", "true"); + }); + }); +}; diff --git a/web/include/vditor/src/ts/markdown/getHTML.ts b/web/include/vditor/src/ts/markdown/getHTML.ts new file mode 100644 index 0000000..20c528c --- /dev/null +++ b/web/include/vditor/src/ts/markdown/getHTML.ts @@ -0,0 +1,11 @@ +import {getMarkdown} from "./getMarkdown"; + +export const getHTML = (vditor: IVditor) => { + if (vditor.currentMode === "sv") { + return vditor.lute.Md2HTML(getMarkdown(vditor)); + } else if (vditor.currentMode === "wysiwyg") { + return vditor.lute.VditorDOM2HTML(vditor.wysiwyg.element.innerHTML); + } else if (vditor.currentMode === "ir") { + return vditor.lute.VditorIRDOM2HTML(vditor.ir.element.innerHTML); + } +}; diff --git a/web/include/vditor/src/ts/markdown/getMarkdown.ts b/web/include/vditor/src/ts/markdown/getMarkdown.ts new file mode 100644 index 0000000..c3578c8 --- /dev/null +++ b/web/include/vditor/src/ts/markdown/getMarkdown.ts @@ -0,0 +1,12 @@ +import {code160to32} from "../util/code160to32"; + +export const getMarkdown = (vditor: IVditor) => { + if (vditor.currentMode === "sv") { + return code160to32(`${vditor.sv.element.textContent}\n`.replace(/\n\n$/, "\n")); + } else if (vditor.currentMode === "wysiwyg") { + return vditor.lute.VditorDOM2Md(vditor.wysiwyg.element.innerHTML); + } else if (vditor.currentMode === "ir") { + return vditor.lute.VditorIRDOM2Md(vditor.ir.element.innerHTML); + } + return ""; +}; diff --git a/web/include/vditor/src/ts/markdown/graphvizRender.ts b/web/include/vditor/src/ts/markdown/graphvizRender.ts new file mode 100644 index 0000000..c809f28 --- /dev/null +++ b/web/include/vditor/src/ts/markdown/graphvizRender.ts @@ -0,0 +1,49 @@ +import {Constants} from "../constants"; +import {addScript} from "../util/addScript"; +import {graphvizRenderAdapter} from "./adapterRender"; + +declare class Viz { + public renderSVGElement: (code: string) => Promise; + + constructor({ }: { worker: Worker }); +} + +export const graphvizRender = (element: HTMLElement, cdn = Constants.CDN) => { + const graphvizElements = graphvizRenderAdapter.getElements(element); + + if (graphvizElements.length === 0) { + return; + } + addScript(`${cdn}/dist/js/graphviz/viz.js`, "vditorGraphVizScript").then(() => { + graphvizElements.forEach((e: HTMLDivElement) => { + const code = graphvizRenderAdapter.getCode(e); + if (e.parentElement.classList.contains("vditor-wysiwyg__pre") || + e.parentElement.classList.contains("vditor-ir__marker--pre")) { + return; + } + + if (e.getAttribute("data-processed") === "true" || code.trim() === "") { + return; + } + + try { + const blob = new Blob([`importScripts('${(document.getElementById("vditorGraphVizScript") as HTMLScriptElement).src.replace("viz.js", "full.render.js")}');`], + { type: "application/javascript" }); + const url = window.URL || window.webkitURL; + const blobUrl = url.createObjectURL(blob); + const worker = new Worker(blobUrl); + new Viz({ worker }) + .renderSVGElement(code).then((result: HTMLElement) => { + e.innerHTML = result.outerHTML; + }).catch((error) => { + e.innerHTML = `graphviz render error:
    ${error}`; + e.className = "vditor-reset--error"; + }); + } catch (e) { + console.error("graphviz error", e); + } + + e.setAttribute("data-processed", "true"); + }); + }); +}; diff --git a/web/include/vditor/src/ts/markdown/highlightRender.ts b/web/include/vditor/src/ts/markdown/highlightRender.ts new file mode 100644 index 0000000..1f8f6f8 --- /dev/null +++ b/web/include/vditor/src/ts/markdown/highlightRender.ts @@ -0,0 +1,90 @@ +import {Constants} from "../constants"; +import {addScript} from "../util/addScript"; +import {addStyle} from "../util/addStyle"; + +declare const hljs: { + highlightElement(element: Element): void; +}; + +export const highlightRender = (hljsOption?: IHljs, element: HTMLElement | Document = document, + cdn = Constants.CDN) => { + let style = hljsOption.style; + if (!Constants.CODE_THEME.includes(style)) { + style = "github"; + } + const vditorHljsStyle = document.getElementById("vditorHljsStyle") as HTMLLinkElement; + const href = `${cdn}/dist/js/highlight.js/styles/${style}.css`; + if (vditorHljsStyle && vditorHljsStyle.getAttribute('href') !== href) { + vditorHljsStyle.remove(); + } + addStyle(`${cdn}/dist/js/highlight.js/styles/${style}.css`, "vditorHljsStyle"); + + if (hljsOption.enable === false) { + return; + } + + const codes = element.querySelectorAll("pre > code"); + if (codes.length === 0) { + return; + } + + addScript(`${cdn}/dist/js/highlight.js/highlight.pack.js`, "vditorHljsScript").then(() => { + addScript(`${cdn}/dist/js/highlight.js/solidity.min.js`, "vditorHljsSolidityScript").then(() => { + addScript(`${cdn}/dist/js/highlight.js/yul.min.js`, "vditorHljsYulScript").then(() => { + element.querySelectorAll("pre > code").forEach((block) => { + // ir & wysiwyg 区域不渲染 + if (block.parentElement.classList.contains("vditor-ir__marker--pre") || + block.parentElement.classList.contains("vditor-wysiwyg__pre")) { + return; + } + + if (block.classList.contains("language-mermaid") || block.classList.contains("language-flowchart") || + block.classList.contains("language-echarts") || block.classList.contains("language-mindmap") || + block.classList.contains("language-plantuml") || + block.classList.contains("language-abc") || block.classList.contains("language-graphviz") || + block.classList.contains("language-math")) { + return; + } + + if (hljsOption.defaultLang !== "" && block.className.indexOf("language-") === -1) { + block.classList.add("language-" + hljsOption.defaultLang) + } + + hljs.highlightElement(block); + + if (!hljsOption.lineNumber) { + return; + } + + block.classList.add("vditor-linenumber"); + let linenNumberTemp: HTMLDivElement = block.querySelector(".vditor-linenumber__temp"); + if (!linenNumberTemp) { + linenNumberTemp = document.createElement("div"); + linenNumberTemp.className = "vditor-linenumber__temp"; + block.insertAdjacentElement("beforeend", linenNumberTemp); + } + const whiteSpace = getComputedStyle(block).whiteSpace; + let isSoftWrap = false; + if (whiteSpace === "pre-wrap" || whiteSpace === "pre-line") { + isSoftWrap = true; + } + let lineNumberHTML = ""; + const lineList = block.textContent.split(/\r\n|\r|\n/g); + lineList.pop(); + lineList.map((line) => { + let lineHeight = ""; + if (isSoftWrap) { + linenNumberTemp.textContent = line || "\n"; + lineHeight = ` style="height:${linenNumberTemp.getBoundingClientRect().height}px"`; + } + lineNumberHTML += ``; + }); + + linenNumberTemp.style.display = "none"; + lineNumberHTML = `${lineNumberHTML}`; + block.insertAdjacentHTML("beforeend", lineNumberHTML); + }); + }) + }) + }); +}; diff --git a/web/include/vditor/src/ts/markdown/lazyLoadImageRender.ts b/web/include/vditor/src/ts/markdown/lazyLoadImageRender.ts new file mode 100644 index 0000000..2c3e8f8 --- /dev/null +++ b/web/include/vditor/src/ts/markdown/lazyLoadImageRender.ts @@ -0,0 +1,56 @@ +declare global { + interface Window { + vditorImageIntersectionObserver: IntersectionObserver; + } +} + +export const lazyLoadImageRender = (element: (HTMLElement | Document) = document) => { + const loadImg = (it: HTMLImageElement) => { + const testImage = document.createElement("img"); + testImage.src = it.getAttribute("data-src"); + testImage.addEventListener("load", () => { + if (!it.getAttribute("style") && !it.getAttribute("class") && + !it.getAttribute("width") && !it.getAttribute("height")) { + if (testImage.naturalHeight > testImage.naturalWidth && + testImage.naturalWidth / testImage.naturalHeight < + document.querySelector(".vditor-reset").clientWidth / (window.innerHeight - 40) && + testImage.naturalHeight > (window.innerHeight - 40)) { + it.style.height = (window.innerHeight - 40) + "px"; + } + } + + it.src = testImage.src; + }); + it.removeAttribute("data-src"); + }; + + if (!("IntersectionObserver" in window)) { + element.querySelectorAll("img").forEach((imgElement: HTMLImageElement) => { + if (imgElement.getAttribute("data-src")) { + loadImg(imgElement); + } + }); + return false; + } + + if (window.vditorImageIntersectionObserver) { + window.vditorImageIntersectionObserver.disconnect(); + element.querySelectorAll("img").forEach((imgElement) => { + window.vditorImageIntersectionObserver.observe(imgElement); + }); + } else { + window.vditorImageIntersectionObserver = new IntersectionObserver((entries) => { + entries.forEach((entrie: IntersectionObserverEntry & { target: HTMLImageElement }) => { + if ((typeof entrie.isIntersecting === "undefined" + ? entrie.intersectionRatio !== 0 + : entrie.isIntersecting) + && entrie.target.getAttribute("data-src")) { + loadImg(entrie.target); + } + }); + }); + element.querySelectorAll("img").forEach((imgElement) => { + window.vditorImageIntersectionObserver.observe(imgElement); + }); + } +}; diff --git a/web/include/vditor/src/ts/markdown/markmapRender.ts b/web/include/vditor/src/ts/markdown/markmapRender.ts new file mode 100644 index 0000000..221ad9a --- /dev/null +++ b/web/include/vditor/src/ts/markdown/markmapRender.ts @@ -0,0 +1,57 @@ +import {Constants} from "../constants"; +import {addScript} from "../util/addScript"; +import {markmapRenderAdapter} from "./adapterRender"; + +declare const window: any; +const enabled: Record = {}; + +const transform = (transformer: any,content: string)=>{ + const result = transformer.transform(content); + const keys = Object.keys(result.features).filter((key) => !enabled[key]); + keys.forEach((key) => { + enabled[key] = true; + }); + const { styles, scripts } = transformer.getAssets(keys); + const { markmap } = window; + if (styles) markmap.loadCSS(styles); + if (scripts) markmap.loadJS(scripts); + return result; +} + +const init = (el: HTMLElement,code: string) => { + const { Transformer, Markmap, deriveOptions , globalCSS} = window.markmap; + const transformer = new Transformer(); + el.innerHTML = ''; + const svg = el.firstChild as SVGElement; + const mm = Markmap.create(svg, null); + const { root, frontmatter } = transform(transformer, code); + const markmapOptions = frontmatter?.markmap; + const frontmatterOptions = deriveOptions(markmapOptions); + mm.setData(root, frontmatterOptions); + mm.fit(); + } + + +export const markmapRender = (element: HTMLElement, cdn = Constants.CDN, theme: string) => { + const markmapElements = markmapRenderAdapter.getElements(element); + if (markmapElements.length === 0) { + return; + } + addScript(`${cdn}/dist/js/markmap/markmap.min.js`, "vditorMermaidScript").then(() => { + markmapElements.forEach((item) => { + const code = markmapRenderAdapter.getCode(item); + if (item.getAttribute("data-processed") === "true" || code.trim() === "") { + return; + } + const render = document.createElement("div") + render.className = "language-markmap" + item.parentNode.appendChild(render) + init(render,code) + + if(item.parentNode.childNodes[0].nodeName == "CODE"){ + item.parentNode.removeChild(item.parentNode.childNodes[0]) + } + }); + }); + +}; diff --git a/web/include/vditor/src/ts/markdown/mathRender.ts b/web/include/vditor/src/ts/markdown/mathRender.ts new file mode 100644 index 0000000..e4f5807 --- /dev/null +++ b/web/include/vditor/src/ts/markdown/mathRender.ts @@ -0,0 +1,148 @@ +import {Constants} from "../constants"; +import {addScript, addScriptSync} from "../util/addScript"; +import {addStyle} from "../util/addStyle"; +import {code160to32} from "../util/code160to32"; +import {mathRenderAdapter} from "./adapterRender"; + +declare const katex: { + renderToString(math: string, option: { + displayMode: boolean; + output: string; + macros: object; + }): string; +}; + +declare global { + interface Window { + MathJax: any; + } +} + +export const mathRender = (element: HTMLElement, options?: { cdn?: string, math?: IMath }) => { + const mathElements = mathRenderAdapter.getElements(element); + + if (mathElements.length === 0) { + return; + } + + const defaultOptions = { + cdn: Constants.CDN, + math: { + engine: "KaTeX", + inlineDigit: false, + macros: {}, + }, + }; + + if (options && options.math) { + options.math = + Object.assign({}, defaultOptions.math, options.math); + } + options = Object.assign({}, defaultOptions, options); + + if (options.math.engine === "KaTeX") { + addStyle(`${options.cdn}/dist/js/katex/katex.min.css?v=0.16.9`, "vditorKatexStyle"); + addScript(`${options.cdn}/dist/js/katex/katex.min.js?v=0.16.9`, "vditorKatexScript").then(() => { + addScript(`${options.cdn}/dist/js/katex/mhchem.min.js?v=0.16.9`, "vditorKatexChemScript").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; + } + const math = code160to32(mathRenderAdapter.getCode(mathElement)); + mathElement.setAttribute("data-math", math); + try { + mathElement.innerHTML = katex.renderToString(math, { + displayMode: mathElement.tagName === "DIV", + output: "html", + macros: options.math.macros, + }); + } catch (e) { + mathElement.innerHTML = e.message; + mathElement.className = "language-math vditor-reset--error"; + } + + mathElement.addEventListener("copy", (event: ClipboardEvent) => { + event.stopPropagation(); + event.preventDefault(); + const vditorMathElement = (event.currentTarget as HTMLElement).closest(".language-math"); + event.clipboardData.setData("text/html", vditorMathElement.innerHTML); + event.clipboardData.setData("text/plain", + vditorMathElement.getAttribute("data-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: { + paths: {mathjax: `${options.cdn}/dist/js/mathjax`}, + }, + startup: { + typeset: false, + }, + tex: { + macros: options.math.macros, + }, + }; + // https://github.com/Vanessa219/vditor/issues/1453 + Object.assign(window.MathJax, options.math.mathJaxOptions); + } + // 循环加载会抛异常 + addScriptSync(`${options.cdn}/dist/js/mathjax/tex-svg-full.js`, "protyleMathJaxScript"); + 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); + 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.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); + }); + } +}; diff --git a/web/include/vditor/src/ts/markdown/mediaRender.ts b/web/include/vditor/src/ts/markdown/mediaRender.ts new file mode 100644 index 0000000..20695df --- /dev/null +++ b/web/include/vditor/src/ts/markdown/mediaRender.ts @@ -0,0 +1,105 @@ +import {getSearch} from "../util/function"; + +const videoRender = (element: HTMLElement, url: string) => { + element.insertAdjacentHTML("afterend", ``); + element.remove(); +}; + +const audioRender = (element: HTMLElement, url: string) => { + element.insertAdjacentHTML("afterend", ``); + element.remove(); +}; + +const iframeRender = (element: HTMLElement, url: string) => { + const youtubeMatch = url.match(/\/\/(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))([\w|-]{11})(?:(?:[\?&]t=)(\S+))?/); + const youkuMatch = url.match(/\/\/v\.youku\.com\/v_show\/id_(\w+)=*\.html/); + const qqMatch = url.match(/\/\/v\.qq\.com\/x\/cover\/.*\/([^\/]+)\.html\??.*/); + const coubMatch = url.match(/(?:www\.|\/\/)coub\.com\/view\/(\w+)/); + const facebookMatch = url.match(/(?:www\.|\/\/)facebook\.com\/([^\/]+)\/videos\/([0-9]+)/); + const dailymotionMatch = url.match(/.+dailymotion.com\/(video|hub)\/(\w+)\?/); + const bilibiliMatch = url.match(/(?:www\.|\/\/)bilibili\.com\/video\/(\w+)/); + const tedMatch = url.match(/(?:www\.|\/\/)ted\.com\/talks\/(\w+)/); + + if (youtubeMatch && youtubeMatch[1].length === 11) { + element.insertAdjacentHTML("afterend", + ``); + element.remove(); + } else if (youkuMatch && youkuMatch[1]) { + element.insertAdjacentHTML("afterend", + ``); + element.remove(); + } else if (qqMatch && qqMatch[1]) { + element.insertAdjacentHTML("afterend", + ``); + element.remove(); + } else if (coubMatch && coubMatch[1]) { + element.insertAdjacentHTML("afterend", + ``); + element.remove(); + } else if (facebookMatch && facebookMatch[0]) { + element.insertAdjacentHTML("afterend", + ``); + element.remove(); + } else if (dailymotionMatch && dailymotionMatch[2]) { + element.insertAdjacentHTML("afterend", + ``); + element.remove(); + } else if (url.indexOf("bilibili.com") > -1 && (url.indexOf("bvid=") > -1 || (bilibiliMatch && bilibiliMatch[1]))) { + const params: IObject = { + bvid: getSearch("bvid", url) || (bilibiliMatch && bilibiliMatch[1]), + page: "1", + high_quality: "1", + as_wide: "1", + allowfullscreen: "true", + autoplay: "0" + }; + new URL(url.startsWith("http") ? url : "https:" + url).search.split("&").forEach((item, index) => { + if (!item) { + return; + } + if (index === 0) { + item = item.substr(1); + } + const keyValue = item.split("="); + params[keyValue[0]] = keyValue[1]; + }); + let src = "https://player.bilibili.com/player.html?"; + const keys = Object.keys(params); + keys.forEach((key, index) => { + src += `${key}=${params[key]}`; + if (index < keys.length - 1) { + src += "&"; + } + }); + element.insertAdjacentHTML("afterend", + ``); + element.remove(); + } else if (tedMatch && tedMatch[1]) { + element.insertAdjacentHTML("afterend", + ``); + element.remove(); + } +}; + +export const mediaRender = (element: HTMLElement) => { + if (!element) { + return; + } + element.querySelectorAll("a").forEach((aElement) => { + const url = aElement.getAttribute("href"); + if (!url) { + return; + } + if (url.match(/^.+.(mp4|m4v|ogg|ogv|webm)$/)) { + videoRender(aElement, url); + } else if (url.match(/^.+.(mp3|wav|flac)$/)) { + audioRender(aElement, url); + } else { + iframeRender(aElement, url); + } + }); +}; diff --git a/web/include/vditor/src/ts/markdown/mermaidRender.ts b/web/include/vditor/src/ts/markdown/mermaidRender.ts new file mode 100644 index 0000000..f3ce151 --- /dev/null +++ b/web/include/vditor/src/ts/markdown/mermaidRender.ts @@ -0,0 +1,60 @@ +import {Constants} from "../constants"; +import {addScript} from "../util/addScript"; +import {mermaidRenderAdapter} from "./adapterRender"; +import {genUUID} from "../util/function"; + +declare const mermaid: { + initialize(options: any): void, + render(id: string, text: string): { svg: string } +}; + +export const mermaidRender = (element: HTMLElement, cdn = Constants.CDN, theme: string) => { + const mermaidElements = mermaidRenderAdapter.getElements(element); + if (mermaidElements.length === 0) { + return; + } + addScript(`${cdn}/dist/js/mermaid/mermaid.min.js`, "vditorMermaidScript").then(() => { + const config: any = { + securityLevel: "loose", // 升级后无 https://github.com/siyuan-note/siyuan/issues/3587,可使用该选项 + altFontFamily: "sans-serif", + fontFamily: "sans-serif", + startOnLoad: false, + flowchart: { + htmlLabels: true, + useMaxWidth: !0 + }, + sequence: { + useMaxWidth: true, + diagramMarginX: 8, + diagramMarginY: 8, + boxMargin: 8, + showSequenceNumbers: true // Mermaid 时序图增加序号 https://github.com/siyuan-note/siyuan/pull/6992 https://mermaid.js.org/syntax/sequenceDiagram.html#sequencenumbers + }, + gantt: { + leftPadding: 75, + rightPadding: 20 + } + }; + if (theme === "dark") { + config.theme = "dark"; + } + mermaid.initialize(config); + mermaidElements.forEach(async (item) => { + const code = mermaidRenderAdapter.getCode(item); + if (item.getAttribute("data-processed") === "true" || code.trim() === "") { + return; + } + const id = "mermaid"+genUUID() + try { + const mermaidData = await mermaid.render(id, item.textContent); + item.innerHTML = mermaidData.svg; + } catch (e) { + const errorElement = document.querySelector("#" + id); + item.innerHTML = `${errorElement.outerHTML}
    +
    ${e.message.replace(/\n/, "
    ")}
    `; + errorElement.parentElement.remove(); + } + item.setAttribute("data-processed", "true"); + }); + }); +}; diff --git a/web/include/vditor/src/ts/markdown/mindmapRender.ts b/web/include/vditor/src/ts/markdown/mindmapRender.ts new file mode 100644 index 0000000..53983c2 --- /dev/null +++ b/web/include/vditor/src/ts/markdown/mindmapRender.ts @@ -0,0 +1,74 @@ +import {Constants} from "../constants"; +import {addScript} from "../util/addScript"; +import {mindmapRenderAdapter} from "./adapterRender"; + +declare const echarts: { + init(element: HTMLElement, theme?: string): IEChart; +}; + +export const mindmapRender = (element: (HTMLElement | Document) = document, cdn = Constants.CDN, theme: string) => { + const mindmapElements = mindmapRenderAdapter.getElements(element); + if (mindmapElements.length > 0) { + addScript(`${cdn}/dist/js/echarts/echarts.min.js?v=5.5.1`, "vditorEchartsScript").then(() => { + mindmapElements.forEach((e: HTMLDivElement) => { + if (e.parentElement.classList.contains("vditor-wysiwyg__pre") || + e.parentElement.classList.contains("vditor-ir__marker--pre")) { + return; + } + const text = mindmapRenderAdapter.getCode(e); + if (!text) { + return; + } + try { + if (e.getAttribute("data-processed") === "true") { + return; + } + echarts.init(e, theme === "dark" ? "dark" : undefined).setOption({ + series: [ + { + data: [JSON.parse(decodeURIComponent(text))], + initialTreeDepth: -1, + itemStyle: { + borderWidth: 0, + color: "#4285f4", + }, + label: { + backgroundColor: "#f6f8fa", + borderColor: "#d1d5da", + borderRadius: 5, + borderWidth: 0.5, + color: "#586069", + lineHeight: 20, + offset: [-5, 0], + padding: [0, 5], + position: "insideRight", + }, + lineStyle: { + color: "#d1d5da", + width: 1, + }, + roam: true, + symbol: (value: number, params: { data?: { children?: object } }) => { + if (params?.data?.children) { + return "circle"; + } else { + return "path://"; + } + }, + type: "tree", + }, + ], + tooltip: { + trigger: "item", + triggerOn: "mousemove", + }, + }); + e.setAttribute("data-processed", "true"); + } catch (error) { + e.className = "vditor-reset--error"; + e.innerHTML = `mindmap render error:
    ${error}`; + } + }); + }); + } +}; diff --git a/web/include/vditor/src/ts/markdown/outlineRender.ts b/web/include/vditor/src/ts/markdown/outlineRender.ts new file mode 100644 index 0000000..cadb84b --- /dev/null +++ b/web/include/vditor/src/ts/markdown/outlineRender.ts @@ -0,0 +1,110 @@ +import {hasClosestByHeadings} from "../util/hasClosestByHeadings"; +import {mathRender} from "./mathRender"; + +export const outlineRender = (contentElement: HTMLElement, targetElement: Element, vditor?: IVditor) => { + let tocHTML = ""; + const ids: string[] = []; + Array.from(contentElement.children).forEach((item: HTMLElement, index: number) => { + if (hasClosestByHeadings(item)) { + if (vditor) { + const lastIndex = item.id.lastIndexOf("_"); + item.id = item.id.substring(0, lastIndex === -1 ? undefined : lastIndex) + "_" + index; + } + ids.push(item.id); + tocHTML += item.outerHTML.replace("", ""); + } + }); + if (tocHTML === "") { + targetElement.innerHTML = ""; + return ""; + } + const tempElement = document.createElement("div"); + if (vditor) { + vditor.lute.SetToC(true); + if (vditor.currentMode === "wysiwyg" && !vditor.preview.element.contains(contentElement)) { + tempElement.innerHTML = vditor.lute.SpinVditorDOM("

    [ToC]

    " + tocHTML); + } else if (vditor.currentMode === "ir" && !vditor.preview.element.contains(contentElement)) { + tempElement.innerHTML = vditor.lute.SpinVditorIRDOM("

    [ToC]

    " + tocHTML); + } else { + tempElement.innerHTML = vditor.lute.HTML2VditorDOM("

    [ToC]

    " + tocHTML); + } + vditor.lute.SetToC(vditor.options.preview.markdown.toc); + } else { + targetElement.classList.add("vditor-outline"); + const lute = Lute.New(); + lute.SetToC(true); + tempElement.innerHTML = lute.HTML2VditorDOM("

    [ToC]

    " + tocHTML); + } + const headingsElement = tempElement.firstElementChild.querySelectorAll("li > span[data-target-id]"); + headingsElement.forEach((item, index) => { + if (item.nextElementSibling && item.nextElementSibling.tagName === "UL") { + let iconHTML = ""; + if (!document.getElementById("vditorIconScript")) { + iconHTML = ''; + } + item.innerHTML = `${iconHTML}${item.innerHTML}`; + } else { + item.innerHTML = `${item.innerHTML}`; + } + item.setAttribute("data-target-id", ids[index]); + }); + tocHTML = tempElement.firstElementChild.innerHTML; + if (headingsElement.length === 0) { + targetElement.innerHTML = ""; + return tocHTML; + } + targetElement.innerHTML = tocHTML; + if (vditor) { + mathRender(targetElement as HTMLElement, { + cdn: vditor.options.cdn, + math: vditor.options.preview.math, + }); + } + targetElement.firstElementChild.addEventListener("click", (event: Event) => { + let target = event.target as HTMLElement; + while (target && !target.isEqualNode(targetElement)) { + if (target.classList.contains("vditor-outline__action")) { + if (target.classList.contains("vditor-outline__action--close")) { + target.classList.remove("vditor-outline__action--close"); + target.parentElement.nextElementSibling.setAttribute("style", "display:block"); + } else { + target.classList.add("vditor-outline__action--close"); + target.parentElement.nextElementSibling.setAttribute("style", "display:none"); + } + event.preventDefault(); + event.stopPropagation(); + break; + } else if (target.getAttribute("data-target-id")) { + event.preventDefault(); + event.stopPropagation(); + const idElement = document.getElementById(target.getAttribute("data-target-id")); + if (!idElement) { + return; + } + if (vditor) { + if (vditor.options.height === "auto") { + let windowScrollY = idElement.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); + } + if (vditor.preview.element.contains(contentElement)) { + contentElement.parentElement.scrollTop = idElement.offsetTop; + } else { + contentElement.scrollTop = idElement.offsetTop; + } + } + } else { + window.scrollTo(window.scrollX, idElement.offsetTop); + } + break; + } + target = target.parentElement; + } + }); + return tocHTML; +}; diff --git a/web/include/vditor/src/ts/markdown/plantumlRender.ts b/web/include/vditor/src/ts/markdown/plantumlRender.ts new file mode 100644 index 0000000..550f276 --- /dev/null +++ b/web/include/vditor/src/ts/markdown/plantumlRender.ts @@ -0,0 +1,32 @@ +import {Constants} from "../constants"; +import {addScript} from "../util/addScript"; +import {plantumlRenderAdapter} from "./adapterRender"; + +declare const plantumlEncoder: { + encode(options: string): string, +}; + +export const plantumlRender = (element: (HTMLElement | Document) = document, cdn = Constants.CDN) => { + const plantumlElements = plantumlRenderAdapter.getElements(element); + if (plantumlElements.length === 0) { + return; + } + addScript(`${cdn}/dist/js/plantuml/plantuml-encoder.min.js`, "vditorPlantumlScript").then(() => { + plantumlElements.forEach((e: HTMLDivElement) => { + if (e.parentElement.classList.contains("vditor-wysiwyg__pre") || + e.parentElement.classList.contains("vditor-ir__marker--pre")) { + return; + } + const text = plantumlRenderAdapter.getCode(e).trim(); + if (!text) { + return; + } + try { + e.innerHTML = ``; + } catch (error) { + e.className = "vditor-reset--error"; + e.innerHTML = `plantuml render error:
    ${error}`; + } + }); + }); +}; diff --git a/web/include/vditor/src/ts/markdown/previewRender.ts b/web/include/vditor/src/ts/markdown/previewRender.ts new file mode 100644 index 0000000..bdda220 --- /dev/null +++ b/web/include/vditor/src/ts/markdown/previewRender.ts @@ -0,0 +1,167 @@ +import {Constants} from "../constants"; +import {setContentTheme} from "../ui/setContentTheme"; +import {addScript, addScriptSync} from "../util/addScript"; +import {hasClosestByClassName, hasClosestByMatchTag} from "../util/hasClosest"; +import {merge} from "../util/merge"; +import {abcRender} from "./abcRender"; +import {anchorRender} from "./anchorRender"; +import {chartRender} from "./chartRender"; +import {codeRender} from "./codeRender"; +import {flowchartRender} from "./flowchartRender"; +import {graphvizRender} from "./graphvizRender"; +import {highlightRender} from "./highlightRender"; +import {lazyLoadImageRender} from "./lazyLoadImageRender"; +import {mathRender} from "./mathRender"; +import {mediaRender} from "./mediaRender"; +import {mermaidRender} from "./mermaidRender"; +import {markmapRender} from "../markdown/markmapRender"; +import {mindmapRender} from "./mindmapRender"; +import {plantumlRender} from "./plantumlRender"; +import {setLute} from "./setLute"; +import {speechRender} from "./speechRender"; + +const mergeOptions = (options?: IPreviewOptions) => { + const defaultOption: IPreviewOptions = { + anchor: 0, + cdn: Constants.CDN, + customEmoji: {}, + emojiPath: `${Constants.CDN}/dist/images/emoji`, + hljs: Constants.HLJS_OPTIONS, + icon: "ant", + lang: "zh_CN", + markdown: Constants.MARKDOWN_OPTIONS, + math: Constants.MATH_OPTIONS, + mode: "light", + speech: { + enable: false, + }, + render: { + media: { + enable: true, + } + }, + theme: Constants.THEME_OPTIONS, + }; + if (options.cdn) { + if (!options.theme?.path) { + defaultOption.theme.path = `${options.cdn}/dist/css/content-theme` + } + if (!options.emojiPath) { + defaultOption.emojiPath = `${options.cdn}/dist/images/emoji`; + } + } + return merge(defaultOption, options); +}; + +export const md2html = (mdText: string, options?: IPreviewOptions) => { + const mergedOptions = mergeOptions(options); + return addScript(`${mergedOptions.cdn}/dist/js/lute/lute.min.js`, "vditorLuteScript").then(() => { + const lute = setLute({ + autoSpace: mergedOptions.markdown.autoSpace, + gfmAutoLink: mergedOptions.markdown.gfmAutoLink, + codeBlockPreview: mergedOptions.markdown.codeBlockPreview, + emojiSite: mergedOptions.emojiPath, + emojis: mergedOptions.customEmoji, + fixTermTypo: mergedOptions.markdown.fixTermTypo, + footnotes: mergedOptions.markdown.footnotes, + headingAnchor: mergedOptions.anchor !== 0, + inlineMathDigit: mergedOptions.math.inlineDigit, + lazyLoadImage: mergedOptions.lazyLoadImage, + linkBase: mergedOptions.markdown.linkBase, + linkPrefix: mergedOptions.markdown.linkPrefix, + listStyle: mergedOptions.markdown.listStyle, + mark: mergedOptions.markdown.mark, + mathBlockPreview: mergedOptions.markdown.mathBlockPreview, + paragraphBeginningSpace: mergedOptions.markdown.paragraphBeginningSpace, + sanitize: mergedOptions.markdown.sanitize, + toc: mergedOptions.markdown.toc, + }); + if (options?.renderers) { + lute.SetJSRenderers({ + renderers: { + Md2HTML: options.renderers, + }, + }); + } + lute.SetHeadingID(true); + return lute.Md2HTML(mdText); + }); +}; + +export const previewRender = async (previewElement: HTMLDivElement, markdown: string, options?: IPreviewOptions) => { + const mergedOptions: IPreviewOptions = mergeOptions(options); + let html = await md2html(markdown, mergedOptions); + if (mergedOptions.transform) { + html = mergedOptions.transform(html); + } + previewElement.innerHTML = html; + previewElement.classList.add("vditor-reset"); + + if (!mergedOptions.i18n) { + if (!["en_US", "fr_FR", "pt_BR", "ja_JP", "ko_KR", "ru_RU", "sv_SE", "zh_CN", "zh_TW"].includes(mergedOptions.lang)) { + throw new Error( + "options.lang error, see https://ld246.com/article/1549638745630#options", + ); + } else { + const i18nScriptPrefix = "vditorI18nScript"; + const i18nScriptID = i18nScriptPrefix + mergedOptions.lang; + document.querySelectorAll(`head script[id^="${i18nScriptPrefix}"]`).forEach((el) => { + if (el.id !== i18nScriptID) { + document.head.removeChild(el); + } + }); + await addScript(`${mergedOptions.cdn}/dist/js/i18n/${mergedOptions.lang}.js`, i18nScriptID); + } + } else { + window.VditorI18n = mergedOptions.i18n; + } + + if (mergedOptions.icon) { + await addScript(`${mergedOptions.cdn}/dist/js/icons/${mergedOptions.icon}.js`, "vditorIconScript"); + } + + setContentTheme(mergedOptions.theme.current, mergedOptions.theme.path); + if (mergedOptions.anchor === 1) { + previewElement.classList.add("vditor-reset--anchor"); + } + codeRender(previewElement, mergedOptions.hljs); + highlightRender(mergedOptions.hljs, previewElement, mergedOptions.cdn); + mathRender(previewElement, { + cdn: mergedOptions.cdn, + math: mergedOptions.math, + }); + mermaidRender(previewElement, mergedOptions.cdn, mergedOptions.mode); + markmapRender(previewElement, mergedOptions.cdn, mergedOptions.mode); + flowchartRender(previewElement, mergedOptions.cdn); + graphvizRender(previewElement, mergedOptions.cdn); + chartRender(previewElement, mergedOptions.cdn, mergedOptions.mode); + mindmapRender(previewElement, mergedOptions.cdn, mergedOptions.mode); + plantumlRender(previewElement, mergedOptions.cdn); + abcRender(previewElement, mergedOptions.cdn); + if (mergedOptions.render.media.enable) { + mediaRender(previewElement); + } + if (mergedOptions.speech.enable) { + speechRender(previewElement); + } + if (mergedOptions.anchor !== 0) { + anchorRender(mergedOptions.anchor); + } + if (mergedOptions.after) { + mergedOptions.after(); + } + if (mergedOptions.lazyLoadImage) { + lazyLoadImageRender(previewElement); + } + previewElement.addEventListener("click", (event: MouseEvent & { target: HTMLElement }) => { + const spanElement = hasClosestByMatchTag(event.target, "SPAN"); + if (spanElement && hasClosestByClassName(spanElement, "vditor-toc")) { + const headingElement = + previewElement.querySelector("#" + spanElement.getAttribute("data-target-id")) as HTMLElement; + if (headingElement) { + window.scrollTo(window.scrollX, headingElement.offsetTop); + } + return; + } + }); +}; diff --git a/web/include/vditor/src/ts/markdown/setLute.ts b/web/include/vditor/src/ts/markdown/setLute.ts new file mode 100644 index 0000000..59bdd75 --- /dev/null +++ b/web/include/vditor/src/ts/markdown/setLute.ts @@ -0,0 +1,24 @@ +export const setLute = (options: ILuteOptions) => { + const lute: Lute = Lute.New(); + lute.PutEmojis(options.emojis); + lute.SetEmojiSite(options.emojiSite); + lute.SetHeadingAnchor(options.headingAnchor); + lute.SetInlineMathAllowDigitAfterOpenMarker(options.inlineMathDigit); + lute.SetAutoSpace(options.autoSpace); + lute.SetToC(options.toc); + lute.SetFootnotes(options.footnotes); + lute.SetFixTermTypo(options.fixTermTypo); + lute.SetVditorCodeBlockPreview(options.codeBlockPreview); + lute.SetVditorMathBlockPreview(options.mathBlockPreview); + lute.SetSanitize(options.sanitize); + lute.SetChineseParagraphBeginningSpace(options.paragraphBeginningSpace); + lute.SetRenderListStyle(options.listStyle); + lute.SetLinkBase(options.linkBase); + lute.SetLinkPrefix(options.linkPrefix); + lute.SetMark(options.mark); + lute.SetGFMAutoLink(options.gfmAutoLink); + if (options.lazyLoadImage) { + lute.SetImageLazyLoading(options.lazyLoadImage); + } + return lute; +}; diff --git a/web/include/vditor/src/ts/markdown/speechRender.ts b/web/include/vditor/src/ts/markdown/speechRender.ts new file mode 100644 index 0000000..4eefb6f --- /dev/null +++ b/web/include/vditor/src/ts/markdown/speechRender.ts @@ -0,0 +1,107 @@ +import {setSelectionFocus} from "../util/selection"; + +declare global { + interface Window { + vditorSpeechRange: Range; + } +} + +export const speechRender = (element: HTMLElement, lang: keyof II18n = "zh_CN") => { + if (typeof speechSynthesis === "undefined" || typeof SpeechSynthesisUtterance === "undefined") { + return; + } + + const getVoice = () => { + const voices = speechSynthesis.getVoices(); + let currentVoice: SpeechSynthesisVoice; + let defaultVoice; + voices.forEach((item) => { + if (item.lang === lang.replace("_", "-")) { + currentVoice = item; + } + if (item.default) { + defaultVoice = item; + } + }); + if (!currentVoice) { + currentVoice = defaultVoice; + } + return currentVoice; + }; + + let playSVG = ''; + let pauseSVG = ''; + if (!document.getElementById("vditorIconScript")) { + playSVG = ''; + pauseSVG = ''; + } + + let speechDom: HTMLButtonElement = document.querySelector(".vditor-speech"); + if (!speechDom) { + speechDom = document.createElement("button"); + speechDom.className = "vditor-speech"; + element.insertAdjacentElement("beforeend", speechDom); + if (speechSynthesis.onvoiceschanged !== undefined) { + speechSynthesis.onvoiceschanged = getVoice; + } + } + const voice = getVoice(); + const utterThis = new SpeechSynthesisUtterance(); + utterThis.voice = voice; + utterThis.onend = utterThis.onerror = () => { + speechDom.style.display = "none"; + speechSynthesis.cancel(); + speechDom.classList.remove("vditor-speech--current"); + speechDom.innerHTML = playSVG; + }; + + element.addEventListener(window.ontouchstart !== undefined ? "touchend" : "click", (event) => { + const target = event.target as HTMLElement + if (target.classList.contains("vditor-speech") || target.parentElement.classList.contains("vditor-speech")) { + if (!speechDom.classList.contains("vditor-speech--current")) { + utterThis.text = speechDom.getAttribute("data-text"); + speechSynthesis.speak(utterThis); + speechDom.classList.add("vditor-speech--current"); + speechDom.innerHTML = pauseSVG; + } else { + if (speechSynthesis.speaking) { + if (speechSynthesis.paused) { + speechSynthesis.resume(); + speechDom.innerHTML = pauseSVG; + } else { + speechSynthesis.pause(); + speechDom.innerHTML = playSVG; + } + } + } + setSelectionFocus(window.vditorSpeechRange); + element.focus(); + return; + } + + speechDom.style.display = "none"; + speechSynthesis.cancel(); + speechDom.classList.remove("vditor-speech--current"); + speechDom.innerHTML = playSVG; + + if (getSelection().rangeCount === 0) { + return; + } + const range = getSelection().getRangeAt(0) + const text = range.toString().trim(); + if (!text) { + return; + } + window.vditorSpeechRange = range.cloneRange(); + const rect = range.getBoundingClientRect(); + speechDom.innerHTML = playSVG; + speechDom.style.display = "block"; + speechDom.style.top = (rect.top + rect.height + document.querySelector("html").scrollTop - 20) + "px"; + if (window.ontouchstart !== undefined) { + speechDom.style.left = ((event as TouchEvent).changedTouches[(event as TouchEvent).changedTouches.length - 1].pageX + 2) + "px"; + } else { + speechDom.style.left = ((event as MouseEvent).clientX + 2) + "px"; + } + speechDom.setAttribute("data-text", text); + }); +}; diff --git a/web/include/vditor/src/ts/outline/index.ts b/web/include/vditor/src/ts/outline/index.ts new file mode 100644 index 0000000..05b898b --- /dev/null +++ b/web/include/vditor/src/ts/outline/index.ts @@ -0,0 +1,45 @@ +import {Constants} from "../constants"; +import {outlineRender} from "../markdown/outlineRender"; +import {setPadding} from "../ui/initUI"; +import {setSelectionFocus} from "../util/selection"; + +export class Outline { + public element: HTMLElement; + + constructor(outlineLabel: string) { + this.element = document.createElement("div"); + this.element.className = "vditor-outline"; + this.element.innerHTML = `
    ${outlineLabel}
    +
    `; + } + + public render(vditor: IVditor) { + let html = ""; + if (vditor.preview.element.style.display === "block") { + html = outlineRender(vditor.preview.previewElement, + this.element.lastElementChild, vditor); + } else { + html = outlineRender(vditor[vditor.currentMode].element, this.element.lastElementChild, vditor); + } + return html; + } + + public toggle(vditor: IVditor, show = true, focus = true) { + const btnElement = vditor.toolbar.elements.outline?.firstElementChild; + if (show && window.innerWidth >= Constants.MOBILE_WIDTH) { + this.element.style.display = "block"; + this.render(vditor); + btnElement?.classList.add("vditor-menu--current"); + } else { + this.element.style.display = "none"; + btnElement?.classList.remove("vditor-menu--current"); + } + if (focus && getSelection().rangeCount > 0) { + const range = getSelection().getRangeAt(0); + if (vditor[vditor.currentMode].element.contains(range.startContainer)) { + setSelectionFocus(range); + } + } + setPadding(vditor); + } +} diff --git a/web/include/vditor/src/ts/preview/image.ts b/web/include/vditor/src/ts/preview/image.ts new file mode 100644 index 0000000..e19644c --- /dev/null +++ b/web/include/vditor/src/ts/preview/image.ts @@ -0,0 +1,50 @@ +export const previewImage = (oldImgElement: HTMLImageElement, lang: keyof II18n = "zh_CN", theme = "classic") => { + const oldImgRect = oldImgElement.getBoundingClientRect(); + const height = 36; + document.body.insertAdjacentHTML("beforeend", `
    +
    + + + ${window.VditorI18n.spin} + + + X  ${window.VditorI18n.close} + +
    +
    + +
    +
    `); + document.body.style.overflow = "hidden"; + + // 图片从原始位置移动到预览正中间的动画效果 + const imgElement = document.querySelector(".vditor-img img") as HTMLImageElement; + const translate3d = `translate3d(${Math.max(0, window.innerWidth - oldImgElement.naturalWidth) / 2}px, ${Math.max(0, window.innerHeight - height - oldImgElement.naturalHeight) / 2}px, 0)`; + setTimeout(() => { + imgElement.setAttribute("style", `transition: transform .3s ease-in-out;transform: ${translate3d}`); + setTimeout(() => { + imgElement.parentElement.scrollTo( + (imgElement.parentElement.scrollWidth - imgElement.parentElement.clientWidth) / 2, + (imgElement.parentElement.scrollHeight - imgElement.parentElement.clientHeight) / 2); + }, 400); + }); + + // 旋转 + const btnElement = document.querySelector(".vditor-img__btn"); + btnElement.addEventListener("click", () => { + const deg = parseInt(btnElement.getAttribute("data-deg"), 10) + 90; + if ((deg / 90) % 2 === 1 && oldImgElement.naturalWidth > imgElement.parentElement.clientHeight) { + imgElement.style.transform = `translate3d(${ + Math.max(0, window.innerWidth - oldImgElement.naturalWidth) / 2}px, ${ + oldImgElement.naturalWidth / 2 - oldImgElement.naturalHeight / 2}px, 0) rotateZ(${deg}deg)`; + } else { + imgElement.style.transform = `${translate3d} rotateZ(${deg}deg)`; + } + btnElement.setAttribute("data-deg", deg.toString()); + setTimeout(() => { + imgElement.parentElement.scrollTo( + (imgElement.parentElement.scrollWidth - imgElement.parentElement.clientWidth) / 2, + (imgElement.parentElement.scrollHeight - imgElement.parentElement.clientHeight) / 2); + }, 400); + }); +}; diff --git a/web/include/vditor/src/ts/preview/index.ts b/web/include/vditor/src/ts/preview/index.ts new file mode 100644 index 0000000..a2537b2 --- /dev/null +++ b/web/include/vditor/src/ts/preview/index.ts @@ -0,0 +1,285 @@ +import {abcRender} from "../markdown/abcRender"; +import {chartRender} from "../markdown/chartRender"; +import {codeRender} from "../markdown/codeRender"; +import {flowchartRender} from "../markdown/flowchartRender"; +import {getMarkdown} from "../markdown/getMarkdown"; +import {graphvizRender} from "../markdown/graphvizRender"; +import {highlightRender} from "../markdown/highlightRender"; +import {mathRender} from "../markdown/mathRender"; +import {mediaRender} from "../markdown/mediaRender"; +import {mermaidRender} from "../markdown/mermaidRender"; +import {markmapRender} from "../markdown/markmapRender"; +import {mindmapRender} from "../markdown/mindmapRender"; +import {plantumlRender} from "../markdown/plantumlRender"; +import {getEventName} from "../util/compatibility"; +import {hasClosestByClassName, hasClosestByMatchTag} from "../util/hasClosest"; +import {hasClosestByTag} from "../util/hasClosestByHeadings"; +import {setSelectionFocus} from "../util/selection"; +import {previewImage} from "./image"; + +export class Preview { + public element: HTMLElement; + public previewElement: HTMLElement; + private mdTimeoutId: number; + + constructor(vditor: IVditor) { + this.element = document.createElement("div"); + this.element.className = `vditor-preview`; + this.previewElement = document.createElement("div"); + this.previewElement.className = "vditor-reset"; + if (vditor.options.classes.preview) { + this.previewElement.classList.add(vditor.options.classes.preview); + } + this.previewElement.style.maxWidth = vditor.options.preview.maxWidth + "px"; + this.previewElement.addEventListener("copy", (event: ClipboardEvent & { target: HTMLElement }) => { + if (event.target.tagName === "TEXTAREA") { + // https://github.com/Vanessa219/vditor/issues/901 + return; + } + const tempElement = document.createElement("div"); + tempElement.className = "vditor-reset"; + tempElement.appendChild(getSelection().getRangeAt(0).cloneContents()); + + this.copyToX(vditor, tempElement, "default"); + event.preventDefault(); + }); + this.previewElement.addEventListener("click", (event: MouseEvent & { target: HTMLElement }) => { + const spanElement = hasClosestByMatchTag(event.target, "SPAN"); + if (spanElement && hasClosestByClassName(spanElement, "vditor-toc")) { + const headingElement = + this.previewElement.querySelector("#" + spanElement.getAttribute("data-target-id")) as HTMLElement; + if (headingElement) { + this.element.scrollTop = headingElement.offsetTop; + } + return; + } + if (event.target.tagName === "A") { + if (vditor.options.link.click) { + vditor.options.link.click(event.target); + } else if (vditor.options.link.isOpen) { + window.open(event.target.getAttribute("href")); + } + event.preventDefault(); + return; + } + if (event.target.tagName === "IMG") { + if (vditor.options.image.preview) { + vditor.options.image.preview(event.target) + } else if (vditor.options.image.isPreview) { + previewImage(event.target as HTMLImageElement, vditor.options.lang, vditor.options.theme); + } + } + }); + this.element.appendChild(this.previewElement); + const actions = vditor.options.preview.actions; + if (actions.length === 0) { + return; + } + const actionElement = document.createElement("div"); + actionElement.className = "vditor-preview__action"; + const actionHtml: string[] = []; + for (let i = 0; i < actions.length; i++) { + const action = actions[i]; + if (typeof action === "object") { + actionHtml.push(``); + continue; + } + switch (action) { + case "desktop": + actionHtml.push(``); + break; + case "tablet": + actionHtml.push(``); + break; + case "mobile": + actionHtml.push(``); + break; + case "mp-wechat": + actionHtml.push(``); + break; + case "zhihu": + actionHtml.push(``); + break; + } + } + actionElement.innerHTML = actionHtml.join(""); + actionElement.addEventListener(getEventName(), (event) => { + const btn = hasClosestByTag(event.target as HTMLElement, "BUTTON"); + if (!btn) { + return; + } + const type = btn.getAttribute("data-type"); + const actionCustom = actions.find((w: IPreviewActionCustom) => w?.key === type) as IPreviewActionCustom; + if (actionCustom) { + actionCustom.click(type); + return; + } + + if (type === "mp-wechat" || type === "zhihu") { + this.copyToX(vditor, this.previewElement.cloneNode(true) as HTMLElement, type); + return; + } + + if (type === "desktop") { + this.previewElement.style.width = "auto"; + } else if (type === "tablet") { + this.previewElement.style.width = "780px"; + } else { + this.previewElement.style.width = "360px"; + } + if (this.previewElement.scrollWidth > this.previewElement.parentElement.clientWidth) { + this.previewElement.style.width = "auto"; + } + this.render(vditor); + actionElement.querySelectorAll("button").forEach((item) => { + item.classList.remove("vditor-preview__action--current"); + }); + btn.classList.add("vditor-preview__action--current"); + }); + this.element.insertBefore(actionElement, this.previewElement); + } + + public render(vditor: IVditor, value?: string) { + clearTimeout(this.mdTimeoutId); + + if (this.element.style.display === "none") { + if (this.element.getAttribute("data-type") === "renderPerformance") { + vditor.tip.hide(); + } + return; + } + + if (value) { + this.previewElement.innerHTML = value; + return; + } + + if (getMarkdown(vditor) + .replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, "") === "") { + this.previewElement.innerHTML = ""; + return; + } + + const renderStartTime = new Date().getTime(); + const markdownText = getMarkdown(vditor); + this.mdTimeoutId = window.setTimeout(() => { + if (vditor.options.preview.url) { + const xhr = new XMLHttpRequest(); + xhr.open("POST", vditor.options.preview.url); + xhr.setRequestHeader("Content-Type", "application/json;charset=UTF-8"); + xhr.onreadystatechange = () => { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + const responseJSON = JSON.parse(xhr.responseText); + if (responseJSON.code !== 0) { + vditor.tip.show(responseJSON.msg); + return; + } + if (vditor.options.preview.transform) { + responseJSON.data = vditor.options.preview.transform(responseJSON.data); + } + this.previewElement.innerHTML = responseJSON.data; + this.afterRender(vditor, renderStartTime); + } else { + let html = vditor.lute.Md2HTML(markdownText); + if (vditor.options.preview.transform) { + html = vditor.options.preview.transform(html); + } + this.previewElement.innerHTML = html; + this.afterRender(vditor, renderStartTime); + } + } + }; + + xhr.send(JSON.stringify({markdownText})); + } else { + let html = vditor.lute.Md2HTML(markdownText); + if (vditor.options.preview.transform) { + html = vditor.options.preview.transform(html); + } + this.previewElement.innerHTML = html; + this.afterRender(vditor, renderStartTime); + } + }, vditor.options.preview.delay); + } + + private afterRender(vditor: IVditor, startTime: number) { + if (vditor.options.preview.parse) { + vditor.options.preview.parse(this.element); + } + const time = (new Date().getTime() - startTime); + if ((new Date().getTime() - startTime) > 2600) { + // https://github.com/b3log/vditor/issues/67 + vditor.tip.show(window.VditorI18n.performanceTip.replace("${x}", time.toString())); + vditor.preview.element.setAttribute("data-type", "renderPerformance"); + } else if (vditor.preview.element.getAttribute("data-type") === "renderPerformance") { + vditor.tip.hide(); + vditor.preview.element.removeAttribute("data-type"); + } + const cmtFocusElement = vditor.preview.element.querySelector(".vditor-comment--focus"); + if (cmtFocusElement) { + cmtFocusElement.classList.remove("vditor-comment--focus"); + } + codeRender(vditor.preview.previewElement, vditor.options.preview.hljs); + highlightRender(vditor.options.preview.hljs, vditor.preview.previewElement, + vditor.options.cdn); + mermaidRender(vditor.preview.previewElement, vditor.options.cdn, vditor.options.theme); + markmapRender(vditor.preview.previewElement, vditor.options.cdn, vditor.options.theme); + flowchartRender(vditor.preview.previewElement, vditor.options.cdn); + graphvizRender(vditor.preview.previewElement, vditor.options.cdn); + chartRender(vditor.preview.previewElement, vditor.options.cdn, vditor.options.theme); + mindmapRender(vditor.preview.previewElement, vditor.options.cdn, vditor.options.theme); + plantumlRender(vditor.preview.previewElement, vditor.options.cdn); + abcRender(vditor.preview.previewElement, vditor.options.cdn); + if (vditor.options.preview.render.media.enable) { + mediaRender(vditor.preview.previewElement); + } + vditor.options.customRenders.forEach((item) => { + item.render(vditor.preview.previewElement, vditor); + }) + // toc render + const editorElement = vditor.preview.element; + let tocHTML = vditor.outline.render(vditor); + if (tocHTML === "") { + tocHTML = "[ToC]"; + } + editorElement.querySelectorAll('[data-type="toc-block"]').forEach((item: HTMLElement) => { + item.innerHTML = tocHTML; + mathRender(item, { + cdn: vditor.options.cdn, + math: vditor.options.preview.math, + }); + }); + mathRender(vditor.preview.previewElement, { + cdn: vditor.options.cdn, + math: vditor.options.preview.math, + }); + } + + private copyToX(vditor: IVditor, copyElement: HTMLElement, type = "mp-wechat") { + // fix math render + if (type !== "zhihu") { + copyElement.querySelectorAll(".katex-html .base").forEach((item: HTMLElement) => { + item.style.display = "initial"; + }); + } else { + copyElement.querySelectorAll(".language-math").forEach((item: HTMLElement) => { + item.outerHTML = `${item.getAttribute(`; + }); + } + // 防止背景色被粘贴到公众号中 + copyElement.style.backgroundColor = "#fff"; + // 代码背景 + copyElement.querySelectorAll("code").forEach((item) => { + item.style.backgroundImage = "none"; + }); + this.element.append(copyElement); + const range = copyElement.ownerDocument.createRange(); + range.selectNode(copyElement); + setSelectionFocus(range); + document.execCommand("copy"); + copyElement.remove(); + + vditor.tip.show(['zhihu', 'mp-wechat'].includes(type) ? `已复制,可到${type === "zhihu" ? "知乎" : "微信公众号平台"}进行粘贴` : `已复制到剪切板`); + } +} diff --git a/web/include/vditor/src/ts/resize/index.ts b/web/include/vditor/src/ts/resize/index.ts new file mode 100644 index 0000000..79e6b47 --- /dev/null +++ b/web/include/vditor/src/ts/resize/index.ts @@ -0,0 +1,57 @@ + +export class Resize { + public element: HTMLElement; + + constructor(vditor: IVditor) { + this.element = document.createElement("div"); + this.element.className = `vditor-resize vditor-resize--${vditor.options.resize.position}`; + this.element.innerHTML = `
    `; + + this.bindEvent(vditor); + } + + private bindEvent(vditor: IVditor) { + this.element.addEventListener("mousedown", (event: MouseEvent) => { + + const documentSelf = document; + const y = event.clientY; + const height = vditor.element.offsetHeight; + const minHeight = 63 + vditor.element.querySelector(".vditor-toolbar").clientHeight; + documentSelf.ondragstart = () => false; + + if (window.captureEvents) { + window.captureEvents(); + } + + this.element.classList.add("vditor-resize--selected"); + + documentSelf.onmousemove = (moveEvent: MouseEvent) => { + if (vditor.options.resize.position === "top") { + vditor.element.style.height = Math.max(minHeight, height + (y - moveEvent.clientY)) + "px"; + } else { + vditor.element.style.height = Math.max(minHeight, height + (moveEvent.clientY - y)) + "px"; + } + if (vditor.options.typewriterMode) { + vditor.sv.element.style.paddingBottom = + vditor.sv.element.parentElement.offsetHeight / 2 + "px"; + } + }; + + documentSelf.onmouseup = () => { + if (vditor.options.resize.after) { + vditor.options.resize.after(vditor.element.offsetHeight - height); + } + + if (window.captureEvents) { + window.captureEvents(); + } + documentSelf.onmousemove = null; + documentSelf.onmouseup = null; + documentSelf.ondragstart = null; + documentSelf.onselectstart = null; + documentSelf.onselect = null; + this.element.classList.remove("vditor-resize--selected"); + }; + }); + } +} diff --git a/web/include/vditor/src/ts/sv/combineFootnote.ts b/web/include/vditor/src/ts/sv/combineFootnote.ts new file mode 100644 index 0000000..3e56e44 --- /dev/null +++ b/web/include/vditor/src/ts/sv/combineFootnote.ts @@ -0,0 +1,27 @@ +/** + * 合并脚注 + * @param elements vditor.sv.element + * @param afterCombine 每个脚注块合并完成后的回调, param: root为合并后的脚注块 + */ +export const combineFootnote = (elements: HTMLElement, afterCombine?: (root: HTMLElement) => void ) => { + elements.querySelectorAll("[data-type=footnotes-link]").forEach((el: Element) => { + const root = el.parentElement + let footnote = root.nextSibling + // 寻找所有该脚注的块 + while (footnote) { + if (footnote.textContent.startsWith(" ")) { + // 解析到四个空格,加入到root并继续解析 + const thisNode = footnote + thisNode.childNodes.forEach(node => { + root.append(node.cloneNode(true)) + }) + footnote = footnote.nextSibling + thisNode.remove() + } else { + // 非空格停止解析 + break + } + } + afterCombine && afterCombine(root) + }) +} diff --git a/web/include/vditor/src/ts/sv/index.ts b/web/include/vditor/src/ts/sv/index.ts new file mode 100644 index 0000000..a029fbe --- /dev/null +++ b/web/include/vditor/src/ts/sv/index.ts @@ -0,0 +1,125 @@ +import {isCtrl, isFirefox} from "../util/compatibility"; +import { + blurEvent, + copyEvent, cutEvent, + dropEvent, + focusEvent, + hotkeyEvent, + scrollCenter, + selectEvent, +} from "../util/editorCommonEvent"; +import {paste} from "../util/fixBrowserBehavior"; +import {getSelectText} from "../util/getSelectText"; +import {inputEvent} from "./inputEvent"; +import {processAfterRender} from "./process"; + +class Editor { + public range: Range; + public element: HTMLPreElement; + public composingLock: boolean = false; + public processTimeoutId: number; + public hlToolbarTimeoutId: number; + public preventInput: boolean; + + constructor(vditor: IVditor) { + this.element = document.createElement("pre"); + this.element.className = "vditor-sv vditor-reset"; + this.element.setAttribute("placeholder", vditor.options.placeholder); + this.element.setAttribute("contenteditable", "true"); + this.element.setAttribute("spellcheck", "false"); + + this.bindEvent(vditor); + + focusEvent(vditor, this.element); + blurEvent(vditor, this.element); + hotkeyEvent(vditor, this.element); + selectEvent(vditor, this.element); + dropEvent(vditor, this.element); + copyEvent(vditor, this.element, this.copy); + cutEvent(vditor, this.element, this.copy); + } + + private copy(event: ClipboardEvent, vditor: IVditor) { + event.stopPropagation(); + event.preventDefault(); + event.clipboardData.setData("text/plain", getSelectText(vditor[vditor.currentMode].element)); + } + + private bindEvent(vditor: IVditor) { + this.element.addEventListener("paste", (event: ClipboardEvent & { target: HTMLElement }) => { + paste(vditor, event, { + pasteCode: (code: string) => { + document.execCommand("insertHTML", false, code); + }, + }); + }); + + this.element.addEventListener("scroll", () => { + if (vditor.preview.element.style.display !== "block") { + return; + } + const textScrollTop = this.element.scrollTop; + const textHeight = this.element.clientHeight; + const textScrollHeight = this.element.scrollHeight - parseFloat(this.element.style.paddingBottom || "0"); + const preview = vditor.preview.element; + if ((textScrollTop / textHeight > 0.5)) { + preview.scrollTop = (textScrollTop + textHeight) * + preview.scrollHeight / textScrollHeight - textHeight; + } else { + preview.scrollTop = textScrollTop * + preview.scrollHeight / textScrollHeight; + } + }); + + this.element.addEventListener("compositionstart", (event: InputEvent) => { + this.composingLock = true; + }); + + this.element.addEventListener("compositionend", (event: InputEvent) => { + if (!isFirefox()) { + inputEvent(vditor, event); + } + this.composingLock = false; + }); + + this.element.addEventListener("input", (event: InputEvent) => { + if (event.inputType === "deleteByDrag" || event.inputType === "insertFromDrop") { + // https://github.com/Vanessa219/vditor/issues/801 编辑器内容拖拽问题 + return; + } + if (this.composingLock || event.data === "‘" || event.data === "“" || event.data === "《") { + return; + } + if (this.preventInput) { + this.preventInput = false; + processAfterRender(vditor, { + enableAddUndoStack: true, + enableHint: true, + enableInput: true, + }); + return; + } + inputEvent(vditor, event); + }); + + this.element.addEventListener("keyup", (event) => { + if (event.isComposing || isCtrl(event)) { + return; + } + if ((event.key === "Backspace" || event.key === "Delete") && + vditor.sv.element.innerHTML !== "" && vditor.sv.element.childNodes.length === 1 && + vditor.sv.element.firstElementChild && vditor.sv.element.firstElementChild.tagName === "DIV" + && vditor.sv.element.firstElementChild.childElementCount === 2 + && (vditor.sv.element.firstElementChild.textContent === "" || vditor.sv.element.textContent === "\n")) { + // 为空时显示 placeholder + vditor.sv.element.innerHTML = ""; + return; + } + if (event.key === "Enter") { + scrollCenter(vditor); + } + }); + } +} + +export {Editor}; diff --git a/web/include/vditor/src/ts/sv/inputEvent.ts b/web/include/vditor/src/ts/sv/inputEvent.ts new file mode 100644 index 0000000..25fbe8d --- /dev/null +++ b/web/include/vditor/src/ts/sv/inputEvent.ts @@ -0,0 +1,188 @@ +import {scrollCenter} from "../util/editorCommonEvent"; +import {hasClosestByAttribute} from "../util/hasClosest"; +import {getSelectPosition, setRangeByWbr} from "../util/selection"; +import {getSideByType, processAfterRender, processSpinVditorSVDOM} from "./process"; +import {combineFootnote} from "./combineFootnote"; + +export const inputEvent = (vditor: IVditor, event?: InputEvent) => { + const range = getSelection().getRangeAt(0).cloneRange(); + let startContainer = range.startContainer; + if (range.startContainer.nodeType !== 3 && (range.startContainer as HTMLElement).tagName === "DIV") { + startContainer = range.startContainer.childNodes[range.startOffset - 1]; + } + let blockElement = hasClosestByAttribute(startContainer, "data-block", "0"); + // 不调用 lute 解析 + if (blockElement && event && (event.inputType === "deleteContentBackward" || event.data === " ")) { + // 开始可以输入空格 + const startOffset = getSelectPosition(blockElement, vditor.sv.element, range).start; + let startSpace = true; + for (let i = startOffset - 1; + // 软换行后有空格 + i > blockElement.textContent.substr(0, startOffset).lastIndexOf("\n"); i--) { + if (blockElement.textContent.charAt(i) !== " " && + // 多个 tab 前删除不形成代码块 https://github.com/Vanessa219/vditor/issues/162 1 + blockElement.textContent.charAt(i) !== "\t") { + startSpace = false; + break; + } + } + if (startOffset === 0) { + startSpace = false; + } + if (startSpace) { + processAfterRender(vditor); + return; + } + + if (event.inputType === "deleteContentBackward") { + // https://github.com/Vanessa219/vditor/issues/584 代码块 marker 删除 + const codeBlockMarkerElement = + hasClosestByAttribute(startContainer, "data-type", "code-block-open-marker") || + hasClosestByAttribute(startContainer, "data-type", "code-block-close-marker"); + if (codeBlockMarkerElement) { + if (codeBlockMarkerElement.getAttribute("data-type") === "code-block-close-marker") { + const openMarkerElement = getSideByType(startContainer, "code-block-open-marker"); + if (openMarkerElement) { + openMarkerElement.textContent = codeBlockMarkerElement.textContent; + processAfterRender(vditor); + return; + } + } + if (codeBlockMarkerElement.getAttribute("data-type") === "code-block-open-marker") { + const openMarkerElement = getSideByType(startContainer, "code-block-close-marker", false); + if (openMarkerElement) { + openMarkerElement.textContent = codeBlockMarkerElement.textContent; + processAfterRender(vditor); + return; + } + } + } + // https://github.com/Vanessa219/vditor/issues/877 数学公式输入删除生成节点 + const mathBlockMarkerElement = + hasClosestByAttribute(startContainer, "data-type", "math-block-open-marker"); + if (mathBlockMarkerElement) { + const mathBlockCloseElement = mathBlockMarkerElement.nextElementSibling.nextElementSibling; + if (mathBlockCloseElement && mathBlockCloseElement.getAttribute("data-type") === "math-block-close-marker") { + mathBlockCloseElement.remove(); + processAfterRender(vditor); + } + return; + } + + blockElement.querySelectorAll('[data-type="code-block-open-marker"]').forEach((item: HTMLElement) => { + if (item.textContent.length === 1) { + item.remove(); + } + }); + blockElement.querySelectorAll('[data-type="code-block-close-marker"]').forEach((item: HTMLElement) => { + if (item.textContent.length === 1) { + item.remove(); + } + }); + + // 标题删除 + const headingElement = hasClosestByAttribute(startContainer, "data-type", "heading-marker"); + if (headingElement && headingElement.textContent.indexOf("#") === -1) { + processAfterRender(vditor); + return; + } + } + // 删除或空格不解析,否则会 format 回去 + if ((event.data === " " || event.inputType === "deleteContentBackward") && + (hasClosestByAttribute(startContainer, "data-type", "padding") // 场景:b 前进行删除 [> 1. a\n> b] + || hasClosestByAttribute(startContainer, "data-type", "li-marker") // 场景:删除最后一个字符 [* 1\n* ] + || hasClosestByAttribute(startContainer, "data-type", "task-marker") // 场景:删除最后一个字符 [* [ ] ] + || hasClosestByAttribute(startContainer, "data-type", "blockquote-marker") // 场景:删除最后一个字符 [> ] + )) { + processAfterRender(vditor); + return; + } + } + if (blockElement && blockElement.textContent.trimRight() === "$$") { + // 内联数学公式 + processAfterRender(vditor); + return; + } + if (!blockElement) { + blockElement = vditor.sv.element; + } + if (blockElement.firstElementChild?.getAttribute("data-type") === "link-ref-defs-block") { + // 修改链接引用 + blockElement = vditor.sv.element; + } + if (hasClosestByAttribute(startContainer, "data-type", "footnotes-link")) { + // 修改脚注角标 + blockElement = vditor.sv.element; + } + // 添加光标位置 + if (blockElement.textContent.indexOf(Lute.Caret) === -1) { + // 点击工具栏会插入 Caret + range.insertNode(document.createTextNode(Lute.Caret)); + } + // 清除浏览器自带的样式 + blockElement.querySelectorAll("[style]").forEach((item) => { // 不可前置,否则会影响 newline 的样式 + item.removeAttribute("style"); + }); + blockElement.querySelectorAll("font").forEach((item) => { // 不可前置,否则会影响光标的位置 + item.outerHTML = item.innerHTML; + }); + let html = blockElement.textContent; + const isSVElement = blockElement.isEqualNode(vditor.sv.element); + if (isSVElement) { + html = blockElement.textContent; + } else { + // 添加前一个块元素 + if (blockElement.previousElementSibling) { + html = blockElement.previousElementSibling.textContent + html; + blockElement.previousElementSibling.remove(); + } + if (blockElement.previousElementSibling && html.indexOf("---\n") === 0) { + // 确认 yaml-front 是否为首行 + html = blockElement.previousElementSibling.textContent + html; + blockElement.previousElementSibling.remove(); + } + // 添加链接引用 + let footnotes = "" + + vditor.sv.element.querySelectorAll("[data-type='link-ref-defs-block']").forEach((item, index) => { + if (item && !(blockElement as HTMLElement).isEqualNode(item.parentElement)) { + footnotes += item.parentElement.textContent + "\n"; + item.parentElement.remove(); + } + }); + + // 添加脚注到文章头,便于lute处理 + vditor.sv.element.querySelectorAll("[data-type='footnotes-link']").forEach((item, index) => { + if (item && !(blockElement as HTMLElement).isEqualNode(item.parentElement)) { + footnotes += item.parentElement.textContent + "\n"; + item.parentElement.remove(); + } + }); + html = footnotes + html; + } + html = processSpinVditorSVDOM(html, vditor); + if (isSVElement) { + blockElement.innerHTML = html; + } else { + blockElement.outerHTML = html; + } + + vditor.sv.element.querySelectorAll("[data-type='link-ref-defs-block']").forEach((item) => { + vditor.sv.element.insertAdjacentElement("beforeend", item.parentElement) + }) + + // 合并脚注 + combineFootnote(vditor.sv.element, (root: HTMLElement) => { + vditor.sv.element.insertAdjacentElement("beforeend", root) + }) + + setRangeByWbr(vditor.sv.element, range); + + scrollCenter(vditor); + + processAfterRender(vditor, { + enableAddUndoStack: true, + enableHint: true, + enableInput: true, + }); +}; diff --git a/web/include/vditor/src/ts/sv/process.ts b/web/include/vditor/src/ts/sv/process.ts new file mode 100644 index 0000000..c024664 --- /dev/null +++ b/web/include/vditor/src/ts/sv/process.ts @@ -0,0 +1,215 @@ +import {getMarkdown} from "../markdown/getMarkdown"; +import {accessLocalStorage} from "../util/compatibility"; +import {scrollCenter} from "../util/editorCommonEvent"; +import {hasClosestBlock, hasClosestByAttribute} from "../util/hasClosest"; +import {hasClosestByTag} from "../util/hasClosestByHeadings"; +import {log} from "../util/log"; +import {getEditorRange, setRangeByWbr} from "../util/selection"; +import {inputEvent} from "./inputEvent"; +import {combineFootnote} from "./combineFootnote"; + + +export const processPaste = (vditor: IVditor, text: string) => { + const range = getEditorRange(vditor); + range.extractContents(); + range.insertNode(document.createTextNode(Lute.Caret)); + range.insertNode(document.createTextNode(text)); + let blockElement = hasClosestByAttribute(range.startContainer, "data-block", "0"); + if (!blockElement) { + blockElement = vditor.sv.element; + } + let spinHTML = vditor.lute.SpinVditorSVDOM(blockElement.textContent) + spinHTML = "
    " + + spinHTML.replace(/
    \n<\/span><\/span>
    \n<\/span><\/span>
    \n

    \n
    <') + + "
    "; + if (blockElement.isEqualNode(vditor.sv.element)) { + blockElement.innerHTML = spinHTML; + } else { + blockElement.outerHTML = spinHTML; + } + combineFootnote(vditor.sv.element) + setRangeByWbr(vditor.sv.element, range); + + scrollCenter(vditor); +}; + +export const getSideByType = (spanNode: Node, type: string, isPrevious = true) => { + let sideElement = spanNode as Element; + if (sideElement.nodeType === 3) { + sideElement = sideElement.parentElement; + } + while (sideElement) { + if (sideElement.getAttribute("data-type") === type) { + return sideElement; + } + if (isPrevious) { + sideElement = sideElement.previousElementSibling; + } else { + sideElement = sideElement.nextElementSibling; + } + } + return false; +}; + +export const processSpinVditorSVDOM = (html: string, vditor: IVditor) => { + log("SpinVditorSVDOM", html, "argument", vditor.options.debugger); + const spinHTML = vditor.lute.SpinVditorSVDOM(html) + html = "
    " + + spinHTML.replace(/
    \n<\/span><\/span>
    \n<\/span><\/span>
    \n

    \n
    <') + + "
    "; + log("SpinVditorSVDOM", html, "result", vditor.options.debugger); + return html; +}; + +export const processPreviousMarkers = (spanElement: HTMLElement) => { + const spanType = spanElement.getAttribute("data-type"); + let previousElement = spanElement.previousElementSibling; + // 有内容的子列表/标题,在其 marker 后换行 + let markerText = (spanType && spanType !== "text" && spanType !== "table" && spanType !== "heading-marker" && + spanType !== "newline" && spanType !== "yaml-front-matter-open-marker" && spanType !== "yaml-front-matter-close-marker" + && spanType !== "code-block-info" && spanType !== "code-block-close-marker" && spanType !== "code-block-open-marker") ? + spanElement.textContent : ""; + let hasNL = false; + if (spanType === "newline") { + hasNL = true; + } + while (previousElement && !hasNL) { + const previousType = previousElement.getAttribute("data-type"); + if (previousType === "li-marker" || previousType === "blockquote-marker" || previousType === "task-marker" || + previousType === "padding") { + const previousText = previousElement.textContent; + if (previousType === "li-marker" && + (spanType === "code-block-open-marker" || spanType === "code-block-info")) { + // https://github.com/Vanessa219/vditor/issues/586 + markerText = previousText.replace(/\S/g, " ") + markerText; + } else if (spanType === "code-block-close-marker" && + previousElement.nextElementSibling.isSameNode(spanElement)) { + // https://github.com/Vanessa219/vditor/issues/594 + const openMarker = getSideByType(spanElement, "code-block-open-marker"); + if (openMarker && openMarker.previousElementSibling) { + previousElement = openMarker.previousElementSibling; + markerText = previousText + markerText; + } + } else { + markerText = previousText + markerText; + } + } else if (previousType === "newline") { + hasNL = true; + } + previousElement = previousElement.previousElementSibling; + } + return markerText; +}; + +export const processAfterRender = (vditor: IVditor, options = { + enableAddUndoStack: true, + enableHint: false, + enableInput: true, +}) => { + if (options.enableHint) { + vditor.hint.render(vditor); + } + + vditor.preview.render(vditor); + + const text = getMarkdown(vditor); + if (typeof vditor.options.input === "function" && options.enableInput) { + vditor.options.input(text); + } + + if (vditor.options.counter.enable) { + vditor.counter.render(vditor, text); + } + + if (vditor.options.cache.enable && accessLocalStorage()) { + localStorage.setItem(vditor.options.cache.id, text); + if (vditor.options.cache.after) { + vditor.options.cache.after(text); + } + } + + if (vditor.devtools) { + vditor.devtools.renderEchart(vditor); + } + + clearTimeout(vditor.sv.processTimeoutId); + vditor.sv.processTimeoutId = window.setTimeout(() => { + if (options.enableAddUndoStack && !vditor.sv.composingLock) { + vditor.undo.addToUndoStack(vditor); + } + }, vditor.options.undoDelay); +}; + +export const processHeading = (vditor: IVditor, value: string) => { + const range = getEditorRange(vditor); + const headingElement = hasClosestByTag(range.startContainer, "SPAN"); + if (headingElement && headingElement.textContent.trim() !== "") { + value = "\n" + value; + } + range.collapse(true); + document.execCommand("insertHTML", false, value); +}; + +export const processToolbar = (vditor: IVditor, actionBtn: Element, prefix: string, suffix: string) => { + const range = getEditorRange(vditor); + const commandName = actionBtn.getAttribute("data-type"); + // 添加 + if (vditor.sv.element.childNodes.length === 0) { + vditor.sv.element.innerHTML = `
    +
    `; + setRangeByWbr(vditor.sv.element, range); + } + const blockElement = hasClosestBlock(range.startContainer); + const spanElement = hasClosestByTag(range.startContainer, "SPAN"); + if (!blockElement) { + return; + } + if (commandName === "link") { + let html; + if (range.toString() === "") { + html = `${prefix}${Lute.Caret}${suffix}`; + } else { + html = `${prefix}${range.toString()}${suffix.replace(")", Lute.Caret + ")")}`; + } + document.execCommand("insertHTML", false, html); + return; + } else if (commandName === "italic" || commandName === "bold" || commandName === "strike" || + commandName === "inline-code" || commandName === "code" || commandName === "table" || commandName === "line") { + let html; + // https://github.com/Vanessa219/vditor/issues/563 代码块不需要后面的 ``` + if (range.toString() === "") { + html = `${prefix}${Lute.Caret}${commandName === "code" ? "" : suffix}`; + } else { + html = `${prefix}${range.toString()}${Lute.Caret}${commandName === "code" ? "" : suffix}`; + } + if (commandName === "table" || (commandName === "code" && spanElement && spanElement.textContent !== "")) { + html = "\n\n" + html; + } else if (commandName === "line") { + html = `\n\n${prefix}\n${Lute.Caret}`; + } + document.execCommand("insertHTML", false, html); + return; + } else if (commandName === "check" || commandName === "list" || commandName === "ordered-list" || + commandName === "quote") { + if (spanElement) { + let marker = "* "; + if (commandName === "check") { + marker = "* [ ] "; + } else if (commandName === "ordered-list") { + marker = "1. "; + } else if (commandName === "quote") { + marker = "> "; + } + const newLine = getSideByType(spanElement, "newline"); + if (newLine) { + newLine.insertAdjacentText("afterend", marker); + } else { + blockElement.insertAdjacentText("afterbegin", marker); + } + inputEvent(vditor); + return; + } + } + setRangeByWbr(vditor.sv.element, range); + processAfterRender(vditor); +}; diff --git a/web/include/vditor/src/ts/sv/processKeydown.ts b/web/include/vditor/src/ts/sv/processKeydown.ts new file mode 100644 index 0000000..8f29dc4 --- /dev/null +++ b/web/include/vditor/src/ts/sv/processKeydown.ts @@ -0,0 +1,202 @@ +import {isCtrl} from "../util/compatibility"; +import {fixTab} from "../util/fixBrowserBehavior"; +import {hasClosestByAttribute} from "../util/hasClosest"; +import {hasClosestByTag} from "../util/hasClosestByHeadings"; +import {getEditorRange, getSelectPosition} from "../util/selection"; +import {inputEvent} from "./inputEvent"; +import {processAfterRender, processPreviousMarkers} from "./process"; + +export const processKeydown = (vditor: IVditor, event: KeyboardEvent) => { + vditor.sv.composingLock = event.isComposing; + if (event.isComposing) { + return false; + } + + if (event.key.indexOf("Arrow") === -1 && event.key !== "Meta" && event.key !== "Control" && event.key !== "Alt" && + event.key !== "Shift" && event.key !== "CapsLock" && event.key !== "Escape" && !/^F\d{1,2}$/.test(event.key)) { + vditor.undo.recordFirstPosition(vditor, event); + } + // 仅处理以下快捷键操作 + if (event.key !== "Enter" && event.key !== "Tab" && event.key !== "Backspace" && event.key.indexOf("Arrow") === -1 + && !isCtrl(event) && event.key !== "Escape") { + return false; + } + const range = getEditorRange(vditor); + let startContainer = range.startContainer; + if (range.startContainer.nodeType !== 3 && (range.startContainer as HTMLElement).tagName === "DIV") { + startContainer = range.startContainer.childNodes[range.startOffset - 1]; + } + const textElement = hasClosestByAttribute(startContainer, "data-type", "text"); + + // blockquote + let blockquoteMarkerElement = hasClosestByAttribute(startContainer, "data-type", "blockquote-marker"); + if (!blockquoteMarkerElement && range.startOffset === 0 && textElement && textElement.previousElementSibling && + textElement.previousElementSibling.getAttribute("data-type") === "blockquote-marker") { + blockquoteMarkerElement = textElement.previousElementSibling as HTMLElement; + } + // 回车逐个删除 blockquote marker 标记 + if (blockquoteMarkerElement) { + if (event.key === "Enter" && !isCtrl(event) && !event.altKey && + blockquoteMarkerElement.nextElementSibling.textContent.trim() === "" && + getSelectPosition(blockquoteMarkerElement, vditor.sv.element, range).start === + blockquoteMarkerElement.textContent.length) { + if (blockquoteMarkerElement.previousElementSibling?.getAttribute("data-type") === "padding") { + // 列表中存在多行 BQ 时,标记回车需跳出列表 + blockquoteMarkerElement.previousElementSibling.setAttribute("data-action", "enter-remove"); + } + blockquoteMarkerElement.remove(); + processAfterRender(vditor); + event.preventDefault(); + return true; + } + } + + // list item + const listMarkerElement = hasClosestByAttribute(startContainer, "data-type", "li-marker") as HTMLElement; + const taskMarkerElement = hasClosestByAttribute(startContainer, "data-type", "task-marker") as HTMLElement; + let listLastMarkerElement = listMarkerElement; + if (!listLastMarkerElement) { + if (taskMarkerElement && taskMarkerElement.nextElementSibling.getAttribute("data-type") !== "task-marker") { + listLastMarkerElement = taskMarkerElement; + } + } + if (!listLastMarkerElement && range.startOffset === 0 && textElement && textElement.previousElementSibling && + (textElement.previousElementSibling.getAttribute("data-type") === "li-marker" || + textElement.previousElementSibling.getAttribute("data-type") === "task-marker")) { + listLastMarkerElement = textElement.previousElementSibling as HTMLElement; + } + if (listLastMarkerElement) { + const startIndex = getSelectPosition(listLastMarkerElement, vditor.sv.element, range).start; + const isTask = listLastMarkerElement.getAttribute("data-type") === "task-marker"; + let listFirstMarkerElement = listLastMarkerElement; + if (isTask) { + listFirstMarkerElement = listLastMarkerElement.previousElementSibling.previousElementSibling + .previousElementSibling as HTMLElement; + } + if (startIndex === listLastMarkerElement.textContent.length) { + // 回车清空列表标记符 + if (event.key === "Enter" && !isCtrl(event) && !event.altKey && !event.shiftKey && + listLastMarkerElement.nextElementSibling.textContent.trim() === "") { + if (listFirstMarkerElement.previousElementSibling?.getAttribute("data-type") === "padding") { + listFirstMarkerElement.previousElementSibling.remove(); + inputEvent(vditor); + } else { + if (isTask) { + listFirstMarkerElement.remove(); + listLastMarkerElement.previousElementSibling.previousElementSibling.remove(); + listLastMarkerElement.previousElementSibling.remove(); + } + listLastMarkerElement.nextElementSibling.remove(); + listLastMarkerElement.remove(); + processAfterRender(vditor); + } + event.preventDefault(); + return true; + } + // 第一个 marker 后 tab 进行缩进 + if (event.key === "Tab") { + if (event.shiftKey) { + if (listFirstMarkerElement.previousElementSibling.getAttribute("data-type") === "padding") { + listFirstMarkerElement.previousElementSibling.remove(); + } + } else { + listFirstMarkerElement.insertAdjacentHTML("beforebegin", + `${listFirstMarkerElement.textContent.replace(/\S/g, " ")}`); + } + if (/^\d/.test(listFirstMarkerElement.textContent)) { + listFirstMarkerElement.textContent = listFirstMarkerElement.textContent.replace(/^\d{1,}/, "1"); + range.selectNodeContents(listLastMarkerElement.firstChild); + range.collapse(false); + } + inputEvent(vditor); + event.preventDefault(); + return true; + } + } + } + + // tab + if (fixTab(vditor, range, event)) { + return true; + } + + const blockElement = hasClosestByAttribute(startContainer, "data-block", "0"); + const spanElement = hasClosestByTag(startContainer, "SPAN"); + // 回车 + if (event.key === "Enter" && !isCtrl(event) && !event.altKey && !event.shiftKey && blockElement) { + let isFirst = false; + const newLineMatch = blockElement.textContent.match(/^\n+/); + if (getSelectPosition(blockElement, vditor.sv.element).start <= (newLineMatch ? newLineMatch[0].length : 0)) { + // 允许段落开始换行 + isFirst = true; + } + let newLineText = "\n"; + if (spanElement) { + if (spanElement.previousElementSibling?.getAttribute("data-action") === "enter-remove") { + // https://github.com/Vanessa219/vditor/issues/596 + spanElement.previousElementSibling.remove(); + processAfterRender(vditor); + event.preventDefault(); + return true; + } else { + newLineText += processPreviousMarkers(spanElement); + } + } + range.insertNode(document.createTextNode(newLineText)); + range.collapse(false); + if (blockElement && blockElement.textContent.trim() !== "" && !isFirst) { + inputEvent(vditor); + } else { + processAfterRender(vditor); + } + event.preventDefault(); + return true; + } + + // 删除后光标前有 newline 的处理 + if (event.key === "Backspace" && !isCtrl(event) && !event.altKey && !event.shiftKey) { + if (spanElement && spanElement.previousElementSibling?.getAttribute("data-type") === "newline" && + getSelectPosition(spanElement, vditor.sv.element, range).start === 1 && + // 飘号的处理需在 inputEvent 中,否则上下飘号对不齐 + spanElement.getAttribute("data-type").indexOf("code-block-") === -1) { + // 光标在每一行的第一个字符后 + range.setStart(spanElement, 0); + range.extractContents(); + if (spanElement.textContent.trim() !== "") { + inputEvent(vditor); + } else { + processAfterRender(vditor); + } + event.preventDefault(); + return true; + } + // 每一段第一个字符前 + if (blockElement && getSelectPosition(blockElement, vditor.sv.element, range).start === 0 && + blockElement.previousElementSibling) { + range.extractContents(); + let previousLastElement = blockElement.previousElementSibling.lastElementChild; + if (previousLastElement.getAttribute("data-type") === "newline") { + previousLastElement.remove(); + previousLastElement = blockElement.previousElementSibling.lastElementChild; + } + // 场景:末尾无法删除 [```\ntext\n```\n\n] + if (previousLastElement.getAttribute("data-type") !== "newline") { + previousLastElement.insertAdjacentHTML("afterend", blockElement.innerHTML); + blockElement.remove(); + } + if (blockElement.textContent.trim() !== "" && !blockElement.previousElementSibling?.querySelector('[data-type="code-block-open-marker"]')) { + inputEvent(vditor); + } else { + if (previousLastElement.getAttribute("data-type") !== "newline") { + // https://github.com/Vanessa219/vditor/issues/597 + range.selectNodeContents(previousLastElement.lastChild); + range.collapse(false); + } + processAfterRender(vditor); + } + event.preventDefault(); + return true; + } + } + return false; +}; diff --git a/web/include/vditor/src/ts/tip/index.ts b/web/include/vditor/src/ts/tip/index.ts new file mode 100644 index 0000000..3a48e05 --- /dev/null +++ b/web/include/vditor/src/ts/tip/index.ts @@ -0,0 +1,39 @@ +export class Tip { + public element: HTMLElement; + + constructor() { + this.element = document.createElement("div"); + this.element.className = "vditor-tip"; + } + + public show(text: string, time: number = 6000) { + this.element.className = "vditor-tip vditor-tip--show"; + if (time === 0) { + this.element.innerHTML = `
    ${text} +
    X
    `; + this.element.querySelector(".vditor-tip__close").addEventListener("click", () => { + this.hide(); + }); + } else { + this.element.innerHTML = `
    ${text}
    `; + setTimeout(() => { + this.hide(); + }, time); + } + + // 需在动画结束后才能确定位置 + this.element.removeAttribute("style") + setTimeout(() => { + const rect = this.element.getBoundingClientRect(); + if (rect.top < 46) { + this.element.style.position = "fixed" + this.element.style.top = "46px" + } + }, 150); + } + + public hide() { + this.element.className = "vditor-messageElementtip"; + this.element.innerHTML = ""; + } +} diff --git a/web/include/vditor/src/ts/toolbar/Both.ts b/web/include/vditor/src/ts/toolbar/Both.ts new file mode 100644 index 0000000..8c5d2a6 --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/Both.ts @@ -0,0 +1,28 @@ +import {Constants} from "../constants"; +import {setPreviewMode} from "../ui/setPreviewMode"; +import {getEventName} from "../util/compatibility"; +import {MenuItem} from "./MenuItem"; + +export class Both extends MenuItem { + constructor(vditor: IVditor, menuItem: IMenuItem) { + super(vditor, menuItem); + if (vditor.options.preview.mode === "both") { + this.element.children[0].classList.add("vditor-menu--current"); + } + this.element.children[0].addEventListener(getEventName(), (event) => { + const btnElement = this.element.firstElementChild; + if (btnElement.classList.contains(Constants.CLASS_MENU_DISABLED)) { + return; + } + event.preventDefault(); + if (vditor.currentMode !== "sv") { + return; + } + if (vditor.options.preview.mode === "both") { + setPreviewMode("editor", vditor); + } else { + setPreviewMode("both", vditor); + } + }); + } +} diff --git a/web/include/vditor/src/ts/toolbar/Br.ts b/web/include/vditor/src/ts/toolbar/Br.ts new file mode 100644 index 0000000..fd49d53 --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/Br.ts @@ -0,0 +1,8 @@ +export class Br { + public element: HTMLElement; + + constructor() { + this.element = document.createElement("div"); + this.element.className = "vditor-toolbar__br"; + } +} diff --git a/web/include/vditor/src/ts/toolbar/CodeTheme.ts b/web/include/vditor/src/ts/toolbar/CodeTheme.ts new file mode 100644 index 0000000..fbe2656 --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/CodeTheme.ts @@ -0,0 +1,36 @@ +import {Constants} from "../constants"; +import {setCodeTheme} from "../ui/setCodeTheme"; +import {getEventName} from "../util/compatibility"; +import {MenuItem} from "./MenuItem"; +import {hidePanel, toggleSubMenu} from "./setToolbar"; + +export class CodeTheme extends MenuItem { + public element: HTMLElement; + + constructor(vditor: IVditor, menuItem: IMenuItem) { + super(vditor, menuItem); + + const actionBtn = this.element.children[0] as HTMLElement; + + const panelElement = document.createElement("div"); + panelElement.className = `vditor-hint${menuItem.level === 2 ? "" : " vditor-panel--arrow"}`; + let innerHTML = ""; + Constants.CODE_THEME.forEach((theme) => { + innerHTML += ``; + }); + panelElement.innerHTML = + `
    ${innerHTML}
    `; + panelElement.addEventListener(getEventName(), (event: MouseEvent & { target: HTMLElement }) => { + if (event.target.tagName === "BUTTON") { + hidePanel(vditor, ["subToolbar"]); + vditor.options.preview.hljs.style = event.target.textContent; + setCodeTheme(event.target.textContent, vditor.options.cdn); + event.preventDefault(); + event.stopPropagation(); + } + }); + this.element.appendChild(panelElement); + + toggleSubMenu(vditor, panelElement, actionBtn, menuItem.level); + } +} diff --git a/web/include/vditor/src/ts/toolbar/ContentTheme.ts b/web/include/vditor/src/ts/toolbar/ContentTheme.ts new file mode 100644 index 0000000..436848b --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/ContentTheme.ts @@ -0,0 +1,35 @@ +import {setContentTheme} from "../ui/setContentTheme"; +import {getEventName} from "../util/compatibility"; +import {MenuItem} from "./MenuItem"; +import {hidePanel, toggleSubMenu} from "./setToolbar"; + +export class ContentTheme extends MenuItem { + public element: HTMLElement; + + constructor(vditor: IVditor, menuItem: IMenuItem) { + super(vditor, menuItem); + + const actionBtn = this.element.children[0] as HTMLElement; + + const panelElement = document.createElement("div"); + panelElement.className = `vditor-hint${menuItem.level === 2 ? "" : " vditor-panel--arrow"}`; + let innerHTML = ""; + Object.keys(vditor.options.preview.theme.list).forEach((key) => { + innerHTML += ``; + }); + panelElement.innerHTML = + `
    ${innerHTML}
    `; + panelElement.addEventListener(getEventName(), (event: MouseEvent & { target: HTMLElement }) => { + if (event.target.tagName === "BUTTON") { + hidePanel(vditor, ["subToolbar"]); + vditor.options.preview.theme.current = event.target.getAttribute("data-type"); + setContentTheme(vditor.options.preview.theme.current, vditor.options.preview.theme.path); + event.preventDefault(); + event.stopPropagation(); + } + }); + this.element.appendChild(panelElement); + + toggleSubMenu(vditor, panelElement, actionBtn, menuItem.level); + } +} diff --git a/web/include/vditor/src/ts/toolbar/Counter.ts b/web/include/vditor/src/ts/toolbar/Counter.ts new file mode 100644 index 0000000..72f02c8 --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/Counter.ts @@ -0,0 +1,40 @@ +export class Counter { + public element: HTMLElement; + + constructor(vditor: IVditor) { + this.element = document.createElement("span"); + this.element.className = "vditor-counter vditor-tooltipped vditor-tooltipped__nw"; + + this.render(vditor, ""); + + } + + public render(vditor: IVditor, mdText: string) { + let length = mdText.endsWith("\n") ? mdText.length - 1 : mdText.length; + if (vditor.options.counter.type === "text" && vditor[vditor.currentMode]) { + const tempElement = vditor[vditor.currentMode].element.cloneNode(true) as HTMLElement; + tempElement.querySelectorAll(".vditor-wysiwyg__preview").forEach((item) => { + item.remove(); + }); + length = tempElement.textContent.length; + } + if (typeof vditor.options.counter.max === "number") { + if (length > vditor.options.counter.max) { + this.element.className = "vditor-counter vditor-counter--error"; + } else { + this.element.className = "vditor-counter"; + } + this.element.innerHTML = `${length}/${vditor.options.counter.max}`; + } else { + this.element.innerHTML = `${length}`; + } + this.element.setAttribute("aria-label", vditor.options.counter.type); + if (vditor.options.counter.after) { + vditor.options.counter.after(length, { + enable: vditor.options.counter.enable, + max: vditor.options.counter.max, + type: vditor.options.counter.type, + }); + } + } +} diff --git a/web/include/vditor/src/ts/toolbar/Custom.ts b/web/include/vditor/src/ts/toolbar/Custom.ts new file mode 100644 index 0000000..45bad3d --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/Custom.ts @@ -0,0 +1,17 @@ +import {Constants} from "../constants"; +import {getEventName} from "../util/compatibility"; +import {MenuItem} from "./MenuItem"; + +export class Custom extends MenuItem { + constructor(vditor: IVditor, menuItem: IMenuItem) { + super(vditor, menuItem); + this.element.children[0].innerHTML = menuItem.icon; + this.element.children[0].addEventListener(getEventName(), (event: Event & { currentTarget: HTMLElement }) => { + event.preventDefault(); + if (event.currentTarget.classList.contains(Constants.CLASS_MENU_DISABLED)) { + return; + } + menuItem.click(event, vditor); + }); + } +} diff --git a/web/include/vditor/src/ts/toolbar/Devtools.ts b/web/include/vditor/src/ts/toolbar/Devtools.ts new file mode 100644 index 0000000..bd23d3c --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/Devtools.ts @@ -0,0 +1,28 @@ +import {Constants} from "../constants"; +import {setPadding} from "../ui/initUI"; +import {getEventName} from "../util/compatibility"; +import {MenuItem} from "./MenuItem"; + +export class Devtools extends MenuItem { + constructor(vditor: IVditor, menuItem: IMenuItem) { + super(vditor, menuItem); + this.element.firstElementChild.addEventListener(getEventName(), (event) => { + const btnElement = this.element.firstElementChild; + if (btnElement.classList.contains(Constants.CLASS_MENU_DISABLED)) { + return; + } + + event.preventDefault(); + if (btnElement.classList.contains("vditor-menu--current")) { + btnElement.classList.remove("vditor-menu--current"); + vditor.devtools.element.style.display = "none"; + setPadding(vditor); + } else { + btnElement.classList.add("vditor-menu--current"); + vditor.devtools.element.style.display = "block"; + setPadding(vditor); + vditor.devtools.renderEchart(vditor); + } + }); + } +} diff --git a/web/include/vditor/src/ts/toolbar/Divider.ts b/web/include/vditor/src/ts/toolbar/Divider.ts new file mode 100644 index 0000000..4523423 --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/Divider.ts @@ -0,0 +1,8 @@ +export class Divider { + public element: HTMLElement; + + constructor() { + this.element = document.createElement("div"); + this.element.className = "vditor-toolbar__divider"; + } +} diff --git a/web/include/vditor/src/ts/toolbar/EditMode.ts b/web/include/vditor/src/ts/toolbar/EditMode.ts new file mode 100644 index 0000000..710e717 --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/EditMode.ts @@ -0,0 +1,195 @@ +import {Constants} from "../constants"; +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"; +import {highlightToolbar} from "../util/highlightToolbar"; +import {processCodeRender} from "../util/processCode"; +import {renderToc} from "../util/toc"; +import {renderDomByMd} from "../wysiwyg/renderDomByMd"; +import {MenuItem} from "./MenuItem"; +import { + disableToolbar, + enableToolbar, + hidePanel, + hideToolbar, + removeCurrentToolbar, + showToolbar, toggleSubMenu, +} from "./setToolbar"; +import {combineFootnote} from "../sv/combineFootnote"; + +export const setEditMode = (vditor: IVditor, type: string, event: Event | string) => { + let markdownText; + if (typeof event !== "string") { + hidePanel(vditor, ["subToolbar", "hint"]); + event.preventDefault(); + markdownText = getMarkdown(vditor); + } else { + markdownText = event; + } + if (vditor.currentMode === type && typeof event !== "string") { + return; + } + if (vditor.devtools) { + vditor.devtools.renderEchart(vditor); + } + if (vditor.options.preview.mode === "both" && type === "sv") { + vditor.preview.element.style.display = "block"; + } else { + vditor.preview.element.style.display = "none"; + } + + enableToolbar(vditor.toolbar.elements, Constants.EDIT_TOOLBARS); + removeCurrentToolbar(vditor.toolbar.elements, Constants.EDIT_TOOLBARS); + disableToolbar(vditor.toolbar.elements, ["outdent", "indent"]); + + if (type === "ir") { + hideToolbar(vditor.toolbar.elements, ["both"]); + showToolbar(vditor.toolbar.elements, ["outdent", "indent", "outline", "insert-before", "insert-after"]); + vditor.sv.element.style.display = "none"; + vditor.wysiwyg.element.parentElement.style.display = "none"; + vditor.ir.element.parentElement.style.display = "block"; + + vditor.lute.SetVditorIR(true); + vditor.lute.SetVditorWYSIWYG(false); + vditor.lute.SetVditorSV(false); + + vditor.currentMode = "ir"; + vditor.ir.element.innerHTML = vditor.lute.Md2VditorIRDOM(markdownText); + processAfterRender(vditor, { + enableAddUndoStack: true, + enableHint: false, + enableInput: false, + }); + + setPadding(vditor); + + 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"]); + vditor.sv.element.style.display = "none"; + vditor.wysiwyg.element.parentElement.style.display = "block"; + vditor.ir.element.parentElement.style.display = "none"; + + vditor.lute.SetVditorIR(false); + vditor.lute.SetVditorWYSIWYG(true); + vditor.lute.SetVditorSV(false); + + vditor.currentMode = "wysiwyg"; + + setPadding(vditor); + renderDomByMd(vditor, markdownText, { + enableAddUndoStack: true, + 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"]); + hideToolbar(vditor.toolbar.elements, ["outdent", "indent", "outline", "insert-before", "insert-after"]); + vditor.wysiwyg.element.parentElement.style.display = "none"; + vditor.ir.element.parentElement.style.display = "none"; + if (vditor.options.preview.mode === "both") { + vditor.sv.element.style.display = "block"; + } else if (vditor.options.preview.mode === "editor") { + vditor.sv.element.style.display = "block"; + } + + vditor.lute.SetVditorIR(false); + vditor.lute.SetVditorWYSIWYG(false); + vditor.lute.SetVditorSV(true); + + vditor.currentMode = "sv"; + let svHTML = processSpinVditorSVDOM(markdownText, vditor); + if (svHTML === "
    ") { + // https://github.com/Vanessa219/vditor/issues/654 SV 模式 Placeholder 显示问题 + svHTML = ""; + } + vditor.sv.element.innerHTML = svHTML; + combineFootnote(vditor.sv.element) + processSVAfterRender(vditor, { + enableAddUndoStack: true, + enableHint: false, + enableInput: false, + }); + setPadding(vditor); + } + vditor.undo.resetIcon(vditor); + if (typeof event !== "string") { + // 初始化不 focus + vditor[vditor.currentMode].element.focus(); + highlightToolbar(vditor); + } + renderToc(vditor); + setTypewriterPosition(vditor); + + if (vditor.toolbar.elements["edit-mode"]) { + vditor.toolbar.elements["edit-mode"].querySelectorAll("button").forEach((item) => { + item.classList.remove("vditor-menu--current"); + }); + vditor.toolbar.elements["edit-mode"].querySelector(`button[data-mode="${vditor.currentMode}"]`).classList.add("vditor-menu--current"); + } + + vditor.outline.toggle(vditor, vditor.currentMode !== "sv" && vditor.options.outline.enable, typeof event !== "string"); +}; + +export class EditMode extends MenuItem { + public element: HTMLElement; + + constructor(vditor: IVditor, menuItem: IMenuItem) { + super(vditor, menuItem); + + const panelElement = document.createElement("div"); + panelElement.className = `vditor-hint${menuItem.level === 2 ? "" : " vditor-panel--arrow"}`; + panelElement.innerHTML = ` + +`; + + this.element.appendChild(panelElement); + + this._bindEvent(vditor, panelElement, menuItem); + } + + public _bindEvent(vditor: IVditor, panelElement: HTMLElement, menuItem: IMenuItem) { + const actionBtn = this.element.children[0] as HTMLElement; + toggleSubMenu(vditor, panelElement, actionBtn, menuItem.level); + + panelElement.children.item(0).addEventListener(getEventName(), (event: Event) => { + // wysiwyg + setEditMode(vditor, "wysiwyg", event); + event.preventDefault(); + event.stopPropagation(); + }); + + panelElement.children.item(1).addEventListener(getEventName(), (event: Event) => { + // ir + setEditMode(vditor, "ir", event); + event.preventDefault(); + event.stopPropagation(); + }); + + panelElement.children.item(2).addEventListener(getEventName(), (event: Event) => { + // markdown + setEditMode(vditor, "sv", event); + event.preventDefault(); + event.stopPropagation(); + }); + } +} diff --git a/web/include/vditor/src/ts/toolbar/Emoji.ts b/web/include/vditor/src/ts/toolbar/Emoji.ts new file mode 100644 index 0000000..7f0021b --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/Emoji.ts @@ -0,0 +1,74 @@ +import {getEventName} from "../util/compatibility"; +import {execAfterRender} from "../util/fixBrowserBehavior"; +import {hasClosestByTag} from "../util/hasClosestByHeadings"; +import {getEditorRange, insertHTML, setSelectionFocus} from "../util/selection"; +import {MenuItem} from "./MenuItem"; +import {toggleSubMenu} from "./setToolbar"; + +export class Emoji extends MenuItem { + public element: HTMLElement; + + constructor(vditor: IVditor, menuItem: IMenuItem) { + super(vditor, menuItem); + const panelElement = document.createElement("div"); + panelElement.className = "vditor-panel vditor-panel--arrow"; + + let commonEmojiHTML = ""; + Object.keys(vditor.options.hint.emoji).forEach((key) => { + const emojiValue = vditor.options.hint.emoji[key]; + if (emojiValue.indexOf(".") > -1) { + commonEmojiHTML += ``; + } else { + commonEmojiHTML += ``; + } + }); + panelElement.innerHTML = `
    ${commonEmojiHTML}
    + ${vditor.options.hint.emojiTail || ""} +
    `; + + this.element.appendChild(panelElement); + + toggleSubMenu(vditor, panelElement, this.element.firstElementChild, menuItem.level); + this.bindEvent(vditor); + } + + private bindEvent(vditor: IVditor) { + this.element.lastElementChild.addEventListener(getEventName(), (event: Event & { target: Element }) => { + const btnElement = hasClosestByTag(event.target, "BUTTON"); + if (btnElement) { + event.preventDefault(); + const value = btnElement.getAttribute("data-value"); + const range = getEditorRange(vditor); + let html = value; + if (vditor.currentMode === "wysiwyg") { + html = vditor.lute.SpinVditorDOM(value); + } else if (vditor.currentMode === "ir") { + html = vditor.lute.SpinVditorIRDOM(value); + } + if (value.indexOf(":") > -1 && vditor.currentMode !== "sv") { + const tempElement = document.createElement("div"); + tempElement.innerHTML = html; + html = tempElement.firstElementChild.firstElementChild.outerHTML + " "; + insertHTML(html, vditor); + } else { + range.extractContents(); + range.insertNode(document.createTextNode(value)); + } + range.collapse(false); + setSelectionFocus(range); + (this.element.lastElementChild as HTMLElement).style.display = "none"; + execAfterRender(vditor); + } + }); + this.element.lastElementChild.addEventListener("mouseover", (event: Event & { target: Element }) => { + const btnElement = hasClosestByTag(event.target, "BUTTON"); + if (btnElement) { + this.element.querySelector(".vditor-emojis__tip").innerHTML = btnElement.getAttribute("data-key"); + } + }); + } +} diff --git a/web/include/vditor/src/ts/toolbar/Export.ts b/web/include/vditor/src/ts/toolbar/Export.ts new file mode 100644 index 0000000..c757149 --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/Export.ts @@ -0,0 +1,41 @@ +import {exportHTML, exportMarkdown, exportPDF} from "../export"; +import {getEventName} from "../util/compatibility"; +import {MenuItem} from "./MenuItem"; +import {hidePanel, toggleSubMenu} from "./setToolbar"; + +export class Export extends MenuItem { + public element: HTMLElement; + + constructor(vditor: IVditor, menuItem: IMenuItem) { + super(vditor, menuItem); + const actionBtn = this.element.children[0] as HTMLElement; + const panelElement = document.createElement("div"); + panelElement.className = `vditor-hint${menuItem.level === 2 ? "" : " vditor-panel--arrow"}`; + panelElement.innerHTML = ` + +`; + panelElement.addEventListener(getEventName(), (event: MouseEvent & { target: HTMLElement }) => { + const btnElement = event.target; + if (btnElement.tagName === "BUTTON") { + switch (btnElement.getAttribute("data-type")) { + case "markdown": + exportMarkdown(vditor); + break; + case "pdf": + exportPDF(vditor); + break; + case "html": + exportHTML(vditor); + break; + default: + break; + } + hidePanel(vditor, ["subToolbar"]); + event.preventDefault(); + event.stopPropagation(); + } + }); + this.element.appendChild(panelElement); + toggleSubMenu(vditor, panelElement, actionBtn, menuItem.level); + } +} diff --git a/web/include/vditor/src/ts/toolbar/Fullscreen.ts b/web/include/vditor/src/ts/toolbar/Fullscreen.ts new file mode 100644 index 0000000..c65d659 --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/Fullscreen.ts @@ -0,0 +1,66 @@ +import {setPadding, setTypewriterPosition} from "../ui/initUI"; +import {getEventName} from "../util/compatibility"; +import {MenuItem} from "./MenuItem"; + +export class Fullscreen extends MenuItem { + constructor(vditor: IVditor, menuItem: IMenuItem) { + super(vditor, menuItem); + this._bindEvent(vditor, menuItem); + } + + public _bindEvent(vditor: IVditor, menuItem: IMenuItem) { + this.element.children[0].addEventListener(getEventName(), function (event) { + event.preventDefault(); + if (vditor.element.className.includes("vditor--fullscreen")) { + if (!menuItem.level) { + this.innerHTML = menuItem.icon; + } + vditor.element.style.zIndex = ""; + document.body.style.overflow = ""; + vditor.element.classList.remove("vditor--fullscreen"); + Object.keys(vditor.toolbar.elements).forEach((key) => { + const svgElement = vditor.toolbar.elements[key].firstChild as HTMLElement; + if (svgElement) { + svgElement.className = svgElement.className.replace("__s", "__n"); + vditor.options.toolbar.forEach(item => { + if (typeof item !== "string" && item.tipPosition && item.name === svgElement.dataset.type) { + svgElement.className = `vditor-tooltipped vditor-tooltipped__${item.tipPosition}`; + } + }) + } + }); + if (vditor.counter) { + vditor.counter.element.className = vditor.counter.element.className.replace("__s", "__n"); + } + } else { + if (!menuItem.level) { + this.innerHTML = ''; + } + vditor.element.style.zIndex = vditor.options.fullscreen.index.toString(); + document.body.style.overflow = "hidden"; + vditor.element.classList.add("vditor--fullscreen"); + Object.keys(vditor.toolbar.elements).forEach((key) => { + const svgElement = vditor.toolbar.elements[key].firstChild as HTMLElement; + if (svgElement) { + svgElement.className = svgElement.className.replace("__n", "__s"); + } + }); + if (vditor.counter) { + vditor.counter.element.className = vditor.counter.element.className.replace("__n", "__s"); + } + } + + if (vditor.devtools) { + vditor.devtools.renderEchart(vditor); + } + + if (menuItem.click) { + menuItem.click(event, vditor); + } + + setPadding(vditor); + + setTypewriterPosition(vditor); + }); + } +} diff --git a/web/include/vditor/src/ts/toolbar/Headings.ts b/web/include/vditor/src/ts/toolbar/Headings.ts new file mode 100644 index 0000000..229c526 --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/Headings.ts @@ -0,0 +1,73 @@ +import {Constants} from "../constants"; +import {processHeading} from "../ir/process"; +import {processHeading as processHeadingSV} from "../sv/process"; +import {getEventName, updateHotkeyTip} from "../util/compatibility"; +import {afterRenderEvent} from "../wysiwyg/afterRenderEvent"; +import {removeHeading, setHeading} from "../wysiwyg/setHeading"; +import {MenuItem} from "./MenuItem"; +import {hidePanel} from "./setToolbar"; + +export class Headings extends MenuItem { + public element: HTMLElement; + + constructor(vditor: IVditor, menuItem: IMenuItem) { + super(vditor, menuItem); + + const panelElement = document.createElement("div"); + panelElement.className = "vditor-hint vditor-panel--arrow"; + panelElement.innerHTML = ` + + + + +`; + + this.element.appendChild(panelElement); + + this._bindEvent(vditor, panelElement); + } + + public _bindEvent(vditor: IVditor, panelElement: HTMLElement) { + const actionBtn = this.element.children[0] as HTMLElement; + actionBtn.addEventListener(getEventName(), (event) => { + event.preventDefault(); + // https://github.com/Vanessa219/vditor/issues/1391 + clearTimeout(vditor.wysiwyg.afterRenderTimeoutId); + clearTimeout(vditor.ir.processTimeoutId); + clearTimeout(vditor.sv.processTimeoutId); + if (actionBtn.classList.contains(Constants.CLASS_MENU_DISABLED)) { + return; + } + actionBtn.blur(); + if (actionBtn.classList.contains("vditor-menu--current")) { + if (vditor.currentMode === "wysiwyg") { + removeHeading(vditor); + afterRenderEvent(vditor); + } else if (vditor.currentMode === "ir") { + processHeading(vditor, ""); + } + actionBtn.classList.remove("vditor-menu--current"); + } else { + hidePanel(vditor, ["subToolbar"]); + panelElement.style.display = "block"; + } + }); + + for (let i = 0; i < 6; i++) { + panelElement.children.item(i).addEventListener(getEventName(), (event: Event) => { + event.preventDefault(); + if (vditor.currentMode === "wysiwyg") { + setHeading(vditor, (event.target as HTMLElement).getAttribute("data-tag")); + afterRenderEvent(vditor); + actionBtn.classList.add("vditor-menu--current"); + } else if (vditor.currentMode === "ir") { + processHeading(vditor, (event.target as HTMLElement).getAttribute("data-value")); + actionBtn.classList.add("vditor-menu--current"); + } else { + processHeadingSV(vditor, (event.target as HTMLElement).getAttribute("data-value")); + } + panelElement.style.display = "none"; + }); + } + } +} diff --git a/web/include/vditor/src/ts/toolbar/Help.ts b/web/include/vditor/src/ts/toolbar/Help.ts new file mode 100644 index 0000000..34abf33 --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/Help.ts @@ -0,0 +1,30 @@ +import {getEventName} from "../util/compatibility"; +import {MenuItem} from "./MenuItem"; + +export class Help extends MenuItem { + constructor(vditor: IVditor, menuItem: IMenuItem) { + super(vditor, menuItem); + this.element.children[0].addEventListener(getEventName(), (event) => { + event.preventDefault(); + vditor.tip.show(`
    +
    +
    Markdown 使用指南
    + +
    +
    +
    Vditor 支持
    + +
    `, 0); + }); + } +} diff --git a/web/include/vditor/src/ts/toolbar/Indent.ts b/web/include/vditor/src/ts/toolbar/Indent.ts new file mode 100644 index 0000000..26c5370 --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/Indent.ts @@ -0,0 +1,25 @@ +import {Constants} from "../constants"; +import {getEventName} from "../util/compatibility"; +import {listIndent} from "../util/fixBrowserBehavior"; +import {hasClosestByMatchTag} from "../util/hasClosest"; +import {getEditorRange} from "../util/selection"; +import {MenuItem} from "./MenuItem"; + +export class Indent extends MenuItem { + constructor(vditor: IVditor, menuItem: IMenuItem) { + super(vditor, menuItem); + + this.element.children[0].addEventListener(getEventName(), (event) => { + event.preventDefault(); + if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED) || + vditor.currentMode === "sv") { + return; + } + const range = getEditorRange(vditor); + const liElement = hasClosestByMatchTag(range.startContainer, "LI"); + if (liElement) { + listIndent(vditor, liElement, range); + } + }); + } +} diff --git a/web/include/vditor/src/ts/toolbar/Info.ts b/web/include/vditor/src/ts/toolbar/Info.ts new file mode 100644 index 0000000..f7d6d90 --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/Info.ts @@ -0,0 +1,43 @@ +import {VDITOR_VERSION} from "../constants"; +import {getEventName} from "../util/compatibility"; +import {MenuItem} from "./MenuItem"; + +export class Info extends MenuItem { + constructor(vditor: IVditor, menuItem: IMenuItem) { + super(vditor, menuItem); + this.element.children[0].addEventListener(getEventName(), (event) => { + event.preventDefault(); + vditor.tip.show(`
    +

    + 下一代的 Markdown 编辑器,为未来而构建 +

    +
    + +
      
    +
    + Vditor 是一款浏览器端的 Markdown 编辑器,支持所见即所得、即时渲染(类似 Typora)和分屏预览模式。 + 它使用 TypeScript 实现,支持原生 JavaScript 以及 Vue、React、Angular 和 Svelte 等框架。 +
    +
    +
    + + +
    +
    `, 0); + }); + } +} diff --git a/web/include/vditor/src/ts/toolbar/InsertAfter.ts b/web/include/vditor/src/ts/toolbar/InsertAfter.ts new file mode 100644 index 0000000..3a8596d --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/InsertAfter.ts @@ -0,0 +1,18 @@ +import {Constants} from "../constants"; +import {getEventName} from "../util/compatibility"; +import {insertEmptyBlock} from "../util/fixBrowserBehavior"; +import {MenuItem} from "./MenuItem"; + +export class InsertAfter extends MenuItem { + constructor(vditor: IVditor, menuItem: IMenuItem) { + super(vditor, menuItem); + this.element.children[0].addEventListener(getEventName(), (event) => { + event.preventDefault(); + if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED) || + vditor.currentMode === "sv") { + return; + } + insertEmptyBlock(vditor, "afterend"); + }); + } +} diff --git a/web/include/vditor/src/ts/toolbar/InsertBefore.ts b/web/include/vditor/src/ts/toolbar/InsertBefore.ts new file mode 100644 index 0000000..a10a67e --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/InsertBefore.ts @@ -0,0 +1,18 @@ +import {Constants} from "../constants"; +import {getEventName} from "../util/compatibility"; +import {insertEmptyBlock} from "../util/fixBrowserBehavior"; +import {MenuItem} from "./MenuItem"; + +export class InsertBefore extends MenuItem { + constructor(vditor: IVditor, menuItem: IMenuItem) { + super(vditor, menuItem); + this.element.children[0].addEventListener(getEventName(), (event) => { + event.preventDefault(); + if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED) || + vditor.currentMode === "sv") { + return; + } + insertEmptyBlock(vditor, "beforebegin"); + }); + } +} diff --git a/web/include/vditor/src/ts/toolbar/MenuItem.ts b/web/include/vditor/src/ts/toolbar/MenuItem.ts new file mode 100644 index 0000000..e9fb4a1 --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/MenuItem.ts @@ -0,0 +1,54 @@ +import {Constants} from "../constants"; +import {processToolbar} from "../ir/process"; +import {processToolbar as processToolbarSV} from "../sv/process"; +import {getEventName} from "../util/compatibility"; +import {updateHotkeyTip} from "../util/compatibility"; +import {toolbarEvent} from "../wysiwyg/toolbarEvent"; + +export class MenuItem { + public element: HTMLElement; + + constructor(vditor: IVditor, menuItem: IMenuItem) { + this.element = document.createElement("div"); + if (menuItem.className) { + this.element.classList.add(...menuItem.className.split(" ")); + } + + let hotkey = menuItem.hotkey ? ` <${updateHotkeyTip(menuItem.hotkey)}>` : ""; + if (menuItem.level === 2) { + hotkey = menuItem.hotkey ? ` <${updateHotkeyTip(menuItem.hotkey)}>` : ""; + } + const tip = menuItem.tip ? menuItem.tip + hotkey : `${window.VditorI18n[menuItem.name]}${hotkey}`; + const tagName = menuItem.name === "upload" ? "div" : "button"; + if (menuItem.level === 2) { + this.element.innerHTML = `<${tagName} data-type="${menuItem.name}">${tip}`; + } else { + this.element.classList.add("vditor-toolbar__item"); + const iconElement = document.createElement(tagName); + iconElement.setAttribute("data-type", menuItem.name); + iconElement.className = `vditor-tooltipped vditor-tooltipped__${menuItem.tipPosition}`; + iconElement.setAttribute("aria-label", tip); + iconElement.innerHTML = menuItem.icon; + this.element.appendChild(iconElement); + } + + if (!menuItem.prefix) { + return; + } + this.element.children[0].addEventListener(getEventName(), (event) => { + event.preventDefault(); + if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED)) { + return; + } + if (vditor.currentMode === "wysiwyg") { + toolbarEvent(vditor, this.element.children[0], event); + } else if (vditor.currentMode === "ir") { + processToolbar(vditor, this.element.children[0], + menuItem.prefix || "", menuItem.suffix || ""); + } else { + processToolbarSV(vditor, this.element.children[0], + menuItem.prefix || "", menuItem.suffix || ""); + } + }); + } +} diff --git a/web/include/vditor/src/ts/toolbar/Outdent.ts b/web/include/vditor/src/ts/toolbar/Outdent.ts new file mode 100644 index 0000000..fd03f60 --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/Outdent.ts @@ -0,0 +1,24 @@ +import {Constants} from "../constants"; +import {getEventName} from "../util/compatibility"; +import { listOutdent} from "../util/fixBrowserBehavior"; +import {hasClosestByMatchTag} from "../util/hasClosest"; +import {getEditorRange} from "../util/selection"; +import {MenuItem} from "./MenuItem"; + +export class Outdent extends MenuItem { + constructor(vditor: IVditor, menuItem: IMenuItem) { + super(vditor, menuItem); + this.element.children[0].addEventListener(getEventName(), (event) => { + event.preventDefault(); + if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED) || + vditor.currentMode === "sv") { + return; + } + const range = getEditorRange(vditor); + const liElement = hasClosestByMatchTag(range.startContainer, "LI"); + if (liElement) { + listOutdent(vditor, liElement, range, liElement.parentElement); + } + }); + } +} diff --git a/web/include/vditor/src/ts/toolbar/Outline.ts b/web/include/vditor/src/ts/toolbar/Outline.ts new file mode 100644 index 0000000..0630304 --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/Outline.ts @@ -0,0 +1,21 @@ +import {Constants} from "../constants"; +import {getEventName} from "../util/compatibility"; +import {MenuItem} from "./MenuItem"; + +export class Outline extends MenuItem { + constructor(vditor: IVditor, menuItem: IMenuItem) { + super(vditor, menuItem); + if (vditor.options.outline) { + this.element.firstElementChild.classList.add("vditor-menu--current"); + } + this.element.children[0].addEventListener(getEventName(), (event) => { + event.preventDefault(); + const btnElement = vditor.toolbar.elements.outline.firstElementChild; + if (btnElement.classList.contains(Constants.CLASS_MENU_DISABLED)) { + return; + } + vditor.options.outline.enable = !this.element.firstElementChild.classList.contains("vditor-menu--current"); + vditor.outline.toggle(vditor, vditor.options.outline.enable); + }); + } +} diff --git a/web/include/vditor/src/ts/toolbar/Preview.ts b/web/include/vditor/src/ts/toolbar/Preview.ts new file mode 100644 index 0000000..72b83b0 --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/Preview.ts @@ -0,0 +1,55 @@ +import {Constants} from "../constants"; +import {setPadding} from "../ui/initUI"; +import {getEventName} from "../util/compatibility"; +import {MenuItem} from "./MenuItem"; +import {disableToolbar, enableToolbar, hidePanel} from "./setToolbar"; + +export class Preview extends MenuItem { + constructor(vditor: IVditor, menuItem: IMenuItem) { + super(vditor, menuItem); + this._bindEvent(vditor); + } + + public _bindEvent(vditor: IVditor) { + this.element.children[0].addEventListener(getEventName(), (event) => { + event.preventDefault(); + const btnElement = this.element.firstElementChild; + if (btnElement.classList.contains(Constants.CLASS_MENU_DISABLED)) { + return; + } + + const toolbars = Constants.EDIT_TOOLBARS.concat(["both", "edit-mode", "devtools"]); + if (btnElement.classList.contains("vditor-menu--current")) { + btnElement.classList.remove("vditor-menu--current"); + if (vditor.currentMode === "sv") { + vditor.sv.element.style.display = "block"; + if (vditor.options.preview.mode === "both") { + vditor.preview.element.style.display = "block"; + } else { + vditor.preview.element.style.display = "none"; + } + } else { + vditor[vditor.currentMode].element.parentElement.style.display = "block"; + vditor.preview.element.style.display = "none"; + } + enableToolbar(vditor.toolbar.elements, toolbars); + vditor.outline.render(vditor); + } else { + disableToolbar(vditor.toolbar.elements, toolbars); + vditor.preview.element.style.display = "block"; + if (vditor.currentMode === "sv") { + vditor.sv.element.style.display = "none"; + } else { + vditor[vditor.currentMode].element.parentElement.style.display = "none"; + } + vditor.preview.render(vditor); + btnElement.classList.add("vditor-menu--current"); + hidePanel(vditor, ["subToolbar", "hint", "popover"]); + setTimeout(() => { + vditor.outline.render(vditor); + }, vditor.options.preview.delay + 10); + } + setPadding(vditor); + }); + } +} diff --git a/web/include/vditor/src/ts/toolbar/Record.ts b/web/include/vditor/src/ts/toolbar/Record.ts new file mode 100644 index 0000000..e5387d7 --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/Record.ts @@ -0,0 +1,60 @@ +import {Constants} from "../constants"; +import {uploadFiles} from "../upload/index"; +import {getEventName} from "../util/compatibility"; +import {RecordMedia} from "../util/RecordMedia"; +import {MenuItem} from "./MenuItem"; + +export class Record extends MenuItem { + constructor(vditor: IVditor, menuItem: IMenuItem) { + super(vditor, menuItem); + this._bindEvent(vditor); + } + + public _bindEvent(vditor: IVditor) { + let mediaRecorder: RecordMedia; + this.element.children[0].addEventListener(getEventName(), (event) => { + event.preventDefault(); + if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED)) { + return; + } + const editorElement = vditor[vditor.currentMode].element; + if (!mediaRecorder) { + navigator.mediaDevices.getUserMedia({audio: true}).then((mediaStream: MediaStream) => { + mediaRecorder = new RecordMedia(mediaStream); + mediaRecorder.recorder.onaudioprocess = (e: AudioProcessingEvent) => { + // Do nothing if not recording: + if (!mediaRecorder.isRecording) { + return; + } + + // Copy the data from the input buffers; + const left = e.inputBuffer.getChannelData(0); + const right = e.inputBuffer.getChannelData(1); + mediaRecorder.cloneChannelData(left, right); + }; + mediaRecorder.startRecordingNewWavFile(); + vditor.tip.show(window.VditorI18n.recording); + editorElement.setAttribute("contenteditable", "false"); + this.element.children[0].classList.add("vditor-menu--current"); + }).catch(() => { + vditor.tip.show(window.VditorI18n["record-tip"]); + }); + return; + } + + if (mediaRecorder.isRecording) { + mediaRecorder.stopRecording(); + vditor.tip.hide(); + const file: File = new File([mediaRecorder.buildWavFileBlob()], + `record${(new Date()).getTime()}.wav`, {type: "video/webm"}); + uploadFiles(vditor, [file]); + this.element.children[0].classList.remove("vditor-menu--current"); + } else { + vditor.tip.show(window.VditorI18n.recording); + editorElement.setAttribute("contenteditable", "false"); + mediaRecorder.startRecordingNewWavFile(); + this.element.children[0].classList.add("vditor-menu--current"); + } + }); + } +} diff --git a/web/include/vditor/src/ts/toolbar/Redo.ts b/web/include/vditor/src/ts/toolbar/Redo.ts new file mode 100644 index 0000000..29c3b6d --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/Redo.ts @@ -0,0 +1,18 @@ +import {Constants} from "../constants"; +import {getEventName} from "../util/compatibility"; +import {MenuItem} from "./MenuItem"; +import {disableToolbar} from "./setToolbar"; + +export class Redo extends MenuItem { + constructor(vditor: IVditor, menuItem: IMenuItem) { + super(vditor, menuItem); + disableToolbar({redo: this.element}, ["redo"]); + this.element.children[0].addEventListener(getEventName(), (event) => { + event.preventDefault(); + if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED)) { + return; + } + vditor.undo.redo(vditor); + }); + } +} diff --git a/web/include/vditor/src/ts/toolbar/Undo.ts b/web/include/vditor/src/ts/toolbar/Undo.ts new file mode 100644 index 0000000..ab28ee6 --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/Undo.ts @@ -0,0 +1,18 @@ +import {Constants} from "../constants"; +import {getEventName} from "../util/compatibility"; +import {MenuItem} from "./MenuItem"; +import {disableToolbar} from "./setToolbar"; + +export class Undo extends MenuItem { + constructor(vditor: IVditor, menuItem: IMenuItem) { + super(vditor, menuItem); + disableToolbar({undo: this.element}, ["undo"]); + this.element.children[0].addEventListener(getEventName(), (event) => { + event.preventDefault(); + if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED)) { + return; + } + vditor.undo.undo(vditor); + }); + } +} diff --git a/web/include/vditor/src/ts/toolbar/Upload.ts b/web/include/vditor/src/ts/toolbar/Upload.ts new file mode 100644 index 0000000..2d1d33a --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/Upload.ts @@ -0,0 +1,41 @@ +import {Constants} from "../constants"; +import {uploadFiles} from "../upload/index"; +import {getEventName} from "../util/compatibility"; +import {MenuItem} from "./MenuItem"; + +export class Upload extends MenuItem { + constructor(vditor: IVditor, menuItem: IMenuItem) { + super(vditor, menuItem); + let inputHTML = '')}${inputHTML}>`; + this._bindEvent(vditor); + } + + public _bindEvent(vditor: IVditor) { + this.element.children[0].addEventListener(getEventName(), (event) => { + if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED)) { + event.stopPropagation(); + event.preventDefault(); + return; + } + }); + this.element.querySelector("input").addEventListener("change", + (event: InputEvent & { target: HTMLInputElement }) => { + if (this.element.firstElementChild.classList.contains(Constants.CLASS_MENU_DISABLED)) { + event.stopPropagation(); + event.preventDefault(); + return; + } + if (event.target.files.length === 0) { + return; + } + uploadFiles(vditor, event.target.files, event.target); + }); + } +} diff --git a/web/include/vditor/src/ts/toolbar/index.ts b/web/include/vditor/src/ts/toolbar/index.ts new file mode 100644 index 0000000..f2b8869 --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/index.ts @@ -0,0 +1,192 @@ +import {getEventName} from "../util/compatibility"; +import {Both} from "./Both"; +import {Br} from "./Br"; +import {CodeTheme} from "./CodeTheme"; +import {ContentTheme} from "./ContentTheme"; +import {Counter} from "./Counter"; +import {Custom} from "./Custom"; +import {Devtools} from "./Devtools"; +import {Divider} from "./Divider"; +import {EditMode} from "./EditMode"; +import {Emoji} from "./Emoji"; +import {Export} from "./Export"; +import {Fullscreen} from "./Fullscreen"; +import {Headings} from "./Headings"; +import {Help} from "./Help"; +import {Indent} from "./Indent"; +import {Info} from "./Info"; +import {InsertAfter} from "./InsertAfter"; +import {InsertBefore} from "./InsertBefore"; +import {MenuItem} from "./MenuItem"; +import {Outdent} from "./Outdent"; +import {Outline} from "./Outline"; +import {Preview} from "./Preview"; +import {Record} from "./Record"; +import {Redo} from "./Redo"; +import {toggleSubMenu} from "./setToolbar"; +import {Undo} from "./Undo"; +import {Upload} from "./Upload"; + +export class Toolbar { + public elements: { [key: string]: HTMLElement }; + public element: HTMLElement; + + constructor(vditor: IVditor) { + const options = vditor.options; + this.elements = {}; + + this.element = document.createElement("div"); + this.element.className = "vditor-toolbar"; + + options.toolbar.forEach((menuItem: IMenuItem, i: number) => { + const itemElement = this.genItem(vditor, menuItem, i); + this.element.appendChild(itemElement); + if (menuItem.toolbar) { + const panelElement = document.createElement("div"); + panelElement.className = "vditor-hint vditor-panel--arrow"; + panelElement.addEventListener(getEventName(), (event) => { + panelElement.style.display = "none"; + }); + menuItem.toolbar.forEach((subMenuItem: IMenuItem, subI: number) => { + subMenuItem.level = 2; + panelElement.appendChild(this.genItem(vditor, subMenuItem, i + subI)); + }); + itemElement.appendChild(panelElement); + toggleSubMenu(vditor, panelElement, itemElement.children[0], 2); + } + }); + + if (vditor.options.toolbarConfig.hide) { + this.element.classList.add("vditor-toolbar--hide"); + } + if (vditor.options.toolbarConfig.pin) { + this.element.classList.add("vditor-toolbar--pin"); + } + + if (vditor.options.counter.enable) { + vditor.counter = new Counter(vditor); + this.element.appendChild(vditor.counter.element); + } + } + + public updateConfig(vditor: IVditor, options: IToolbarConfig) { + vditor.options.toolbarConfig = Object.assign({ + hide: false, + pin: false, + }, options); + if (vditor.options.toolbarConfig.hide) { + this.element.classList.add("vditor-toolbar--hide"); + } else { + this.element.classList.remove("vditor-toolbar--hide"); + } + if (vditor.options.toolbarConfig.pin) { + this.element.classList.add("vditor-toolbar--pin"); + } else { + this.element.classList.remove("vditor-toolbar--pin"); + } + } + + private genItem(vditor: IVditor, menuItem: IMenuItem, index: number) { + let menuItemObj; + switch (menuItem.name) { + case "bold": + case "italic": + case "more": + case "strike": + case "line": + case "quote": + case "list": + case "ordered-list": + case "check": + case "code": + case "inline-code": + case "link": + case "table": + menuItemObj = new MenuItem(vditor, menuItem); + break; + case "emoji": + menuItemObj = new Emoji(vditor, menuItem); + break; + case "headings": + menuItemObj = new Headings(vditor, menuItem); + break; + case "|": + menuItemObj = new Divider(); + break; + case "br": + menuItemObj = new Br(); + break; + case "undo": + menuItemObj = new Undo(vditor, menuItem); + break; + case "redo": + menuItemObj = new Redo(vditor, menuItem); + break; + case "help": + menuItemObj = new Help(vditor, menuItem); + break; + case "both": + menuItemObj = new Both(vditor, menuItem); + break; + case "preview": + menuItemObj = new Preview(vditor, menuItem); + break; + case "fullscreen": + menuItemObj = new Fullscreen(vditor, menuItem); + break; + case "upload": + menuItemObj = new Upload(vditor, menuItem); + break; + case "record": + menuItemObj = new Record(vditor, menuItem); + break; + case "info": + menuItemObj = new Info(vditor, menuItem); + break; + case "edit-mode": + menuItemObj = new EditMode(vditor, menuItem); + break; + case "devtools": + menuItemObj = new Devtools(vditor, menuItem); + break; + case "outdent": + menuItemObj = new Outdent(vditor, menuItem); + break; + case "indent": + menuItemObj = new Indent(vditor, menuItem); + break; + case "outline": + menuItemObj = new Outline(vditor, menuItem); + break; + case "insert-after": + menuItemObj = new InsertAfter(vditor, menuItem); + break; + case "insert-before": + menuItemObj = new InsertBefore(vditor, menuItem); + break; + case "code-theme": + menuItemObj = new CodeTheme(vditor, menuItem); + break; + case "content-theme": + menuItemObj = new ContentTheme(vditor, menuItem); + break; + case "export": + menuItemObj = new Export(vditor, menuItem); + break; + default: + menuItemObj = new Custom(vditor, menuItem); + break; + } + + if (!menuItemObj) { + return; + } + let key = menuItem.name; + if (key === "br" || key === "|") { + key = key + index; + } + + this.elements[key] = menuItemObj.element; + return menuItemObj.element; + } +} diff --git a/web/include/vditor/src/ts/toolbar/setToolbar.ts b/web/include/vditor/src/ts/toolbar/setToolbar.ts new file mode 100644 index 0000000..bd2bb34 --- /dev/null +++ b/web/include/vditor/src/ts/toolbar/setToolbar.ts @@ -0,0 +1,120 @@ +import {Constants} from "../constants"; +import {getEventName} from "../util/compatibility"; + +export const removeCurrentToolbar = (toolbar: { [key: string]: HTMLElement }, names: string[]) => { + names.forEach((name) => { + if (!toolbar[name]) { + return; + } + const itemElement = toolbar[name].children[0]; + if (itemElement && itemElement.classList.contains("vditor-menu--current")) { + itemElement.classList.remove("vditor-menu--current"); + } + }); +}; + +export const setCurrentToolbar = (toolbar: { [key: string]: HTMLElement }, names: string[]) => { + names.forEach((name) => { + if (!toolbar[name]) { + return; + } + const itemElement = toolbar[name].children[0]; + if (itemElement && !itemElement.classList.contains("vditor-menu--current")) { + itemElement.classList.add("vditor-menu--current"); + } + }); +}; + +export const enableToolbar = (toolbar: { [key: string]: HTMLElement }, names: string[]) => { + names.forEach((name) => { + if (!toolbar[name]) { + return; + } + const itemElement = toolbar[name].children[0]; + if (itemElement && itemElement.classList.contains(Constants.CLASS_MENU_DISABLED)) { + itemElement.classList.remove(Constants.CLASS_MENU_DISABLED); + } + }); +}; + +export const disableToolbar = (toolbar: { [key: string]: HTMLElement }, names: string[]) => { + names.forEach((name) => { + if (!toolbar[name]) { + return; + } + const itemElement = toolbar[name].children[0]; + if (itemElement && !itemElement.classList.contains(Constants.CLASS_MENU_DISABLED)) { + itemElement.classList.add(Constants.CLASS_MENU_DISABLED); + } + }); +}; + +export const hideToolbar = (toolbar: { [key: string]: HTMLElement }, names: string[]) => { + names.forEach((name) => { + if (!toolbar[name]) { + return; + } + if (toolbar[name]) { + toolbar[name].style.display = "none"; + } + }); +}; + +export const showToolbar = (toolbar: { [key: string]: HTMLElement }, names: string[]) => { + names.forEach((name) => { + if (!toolbar[name]) { + return; + } + if (toolbar[name]) { + toolbar[name].style.display = "block"; + } + }); +}; + +// "subToolbar", "hint", "popover" +export const hidePanel = (vditor: IVditor, panels: string[], exceptElement?: HTMLElement) => { + if (panels.includes("subToolbar")) { + vditor.toolbar.element.querySelectorAll(".vditor-hint").forEach((item: HTMLElement) => { + if (exceptElement && item.isEqualNode(exceptElement)) { + return; + } + item.style.display = "none"; + }); + if (vditor.toolbar.elements.emoji) { + (vditor.toolbar.elements.emoji.lastElementChild as HTMLElement).style.display = "none"; + } + } + if (panels.includes("hint")) { + vditor.hint.element.style.display = "none"; + } + if (vditor.wysiwyg.popover && panels.includes("popover")) { + vditor.wysiwyg.popover.style.display = "none"; + } +}; + +export const toggleSubMenu = (vditor: IVditor, panelElement: HTMLElement, actionBtn: Element, level: number) => { + actionBtn.addEventListener(getEventName(), (event) => { + event.preventDefault(); + event.stopPropagation(); + if (actionBtn.classList.contains(Constants.CLASS_MENU_DISABLED)) { + return; + } + vditor.toolbar.element.querySelectorAll(".vditor-hint--current").forEach((item) => { + item.classList.remove("vditor-hint--current"); + }); + if (panelElement.style.display === "block") { + panelElement.style.display = "none"; + } else { + hidePanel(vditor, ["subToolbar", "hint", "popover"], actionBtn.parentElement.parentElement); + if (!actionBtn.classList.contains("vditor-tooltipped")) { + actionBtn.classList.add("vditor-hint--current"); + } + panelElement.style.display = "block"; + if (vditor.toolbar.element.getBoundingClientRect().right - actionBtn.getBoundingClientRect().right < 250) { + panelElement.classList.add("vditor-panel--left"); + } else { + panelElement.classList.remove("vditor-panel--left"); + } + } + }); +}; diff --git a/web/include/vditor/src/ts/ui/initUI.ts b/web/include/vditor/src/ts/ui/initUI.ts new file mode 100644 index 0000000..5549777 --- /dev/null +++ b/web/include/vditor/src/ts/ui/initUI.ts @@ -0,0 +1,180 @@ +import {Constants} from "../constants"; +import {setEditMode} from "../toolbar/EditMode"; +import {hidePanel} from "../toolbar/setToolbar"; +import {accessLocalStorage} from "../util/compatibility"; +import {setContentTheme} from "./setContentTheme"; +import {setTheme} from "./setTheme"; + +export const initUI = (vditor: IVditor) => { + vditor.element.innerHTML = ""; + vditor.element.classList.add("vditor"); + // 支持 RTL + if (vditor.options.rtl) { + vditor.element.setAttribute("dir", "rtl") + } + setTheme(vditor); + setContentTheme(vditor.options.preview.theme.current, vditor.options.preview.theme.path); + if (typeof vditor.options.height === "number") { + vditor.element.style.height = vditor.options.height + "px"; + } else { + vditor.element.style.height = vditor.options.height; + } + if (typeof vditor.options.minHeight === "number") { + vditor.element.style.minHeight = vditor.options.minHeight + "px"; + } + if (typeof vditor.options.width === "number") { + vditor.element.style.width = vditor.options.width + "px"; + } else { + vditor.element.style.width = vditor.options.width; + } + + vditor.element.appendChild(vditor.toolbar.element); + + const contentElement = document.createElement("div"); + contentElement.className = "vditor-content"; + + if (vditor.options.outline.position === "left") { + contentElement.appendChild(vditor.outline.element); + } + + contentElement.appendChild(vditor.wysiwyg.element.parentElement); + + contentElement.appendChild(vditor.sv.element); + + contentElement.appendChild(vditor.ir.element.parentElement); + + contentElement.appendChild(vditor.preview.element); + + if (vditor.toolbar.elements.devtools) { + contentElement.appendChild(vditor.devtools.element); + } + + if (vditor.options.outline.position === "right") { + vditor.outline.element.classList.add("vditor-outline--right"); + contentElement.appendChild(vditor.outline.element); + } + + if (vditor.upload) { + contentElement.appendChild(vditor.upload.element); + } + + if (vditor.options.resize.enable) { + contentElement.appendChild(vditor.resize.element); + } + + contentElement.appendChild(vditor.hint.element); + + contentElement.appendChild(vditor.tip.element); + + vditor.element.appendChild(contentElement); + + contentElement.addEventListener("click", () => { + hidePanel(vditor, ["subToolbar"]); + }); + + if (vditor.toolbar.elements.export) { + // for export pdf + vditor.element.insertAdjacentHTML("beforeend", + ''); + } + + setEditMode(vditor, vditor.options.mode, afterRender(vditor)); + + document.execCommand("DefaultParagraphSeparator", false, "p"); + + if (navigator.userAgent.indexOf("iPhone") > -1 && typeof window.visualViewport !== "undefined") { + // https://github.com/Vanessa219/vditor/issues/379 + let pendingUpdate = false; + const viewportHandler = (event: Event) => { + if (pendingUpdate) { + return; + } + pendingUpdate = true; + + requestAnimationFrame(() => { + pendingUpdate = false; + const layoutViewport = vditor.toolbar.element; + layoutViewport.style.transform = "none"; + if (layoutViewport.getBoundingClientRect().top < 0) { + layoutViewport.style.transform = `translate(0, ${-layoutViewport.getBoundingClientRect().top}px)`; + } + }); + }; + window.visualViewport.addEventListener("scroll", viewportHandler); + window.visualViewport.addEventListener("resize", viewportHandler); + } +}; + +export const setPadding = (vditor: IVditor) => { + const minPadding = window.innerWidth <= Constants.MOBILE_WIDTH ? 10 : 35; + if (vditor.wysiwyg.element.parentElement.style.display !== "none") { + const padding = (vditor.wysiwyg.element.parentElement.clientWidth + - vditor.options.preview.maxWidth) / 2; + vditor.wysiwyg.element.style.padding = `10px ${Math.max(minPadding, padding)}px`; + } + + if (vditor.ir.element.parentElement.style.display !== "none") { + const padding = (vditor.ir.element.parentElement.clientWidth + - vditor.options.preview.maxWidth) / 2; + vditor.ir.element.style.padding = `10px ${Math.max(minPadding, padding)}px`; + } + + if (vditor.preview.element.style.display !== "block") { + vditor.toolbar.element.style.paddingLeft = Math.max(5, + parseInt(vditor[vditor.currentMode].element.style.paddingLeft || "0", 10) + + (vditor.options.outline.position === "left" ? vditor.outline.element.offsetWidth : 0)) + "px"; + } else { + vditor.toolbar.element.style.paddingLeft = (5 + + (vditor.options.outline.position === "left" ? vditor.outline.element.offsetWidth : 0)) + "px"; + } +}; + +export const setTypewriterPosition = (vditor: IVditor) => { + if (!vditor.options.typewriterMode) { + return; + } + let height: number = window.innerHeight; + if (typeof vditor.options.height === "number") { + height = vditor.options.height; + if (typeof vditor.options.minHeight === "number") { + height = Math.max(height, vditor.options.minHeight); + } + height = Math.min(window.innerHeight, height); + } else { + height = vditor.element.clientHeight; + } + if (vditor.element.classList.contains("vditor--fullscreen")) { + height = window.innerHeight; + } + // 由于 Firefox padding-bottom bug,只能使用 :after + vditor[vditor.currentMode].element.style.setProperty("--editor-bottom", + ((height - vditor.toolbar.element.offsetHeight) / 2) + "px"); +}; + +let resizeCb: () => void; + +export function UIUnbindListener() { + window.removeEventListener("resize", resizeCb); +} + +const afterRender = (vditor: IVditor) => { + setTypewriterPosition(vditor); + UIUnbindListener(); + window.addEventListener("resize", resizeCb = () => { + setPadding(vditor); + setTypewriterPosition(vditor); + }); + + // set default value + let initValue = accessLocalStorage() && localStorage.getItem(vditor.options.cache.id); + if (!vditor.options.cache.enable || !initValue) { + if (vditor.options.value) { + initValue = vditor.options.value; + } else if (vditor.originalInnerHTML) { + initValue = vditor.lute.HTML2Md(vditor.originalInnerHTML); + } else if (!vditor.options.cache.enable) { + initValue = ""; + } + } + return initValue || ""; +}; diff --git a/web/include/vditor/src/ts/ui/setCodeTheme.ts b/web/include/vditor/src/ts/ui/setCodeTheme.ts new file mode 100644 index 0000000..1f920e7 --- /dev/null +++ b/web/include/vditor/src/ts/ui/setCodeTheme.ts @@ -0,0 +1,16 @@ +import {Constants} from "../constants"; +import {addStyle} from "../util/addStyle"; + +export const setCodeTheme = (codeTheme: string, cdn = Constants.CDN) => { + if (!Constants.CODE_THEME.includes(codeTheme)) { + codeTheme = "github"; + } + const vditorHljsStyle = document.getElementById("vditorHljsStyle") as HTMLLinkElement; + const href = `${cdn}/dist/js/highlight.js/styles/${codeTheme}.css`; + if (!vditorHljsStyle) { + addStyle(href, "vditorHljsStyle"); + } else if (vditorHljsStyle.getAttribute('href') !== href) { + vditorHljsStyle.remove(); + addStyle(href, "vditorHljsStyle"); + } +}; diff --git a/web/include/vditor/src/ts/ui/setContentTheme.ts b/web/include/vditor/src/ts/ui/setContentTheme.ts new file mode 100644 index 0000000..dbe9913 --- /dev/null +++ b/web/include/vditor/src/ts/ui/setContentTheme.ts @@ -0,0 +1,15 @@ +import {addStyle} from "../util/addStyle"; + +export const setContentTheme = (contentTheme: string, path: string) => { + if (!contentTheme || !path) { + return; + } + const vditorContentTheme = document.getElementById("vditorContentTheme") as HTMLLinkElement; + const cssPath = `${path}/${contentTheme}.css`; + if (!vditorContentTheme) { + addStyle(cssPath, "vditorContentTheme"); + } else if (vditorContentTheme.getAttribute("href") !== cssPath) { + vditorContentTheme.remove(); + addStyle(cssPath, "vditorContentTheme"); + } +}; diff --git a/web/include/vditor/src/ts/ui/setPreviewMode.ts b/web/include/vditor/src/ts/ui/setPreviewMode.ts new file mode 100644 index 0000000..d7af882 --- /dev/null +++ b/web/include/vditor/src/ts/ui/setPreviewMode.ts @@ -0,0 +1,31 @@ +import {removeCurrentToolbar} from "../toolbar/setToolbar"; +import {setCurrentToolbar} from "../toolbar/setToolbar"; + +export const setPreviewMode = (mode: "both" | "editor", vditor: IVditor) => { + if (vditor.options.preview.mode === mode) { + return; + } + vditor.options.preview.mode = mode; + + switch (mode) { + case "both": + vditor.sv.element.style.display = "block"; + vditor.preview.element.style.display = "block"; + vditor.preview.render(vditor); + + setCurrentToolbar(vditor.toolbar.elements, ["both"]); + break; + case "editor": + vditor.sv.element.style.display = "block"; + vditor.preview.element.style.display = "none"; + + removeCurrentToolbar(vditor.toolbar.elements, ["both"]); + break; + default: + break; + } + + if (vditor.devtools) { + vditor.devtools.renderEchart(vditor); + } +}; diff --git a/web/include/vditor/src/ts/ui/setTheme.ts b/web/include/vditor/src/ts/ui/setTheme.ts new file mode 100644 index 0000000..8547e6c --- /dev/null +++ b/web/include/vditor/src/ts/ui/setTheme.ts @@ -0,0 +1,7 @@ +export const setTheme = (vditor: IVditor) => { + if (vditor.options.theme === "dark") { + vditor.element.classList.add("vditor--dark"); + } else { + vditor.element.classList.remove("vditor--dark"); + } +}; diff --git a/web/include/vditor/src/ts/undo/index.ts b/web/include/vditor/src/ts/undo/index.ts new file mode 100644 index 0000000..b2a7d0a --- /dev/null +++ b/web/include/vditor/src/ts/undo/index.ts @@ -0,0 +1,260 @@ +import * as DiffMatchPatch from "diff-match-patch"; +import {disableToolbar, enableToolbar, hidePanel} from "../toolbar/setToolbar"; +import {isFirefox, isSafari} from "../util/compatibility"; +import {scrollCenter} from "../util/editorCommonEvent"; +import {execAfterRender} from "../util/fixBrowserBehavior"; +import {highlightToolbar} from "../util/highlightToolbar"; +import {processCodeRender} from "../util/processCode"; +import {setRangeByWbr, setSelectionFocus} from "../util/selection"; +import {renderToc} from "../util/toc"; + +interface IUndo { + hasUndo: boolean; + lastText: string; + redoStack: DiffMatchPatch.patch_obj[][]; + undoStack: DiffMatchPatch.patch_obj[][]; +} + +class Undo { + private stackSize = 50; + private dmp: DiffMatchPatch.diff_match_patch; + private wysiwyg: IUndo; + private ir: IUndo; + private sv: IUndo; + + constructor() { + this.resetStack(); + // @ts-ignore + this.dmp = new DiffMatchPatch(); + } + + public clearStack(vditor: IVditor) { + this.resetStack(); + this.resetIcon(vditor); + } + + public resetIcon(vditor: IVditor) { + if (!vditor.toolbar) { + return; + } + + if (this[vditor.currentMode].undoStack.length > 1) { + enableToolbar(vditor.toolbar.elements, ["undo"]); + } else { + disableToolbar(vditor.toolbar.elements, ["undo"]); + } + + if (this[vditor.currentMode].redoStack.length !== 0) { + enableToolbar(vditor.toolbar.elements, ["redo"]); + } else { + disableToolbar(vditor.toolbar.elements, ["redo"]); + } + } + + public undo(vditor: IVditor) { + if (vditor[vditor.currentMode].element.getAttribute("contenteditable") === "false") { + return; + } + if (this[vditor.currentMode].undoStack.length < 2) { + return; + } + const state = this[vditor.currentMode].undoStack.pop(); + if (!state) { + return; + } + this[vditor.currentMode].redoStack.push(state); + this.renderDiff(state, vditor); + this[vditor.currentMode].hasUndo = true; + // undo 操作后,需要关闭 hint + hidePanel(vditor, ["hint"]); + } + + public redo(vditor: IVditor) { + if (vditor[vditor.currentMode].element.getAttribute("contenteditable") === "false") { + return; + } + const state = this[vditor.currentMode].redoStack.pop(); + if (!state) { + return; + } + this[vditor.currentMode].undoStack.push(state); + this.renderDiff(state, vditor, true); + } + + public recordFirstPosition(vditor: IVditor, event: KeyboardEvent) { + if (getSelection().rangeCount === 0) { + return; + } + if (this[vditor.currentMode].undoStack.length !== 1 || this[vditor.currentMode].undoStack[0].length === 0 || + this[vditor.currentMode].redoStack.length > 0) { + return; + } + if (isFirefox() && event.key === "Backspace") { + // Firefox 第一次删除无效 + return; + } + if (isSafari()) { + // Safari keydown 在 input 之后,不需要重复记录历史 + return; + } + const text = this.addCaret(vditor); + if (text.replace("", "").replace(" vditor-ir__node--expand", "") + !== this[vditor.currentMode].undoStack[0][0].diffs[0][1].replace("", "")) { + // 当还不没有存入 undo 栈时,按下 ctrl 后会覆盖 lastText + return; + } + this[vditor.currentMode].undoStack[0][0].diffs[0][1] = text; + this[vditor.currentMode].lastText = text; + // 不能添加 setSelectionFocus(cloneRange); 否则 windows chrome 首次输入会烂 + } + + public addToUndoStack(vditor: IVditor) { + // afterRenderEvent.ts 已经 debounce + const text = this.addCaret(vditor, true); + const diff = this.dmp.diff_main(text, this[vditor.currentMode].lastText, true); + const patchList = this.dmp.patch_make(text, this[vditor.currentMode].lastText, diff); + if (patchList.length === 0 && this[vditor.currentMode].undoStack.length > 0) { + return; + } + this[vditor.currentMode].lastText = text; + this[vditor.currentMode].undoStack.push(patchList); + if (this[vditor.currentMode].undoStack.length > this.stackSize) { + this[vditor.currentMode].undoStack.shift(); + } + if (this[vditor.currentMode].hasUndo) { + this[vditor.currentMode].redoStack = []; + this[vditor.currentMode].hasUndo = false; + disableToolbar(vditor.toolbar.elements, ["redo"]); + } + + if (this[vditor.currentMode].undoStack.length > 1) { + enableToolbar(vditor.toolbar.elements, ["undo"]); + } + } + + private renderDiff(state: DiffMatchPatch.patch_obj[], vditor: IVditor, isRedo: boolean = false) { + let text; + if (isRedo) { + const redoPatchList = this.dmp.patch_deepCopy(state).reverse(); + redoPatchList.forEach((patch) => { + patch.diffs.forEach((diff) => { + diff[0] = -diff[0]; + }); + }); + text = this.dmp.patch_apply(redoPatchList, this[vditor.currentMode].lastText)[0]; + } else { + text = this.dmp.patch_apply(state, this[vditor.currentMode].lastText)[0]; + } + + this[vditor.currentMode].lastText = text; + vditor[vditor.currentMode].element.innerHTML = text; + if (vditor.currentMode !== "sv") { + vditor[vditor.currentMode].element.querySelectorAll(`.vditor-${vditor.currentMode}__preview[data-render='2']`) + .forEach((blockElement: HTMLElement) => { + processCodeRender(blockElement, vditor); + }); + } + + if (!vditor[vditor.currentMode].element.querySelector("wbr")) { + // Safari 第一次输入没有光标,需手动定位到结尾 + const range = getSelection().getRangeAt(0); + range.setEndBefore(vditor[vditor.currentMode].element); + range.collapse(false); + } else { + setRangeByWbr( + vditor[vditor.currentMode].element, vditor[vditor.currentMode].element.ownerDocument.createRange()); + scrollCenter(vditor); + } + + renderToc(vditor); + + execAfterRender(vditor, { + enableAddUndoStack: false, + enableHint: false, + enableInput: true, + }); + highlightToolbar(vditor); + + vditor[vditor.currentMode].element.querySelectorAll(`.vditor-${vditor.currentMode}__preview[data-render='2']`) + .forEach((item: HTMLElement) => { + processCodeRender(item, vditor); + }); + + if (this[vditor.currentMode].undoStack.length > 1) { + enableToolbar(vditor.toolbar.elements, ["undo"]); + } else { + disableToolbar(vditor.toolbar.elements, ["undo"]); + } + + if (this[vditor.currentMode].redoStack.length !== 0) { + enableToolbar(vditor.toolbar.elements, ["redo"]); + } else { + disableToolbar(vditor.toolbar.elements, ["redo"]); + } + } + + private resetStack() { + this.ir = { + hasUndo: false, + lastText: "", + redoStack: [], + undoStack: [], + }; + this.sv = { + hasUndo: false, + lastText: "", + redoStack: [], + undoStack: [], + }; + this.wysiwyg = { + hasUndo: false, + lastText: "", + redoStack: [], + undoStack: [], + }; + } + + private addCaret(vditor: IVditor, setFocus = false) { + let cloneRange: Range; + if (getSelection().rangeCount !== 0 && !vditor[vditor.currentMode].element.querySelector("wbr")) { + const range = getSelection().getRangeAt(0); + if (vditor[vditor.currentMode].element.contains(range.startContainer)) { + cloneRange = range.cloneRange(); + const wbrElement = document.createElement("span"); + wbrElement.className = "vditor-wbr"; + range.insertNode(wbrElement); + } + } + // 移除数学公式、echart 渲染 https://github.com/siyuan-note/siyuan/issues/537 + const cloneElement = vditor.ir.element.cloneNode(true) as HTMLElement; + cloneElement.querySelectorAll(`.vditor-${vditor.currentMode}__preview[data-render='1']`) + .forEach((item: HTMLElement) => { + if (!item.firstElementChild) { + return + } + if (item.firstElementChild.classList.contains("language-echarts") || + item.firstElementChild.classList.contains("language-plantuml") || + item.firstElementChild.classList.contains("language-mindmap")) { + item.firstElementChild.removeAttribute("_echarts_instance_"); + item.firstElementChild.removeAttribute("data-processed"); + item.firstElementChild.innerHTML = item.previousElementSibling.firstElementChild.innerHTML; + item.setAttribute("data-render", "2"); + } else if (item.firstElementChild.classList.contains("language-math")) { + item.setAttribute("data-render", "2"); + item.firstElementChild.textContent = item.firstElementChild.getAttribute("data-math"); + item.firstElementChild.removeAttribute("data-math"); + } + }); + const text = vditor[vditor.currentMode].element.innerHTML; + vditor[vditor.currentMode].element.querySelectorAll(".vditor-wbr").forEach((item) => { + item.remove(); + // 使用 item.outerHTML = "" 会产生 https://github.com/Vanessa219/vditor/pull/686; + }); + if (setFocus && cloneRange) { + setSelectionFocus(cloneRange); + } + return text.replace('', ""); + } +} + +export {Undo}; diff --git a/web/include/vditor/src/ts/upload/getElement.ts b/web/include/vditor/src/ts/upload/getElement.ts new file mode 100644 index 0000000..eee4224 --- /dev/null +++ b/web/include/vditor/src/ts/upload/getElement.ts @@ -0,0 +1,10 @@ +export const getElement = (vditor: IVditor) => { + switch (vditor.currentMode) { + case "ir": + return vditor.ir.element; + case "wysiwyg": + return vditor.wysiwyg.element; + case "sv": + return vditor.sv.element; + } +}; diff --git a/web/include/vditor/src/ts/upload/index.ts b/web/include/vditor/src/ts/upload/index.ts new file mode 100644 index 0000000..28b862d --- /dev/null +++ b/web/include/vditor/src/ts/upload/index.ts @@ -0,0 +1,260 @@ +import {getEditorRange, setSelectionFocus} from "../util/selection"; +import {getElement} from "./getElement"; +import {setHeaders} from "./setHeaders"; + +class Upload { + public element: HTMLElement; + public isUploading: boolean; + public range: Range; + + constructor() { + this.isUploading = false; + this.element = document.createElement("div"); + this.element.className = "vditor-upload"; + } +} + +const validateFile = (vditor: IVditor, files: File[]) => { + vditor.tip.hide(); + const uploadFileList = []; + let errorTip = ""; + let uploadingStr = ""; + const lang: keyof II18n | "" = vditor.options.lang; + const options: IOptions = vditor.options; + + for (let iMax = files.length, i = 0; i < iMax; i++) { + const file = files[i]; + let validate = true; + + if (!file.name) { + errorTip += `
  • ${window.VditorI18n.nameEmpty}
  • `; + validate = false; + } + + if (file.size > vditor.options.upload.max) { + errorTip += `
  • ${file.name} ${window.VditorI18n.over} ${vditor.options.upload.max / 1024 / 1024}M
  • `; + validate = false; + } + + const lastIndex = file.name.lastIndexOf("."); + const fileExt = file.name.substr(lastIndex); + const filename = vditor.options.upload.filename(file.name.substr(0, lastIndex)) + fileExt; + + if (vditor.options.upload.accept) { + const isAccept = vditor.options.upload.accept.split(",").some((item) => { + const type = item.trim(); + if (type.indexOf(".") === 0) { + if (fileExt.toLowerCase() === type.toLowerCase()) { + return true; + } + } else { + if (file.type.split("/")[0] === type.split("/")[0]) { + return true; + } + } + return false; + }); + + if (!isAccept) { + errorTip += `
  • ${file.name} ${window.VditorI18n.fileTypeError}
  • `; + validate = false; + } + } + + if (validate) { + uploadFileList.push(file); + uploadingStr += `
  • ${filename} ${window.VditorI18n.uploading}
  • `; + } + } + + vditor.tip.show(`
      ${errorTip}${uploadingStr}
    `); + + return uploadFileList; +}; + +const genUploadedLabel = (responseText: string, vditor: IVditor) => { + const editorElement = getElement(vditor); + editorElement.focus(); + const response = JSON.parse(responseText); + let errorTip = ""; + + if (response.code === 1) { + errorTip = `${response.msg}`; + } + + if (response.data.errFiles && response.data.errFiles.length > 0) { + errorTip = `
    • ${errorTip}
    • `; + response.data.errFiles.forEach((data: string) => { + const lastIndex = data.lastIndexOf("."); + const filename = vditor.options.upload.filename(data.substr(0, lastIndex)) + data.substr(lastIndex); + errorTip += `
    • ${filename} ${window.VditorI18n.uploadError}
    • `; + }); + errorTip += "
    "; + } + + if (errorTip) { + vditor.tip.show(errorTip); + } else { + vditor.tip.hide(); + } + + let succFileText = ""; + Object.keys(response.data.succMap).forEach((key) => { + const path = response.data.succMap[key]; + const lastIndex = key.lastIndexOf("."); + let type = key.substr(lastIndex); + const filename = vditor.options.upload.filename(key.substr(0, lastIndex)) + type; + type = type.toLowerCase(); + if (type.indexOf(".wav") === 0 || type.indexOf(".mp3") === 0 || type.indexOf(".ogg") === 0) { + if (vditor.currentMode === "wysiwyg") { + succFileText += `
    <audio controls="controls" src="${path}"></audio>
    \n`; + } else if (vditor.currentMode === "ir") { + succFileText += `\n`; + } else { + succFileText += `[${filename}](${path})\n`; + } + } else if (type.indexOf(".apng") === 0 + || type.indexOf(".bmp") === 0 + || type.indexOf(".gif") === 0 + || type.indexOf(".ico") === 0 || type.indexOf(".cur") === 0 + || type.indexOf(".jpg") === 0 || type.indexOf(".jpeg") === 0 || type.indexOf(".jfif") === 0 || type.indexOf(".pjp") === 0 || type.indexOf(".pjpeg") === 0 + || type.indexOf(".png") === 0 + || type.indexOf(".svg") === 0 + || type.indexOf(".webp") === 0) { + if (vditor.currentMode === "wysiwyg") { + succFileText += `${filename}\n`; + } else { + succFileText += `![${filename}](${path})\n`; + } + } else { + if (vditor.currentMode === "wysiwyg") { + succFileText += `${filename}\n`; + } else { + succFileText += `[${filename}](${path})\n`; + } + } + }); + setSelectionFocus(vditor.upload.range); + document.execCommand("insertHTML", false, succFileText); + vditor.upload.range = getSelection().getRangeAt(0).cloneRange(); +}; + +const uploadFiles = + async (vditor: IVditor, files: FileList | DataTransferItemList | File[], element?: HTMLInputElement) => { + // FileList | DataTransferItemList | File[] => File[] + let fileList = []; + const filesMax = vditor.options.upload.multiple === true ? files.length : 1; + for (let i = 0; i < filesMax; i++) { + let fileItem = files[i]; + if (fileItem instanceof DataTransferItem) { + fileItem = fileItem.getAsFile(); + } + fileList.push(fileItem); + } + + if (vditor.options.upload.handler) { + const isValidate = await vditor.options.upload.handler(fileList); + if (element) { + element.value = ""; + } + if (typeof isValidate === "string") { + vditor.tip.show(isValidate); + return; + } + return; + } + + if (!vditor.options.upload.url || !vditor.upload) { + if (element) { + element.value = ""; + } + vditor.tip.show("please config: options.upload.url"); + return; + } + + if (vditor.options.upload.file) { + fileList = await vditor.options.upload.file(fileList); + } + + if (vditor.options.upload.validate) { + const isValidate = vditor.options.upload.validate(fileList); + if (typeof isValidate === "string") { + vditor.tip.show(isValidate); + return; + } + } + const editorElement = getElement(vditor); + + vditor.upload.range = getEditorRange(vditor); + + const validateResult = validateFile(vditor, fileList); + if (validateResult.length === 0) { + if (element) { + element.value = ""; + } + return; + } + + const formData = new FormData(); + + const extraData = vditor.options.upload.extraData; + for (const key of Object.keys(extraData)) { + formData.append(key, extraData[key]); + } + + for (let i = 0, iMax = validateResult.length; i < iMax; i++) { + formData.append(vditor.options.upload.fieldName, validateResult[i]); + } + + const xhr = new XMLHttpRequest(); + xhr.open("POST", vditor.options.upload.url); + if (vditor.options.upload.token) { + xhr.setRequestHeader("X-Upload-Token", vditor.options.upload.token); + } + if (vditor.options.upload.withCredentials) { + xhr.withCredentials = true; + } + setHeaders(vditor, xhr); + vditor.upload.isUploading = true; + editorElement.setAttribute("contenteditable", "false"); + xhr.onreadystatechange = () => { + if (xhr.readyState === XMLHttpRequest.DONE) { + vditor.upload.isUploading = false; + editorElement.setAttribute("contenteditable", "true"); + if (xhr.status >= 200 && xhr.status < 300) { + if (vditor.options.upload.success) { + vditor.options.upload.success(editorElement, xhr.responseText); + } else { + let responseText = xhr.responseText; + if (vditor.options.upload.format) { + responseText = vditor.options.upload.format(files as File [], xhr.responseText); + } + genUploadedLabel(responseText, vditor); + } + } else { + if (vditor.options.upload.error) { + vditor.options.upload.error(xhr.responseText); + } else { + vditor.tip.show(xhr.responseText); + } + } + if (element) { + element.value = ""; + } + vditor.upload.element.style.display = "none"; + } + }; + xhr.upload.onprogress = (event: ProgressEvent) => { + if (!event.lengthComputable) { + return; + } + const progress = event.loaded / event.total * 100; + vditor.upload.element.style.display = "block"; + const progressBar = vditor.upload.element; + progressBar.style.width = progress + "%"; + }; + xhr.send(formData); + }; + +export {Upload, uploadFiles}; diff --git a/web/include/vditor/src/ts/upload/setHeaders.ts b/web/include/vditor/src/ts/upload/setHeaders.ts new file mode 100644 index 0000000..662a2e7 --- /dev/null +++ b/web/include/vditor/src/ts/upload/setHeaders.ts @@ -0,0 +1,10 @@ +export const setHeaders = (vditor: IVditor, xhr: XMLHttpRequest) => { + if (vditor.options.upload.setHeaders) { + vditor.options.upload.headers = vditor.options.upload.setHeaders(); + } + if (vditor.options.upload.headers) { + Object.keys(vditor.options.upload.headers).forEach((key) => { + xhr.setRequestHeader(key, vditor.options.upload.headers[key]); + }); + } +}; diff --git a/web/include/vditor/src/ts/util/Options.ts b/web/include/vditor/src/ts/util/Options.ts new file mode 100644 index 0000000..ae25751 --- /dev/null +++ b/web/include/vditor/src/ts/util/Options.ts @@ -0,0 +1,454 @@ +import {Constants} from "../constants"; +import {merge} from "./merge"; + +export class Options { + public options: IOptions; + private defaultOptions: IOptions = { + rtl: false, + after: undefined, + cache: { + enable: true, + }, + cdn: Constants.CDN, + classes: { + preview: "", + }, + comment: { + enable: false, + }, + counter: { + enable: false, + type: "markdown", + }, + customRenders: [], + debugger: false, + fullscreen: { + index: 90, + }, + height: "auto", + hint: { + delay: 200, + emoji: { + "+1": "👍", + "-1": "👎", + "confused": "😕", + "eyes": "👀️", + "heart": "❤️", + "rocket": "🚀️", + "smile": "😄", + "tada": "🎉️", + }, + emojiPath: `${Constants.CDN}/dist/images/emoji`, + extend: [], + parse: true, + }, + icon: "ant", + lang: "zh_CN", + mode: "ir", + outline: { + enable: false, + position: "left", + }, + placeholder: "", + preview: { + actions: ["desktop", "tablet", "mobile", "mp-wechat", "zhihu"], + delay: 1000, + hljs: Constants.HLJS_OPTIONS, + markdown: Constants.MARKDOWN_OPTIONS, + math: Constants.MATH_OPTIONS, + maxWidth: 800, + mode: "both", + theme: Constants.THEME_OPTIONS, + render: { + media: { + enable: true, + } + } + }, + link: { + isOpen: true, + }, + image: { + isPreview: true, + }, + resize: { + enable: false, + position: "bottom", + }, + theme: "classic", + toolbar: [ + "emoji", + "headings", + "bold", + "italic", + "strike", + "link", + "|", + "list", + "ordered-list", + "check", + "outdent", + "indent", + "|", + "quote", + "line", + "code", + "inline-code", + "insert-before", + "insert-after", + "|", + "upload", + "record", + "table", + "|", + "undo", + "redo", + "|", + "fullscreen", + "edit-mode", + { + name: "more", + toolbar: [ + "both", + "code-theme", + "content-theme", + "export", + "outline", + "preview", + "devtools", + "info", + "help", + ], + }, + ], + toolbarConfig: { + hide: false, + pin: false, + }, + typewriterMode: false, + undoDelay: 800, + upload: { + extraData: {}, + fieldName: "file[]", + filename: (name: string) => name.replace(/\W/g, ""), + linkToImgUrl: "", + max: 10 * 1024 * 1024, + multiple: true, + url: "", + withCredentials: false, + }, + value: "", + width: "auto", + }; + + constructor(options: IOptions) { + this.options = options; + } + + public merge(): IOptions { + if (this.options) { + if (this.options.toolbar) { + this.options.toolbar = this.mergeToolbar(this.options.toolbar); + } else { + this.options.toolbar = this.mergeToolbar(this.defaultOptions.toolbar); + } + if (this.options.preview?.theme?.list) { + this.defaultOptions.preview.theme.list = this.options.preview.theme.list; + } + if (this.options.preview?.render?.media?.enable) { + this.defaultOptions.preview.render.media.enable = this.options.preview.render.media.enable; + } + if (this.options.hint?.emoji) { + this.defaultOptions.hint.emoji = this.options.hint.emoji; + } + if (this.options.comment) { + this.defaultOptions.comment = this.options.comment; + } + + if (this.options.cdn) { + if (!this.options.preview?.theme?.path) { + this.defaultOptions.preview.theme.path = `${this.options.cdn}/dist/css/content-theme` + } + if (!this.options.hint?.emojiPath) { + this.defaultOptions.hint.emojiPath = `${this.options.cdn}/dist/images/emoji`; + } + } + } + + const mergedOptions = merge(this.defaultOptions, this.options); + + if (mergedOptions.cache.enable && !mergedOptions.cache.id) { + throw new Error( + "need options.cache.id, see https://ld246.com/article/1549638745630#options", + ); + } + + return mergedOptions; + } + + private mergeToolbar(toolbar: Array) { + const toolbarItem = [ + { + icon: '', + name: "export", + tipPosition: "ne", + }, + { + hotkey: "⌘E", + icon: '', + name: "emoji", + tipPosition: "ne", + }, + { + hotkey: "⌘H", + icon: + '', + name: "headings", + tipPosition: "ne", + }, + { + hotkey: "⌘B", + icon: '', + name: "bold", + prefix: "**", + suffix: "**", + tipPosition: "ne", + }, + { + hotkey: "⌘I", + icon: '', + name: "italic", + prefix: "*", + suffix: "*", + tipPosition: "ne", + }, + { + hotkey: "⌘D", + icon: '', + name: "strike", + prefix: "~~", + suffix: "~~", + tipPosition: "ne", + }, + { + hotkey: "⌘K", + icon: '', + name: "link", + prefix: "[", + suffix: "](https://)", + tipPosition: "n", + }, + { + name: "|", + }, + { + hotkey: "⌘L", + icon: '', + name: "list", + prefix: "* ", + tipPosition: "n", + }, + { + hotkey: "⌘O", + icon: + '', + name: "ordered-list", + prefix: "1. ", + tipPosition: "n", + }, + { + hotkey: "⌘J", + icon: '', + name: "check", + prefix: "* [ ] ", + tipPosition: "n", + }, + { + hotkey: "⇧⌘I", + icon: + '', + name: "outdent", + tipPosition: "n", + }, + { + hotkey: "⇧⌘O", + icon: '', + name: "indent", + tipPosition: "n", + }, + { + name: "|", + }, + { + hotkey: "⌘;", + icon: '', + name: "quote", + prefix: "> ", + tipPosition: "n", + }, + { + hotkey: "⇧⌘H", + icon: '', + name: "line", + prefix: "---", + tipPosition: "n", + }, + { + hotkey: "⌘U", + icon: '', + name: "code", + prefix: "```", + suffix: "\n```", + tipPosition: "n", + }, + { + hotkey: "⌘G", + icon: + '', + name: "inline-code", + prefix: "`", + suffix: "`", + tipPosition: "n", + }, + { + hotkey: "⇧⌘B", + icon: '', + name: "insert-before", + tipPosition: "n", + }, + { + hotkey: "⇧⌘E", + icon: '', + name: "insert-after", + tipPosition: "n", + }, + { + name: "|", + }, + { + icon: '', + name: "upload", + tipPosition: "n", + }, + { + icon: '', + name: "record", + tipPosition: "n", + }, + { + hotkey: "⌘M", + icon: '', + name: "table", + prefix: "| col1", + suffix: + " | col2 | col3 |\n| --- | --- | --- |\n| | | |\n| | | |", + tipPosition: "n", + }, + { + name: "|", + }, + { + hotkey: "⌘Z", + icon: '', + name: "undo", + tipPosition: "nw", + }, + { + hotkey: "⌘Y", + icon: '', + name: "redo", + tipPosition: "nw", + }, + { + name: "|", + }, + { + icon: '', + name: "more", + tipPosition: "e", + }, + { + hotkey: "⌘'", + icon: + '', + name: "fullscreen", + tipPosition: "nw", + }, + { + icon: '', + name: "edit-mode", + tipPosition: "nw", + }, + { + hotkey: "⌘P", + icon: '', + name: "both", + tipPosition: "nw", + }, + { + icon: + '', + name: "preview", + tipPosition: "nw", + }, + { + icon: + '', + name: "outline", + tipPosition: "nw", + }, + { + icon: '', + name: "content-theme", + tipPosition: "nw", + }, + { + icon: + '', + name: "code-theme", + tipPosition: "nw", + }, + { + icon: '', + name: "devtools", + tipPosition: "nw", + }, + { + icon: '', + name: "info", + tipPosition: "nw", + }, + { + icon: '', + name: "help", + tipPosition: "nw", + }, + { + name: "br", + }, + ]; + const toolbarResult: IMenuItem[] = []; + toolbar.forEach((menuItem: IMenuItem) => { + let currentMenuItem = menuItem; + toolbarItem.forEach((defaultMenuItem: IMenuItem) => { + if ( + typeof menuItem === "string" && + defaultMenuItem.name === menuItem + ) { + currentMenuItem = defaultMenuItem; + } + if ( + typeof menuItem === "object" && + defaultMenuItem.name === menuItem.name + ) { + currentMenuItem = Object.assign({}, defaultMenuItem, menuItem); + } + }); + if (menuItem.toolbar) { + currentMenuItem.toolbar = this.mergeToolbar(menuItem.toolbar); + } + toolbarResult.push(currentMenuItem); + }); + return toolbarResult; + } +} diff --git a/web/include/vditor/src/ts/util/RecordMedia.ts b/web/include/vditor/src/ts/util/RecordMedia.ts new file mode 100644 index 0000000..828b409 --- /dev/null +++ b/web/include/vditor/src/ts/util/RecordMedia.ts @@ -0,0 +1,170 @@ +export class RecordMedia { + public SAMPLE_RATE = 5000; // 44100 suggested by demos; + public DEFAULT_SAMPLE_RATE: number; + public isRecording = false; + public readyFlag = false; + public leftChannel: Float32List[] = []; + public rightChannel: Float32List[] = []; + public recordingLength = 0; + // This needs to be public so the 'onaudioprocess' event handler can be defined externally. + public recorder: ScriptProcessorNode; + + constructor(e: MediaStream) { + let context; + // creates the audio context + if (typeof AudioContext !== "undefined") { + context = new AudioContext(); + } else if (webkitAudioContext) { + context = new webkitAudioContext(); + } else { + return; + } + + this.DEFAULT_SAMPLE_RATE = context.sampleRate; + + // creates a gain node + const volume = context.createGain(); + + // creates an audio node from the microphone incoming stream + const audioInput = context.createMediaStreamSource(e); + + // connect the stream to the gain node + audioInput.connect(volume); + + /* From the spec: The size of the buffer controls how frequently the audioprocess event is + dispatched and how many sample-frames need to be processed each call. + Lower values for buffer size will result in a lower (better) latency. + Higher values will be necessary to avoid audio breakup and glitches */ + this.recorder = context.createScriptProcessor(2048, 2, 1); + + // The onaudioprocess event needs to be defined externally, so make sure it is not set: + this.recorder.onaudioprocess = null; + + // we connect the recorder + volume.connect(this.recorder); + this.recorder.connect(context.destination); + this.readyFlag = true; + } + + // Publicly accessible methods: + public cloneChannelData(leftChannelData: Float32List, rightChannelData: Float32List) { + this.leftChannel.push(new Float32Array(leftChannelData)); + this.rightChannel.push(new Float32Array(rightChannelData)); + this.recordingLength += 2048; + } + + public startRecordingNewWavFile() { + if (this.readyFlag) { + this.isRecording = true; + this.leftChannel.length = this.rightChannel.length = 0; + this.recordingLength = 0; + } + } + + public stopRecording() { + this.isRecording = false; + } + + public buildWavFileBlob() { + // we flat the left and right channels down + const leftBuffer = this.mergeBuffers(this.leftChannel); + const rightBuffer = this.mergeBuffers(this.rightChannel); + + // Interleave the left and right channels together: + let interleaved: Float32Array = new Float32Array(leftBuffer.length); + + for (let i = 0; i < leftBuffer.length; ++i) { + interleaved[i] = 0.5 * (leftBuffer[i] + rightBuffer[i]); + } + + // Downsample the audio data if necessary: + if (this.DEFAULT_SAMPLE_RATE > this.SAMPLE_RATE) { + interleaved = this.downSampleBuffer(interleaved, this.SAMPLE_RATE); + } + + const totalByteCount = (44 + interleaved.length * 2); + const buffer = new ArrayBuffer(totalByteCount); + const view = new DataView(buffer); + + // Build the RIFF chunk descriptor: + this.writeUTFBytes(view, 0, "RIFF"); + view.setUint32(4, totalByteCount, true); + this.writeUTFBytes(view, 8, "WAVE"); + + // Build the FMT sub-chunk: + this.writeUTFBytes(view, 12, "fmt "); // subchunk1 ID is format + view.setUint32(16, 16, true); // The sub-chunk size is 16. + view.setUint16(20, 1, true); // The audio format is 1. + view.setUint16(22, 1, true); // Number of interleaved channels. + view.setUint32(24, this.SAMPLE_RATE, true); // Sample rate. + view.setUint32(28, this.SAMPLE_RATE * 2, true); // Byte rate. + view.setUint16(32, 2, true); // Block align + view.setUint16(34, 16, true); // Bits per sample. + + // Build the data sub-chunk: + const subChunk2ByteCount = interleaved.length * 2; + this.writeUTFBytes(view, 36, "data"); + view.setUint32(40, subChunk2ByteCount, true); + + // Write the PCM samples to the view: + const lng = interleaved.length; + let index = 44; + const volume = 1; + for (let j = 0; j < lng; j++) { + view.setInt16(index, interleaved[j] * (0x7FFF * volume), true); + index += 2; + } + + return new Blob([view], {type: "audio/wav"}); + } + + private downSampleBuffer(buffer: Float32Array, rate: number) { + if (rate === this.DEFAULT_SAMPLE_RATE) { + return buffer; + } + + if (rate > this.DEFAULT_SAMPLE_RATE) { + // throw "downsampling rate show be smaller than original sample rate"; + return buffer; + } + + const sampleRateRatio = this.DEFAULT_SAMPLE_RATE / rate; + const newLength = Math.round(buffer.length / sampleRateRatio); + const result = new Float32Array(newLength); + let offsetResult = 0; + let offsetBuffer = 0; + + while (offsetResult < result.length) { + const nextOffsetBuffer = Math.round((offsetResult + 1) * sampleRateRatio); + let accum = 0; + let count = 0; + for (let i = offsetBuffer; i < nextOffsetBuffer && i < buffer.length; i++) { + accum += buffer[i]; + count++; + } + result[offsetResult] = accum / count; + offsetResult++; + offsetBuffer = nextOffsetBuffer; + } + return result; + } + + private mergeBuffers(desiredChannelBuffer: Float32List[]) { + const result = new Float32Array(this.recordingLength); + let offset = 0; + const lng = desiredChannelBuffer.length; + for (let i = 0; i < lng; ++i) { + const buffer = desiredChannelBuffer[i]; + result.set(buffer, offset); + offset += buffer.length; + } + return result; + } + + private writeUTFBytes(view: DataView, offset: number, value: string) { + const lng = value.length; + for (let i = 0; i < lng; i++) { + view.setUint8(offset + i, value.charCodeAt(i)); + } + } +} diff --git a/web/include/vditor/src/ts/util/addScript.ts b/web/include/vditor/src/ts/util/addScript.ts new file mode 100644 index 0000000..5af2055 --- /dev/null +++ b/web/include/vditor/src/ts/util/addScript.ts @@ -0,0 +1,43 @@ +export const addScriptSync = (path: string, id: string) => { + if (document.getElementById(id)) { + return false; + } + const xhrObj = new XMLHttpRequest(); + xhrObj.open("GET", path, false); + xhrObj.setRequestHeader("Accept", + "text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01"); + xhrObj.send(""); + const scriptElement = document.createElement("script"); + scriptElement.type = "text/javascript"; + scriptElement.text = xhrObj.responseText; + scriptElement.id = id; + document.head.appendChild(scriptElement); +}; + +export const addScript = (path: string, id: string) => { + return new Promise((resolve, reject) => { + if (document.getElementById(id)) { + // 脚本加载后再次调用直接返回 + resolve(true); + return false; + } + const scriptElement = document.createElement("script"); + scriptElement.src = path; + scriptElement.async = true; + // 循环调用时 Chrome 不会重复请求 js + document.head.appendChild(scriptElement); + scriptElement.onerror = (event) => { + reject(event); + } + scriptElement.onload = () => { + if (document.getElementById(id)) { + // 循环调用需清除 DOM 中的 script 标签 + scriptElement.remove(); + resolve(true); + return false; + } + scriptElement.id = id; + resolve(true); + }; + }); +}; diff --git a/web/include/vditor/src/ts/util/addStyle.ts b/web/include/vditor/src/ts/util/addStyle.ts new file mode 100644 index 0000000..1b2845f --- /dev/null +++ b/web/include/vditor/src/ts/util/addStyle.ts @@ -0,0 +1,10 @@ +export const addStyle = (url: string, id: string) => { + if (!document.getElementById(id)) { + const styleElement = document.createElement("link"); + styleElement.id = id; + styleElement.rel = "stylesheet"; + styleElement.type = "text/css"; + styleElement.href = url; + document.getElementsByTagName("head")[0].appendChild(styleElement); + } +}; diff --git a/web/include/vditor/src/ts/util/code160to32.ts b/web/include/vditor/src/ts/util/code160to32.ts new file mode 100644 index 0000000..d54779e --- /dev/null +++ b/web/include/vditor/src/ts/util/code160to32.ts @@ -0,0 +1,4 @@ +export const code160to32 = (text: string) => { + // 非打断空格转换为空格 + return text.replace(/\u00a0/g, " "); +}; diff --git a/web/include/vditor/src/ts/util/compatibility.ts b/web/include/vditor/src/ts/util/compatibility.ts new file mode 100644 index 0000000..4e245f3 --- /dev/null +++ b/web/include/vditor/src/ts/util/compatibility.ts @@ -0,0 +1,67 @@ +export const isSafari = () => { + return navigator.userAgent.indexOf("Safari") > -1 && navigator.userAgent.indexOf("Chrome") === -1; +}; + +export const isFirefox = () => { + return navigator.userAgent.toLowerCase().indexOf("firefox") > -1; +}; + +export const accessLocalStorage = () => { + try { + return typeof localStorage !== "undefined"; + } catch (e) { + return false; + } +}; + +// 用户 iPhone 点击延迟/需要双击的处理 +export const getEventName = () => { + if (navigator.userAgent.indexOf("iPhone") > -1) { + return "touchstart"; + } else { + return "click"; + } +}; + +// 区别 mac 上的 ctrl 和 meta +export const isCtrl = (event: KeyboardEvent) => { + if (navigator.platform.toUpperCase().indexOf("MAC") >= 0) { + // mac + if (event.metaKey && !event.ctrlKey) { + return true; + } + return false; + } else { + if (!event.metaKey && event.ctrlKey) { + return true; + } + return false; + } +}; +// Mac,Windows 快捷键展示 +export const updateHotkeyTip = (hotkey: string) => { + if (/Mac/.test(navigator.platform) || navigator.platform === "iPhone") { + if (hotkey.indexOf("⇧") > -1 && isFirefox()) { + // Mac Firefox 按下 shift 后,key 同 windows 系统 + hotkey = hotkey.replace(";", ":").replace("=", "+").replace("-", "_"); + } + } else { + if (hotkey.startsWith("⌘")) { + hotkey = hotkey.replace("⌘", "⌘+"); + } else if (hotkey.startsWith("⌥") && hotkey.substr(1, 1) !== "⌘") { + hotkey = hotkey.replace("⌥", "⌥+"); + } else { + hotkey = hotkey.replace("⇧⌘", "⌘+⇧+").replace("⌥⌘", "⌥+⌘+"); + } + hotkey = hotkey.replace("⌘", "Ctrl").replace("⇧", "Shift") + .replace("⌥", "Alt"); + if (hotkey.indexOf("Shift") > -1) { + hotkey = hotkey.replace(";", ":").replace("=", "+").replace("-", "_"); + } + } + return hotkey; +}; + +export const isChrome = () => { + return /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor); +}; diff --git a/web/include/vditor/src/ts/util/editorCommonEvent.ts b/web/include/vditor/src/ts/util/editorCommonEvent.ts new file mode 100644 index 0000000..a51096f --- /dev/null +++ b/web/include/vditor/src/ts/util/editorCommonEvent.ts @@ -0,0 +1,261 @@ +import {Constants} from "../constants"; +import {processHeading} from "../ir/process"; +import {processKeydown as irProcessKeydown} from "../ir/processKeydown"; +import {getMarkdown} from "../markdown/getMarkdown"; +import {previewImage} from "../preview/image"; +import {processHeading as processHeadingSV} from "../sv/process"; +import {processKeydown as mdProcessKeydown} from "../sv/processKeydown"; +import {setEditMode} from "../toolbar/EditMode"; +import {hidePanel} from "../toolbar/setToolbar"; +import {afterRenderEvent} from "../wysiwyg/afterRenderEvent"; +import {processKeydown} from "../wysiwyg/processKeydown"; +import {removeHeading, setHeading} from "../wysiwyg/setHeading"; +import {getEventName, isCtrl} from "./compatibility"; +import {execAfterRender, paste} from "./fixBrowserBehavior"; +import {getSelectText} from "./getSelectText"; +import {hasClosestByAttribute, hasClosestByMatchTag} from "./hasClosest"; +import {matchHotKey} from "./hotKey"; +import {getCursorPosition, getEditorRange} from "./selection"; + +export const focusEvent = (vditor: IVditor, editorElement: HTMLElement) => { + editorElement.addEventListener("focus", () => { + if (vditor.options.focus) { + vditor.options.focus(getMarkdown(vditor)); + } + hidePanel(vditor, ["subToolbar", "hint"]); + }); +}; + +export const dblclickEvent = (vditor: IVditor, editorElement: HTMLElement) => { + editorElement.addEventListener("dblclick", (event: MouseEvent & { target: HTMLElement }) => { + if (event.target.tagName === "IMG") { + if (vditor.options.image.preview) { + vditor.options.image.preview(event.target); + } else if (vditor.options.image.isPreview) { + previewImage(event.target as HTMLImageElement, vditor.options.lang, vditor.options.theme); + } + } + }); +}; + +export const blurEvent = (vditor: IVditor, editorElement: HTMLElement) => { + editorElement.addEventListener("blur", (event) => { + if (vditor.currentMode === "ir") { + const expandElement = vditor.ir.element.querySelector(".vditor-ir__node--expand"); + if (expandElement) { + expandElement.classList.remove("vditor-ir__node--expand"); + } + } else if (vditor.currentMode === "wysiwyg" && + !vditor.wysiwyg.selectPopover.contains(event.relatedTarget as HTMLElement)) { + vditor.wysiwyg.hideComment(); + } + vditor[vditor.currentMode].range = getEditorRange(vditor); + if (vditor.options.blur) { + vditor.options.blur(getMarkdown(vditor)); + } + }); +}; + +export const dropEvent = (vditor: IVditor, editorElement: HTMLElement) => { + editorElement.addEventListener("dragstart", (event) => { + // 选中编辑器中的文字进行拖拽 + event.dataTransfer.setData(Constants.DROP_EDITOR, Constants.DROP_EDITOR); + }); + editorElement.addEventListener("drop", + (event: ClipboardEvent & { dataTransfer?: DataTransfer, target: HTMLElement }) => { + if (event.dataTransfer.getData(Constants.DROP_EDITOR)) { + // 编辑器内选中文字拖拽 + execAfterRender(vditor); + } else if (event.dataTransfer.types.includes("Files") || event.dataTransfer.types.includes("text/html")) { + // 外部文件拖入编辑器中或者编辑器内选中文字拖拽 + paste(vditor, event, { + pasteCode: (code: string) => { + document.execCommand("insertHTML", false, code); + }, + }); + } + }); +}; + +export const copyEvent = + (vditor: IVditor, editorElement: HTMLElement, copy: (event: ClipboardEvent, vditor: IVditor) => void) => { + editorElement.addEventListener("copy", (event: ClipboardEvent) => copy(event, vditor)); + }; + +export const cutEvent = + (vditor: IVditor, editorElement: HTMLElement, copy: (event: ClipboardEvent, vditor: IVditor) => void) => { + editorElement.addEventListener("cut", (event: ClipboardEvent) => { + copy(event, vditor); + // 获取 comment + if (vditor.options.comment.enable && vditor.currentMode === "wysiwyg") { + vditor.wysiwyg.getComments(vditor); + } + document.execCommand("delete"); + }); + }; + +export const scrollCenter = (vditor: IVditor) => { + if (vditor.currentMode === "wysiwyg" && vditor.options.comment.enable) { + vditor.options.comment.adjustTop(vditor.wysiwyg.getComments(vditor, true)); + } + if (!vditor.options.typewriterMode) { + return; + } + const editorElement = vditor[vditor.currentMode].element; + const cursorTop = getCursorPosition(editorElement).top; + if (vditor.options.height === "auto" && !vditor.element.classList.contains("vditor--fullscreen")) { + window.scrollTo(window.scrollX, + cursorTop + vditor.element.offsetTop + vditor.toolbar.element.offsetHeight - window.innerHeight / 2 + 10); + } + if (vditor.options.height !== "auto" || vditor.element.classList.contains("vditor--fullscreen")) { + editorElement.scrollTop = cursorTop + editorElement.scrollTop - editorElement.clientHeight / 2 + 10; + } +}; + +export const hotkeyEvent = (vditor: IVditor, editorElement: HTMLElement) => { + editorElement.addEventListener("keydown", (event: KeyboardEvent & { target: HTMLElement }) => { + if (!event.isComposing && vditor.options.keydown) { + vditor.options.keydown(event); + } + // hint: 上下选择 + if ((vditor.options.hint.extend.length > 1 || vditor.toolbar.elements.emoji) && + vditor.hint.select(event, vditor)) { + return; + } + + // 重置 comment + if (vditor.options.comment.enable && vditor.currentMode === "wysiwyg" && + (event.key === "Backspace" || matchHotKey("⌘X", event))) { + vditor.wysiwyg.getComments(vditor); + } + + if (vditor.currentMode === "sv") { + if (mdProcessKeydown(vditor, event)) { + return; + } + } else if (vditor.currentMode === "wysiwyg") { + if (processKeydown(vditor, event)) { + return; + } + } else if (vditor.currentMode === "ir") { + if (irProcessKeydown(vditor, event)) { + return; + } + } + + if (vditor.options.ctrlEnter && matchHotKey("⌘Enter", event)) { + vditor.options.ctrlEnter(getMarkdown(vditor)); + event.preventDefault(); + return; + } + + // undo + if (matchHotKey("⌘Z", event) && !vditor.toolbar.elements.undo) { + vditor.undo.undo(vditor); + event.preventDefault(); + return; + } + + // redo + if (matchHotKey("⌘Y", event) && !vditor.toolbar.elements.redo) { + vditor.undo.redo(vditor); + event.preventDefault(); + return; + } + + // esc + if (event.key === "Escape") { + if (vditor.hint.element.style.display === "block") { + vditor.hint.element.style.display = "none"; + } else if (vditor.options.esc && !event.isComposing) { + vditor.options.esc(getMarkdown(vditor)); + } + event.preventDefault(); + return; + } + + // h1 - h6 hotkey + if (isCtrl(event) && event.altKey && !event.shiftKey && /^Digit[1-6]$/.test(event.code)) { + if (vditor.currentMode === "wysiwyg") { + const tagName = event.code.replace("Digit", "H"); + if (hasClosestByMatchTag(getSelection().getRangeAt(0).startContainer, tagName)) { + removeHeading(vditor); + } else { + setHeading(vditor, tagName); + } + afterRenderEvent(vditor); + } else if (vditor.currentMode === "sv") { + processHeadingSV(vditor, "#".repeat(parseInt(event.code.replace("Digit", ""), 10)) + " "); + } else if (vditor.currentMode === "ir") { + processHeading(vditor, "#".repeat(parseInt(event.code.replace("Digit", ""), 10)) + " "); + } + event.preventDefault(); + return true; + } + + // toggle edit mode + if (isCtrl(event) && event.altKey && !event.shiftKey && /^Digit[7-9]$/.test(event.code)) { + if (event.code === "Digit7") { + setEditMode(vditor, "wysiwyg", event); + } else if (event.code === "Digit8") { + setEditMode(vditor, "ir", event); + } else if (event.code === "Digit9") { + setEditMode(vditor, "sv", event); + } + return true; + } + + // toolbar action + vditor.options.toolbar.find((menuItem: IMenuItem) => { + if (!menuItem.hotkey || menuItem.toolbar) { + if (menuItem.toolbar) { + const sub = menuItem.toolbar.find((subMenuItem: IMenuItem) => { + if (!subMenuItem.hotkey) { + return false; + } + if (matchHotKey(subMenuItem.hotkey, event)) { + vditor.toolbar.elements[subMenuItem.name].children[0] + .dispatchEvent(new CustomEvent(getEventName())); + event.preventDefault(); + return true; + } + }); + return sub ? true : false; + } + return false; + } + if (matchHotKey(menuItem.hotkey, event)) { + vditor.toolbar.elements[menuItem.name].children[0].dispatchEvent(new CustomEvent(getEventName())); + event.preventDefault(); + return true; + } + }); + }); +}; + +export const selectEvent = (vditor: IVditor, editorElement: HTMLElement) => { + editorElement.addEventListener("selectstart", (event: Event & { target: HTMLElement }) => { + editorElement.onmouseup = () => { + setTimeout(() => { // 鼠标放开后 range 没有即时更新 + const selectText = getSelectText(vditor[vditor.currentMode].element); + if (selectText.trim()) { + if (vditor.currentMode === "wysiwyg" && vditor.options.comment.enable) { + if (!hasClosestByAttribute(event.target, "data-type", "footnotes-block") && + !hasClosestByAttribute(event.target, "data-type", "link-ref-defs-block")) { + vditor.wysiwyg.showComment(); + } else { + vditor.wysiwyg.hideComment(); + } + } + if (vditor.options.select) { + vditor.options.select(selectText); + } + } else { + if (vditor.currentMode === "wysiwyg" && vditor.options.comment.enable) { + vditor.wysiwyg.hideComment(); + } + } + }); + }; + }); +}; diff --git a/web/include/vditor/src/ts/util/fixBrowserBehavior.ts b/web/include/vditor/src/ts/util/fixBrowserBehavior.ts new file mode 100644 index 0000000..7adbe59 --- /dev/null +++ b/web/include/vditor/src/ts/util/fixBrowserBehavior.ts @@ -0,0 +1,1501 @@ +import {Constants} from "../constants"; +import {input as IRInput} from "../ir/input"; +import {processAfterRender} from "../ir/process"; +import {processAfterRender as processSVAfterRender, processPaste} from "../sv/process"; +import {uploadFiles} from "../upload/index"; +import {setHeaders} from "../upload/setHeaders"; +import {afterRenderEvent} from "../wysiwyg/afterRenderEvent"; +import {input} from "../wysiwyg/input"; +import {isCtrl, isFirefox} from "./compatibility"; +import {scrollCenter} from "./editorCommonEvent"; +import { + getTopList, + hasClosestBlock, + hasClosestByAttribute, + hasClosestByClassName, + hasClosestByMatchTag, +} from "./hasClosest"; +import {getLastNode} from "./hasClosest"; +import {highlightToolbar} from "./highlightToolbar"; +import {matchHotKey} from "./hotKey"; +import {processCodeRender, processPasteCode} from "./processCode"; +import { + getEditorRange, + getSelectPosition, + insertHTML, + setRangeByWbr, + setSelectionByPosition, setSelectionFocus, +} from "./selection"; + +// https://github.com/Vanessa219/vditor/issues/508 软键盘无法删除空块 +export const fixGSKeyBackspace = (event: KeyboardEvent, vditor: IVditor, startContainer: Node) => { + if (event.keyCode === 229 && event.code === "" && event.key === "Unidentified" && vditor.currentMode !== "sv") { + const blockElement = hasClosestBlock(startContainer); + // 移动端的标点符号都显示为 299,因此需限定为空删除的条件 + if (blockElement && blockElement.textContent.trim() === "") { + vditor[vditor.currentMode].composingLock = true; + return false; + } + } + return true; +}; + +// https://github.com/Vanessa219/vditor/issues/361 代码块后输入中文 +export const fixCJKPosition = (range: Range, vditor: IVditor, event: KeyboardEvent) => { + if (event.key === "Enter" || event.key === "Tab" || event.key === "Backspace" || event.key.indexOf("Arrow") > -1 + || isCtrl(event) || event.key === "Escape" || event.shiftKey || event.altKey) { + return; + } + const pLiElement = hasClosestByMatchTag(range.startContainer, "P") || + hasClosestByMatchTag(range.startContainer, "LI"); + if (pLiElement && getSelectPosition(pLiElement, vditor[vditor.currentMode].element, range).start === 0) { + + // https://github.com/Vanessa219/vditor/issues/1289 WKWebView切换输入法产生六分之一空格,造成光标错位 + if (pLiElement.nodeValue) { + pLiElement.nodeValue = pLiElement.nodeValue.replace(/\u2006/g, ''); + } + + const zwspNode = document.createTextNode(Constants.ZWSP); + range.insertNode(zwspNode); + range.setStartAfter(zwspNode); + } +}; + +// https://github.com/Vanessa219/vditor/issues/381 光标在内联数学公式中无法向下移动 +export const fixCursorDownInlineMath = (range: Range, key: string) => { + if (key === "ArrowDown" || key === "ArrowUp") { + const inlineElement = hasClosestByAttribute(range.startContainer, "data-type", "math-inline") || + hasClosestByAttribute(range.startContainer, "data-type", "html-entity") || + hasClosestByAttribute(range.startContainer, "data-type", "html-inline"); + if (inlineElement) { + if (key === "ArrowDown") { + range.setStartAfter(inlineElement.parentElement); + } + if (key === "ArrowUp") { + range.setStartBefore(inlineElement.parentElement); + } + } + } +}; + +export const insertEmptyBlock = (vditor: IVditor, position: InsertPosition) => { + const range = getEditorRange(vditor); + const blockElement = hasClosestBlock(range.startContainer); + if (blockElement) { + blockElement.insertAdjacentHTML(position, `

    ${Constants.ZWSP}\n

    `); + setRangeByWbr(vditor[vditor.currentMode].element, range); + highlightToolbar(vditor); + execAfterRender(vditor); + } +}; + +export const isFirstCell = (cellElement: HTMLElement) => { + const tableElement = hasClosestByMatchTag(cellElement, "TABLE") as HTMLTableElement; + if (tableElement && tableElement.rows[0].cells[0].isSameNode(cellElement)) { + return tableElement; + } + return false; +}; + +export const isLastCell = (cellElement: HTMLElement) => { + const tableElement = hasClosestByMatchTag(cellElement, "TABLE") as HTMLTableElement; + if (tableElement && tableElement.lastElementChild.lastElementChild.lastElementChild.isSameNode(cellElement)) { + return tableElement; + } + return false; +}; + +// 光标设置到前一个表格中 +const goPreviousCell = (cellElement: HTMLElement, range: Range, isSelected = true) => { + let previousElement = cellElement.previousElementSibling; + if (!previousElement) { + if (cellElement.parentElement.previousElementSibling) { + previousElement = cellElement.parentElement.previousElementSibling.lastElementChild; + } else if (cellElement.parentElement.parentElement.tagName === "TBODY" && + cellElement.parentElement.parentElement.previousElementSibling) { + previousElement = cellElement.parentElement + .parentElement.previousElementSibling.lastElementChild.lastElementChild; + } else { + previousElement = null; + } + } + if (previousElement) { + range.selectNodeContents(previousElement); + if (!isSelected) { + range.collapse(false); + } + setSelectionFocus(range); + } + return previousElement; +}; + +export const insertAfterBlock = (vditor: IVditor, event: KeyboardEvent, range: Range, element: HTMLElement, + blockElement: HTMLElement) => { + const position = getSelectPosition(element, vditor[vditor.currentMode].element, range); + if ((event.key === "ArrowDown" && element.textContent.trimRight().substr(position.start).indexOf("\n") === -1) || + (event.key === "ArrowRight" && position.start >= element.textContent.trimRight().length)) { + const nextElement = blockElement.nextElementSibling; + if (!nextElement || + (nextElement && (nextElement.tagName === "TABLE" || nextElement.getAttribute("data-type")))) { + blockElement.insertAdjacentHTML("afterend", + `

    ${Constants.ZWSP}

    `); + setRangeByWbr(vditor[vditor.currentMode].element, range); + } else { + range.selectNodeContents(nextElement); + range.collapse(true); + setSelectionFocus(range); + } + event.preventDefault(); + return true; + } + return false; +}; + +export const insertBeforeBlock = (vditor: IVditor, event: KeyboardEvent, range: Range, element: HTMLElement, + blockElement: HTMLElement) => { + const position = getSelectPosition(element, vditor[vditor.currentMode].element, range); + if ((event.key === "ArrowUp" && element.textContent.substr(0, position.start).indexOf("\n") === -1) || + ((event.key === "ArrowLeft" || (event.key === "Backspace" && range.toString() === "")) && + position.start === 0)) { + const previousElement = blockElement.previousElementSibling; + // table || code + if (!previousElement || + (previousElement && (previousElement.tagName === "TABLE" || previousElement.getAttribute("data-type")))) { + blockElement.insertAdjacentHTML("beforebegin", + `

    ${Constants.ZWSP}

    `); + setRangeByWbr(vditor[vditor.currentMode].element, range); + } else { + range.selectNodeContents(previousElement); + range.collapse(false); + setSelectionFocus(range); + } + event.preventDefault(); + return true; + } + return false; +}; + +export const listToggle = (vditor: IVditor, range: Range, type: string, cancel = true) => { + const itemElement = hasClosestByMatchTag(range.startContainer, "LI"); + vditor[vditor.currentMode].element.querySelectorAll("wbr").forEach((wbr) => { + wbr.remove(); + }); + range.insertNode(document.createElement("wbr")); + + if (cancel && itemElement) { + // 取消 + let pHTML = ""; + for (let i = 0; i < itemElement.parentElement.childElementCount; i++) { + const inputElement = itemElement.parentElement.children[i].querySelector("input"); + if (inputElement) { + inputElement.remove(); + } + pHTML += `

    ${itemElement.parentElement.children[i].innerHTML.trimLeft()}

    `; + } + itemElement.parentElement.insertAdjacentHTML("beforebegin", pHTML); + itemElement.parentElement.remove(); + } else { + if (!itemElement) { + // 添加 + let blockElement = hasClosestByAttribute(range.startContainer, "data-block", "0"); + if (!blockElement) { + vditor[vditor.currentMode].element.querySelector("wbr").remove(); + blockElement = vditor[vditor.currentMode].element.querySelector("p"); + blockElement.innerHTML = ""; + } + if (type === "check") { + blockElement.insertAdjacentHTML("beforebegin", + `
    • ${blockElement.innerHTML}
    `); + blockElement.remove(); + } else if (type === "list") { + blockElement.insertAdjacentHTML("beforebegin", + `
    • ${blockElement.innerHTML}
    `); + blockElement.remove(); + } else if (type === "ordered-list") { + blockElement.insertAdjacentHTML("beforebegin", + `
    1. ${blockElement.innerHTML}
    `); + blockElement.remove(); + } + } else { + // 切换 + if (type === "check") { + itemElement.parentElement.querySelectorAll("li").forEach((item) => { + item.insertAdjacentHTML("afterbegin", + `${item.textContent.indexOf(" ") === 0 ? "" : " "}`); + item.classList.add("vditor-task"); + }); + } else { + if (itemElement.querySelector("input")) { + itemElement.parentElement.querySelectorAll("li").forEach((item) => { + item.querySelector("input").remove(); + item.classList.remove("vditor-task"); + }); + } + let element; + if (type === "list") { + element = document.createElement("ul"); + element.setAttribute("data-marker", "*"); + } else { + element = document.createElement("ol"); + element.setAttribute("data-marker", "1."); + } + element.setAttribute("data-block", "0"); + element.setAttribute("data-tight", itemElement.parentElement.getAttribute("data-tight")); + element.innerHTML = itemElement.parentElement.innerHTML; + itemElement.parentElement.parentNode.replaceChild(element, itemElement.parentElement); + } + } + } +}; + +export const listIndent = (vditor: IVditor, liElement: HTMLElement, range: Range) => { + const previousElement = liElement.previousElementSibling; + if (liElement && previousElement) { + const liElements: HTMLElement[] = [liElement]; + Array.from(range.cloneContents().children).forEach((item, index) => { + if (item.nodeType !== 3 && liElement && item.textContent.trim() !== "" + && liElement.getAttribute("data-node-id") === item.getAttribute("data-node-id")) { + if (index !== 0) { + liElements.push(liElement); + } + liElement = liElement.nextElementSibling as HTMLElement; + } + }); + + vditor[vditor.currentMode].element.querySelectorAll("wbr").forEach((wbr) => { + wbr.remove(); + }); + range.insertNode(document.createElement("wbr")); + const liParentElement = previousElement.parentElement; + + let liHTML = ""; + liElements.forEach((item: HTMLElement) => { + let marker = item.getAttribute("data-marker"); + if (marker.length !== 1) { + marker = `1${marker.slice(-1)}`; + } + liHTML += `
  • ${item.innerHTML}
  • `; + item.remove(); + }); + previousElement.insertAdjacentHTML("beforeend", + `<${liParentElement.tagName} data-block="0">${liHTML}`); + + if (vditor.currentMode === "wysiwyg") { + liParentElement.outerHTML = vditor.lute.SpinVditorDOM(liParentElement.outerHTML); + } else { + liParentElement.outerHTML = vditor.lute.SpinVditorIRDOM(liParentElement.outerHTML); + } + + setRangeByWbr(vditor[vditor.currentMode].element, range); + const tempTopListElement = getTopList(range.startContainer); + if (tempTopListElement) { + tempTopListElement.querySelectorAll(`.vditor-${vditor.currentMode}__preview[data-render='2']`) + .forEach((item: HTMLElement) => { + processCodeRender(item, vditor); + if (vditor.currentMode === "wysiwyg") { + item.previousElementSibling.setAttribute("style", "display:none"); + } + }); + } + execAfterRender(vditor); + highlightToolbar(vditor); + } else { + vditor[vditor.currentMode].element.focus(); + } +}; + +export const listOutdent = (vditor: IVditor, liElement: HTMLElement, range: Range, topListElement: HTMLElement) => { + const liParentLiElement = hasClosestByMatchTag(liElement.parentElement, "LI"); + if (liParentLiElement) { + vditor[vditor.currentMode].element.querySelectorAll("wbr").forEach((wbr) => { + wbr.remove(); + }); + range.insertNode(document.createElement("wbr")); + + const liParentElement = liElement.parentElement; + const liParentAfterElement = liParentElement.cloneNode() as HTMLElement; + const liElements: HTMLElement[] = [liElement]; + Array.from(range.cloneContents().children).forEach((item, index) => { + if (item.nodeType !== 3 && liElement && item.textContent.trim() !== "" && + liElement.getAttribute("data-node-id") === item.getAttribute("data-node-id")) { + if (index !== 0) { + liElements.push(liElement); + } + liElement = liElement.nextElementSibling as HTMLElement; + } + }); + let isMatch = false; + let afterHTML = ""; + liParentElement.querySelectorAll("li").forEach((item) => { + if (isMatch) { + afterHTML += item.outerHTML; + if (!item.nextElementSibling && !item.previousElementSibling) { + item.parentElement.remove(); + } else { + item.remove(); + } + } + if (item.isSameNode(liElements[liElements.length - 1])) { + isMatch = true; + } + }); + + liElements.reverse().forEach((item) => { + liParentLiElement.insertAdjacentElement("afterend", item); + }); + + if (afterHTML) { + liParentAfterElement.innerHTML = afterHTML; + liElements[0].insertAdjacentElement("beforeend", liParentAfterElement); + } + + if (vditor.currentMode === "wysiwyg") { + topListElement.outerHTML = vditor.lute.SpinVditorDOM(topListElement.outerHTML); + } else { + topListElement.outerHTML = vditor.lute.SpinVditorIRDOM(topListElement.outerHTML); + } + + setRangeByWbr(vditor[vditor.currentMode].element, range); + const tempTopListElement = getTopList(range.startContainer); + if (tempTopListElement) { + tempTopListElement.querySelectorAll(`.vditor-${vditor.currentMode}__preview[data-render='2']`) + .forEach((item: HTMLElement) => { + processCodeRender(item, vditor); + if (vditor.currentMode === "wysiwyg") { + item.previousElementSibling.setAttribute("style", "display:none"); + } + }); + } + execAfterRender(vditor); + highlightToolbar(vditor); + } else { + vditor[vditor.currentMode].element.focus(); + } +}; + +export const setTableAlign = (tableElement: HTMLTableElement, type: string) => { + const cell = getSelection().getRangeAt(0).startContainer.parentElement; + + const columnCnt = tableElement.rows[0].cells.length; + const rowCnt = tableElement.rows.length; + let currentColumn = 0; + + for (let i = 0; i < rowCnt; i++) { + for (let j = 0; j < columnCnt; j++) { + if (tableElement.rows[i].cells[j].isSameNode(cell)) { + currentColumn = j; + break; + } + } + } + for (let k = 0; k < rowCnt; k++) { + tableElement.rows[k].cells[currentColumn].setAttribute("align", type); + } +}; + +export const isHrMD = (text: string) => { + // - _ * + const marker = text.trimRight().split("\n").pop(); + if (marker === "") { + return false; + } + if (marker.replace(/ |-/g, "") === "" + || marker.replace(/ |_/g, "") === "" + || marker.replace(/ |\*/g, "") === "") { + if (marker.replace(/ /g, "").length > 2) { + if (marker.indexOf("-") > -1 && marker.trimLeft().indexOf(" ") === -1 + && text.trimRight().split("\n").length > 1) { + // 满足 heading + return false; + } + if (marker.indexOf(" ") === 0 || marker.indexOf("\t") === 0) { + // 代码块 + return false; + } + return true; + } + return false; + } + return false; +}; + +export const isHeadingMD = (text: string) => { + // - = + const textArray = text.trimRight().split("\n"); + text = textArray.pop(); + + if (text.indexOf(" ") === 0 || text.indexOf("\t") === 0) { + return false; + } + + text = text.trimLeft(); + if (text === "" || textArray.length === 0) { + return false; + } + if (text.replace(/-/g, "") === "" + || text.replace(/=/g, "") === "") { + return true; + } + return false; +}; + +export const execAfterRender = (vditor: IVditor, options = { + enableAddUndoStack: true, + enableHint: false, + enableInput: true, +}) => { + if (vditor.currentMode === "wysiwyg") { + afterRenderEvent(vditor, options); + } else if (vditor.currentMode === "ir") { + processAfterRender(vditor, options); + } else if (vditor.currentMode === "sv") { + processSVAfterRender(vditor, options); + } +}; + +export const fixList = (range: Range, vditor: IVditor, pElement: HTMLElement | false, event: KeyboardEvent) => { + const startContainer = range.startContainer; + const liElement = hasClosestByMatchTag(startContainer, "LI"); + if (liElement) { + if (!isCtrl(event) && !event.altKey && event.key === "Enter" && + // fix li 中有多个 P 时,在第一个 P 中换行会在下方生成新的 li + (!event.shiftKey && pElement && liElement.contains(pElement) && pElement.nextElementSibling)) { + if (liElement && !liElement.textContent.endsWith("\n")) { + // li 结尾需 \n + liElement.insertAdjacentText("beforeend", "\n"); + } + range.insertNode(document.createTextNode("\n\n")); + range.collapse(false); + execAfterRender(vditor); + event.preventDefault(); + return true; + } + + if (!isCtrl(event) && !event.shiftKey && !event.altKey && event.key === "Backspace" && + !liElement.previousElementSibling && range.toString() === "" && + getSelectPosition(liElement, vditor[vditor.currentMode].element, range).start === 0) { + // 光标位于点和第一个字符中间时,无法删除 li 元素 + if (liElement.nextElementSibling) { + liElement.parentElement.insertAdjacentHTML("beforebegin", + `

    ${liElement.innerHTML}

    `); + liElement.remove(); + } else { + liElement.parentElement.outerHTML = `

    ${liElement.innerHTML}

    `; + } + setRangeByWbr(vditor[vditor.currentMode].element, range); + execAfterRender(vditor); + event.preventDefault(); + return true; + } + + // 空列表删除后与上一级段落对齐 + if (!isCtrl(event) && !event.shiftKey && !event.altKey && event.key === "Backspace" && + liElement.textContent.trim().replace(Constants.ZWSP, "") === "" && + range.toString() === "" && liElement.previousElementSibling?.tagName === "LI") { + liElement.previousElementSibling.insertAdjacentText("beforeend", "\n\n"); + range.selectNodeContents(liElement.previousElementSibling); + range.collapse(false); + liElement.remove(); + setRangeByWbr(vditor[vditor.currentMode].element, range); + execAfterRender(vditor); + event.preventDefault(); + return true; + } + + if (!isCtrl(event) && !event.altKey && event.key === "Tab") { + // 光标位于第一/零字符时,tab 用于列表的缩进 + let isFirst = false; + if (range.startOffset === 0 + && ((startContainer.nodeType === 3 && !startContainer.previousSibling) + || (startContainer.nodeType !== 3 && startContainer.nodeName === "LI"))) { + // 有序/无序列表 + isFirst = true; + } else if (liElement.classList.contains("vditor-task") && range.startOffset === 1 + && startContainer.previousSibling.nodeType !== 3 + && (startContainer.previousSibling as HTMLElement).tagName === "INPUT") { + // 任务列表 + isFirst = true; + } + + if (isFirst || range.toString() !== "") { + if (event.shiftKey) { + listOutdent(vditor, liElement, range, liElement.parentElement); + } else { + listIndent(vditor, liElement, range); + } + event.preventDefault(); + return true; + } + } + } + return false; +}; + +// tab 处理: block code render, table, 列表第一个字符中的 tab 处理单独写在上面 +export const fixTab = (vditor: IVditor, range: Range, event: KeyboardEvent) => { + if (vditor.options.tab && event.key === "Tab") { + if (event.shiftKey) { + // TODO shift+tab + } else { + if (range.toString() === "") { + range.insertNode(document.createTextNode(vditor.options.tab)); + range.collapse(false); + } else { + range.extractContents(); + range.insertNode(document.createTextNode(vditor.options.tab)); + range.collapse(false); + } + } + setSelectionFocus(range); + execAfterRender(vditor); + event.preventDefault(); + return true; + } +}; + +export const fixMarkdown = (event: KeyboardEvent, vditor: IVditor, pElement: HTMLElement | false, range: Range) => { + if (!pElement) { + return; + } + if (!isCtrl(event) && !event.altKey && event.key === "Enter") { + const pText = String.raw`${pElement.textContent}`.replace(/\\\|/g, "").trim(); + const pTextList = pText.split("|"); + if (pText.startsWith("|") && pText.endsWith("|") && pTextList.length > 3) { + // table 自动完成 + let tableHeaderMD = pTextList.map(() => "---").join("|"); + tableHeaderMD = + pElement.textContent + "\n" + tableHeaderMD.substring(3, tableHeaderMD.length - 3) + "\n|"; + pElement.outerHTML = vditor.lute.SpinVditorDOM(tableHeaderMD); + setRangeByWbr(vditor[vditor.currentMode].element, range); + execAfterRender(vditor); + scrollCenter(vditor); + event.preventDefault(); + return true; + } + + // hr 渲染 + if (isHrMD(pElement.innerHTML) && pElement.previousElementSibling) { + // 软换行后 hr 前有内容 + let pInnerHTML = ""; + const innerHTMLList = pElement.innerHTML.trimRight().split("\n"); + if (innerHTMLList.length > 1) { + innerHTMLList.pop(); + pInnerHTML = `

    ${innerHTMLList.join("\n")}

    `; + } + + pElement.insertAdjacentHTML("afterend", + `${pInnerHTML}

    \n

    `); + pElement.remove(); + setRangeByWbr(vditor[vditor.currentMode].element, range); + execAfterRender(vditor); + scrollCenter(vditor); + event.preventDefault(); + return true; + } + + if (isHeadingMD(pElement.innerHTML)) { + // heading 渲染 + if (vditor.currentMode === "wysiwyg") { + pElement.outerHTML = vditor.lute.SpinVditorDOM(pElement.innerHTML + '

    \n

    '); + } else { + pElement.outerHTML = vditor.lute.SpinVditorIRDOM(pElement.innerHTML + '

    \n

    '); + } + setRangeByWbr(vditor[vditor.currentMode].element, range); + execAfterRender(vditor); + scrollCenter(vditor); + event.preventDefault(); + return true; + } + } + + // 软换行会被切割 https://github.com/Vanessa219/vditor/issues/220 + if (range.collapsed && pElement.previousElementSibling && event.key === "Backspace" && + !isCtrl(event) && !event.altKey && !event.shiftKey && + pElement.textContent.trimRight().split("\n").length > 1 && + getSelectPosition(pElement, vditor[vditor.currentMode].element, range).start === 0) { + const lastElement = getLastNode(pElement.previousElementSibling) as HTMLElement; + if (!lastElement.textContent.endsWith("\n")) { + lastElement.textContent = lastElement.textContent + "\n"; + } + lastElement.parentElement.insertAdjacentHTML("beforeend", `${pElement.innerHTML}`); + pElement.remove(); + setRangeByWbr(vditor[vditor.currentMode].element, range); + return false; + } + return false; +}; + +export const insertRow = (vditor: IVditor, range: Range, cellElement: HTMLElement) => { + let rowHTML = ""; + for (let m = 0; m < cellElement.parentElement.childElementCount; m++) { + rowHTML += ` `; + } + if (cellElement.tagName === "TH") { + cellElement.parentElement.parentElement.insertAdjacentHTML("afterend", + `${rowHTML}`); + } else { + cellElement.parentElement.insertAdjacentHTML("afterend", `${rowHTML}`); + } + execAfterRender(vditor); +}; + +export const insertRowAbove = (vditor: IVditor, range: Range, cellElement: HTMLElement) => { + let rowHTML = ""; + for (let m = 0; m < cellElement.parentElement.childElementCount; m++) { + if (cellElement.tagName === "TH") { + rowHTML += ` `; + } else { + rowHTML += ` `; + } + } + if (cellElement.tagName === "TH") { + cellElement.parentElement.parentElement.insertAdjacentHTML("beforebegin", `${rowHTML}`); + + range.insertNode(document.createElement("wbr")); + const theadHTML = cellElement.parentElement.innerHTML.replace(//g, "").replace(/<\/th>/g, ""); + cellElement.parentElement.parentElement.nextElementSibling.insertAdjacentHTML("afterbegin", theadHTML); + + cellElement.parentElement.parentElement.remove(); + setRangeByWbr(vditor.ir.element, range); + } else { + cellElement.parentElement.insertAdjacentHTML("beforebegin", `${rowHTML}`); + } + execAfterRender(vditor); +}; + +export const insertColumn = + (vditor: IVditor, tableElement: HTMLTableElement, cellElement: HTMLElement, type: InsertPosition = "afterend") => { + let index = 0; + let previousElement = cellElement.previousElementSibling; + while (previousElement) { + index++; + previousElement = previousElement.previousElementSibling; + } + for (let i = 0; i < tableElement.rows.length; i++) { + if (i === 0) { + tableElement.rows[i].cells[index].insertAdjacentHTML(type, " "); + } else { + tableElement.rows[i].cells[index].insertAdjacentHTML(type, " "); + } + } + execAfterRender(vditor); + }; +export const deleteRow = (vditor: IVditor, range: Range, cellElement: HTMLElement) => { + if (cellElement.tagName === "TD") { + const tbodyElement = cellElement.parentElement.parentElement; + if (cellElement.parentElement.previousElementSibling) { + range.selectNodeContents(cellElement.parentElement.previousElementSibling.lastElementChild); + } else { + range.selectNodeContents(tbodyElement.previousElementSibling.lastElementChild.lastElementChild); + } + + if (tbodyElement.childElementCount === 1) { + tbodyElement.remove(); + } else { + cellElement.parentElement.remove(); + } + + range.collapse(false); + setSelectionFocus(range); + execAfterRender(vditor); + } +}; + +export const deleteColumn = + (vditor: IVditor, range: Range, tableElement: HTMLTableElement, cellElement: HTMLElement) => { + let index = 0; + let previousElement = cellElement.previousElementSibling; + while (previousElement) { + index++; + previousElement = previousElement.previousElementSibling; + } + if (cellElement.previousElementSibling || cellElement.nextElementSibling) { + range.selectNodeContents(cellElement.previousElementSibling || cellElement.nextElementSibling); + range.collapse(true); + } + for (let i = 0; i < tableElement.rows.length; i++) { + const cells = tableElement.rows[i].cells; + if (cells.length === 1) { + tableElement.remove(); + highlightToolbar(vditor); + break; + } + cells[index].remove(); + } + setSelectionFocus(range); + execAfterRender(vditor); + }; + +export const fixTable = (vditor: IVditor, event: KeyboardEvent, range: Range) => { + const startContainer = range.startContainer; + const cellElement = hasClosestByMatchTag(startContainer, "TD") || + hasClosestByMatchTag(startContainer, "TH"); + if (cellElement) { + // 换行或软换行:在 cell 中添加 br + if (!isCtrl(event) && !event.altKey && event.key === "Enter") { + if (!cellElement.lastElementChild || + (cellElement.lastElementChild && (!cellElement.lastElementChild.isSameNode(cellElement.lastChild) || + cellElement.lastElementChild.tagName !== "BR"))) { + cellElement.insertAdjacentHTML("beforeend", "
    "); + } + const brElement = document.createElement("br"); + range.insertNode(brElement); + range.setStartAfter(brElement); + execAfterRender(vditor); + scrollCenter(vditor); + event.preventDefault(); + return true; + } + + // tab:光标移向下一个 cell + if (event.key === "Tab") { + if (event.shiftKey) { + // shift + tab 光标移动到前一个 cell + goPreviousCell(cellElement, range); + event.preventDefault(); + return true; + } + + let nextElement = cellElement.nextElementSibling; + if (!nextElement) { + if (cellElement.parentElement.nextElementSibling) { + nextElement = cellElement.parentElement.nextElementSibling.firstElementChild; + } else if (cellElement.parentElement.parentElement.tagName === "THEAD" && + cellElement.parentElement.parentElement.nextElementSibling) { + nextElement = + cellElement.parentElement.parentElement.nextElementSibling.firstElementChild.firstElementChild; + } else { + nextElement = null; + } + } + if (nextElement) { + range.selectNodeContents(nextElement); + setSelectionFocus(range); + } + event.preventDefault(); + return true; + } + + const tableElement = cellElement.parentElement.parentElement.parentElement as HTMLTableElement; + if (event.key === "ArrowUp") { + event.preventDefault(); + if (cellElement.tagName === "TH") { + if (tableElement.previousElementSibling) { + range.selectNodeContents(tableElement.previousElementSibling); + range.collapse(false); + setSelectionFocus(range); + } else { + insertEmptyBlock(vditor, "beforebegin"); + } + return true; + } + + let m = 0; + const trElement = cellElement.parentElement as HTMLTableRowElement; + for (; m < trElement.cells.length; m++) { + if (trElement.cells[m].isSameNode(cellElement)) { + break; + } + } + + let previousElement = trElement.previousElementSibling as HTMLTableRowElement; + if (!previousElement) { + previousElement = trElement.parentElement.previousElementSibling.firstChild as HTMLTableRowElement; + } + range.selectNodeContents(previousElement.cells[m]); + range.collapse(false); + setSelectionFocus(range); + return true; + } + + if (event.key === "ArrowDown") { + event.preventDefault(); + const trElement = cellElement.parentElement as HTMLTableRowElement; + if (!trElement.nextElementSibling && cellElement.tagName === "TD") { + if (tableElement.nextElementSibling) { + range.selectNodeContents(tableElement.nextElementSibling); + range.collapse(true); + setSelectionFocus(range); + } else { + insertEmptyBlock(vditor, "afterend"); + } + return true; + } + + let m = 0; + for (; m < trElement.cells.length; m++) { + if (trElement.cells[m].isSameNode(cellElement)) { + break; + } + } + + let nextElement = trElement.nextElementSibling as HTMLTableRowElement; + if (!nextElement) { + nextElement = trElement.parentElement.nextElementSibling.firstChild as HTMLTableRowElement; + } + range.selectNodeContents(nextElement.cells[m]); + range.collapse(true); + setSelectionFocus(range); + return true; + } + + // focus row input, only wysiwyg + if (vditor.currentMode === "wysiwyg" && + !isCtrl(event) && event.key === "Enter" && !event.shiftKey && event.altKey) { + const inputElement = (vditor.wysiwyg.popover.querySelector(".vditor-input") as HTMLInputElement); + inputElement.focus(); + inputElement.select(); + event.preventDefault(); + return true; + } + + // Backspace:光标移动到前一个 cell + if (!isCtrl(event) && !event.shiftKey && !event.altKey && event.key === "Backspace" + && range.startOffset === 0 && range.toString() === "") { + const previousCellElement = goPreviousCell(cellElement, range, false); + if (!previousCellElement && tableElement) { + if (tableElement.textContent.trim() === "") { + tableElement.outerHTML = `

    \n

    `; + setRangeByWbr(vditor[vditor.currentMode].element, range); + } else { + range.setStartBefore(tableElement); + range.collapse(true); + } + execAfterRender(vditor); + } + event.preventDefault(); + return true; + } + // 上方新添加一行 + if (matchHotKey("⇧⌘F", event)) { + insertRowAbove(vditor, range, cellElement); + event.preventDefault(); + return true; + } + + // 下方新添加一行 https://github.com/Vanessa219/vditor/issues/46 + if (matchHotKey("⌘=", event)) { + insertRow(vditor, range, cellElement); + event.preventDefault(); + return true; + } + + // 左方新添加一列 + if (matchHotKey("⇧⌘G", event)) { + insertColumn(vditor, tableElement, cellElement, "beforebegin"); + event.preventDefault(); + return true; + } + + // 后方新添加一列 + if (matchHotKey("⇧⌘=", event)) { + insertColumn(vditor, tableElement, cellElement); + event.preventDefault(); + return true; + } + + // 删除当前行 + if (matchHotKey("⌘-", event)) { + deleteRow(vditor, range, cellElement); + event.preventDefault(); + return true; + } + + // 删除当前列 + if (matchHotKey("⇧⌘-", event)) { + deleteColumn(vditor, range, tableElement, cellElement); + event.preventDefault(); + return true; + } + + // 剧左 + if (matchHotKey("⇧⌘L", event)) { + if (vditor.currentMode === "ir") { + setTableAlign(tableElement, "left"); + execAfterRender(vditor); + event.preventDefault(); + return true; + } else { + const itemElement: HTMLElement = vditor.wysiwyg.popover.querySelector('[data-type="left"]'); + if (itemElement) { + itemElement.click(); + event.preventDefault(); + return true; + } + } + } + + // 剧中 + if (matchHotKey("⇧⌘C", event)) { + if (vditor.currentMode === "ir") { + setTableAlign(tableElement, "center"); + execAfterRender(vditor); + event.preventDefault(); + return true; + } else { + const itemElement: HTMLElement = vditor.wysiwyg.popover.querySelector('[data-type="center"]'); + if (itemElement) { + itemElement.click(); + event.preventDefault(); + return true; + } + } + } + // 剧右 + if (matchHotKey("⇧⌘R", event)) { + if (vditor.currentMode === "ir") { + setTableAlign(tableElement, "right"); + execAfterRender(vditor); + event.preventDefault(); + return true; + } else { + const itemElement: HTMLElement = vditor.wysiwyg.popover.querySelector('[data-type="right"]'); + if (itemElement) { + itemElement.click(); + event.preventDefault(); + return true; + } + } + } + } + return false; +}; + +export const fixCodeBlock = (vditor: IVditor, event: KeyboardEvent, codeRenderElement: HTMLElement, range: Range) => { + // 行级代码块中 command + a,近对当前代码块进行全选 + if (codeRenderElement.tagName === "PRE" && matchHotKey("⌘A", event)) { + range.selectNodeContents(codeRenderElement.firstElementChild); + event.preventDefault(); + return true; + } + + // tab + // TODO shift + tab, shift and 选中文字 + if (vditor.options.tab && event.key === "Tab" && !event.shiftKey && range.toString() === "") { + range.insertNode(document.createTextNode(vditor.options.tab)); + range.collapse(false); + execAfterRender(vditor); + event.preventDefault(); + return true; + } + + // Backspace: 光标位于第零个字符,仅删除代码块标签 + if (event.key === "Backspace" && !isCtrl(event) && !event.shiftKey && !event.altKey) { + const codePosition = getSelectPosition(codeRenderElement, vditor[vditor.currentMode].element, range); + if ((codePosition.start === 0 || + (codePosition.start === 1 && codeRenderElement.innerText === "\n")) // 空代码块,光标在 \n 后 + && range.toString() === "") { + codeRenderElement.parentElement.outerHTML = + `

    ${codeRenderElement.firstElementChild.innerHTML}

    `; + setRangeByWbr(vditor[vditor.currentMode].element, range); + execAfterRender(vditor); + event.preventDefault(); + return true; + } + } + + // 换行 + if (!isCtrl(event) && !event.altKey && event.key === "Enter") { + if (!codeRenderElement.firstElementChild.textContent.endsWith("\n")) { + codeRenderElement.firstElementChild.insertAdjacentText("beforeend", "\n"); + } + range.extractContents(); + range.insertNode(document.createTextNode("\n")); + range.collapse(false); + setSelectionFocus(range); + if (!isFirefox()) { + if (vditor.currentMode === "wysiwyg") { + input(vditor, range); + } else { + IRInput(vditor, range); + } + } + scrollCenter(vditor); + event.preventDefault(); + return true; + } + return false; +}; + +export const fixBlockquote = (vditor: IVditor, range: Range, event: KeyboardEvent, pElement: HTMLElement | false) => { + const startContainer = range.startContainer; + const blockquoteElement = hasClosestByMatchTag(startContainer, "BLOCKQUOTE"); + if (blockquoteElement && range.toString() === "") { + if (event.key === "Backspace" && !isCtrl(event) && !event.shiftKey && !event.altKey && + getSelectPosition(blockquoteElement, vditor[vditor.currentMode].element, range).start === 0) { + // Backspace: 光标位于引用中的第零个字符,仅删除引用标签 + range.insertNode(document.createElement("wbr")); + blockquoteElement.outerHTML = blockquoteElement.innerHTML; + setRangeByWbr(vditor[vditor.currentMode].element, range); + execAfterRender(vditor); + event.preventDefault(); + return true; + } + + if (pElement && event.key === "Enter" && !isCtrl(event) && !event.shiftKey && !event.altKey + && pElement.parentElement.tagName === "BLOCKQUOTE") { + // Enter: 空行回车应逐层跳出 + let isEmpty = false; + if (pElement.innerHTML.replace(Constants.ZWSP, "") === "\n" || + pElement.innerHTML.replace(Constants.ZWSP, "") === "") { + // 空 P + isEmpty = true; + pElement.remove(); + } else if (pElement.innerHTML.endsWith("\n\n") && + getSelectPosition(pElement, vditor[vditor.currentMode].element, range).start === + pElement.textContent.length - 1) { + // 软换行 + pElement.innerHTML = pElement.innerHTML.substr(0, pElement.innerHTML.length - 2); + isEmpty = true; + } + if (isEmpty) { + // 需添加零宽字符,否则的话无法记录 undo + blockquoteElement.insertAdjacentHTML("afterend", `

    ${Constants.ZWSP}\n

    `); + setRangeByWbr(vditor[vditor.currentMode].element, range); + execAfterRender(vditor); + event.preventDefault(); + return true; + } + } + const blockElement = hasClosestBlock(startContainer); + if (vditor.currentMode === "wysiwyg" && blockElement && matchHotKey("⇧⌘;", event)) { + // 插入 blockquote + range.insertNode(document.createElement("wbr")); + blockElement.outerHTML = `
    ${blockElement.outerHTML}
    `; + setRangeByWbr(vditor.wysiwyg.element, range); + afterRenderEvent(vditor); + event.preventDefault(); + return true; + } + + if (insertAfterBlock(vditor, event, range, blockquoteElement, blockquoteElement)) { + return true; + } + if (insertBeforeBlock(vditor, event, range, blockquoteElement, blockquoteElement)) { + return true; + } + } + return false; +}; + +export const fixTask = (vditor: IVditor, range: Range, event: KeyboardEvent) => { + const startContainer = range.startContainer; + const taskItemElement = hasClosestByClassName(startContainer, "vditor-task"); + if (taskItemElement) { + if (matchHotKey("⇧⌘J", event)) { + // ctrl + shift: toggle checked + const inputElement = taskItemElement.firstElementChild as HTMLInputElement; + if (inputElement.checked) { + inputElement.removeAttribute("checked"); + } else { + inputElement.setAttribute("checked", "checked"); + } + execAfterRender(vditor); + event.preventDefault(); + return true; + } + + // Backspace: 在选择框前进行删除 + if (event.key === "Backspace" && !isCtrl(event) && !event.shiftKey && !event.altKey && range.toString() === "" + && range.startOffset === 1 + && ((startContainer.nodeType === 3 && startContainer.previousSibling && + (startContainer.previousSibling as HTMLElement).tagName === "INPUT") + || startContainer.nodeType !== 3)) { + const previousElement = taskItemElement.previousElementSibling; + taskItemElement.querySelector("input").remove(); + if (previousElement) { + const lastNode = getLastNode(previousElement); + lastNode.parentElement.insertAdjacentHTML("beforeend", "" + taskItemElement.innerHTML.trim()); + taskItemElement.remove(); + } else { + taskItemElement.parentElement.insertAdjacentHTML("beforebegin", + `

    ${taskItemElement.innerHTML.trim() || "\n"}

    `); + if (taskItemElement.nextElementSibling) { + taskItemElement.remove(); + } else { + taskItemElement.parentElement.remove(); + } + } + setRangeByWbr(vditor[vditor.currentMode].element, range); + execAfterRender(vditor); + event.preventDefault(); + return true; + } + + if (event.key === "Enter" && !isCtrl(event) && !event.shiftKey && !event.altKey) { + if (taskItemElement.textContent.trim() === "") { + // 当前任务列表无文字 + if (hasClosestByClassName(taskItemElement.parentElement, "vditor-task")) { + // 为子元素时,需进行反向缩进 + const topListElement = getTopList(startContainer); + if (topListElement) { + listOutdent(vditor, taskItemElement, range, topListElement); + } + } else { + // 仅有一级任务列表 + if (taskItemElement.nextElementSibling) { + // 任务列表下方还有元素,需要使用用段落隔断 + let afterHTML = ""; + let beforeHTML = ""; + let isAfter = false; + Array.from(taskItemElement.parentElement.children).forEach((taskItem) => { + if (taskItemElement.isSameNode(taskItem)) { + isAfter = true; + } else { + if (isAfter) { + afterHTML += taskItem.outerHTML; + } else { + beforeHTML += taskItem.outerHTML; + } + } + }); + const parentTagName = taskItemElement.parentElement.tagName; + const dataMarker = taskItemElement.parentElement.tagName === "OL" ? "" : ` data-marker="${taskItemElement.parentElement.getAttribute("data-marker")}"`; + let startAttribute = ""; + if (beforeHTML) { + startAttribute = taskItemElement.parentElement.tagName === "UL" ? "" : ` start="1"`; + beforeHTML = `<${parentTagName} data-tight="true"${dataMarker} data-block="0">${beforeHTML}`; + } + //

    \n

    =>

    \n

    + // https://github.com/Vanessa219/vditor/issues/430 + taskItemElement.parentElement.outerHTML = `${beforeHTML}

    \n

    <${parentTagName} + data-tight="true"${dataMarker} data-block="0"${startAttribute}>${afterHTML}`; + } else { + // 任务列表下方无任务列表元素 + taskItemElement.parentElement.insertAdjacentHTML("afterend", `

    \n

    `); + if (taskItemElement.parentElement.querySelectorAll("li").length === 1) { + // 任务列表仅有一项时,使用 p 元素替换 + taskItemElement.parentElement.remove(); + } else { + // 任务列表有多项时,当前任务列表位于最后一项,移除该任务列表 + taskItemElement.remove(); + } + } + } + } else if (startContainer.nodeType !== 3 && range.startOffset === 0 && + (startContainer.firstChild as HTMLElement).tagName === "INPUT") { + // 光标位于 input 之前 + range.setStart(startContainer.childNodes[1], 1); + } else { + // 当前任务列表有文字,光标后的文字需添加到新任务列表中 + range.setEndAfter(taskItemElement.lastChild); + taskItemElement.insertAdjacentHTML("afterend", `
  • `); + document.querySelector("wbr").after(range.extractContents()); + } + setRangeByWbr(vditor[vditor.currentMode].element, range); + execAfterRender(vditor); + scrollCenter(vditor); + event.preventDefault(); + return true; + } + } + return false; +}; + +export const fixDelete = (vditor: IVditor, range: Range, event: KeyboardEvent, pElement: HTMLElement | false) => { + if (range.startContainer.nodeType !== 3) { + // 光标位于 hr 前,hr 前有内容 + const rangeElement = (range.startContainer as HTMLElement).children[range.startOffset]; + if (rangeElement && rangeElement.tagName === "HR") { + range.selectNodeContents(rangeElement.previousElementSibling); + range.collapse(false); + event.preventDefault(); + return true; + } + } + + if (pElement) { + const previousElement = pElement.previousElementSibling; + if (previousElement && getSelectPosition(pElement, vditor[vditor.currentMode].element, range).start === 0 && + ((isFirefox() && previousElement.tagName === "HR") || previousElement.tagName === "TABLE")) { + if (previousElement.tagName === "TABLE") { + // table 后删除 https://github.com/Vanessa219/vditor/issues/243 + const lastCellElement = previousElement.lastElementChild.lastElementChild.lastElementChild; + lastCellElement.innerHTML = + lastCellElement.innerHTML.trimLeft() + "" + pElement.textContent.trim(); + pElement.remove(); + } else { + // 光标位于 hr 后进行删除 + previousElement.remove(); + } + setRangeByWbr(vditor[vditor.currentMode].element, range); + execAfterRender(vditor); + event.preventDefault(); + return true; + } + } + return false; +}; + +export const fixHR = (range: Range) => { + if (isFirefox() && range.startContainer.nodeType !== 3 && + (range.startContainer as HTMLElement).tagName === "HR") { + range.setStartBefore(range.startContainer); + } +}; + +// firefox https://github.com/Vanessa219/vditor/issues/407 +export const fixFirefoxArrowUpTable = (event: KeyboardEvent, blockElement: false | HTMLElement, range: Range) => { + if (!isFirefox()) { + return false; + } + if (event.key === "ArrowUp" && blockElement && blockElement.previousElementSibling?.tagName === "TABLE") { + const tableElement = blockElement.previousElementSibling as HTMLTableElement; + range.selectNodeContents(tableElement.rows[tableElement.rows.length - 1].lastElementChild); + range.collapse(false); + event.preventDefault(); + return true; + } + if (event.key === "ArrowDown" && blockElement && blockElement.nextElementSibling?.tagName === "TABLE") { + range.selectNodeContents((blockElement.nextElementSibling as HTMLTableElement).rows[0].cells[0]); + range.collapse(true); + event.preventDefault(); + return true; + } + return false; +}; + +export const paste = async (vditor: IVditor, event: (ClipboardEvent | DragEvent) & { target: HTMLElement }, callback: { + pasteCode(code: string): void, +}) => { + if (vditor[vditor.currentMode].element.getAttribute("contenteditable") !== "true") { + return; + } + event.stopPropagation(); + event.preventDefault(); + let textHTML; + let textPlain; + let files; + + if ("clipboardData" in event) { + textHTML = event.clipboardData.getData("text/html"); + textPlain = event.clipboardData.getData("text/plain"); + files = event.clipboardData.files; + } else { + textHTML = event.dataTransfer.getData("text/html"); + textPlain = event.dataTransfer.getData("text/plain"); + if (event.dataTransfer.types.includes("Files")) { + files = event.dataTransfer.items; + } + } + const renderers: { + HTML2VditorDOM?: ILuteRender, + HTML2VditorIRDOM?: ILuteRender, + Md2VditorDOM?: ILuteRender, + Md2VditorIRDOM?: ILuteRender, + Md2VditorSVDOM?: ILuteRender, + } = {}; + const renderLinkDest: ILuteRenderCallback = (node, entering) => { + if (!entering) { + return ["", Lute.WalkContinue]; + } + + if (vditor.options.upload.renderLinkDest) { + return vditor.options.upload.renderLinkDest(vditor, node, entering); + } + + const src = node.TokensStr(); + if (node.__internal_object__.Parent.Type === 34 && src && src.indexOf("file://") === -1 && + vditor.options.upload.linkToImgUrl) { + const xhr = new XMLHttpRequest(); + xhr.open("POST", vditor.options.upload.linkToImgUrl); + if (vditor.options.upload.token) { + xhr.setRequestHeader("X-Upload-Token", vditor.options.upload.token); + } + if (vditor.options.upload.withCredentials) { + xhr.withCredentials = true; + } + setHeaders(vditor, xhr); + xhr.setRequestHeader("Content-Type", "application/json; charset=utf-8"); + xhr.onreadystatechange = () => { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + let responseText = xhr.responseText; + if (vditor.options.upload.linkToImgFormat) { + responseText = vditor.options.upload.linkToImgFormat(xhr.responseText); + } + const responseJSON = JSON.parse(responseText); + if (responseJSON.code !== 0) { + vditor.tip.show(responseJSON.msg); + return; + } + const original = responseJSON.data.originalURL; + if (vditor.currentMode === "sv") { + vditor.sv.element.querySelectorAll(".vditor-sv__marker--link") + .forEach((item: HTMLElement) => { + if (item.textContent === original) { + item.textContent = responseJSON.data.url; + } + }); + } else { + const imgElement: HTMLImageElement = + vditor[vditor.currentMode].element.querySelector(`img[src="${original}"]`); + imgElement.src = responseJSON.data.url; + if (vditor.currentMode === "ir") { + imgElement.previousElementSibling.previousElementSibling.innerHTML = + responseJSON.data.url; + } + } + execAfterRender(vditor); + } else { + vditor.tip.show(xhr.responseText); + } + if (vditor.options.upload.linkToImgCallback) { + vditor.options.upload.linkToImgCallback(xhr.responseText); + } + } + }; + xhr.send(JSON.stringify({url: src})); + } + if (vditor.currentMode === "ir") { + return [`${Lute.EscapeHTMLStr(src)}`, Lute.WalkContinue]; + } else if (vditor.currentMode === "wysiwyg") { + return ["", Lute.WalkContinue]; + } else { + return [`${Lute.EscapeHTMLStr(src)}`, Lute.WalkContinue]; + } + }; + + // 浏览器地址栏拷贝处理 + if (textHTML.replace(/&/g, "&").replace(/<(|\/)(html|body|meta)[^>]*?>/ig, "").trim() === + `${textPlain}` || + textHTML.replace(/&/g, "&").replace(/<(|\/)(html|body|meta)[^>]*?>/ig, "").trim() === + `${textPlain}`) { + textHTML = ""; + } + + // process word + const doc = new DOMParser().parseFromString(textHTML, "text/html"); + if (doc.body) { + textHTML = doc.body.innerHTML; + } + textHTML = Lute.Sanitize(textHTML); + vditor.wysiwyg.getComments(vditor); + + // process code + const height = vditor[vditor.currentMode].element.scrollHeight; + const code = processPasteCode(textHTML, textPlain, vditor.currentMode); + const codeElement = vditor.currentMode === "sv" ? + hasClosestByAttribute(event.target, "data-type", "code-block") : + hasClosestByMatchTag(event.target, "CODE"); + if (codeElement) { + // 粘贴在代码位置 + if (vditor.currentMode === "sv") { + document.execCommand("insertHTML", false, textPlain.replace(/&/g, "&").replace(/ { + e.removeAttribute("style"); + }); + tempElement.querySelectorAll(".vditor-copy").forEach((e) => { + e.remove(); + }); + if (vditor.currentMode === "ir") { + renderers.HTML2VditorIRDOM = {renderLinkDest}; + vditor.lute.SetJSRenderers({renderers}); + insertHTML(vditor.lute.HTML2VditorIRDOM(tempElement.innerHTML), vditor); + } else if (vditor.currentMode === "wysiwyg") { + renderers.HTML2VditorDOM = {renderLinkDest}; + vditor.lute.SetJSRenderers({renderers}); + insertHTML(vditor.lute.HTML2VditorDOM(tempElement.innerHTML), vditor); + } else { + renderers.Md2VditorSVDOM = {renderLinkDest}; + vditor.lute.SetJSRenderers({renderers}); + processPaste(vditor, vditor.lute.HTML2Md(tempElement.innerHTML).trimRight()); + } + vditor.outline.render(vditor); + } else if (files.length > 0) { + if (vditor.options.upload.url || vditor.options.upload.handler) { + await uploadFiles(vditor, files); + } else { + const fileReader = new FileReader(); + let file: File; + if ("clipboardData" in event) { + files = event.clipboardData.files; + file = files[0]; + } else if (event.dataTransfer.types.includes("Files")) { + files = event.dataTransfer.items; + file = files[0].getAsFile(); + } + if (file && file.type.startsWith("image")) { + fileReader.readAsDataURL(file); + fileReader.onload = () => { + let imgHTML = '' + if (vditor.currentMode === "wysiwyg") { + imgHTML += `${file.name}\n`; + } else { + imgHTML += `![${file.name}](${fileReader.result.toString()})\n`; + } + document.execCommand("insertHTML", false, imgHTML); + } + } + } + } else if (textPlain.trim() !== "" && files.length === 0) { + const range = getEditorRange(vditor); + if (range.toString() !== "" && vditor.lute.IsValidLinkDest(textPlain)) { + textPlain = `[${range.toString()}](${textPlain})`; + } + if (vditor.currentMode === "ir") { + renderers.Md2VditorIRDOM = {renderLinkDest}; + vditor.lute.SetJSRenderers({renderers}); + insertHTML(vditor.lute.Md2VditorIRDOM(textPlain), vditor); + } else if (vditor.currentMode === "wysiwyg") { + renderers.Md2VditorDOM = {renderLinkDest}; + vditor.lute.SetJSRenderers({renderers}); + insertHTML(vditor.lute.Md2VditorDOM(textPlain), vditor); + } else { + renderers.Md2VditorSVDOM = {renderLinkDest}; + vditor.lute.SetJSRenderers({renderers}); + processPaste(vditor, textPlain); + } + vditor.outline.render(vditor); + } + } + if (vditor.currentMode !== "sv") { + const blockElement = hasClosestBlock(getEditorRange(vditor).startContainer); + if (blockElement) { + // https://github.com/Vanessa219/vditor/issues/591 + const range = getEditorRange(vditor); + vditor[vditor.currentMode].element.querySelectorAll("wbr").forEach((wbr) => { + wbr.remove(); + }); + range.insertNode(document.createElement("wbr")); + if (vditor.currentMode === "wysiwyg") { + blockElement.outerHTML = vditor.lute.SpinVditorDOM(blockElement.outerHTML); + } else { + blockElement.outerHTML = vditor.lute.SpinVditorIRDOM(blockElement.outerHTML); + } + setRangeByWbr(vditor[vditor.currentMode].element, range); + } + vditor[vditor.currentMode].element.querySelectorAll(`.vditor-${vditor.currentMode}__preview[data-render='2']`) + .forEach((item: HTMLElement) => { + processCodeRender(item, vditor); + }); + } + vditor.wysiwyg.triggerRemoveComment(vditor); + execAfterRender(vditor); + if (vditor[vditor.currentMode].element.scrollHeight - height > + Math.min(vditor[vditor.currentMode].element.clientHeight, window.innerHeight) / 2) { + scrollCenter(vditor); + } +}; diff --git a/web/include/vditor/src/ts/util/function.ts b/web/include/vditor/src/ts/util/function.ts new file mode 100644 index 0000000..31cf560 --- /dev/null +++ b/web/include/vditor/src/ts/util/function.ts @@ -0,0 +1,15 @@ +export const genUUID = () => ([1e7].toString() + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => + (parseInt(c, 10) ^ (window.crypto.getRandomValues(new Uint32Array(1))[0] & (15 >> (parseInt(c, 10) / 4)))).toString(16) +); + +export const getSearch = (key: string, link = window.location.search) => { + const params = link.substring(link.indexOf("?")); + const hashIndex = params.indexOf("#"); + // REF https://developer.mozilla.org/zh-CN/docs/Web/API/URLSearchParams + const urlSearchParams = new URLSearchParams(params.substring(0, hashIndex >= 0 ? hashIndex : undefined)); + return urlSearchParams.get(key); +}; + +export const looseJsonParse = (text: string) => { + return Function(`"use strict";return (${text})`)(); +}; diff --git a/web/include/vditor/src/ts/util/getSelectText.ts b/web/include/vditor/src/ts/util/getSelectText.ts new file mode 100644 index 0000000..3bdfaf4 --- /dev/null +++ b/web/include/vditor/src/ts/util/getSelectText.ts @@ -0,0 +1,8 @@ +import {selectIsEditor} from "./selection"; + +export const getSelectText = (editor: HTMLElement, range?: Range) => { + if (selectIsEditor(editor, range)) { + return getSelection().toString(); + } + return ""; +}; diff --git a/web/include/vditor/src/ts/util/hasClosest.ts b/web/include/vditor/src/ts/util/hasClosest.ts new file mode 100644 index 0000000..50ae4ca --- /dev/null +++ b/web/include/vditor/src/ts/util/hasClosest.ts @@ -0,0 +1,154 @@ +import {hasClosestByTag} from "./hasClosestByHeadings"; + +export const hasTopClosestByClassName = (element: Node, className: string) => { + let closest = hasClosestByClassName(element, className); + let parentClosest: boolean | HTMLElement = false; + let findTop = false; + while (closest && !closest.classList.contains("vditor-reset") && !findTop) { + parentClosest = hasClosestByClassName(closest.parentElement, className); + if (parentClosest) { + closest = parentClosest; + } else { + findTop = true; + } + } + return closest || false; +}; + +export const hasTopClosestByAttribute = (element: Node, attr: string, value: string) => { + let closest = hasClosestByAttribute(element, attr, value); + let parentClosest: boolean | HTMLElement = false; + let findTop = false; + while (closest && !closest.classList.contains("vditor-reset") && !findTop) { + parentClosest = hasClosestByAttribute(closest.parentElement, attr, value); + if (parentClosest) { + closest = parentClosest; + } else { + findTop = true; + } + } + return closest || false; +}; + +export const hasTopClosestByTag = (element: Node, nodeName: string) => { + let closest = hasClosestByTag(element, nodeName); + let parentClosest: boolean | HTMLElement = false; + let findTop = false; + while (closest && !closest.classList.contains("vditor-reset") && !findTop) { + parentClosest = hasClosestByTag(closest.parentElement, nodeName); + if (parentClosest) { + closest = parentClosest; + } else { + findTop = true; + } + } + return closest || false; +}; + +export const getTopList = (element: Node) => { + const topUlElement = hasTopClosestByTag(element, "UL"); + const topOlElement = hasTopClosestByTag(element, "OL"); + let topListElement = topUlElement; + if (topOlElement && (!topUlElement || (topUlElement && topOlElement.contains(topUlElement)))) { + topListElement = topOlElement; + } + return topListElement; +}; + +export const hasClosestByAttribute = (element: Node, attr: string, value: string) => { + if (!element) { + return false; + } + if (element.nodeType === 3) { + element = element.parentElement; + } + let e = element as HTMLElement; + let isClosest = false; + while (e && !isClosest && !e.classList.contains("vditor-reset")) { + if (e.getAttribute(attr) === value) { + isClosest = true; + } else { + e = e.parentElement; + } + } + return isClosest && e; +}; + +export const hasClosestBlock = (element: Node) => { + if (!element) { + return false; + } + if (element.nodeType === 3) { + element = element.parentElement; + } + let e = element as HTMLElement; + let isClosest = false; + + const blockElement = hasClosestByAttribute(element as HTMLElement, "data-block", "0"); + if (blockElement) { + return blockElement; + } + + while (e && !isClosest && !e.classList.contains("vditor-reset")) { + if (e.tagName === "H1" || + e.tagName === "H2" || + e.tagName === "H3" || + e.tagName === "H4" || + e.tagName === "H5" || + e.tagName === "H6" || + e.tagName === "P" || + e.tagName === "BLOCKQUOTE" || + e.tagName === "OL" || + e.tagName === "UL") { + isClosest = true; + } else { + e = e.parentElement; + } + } + return isClosest && e; +}; + +export const hasClosestByMatchTag = (element: Node, nodeName: string) => { + if (!element) { + return false; + } + if (element.nodeType === 3) { + element = element.parentElement; + } + let e = element as HTMLElement; + let isClosest = false; + while (e && !isClosest && !e.classList.contains("vditor-reset")) { + if (e.nodeName === nodeName) { + isClosest = true; + } else { + e = e.parentElement; + } + } + return isClosest && e; +}; + +export const hasClosestByClassName = (element: Node, className: string) => { + if (!element) { + return false; + } + if (element.nodeType === 3) { + element = element.parentElement; + } + let e = element as HTMLElement; + let isClosest = false; + while (e && !isClosest && !e.classList.contains("vditor-reset")) { + if (e.classList.contains(className)) { + isClosest = true; + } else { + e = e.parentElement; + } + } + return isClosest && e; +}; + +export const getLastNode = (node: Node) => { + while (node && node.lastChild) { + node = node.lastChild; + } + return node; +}; diff --git a/web/include/vditor/src/ts/util/hasClosestByHeadings.ts b/web/include/vditor/src/ts/util/hasClosestByHeadings.ts new file mode 100644 index 0000000..9d27800 --- /dev/null +++ b/web/include/vditor/src/ts/util/hasClosestByHeadings.ts @@ -0,0 +1,27 @@ +// NOTE: 减少 method.ts 打包,故从 hasClosest.ts 中拆分 +export const hasClosestByTag = (element: Node, nodeName: string) => { + if (!element) { + return false; + } + if (element.nodeType === 3) { + element = element.parentElement; + } + let e = element as HTMLElement; + let isClosest = false; + while (e && !isClosest && !e.classList.contains("vditor-reset")) { + if (e.nodeName.indexOf(nodeName) === 0) { + isClosest = true; + } else { + e = e.parentElement; + } + } + return isClosest && e; +}; + +export const hasClosestByHeadings = (element: Node) => { + const headingElement = hasClosestByTag(element, "H"); + if (headingElement && headingElement.tagName.length === 2 && headingElement.tagName !== "HR") { + return headingElement; + } + return false; +}; diff --git a/web/include/vditor/src/ts/util/highlightToolbar.ts b/web/include/vditor/src/ts/util/highlightToolbar.ts new file mode 100644 index 0000000..2615e16 --- /dev/null +++ b/web/include/vditor/src/ts/util/highlightToolbar.ts @@ -0,0 +1,10 @@ +import {highlightToolbarIR} from "../ir/highlightToolbarIR"; +import {highlightToolbarWYSIWYG} from "../wysiwyg/highlightToolbarWYSIWYG"; + +export const highlightToolbar = (vditor: IVditor) => { + if (vditor.currentMode === "wysiwyg") { + highlightToolbarWYSIWYG(vditor); + } else if (vditor.currentMode === "ir") { + highlightToolbarIR(vditor); + } +}; diff --git a/web/include/vditor/src/ts/util/hotKey.ts b/web/include/vditor/src/ts/util/hotKey.ts new file mode 100644 index 0000000..6c563ea --- /dev/null +++ b/web/include/vditor/src/ts/util/hotKey.ts @@ -0,0 +1,54 @@ +import {isCtrl, isFirefox} from "./compatibility"; + +// 是否匹配 ⇧⌘[] / ⌘[] / ⌥[] / ⌥⌘[] / ⇧Tab / [] +export const matchHotKey = (hotKey: string, event: KeyboardEvent) => { + if (hotKey === "") { + return false; + } + + // [] + if (hotKey.indexOf("⇧") === -1 && hotKey.indexOf("⌘") === -1 && hotKey.indexOf("⌥") === -1) { + if (!isCtrl(event) && !event.altKey && !event.shiftKey && event.code === hotKey) { + return true; + } + return false; + } + + // 是否匹配 ⇧Tab + if (hotKey === "⇧Tab") { + if (!isCtrl(event) && !event.altKey && event.shiftKey && event.code === "Tab") { + return true; + } + return false; + } + + let hotKeys = hotKey.split(""); + if (hotKey.startsWith("⌥")) { + // 是否匹配 ⌥[] / ⌥⌘[] + const keyCode = hotKeys.length === 3 ? hotKeys[2] : hotKeys[1]; + if ((hotKeys.length === 3 ? isCtrl(event) : !isCtrl(event)) && event.altKey && !event.shiftKey && + event.code === (/^[0-9]$/.test(keyCode) ? "Digit" : "Key") + keyCode) { + return true; + } + return false; + } + + // 是否匹配 ⇧⌘[] / ⌘[] + if (hotKey === "⌘Enter") { + hotKeys = ["⌘", "Enter"]; + } + const hasShift = hotKeys.length > 2 && (hotKeys[0] === "⇧"); + let key = (hasShift ? hotKeys[2] : hotKeys[1]); + if (hasShift && (isFirefox() || !/Mac/.test(navigator.platform))) { + if (key === "-") { + key = "_"; + } else if (key === "=") { + key = "+"; + } + } + if (isCtrl(event) && event.key.toLowerCase() === key.toLowerCase() && !event.altKey + && ((!hasShift && !event.shiftKey) || (hasShift && event.shiftKey))) { + return true; + } + return false; +}; diff --git a/web/include/vditor/src/ts/util/log.ts b/web/include/vditor/src/ts/util/log.ts new file mode 100644 index 0000000..566102b --- /dev/null +++ b/web/include/vditor/src/ts/util/log.ts @@ -0,0 +1,6 @@ +export const log = (method: string, content: string, type: string, print: boolean) => { + if (print) { + // @ts-ignore + console.log(`${method} - ${type}: ${content}`); + } +}; diff --git a/web/include/vditor/src/ts/util/merge.ts b/web/include/vditor/src/ts/util/merge.ts new file mode 100644 index 0000000..f2fda36 --- /dev/null +++ b/web/include/vditor/src/ts/util/merge.ts @@ -0,0 +1,18 @@ +export const merge = (...options: any[]) => { + const target: any = {}; + const merger = (obj: any) => { + for (const prop in obj) { + if (obj.hasOwnProperty(prop)) { + if (Object.prototype.toString.call(obj[prop]) === "[object Object]") { + target[prop] = merge(target[prop], obj[prop]); + } else { + target[prop] = obj[prop]; + } + } + } + }; + for (let i = 0; i < options.length; i++) { + merger(options[i]); + } + return target; +}; diff --git a/web/include/vditor/src/ts/util/processCode.ts b/web/include/vditor/src/ts/util/processCode.ts new file mode 100644 index 0000000..81dddf6 --- /dev/null +++ b/web/include/vditor/src/ts/util/processCode.ts @@ -0,0 +1,98 @@ +import {abcRender} from "../markdown/abcRender"; +import {chartRender} from "../markdown/chartRender"; +import {codeRender} from "../markdown/codeRender"; +import {flowchartRender} from "../markdown/flowchartRender"; +import {graphvizRender} from "../markdown/graphvizRender"; +import {highlightRender} from "../markdown/highlightRender"; +import {mathRender} from "../markdown/mathRender"; +import {mermaidRender} from "../markdown/mermaidRender"; +import {markmapRender} from "../markdown/markmapRender"; +import {mindmapRender} from "../markdown/mindmapRender"; +import {plantumlRender} from "../markdown/plantumlRender"; + +export const processPasteCode = (html: string, text: string, type = "sv") => { + const tempElement = document.createElement("div"); + tempElement.innerHTML = html; + let isCode = false; + if (tempElement.childElementCount === 1 && + (tempElement.lastElementChild as HTMLElement).style.fontFamily.indexOf("monospace") > -1) { + // VS Code + isCode = true; + } + const pres = tempElement.querySelectorAll("pre"); + if (tempElement.childElementCount === 1 && pres.length === 1 + && pres[0].className !== "vditor-wysiwyg" + && pres[0].className !== "vditor-sv") { + // IDE + isCode = true; + } + if (html.indexOf('\n

    ') === 0) { + // Xcode + isCode = true; + } + if (tempElement.childElementCount === 1 && tempElement.firstElementChild.tagName === "TABLE" && + tempElement.querySelector(".line-number") && tempElement.querySelector(".line-content")) { + // 网页源码 + isCode = true; + } + + if (isCode) { + const code = text || html; + if (/\n/.test(code) || pres.length === 1) { + if (type === "wysiwyg") { + return `

    ${
    +                    code.replace(/&/g, "&").replace(/
    `; + } + return "\n```\n" + code.replace(/&/g, "&").replace(/${code.replace(/&/g, "&").replace(/`; + } + return `\`${code}\``; + } + } + return false; +}; + +export const processCodeRender = (previewPanel: HTMLElement, vditor: IVditor) => { + if (!previewPanel) { + return; + } + if (previewPanel.parentElement.getAttribute("data-type") === "html-block") { + previewPanel.setAttribute("data-render", "1"); + return; + } + const language = previewPanel.firstElementChild.className.replace("language-", ""); + if (language === "abc") { + abcRender(previewPanel, vditor.options.cdn); + } else if (language === "mermaid") { + mermaidRender(previewPanel, vditor.options.cdn, vditor.options.theme); + } else if (language === "markmap") { + markmapRender(previewPanel, vditor.options.cdn, vditor.options.theme); + } else if (language === "flowchart") { + flowchartRender(previewPanel, vditor.options.cdn); + } else if (language === "echarts") { + chartRender(previewPanel, vditor.options.cdn, vditor.options.theme); + } else if (language === "mindmap") { + mindmapRender(previewPanel, vditor.options.cdn, vditor.options.theme); + } else if (language === "plantuml") { + plantumlRender(previewPanel, vditor.options.cdn); + } else if (language === "graphviz") { + graphvizRender(previewPanel, vditor.options.cdn); + } else if (language === "math") { + mathRender(previewPanel, {cdn: vditor.options.cdn, math: vditor.options.preview.math}); + } else { + const cRender = vditor.options.customRenders.find((item) => { + if (item.language === language) { + item.render(previewPanel, vditor); + return true + } + }) + if (!cRender) { + highlightRender(Object.assign({}, vditor.options.preview.hljs), previewPanel, vditor.options.cdn); + codeRender(previewPanel, vditor.options.preview.hljs); + } + } + + previewPanel.setAttribute("data-render", "1"); +}; diff --git a/web/include/vditor/src/ts/util/selection.ts b/web/include/vditor/src/ts/util/selection.ts new file mode 100644 index 0000000..e38784c --- /dev/null +++ b/web/include/vditor/src/ts/util/selection.ts @@ -0,0 +1,275 @@ +import {Constants} from "../constants"; +import {isChrome} from "./compatibility"; +import {hasClosestBlock, hasClosestByClassName} from "./hasClosest"; + +export const getEditorRange = (vditor: IVditor) => { + let range: Range; + const element = vditor[vditor.currentMode].element; + if (getSelection().rangeCount > 0) { + range = getSelection().getRangeAt(0); + if (element.isEqualNode(range.startContainer) || element.contains(range.startContainer)) { + return range; + } + } + if (vditor[vditor.currentMode].range) { + return vditor[vditor.currentMode].range; + } + element.focus(); + range = element.ownerDocument.createRange(); + range.setStart(element, 0); + range.collapse(true); + return range; +}; + +export const getCursorPosition = (editor: HTMLElement) => { + const range = window.getSelection().getRangeAt(0); + if (!editor.contains(range.startContainer) && !hasClosestByClassName(range.startContainer, "vditor-panel--none")) { + return { + left: 0, + top: 0, + }; + } + const parentRect = editor.parentElement.getBoundingClientRect(); + let cursorRect; + if (range.getClientRects().length === 0) { + if (range.startContainer.nodeType === 3) { + // 空行时,会出现没有 br 的情况,需要根据父元素

    获取位置信息 + const parent = range.startContainer.parentElement; + if (parent && parent.getClientRects().length > 0) { + cursorRect = parent.getClientRects()[0]; + } else { + return { + left: 0, + top: 0, + }; + } + } else { + const children = (range.startContainer as Element).children; + if (children[range.startOffset] && + children[range.startOffset].getClientRects().length > 0) { + // markdown 模式回车 + cursorRect = children[range.startOffset].getClientRects()[0]; + } else if (range.startContainer.childNodes.length > 0) { + // in table or code block + const cloneRange = range.cloneRange(); + range.selectNode(range.startContainer.childNodes[Math.max(0, range.startOffset - 1)]); + cursorRect = range.getClientRects()[0]; + range.setEnd(cloneRange.endContainer, cloneRange.endOffset); + range.setStart(cloneRange.startContainer, cloneRange.startOffset); + } else { + cursorRect = (range.startContainer as HTMLElement).getClientRects()[0]; + } + if (!cursorRect) { + let parentElement = range.startContainer.childNodes[range.startOffset] as HTMLElement; + while (!parentElement.getClientRects || + (parentElement.getClientRects && parentElement.getClientRects().length === 0)) { + parentElement = parentElement.parentElement; + } + cursorRect = parentElement.getClientRects()[0]; + } + } + + } else { + cursorRect = range.getClientRects()[0]; + } + + return { + left: cursorRect.left - parentRect.left, + top: cursorRect.top - parentRect.top, + }; +}; + +export const selectIsEditor = (editor: HTMLElement, range?: Range) => { + if (!range) { + if (getSelection().rangeCount === 0) { + return false; + } else { + range = getSelection().getRangeAt(0); + } + } + const container = range.commonAncestorContainer; + + return editor.isEqualNode(container) || editor.contains(container); +}; + +export const setSelectionFocus = (range: Range) => { + const selection = window.getSelection(); + selection.removeAllRanges(); + selection.addRange(range); +}; + +export const getSelectPosition = (selectElement: HTMLElement, editorElement: HTMLElement, range?: Range) => { + const position = { + end: 0, + start: 0, + }; + + if (!range) { + if (getSelection().rangeCount === 0) { + return position; + } + range = window.getSelection().getRangeAt(0); + } + + if (selectIsEditor(editorElement, range)) { + const preSelectionRange = range.cloneRange(); + if (selectElement.childNodes[0] && selectElement.childNodes[0].childNodes[0]) { + preSelectionRange.setStart(selectElement.childNodes[0].childNodes[0], 0); + } else { + preSelectionRange.selectNodeContents(selectElement); + } + preSelectionRange.setEnd(range.startContainer, range.startOffset); + position.start = preSelectionRange.toString().length; + position.end = position.start + range.toString().length; + } + return position; +}; + +export const setSelectionByPosition = (start: number, end: number, editor: HTMLElement) => { + let charIndex = 0; + let line = 0; + let pNode = editor.childNodes[line]; + let foundStart = false; + let stop = false; + start = Math.max(0, start); + end = Math.max(0, end); + + const range = editor.ownerDocument.createRange(); + range.setStart(pNode || editor, 0); + range.collapse(true); + + while (!stop && pNode) { + const nextCharIndex = charIndex + pNode.textContent.length; + if (!foundStart && start >= charIndex && start <= nextCharIndex) { + if (start === 0) { + range.setStart(pNode, 0); + } else { + if (pNode.childNodes[0].nodeType === 3) { + range.setStart(pNode.childNodes[0], start - charIndex); + } else if (pNode.nextSibling) { + range.setStartBefore(pNode.nextSibling); + } else { + range.setStartAfter(pNode); + } + } + foundStart = true; + if (start === end) { + stop = true; + break; + } + } + if (foundStart && end >= charIndex && end <= nextCharIndex) { + if (end === 0) { + range.setEnd(pNode, 0); + } else { + if (pNode.childNodes[0].nodeType === 3) { + range.setEnd(pNode.childNodes[0], end - charIndex); + } else if (pNode.nextSibling) { + range.setEndBefore(pNode.nextSibling); + } else { + range.setEndAfter(pNode); + } + } + stop = true; + } + charIndex = nextCharIndex; + pNode = editor.childNodes[++line]; + } + + if (!stop && editor.childNodes[line - 1]) { + range.setStartBefore(editor.childNodes[line - 1]); + } + + setSelectionFocus(range); + return range; +}; + +export const setRangeByWbr = (element: HTMLElement, range: Range) => { + const wbrElement = element.querySelector("wbr"); + if (!wbrElement) { + return; + } + if (!wbrElement.previousElementSibling) { + if (wbrElement.previousSibling) { + // text + range.setStart(wbrElement.previousSibling, wbrElement.previousSibling.textContent.length); + } else if (wbrElement.nextSibling) { + if (wbrElement.nextSibling.nodeType === 3) { + // text + range.setStart(wbrElement.nextSibling, 0); + } else { + //
    https://github.com/Vanessa219/vditor/issues/400 + range.setStartBefore(wbrElement.nextSibling); + } + } else { + // 内容为空 + range.setStart(wbrElement.parentElement, 0); + } + } else { + if (wbrElement.previousElementSibling.isSameNode(wbrElement.previousSibling)) { + if (wbrElement.previousElementSibling.lastChild) { + // text + range.setStartBefore(wbrElement); + range.collapse(true); + setSelectionFocus(range); + // fix Chrome set range bug: **c** + if (isChrome() && (wbrElement.previousElementSibling.tagName === "EM" || + wbrElement.previousElementSibling.tagName === "STRONG" || + wbrElement.previousElementSibling.tagName === "S")) { + range.insertNode(document.createTextNode(Constants.ZWSP)); + range.collapse(false); + } + wbrElement.remove(); + return; + } else { + //
    + range.setStartAfter(wbrElement.previousElementSibling); + } + } else { + // texttext + range.setStart(wbrElement.previousSibling, wbrElement.previousSibling.textContent.length); + } + } + range.collapse(true); + wbrElement.remove(); + setSelectionFocus(range); +}; + +export const insertHTML = (html: string, vditor: IVditor) => { + // 使用 lute 方法会添加 p 元素,只有一个 p 元素的时候进行删除 + const tempElement = document.createElement("div"); + tempElement.innerHTML = html; + const tempBlockElement = tempElement.querySelectorAll("p"); + if (tempBlockElement.length === 1 && !tempBlockElement[0].previousSibling && !tempBlockElement[0].nextSibling && + vditor[vditor.currentMode].element.children.length > 0 && tempElement.firstElementChild.tagName === "P") { + html = tempBlockElement[0].innerHTML.trim(); + } + + const pasteElement = document.createElement("div"); + pasteElement.innerHTML = html; + + const range = getEditorRange(vditor); + if (range.toString() !== "") { + vditor[vditor.currentMode].preventInput = true; + document.execCommand("delete", false, ""); + } + + if (pasteElement.firstElementChild && + pasteElement.firstElementChild.getAttribute("data-block") === "0") { + // 粘贴内容为块元素时,应在下一段落中插入 + pasteElement.lastElementChild.insertAdjacentHTML("beforeend", ""); + const blockElement = hasClosestBlock(range.startContainer); + if (!blockElement) { + vditor[vditor.currentMode].element.insertAdjacentHTML("beforeend", pasteElement.innerHTML); + } else { + blockElement.insertAdjacentHTML("afterend", pasteElement.innerHTML); + } + setRangeByWbr(vditor[vditor.currentMode].element, range); + } else { + const pasteTemplate = document.createElement("template"); + pasteTemplate.innerHTML = html; + range.insertNode(pasteTemplate.content.cloneNode(true)); + range.collapse(false); + setSelectionFocus(range); + } +}; diff --git a/web/include/vditor/src/ts/util/toc.ts b/web/include/vditor/src/ts/util/toc.ts new file mode 100644 index 0000000..edf2fab --- /dev/null +++ b/web/include/vditor/src/ts/util/toc.ts @@ -0,0 +1,83 @@ +import {mathRender} from "../markdown/mathRender"; +import {execAfterRender, insertAfterBlock, insertBeforeBlock} from "./fixBrowserBehavior"; +import {hasClosestByClassName, hasClosestByMatchTag} from "./hasClosest"; +import {getSelectPosition} from "./selection"; + +export const renderToc = (vditor: IVditor) => { + if (vditor.currentMode === "sv") { + return; + } + const editorElement = vditor[vditor.currentMode].element; + let tocHTML = vditor.outline.render(vditor); + if (tocHTML === "") { + tocHTML = "[ToC]"; + } + 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) => { + 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; + } + } +}; diff --git a/web/include/vditor/src/ts/wysiwyg/afterRenderEvent.ts b/web/include/vditor/src/ts/wysiwyg/afterRenderEvent.ts new file mode 100644 index 0000000..1708a77 --- /dev/null +++ b/web/include/vditor/src/ts/wysiwyg/afterRenderEvent.ts @@ -0,0 +1,41 @@ +import {getMarkdown} from "../markdown/getMarkdown"; +import {accessLocalStorage} from "../util/compatibility"; + +export const afterRenderEvent = (vditor: IVditor, options = { + enableAddUndoStack: true, + enableHint: false, + enableInput: true, +}) => { + if (options.enableHint) { + vditor.hint.render(vditor); + } + clearTimeout(vditor.wysiwyg.afterRenderTimeoutId); + vditor.wysiwyg.afterRenderTimeoutId = window.setTimeout(() => { + if (vditor.wysiwyg.composingLock) { + return; + } + const text = getMarkdown(vditor); + if (typeof vditor.options.input === "function" && options.enableInput) { + vditor.options.input(text); + } + + if (vditor.options.counter.enable) { + vditor.counter.render(vditor, text); + } + + if (vditor.options.cache.enable && accessLocalStorage()) { + localStorage.setItem(vditor.options.cache.id, text); + if (vditor.options.cache.after) { + vditor.options.cache.after(text); + } + } + + if (vditor.devtools) { + vditor.devtools.renderEchart(vditor); + } + + if (options.enableAddUndoStack) { + vditor.undo.addToUndoStack(vditor); + } + }, vditor.options.undoDelay); +}; diff --git a/web/include/vditor/src/ts/wysiwyg/highlightToolbarWYSIWYG.ts b/web/include/vditor/src/ts/wysiwyg/highlightToolbarWYSIWYG.ts new file mode 100644 index 0000000..699a389 --- /dev/null +++ b/web/include/vditor/src/ts/wysiwyg/highlightToolbarWYSIWYG.ts @@ -0,0 +1,1139 @@ +import {Constants} from "../constants"; +import {disableToolbar} from "../toolbar/setToolbar"; +import {enableToolbar} from "../toolbar/setToolbar"; +import {removeCurrentToolbar} from "../toolbar/setToolbar"; +import {setCurrentToolbar} from "../toolbar/setToolbar"; +import {isCtrl, updateHotkeyTip} from "../util/compatibility"; +import {scrollCenter} from "../util/editorCommonEvent"; +import { + deleteColumn, + deleteRow, + insertColumn, + insertRow, + insertRowAbove, + setTableAlign, +} from "../util/fixBrowserBehavior"; +import { + hasClosestByAttribute, + hasClosestByClassName, + hasClosestByMatchTag, +} from "../util/hasClosest"; +import { + hasClosestByHeadings, + hasClosestByTag, +} from "../util/hasClosestByHeadings"; +import {processCodeRender} from "../util/processCode"; +import { + getEditorRange, + selectIsEditor, + setRangeByWbr, + setSelectionFocus, +} from "../util/selection"; +import {afterRenderEvent} from "./afterRenderEvent"; +import {removeBlockElement} from "./processKeydown"; +import {renderToc} from "../util/toc"; +import {getMarkdown} from "../markdown/getMarkdown"; + +export const highlightToolbarWYSIWYG = (vditor: IVditor) => { + clearTimeout(vditor.wysiwyg.hlToolbarTimeoutId); + vditor.wysiwyg.hlToolbarTimeoutId = window.setTimeout(() => { + if ( + vditor.wysiwyg.element.getAttribute("contenteditable") === "false" + ) { + return; + } + if (!selectIsEditor(vditor.wysiwyg.element)) { + return; + } + + removeCurrentToolbar(vditor.toolbar.elements, Constants.EDIT_TOOLBARS); + enableToolbar(vditor.toolbar.elements, Constants.EDIT_TOOLBARS); + + const range = getSelection().getRangeAt(0); + let typeElement = range.startContainer as HTMLElement; + if (range.startContainer.nodeType === 3) { + typeElement = range.startContainer.parentElement; + } else { + typeElement = typeElement.childNodes[ + range.startOffset >= typeElement.childNodes.length + ? typeElement.childNodes.length - 1 + : range.startOffset + ] as HTMLElement; + } + + const footnotesElement = hasClosestByAttribute(typeElement, "data-type", "footnotes-block"); + if (footnotesElement) { + vditor.wysiwyg.popover.innerHTML = ""; + genClose(footnotesElement, vditor); + setPopoverPosition(vditor, footnotesElement); + return; + } + + // 工具栏高亮和禁用 + const liElement = hasClosestByMatchTag(typeElement, "LI"); + if (liElement) { + if (liElement.classList.contains("vditor-task")) { + setCurrentToolbar(vditor.toolbar.elements, ["check"]); + } else if (liElement.parentElement.tagName === "OL") { + setCurrentToolbar(vditor.toolbar.elements, ["ordered-list"]); + } else if (liElement.parentElement.tagName === "UL") { + setCurrentToolbar(vditor.toolbar.elements, ["list"]); + } + enableToolbar(vditor.toolbar.elements, ["outdent", "indent"]); + } else { + disableToolbar(vditor.toolbar.elements, ["outdent", "indent"]); + } + + if (hasClosestByMatchTag(typeElement, "BLOCKQUOTE")) { + setCurrentToolbar(vditor.toolbar.elements, ["quote"]); + } + + if ( + hasClosestByMatchTag(typeElement, "B") || + hasClosestByMatchTag(typeElement, "STRONG") + ) { + setCurrentToolbar(vditor.toolbar.elements, ["bold"]); + } + + if ( + hasClosestByMatchTag(typeElement, "I") || + hasClosestByMatchTag(typeElement, "EM") + ) { + setCurrentToolbar(vditor.toolbar.elements, ["italic"]); + } + + if ( + hasClosestByMatchTag(typeElement, "STRIKE") || + hasClosestByMatchTag(typeElement, "S") + ) { + setCurrentToolbar(vditor.toolbar.elements, ["strike"]); + } + + // comments + vditor.wysiwyg.element + .querySelectorAll(".vditor-comment--focus") + .forEach((item) => { + item.classList.remove("vditor-comment--focus"); + }); + const commentElement = hasClosestByClassName(typeElement, "vditor-comment"); + if (commentElement) { + let ids = commentElement.getAttribute("data-cmtids").split(" "); + if (ids.length > 1 && commentElement.nextSibling.isSameNode(commentElement.nextElementSibling)) { + const nextIds = commentElement.nextElementSibling + .getAttribute("data-cmtids") + .split(" "); + ids.find((id) => { + if (nextIds.includes(id)) { + ids = [id]; + return true; + } + }); + } + vditor.wysiwyg.element + .querySelectorAll(".vditor-comment") + .forEach((item) => { + if (item.getAttribute("data-cmtids").indexOf(ids[0]) > -1) { + item.classList.add("vditor-comment--focus"); + } + }); + } + + const aElement = hasClosestByMatchTag(typeElement, "A"); + if (aElement) { + setCurrentToolbar(vditor.toolbar.elements, ["link"]); + } + const tableElement = hasClosestByMatchTag(typeElement, "TABLE") as HTMLTableElement; + const headingElement = hasClosestByHeadings(typeElement) as HTMLElement; + if (hasClosestByMatchTag(typeElement, "CODE")) { + if (hasClosestByMatchTag(typeElement, "PRE")) { + disableToolbar(vditor.toolbar.elements, [ + "headings", + "bold", + "italic", + "strike", + "line", + "quote", + "list", + "ordered-list", + "check", + "code", + "inline-code", + "upload", + "link", + "table", + "record", + ]); + setCurrentToolbar(vditor.toolbar.elements, ["code"]); + } else { + disableToolbar(vditor.toolbar.elements, [ + "headings", + "bold", + "italic", + "strike", + "line", + "quote", + "list", + "ordered-list", + "check", + "code", + "upload", + "link", + "table", + "record", + ]); + setCurrentToolbar(vditor.toolbar.elements, ["inline-code"]); + } + } else if (headingElement) { + disableToolbar(vditor.toolbar.elements, ["bold"]); + setCurrentToolbar(vditor.toolbar.elements, ["headings"]); + } else if (tableElement) { + disableToolbar(vditor.toolbar.elements, ["table"]); + } + + // toc popover + const tocElement = hasClosestByClassName(typeElement, "vditor-toc") as HTMLElement; + if (tocElement) { + vditor.wysiwyg.popover.innerHTML = ""; + genClose(tocElement, vditor); + setPopoverPosition(vditor, tocElement); + return; + } + + // quote popover + const blockquoteElement = hasClosestByTag(typeElement, "BLOCKQUOTE") as HTMLTableElement; + if (blockquoteElement) { + vditor.wysiwyg.popover.innerHTML = ""; + genUp(range, blockquoteElement, vditor); + genDown(range, blockquoteElement, vditor); + genClose(blockquoteElement, vditor); + setPopoverPosition(vditor, blockquoteElement); + } + + // list item popover + if (liElement) { + vditor.wysiwyg.popover.innerHTML = ""; + + genUp(range, liElement, vditor); + genDown(range, liElement, vditor); + genClose(liElement, vditor); + + setPopoverPosition(vditor, liElement); + } + + // table popover + if (tableElement) { + const lang: keyof II18n | "" = vditor.options.lang; + const options: IOptions = vditor.options; + vditor.wysiwyg.popover.innerHTML = ""; + const updateTable = () => { + const oldRow = tableElement.rows.length; + const oldColumn = tableElement.rows[0].cells.length; + const row = parseInt(input.value, 10) || oldRow; + const column = parseInt(input2.value, 10) || oldColumn; + + if (row === oldRow && oldColumn === column) { + return; + } + + if (oldColumn !== column) { + const columnDiff = column - oldColumn; + for (let i = 0; i < tableElement.rows.length; i++) { + if (columnDiff > 0) { + for (let j = 0; j < columnDiff; j++) { + if (i === 0) { + tableElement.rows[i].lastElementChild.insertAdjacentHTML("afterend", " "); + } else { + tableElement.rows[i].lastElementChild.insertAdjacentHTML("afterend", " "); + } + } + } else { + for (let k = oldColumn - 1; k >= column; k--) { + tableElement.rows[i].cells[k].remove(); + } + } + } + } + + if (oldRow !== row) { + const rowDiff = row - oldRow; + if (rowDiff > 0) { + let rowHTML = ""; + for (let m = 0; m < column; m++) { + rowHTML += " "; + } + for (let l = 0; l < rowDiff; l++) { + if (tableElement.querySelector("tbody")) { + tableElement + .querySelector("tbody") + .insertAdjacentHTML("beforeend", rowHTML); + } else { + tableElement + .querySelector("thead") + .insertAdjacentHTML("afterend", rowHTML + ""); + } + } + } else { + for (let m = oldRow - 1; m >= row; m--) { + tableElement.rows[m].remove(); + if (tableElement.rows.length === 1) { + tableElement.querySelector("tbody").remove(); + } + } + } + } + if (typeof vditor.options.input === "function") { + vditor.options.input(getMarkdown(vditor)); + } + }; + + const setAlign = (type: string) => { + setTableAlign(tableElement, type); + if (type === "right") { + left.classList.remove("vditor-icon--current"); + center.classList.remove("vditor-icon--current"); + right.classList.add("vditor-icon--current"); + } else if (type === "center") { + left.classList.remove("vditor-icon--current"); + right.classList.remove("vditor-icon--current"); + center.classList.add("vditor-icon--current"); + } else { + center.classList.remove("vditor-icon--current"); + right.classList.remove("vditor-icon--current"); + left.classList.add("vditor-icon--current"); + } + setSelectionFocus(range); + afterRenderEvent(vditor); + }; + + const td = hasClosestByMatchTag(typeElement, "TD"); + const th = hasClosestByMatchTag(typeElement, "TH"); + let alignType = "left"; + if (td) { + alignType = td.getAttribute("align") || "left"; + } else if (th) { + alignType = th.getAttribute("align") || "center"; + } + + const left = document.createElement("button"); + left.setAttribute("type", "button"); + left.setAttribute("aria-label", window.VditorI18n.alignLeft + "<" + updateHotkeyTip("⇧⌘L") + ">"); + left.setAttribute("data-type", "left"); + left.innerHTML = + ''; + left.className = + "vditor-icon vditor-tooltipped vditor-tooltipped__n" + + (alignType === "left" ? " vditor-icon--current" : ""); + left.onclick = () => { + setAlign("left"); + }; + + const center = document.createElement("button"); + center.setAttribute("type", "button"); + center.setAttribute("aria-label", window.VditorI18n.alignCenter + "<" + updateHotkeyTip("⇧⌘C") + ">"); + center.setAttribute("data-type", "center"); + center.innerHTML = + ''; + center.className = + "vditor-icon vditor-tooltipped vditor-tooltipped__n" + + (alignType === "center" ? " vditor-icon--current" : ""); + center.onclick = () => { + setAlign("center"); + }; + + const right = document.createElement("button"); + right.setAttribute("type", "button"); + right.setAttribute("aria-label", window.VditorI18n.alignRight + "<" + updateHotkeyTip("⇧⌘R") + ">"); + right.setAttribute("data-type", "right"); + right.innerHTML = + ''; + right.className = + "vditor-icon vditor-tooltipped vditor-tooltipped__n" + + (alignType === "right" ? " vditor-icon--current" : ""); + right.onclick = () => { + setAlign("right"); + }; + + const insertRowElement = document.createElement("button"); + insertRowElement.setAttribute("type", "button"); + insertRowElement.setAttribute("aria-label", window.VditorI18n.insertRowBelow + "<" + updateHotkeyTip("⌘=") + ">"); + insertRowElement.setAttribute("data-type", "insertRow"); + insertRowElement.innerHTML = + ''; + insertRowElement.className = + "vditor-icon vditor-tooltipped vditor-tooltipped__n"; + insertRowElement.onclick = () => { + const startContainer = getSelection().getRangeAt(0) + .startContainer; + const cellElement = + hasClosestByMatchTag(startContainer, "TD") || + hasClosestByMatchTag(startContainer, "TH"); + if (cellElement) { + insertRow(vditor, range, cellElement); + } + }; + + const insertRowBElement = document.createElement("button"); + insertRowBElement.setAttribute("type", "button"); + insertRowBElement.setAttribute("aria-label", + window.VditorI18n.insertRowAbove + "<" + updateHotkeyTip("⇧⌘F") + ">"); + insertRowBElement.setAttribute("data-type", "insertRow"); + insertRowBElement.innerHTML = + ''; + insertRowBElement.className = + "vditor-icon vditor-tooltipped vditor-tooltipped__n"; + insertRowBElement.onclick = () => { + const startContainer = getSelection().getRangeAt(0) + .startContainer; + const cellElement = + hasClosestByMatchTag(startContainer, "TD") || + hasClosestByMatchTag(startContainer, "TH"); + if (cellElement) { + insertRowAbove(vditor, range, cellElement); + } + }; + + const insertColumnElement = document.createElement("button"); + insertColumnElement.setAttribute("type", "button"); + insertColumnElement.setAttribute("aria-label", window.VditorI18n.insertColumnRight + "<" + updateHotkeyTip("⇧⌘=") + ">"); + insertColumnElement.setAttribute("data-type", "insertColumn"); + insertColumnElement.innerHTML = + ''; + insertColumnElement.className = + "vditor-icon vditor-tooltipped vditor-tooltipped__n"; + insertColumnElement.onclick = () => { + const startContainer = getSelection().getRangeAt(0) + .startContainer; + const cellElement = + hasClosestByMatchTag(startContainer, "TD") || + hasClosestByMatchTag(startContainer, "TH"); + if (cellElement) { + insertColumn(vditor, tableElement, cellElement); + } + }; + + const insertColumnBElement = document.createElement("button"); + insertColumnBElement.setAttribute("type", "button"); + insertColumnBElement.setAttribute("aria-label", window.VditorI18n.insertColumnLeft + "<" + updateHotkeyTip("⇧⌘G") + ">"); + insertColumnBElement.setAttribute("data-type", "insertColumn"); + insertColumnBElement.innerHTML = + ''; + insertColumnBElement.className = + "vditor-icon vditor-tooltipped vditor-tooltipped__n"; + insertColumnBElement.onclick = () => { + const startContainer = getSelection().getRangeAt(0) + .startContainer; + const cellElement = + hasClosestByMatchTag(startContainer, "TD") || + hasClosestByMatchTag(startContainer, "TH"); + if (cellElement) { + insertColumn(vditor, tableElement, cellElement, "beforebegin"); + } + }; + + const deleteRowElement = document.createElement("button"); + deleteRowElement.setAttribute("type", "button"); + deleteRowElement.setAttribute("aria-label", window.VditorI18n["delete-row"] + "<" + updateHotkeyTip("⌘-") + ">"); + deleteRowElement.setAttribute("data-type", "deleteRow"); + deleteRowElement.innerHTML = + ''; + deleteRowElement.className = + "vditor-icon vditor-tooltipped vditor-tooltipped__n"; + deleteRowElement.onclick = () => { + const startContainer = getSelection().getRangeAt(0) + .startContainer; + const cellElement = + hasClosestByMatchTag(startContainer, "TD") || + hasClosestByMatchTag(startContainer, "TH"); + if (cellElement) { + deleteRow(vditor, range, cellElement); + } + }; + + const deleteColumnElement = document.createElement("button"); + deleteColumnElement.setAttribute("type", "button"); + deleteColumnElement.setAttribute("aria-label", window.VditorI18n["delete-column"] + "<" + updateHotkeyTip("⇧⌘-") + ">"); + deleteColumnElement.setAttribute("data-type", "deleteColumn"); + deleteColumnElement.innerHTML = + ''; + deleteColumnElement.className = + "vditor-icon vditor-tooltipped vditor-tooltipped__n"; + deleteColumnElement.onclick = () => { + const startContainer = getSelection().getRangeAt(0) + .startContainer; + const cellElement = + hasClosestByMatchTag(startContainer, "TD") || + hasClosestByMatchTag(startContainer, "TH"); + if (cellElement) { + deleteColumn(vditor, range, tableElement, cellElement); + } + }; + + const inputWrap = document.createElement("span"); + inputWrap.setAttribute("aria-label", window.VditorI18n.row); + inputWrap.className = "vditor-tooltipped vditor-tooltipped__n"; + const input = document.createElement("input"); + inputWrap.appendChild(input); + input.type = "number"; + input.min = "1"; + input.className = "vditor-input"; + input.style.width = "42px"; + input.style.textAlign = "center"; + input.setAttribute("placeholder", window.VditorI18n.row); + input.value = tableElement.rows.length.toString(); + input.oninput = () => { + updateTable(); + }; + input.onkeydown = (event) => { + if (event.isComposing) { + return; + } + if (event.key === "Tab") { + input2.focus(); + input2.select(); + event.preventDefault(); + return; + } + if (removeBlockElement(vditor, event)) { + return; + } + if (focusToElement(event, range)) { + return; + } + }; + + const input2Wrap = document.createElement("span"); + input2Wrap.setAttribute("aria-label", window.VditorI18n.column); + input2Wrap.className = "vditor-tooltipped vditor-tooltipped__n"; + const input2 = document.createElement("input"); + input2Wrap.appendChild(input2); + input2.type = "number"; + input2.min = "1"; + input2.className = "vditor-input"; + input2.style.width = "42px"; + input2.style.textAlign = "center"; + input2.setAttribute("placeholder", window.VditorI18n.column); + input2.value = tableElement.rows[0].cells.length.toString(); + input2.oninput = () => { + updateTable(); + }; + input2.onkeydown = (event) => { + if (event.isComposing) { + return; + } + if (event.key === "Tab") { + input.focus(); + input.select(); + event.preventDefault(); + return; + } + if (removeBlockElement(vditor, event)) { + return; + } + if (focusToElement(event, range)) { + return; + } + }; + + genUp(range, tableElement, vditor); + genDown(range, tableElement, vditor); + genClose(tableElement, vditor); + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", left); + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", center); + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", right); + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", insertRowBElement); + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", insertRowElement); + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", insertColumnBElement); + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", insertColumnElement); + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", deleteRowElement); + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", deleteColumnElement); + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", inputWrap); + vditor.wysiwyg.popover.insertAdjacentHTML("beforeend", " x "); + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", input2Wrap); + setPopoverPosition(vditor, tableElement); + } + + // link ref popover + const linkRefElement = hasClosestByAttribute(typeElement, "data-type", "link-ref"); + if (linkRefElement) { + genLinkRefPopover(vditor, linkRefElement, range); + } + + // footnote popover + const footnotesRefElement = hasClosestByAttribute(typeElement, "data-type", "footnotes-ref"); + if (footnotesRefElement) { + const lang: keyof II18n | "" = vditor.options.lang; + const options: IOptions = vditor.options; + vditor.wysiwyg.popover.innerHTML = ""; + + const inputWrap = document.createElement("span"); + inputWrap.setAttribute("aria-label", window.VditorI18n.footnoteRef + "<" + updateHotkeyTip("⌥Enter") + ">"); + inputWrap.className = "vditor-tooltipped vditor-tooltipped__n"; + const input = document.createElement("input"); + inputWrap.appendChild(input); + input.className = "vditor-input"; + input.setAttribute("placeholder", window.VditorI18n.footnoteRef + "<" + updateHotkeyTip("⌥Enter") + ">"); + input.style.width = "120px"; + input.value = footnotesRefElement.getAttribute("data-footnotes-label"); + input.oninput = () => { + if (input.value.trim() !== "") { + footnotesRefElement.setAttribute("data-footnotes-label", input.value); + } + if (typeof vditor.options.input === "function") { + vditor.options.input(getMarkdown(vditor)); + } + }; + input.onkeydown = (event) => { + if (event.isComposing) { + return; + } + if (removeBlockElement(vditor, event)) { + return; + } + if (focusToElement(event, range)) { + return; + } + }; + + genClose(footnotesRefElement, vditor); + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", inputWrap); + setPopoverPosition(vditor, footnotesRefElement); + } + + // block popover: math-inline, math-block, html-block, html-inline, code-block, html-entity + let blockRenderElement = hasClosestByClassName(typeElement, "vditor-wysiwyg__block") as HTMLElement; + const isBlock = blockRenderElement ? blockRenderElement.getAttribute("data-type").indexOf("block") > -1 : false; + vditor.wysiwyg.element + .querySelectorAll(".vditor-wysiwyg__preview") + .forEach((itemElement) => { + if (!blockRenderElement || (blockRenderElement && isBlock && !blockRenderElement.contains(itemElement))) { + const previousElement = itemElement.previousElementSibling as HTMLElement; + previousElement.style.display = "none"; + } + }); + if (blockRenderElement && isBlock) { + vditor.wysiwyg.popover.innerHTML = ""; + genUp(range, blockRenderElement, vditor); + genDown(range, blockRenderElement, vditor); + genClose(blockRenderElement, vditor); + + if (blockRenderElement.getAttribute("data-type") === "code-block") { + const languageWrap = document.createElement("span"); + languageWrap.setAttribute("aria-label", window.VditorI18n.language + "<" + updateHotkeyTip("⌥Enter") + ">"); + languageWrap.className = "vditor-tooltipped vditor-tooltipped__n"; + const language = document.createElement("input"); + languageWrap.appendChild(language); + + const codeElement = + blockRenderElement.firstElementChild.firstElementChild; + + language.className = "vditor-input"; + language.setAttribute("placeholder", + window.VditorI18n.language + "<" + updateHotkeyTip("⌥Enter") + ">"); + language.value = + codeElement.className.indexOf("language-") > -1 + ? codeElement.className.split("-")[1].split(" ")[0] + : ""; + language.oninput = (e: InputEvent) => { + if (language.value.trim() !== "") { + codeElement.className = `language-${language.value}`; + } else { + codeElement.className = ""; + vditor.hint.recentLanguage = ""; + } + if (blockRenderElement.lastElementChild.classList.contains("vditor-wysiwyg__preview")) { + blockRenderElement.lastElementChild.innerHTML = + blockRenderElement.firstElementChild.innerHTML; + processCodeRender(blockRenderElement.lastElementChild as HTMLElement, vditor); + } + afterRenderEvent(vditor); + // 当鼠标点选语言时,触发自定义input事件 + if (e.detail === 1) { + // 选择语言后,输入焦点切换到代码输入框 + range.setStart(codeElement.firstChild, 0); + range.collapse(true); + setSelectionFocus(range); + } + }; + language.onkeydown = (event: KeyboardEvent) => { + if (event.isComposing) { + return; + } + if (removeBlockElement(vditor, event)) { + return; + } + if ( + event.key === "Escape" && + vditor.hint.element.style.display === "block" + ) { + vditor.hint.element.style.display = "none"; + event.preventDefault(); + return; + } + vditor.hint.select(event, vditor); + focusToElement(event, range); + }; + language.onkeyup = (event: KeyboardEvent) => { + if ( + event.isComposing || + event.key === "Enter" || + event.key === "ArrowUp" || + event.key === "Escape" || + event.key === "ArrowDown" + ) { + return; + } + const matchLangData: IHintData[] = []; + const key = language.value.substring(0, language.selectionStart); + (vditor.options.preview.hljs.langs || Constants.CODE_LANGUAGES).forEach((keyName) => { + if (keyName.indexOf(key.toLowerCase()) > -1) { + matchLangData.push({ + html: keyName, + value: keyName, + }); + } + }); + vditor.hint.genHTML(matchLangData, key, vditor); + event.preventDefault(); + }; + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", languageWrap); + } + setPopoverPosition(vditor, blockRenderElement); + } else { + blockRenderElement = undefined; + } + if (headingElement) { + vditor.wysiwyg.popover.innerHTML = ""; + + const inputWrap = document.createElement("span"); + inputWrap.setAttribute("aria-label", "ID" + "<" + updateHotkeyTip("⌥Enter") + ">"); + inputWrap.className = "vditor-tooltipped vditor-tooltipped__n"; + const input = document.createElement("input"); + inputWrap.appendChild(input); + input.className = "vditor-input"; + input.setAttribute("placeholder", "ID" + "<" + updateHotkeyTip("⌥Enter") + ">"); + input.style.width = "120px"; + input.value = headingElement.getAttribute("data-id") || ""; + input.oninput = () => { + headingElement.setAttribute("data-id", input.value); + if (typeof vditor.options.input === "function") { + vditor.options.input(getMarkdown(vditor)); + } + }; + input.onkeydown = (event) => { + if (event.isComposing) { + return; + } + if (removeBlockElement(vditor, event)) { + return; + } + if (focusToElement(event, range)) { + return; + } + }; + + genUp(range, headingElement, vditor); + genDown(range, headingElement, vditor); + genClose(headingElement, vditor); + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", inputWrap); + setPopoverPosition(vditor, headingElement); + } + + // a popover + if (aElement) { + genAPopover(vditor, aElement, range); + } + + if ( + !blockquoteElement && + !liElement && + !tableElement && + !blockRenderElement && + !aElement && + !linkRefElement && + !footnotesRefElement && + !headingElement && + !tocElement + ) { + const blockElement = hasClosestByAttribute(typeElement, "data-block", "0"); + if ( + blockElement && + blockElement.parentElement.isEqualNode(vditor.wysiwyg.element) + ) { + vditor.wysiwyg.popover.innerHTML = ""; + genUp(range, blockElement, vditor); + genDown(range, blockElement, vditor); + genClose(blockElement, vditor); + + setPopoverPosition(vditor, blockElement); + } else { + vditor.wysiwyg.popover.style.display = "none"; + } + } + + // 反斜杠特殊处理 + vditor.wysiwyg.element + .querySelectorAll('span[data-type="backslash"] > span') + .forEach((item: HTMLElement) => { + item.style.display = "none"; + }); + const backslashElement = hasClosestByAttribute(range.startContainer, "data-type", "backslash"); + if (backslashElement) { + backslashElement.querySelector("span").style.display = "inline"; + } + }, 200); +}; + +const setPopoverPosition = (vditor: IVditor, element: HTMLElement) => { + let targetElement = element; + const tableElement = hasClosestByMatchTag(element, "TABLE"); + if (tableElement) { + targetElement = tableElement; + } + vditor.wysiwyg.popover.style.left = "0"; + vditor.wysiwyg.popover.style.display = "block"; + vditor.wysiwyg.popover.style.top = + Math.max(-8, targetElement.offsetTop - 21 - vditor.wysiwyg.element.scrollTop) + "px"; + vditor.wysiwyg.popover.style.left = + Math.min(targetElement.offsetLeft, vditor.wysiwyg.element.clientWidth - vditor.wysiwyg.popover.clientWidth) + "px"; + vditor.wysiwyg.popover.setAttribute("data-top", (targetElement.offsetTop - 21).toString()); +}; + +export const genLinkRefPopover = (vditor: IVditor, linkRefElement: HTMLElement, range = getSelection().getRangeAt(0)) => { + vditor.wysiwyg.popover.innerHTML = ""; + const updateLinkRef = () => { + if (input.value.trim() !== "") { + if (linkRefElement.tagName === "IMG") { + linkRefElement.setAttribute("alt", input.value); + } else { + linkRefElement.textContent = input.value; + } + } + // data-link-label + if (input1.value.trim() !== "") { + linkRefElement.setAttribute("data-link-label", input1.value); + } + if (typeof vditor.options.input === "function") { + vditor.options.input(getMarkdown(vditor)); + } + }; + + const inputWrap = document.createElement("span"); + inputWrap.setAttribute("aria-label", window.VditorI18n.textIsNotEmpty); + inputWrap.className = "vditor-tooltipped vditor-tooltipped__n"; + const input = document.createElement("input"); + inputWrap.appendChild(input); + input.className = "vditor-input"; + input.setAttribute("placeholder", window.VditorI18n.textIsNotEmpty); + input.style.width = "120px"; + input.value = + linkRefElement.getAttribute("alt") || linkRefElement.textContent; + input.oninput = () => { + updateLinkRef(); + }; + input.onkeydown = (event) => { + if (removeBlockElement(vditor, event)) { + return; + } + if (focusToElement(event, range)) { + return; + } + linkHotkey(vditor, linkRefElement, event, input1); + }; + + const input1Wrap = document.createElement("span"); + input1Wrap.setAttribute("aria-label", window.VditorI18n.linkRef); + input1Wrap.className = "vditor-tooltipped vditor-tooltipped__n"; + const input1 = document.createElement("input"); + input1Wrap.appendChild(input1); + input1.className = "vditor-input"; + input1.setAttribute("placeholder", window.VditorI18n.linkRef); + input1.value = linkRefElement.getAttribute("data-link-label"); + input1.oninput = () => { + updateLinkRef(); + }; + input1.onkeydown = (event) => { + if (removeBlockElement(vditor, event)) { + return; + } + if (focusToElement(event, range)) { + return; + } + linkHotkey(vditor, linkRefElement, event, input); + }; + + genClose(linkRefElement, vditor); + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", inputWrap); + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", input1Wrap); + setPopoverPosition(vditor, linkRefElement); +}; + +const genUp = (range: Range, element: HTMLElement, vditor: IVditor) => { + const previousElement = element.previousElementSibling; + if ( + !previousElement || + (!element.parentElement.isEqualNode(vditor.wysiwyg.element) && + element.tagName !== "LI") + ) { + return; + } + const upElement = document.createElement("button"); + upElement.setAttribute("type", "button"); + upElement.setAttribute("data-type", "up"); + upElement.setAttribute("aria-label", window.VditorI18n.up + "<" + updateHotkeyTip("⇧⌘U") + ">"); + upElement.innerHTML = ''; + upElement.className = "vditor-icon vditor-tooltipped vditor-tooltipped__n"; + upElement.onclick = () => { + range.insertNode(document.createElement("wbr")); + previousElement.insertAdjacentElement("beforebegin", element); + setRangeByWbr(vditor.wysiwyg.element, range); + afterRenderEvent(vditor); + highlightToolbarWYSIWYG(vditor); + scrollCenter(vditor); + }; + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", upElement); +}; + +const genDown = (range: Range, element: HTMLElement, vditor: IVditor) => { + const nextElement = element.nextElementSibling; + if ( + !nextElement || + (!element.parentElement.isEqualNode(vditor.wysiwyg.element) && + element.tagName !== "LI") + ) { + return; + } + const downElement = document.createElement("button"); + downElement.setAttribute("type", "button"); + downElement.setAttribute("data-type", "down"); + downElement.setAttribute("aria-label", window.VditorI18n.down + "<" + updateHotkeyTip("⇧⌘D") + ">"); + downElement.innerHTML = + ''; + downElement.className = + "vditor-icon vditor-tooltipped vditor-tooltipped__n"; + downElement.onclick = () => { + range.insertNode(document.createElement("wbr")); + nextElement.insertAdjacentElement("afterend", element); + setRangeByWbr(vditor.wysiwyg.element, range); + afterRenderEvent(vditor); + highlightToolbarWYSIWYG(vditor); + scrollCenter(vditor); + }; + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", downElement); +}; + +const genClose = (element: HTMLElement, vditor: IVditor) => { + const close = document.createElement("button"); + close.setAttribute("type", "button"); + close.setAttribute("data-type", "remove"); + close.setAttribute("aria-label", window.VditorI18n.remove + "<" + updateHotkeyTip("⇧⌘X") + ">"); + close.innerHTML = + ''; + close.className = "vditor-icon vditor-tooltipped vditor-tooltipped__n"; + close.onclick = () => { + const range = getEditorRange(vditor); + range.setStartAfter(element); + setSelectionFocus(range); + element.remove(); + afterRenderEvent(vditor); + highlightToolbarWYSIWYG(vditor); + if (["H1", "H2", "H3", "H4", "H5", "H6"].includes(element.tagName)) { + renderToc(vditor); + } + }; + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", close); +}; + +const linkHotkey = ( + vditor: IVditor, + element: HTMLElement, + event: KeyboardEvent, + nextInputElement: HTMLInputElement, +) => { + if (event.isComposing) { + return; + } + if (event.key === "Tab") { + nextInputElement.focus(); + nextInputElement.select(); + event.preventDefault(); + return; + } + if ( + !isCtrl(event) && + !event.shiftKey && + event.altKey && + event.key === "Enter" + ) { + const range = getEditorRange(vditor); + // firefox 不会打断 link https://github.com/Vanessa219/vditor/issues/193 + element.insertAdjacentHTML("afterend", Constants.ZWSP); + range.setStartAfter(element.nextSibling); + range.collapse(true); + setSelectionFocus(range); + event.preventDefault(); + } +}; + +export const genAPopover = (vditor: IVditor, aElement: HTMLElement, range: Range) => { + vditor.wysiwyg.popover.innerHTML = ""; + + const updateA = () => { + if (input.value.trim() !== "") { + aElement.innerHTML = input.value; + } + aElement.setAttribute("href", input1.value); + aElement.setAttribute("title", input2.value); + afterRenderEvent(vditor); + }; + + aElement.querySelectorAll("[data-marker]").forEach((item: HTMLElement) => { + item.removeAttribute("data-marker"); + }); + const inputWrap = document.createElement("span"); + inputWrap.setAttribute("aria-label", window.VditorI18n.textIsNotEmpty); + inputWrap.className = "vditor-tooltipped vditor-tooltipped__n"; + const input = document.createElement("input"); + inputWrap.appendChild(input); + input.className = "vditor-input"; + input.setAttribute("placeholder", window.VditorI18n.textIsNotEmpty); + input.style.width = "120px"; + input.value = aElement.innerHTML || ""; + input.oninput = () => { + updateA(); + }; + input.onkeydown = (event) => { + if (removeBlockElement(vditor, event)) { + return; + } + if (focusToElement(event, range)) { + return; + } + linkHotkey(vditor, aElement, event, input1); + }; + + const input1Wrap = document.createElement("span"); + input1Wrap.setAttribute("aria-label", window.VditorI18n.link); + input1Wrap.className = "vditor-tooltipped vditor-tooltipped__n"; + const input1 = document.createElement("input"); + input1Wrap.appendChild(input1); + input1.className = "vditor-input"; + input1.setAttribute("placeholder", window.VditorI18n.link); + input1.value = aElement.getAttribute("href") || ""; + input1.oninput = () => { + updateA(); + }; + input1.onkeydown = (event) => { + if (removeBlockElement(vditor, event)) { + return; + } + if (focusToElement(event, range)) { + return; + } + linkHotkey(vditor, aElement, event, input2); + }; + + const input2Wrap = document.createElement("span"); + input2Wrap.setAttribute("aria-label", window.VditorI18n.tooltipText); + input2Wrap.className = "vditor-tooltipped vditor-tooltipped__n"; + const input2 = document.createElement("input"); + input2Wrap.appendChild(input2); + input2.className = "vditor-input"; + input2.setAttribute("placeholder", window.VditorI18n.tooltipText); + input2.style.width = "60px"; + input2.value = aElement.getAttribute("title") || ""; + input2.oninput = () => { + updateA(); + }; + input2.onkeydown = (event) => { + if (removeBlockElement(vditor, event)) { + return; + } + if (focusToElement(event, range)) { + return; + } + linkHotkey(vditor, aElement, event, input); + }; + + genClose(aElement, vditor); + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", inputWrap); + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", input1Wrap); + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", input2Wrap); + setPopoverPosition(vditor, aElement); +}; + +export const genImagePopover = (event: Event, vditor: IVditor) => { + const imgElement = event.target as HTMLImageElement; + vditor.wysiwyg.popover.innerHTML = ""; + const updateImg = () => { + imgElement.setAttribute("src", inputElement.value); + imgElement.setAttribute("alt", alt.value); + imgElement.setAttribute("title", title.value); + if (typeof vditor.options.input === "function") { + vditor.options.input(getMarkdown(vditor)); + } + }; + + const inputWrap = document.createElement("span"); + inputWrap.setAttribute("aria-label", window.VditorI18n.imageURL); + inputWrap.className = "vditor-tooltipped vditor-tooltipped__n"; + const inputElement = document.createElement("input"); + inputWrap.appendChild(inputElement); + inputElement.className = "vditor-input"; + inputElement.setAttribute("placeholder", window.VditorI18n.imageURL); + inputElement.value = imgElement.getAttribute("src") || ""; + inputElement.oninput = () => { + updateImg(); + }; + inputElement.onkeydown = (elementEvent) => { + removeBlockElement(vditor, elementEvent); + }; + + const altWrap = document.createElement("span"); + altWrap.setAttribute("aria-label", window.VditorI18n.alternateText); + altWrap.className = "vditor-tooltipped vditor-tooltipped__n"; + const alt = document.createElement("input"); + altWrap.appendChild(alt); + alt.className = "vditor-input"; + alt.setAttribute("placeholder", window.VditorI18n.alternateText); + alt.style.width = "52px"; + alt.value = imgElement.getAttribute("alt") || ""; + alt.oninput = () => { + updateImg(); + }; + alt.onkeydown = (elementEvent) => { + removeBlockElement(vditor, elementEvent); + }; + + const titleWrap = document.createElement("span"); + titleWrap.setAttribute("aria-label", window.VditorI18n.title); + titleWrap.className = "vditor-tooltipped vditor-tooltipped__n"; + const title = document.createElement("input"); + titleWrap.appendChild(title); + title.className = "vditor-input"; + title.setAttribute("placeholder", window.VditorI18n.title); + title.value = imgElement.getAttribute("title") || ""; + title.oninput = () => { + updateImg(); + }; + title.onkeydown = (elementEvent) => { + removeBlockElement(vditor, elementEvent); + }; + genClose(imgElement, vditor); + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", inputWrap); + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", altWrap); + vditor.wysiwyg.popover.insertAdjacentElement("beforeend", titleWrap); + + setPopoverPosition(vditor, imgElement); +}; + + +const focusToElement = (event: KeyboardEvent, range: Range) => { + if ((!isCtrl(event) && !event.shiftKey && event.key === "Enter") || event.key === "Escape") { + if (range) { + setSelectionFocus(range); + } + event.preventDefault(); + event.stopPropagation(); + return true; + } +}; diff --git a/web/include/vditor/src/ts/wysiwyg/index.ts b/web/include/vditor/src/ts/wysiwyg/index.ts new file mode 100644 index 0000000..bd3aabe --- /dev/null +++ b/web/include/vditor/src/ts/wysiwyg/index.ts @@ -0,0 +1,575 @@ +import {Constants} from "../constants"; +import {hidePanel} from "../toolbar/setToolbar"; +import {isCtrl, isFirefox} from "../util/compatibility"; +import { + blurEvent, + copyEvent, cutEvent, dblclickEvent, + dropEvent, + focusEvent, + hotkeyEvent, + scrollCenter, + selectEvent, +} from "../util/editorCommonEvent"; +import {isHeadingMD, isHrMD, paste} from "../util/fixBrowserBehavior"; +import { + hasClosestBlock, hasClosestByAttribute, + hasClosestByClassName, hasClosestByMatchTag, +} from "../util/hasClosest"; +import {hasClosestByHeadings} from "../util/hasClosestByHeadings"; +import { + getCursorPosition, + getEditorRange, + 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"; +import {input} from "./input"; +import {showCode} from "./showCode"; +import {getMarkdown} from "../markdown/getMarkdown"; + +class WYSIWYG { + public range: Range; + public element: HTMLPreElement; + public popover: HTMLDivElement; + public selectPopover: HTMLDivElement; + public afterRenderTimeoutId: number; + public hlToolbarTimeoutId: number; + public preventInput: boolean; + public composingLock = false; + public commentIds: string[] = []; + private scrollListener: () => void; + + constructor(vditor: IVditor) { + const divElement = document.createElement("div"); + divElement.className = "vditor-wysiwyg"; + + divElement.innerHTML = `

    
    +
    +
    + +
    `; + + this.element = divElement.firstElementChild as HTMLPreElement; + this.popover = divElement.firstElementChild.nextElementSibling as HTMLDivElement; + this.selectPopover = divElement.lastElementChild as HTMLDivElement; + + this.bindEvent(vditor); + + focusEvent(vditor, this.element); + dblclickEvent(vditor, this.element); + blurEvent(vditor, this.element); + hotkeyEvent(vditor, this.element); + selectEvent(vditor, this.element); + dropEvent(vditor, this.element); + copyEvent(vditor, this.element, this.copy); + cutEvent(vditor, this.element, this.copy); + + if (vditor.options.comment.enable) { + this.selectPopover.querySelector("button").onclick = () => { + const id = Lute.NewNodeID(); + const range = getSelection().getRangeAt(0); + const rangeClone = range.cloneRange(); + const contents = range.extractContents(); + let blockStartElement: HTMLElement; + let blockEndElement: HTMLElement; + let removeStart = false; + let removeEnd = false; + contents.childNodes.forEach((item: HTMLElement, index: number) => { + let wrap = false; + if (item.nodeType === 3) { + wrap = true; + } else if (!item.classList.contains("vditor-comment")) { + wrap = true; + } else if (item.classList.contains("vditor-comment")) { + item.setAttribute("data-cmtids", item.getAttribute("data-cmtids") + " " + id); + } + if (wrap) { + if (item.nodeType !== 3 && item.getAttribute("data-block") === "0" + && index === 0 && rangeClone.startOffset > 0) { + item.innerHTML = + `${item.innerHTML}`; + blockStartElement = item; + } else if (item.nodeType !== 3 && item.getAttribute("data-block") === "0" + && index === contents.childNodes.length - 1 + && rangeClone.endOffset < rangeClone.endContainer.textContent.length) { + item.innerHTML = + `${item.innerHTML}`; + blockEndElement = item; + } else if (item.nodeType !== 3 && item.getAttribute("data-block") === "0") { + if (index === 0) { + removeStart = true; + } else if (index === contents.childNodes.length - 1) { + removeEnd = true; + } + item.innerHTML = + `${item.innerHTML}`; + } else { + const commentElement = document.createElement("span"); + commentElement.classList.add("vditor-comment"); + commentElement.setAttribute("data-cmtids", id); + item.parentNode.insertBefore(commentElement, item); + commentElement.appendChild(item); + } + } + }); + const startElement = hasClosestBlock(rangeClone.startContainer); + if (startElement) { + if (blockStartElement) { + startElement.insertAdjacentHTML("beforeend", blockStartElement.innerHTML); + blockStartElement.remove(); + } else if (startElement.textContent.trim().replace(Constants.ZWSP, "") === "" && removeStart) { + startElement.remove(); + } + } + const endElement = hasClosestBlock(rangeClone.endContainer); + if (endElement) { + if (blockEndElement) { + endElement.insertAdjacentHTML("afterbegin", blockEndElement.innerHTML); + blockEndElement.remove(); + } else if (endElement.textContent.trim().replace(Constants.ZWSP, "") === "" && removeEnd) { + endElement.remove(); + } + } + range.insertNode(contents); + vditor.options.comment.add(id, range.toString(), this.getComments(vditor, true)); + afterRenderEvent(vditor, { + enableAddUndoStack: true, + enableHint: false, + enableInput: false, + }); + this.hideComment(); + }; + } + } + + public getComments(vditor: IVditor, getData = false) { + if (vditor.currentMode === "wysiwyg" && vditor.options.comment.enable) { + this.commentIds = []; + this.element.querySelectorAll(".vditor-comment").forEach((item) => { + this.commentIds = + this.commentIds.concat(item.getAttribute("data-cmtids").split(" ")); + }); + this.commentIds = Array.from(new Set(this.commentIds)); + + const comments: ICommentsData[] = []; + if (getData) { + this.commentIds.forEach((id) => { + comments.push({ + id, + top: + (this.element.querySelector(`.vditor-comment[data-cmtids="${id}"]`) as HTMLElement).offsetTop, + }); + }); + return comments; + } + } else { + return []; + } + } + + public triggerRemoveComment(vditor: IVditor) { + const difference = (a: string[], b: string[]) => { + const s = new Set(b); + return a.filter((x) => !s.has(x)); + }; + if (vditor.currentMode === "wysiwyg" && vditor.options.comment.enable && vditor.wysiwyg.commentIds.length > 0) { + const oldIds = JSON.parse(JSON.stringify(this.commentIds)); + this.getComments(vditor); + const removedIds = difference(oldIds, this.commentIds); + if (removedIds.length > 0) { + vditor.options.comment.remove(removedIds); + } + } + } + + public showComment() { + const position = getCursorPosition(this.element); + this.selectPopover.setAttribute("style", `left:${position.left}px;display:block;top:${Math.max(-8, position.top - 21)}px`); + } + + public hideComment() { + this.selectPopover.setAttribute("style", "display:none"); + } + + public unbindListener() { + window.removeEventListener("scroll", this.scrollListener); + } + + private copy(event: ClipboardEvent, vditor: IVditor) { + const range = getSelection().getRangeAt(0); + if (range.toString() === "") { + return; + } + event.stopPropagation(); + event.preventDefault(); + + const codeElement = hasClosestByMatchTag(range.startContainer, "CODE"); + const codeEndElement = hasClosestByMatchTag(range.endContainer, "CODE"); + if (codeElement && codeEndElement && codeEndElement.isSameNode(codeElement)) { + let codeText = ""; + if (codeElement.parentElement.tagName === "PRE") { + codeText = range.toString(); + } else { + codeText = "`" + range.toString() + "`"; + } + event.clipboardData.setData("text/plain", codeText); + event.clipboardData.setData("text/html", ""); + return; + } + + const aElement = hasClosestByMatchTag(range.startContainer, "A"); + const aEndElement = hasClosestByMatchTag(range.endContainer, "A"); + if (aElement && aEndElement && aEndElement.isSameNode(aElement)) { + let aTitle = aElement.getAttribute("title") || ""; + if (aTitle) { + aTitle = ` "${aTitle}"`; + } + event.clipboardData.setData("text/plain", + `[${range.toString()}](${aElement.getAttribute("href")}${aTitle})`); + event.clipboardData.setData("text/html", ""); + return; + } + + const tempElement = document.createElement("div"); + tempElement.appendChild(range.cloneContents()); + + event.clipboardData.setData("text/plain", vditor.lute.VditorDOM2Md(tempElement.innerHTML).trim()); + event.clipboardData.setData("text/html", ""); + } + + private bindEvent(vditor: IVditor) { + this.unbindListener(); + window.addEventListener("scroll", this.scrollListener = () => { + hidePanel(vditor, ["hint"]); + if (this.popover.style.display !== "block" || this.selectPopover.style.display !== "block") { + return; + } + const top = parseInt(this.popover.getAttribute("data-top"), 10); + if (vditor.options.height !== "auto") { + if (vditor.options.toolbarConfig.pin && vditor.toolbar.element.getBoundingClientRect().top === 0) { + const popoverTop = Math.max(window.scrollY - vditor.element.offsetTop - 8, + Math.min(top - vditor.wysiwyg.element.scrollTop, this.element.clientHeight - 21)) + "px"; + if (this.popover.style.display === "block") { + this.popover.style.top = popoverTop; + } + if (this.selectPopover.style.display === "block") { + this.selectPopover.style.top = popoverTop; + } + } + return; + } else if (!vditor.options.toolbarConfig.pin) { + return; + } + const popoverTop1 = Math.max(top, (window.scrollY - vditor.element.offsetTop - 8)) + "px"; + if (this.popover.style.display === "block") { + this.popover.style.top = popoverTop1; + } + if (this.selectPopover.style.display === "block") { + this.selectPopover.style.top = popoverTop1; + } + }); + + this.element.addEventListener("scroll", () => { + hidePanel(vditor, ["hint"]); + if (vditor.options.comment && vditor.options.comment.enable && vditor.options.comment.scroll) { + vditor.options.comment.scroll(vditor.wysiwyg.element.scrollTop); + } + if (this.popover.style.display !== "block") { + return; + } + const top = parseInt(this.popover.getAttribute("data-top"), 10) - vditor.wysiwyg.element.scrollTop; + let max = -8; + if (vditor.options.toolbarConfig.pin && vditor.toolbar.element.getBoundingClientRect().top === 0) { + max = window.scrollY - vditor.element.offsetTop + max; + } + const topPx = Math.max(max, Math.min(top, this.element.clientHeight - 21)) + "px"; + this.popover.style.top = topPx; + this.selectPopover.style.top = topPx; + }); + + this.element.addEventListener("paste", (event: ClipboardEvent & { target: HTMLElement }) => { + paste(vditor, event, { + pasteCode: (code: string) => { + const range = getEditorRange(vditor); + const node = document.createElement("template"); + node.innerHTML = code; + range.insertNode(node.content.cloneNode(true)); + const blockElement = hasClosestByAttribute(range.startContainer, "data-block", "0"); + if (blockElement) { + blockElement.outerHTML = vditor.lute.SpinVditorDOM(blockElement.outerHTML); + } else { + vditor.wysiwyg.element.innerHTML = vditor.lute.SpinVditorDOM(vditor.wysiwyg.element.innerHTML); + } + setRangeByWbr(vditor.wysiwyg.element, range); + }, + }); + }); + + // 中文处理 + this.element.addEventListener("compositionstart", () => { + this.composingLock = true; + }); + + this.element.addEventListener("compositionend", (event: InputEvent) => { + const headingElement = hasClosestByHeadings(getSelection().getRangeAt(0).startContainer); + if (headingElement && headingElement.textContent === "") { + // heading 为空删除 https://github.com/Vanessa219/vditor/issues/150 + renderToc(vditor); + return; + } + if (!isFirefox()) { + input(vditor, getSelection().getRangeAt(0).cloneRange(), event); + } + this.composingLock = false; + }); + + this.element.addEventListener("input", (event: InputEvent) => { + if (event.inputType === "deleteByDrag" || event.inputType === "insertFromDrop") { + // https://github.com/Vanessa219/vditor/issues/801 编辑器内容拖拽问题 + return; + } + if (this.preventInput) { + this.preventInput = false; + afterRenderEvent(vditor); + return; + } + if (this.composingLock || event.data === "‘" || event.data === "“" || event.data === "《") { + afterRenderEvent(vditor); + return; + } + const range = getSelection().getRangeAt(0); + let blockElement = hasClosestBlock(range.startContainer); + if (!blockElement) { + // 没有被块元素包裹 + modifyPre(vditor, range); + blockElement = hasClosestBlock(range.startContainer); + } + if (!blockElement) { + return; + } + + // 前后空格处理 + const startOffset = getSelectPosition(blockElement, vditor.wysiwyg.element, range).start; + + // 开始可以输入空格 + let startSpace = true; + for (let i = startOffset - 1; i > blockElement.textContent.substr(0, startOffset).lastIndexOf("\n"); i--) { + if (blockElement.textContent.charAt(i) !== " " && + // 多个 tab 前删除不形成代码块 https://github.com/Vanessa219/vditor/issues/162 1 + blockElement.textContent.charAt(i) !== "\t") { + startSpace = false; + break; + } + } + if (startOffset === 0) { + startSpace = false; + } + + // 结尾可以输入空格 + let endSpace = true; + for (let i = startOffset - 1; i < blockElement.textContent.length; i++) { + if (blockElement.textContent.charAt(i) !== " " && blockElement.textContent.charAt(i) !== "\n") { + endSpace = false; + break; + } + } + + // https://github.com/Vanessa219/vditor/issues/729 + if (endSpace && /^#{1,6} $/.test(blockElement.textContent)) { + endSpace = false; + } + + const headingElement = hasClosestByHeadings(getSelection().getRangeAt(0).startContainer); + if (headingElement && headingElement.textContent === "") { + // heading 为空删除 https://github.com/Vanessa219/vditor/issues/150 + renderToc(vditor); + headingElement.remove(); + } + + if ((startSpace && blockElement.getAttribute("data-type") !== "code-block") + || endSpace || isHeadingMD(blockElement.innerHTML) || + (isHrMD(blockElement.innerHTML) && blockElement.previousElementSibling)) { + if (typeof vditor.options.input === "function") { + vditor.options.input(getMarkdown(vditor)); + } + return; + } + // https://github.com/Vanessa219/vditor/issues/1565 + if (event.inputType === "insertParagraph" && this.element.innerHTML === '



    ') { + blockElement.previousElementSibling.remove(); + } + + input(vditor, range, event); + }); + + this.element.addEventListener("click", (event: MouseEvent & { target: HTMLElement }) => { + if (event.target.tagName === "INPUT") { + const checkElement = event.target as HTMLInputElement; + if (checkElement.checked) { + checkElement.setAttribute("checked", "checked"); + } else { + checkElement.removeAttribute("checked"); + } + this.preventInput = true; + afterRenderEvent(vditor); + return; + } + + if (event.target.tagName === "IMG" && + // plantuml 图片渲染不进行提示 + !event.target.parentElement.classList.contains("vditor-wysiwyg__preview")) { + if (event.target.getAttribute("data-type") === "link-ref") { + genLinkRefPopover(vditor, event.target); + } else { + genImagePopover(event, vditor); + } + return; + } + + // 打开链接 + const a = hasClosestByMatchTag(event.target, "A"); + if (a) { + if (vditor.options.link.click) { + vditor.options.link.click(a); + } else if (vditor.options.link.isOpen) { + window.open(a.getAttribute("href")); + } + event.preventDefault(); + return; + } + + const range = getEditorRange(vditor); + if (event.target.isEqualNode(this.element) && this.element.lastElementChild && range.collapsed) { + const lastRect = this.element.lastElementChild.getBoundingClientRect(); + if (event.y > lastRect.top + lastRect.height) { + if (this.element.lastElementChild.tagName === "P" && + this.element.lastElementChild.textContent.trim().replace(Constants.ZWSP, "") === "") { + range.selectNodeContents(this.element.lastElementChild); + range.collapse(false); + } else { + this.element.insertAdjacentHTML("beforeend", + `

    ${Constants.ZWSP}

    `); + setRangeByWbr(this.element, range); + } + } + } + + highlightToolbarWYSIWYG(vditor); + + // 点击后光标落于预览区,需展开代码块 + let previewElement = hasClosestByClassName(event.target, "vditor-wysiwyg__preview"); + if (!previewElement) { + previewElement = + hasClosestByClassName(getEditorRange(vditor).startContainer, "vditor-wysiwyg__preview"); + } + if (previewElement) { + showCode(previewElement, vditor); + } + + clickToc(event, vditor); + }); + + this.element.addEventListener("keyup", (event: KeyboardEvent & { target: HTMLElement }) => { + if (event.isComposing || isCtrl(event)) { + return; + } + // 除 md 处理、cell 内换行、table 添加新行/列、代码块语言切换、block render 换行、跳出/逐层跳出 blockquote、h6 换行、 + // 任务列表换行、软换行外需在换行时调整文档位置 + if (event.key === "Enter") { + scrollCenter(vditor); + } + if ((event.key === "Backspace" || event.key === "Delete") && + vditor.wysiwyg.element.innerHTML !== "" && vditor.wysiwyg.element.childNodes.length === 1 && + vditor.wysiwyg.element.firstElementChild && vditor.wysiwyg.element.firstElementChild.tagName === "P" + && vditor.wysiwyg.element.firstElementChild.childElementCount === 0 + && (vditor.wysiwyg.element.textContent === "" || vditor.wysiwyg.element.textContent === "\n")) { + // 为空时显示 placeholder + vditor.wysiwyg.element.innerHTML = ""; + } + const range = getEditorRange(vditor); + if (event.key === "Backspace") { + // firefox headings https://github.com/Vanessa219/vditor/issues/211 + if (isFirefox() && range.startContainer.textContent === "\n" && range.startOffset === 1) { + range.startContainer.textContent = ""; + } + } + + // 没有被块元素包裹 + modifyPre(vditor, range); + + highlightToolbarWYSIWYG(vditor); + + if (event.key !== "ArrowDown" && event.key !== "ArrowRight" && event.key !== "Backspace" + && event.key !== "ArrowLeft" && event.key !== "ArrowUp") { + return; + } + + if (event.key === "ArrowLeft" || event.key === "ArrowRight") { + vditor.hint.render(vditor); + } + + // 上下左右,删除遇到块预览的处理 + let previewElement = hasClosestByClassName(range.startContainer, "vditor-wysiwyg__preview"); + if (!previewElement && range.startContainer.nodeType !== 3 && range.startOffset > 0) { + // table 前删除遇到代码块 + const blockRenderElement = range.startContainer as HTMLElement; + if (blockRenderElement.classList.contains("vditor-wysiwyg__block")) { + previewElement = blockRenderElement.lastElementChild as HTMLElement; + } + } + if (!previewElement) { + return; + } + const previousElement = previewElement.previousElementSibling as HTMLElement; + if (previousElement.style.display === "none") { + if (event.key === "ArrowDown" || event.key === "ArrowRight") { + showCode(previewElement, vditor); + } else { + showCode(previewElement, vditor, false); + } + return; + } + + let codeElement = previewElement.previousElementSibling as HTMLElement; + if (codeElement.tagName === "PRE") { + codeElement = codeElement.firstElementChild as HTMLElement; + } + + if (event.key === "ArrowDown" || event.key === "ArrowRight") { + const blockRenderElement = previewElement.parentElement; + let nextNode = getRenderElementNextNode(blockRenderElement) as HTMLElement; + if (nextNode && nextNode.nodeType !== 3) { + // 下一节点依旧为代码渲染块 + const nextRenderElement = nextNode.querySelector(".vditor-wysiwyg__preview") as HTMLElement; + if (nextRenderElement) { + showCode(nextRenderElement, vditor); + return; + } + } + // 跳过渲染块,光标移动到下一个节点 + if (nextNode.nodeType === 3) { + // inline + while (nextNode.textContent.length === 0 && nextNode.nextSibling) { + // https://github.com/Vanessa219/vditor/issues/100 2 + nextNode = nextNode.nextSibling as HTMLElement; + } + range.setStart(nextNode, 1); + } else { + // block + range.setStart(nextNode.firstChild, 0); + } + } else { + range.selectNodeContents(codeElement); + range.collapse(false); + } + }); + } +} + +export {WYSIWYG}; diff --git a/web/include/vditor/src/ts/wysiwyg/inlineTag.ts b/web/include/vditor/src/ts/wysiwyg/inlineTag.ts new file mode 100644 index 0000000..c409629 --- /dev/null +++ b/web/include/vditor/src/ts/wysiwyg/inlineTag.ts @@ -0,0 +1,129 @@ +import {Constants} from "../constants"; +import {setRangeByWbr, setSelectionFocus} from "../util/selection"; + +export const previoueIsEmptyA = (node: Node) => { + let previousNode = node.previousSibling as HTMLElement; + while (previousNode) { + if (previousNode.nodeType !== 3 && previousNode.tagName === "A" && !previousNode.previousSibling + && previousNode.innerHTML.replace(Constants.ZWSP, "") === "" && previousNode.nextSibling) { + return previousNode; + } + previousNode = previousNode.previousSibling as HTMLElement; + } + return false; +}; + +export const nextIsCode = (range: Range) => { + let nextNode: HTMLElement = range.startContainer.nextSibling as HTMLElement; + while (nextNode && nextNode.textContent === "") { + nextNode = nextNode.nextSibling as HTMLElement; + } + + if (nextNode && nextNode.nodeType !== 3 && (nextNode.tagName === "CODE" || + nextNode.getAttribute("data-type") === "math-inline" || + nextNode.getAttribute("data-type") === "html-entity" || + nextNode.getAttribute("data-type") === "html-inline") + ) { + return true; + } + return false; +}; + +export const getNextHTML = (node: Node) => { + let html = ""; + let nextNode = node.nextSibling; + while (nextNode) { + if (nextNode.nodeType === 3) { + html += nextNode.textContent; + } else { + html += (nextNode as HTMLElement).outerHTML; + } + nextNode = nextNode.nextSibling; + } + return html; +}; + +export const getPreviousHTML = (node: Node) => { + let html = ""; + let previousNode = node.previousSibling; + while (previousNode) { + if (previousNode.nodeType === 3) { + html = previousNode.textContent + html; + } else { + html = (previousNode as HTMLElement).outerHTML + html; + } + previousNode = previousNode.previousSibling; + } + return html; +}; + +export const getRenderElementNextNode = (blockCodeElement: HTMLElement) => { + let nextNode = blockCodeElement; + while (nextNode && !nextNode.nextSibling) { + nextNode = nextNode.parentElement; + } + + return nextNode.nextSibling; +}; + +export const splitElement = (range: Range) => { + const previousHTML = getPreviousHTML(range.startContainer); + const nextHTML = getNextHTML(range.startContainer); + const text = range.startContainer.textContent; + const offset = range.startOffset; + + let beforeHTML = ""; + let afterHTML = ""; + + if (text.substr(0, offset) !== "" && text.substr(0, offset) !== Constants.ZWSP || previousHTML) { + beforeHTML = `${previousHTML}${text.substr(0, offset)}`; + } + if (text.substr(offset) !== "" && text.substr(offset) !== Constants.ZWSP || nextHTML) { + afterHTML = `${text.substr(offset)}${nextHTML}`; + } + + return { + afterHTML, + beforeHTML, + }; +}; + +export const modifyPre = (vditor: IVditor, range: Range) => { + // 没有被块元素包裹 + Array.from(vditor.wysiwyg.element.childNodes).find((node: HTMLElement) => { + if (node.nodeType === 3) { + const pElement = document.createElement("p"); + pElement.setAttribute("data-block", "0"); + pElement.textContent = node.textContent; + // 为空按下 tab 且 tab = ' ' 时,range.startContainer 不为 node + const cloneRangeOffset = range.startContainer.nodeType === 3 ? range.startOffset : node.textContent.length; + node.parentNode.insertBefore(pElement, node); + node.remove(); + range.setStart(pElement.firstChild, Math.min(pElement.firstChild.textContent.length, cloneRangeOffset)); + range.collapse(true); + setSelectionFocus(range); + return true; + } else if (!node.getAttribute("data-block")) { + if (node.tagName === "P") { + node.remove(); + } else { + if (node.tagName === "DIV") { + range.insertNode(document.createElement("wbr")); + // firefox 列表换行产生 div + node.outerHTML = `

    ${node.innerHTML}

    `; + } else { + if (node.tagName === "BR") { + // firefox 空换行产生 BR + node.outerHTML = `

    ${node.outerHTML}

    `; + } else { + range.insertNode(document.createElement("wbr")); + node.outerHTML = `

    ${node.outerHTML}

    `; + } + } + setRangeByWbr(vditor.wysiwyg.element, range); + range = getSelection().getRangeAt(0); + } + return true; + } + }); +}; diff --git a/web/include/vditor/src/ts/wysiwyg/input.ts b/web/include/vditor/src/ts/wysiwyg/input.ts new file mode 100644 index 0000000..bccdcd4 --- /dev/null +++ b/web/include/vditor/src/ts/wysiwyg/input.ts @@ -0,0 +1,212 @@ +import { + getTopList, + hasClosestBlock, hasClosestByAttribute, hasTopClosestByTag, +} from "../util/hasClosest"; +import {hasClosestByTag} from "../util/hasClosestByHeadings"; +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"; + +export const input = (vditor: IVditor, range: Range, event?: InputEvent) => { + let blockElement = hasClosestBlock(range.startContainer); + + if (!blockElement) { + // 使用顶级块元素,应使用 innerHTML + blockElement = vditor.wysiwyg.element; + } + + if (event && event.inputType !== "formatItalic" + && event.inputType !== "deleteByDrag" + && event.inputType !== "insertFromDrop" + && event.inputType !== "formatBold" + && event.inputType !== "formatRemove" + && event.inputType !== "formatStrikeThrough" + && event.inputType !== "insertUnorderedList" + && event.inputType !== "insertOrderedList" + && event.inputType !== "formatOutdent" + && event.inputType !== "formatIndent" + && event.inputType !== "" // document.execCommand('unlink', false) + || !event + ) { + const previousAEmptyElement = previoueIsEmptyA(range.startContainer); + if (previousAEmptyElement) { + // 链接结尾回车不应该复制到下一行 https://github.com/Vanessa219/vditor/issues/163 + previousAEmptyElement.remove(); + } + + // 保存光标 + vditor.wysiwyg.element.querySelectorAll("wbr").forEach((wbr) => { + wbr.remove(); + }); + range.insertNode(document.createElement("wbr")); + + // 在行首进行删除,后面的元素会带有样式,需清除 + blockElement.querySelectorAll("[style]").forEach((item) => { + item.removeAttribute("style"); + }); + + // 移除空评论 + blockElement.querySelectorAll(".vditor-comment").forEach((item) => { + if (item.textContent.trim() === "") { + item.classList.remove("vditor-comment", "vditor-comment--focus"); + item.removeAttribute("data-cmtids"); + } + }); + // 在有评论的行首换行后,该行的前一段会带有评论标识 + blockElement.previousElementSibling?.querySelectorAll(".vditor-comment").forEach((item) => { + if (item.textContent.trim() === "") { + item.classList.remove("vditor-comment", "vditor-comment--focus"); + item.removeAttribute("data-cmtids"); + } + }); + + let html = ""; + if (blockElement.getAttribute("data-type") === "link-ref-defs-block") { + // 修改链接引用 + blockElement = vditor.wysiwyg.element; + } + + const isWYSIWYGElement = blockElement.isEqualNode(vditor.wysiwyg.element); + const footnoteElement = hasClosestByAttribute(blockElement, "data-type", "footnotes-block"); + + if (!isWYSIWYGElement) { + // 列表需要到最顶层 + const topListElement = getTopList(range.startContainer); + if (topListElement && !footnoteElement) { + const blockquoteElement = hasClosestByTag(range.startContainer, "BLOCKQUOTE"); + if (blockquoteElement) { + // li 中有 blockquote 就只渲染 blockquote + blockElement = hasClosestBlock(range.startContainer) || blockElement; + } else { + blockElement = topListElement; + } + } + + // 修改脚注 + if (footnoteElement) { + blockElement = footnoteElement; + } + + html = blockElement.outerHTML; + + if (blockElement.tagName === "UL" || blockElement.tagName === "OL") { + // 如果为列表的话,需要把上下的列表都重绘 + const listPrevElement = blockElement.previousElementSibling; + const listNextElement = blockElement.nextElementSibling; + if (listPrevElement && (listPrevElement.tagName === "UL" || listPrevElement.tagName === "OL")) { + html = listPrevElement.outerHTML + html; + listPrevElement.remove(); + } + if (listNextElement && (listNextElement.tagName === "UL" || listNextElement.tagName === "OL")) { + html = html + listNextElement.outerHTML; + listNextElement.remove(); + } + // firefox 列表回车不会产生新的 list item https://github.com/Vanessa219/vditor/issues/194 + html = html.replace("

    ", "

  • "); + } + + if (!blockElement.innerText.startsWith("```")) { + // 添加链接引用 + vditor.wysiwyg.element.querySelectorAll("[data-type='link-ref-defs-block']").forEach((item) => { + if (item && !(blockElement as HTMLElement).isEqualNode(item)) { + html += item.outerHTML; + item.remove(); + } + }); + + // 添加脚注 + vditor.wysiwyg.element.querySelectorAll("[data-type='footnotes-block']").forEach((item) => { + if (item && !(blockElement as HTMLElement).isEqualNode(item)) { + html += item.outerHTML; + item.remove(); + } + }); + } + } else { + html = blockElement.innerHTML; + } + + // 合并多个 em, strong,s。以防止多个相同元素在一起时不满足 commonmark 规范,出现标记符 + html = html.replace(/<\/(strong|b)>/g, "") + .replace(/<\/(em|i)>/g, "") + .replace(/<\/(s|strike)>/g, ""); + + if (html === '

    ```

    ' && vditor.hint.recentLanguage) { + html = '

    ```

    '.replace("```", "```" + vditor.hint.recentLanguage); + } + + log("SpinVditorDOM", html, "argument", vditor.options.debugger); + html = vditor.lute.SpinVditorDOM(html); + log("SpinVditorDOM", html, "result", vditor.options.debugger); + + if (isWYSIWYGElement) { + blockElement.innerHTML = html; + } else { + blockElement.outerHTML = html; + + if (footnoteElement) { + // 更新正文中的 tip + const footnoteItemElement = hasTopClosestByTag(vditor.wysiwyg.element.querySelector("wbr"), "LI"); + if (footnoteItemElement) { + const footnoteRefElement = vditor.wysiwyg.element.querySelector(`sup[data-type="footnotes-ref"][data-footnotes-label="${footnoteItemElement.getAttribute("data-marker")}"]`); + if (footnoteRefElement) { + footnoteRefElement.setAttribute("aria-label", + footnoteItemElement.textContent.trim().substr(0, 24)); + } + } + } + } + + let firstLinkRefDefElement: Element; + const allLinkRefDefsElement = vditor.wysiwyg.element.querySelectorAll("[data-type='link-ref-defs-block']"); + allLinkRefDefsElement.forEach((item, index) => { + if (index === 0) { + firstLinkRefDefElement = item; + } else { + firstLinkRefDefElement.insertAdjacentHTML("beforeend", item.innerHTML); + item.remove(); + } + }); + if (allLinkRefDefsElement.length > 0) { + vditor.wysiwyg.element.insertAdjacentElement("beforeend", allLinkRefDefsElement[0]); + } + + // 脚注合并后添加的末尾 + let firstFootnoteElement: Element; + const allFootnoteElement = vditor.wysiwyg.element.querySelectorAll("[data-type='footnotes-block']"); + allFootnoteElement.forEach((item, index) => { + if (index === 0) { + firstFootnoteElement = item; + } else { + firstFootnoteElement.insertAdjacentHTML("beforeend", item.innerHTML); + item.remove(); + } + }); + if (allFootnoteElement.length > 0) { + vditor.wysiwyg.element.insertAdjacentElement("beforeend", allFootnoteElement[0]); + } + + // 设置光标 + setRangeByWbr(vditor.wysiwyg.element, range); + + vditor.wysiwyg.element.querySelectorAll(".vditor-wysiwyg__preview[data-render='2']") + .forEach((item: HTMLElement) => { + processCodeRender(item, vditor); + }); + + if (event && (event.inputType === "deleteContentBackward" || event.inputType === "deleteContentForward") && + vditor.options.comment.enable) { + vditor.wysiwyg.triggerRemoveComment(vditor); + vditor.options.comment.adjustTop(vditor.wysiwyg.getComments(vditor, true)); + } + } + renderToc(vditor); + afterRenderEvent(vditor, { + enableAddUndoStack: true, + enableHint: true, + enableInput: true, + }); +}; diff --git a/web/include/vditor/src/ts/wysiwyg/processKeydown.ts b/web/include/vditor/src/ts/wysiwyg/processKeydown.ts new file mode 100644 index 0000000..a1add1c --- /dev/null +++ b/web/include/vditor/src/ts/wysiwyg/processKeydown.ts @@ -0,0 +1,363 @@ +import {Constants} from "../constants"; +import {isCtrl, isFirefox} from "../util/compatibility"; +import {scrollCenter} from "../util/editorCommonEvent"; +import { + fixBlockquote, fixCJKPosition, + fixCodeBlock, fixCursorDownInlineMath, fixDelete, fixFirefoxArrowUpTable, fixGSKeyBackspace, fixHR, + fixList, + fixMarkdown, + fixTab, + fixTable, + fixTask, insertAfterBlock, insertBeforeBlock, +} from "../util/fixBrowserBehavior"; +import { + hasClosestBlock, + hasClosestByAttribute, + hasClosestByClassName, + hasClosestByMatchTag, + hasTopClosestByTag, +} from "../util/hasClosest"; +import {hasClosestByHeadings} from "../util/hasClosestByHeadings"; +import {matchHotKey} from "../util/hotKey"; +import {getEditorRange, getSelectPosition, setSelectionFocus} from "../util/selection"; +import {keydownToc} from "../util/toc"; +import {afterRenderEvent} from "./afterRenderEvent"; +import {nextIsCode} from "./inlineTag"; +import {removeHeading, setHeading} from "./setHeading"; +import {showCode} from "./showCode"; + +export const processKeydown = (vditor: IVditor, event: KeyboardEvent) => { + // Chrome firefox 触发 compositionend 机制不一致 https://github.com/Vanessa219/vditor/issues/188 + vditor.wysiwyg.composingLock = event.isComposing; + if (event.isComposing) { + return false; + } + + // 添加第一次记录 undo 的光标 + if (event.key.indexOf("Arrow") === -1 && event.key !== "Meta" && event.key !== "Control" && event.key !== "Alt" && + event.key !== "Shift" && event.key !== "CapsLock" && event.key !== "Escape" && !/^F\d{1,2}$/.test(event.key)) { + vditor.undo.recordFirstPosition(vditor, event); + } + + const range = getEditorRange(vditor); + const startContainer = range.startContainer; + + if (!fixGSKeyBackspace(event, vditor, startContainer)) { + return false; + } + + fixCJKPosition(range, vditor, event); + + fixHR(range); + + // 仅处理以下快捷键操作 + if (event.key !== "Enter" && event.key !== "Tab" && event.key !== "Backspace" && event.key.indexOf("Arrow") === -1 + && !isCtrl(event) && event.key !== "Escape" && event.key !== "Delete") { + return false; + } + + const blockElement = hasClosestBlock(startContainer); + const pElement = hasClosestByMatchTag(startContainer, "P"); + + // md 处理 + if (fixMarkdown(event, vditor, pElement, range)) { + return true; + } + + // li + if (fixList(range, vditor, pElement, event)) { + return true; + } + + // table + if (fixTable(vditor, event, range)) { + return true; + } + + // code render + const codeRenderElement = hasClosestByClassName(startContainer, "vditor-wysiwyg__block"); + if (codeRenderElement) { + // esc: 退出编辑,仅展示渲染 + if (event.key === "Escape" && codeRenderElement.children.length === 2) { + vditor.wysiwyg.popover.style.display = "none"; + (codeRenderElement.firstElementChild as HTMLElement).style.display = "none"; + vditor.wysiwyg.element.blur(); + event.preventDefault(); + return true; + } + + // alt+enter: 代码块切换到语言 https://github.com/Vanessa219/vditor/issues/54 + if (!isCtrl(event) && !event.shiftKey && event.altKey && event.key === "Enter" && + codeRenderElement.getAttribute("data-type") === "code-block") { + const inputElemment = (vditor.wysiwyg.popover.querySelector(".vditor-input") as HTMLInputElement); + inputElemment.focus(); + inputElemment.select(); + event.preventDefault(); + return true; + } + + if (codeRenderElement.getAttribute("data-block") === "0") { + if (fixCodeBlock(vditor, event, codeRenderElement.firstElementChild as HTMLElement, range)) { + return true; + } + if (insertAfterBlock(vditor, event, range, codeRenderElement.firstElementChild as HTMLElement, + codeRenderElement)) { + return true; + } + + if (codeRenderElement.getAttribute("data-type") !== "yaml-front-matter" && + insertBeforeBlock(vditor, event, range, codeRenderElement.firstElementChild as HTMLElement, + codeRenderElement)) { + return true; + } + } + } + + // blockquote + if (fixBlockquote(vditor, range, event, pElement)) { + return true; + } + + // 顶层 blockquote + const topBQElement = hasTopClosestByTag(startContainer, "BLOCKQUOTE"); + if (topBQElement) { + if (!event.shiftKey && event.altKey && event.key === "Enter") { + if (!isCtrl(event)) { + // alt+enter: 跳出多层 blockquote 嵌套之后 https://github.com/Vanessa219/vditor/issues/51 + range.setStartAfter(topBQElement); + } else { + // ctrl+alt+enter: 跳出多层 blockquote 嵌套之前 + range.setStartBefore(topBQElement); + } + setSelectionFocus(range); + const node = document.createElement("p"); + node.setAttribute("data-block", "0"); + node.innerHTML = "\n"; + range.insertNode(node); + range.collapse(true); + setSelectionFocus(range); + afterRenderEvent(vditor); + scrollCenter(vditor); + event.preventDefault(); + return true; + } + } + + // h1-h6 + const headingElement = hasClosestByHeadings(startContainer); + if (headingElement) { + if (headingElement.tagName === "H6" && startContainer.textContent.length === range.startOffset && + !isCtrl(event) && !event.shiftKey && !event.altKey && event.key === "Enter") { + // enter: H6 回车解析问题 https://github.com/Vanessa219/vditor/issues/48 + const pTempElement = document.createElement("p"); + pTempElement.textContent = "\n"; + pTempElement.setAttribute("data-block", "0"); + startContainer.parentElement.insertAdjacentElement("afterend", pTempElement); + range.setStart(pTempElement, 0); + setSelectionFocus(range); + afterRenderEvent(vditor); + scrollCenter(vditor); + event.preventDefault(); + return true; + } + + // enter++: 标题变大 + if (matchHotKey("⌘=", event)) { + const index = parseInt((headingElement as HTMLElement).tagName.substr(1), 10) - 1; + if (index > 0) { + setHeading(vditor, `h${index}`); + afterRenderEvent(vditor); + } + event.preventDefault(); + return true; + } + + // enter++: 标题变小 + if (matchHotKey("⌘-", event)) { + const index = parseInt((headingElement as HTMLElement).tagName.substr(1), 10) + 1; + if (index < 7) { + setHeading(vditor, `h${index}`); + afterRenderEvent(vditor); + } + event.preventDefault(); + return true; + } + + if (event.key === "Backspace" && !isCtrl(event) && !event.shiftKey && !event.altKey + && headingElement.textContent.length === 1) { + // 删除后变为空 + removeHeading(vditor); + } + } + + // task list + if (fixTask(vditor, range, event)) { + return true; + } + + // alt+enter + if (event.altKey && event.key === "Enter" && !isCtrl(event) && !event.shiftKey) { + // 切换到链接、链接引用、脚注引用弹出的输入框中 + const aElement = hasClosestByMatchTag(startContainer, "A"); + const linRefElement = hasClosestByAttribute(startContainer, "data-type", "link-ref"); + const footnoteRefElement = hasClosestByAttribute(startContainer, "data-type", "footnotes-ref"); + if (aElement || linRefElement || footnoteRefElement || + (headingElement && headingElement.tagName.length === 2)) { + const inputElement = vditor.wysiwyg.popover.querySelector("input"); + inputElement.focus(); + inputElement.select(); + } + } + + // 删除有子工具栏的块 + if (removeBlockElement(vditor, event)) { + return true; + } + + // 对有子工具栏的块上移 + if (matchHotKey("⇧⌘U", event)) { + const itemElement: HTMLElement = vditor.wysiwyg.popover.querySelector('[data-type="up"]'); + if (itemElement) { + itemElement.click(); + event.preventDefault(); + return true; + } + } + + // 对有子工具栏的块下移 + if (matchHotKey("⇧⌘D", event)) { + const itemElement: HTMLElement = vditor.wysiwyg.popover.querySelector('[data-type="down"]'); + if (itemElement) { + itemElement.click(); + event.preventDefault(); + return true; + } + } + + if (fixTab(vditor, range, event)) { + return true; + } + + // shift+enter:软换行,但 table/hr/heading 处理、cell 内换行、block render 换行处理单独写在上面,li & p 使用浏览器默认 + if (!isCtrl(event) && event.shiftKey && !event.altKey && event.key === "Enter" && + startContainer.parentElement.tagName !== "LI" && startContainer.parentElement.tagName !== "P") { + if (["STRONG", "STRIKE", "S", "I", "EM", "B"].includes(startContainer.parentElement.tagName)) { + // 行内元素软换行需继续 https://github.com/Vanessa219/vditor/issues/170 + range.insertNode(document.createTextNode("\n" + Constants.ZWSP)); + } else { + range.insertNode(document.createTextNode("\n")); + } + range.collapse(false); + setSelectionFocus(range); + afterRenderEvent(vditor); + scrollCenter(vditor); + event.preventDefault(); + return true; + } + + // 删除 + if (event.key === "Backspace" && !isCtrl(event) && !event.shiftKey && !event.altKey && range.toString() === "") { + if (fixDelete(vditor, range, event, pElement)) { + return true; + } + if (blockElement) { + if (blockElement.previousElementSibling + && blockElement.previousElementSibling.classList.contains("vditor-wysiwyg__block") + && blockElement.previousElementSibling.getAttribute("data-block") === "0" + // https://github.com/Vanessa219/vditor/issues/946 + && blockElement.tagName !== "UL" && blockElement.tagName !== "OL" + ) { + const rangeStart = getSelectPosition(blockElement, vditor.wysiwyg.element, range).start; + if ((rangeStart === 0 && range.startOffset === 0) || // https://github.com/Vanessa219/vditor/issues/894 + (rangeStart === 1 && blockElement.innerText.startsWith(Constants.ZWSP))) { + // 当前块删除后光标落于代码渲染块上,当前块会被删除,因此需要阻止事件,不能和 keyup 中的代码块处理合并 + showCode(blockElement.previousElementSibling.lastElementChild as HTMLElement, vditor, false); + if (blockElement.innerHTML.trim().replace(Constants.ZWSP, "") === "") { + // 当前块为空且不是最后一个时,需要删除 + blockElement.remove(); + afterRenderEvent(vditor); + } + event.preventDefault(); + return true; + } + } + + const rangeStartOffset = range.startOffset; + if (range.toString() === "" && startContainer.nodeType === 3 && + startContainer.textContent.charAt(rangeStartOffset - 2) === "\n" && + startContainer.textContent.charAt(rangeStartOffset - 1) !== Constants.ZWSP + && ["STRONG", "STRIKE", "S", "I", "EM", "B"].includes(startContainer.parentElement.tagName)) { + // 保持行内元素软换行需继续的一致性 + startContainer.textContent = startContainer.textContent.substring(0, rangeStartOffset - 1) + + Constants.ZWSP; + range.setStart(startContainer, rangeStartOffset); + range.collapse(true); + afterRenderEvent(vditor); + event.preventDefault(); + return true; + } + + // inline code、math、html 行前零宽字符后进行删除 + if (startContainer.textContent === Constants.ZWSP && range.startOffset === 1 + && !startContainer.previousSibling && nextIsCode(range)) { + startContainer.textContent = ""; + // 不能返回,其前面为代码渲染块时需进行以下处理:修正光标位于 inline math/html 前,按下删除按钮 code 中内容会被删除 + } + + // 修正光标位于 inline math/html, html-entity 前,按下删除按钮 code 中内容会被删除, 不能返回,还需要进行后续处理 + blockElement.querySelectorAll("span.vditor-wysiwyg__block[data-type='math-inline']").forEach((item) => { + (item.firstElementChild as HTMLElement).style.display = "inline"; + (item.lastElementChild as HTMLElement).style.display = "none"; + }); + blockElement.querySelectorAll("span.vditor-wysiwyg__block[data-type='html-entity']").forEach((item) => { + (item.firstElementChild as HTMLElement).style.display = "inline"; + (item.lastElementChild as HTMLElement).style.display = "none"; + }); + } + } + + if (isFirefox() && range.startOffset === 1 && startContainer.textContent.indexOf(Constants.ZWSP) > -1 && + startContainer.previousSibling && startContainer.previousSibling.nodeType !== 3 && + (startContainer.previousSibling as HTMLElement).tagName === "CODE" && + (event.key === "Backspace" || event.key === "ArrowLeft")) { + // https://github.com/Vanessa219/vditor/issues/410 + range.selectNodeContents(startContainer.previousSibling); + range.collapse(false); + event.preventDefault(); + return true; + } + + if (fixFirefoxArrowUpTable(event, blockElement, range)) { + event.preventDefault(); + return true; + } + + fixCursorDownInlineMath(range, event.key); + + if (event.key === "ArrowDown") { + // 光标位于内联数学公式前,按下键无作用 + const nextElement = startContainer.nextSibling as HTMLElement; + if (nextElement && nextElement.nodeType !== 3 && nextElement.getAttribute("data-type") === "math-inline") { + range.setStartAfter(nextElement); + } + } + + if (blockElement && keydownToc(blockElement, vditor, event, range)) { + event.preventDefault(); + return true; + } + + return false; +}; + +export const removeBlockElement = (vditor: IVditor, event: KeyboardEvent) => { + // 删除有子工具栏的块 + if (matchHotKey("⇧⌘X", event)) { + const itemElement: HTMLElement = vditor.wysiwyg.popover.querySelector('[data-type="remove"]'); + if (itemElement) { + itemElement.click(); + } + event.preventDefault(); + return true; + } +}; diff --git a/web/include/vditor/src/ts/wysiwyg/renderDomByMd.ts b/web/include/vditor/src/ts/wysiwyg/renderDomByMd.ts new file mode 100644 index 0000000..4e1942c --- /dev/null +++ b/web/include/vditor/src/ts/wysiwyg/renderDomByMd.ts @@ -0,0 +1,18 @@ +import {processCodeRender} from "../util/processCode"; +import {afterRenderEvent} from "./afterRenderEvent"; + +export const renderDomByMd = (vditor: IVditor, md: string, options = { + enableAddUndoStack: true, + enableHint: false, + enableInput: true, +}) => { + const editorElement = vditor.wysiwyg.element; + editorElement.innerHTML = vditor.lute.Md2VditorDOM(md); + + editorElement.querySelectorAll(".vditor-wysiwyg__preview[data-render='2']").forEach((item: HTMLElement) => { + processCodeRender(item, vditor); + item.previousElementSibling.setAttribute("style", "display:none"); + }); + + afterRenderEvent(vditor, options); +}; diff --git a/web/include/vditor/src/ts/wysiwyg/setHeading.ts b/web/include/vditor/src/ts/wysiwyg/setHeading.ts new file mode 100644 index 0000000..6c0586a --- /dev/null +++ b/web/include/vditor/src/ts/wysiwyg/setHeading.ts @@ -0,0 +1,43 @@ +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); + let blockElement = hasClosestBlock(range.startContainer); + if (!blockElement) { + blockElement = range.startContainer.childNodes[range.startOffset] as HTMLElement; + } + if (!blockElement && vditor.wysiwyg.element.children.length === 0) { + blockElement = vditor.wysiwyg.element; + } + if (blockElement && !blockElement.classList.contains("vditor-wysiwyg__block")) { + range.insertNode(document.createElement("wbr")); + // Firefox 需要 trim https://github.com/Vanessa219/vditor/issues/207 + if (blockElement.innerHTML.trim() === "") { + // Firefox 光标对不齐 https://github.com/Vanessa219/vditor/issues/199 1 + blockElement.innerHTML = "
    "; + } + if (blockElement.tagName === "BLOCKQUOTE" || blockElement.classList.contains("vditor-reset")) { + blockElement.innerHTML = `<${tagName} data-block="0">${blockElement.innerHTML.trim()}`; + } else { + blockElement.outerHTML = `<${tagName} data-block="0">${blockElement.innerHTML.trim()}`; + } + setRangeByWbr(vditor.wysiwyg.element, range); + renderToc(vditor); + } +}; + +export const removeHeading = (vditor: IVditor) => { + const range = getSelection().getRangeAt(0); + let blockElement = hasClosestBlock(range.startContainer); + if (!blockElement) { + blockElement = range.startContainer.childNodes[range.startOffset] as HTMLElement; + } + if (blockElement) { + range.insertNode(document.createElement("wbr")); + blockElement.outerHTML = `

    ${blockElement.innerHTML}

    `; + setRangeByWbr(vditor.wysiwyg.element, range); + } + vditor.wysiwyg.popover.style.display = "none"; +}; diff --git a/web/include/vditor/src/ts/wysiwyg/showCode.ts b/web/include/vditor/src/ts/wysiwyg/showCode.ts new file mode 100644 index 0000000..9bac43e --- /dev/null +++ b/web/include/vditor/src/ts/wysiwyg/showCode.ts @@ -0,0 +1,32 @@ +import {scrollCenter} from "../util/editorCommonEvent"; +import {setSelectionFocus} from "../util/selection"; + +export const showCode = (previewElement: HTMLElement, vditor: IVditor, first = true) => { + const previousElement = previewElement.previousElementSibling as HTMLElement; + const range = previousElement.ownerDocument.createRange(); + if (previousElement.tagName === "CODE") { + previousElement.style.display = "inline-block"; + if (first) { + range.setStart(previousElement.firstChild, 1); + } else { + range.selectNodeContents(previousElement); + } + } else { + previousElement.style.display = "block"; + + if (!previousElement.firstChild.firstChild) { + previousElement.firstChild.appendChild(document.createTextNode("")); + } + range.selectNodeContents(previousElement.firstChild); + } + if (first) { + range.collapse(true); + } else { + range.collapse(false); + } + setSelectionFocus(range); + if (previewElement.firstElementChild.classList.contains("language-mindmap")) { + return; + } + scrollCenter(vditor); +}; diff --git a/web/include/vditor/src/ts/wysiwyg/toolbarEvent.ts b/web/include/vditor/src/ts/wysiwyg/toolbarEvent.ts new file mode 100644 index 0000000..06da83a --- /dev/null +++ b/web/include/vditor/src/ts/wysiwyg/toolbarEvent.ts @@ -0,0 +1,333 @@ +import {Constants} from "../constants"; +import {removeCurrentToolbar} from "../toolbar/setToolbar"; +import {listToggle} from "../util/fixBrowserBehavior"; +import {hasClosestBlock, hasClosestByMatchTag} from "../util/hasClosest"; +import {processCodeRender} from "../util/processCode"; +import {getEditorRange, setRangeByWbr, setSelectionFocus} from "../util/selection"; +import {afterRenderEvent} from "./afterRenderEvent"; +import {genAPopover, highlightToolbarWYSIWYG} from "./highlightToolbarWYSIWYG"; +import {getNextHTML, getPreviousHTML, splitElement} from "./inlineTag"; + +const cancelBES = (range: Range, vditor: IVditor, commandName: string) => { + let element = range.startContainer.parentElement; + let jump = false; + let lastTagName = ""; + let lastEndTagName = ""; + + const splitHTML = splitElement(range); + let lastBeforeHTML = splitHTML.beforeHTML; + let lastAfterHTML = splitHTML.afterHTML; + + while (element && !jump) { + let tagName = element.tagName; + if (tagName === "STRIKE") { + tagName = "S"; + } + if (tagName === "I") { + tagName = "EM"; + } + if (tagName === "B") { + tagName = "STRONG"; + } + if (tagName === "S" || tagName === "STRONG" || tagName === "EM") { + let insertHTML = ""; + let previousHTML = ""; + let nextHTML = ""; + if (element.parentElement.getAttribute("data-block") !== "0") { + previousHTML = getPreviousHTML(element); + nextHTML = getNextHTML(element); + } + + if (lastBeforeHTML || previousHTML) { + insertHTML = `${previousHTML}<${tagName}>${lastBeforeHTML}`; + lastBeforeHTML = insertHTML; + } + if ((commandName === "bold" && tagName === "STRONG") || + (commandName === "italic" && tagName === "EM") || + (commandName === "strikeThrough" && tagName === "S")) { + // 取消 + insertHTML += `${lastTagName}${Constants.ZWSP}${lastEndTagName}`; + jump = true; + } + + if (lastAfterHTML || nextHTML) { + lastAfterHTML = `<${tagName}>${lastAfterHTML}${nextHTML}`; + insertHTML += lastAfterHTML; + } + + if (element.parentElement.getAttribute("data-block") !== "0") { + element = element.parentElement; + element.innerHTML = insertHTML; + } else { + element.outerHTML = insertHTML; + element = element.parentElement; + } + + lastTagName = `<${tagName}>` + lastTagName; + lastEndTagName = `` + lastEndTagName; + } else { + jump = true; + } + } + + setRangeByWbr(vditor.wysiwyg.element, range); +}; + +export const toolbarEvent = (vditor: IVditor, actionBtn: Element, event: Event) => { + if (vditor.wysiwyg.composingLock // Mac Chrome 中韩文结束会出发此事件,导致重复末尾字符 https://github.com/Vanessa219/vditor/issues/188 + && event instanceof CustomEvent // 点击按钮应忽略输入法 https://github.com/Vanessa219/vditor/issues/473 + ) { + return; + } + + let useHighlight = true; + let useRender = true; + if (vditor.wysiwyg.element.querySelector("wbr")) { + vditor.wysiwyg.element.querySelector("wbr").remove(); + } + const range = getEditorRange(vditor); + + let commandName = actionBtn.getAttribute("data-type"); + + // 移除 + if (actionBtn.classList.contains("vditor-menu--current")) { + if (commandName === "strike") { + commandName = "strikeThrough"; + } + + if (commandName === "quote") { + let quoteElement = hasClosestByMatchTag(range.startContainer, "BLOCKQUOTE"); + if (!quoteElement) { + quoteElement = range.startContainer.childNodes[range.startOffset] as HTMLElement; + } + if (quoteElement) { + useHighlight = false; + actionBtn.classList.remove("vditor-menu--current"); + range.insertNode(document.createElement("wbr")); + quoteElement.outerHTML = quoteElement.innerHTML.trim() === "" ? + `

    ${quoteElement.innerHTML}

    ` : quoteElement.innerHTML; + setRangeByWbr(vditor.wysiwyg.element, range); + } + } else if (commandName === "inline-code") { + let inlineCodeElement = hasClosestByMatchTag(range.startContainer, "CODE"); + if (!inlineCodeElement) { + inlineCodeElement = range.startContainer.childNodes[range.startOffset] as HTMLElement; + } + if (inlineCodeElement) { + inlineCodeElement.outerHTML = inlineCodeElement.innerHTML.replace(Constants.ZWSP, "") + ""; + setRangeByWbr(vditor.wysiwyg.element, range); + } + } else if (commandName === "link") { + if (!range.collapsed) { + document.execCommand("unlink", false, ""); + } else { + range.selectNode(range.startContainer.parentElement); + document.execCommand("unlink", false, ""); + } + } else if (commandName === "check" || commandName === "list" || commandName === "ordered-list") { + listToggle(vditor, range, commandName); + setRangeByWbr(vditor.wysiwyg.element, range); + useHighlight = false; + actionBtn.classList.remove("vditor-menu--current"); + } else { + // bold, italic, strike + useHighlight = false; + actionBtn.classList.remove("vditor-menu--current"); + if (range.toString() === "") { + cancelBES(range, vditor, commandName); + } else { + document.execCommand(commandName, false, ""); + } + } + } else { + // 添加 + if (vditor.wysiwyg.element.childNodes.length === 0) { + vditor.wysiwyg.element.innerHTML = '

    '; + setRangeByWbr(vditor.wysiwyg.element, range); + } + + let blockElement = hasClosestBlock(range.startContainer); + if (commandName === "quote") { + if (!blockElement) { + blockElement = range.startContainer.childNodes[range.startOffset] as HTMLElement; + } + + if (blockElement) { + useHighlight = false; + actionBtn.classList.add("vditor-menu--current"); + range.insertNode(document.createElement("wbr")); + + const liElement = hasClosestByMatchTag(range.startContainer, "LI"); + // li 中软换行 + if (liElement && blockElement.contains(liElement)) { + liElement.innerHTML = `
    ${liElement.innerHTML}
    `; + } else { + blockElement.outerHTML = `
    ${blockElement.outerHTML}
    `; + } + setRangeByWbr(vditor.wysiwyg.element, range); + } + } else if (commandName === "check" || commandName === "list" || commandName === "ordered-list") { + listToggle(vditor, range, commandName, false); + setRangeByWbr(vditor.wysiwyg.element, range); + useHighlight = false; + removeCurrentToolbar(vditor.toolbar.elements, ["check", "list", "ordered-list"]); + actionBtn.classList.add("vditor-menu--current"); + } else if (commandName === "inline-code") { + if (range.toString() === "") { + const node = document.createElement("code"); + node.textContent = Constants.ZWSP; + range.insertNode(node); + range.setStart(node.firstChild, 1); + range.collapse(true); + setSelectionFocus(range); + } else if (range.startContainer.nodeType === 3) { + const node = document.createElement("code"); + range.surroundContents(node); + range.insertNode(node); + setSelectionFocus(range); + } + actionBtn.classList.add("vditor-menu--current"); + } else if (commandName === "code") { + const node = document.createElement("div"); + node.className = "vditor-wysiwyg__block"; + node.setAttribute("data-type", "code-block"); + node.setAttribute("data-block", "0"); + node.setAttribute("data-marker", "```"); + if (range.toString() === "") { + node.innerHTML = "
    \n
    "; + } else { + node.innerHTML = `
    ${range.toString()}
    `; + range.deleteContents(); + } + range.insertNode(node); + if (blockElement) { + blockElement.outerHTML = vditor.lute.SpinVditorDOM(blockElement.outerHTML); + } + setRangeByWbr(vditor.wysiwyg.element, range); + vditor.wysiwyg.element.querySelectorAll(".vditor-wysiwyg__preview[data-render='2']").forEach( + (item: HTMLElement) => { + processCodeRender(item, vditor); + }); + actionBtn.classList.add("vditor-menu--disabled"); + } else if (commandName === "link") { + if (range.toString() === "") { + const aElement = document.createElement("a"); + aElement.innerText = Constants.ZWSP; + range.insertNode(aElement); + range.setStart(aElement.firstChild, 1); + range.collapse(true); + genAPopover(vditor, aElement, range); + const textInputElement = vditor.wysiwyg.popover.querySelector("input"); + textInputElement.value = ""; + textInputElement.focus(); + useRender = false; + } else { + const node = document.createElement("a"); + node.setAttribute("href", ""); + node.innerHTML = range.toString(); + range.surroundContents(node); + range.insertNode(node); + setSelectionFocus(range); + genAPopover(vditor, node, range); + const textInputElements = vditor.wysiwyg.popover.querySelectorAll("input"); + textInputElements[0].value = node.innerText; + textInputElements[1].focus(); + } + useHighlight = false; + actionBtn.classList.add("vditor-menu--current"); + } else if (commandName === "table") { + let tableHTML = `
    col1col2col3
    `; + if (range.toString().trim() === "") { + if (blockElement && blockElement.innerHTML.trim().replace(Constants.ZWSP, "") === "") { + blockElement.outerHTML = tableHTML; + } else { + document.execCommand("insertHTML", false, tableHTML); + } + range.selectNode(vditor.wysiwyg.element.querySelector("wbr").previousSibling); + vditor.wysiwyg.element.querySelector("wbr").remove(); + setSelectionFocus(range); + } else { + tableHTML = ``; + const tableText = range.toString().split("\n"); + const delimiter = tableText[0].split(",").length > tableText[0].split("\t").length ? "," : "\t"; + + tableText.forEach((rows, index) => { + if (index === 0) { + rows.split(delimiter).forEach((header, subIndex) => { + if (subIndex === 0) { + tableHTML += ``; + } else { + tableHTML += ``; + } + }); + tableHTML += ""; + } else { + if (index === 1) { + tableHTML += ""; + } else { + tableHTML += ""; + } + rows.split(delimiter).forEach((cell) => { + tableHTML += ``; + }); + tableHTML += ``; + } + }); + tableHTML += "
    ${header}${header}
    ${cell}
    "; + document.execCommand("insertHTML", false, tableHTML); + setRangeByWbr(vditor.wysiwyg.element, range); + } + useHighlight = false; + actionBtn.classList.add("vditor-menu--disabled"); + } else if (commandName === "line") { + if (blockElement) { + const hrHTML = '

    \n

    '; + if (blockElement.innerHTML.trim() === "") { + blockElement.outerHTML = hrHTML; + } else { + blockElement.insertAdjacentHTML("afterend", hrHTML); + } + setRangeByWbr(vditor.wysiwyg.element, range); + } + } else { + // bold, italic, strike + useHighlight = false; + actionBtn.classList.add("vditor-menu--current"); + + if (commandName === "strike") { + commandName = "strikeThrough"; + } + if (range.toString() === "" && (commandName === "bold" || commandName === "italic" || commandName === "strikeThrough")) { + let tagName = "strong"; + if (commandName === "italic") { + tagName = "em"; + } else if (commandName === "strikeThrough") { + tagName = "s"; + } + const node = document.createElement(tagName); + node.textContent = Constants.ZWSP; + + range.insertNode(node); + + if (node.previousSibling && node.previousSibling.textContent === Constants.ZWSP) { + // 移除多层嵌套中的 zwsp + node.previousSibling.textContent = ""; + } + + range.setStart(node.firstChild, 1); + range.collapse(true); + setSelectionFocus(range); + } else { + document.execCommand(commandName, false, ""); + } + } + } + + if (useHighlight) { + highlightToolbarWYSIWYG(vditor); + } + + if (useRender) { + afterRenderEvent(vditor); + } +}; diff --git a/web/swadmin/kindeditor.php b/web/swadmin/kindeditor.php index 62cb78e..b00361a 100644 --- a/web/swadmin/kindeditor.php +++ b/web/swadmin/kindeditor.php @@ -6,10 +6,10 @@ selector: 'textarea.kindeditor', uploadUrl: '../kindeditor/php/upload_json.php', accept: '*/*', - mode: 'sv', + mode: 'wysiwyg', ctrlEnterFormName: 'example', preview: { - mode: 'both', + mode: 'editor', delay: 0, actions: [], markdown: { diff --git a/web/template/syzoj/problem.php b/web/template/syzoj/problem.php index 9eb633f..c260faa 100644 --- a/web/template/syzoj/problem.php +++ b/web/template/syzoj/problem.php @@ -34,28 +34,6 @@ if ($pr_flag) { box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05), 0 2px 4px rgba(0, 0, 0, 0.05); } - .md pre { - position: relative; - } - - .md-code-copy { - position: absolute; - top: 8px; - right: 8px; - z-index: 1; - font-size: 12px; - color: #4d4d4d; - background-color: #fff; - border: 1px solid #e0e0e0; - padding: 2px 8px; - border-radius: 4px; - cursor: pointer; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); - } - - .md-code-copy:hover { - background-color: #f5f5f5; - } @@ -537,55 +515,6 @@ if ($pr_flag) { $(document).ready(function () { $("#creator").load("problem-ajax.php?pid="); - function copyText(content, onSuccess, onFail) { - if (navigator.clipboard && window.isSecureContext) { - navigator.clipboard.writeText(content).then(onSuccess).catch(onFail); - return; - } - const textarea = document.createElement('textarea'); - textarea.value = content; - textarea.style.position = 'fixed'; - textarea.style.left = '-9999px'; - document.body.appendChild(textarea); - textarea.focus(); - textarea.select(); - try { - const copied = document.execCommand('copy'); - copied ? onSuccess() : onFail(); - } catch (err) { - onFail(); - } - document.body.removeChild(textarea); - } - - function initMarkdownCodeCopy() { - $('.md pre').each(function () { - const pre = $(this); - if (pre.find('.md-code-copy').length > 0) return; - - const btn = $(''); - btn.on('click', function () { - const code = pre.find('code').first(); - const text = code.length > 0 ? code.text() : pre.text(); - const currentBtn = $(this); - - copyText( - text, - function () { - currentBtn.text('!'); - setTimeout(function () { currentBtn.text(''); }, 1500); - }, - function () { - currentBtn.text('!'); - setTimeout(function () { currentBtn.text(''); }, 1500); - } - ); - }); - - pre.prepend(btn); - }); - } - function addMarkdownInputOutputLabels() { for (let i = 1; i < 10; i++) { $(".language-input" + i).parent().each(function () { @@ -623,7 +552,6 @@ if ($pr_flag) { return HustOJVditor.renderMarkdownBlocks('.md').then(function () { addMarkdownInputOutputLabels(); styleMarkdownTables(); - initMarkdownCodeCopy(); }); return Promise.resolve([]);