mirror of https://github.com/halo-dev/halo
refactor: post editor (halo-dev/console#670)
#### What type of PR is this? /kind improvement #### What this PR does / why we need it: 升级 `@halo-dev/richtext-editor`。重构编辑器的结构,目前可以在外部添加菜单项和指令,意味着可以被扩展。 添加 tiptap 拓展的方式: ``` pnpm install @tiptap/extension-character-count ``` 然后在创建 Editor 实例的时候需要将拓展添加到 extensions 数组,如: ```ts const editor = useEditor({ content: props.modelValue, extensions: [ ... ExtensionCharacterCount, ], }); ``` 最终如果要通过我们的插件机制来拓展编辑器,那么就需要对 extensions 提供可拓展点。 #### Special notes for your reviewer: /cc @halo-dev/sig-halo-console #### Does this PR introduce a user-facing change? ```release-note None ```pull/3445/head
parent
7c621c8afa
commit
6bb7af1762
|
@ -36,7 +36,7 @@
|
||||||
"@halo-dev/api-client": "^0.0.41",
|
"@halo-dev/api-client": "^0.0.41",
|
||||||
"@halo-dev/components": "workspace:*",
|
"@halo-dev/components": "workspace:*",
|
||||||
"@halo-dev/console-shared": "workspace:*",
|
"@halo-dev/console-shared": "workspace:*",
|
||||||
"@halo-dev/richtext-editor": "^0.0.0-alpha.8",
|
"@halo-dev/richtext-editor": "^0.0.0-alpha.11",
|
||||||
"@tiptap/extension-character-count": "^2.0.0-beta.199",
|
"@tiptap/extension-character-count": "^2.0.0-beta.199",
|
||||||
"@uppy/core": "^3.0.4",
|
"@uppy/core": "^3.0.4",
|
||||||
"@uppy/dashboard": "^3.1.0",
|
"@uppy/dashboard": "^3.1.0",
|
||||||
|
|
133
pnpm-lock.yaml
133
pnpm-lock.yaml
|
@ -16,7 +16,7 @@ importers:
|
||||||
'@halo-dev/api-client': ^0.0.41
|
'@halo-dev/api-client': ^0.0.41
|
||||||
'@halo-dev/components': workspace:*
|
'@halo-dev/components': workspace:*
|
||||||
'@halo-dev/console-shared': workspace:*
|
'@halo-dev/console-shared': workspace:*
|
||||||
'@halo-dev/richtext-editor': ^0.0.0-alpha.8
|
'@halo-dev/richtext-editor': ^0.0.0-alpha.11
|
||||||
'@iconify-json/mdi': ^1.1.34
|
'@iconify-json/mdi': ^1.1.34
|
||||||
'@iconify-json/vscode-icons': ^1.1.16
|
'@iconify-json/vscode-icons': ^1.1.16
|
||||||
'@rushstack/eslint-patch': ^1.2.0
|
'@rushstack/eslint-patch': ^1.2.0
|
||||||
|
@ -111,7 +111,7 @@ importers:
|
||||||
'@halo-dev/api-client': 0.0.41
|
'@halo-dev/api-client': 0.0.41
|
||||||
'@halo-dev/components': link:packages/components
|
'@halo-dev/components': link:packages/components
|
||||||
'@halo-dev/console-shared': link:packages/shared
|
'@halo-dev/console-shared': link:packages/shared
|
||||||
'@halo-dev/richtext-editor': 0.0.0-alpha.8_vue@3.2.41
|
'@halo-dev/richtext-editor': 0.0.0-alpha.11_vue@3.2.41
|
||||||
'@tiptap/extension-character-count': 2.0.0-beta.199
|
'@tiptap/extension-character-count': 2.0.0-beta.199
|
||||||
'@uppy/core': 3.0.4
|
'@uppy/core': 3.0.4
|
||||||
'@uppy/dashboard': 3.1.0_@uppy+core@3.0.4
|
'@uppy/dashboard': 3.1.0_@uppy+core@3.0.4
|
||||||
|
@ -1957,33 +1957,33 @@ packages:
|
||||||
resolution: {integrity: sha512-YpwoIyT+6BjNEfhQqZPSG7dewmC9AE7wxc/uaIRcVZdKr0C4GLmbiBKGOFFnVWIYr42islhMWjqYCGaiJSzkUg==}
|
resolution: {integrity: sha512-YpwoIyT+6BjNEfhQqZPSG7dewmC9AE7wxc/uaIRcVZdKr0C4GLmbiBKGOFFnVWIYr42islhMWjqYCGaiJSzkUg==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@halo-dev/richtext-editor/0.0.0-alpha.8_vue@3.2.41:
|
/@halo-dev/richtext-editor/0.0.0-alpha.11_vue@3.2.41:
|
||||||
resolution: {integrity: sha512-3lENKyg6UqXpxUWCfhefQ3xx2e1jOzj/2TUBpblaRToP2WZSAw8wpcG5m3gTqsp5+0lHLtfrpgTvSr3KYO+08w==}
|
resolution: {integrity: sha512-JtUBwzTp57lrBi3HUvypQaMyZIU/g9KER34PRoYgtc+12Uhjq7XXosrtaOrEL1pHjYPqyjxawUyPUJuzXONuig==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.2.37
|
vue: ^3.2.37
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.0.0-beta.195
|
'@tiptap/core': 2.0.0-beta.195
|
||||||
'@tiptap/extension-blockquote': 2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-blockquote': 2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-bold': 2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-bold': 2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-bullet-list': 2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-bullet-list': 2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-code': 2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-code': 2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-code-block': 2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-code-block': 2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-code-block-lowlight': 2.0.0-beta.195_ndbd4h3iw4l6bgxmwshcxebdty
|
'@tiptap/extension-code-block-lowlight': 2.0.0-beta.195_tmbwbnesgmzomdlloh7t4ntnfa
|
||||||
'@tiptap/extension-document': 2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-document': 2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-dropcursor': 2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-dropcursor': 2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-gapcursor': 2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-gapcursor': 2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-hard-break': 2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-hard-break': 2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-heading': 2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-heading': 2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-history': 2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-history': 2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-horizontal-rule': 2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-horizontal-rule': 2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-image': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-image': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-italic': 2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-italic': 2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-link': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-link': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-list-item': 2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-list-item': 2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-ordered-list': 2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-ordered-list': 2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-paragraph': 2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-paragraph': 2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-placeholder': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-placeholder': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-strike': 2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-strike': 2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-subscript': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-subscript': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-superscript': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-superscript': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-table': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-table': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
|
@ -1992,14 +1992,13 @@ packages:
|
||||||
'@tiptap/extension-table-row': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-table-row': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-task-item': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-task-item': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-task-list': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-task-list': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-text': 2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-text': 2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-text-align': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-text-align': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/extension-underline': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-underline': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/suggestion': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/suggestion': 2.0.0-beta.195_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
'@tiptap/vue-3': 2.0.0-beta.195_halyxqfan7x4orbb3jo3vn6fii
|
'@tiptap/vue-3': 2.0.0-beta.195_halyxqfan7x4orbb3jo3vn6fii
|
||||||
floating-vue: 2.0.0-beta.20_vue@3.2.41
|
floating-vue: 2.0.0-beta.20_vue@3.2.41
|
||||||
github-markdown-css: 5.1.0
|
katex: 0.16.3
|
||||||
katex: 0.16.2
|
|
||||||
lowlight: 2.7.0
|
lowlight: 2.7.0
|
||||||
tippy.js: 6.3.7
|
tippy.js: 6.3.7
|
||||||
vue: 3.2.41
|
vue: 3.2.41
|
||||||
|
@ -2651,16 +2650,16 @@ packages:
|
||||||
prosemirror-view: 1.26.2
|
prosemirror-view: 1.26.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-blockquote/2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a:
|
/@tiptap/extension-blockquote/2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a:
|
||||||
resolution: {integrity: sha512-FWlSR4TwSbYj8Ukc82M9s4qx+yFNoDTBjvsM8rA+6JxBJikSIiwOD5ht71oylA2rojWMQx75IZlYe6IBqqko0A==}
|
resolution: {integrity: sha512-BbHKaIkVYgJCV5giJC3/bdXMZWxFylLKiAbOGSGwIsnnS5/oL+V4XN6hqcIDBxlcj3MQ/d9zG0+mvFyjRssAkg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.0.0-beta.1
|
'@tiptap/core': ^2.0.0-beta.1
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.0.0-beta.195
|
'@tiptap/core': 2.0.0-beta.195
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-bold/2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a:
|
/@tiptap/extension-bold/2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a:
|
||||||
resolution: {integrity: sha512-yVXIH6ccTqiUe9c+7gMsYS77MxmgnP7mAcj59hPR9tSjSk9ymzrfFr9sXiWZ455HJmKuvkEBSPtMzqr8P7fWOA==}
|
resolution: {integrity: sha512-l513jgGLmt8C69Yuh5Et7a46Tn8QpW4q1HhZK6ih0ajNT+L5Xk0CSxEK/K5EmHSACPhwqjsJztLpGjAdoOn0mA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.0.0-beta.193
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2678,8 +2677,8 @@ packages:
|
||||||
tippy.js: 6.3.7
|
tippy.js: 6.3.7
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-bullet-list/2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a:
|
/@tiptap/extension-bullet-list/2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a:
|
||||||
resolution: {integrity: sha512-T+/pWQNlCz6AsIlx/Aryy3+HUYxMAgS7bTa069GbRAxb0K7aS3+5j9CXfVxUhC+Q+pIqCNUx86MzcY6ea//MBQ==}
|
resolution: {integrity: sha512-gGRQRqdQqCZQstB3ztSy8yzIdm5/5IIYxhCuFNb3Z9c9p/CzyRmaNqa7XkRLrXSajp4lS0OH8RkFUJqL6U+/9w==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.0.0-beta.193
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2695,21 +2694,21 @@ packages:
|
||||||
prosemirror-state: 1.4.1
|
prosemirror-state: 1.4.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-code-block-lowlight/2.0.0-beta.195_ndbd4h3iw4l6bgxmwshcxebdty:
|
/@tiptap/extension-code-block-lowlight/2.0.0-beta.195_tmbwbnesgmzomdlloh7t4ntnfa:
|
||||||
resolution: {integrity: sha512-GolPGlT9wNu9eol7K3i5Oj8woeDE8KM8jaLZT/+Bos0WJGw++atzvlYq7g6SdPxHJl64Q2phWAlx9zvlM/yhXg==}
|
resolution: {integrity: sha512-GolPGlT9wNu9eol7K3i5Oj8woeDE8KM8jaLZT/+Bos0WJGw++atzvlYq7g6SdPxHJl64Q2phWAlx9zvlM/yhXg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.0.0-beta.193
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
'@tiptap/extension-code-block': ^2.0.0-beta.193
|
'@tiptap/extension-code-block': ^2.0.0-beta.193
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.0.0-beta.195
|
'@tiptap/core': 2.0.0-beta.195
|
||||||
'@tiptap/extension-code-block': 2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a
|
'@tiptap/extension-code-block': 2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a
|
||||||
prosemirror-model: 1.18.1
|
prosemirror-model: 1.18.1
|
||||||
prosemirror-state: 1.4.1
|
prosemirror-state: 1.4.1
|
||||||
prosemirror-view: 1.26.2
|
prosemirror-view: 1.26.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-code-block/2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a:
|
/@tiptap/extension-code-block/2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a:
|
||||||
resolution: {integrity: sha512-whDf00B7WkM8pA/f+zyuTa4660mht58DFokBAcWLwmdt8w1r+SDp1C3pZG2AD9pAXMXdfm6DcRNNNgbr4KZ03Q==}
|
resolution: {integrity: sha512-ZfftYE1kHA2pD46hXDkeYd1vuxp3bJLS854B2yHfw1cp3JVDjMXzm4Mzg7zLfr+YV1dT/N/fUfdCg38fqEUCyA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.0.0-beta.193
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2717,24 +2716,24 @@ packages:
|
||||||
prosemirror-state: 1.4.1
|
prosemirror-state: 1.4.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-code/2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a:
|
/@tiptap/extension-code/2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a:
|
||||||
resolution: {integrity: sha512-Azp7ohHCOSVCt48MEBGXiyjaCr50Iw1TCZg8R3uubuHviYbhXQo2vD1z9Njsp1yKYCkO5lc11rvr675U6V4aog==}
|
resolution: {integrity: sha512-P1U/xYD0MLT7JU2OHb3QoW7+JiPZXizFG/gTYmAHQV/gLH87cmflI7pPnloBdTkeIF0Q/cd6sSd75V9FxR4XJA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.0.0-beta.193
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.0.0-beta.195
|
'@tiptap/core': 2.0.0-beta.195
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-document/2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a:
|
/@tiptap/extension-document/2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a:
|
||||||
resolution: {integrity: sha512-q6z+71hgqMKxmi0F+9G4IYarSAGzK99EwvSAcbKa2lld9KS7NEXc2vHPaqECdInnIIziTZfoyBjz/G6weyNXXw==}
|
resolution: {integrity: sha512-l/3k9N2O4wIMQoN/SM3aIBwOhZ2KRxQoqGJfsbAUUwBURBDiT4N2VZaNiJC/w3xCVQXIxHSIlqtm9ZBcZeiH/Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.0.0-beta.193
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.0.0-beta.195
|
'@tiptap/core': 2.0.0-beta.195
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-dropcursor/2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a:
|
/@tiptap/extension-dropcursor/2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a:
|
||||||
resolution: {integrity: sha512-8vIeOkPuonZuK0byEsX8R4c25GAGr6PW/h6+UND2eBhn2TNwiteknJQAawqY17hHVLkUlFcwGt+lt+hmo+zR2A==}
|
resolution: {integrity: sha512-RhdYm0yBJxVLECaHWsZcBIwRJUoUqZ79jvs+kUVodxHW4+IxRAgEA+lImr0GD+kk8aX5Mrk8YhWuUUeu5nzpTg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.0.0-beta.193
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2753,8 +2752,8 @@ packages:
|
||||||
tippy.js: 6.3.7
|
tippy.js: 6.3.7
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-gapcursor/2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a:
|
/@tiptap/extension-gapcursor/2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a:
|
||||||
resolution: {integrity: sha512-6N76xaMGXLjkQN7iSKRrzKICUqiVgARV0PYO9E6uX48/Q5LPXjk48T5fsUbUeT2u+XvJc1nT0m8CMPeRH+3dQA==}
|
resolution: {integrity: sha512-0TDpDfDyay+IbD+wJMsBJ2c0Cq0NtllUOxbi0NPjjWW94Jrvs1yqUSzX4Qp9m5MW8qP24IV6krgZBM1JyQc6ng==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.0.0-beta.193
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2762,24 +2761,24 @@ packages:
|
||||||
prosemirror-gapcursor: 1.3.1
|
prosemirror-gapcursor: 1.3.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-hard-break/2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a:
|
/@tiptap/extension-hard-break/2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a:
|
||||||
resolution: {integrity: sha512-f8BdsqQ4oxXxD5c63guaEz6P+Em7tVHwrc/VnVL79YYKzlzAdg+sIOLTFABuhuGaQHT+jn4dXrBT6rSUyjUcBw==}
|
resolution: {integrity: sha512-DF2wDo/+gSYRhzGowCvZJk3/j/zYJ22BHxZpkAEmLJ69mWSIqZv3S2/brujnNmnji9c3/+JN7ppPSeVykz0b9Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.0.0-beta.193
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.0.0-beta.195
|
'@tiptap/core': 2.0.0-beta.195
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-heading/2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a:
|
/@tiptap/extension-heading/2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a:
|
||||||
resolution: {integrity: sha512-WZOyDPqbdav3K/IKEhdgwIFXrC1eyikMHk4ulXNHhrroNnT9SNVvSBu5vFS2fAA9ynLRAIs0Ita5RX50Zo0qEw==}
|
resolution: {integrity: sha512-WGQ7ET2TBpldrD8JX37OXHXq05LU3OWItIVBs9nKGh4otZTUwPtwfOyMlFfA+IMfQif+ilwLGvUC6EHOw/LwxQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.0.0-beta.193
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.0.0-beta.195
|
'@tiptap/core': 2.0.0-beta.195
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-history/2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a:
|
/@tiptap/extension-history/2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a:
|
||||||
resolution: {integrity: sha512-bBCP+Gk3T8kTBmgebRsPM8/w/m6R1FpkPp9n9SEIMxAKqNqBWShyJOaDaCTqmKvOKM+HqG3D7qdS/smqlN+u/w==}
|
resolution: {integrity: sha512-oZMjKHFqqZuUuf0+IG5+OoKw9DIGilG+v8cm2JK9XnxF5CxF6HIXNDWl3552wRIA+Ro7fBRJEJ//hfJzp0Uhjw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.0.0-beta.193
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2787,8 +2786,8 @@ packages:
|
||||||
prosemirror-history: 1.3.0
|
prosemirror-history: 1.3.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-horizontal-rule/2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a:
|
/@tiptap/extension-horizontal-rule/2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a:
|
||||||
resolution: {integrity: sha512-UFG+asi5cEsVBG1dETnAfC+iSKfegl2A5wKJmGRQk5tiEg8HBpOAcsI1C0W+9+NnwDhpbEHTn64MLs74PnPZHA==}
|
resolution: {integrity: sha512-ISQndGiC6Y3+Ds3OJHKa2iB7s4FkRQxn8US/Hhj4yK7DOifoykLOrgDghwLu0H0dSM8KNb9caYEtmj64vDogNg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.0.0-beta.193
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2804,8 +2803,8 @@ packages:
|
||||||
'@tiptap/core': 2.0.0-beta.195
|
'@tiptap/core': 2.0.0-beta.195
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-italic/2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a:
|
/@tiptap/extension-italic/2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a:
|
||||||
resolution: {integrity: sha512-2IUI7iaXRX2PsnizfDjDhbJ84Ws8OKCb4N5H5ofXv4wPQN6vbt+8Vd0Wwa0FcgWHk7RB8Jk5YX8znwUUHeyUWA==}
|
resolution: {integrity: sha512-jaYJr5ZMxU2swK6h1XJr6Wb1LlWOWbvsX/wo59iZ9KVv1AHiKZlCMcWGThy4aoAs/CUT11pB8qbzyOO163LHZg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.0.0-beta.193
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2823,24 +2822,24 @@ packages:
|
||||||
prosemirror-state: 1.4.1
|
prosemirror-state: 1.4.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-list-item/2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a:
|
/@tiptap/extension-list-item/2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a:
|
||||||
resolution: {integrity: sha512-2P7LeHGbsfSAjdEGinyk7jxSqWHDTF+E1R6Vd+FhpDGdcGoEwYD0QEVIXF3nMPzkYtCYXGfeqasvuf0+bEdyqQ==}
|
resolution: {integrity: sha512-rzcz5MJgoX1M9M9e1iruyRxcwYyYmdCXsl9gB8hhJYh4R+AW1peRmHJ3vVX5oPZXg/tXOMTv/or2x8v30c9tJw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.0.0-beta.193
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.0.0-beta.195
|
'@tiptap/core': 2.0.0-beta.195
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-ordered-list/2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a:
|
/@tiptap/extension-ordered-list/2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a:
|
||||||
resolution: {integrity: sha512-k3DvzfVuZQYr5jcqVnrDT95lTcnuTW6YOcL4mtAWklnWWSVosZ0rDVlVqMuVA/MwTPr57sNTpzb5OM8WN6U/rA==}
|
resolution: {integrity: sha512-ciQhBRtNUudQyCgvQKRZ1WbV7Q9IZP82GHEsk+wScZgI0SsrGY8pnfJT7CyF8aPIjkQkccozKVTbyMrjBOqWSw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.0.0-beta.193
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.0.0-beta.195
|
'@tiptap/core': 2.0.0-beta.195
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-paragraph/2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a:
|
/@tiptap/extension-paragraph/2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a:
|
||||||
resolution: {integrity: sha512-skCdQdZYuWYUmY/4QI1zR8dA+Icu9gerghYv5zGQKJ0DTgGa/FtBelPX6ahEy9EP08/LcRvCuM68ysus6Ouo3g==}
|
resolution: {integrity: sha512-+BoMCaxlsHqw065zTUNd+ywkvFJzNKbTY461/AlKX2dgHeaO8doXHDQK+9icOpibQvrKaMhOJmuBTgGlJlUUgw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.0.0-beta.193
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2858,8 +2857,8 @@ packages:
|
||||||
prosemirror-view: 1.26.2
|
prosemirror-view: 1.26.2
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-strike/2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a:
|
/@tiptap/extension-strike/2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a:
|
||||||
resolution: {integrity: sha512-/Lr8UtOJpybeS2TmztPI0ggBOXkY/qy/9rafJLNZgiVAYguGNWD7BTVZxzJtBG3h4lzqSWvHV0mOANXp/vtOIQ==}
|
resolution: {integrity: sha512-KyN5+d9o9FGvrSiSuh81oo4+XjMDsZVY4UHc9lBY0nAzaGAkJOwkCjk40RfyO5ZJ2GdEEQ6Nh/3YqVMcJTY+rA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.0.0-beta.193
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -2943,8 +2942,8 @@ packages:
|
||||||
'@tiptap/core': 2.0.0-beta.195
|
'@tiptap/core': 2.0.0-beta.195
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@tiptap/extension-text/2.0.0-beta.197_ujyaqudhnf6lg6m3pea2tubg4a:
|
/@tiptap/extension-text/2.0.0-beta.199_ujyaqudhnf6lg6m3pea2tubg4a:
|
||||||
resolution: {integrity: sha512-yNu5/YyEZfCmjAF/N/XbpU8DtRusu0gjQTbdhId1G8GgGpkECYUPS2VGNH4WxfFjJAdMOAUpLFqbmPAn0cXerw==}
|
resolution: {integrity: sha512-ntOqEhkBjDHrdzxvpPe4U1JB5GgE9/yyWqWdgzSL9lpSndRTJN1xQLOmyuv0qsLqOgBHn1YITHvaxPb3t8FrFw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.0.0-beta.193
|
'@tiptap/core': ^2.0.0-beta.193
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -5888,10 +5887,6 @@ packages:
|
||||||
assert-plus: 1.0.0
|
assert-plus: 1.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/github-markdown-css/5.1.0:
|
|
||||||
resolution: {integrity: sha512-QLtORwHHtUHhPMHu7i4GKfP6Vx5CWZn+NKQXe+cBhslY1HEt0CTEkP4d/vSROKV0iIJSpl4UtlQ16AD8C6lMug==}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/glob-parent/5.1.2:
|
/glob-parent/5.1.2:
|
||||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
@ -6681,8 +6676,8 @@ packages:
|
||||||
resolution: {integrity: sha512-iLt/RPnrazs4D4IVaO1Hac2W/WH9SiCT3CuhX9hip0xWVnUe+28Lyse4w/OxjSwdZ0CzduimWPMZH/KwxnCAEw==}
|
resolution: {integrity: sha512-iLt/RPnrazs4D4IVaO1Hac2W/WH9SiCT3CuhX9hip0xWVnUe+28Lyse4w/OxjSwdZ0CzduimWPMZH/KwxnCAEw==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/katex/0.16.2:
|
/katex/0.16.3:
|
||||||
resolution: {integrity: sha512-70DJdQAyh9EMsthw3AaQlDyFf54X7nWEUIa5W+rq8XOpEk//w5Th7/8SqFqpvi/KZ2t6MHUj4f9wLmztBmAYQA==}
|
resolution: {integrity: sha512-3EykQddareoRmbtNiNEDgl3IGjryyrp2eg/25fHDEnlHymIDi33bptkMv6K4EOC2LZCybLW/ZkEo6Le+EM9pmA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
dependencies:
|
dependencies:
|
||||||
commander: 8.3.0
|
commander: 8.3.0
|
||||||
|
|
|
@ -0,0 +1,485 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {
|
||||||
|
Extension,
|
||||||
|
RichTextEditor,
|
||||||
|
useEditor,
|
||||||
|
ExtensionBlockquote,
|
||||||
|
ExtensionBold,
|
||||||
|
ExtensionBulletList,
|
||||||
|
ExtensionCode,
|
||||||
|
ExtensionDocument,
|
||||||
|
ExtensionDropcursor,
|
||||||
|
ExtensionGapcursor,
|
||||||
|
ExtensionHardBreak,
|
||||||
|
ExtensionHeading,
|
||||||
|
ExtensionHistory,
|
||||||
|
ExtensionHorizontalRule,
|
||||||
|
ExtensionItalic,
|
||||||
|
ExtensionListItem,
|
||||||
|
ExtensionOrderedList,
|
||||||
|
ExtensionParagraph,
|
||||||
|
ExtensionStrike,
|
||||||
|
ExtensionText,
|
||||||
|
ExtensionImage,
|
||||||
|
ExtensionTaskList,
|
||||||
|
ExtensionTaskItem,
|
||||||
|
ExtensionLink,
|
||||||
|
ExtensionTextAlign,
|
||||||
|
ExtensionUnderline,
|
||||||
|
ExtensionTable,
|
||||||
|
ExtensionTableHeader,
|
||||||
|
ExtensionTableCell,
|
||||||
|
ExtensionTableRow,
|
||||||
|
ExtensionSubscript,
|
||||||
|
ExtensionSuperscript,
|
||||||
|
ExtensionPlaceholder,
|
||||||
|
ExtensionCommands,
|
||||||
|
CommandsSuggestion,
|
||||||
|
CommandHeader1,
|
||||||
|
CommandHeader2,
|
||||||
|
CommandHeader3,
|
||||||
|
CommandHeader4,
|
||||||
|
CommandHeader5,
|
||||||
|
CommandHeader6,
|
||||||
|
CommandCodeBlock,
|
||||||
|
ExtensionCodeBlock,
|
||||||
|
lowlight,
|
||||||
|
UndoMenuItem,
|
||||||
|
RedoMenuItem,
|
||||||
|
BoldMenuItem,
|
||||||
|
ItalicMenuItem,
|
||||||
|
UnderlineMenuItem,
|
||||||
|
StrikeMenuItem,
|
||||||
|
QuoteMenuItem,
|
||||||
|
CodeMenuItem,
|
||||||
|
SuperScriptMenuItem,
|
||||||
|
SubScriptMenuItem,
|
||||||
|
CodeBlockMenuItem,
|
||||||
|
HeadingMenuItem,
|
||||||
|
AlignLeftMenuItem,
|
||||||
|
AlignCenterMenuItem,
|
||||||
|
AlignRightMenuItem,
|
||||||
|
AlignJustifyMenuItem,
|
||||||
|
} from "@halo-dev/richtext-editor";
|
||||||
|
import {
|
||||||
|
IconCalendar,
|
||||||
|
IconCharacterRecognition,
|
||||||
|
IconLink,
|
||||||
|
IconUserFollow,
|
||||||
|
VTabItem,
|
||||||
|
VTabs,
|
||||||
|
} from "@halo-dev/components";
|
||||||
|
import AttachmentSelectorModal from "@/modules/contents/attachments/components/AttachmentSelectorModal.vue";
|
||||||
|
import ExtensionCharacterCount from "@tiptap/extension-character-count";
|
||||||
|
import MdiFileImageBox from "~icons/mdi/file-image-box";
|
||||||
|
import MdiFormatHeader1 from "~icons/mdi/format-header-1";
|
||||||
|
import MdiFormatHeader2 from "~icons/mdi/format-header-2";
|
||||||
|
import MdiFormatHeader3 from "~icons/mdi/format-header-3";
|
||||||
|
import MdiFormatHeader4 from "~icons/mdi/format-header-4";
|
||||||
|
import MdiFormatHeader5 from "~icons/mdi/format-header-5";
|
||||||
|
import MdiFormatHeader6 from "~icons/mdi/format-header-6";
|
||||||
|
import { computed, markRaw, nextTick, ref, watch } from "vue";
|
||||||
|
import { formatDatetime } from "@/utils/date";
|
||||||
|
import { useAttachmentSelect } from "@/modules/contents/attachments/composables/use-attachment";
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue?: string;
|
||||||
|
owner?: string;
|
||||||
|
permalink?: string;
|
||||||
|
publishTime?: string | null;
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
modelValue: "",
|
||||||
|
owner: undefined,
|
||||||
|
permalink: undefined,
|
||||||
|
publishTime: undefined,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "update:modelValue", value: string): void;
|
||||||
|
(event: "update", value: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
interface HeadingNode {
|
||||||
|
id: string;
|
||||||
|
level: number;
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headingIcons = {
|
||||||
|
1: markRaw(MdiFormatHeader1),
|
||||||
|
2: markRaw(MdiFormatHeader2),
|
||||||
|
3: markRaw(MdiFormatHeader3),
|
||||||
|
4: markRaw(MdiFormatHeader4),
|
||||||
|
5: markRaw(MdiFormatHeader5),
|
||||||
|
6: markRaw(MdiFormatHeader6),
|
||||||
|
};
|
||||||
|
|
||||||
|
const headingNodes = ref<HeadingNode[]>();
|
||||||
|
const selectedHeadingNode = ref<HeadingNode>();
|
||||||
|
const extraActiveId = ref("toc");
|
||||||
|
const attachmentSelectorModal = ref(false);
|
||||||
|
|
||||||
|
const editor = useEditor({
|
||||||
|
content: props.modelValue,
|
||||||
|
extensions: [
|
||||||
|
ExtensionBlockquote,
|
||||||
|
ExtensionBold,
|
||||||
|
ExtensionBulletList,
|
||||||
|
ExtensionCode,
|
||||||
|
ExtensionDocument,
|
||||||
|
ExtensionDropcursor,
|
||||||
|
ExtensionGapcursor,
|
||||||
|
ExtensionHardBreak,
|
||||||
|
ExtensionHeading,
|
||||||
|
ExtensionHistory,
|
||||||
|
ExtensionHorizontalRule,
|
||||||
|
ExtensionItalic,
|
||||||
|
ExtensionListItem,
|
||||||
|
ExtensionOrderedList,
|
||||||
|
ExtensionParagraph,
|
||||||
|
ExtensionStrike,
|
||||||
|
ExtensionText,
|
||||||
|
ExtensionImage.configure({
|
||||||
|
inline: true,
|
||||||
|
HTMLAttributes: {
|
||||||
|
loading: "lazy",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
ExtensionTaskList,
|
||||||
|
ExtensionTaskItem,
|
||||||
|
ExtensionLink.configure({
|
||||||
|
autolink: true,
|
||||||
|
openOnClick: false,
|
||||||
|
}),
|
||||||
|
ExtensionTextAlign.configure({
|
||||||
|
types: ["heading", "paragraph"],
|
||||||
|
}),
|
||||||
|
ExtensionUnderline,
|
||||||
|
ExtensionTable.configure({
|
||||||
|
resizable: true,
|
||||||
|
}),
|
||||||
|
ExtensionTableHeader,
|
||||||
|
ExtensionTableCell,
|
||||||
|
ExtensionTableRow,
|
||||||
|
ExtensionSubscript,
|
||||||
|
ExtensionSuperscript,
|
||||||
|
ExtensionPlaceholder.configure({
|
||||||
|
placeholder: "输入 / 以选择输入类型",
|
||||||
|
}),
|
||||||
|
ExtensionCommands.configure({
|
||||||
|
suggestion: {
|
||||||
|
...CommandsSuggestion,
|
||||||
|
items: ({ query }: { query: string }) => {
|
||||||
|
return [
|
||||||
|
CommandHeader1,
|
||||||
|
CommandHeader2,
|
||||||
|
CommandHeader3,
|
||||||
|
CommandHeader4,
|
||||||
|
CommandHeader5,
|
||||||
|
CommandHeader6,
|
||||||
|
CommandCodeBlock,
|
||||||
|
].filter((item) =>
|
||||||
|
[...item.keywords, item.title].some((keyword) =>
|
||||||
|
keyword.includes(query)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
ExtensionCodeBlock.configure({
|
||||||
|
lowlight,
|
||||||
|
}),
|
||||||
|
ExtensionCharacterCount,
|
||||||
|
Extension.create({
|
||||||
|
addGlobalAttributes() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
types: ["heading"],
|
||||||
|
attributes: {
|
||||||
|
id: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
autofocus: "start",
|
||||||
|
onUpdate: () => {
|
||||||
|
emit("update:modelValue", editor.value?.getHTML() + "");
|
||||||
|
emit("update", editor.value?.getHTML() + "");
|
||||||
|
nextTick(() => {
|
||||||
|
handleGenerateTableOfContent();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const toolbarMenuItems = computed(() => {
|
||||||
|
if (!editor.value) return [];
|
||||||
|
return [
|
||||||
|
UndoMenuItem(editor.value),
|
||||||
|
RedoMenuItem(editor.value),
|
||||||
|
BoldMenuItem(editor.value),
|
||||||
|
ItalicMenuItem(editor.value),
|
||||||
|
UnderlineMenuItem(editor.value),
|
||||||
|
StrikeMenuItem(editor.value),
|
||||||
|
QuoteMenuItem(editor.value),
|
||||||
|
CodeMenuItem(editor.value),
|
||||||
|
SuperScriptMenuItem(editor.value),
|
||||||
|
SubScriptMenuItem(editor.value),
|
||||||
|
CodeBlockMenuItem(editor.value),
|
||||||
|
HeadingMenuItem(editor.value),
|
||||||
|
AlignLeftMenuItem(editor.value),
|
||||||
|
AlignCenterMenuItem(editor.value),
|
||||||
|
AlignRightMenuItem(editor.value),
|
||||||
|
AlignJustifyMenuItem(editor.value),
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
icon: markRaw(MdiFileImageBox),
|
||||||
|
title: "SuperScript",
|
||||||
|
action: () => (attachmentSelectorModal.value = true),
|
||||||
|
isActive: () => false,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const bubbleMenuItems = computed(() => {
|
||||||
|
if (!editor.value) return [];
|
||||||
|
return [
|
||||||
|
BoldMenuItem(editor.value),
|
||||||
|
ItalicMenuItem(editor.value),
|
||||||
|
UnderlineMenuItem(editor.value),
|
||||||
|
StrikeMenuItem(editor.value),
|
||||||
|
QuoteMenuItem(editor.value),
|
||||||
|
CodeMenuItem(editor.value),
|
||||||
|
CodeBlockMenuItem(editor.value),
|
||||||
|
SuperScriptMenuItem(editor.value),
|
||||||
|
SubScriptMenuItem(editor.value),
|
||||||
|
AlignLeftMenuItem(editor.value),
|
||||||
|
AlignCenterMenuItem(editor.value),
|
||||||
|
AlignRightMenuItem(editor.value),
|
||||||
|
AlignJustifyMenuItem(editor.value),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleGenerateTableOfContent = () => {
|
||||||
|
if (!editor.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headings: HeadingNode[] = [];
|
||||||
|
const transaction = editor.value.state.tr;
|
||||||
|
|
||||||
|
editor.value.state.doc.descendants((node, pos) => {
|
||||||
|
if (node.type.name === "heading") {
|
||||||
|
const id = `heading-${headings.length + 1}`;
|
||||||
|
|
||||||
|
if (node.attrs.id !== id) {
|
||||||
|
transaction?.setNodeMarkup(pos, undefined, {
|
||||||
|
...node.attrs,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
headings.push({
|
||||||
|
level: node.attrs.level,
|
||||||
|
text: node.textContent,
|
||||||
|
id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
transaction.setMeta("addToHistory", false);
|
||||||
|
transaction.setMeta("preventUpdate", true);
|
||||||
|
|
||||||
|
editor.value.view.dispatch(transaction);
|
||||||
|
|
||||||
|
headingNodes.value = headings;
|
||||||
|
|
||||||
|
if (!selectedHeadingNode.value) {
|
||||||
|
selectedHeadingNode.value = headings[0];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectHeadingNode = (node: HeadingNode) => {
|
||||||
|
selectedHeadingNode.value = node;
|
||||||
|
document.getElementById(node.id)?.scrollIntoView({ behavior: "smooth" });
|
||||||
|
};
|
||||||
|
|
||||||
|
const { onAttachmentSelect } = useAttachmentSelect(editor);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.modelValue,
|
||||||
|
() => {
|
||||||
|
if (props.modelValue !== editor.value?.getHTML()) {
|
||||||
|
editor.value?.commands.setContent(props.modelValue);
|
||||||
|
nextTick(() => {
|
||||||
|
handleGenerateTableOfContent();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AttachmentSelectorModal
|
||||||
|
v-model:visible="attachmentSelectorModal"
|
||||||
|
@select="onAttachmentSelect"
|
||||||
|
/>
|
||||||
|
<RichTextEditor
|
||||||
|
v-if="editor"
|
||||||
|
:editor="editor"
|
||||||
|
:toolbar-menu-items="toolbarMenuItems"
|
||||||
|
:bubble-menu-items="bubbleMenuItems"
|
||||||
|
>
|
||||||
|
<template #extra>
|
||||||
|
<div class="h-full w-72 overflow-y-auto border-l bg-white">
|
||||||
|
<VTabs v-model:active-id="extraActiveId" type="outline">
|
||||||
|
<VTabItem id="toc" label="大纲">
|
||||||
|
<div class="p-1 pt-0">
|
||||||
|
<ul class="space-y-1">
|
||||||
|
<li
|
||||||
|
v-for="(node, index) in headingNodes"
|
||||||
|
:key="index"
|
||||||
|
:class="[
|
||||||
|
{ 'bg-gray-100': node.id === selectedHeadingNode?.id },
|
||||||
|
]"
|
||||||
|
class="group cursor-pointer truncate rounded-base px-1.5 py-1 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||||
|
@click="handleSelectHeadingNode(node)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
paddingLeft: `${(node.level - 1) * 0.8}rem`,
|
||||||
|
}"
|
||||||
|
class="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<component
|
||||||
|
:is="headingIcons[node.level]"
|
||||||
|
class="h-4 w-4 rounded-sm bg-gray-100 p-0.5 group-hover:bg-white"
|
||||||
|
:class="[
|
||||||
|
{ '!bg-white': node.id === selectedHeadingNode?.id },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
<span class="flex-1 truncate">{{ node.text }}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</VTabItem>
|
||||||
|
<VTabItem id="information" label="详情">
|
||||||
|
<div class="flex flex-col gap-2 p-1 pt-0">
|
||||||
|
<div class="grid grid-cols-2 gap-2">
|
||||||
|
<div
|
||||||
|
class="group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div
|
||||||
|
class="text-sm text-gray-500 group-hover:text-gray-900"
|
||||||
|
>
|
||||||
|
字符数
|
||||||
|
</div>
|
||||||
|
<div class="rounded bg-gray-200 p-0.5">
|
||||||
|
<IconCharacterRecognition
|
||||||
|
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-base font-medium text-gray-900">
|
||||||
|
{{ editor.storage.characterCount.characters() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div
|
||||||
|
class="text-sm text-gray-500 group-hover:text-gray-900"
|
||||||
|
>
|
||||||
|
词数
|
||||||
|
</div>
|
||||||
|
<div class="rounded bg-gray-200 p-0.5">
|
||||||
|
<IconCharacterRecognition
|
||||||
|
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-base font-medium text-gray-900">
|
||||||
|
{{ editor.storage.characterCount.words() }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="publishTime" class="grid grid-cols-1 gap-2">
|
||||||
|
<div
|
||||||
|
class="group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div
|
||||||
|
class="text-sm text-gray-500 group-hover:text-gray-900"
|
||||||
|
>
|
||||||
|
发布时间
|
||||||
|
</div>
|
||||||
|
<div class="rounded bg-gray-200 p-0.5">
|
||||||
|
<IconCalendar
|
||||||
|
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-base font-medium text-gray-900">
|
||||||
|
{{ formatDatetime(publishTime) || "未发布" }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="owner" class="grid grid-cols-1 gap-2">
|
||||||
|
<div
|
||||||
|
class="group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div
|
||||||
|
class="text-sm text-gray-500 group-hover:text-gray-900"
|
||||||
|
>
|
||||||
|
创建者
|
||||||
|
</div>
|
||||||
|
<div class="rounded bg-gray-200 p-0.5">
|
||||||
|
<IconUserFollow
|
||||||
|
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-base font-medium text-gray-900">
|
||||||
|
{{ owner }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="permalink" class="grid grid-cols-1 gap-2">
|
||||||
|
<div
|
||||||
|
class="group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div
|
||||||
|
class="text-sm text-gray-500 group-hover:text-gray-900"
|
||||||
|
>
|
||||||
|
访问链接
|
||||||
|
</div>
|
||||||
|
<div class="rounded bg-gray-200 p-0.5">
|
||||||
|
<IconLink
|
||||||
|
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-gray-900 hover:text-blue-600">
|
||||||
|
{{ permalink }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</VTabItem>
|
||||||
|
</VTabs>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</RichTextEditor>
|
||||||
|
</template>
|
|
@ -62,6 +62,8 @@ const searchResults = computed((): SearchableItem[] => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleBuildSearchIndex = () => {
|
const handleBuildSearchIndex = () => {
|
||||||
|
fuse.remove(() => true);
|
||||||
|
|
||||||
const routes = router.getRoutes().filter((route) => {
|
const routes = router.getRoutes().filter((route) => {
|
||||||
return !!route.meta?.title && route.meta?.searchable;
|
return !!route.meta?.title && route.meta?.searchable;
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,22 +6,15 @@ import {
|
||||||
VButton,
|
VButton,
|
||||||
IconSave,
|
IconSave,
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
|
import DefaultEditor from "@/components/editor/DefaultEditor.vue";
|
||||||
import SinglePageSettingModal from "./components/SinglePageSettingModal.vue";
|
import SinglePageSettingModal from "./components/SinglePageSettingModal.vue";
|
||||||
import PostPreviewModal from "../posts/components/PostPreviewModal.vue";
|
import PostPreviewModal from "../posts/components/PostPreviewModal.vue";
|
||||||
import AttachmentSelectorModal from "../attachments/components/AttachmentSelectorModal.vue";
|
|
||||||
import {
|
|
||||||
allExtensions,
|
|
||||||
RichTextEditor,
|
|
||||||
useEditor,
|
|
||||||
} from "@halo-dev/richtext-editor";
|
|
||||||
import type { SinglePageRequest } from "@halo-dev/api-client";
|
import type { SinglePageRequest } from "@halo-dev/api-client";
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
import { computed, onMounted, ref, watch } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
import { useAttachmentSelect } from "../attachments/composables/use-attachment";
|
|
||||||
import MdiFileImageBox from "~icons/mdi/file-image-box";
|
|
||||||
|
|
||||||
const initialFormState: SinglePageRequest = {
|
const initialFormState: SinglePageRequest = {
|
||||||
page: {
|
page: {
|
||||||
|
@ -61,37 +54,11 @@ const formState = ref<SinglePageRequest>(cloneDeep(initialFormState));
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const settingModal = ref(false);
|
const settingModal = ref(false);
|
||||||
const previewModal = ref(false);
|
const previewModal = ref(false);
|
||||||
const attachmentSelectorModal = ref(false);
|
|
||||||
|
|
||||||
const isUpdateMode = computed(() => {
|
const isUpdateMode = computed(() => {
|
||||||
return !!formState.value.page.metadata.creationTimestamp;
|
return !!formState.value.page.metadata.creationTimestamp;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Editor
|
|
||||||
const editor = useEditor({
|
|
||||||
content: formState.value.content.raw,
|
|
||||||
extensions: [...allExtensions],
|
|
||||||
autofocus: "start",
|
|
||||||
onUpdate: () => {
|
|
||||||
formState.value.content.raw = editor.value?.getHTML() + "";
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => formState.value.content.raw,
|
|
||||||
(newValue) => {
|
|
||||||
const isSame = editor.value?.getHTML() === newValue;
|
|
||||||
|
|
||||||
if (isSame) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.value?.commands.setContent(newValue as string, false);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const { onAttachmentSelect } = useAttachmentSelect(editor);
|
|
||||||
|
|
||||||
const routeQueryName = useRouteQuery<string>("name");
|
const routeQueryName = useRouteQuery<string>("name");
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
|
@ -172,10 +139,6 @@ onMounted(async () => {
|
||||||
@saved="onSettingSaved"
|
@saved="onSettingSaved"
|
||||||
/>
|
/>
|
||||||
<PostPreviewModal v-model:visible="previewModal" />
|
<PostPreviewModal v-model:visible="previewModal" />
|
||||||
<AttachmentSelectorModal
|
|
||||||
v-model:visible="attachmentSelectorModal"
|
|
||||||
@select="onAttachmentSelect"
|
|
||||||
/>
|
|
||||||
<VPageHeader title="自定义页面">
|
<VPageHeader title="自定义页面">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconPages class="mr-2 self-center" />
|
<IconPages class="mr-2 self-center" />
|
||||||
|
@ -204,19 +167,11 @@ onMounted(async () => {
|
||||||
</template>
|
</template>
|
||||||
</VPageHeader>
|
</VPageHeader>
|
||||||
<div class="editor border-t" style="height: calc(100vh - 3.5rem)">
|
<div class="editor border-t" style="height: calc(100vh - 3.5rem)">
|
||||||
<RichTextEditor
|
<DefaultEditor
|
||||||
v-if="editor"
|
v-model="formState.content.raw"
|
||||||
:editor="editor"
|
:owner="formState.page.spec.owner"
|
||||||
:additional-menu-items="[
|
:permalink="formState.page.status?.permalink"
|
||||||
{
|
:publish-time="formState.page.spec.publishTime"
|
||||||
type: 'button',
|
/>
|
||||||
icon: MdiFileImageBox,
|
|
||||||
title: 'SuperScript',
|
|
||||||
action: () => (attachmentSelectorModal = true),
|
|
||||||
isActive: () => false,
|
|
||||||
},
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
</RichTextEditor>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,42 +1,20 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {
|
import {
|
||||||
IconBookRead,
|
IconBookRead,
|
||||||
IconCalendar,
|
|
||||||
IconCharacterRecognition,
|
|
||||||
IconLink,
|
|
||||||
IconSave,
|
IconSave,
|
||||||
IconUserFollow,
|
|
||||||
VButton,
|
VButton,
|
||||||
VPageHeader,
|
VPageHeader,
|
||||||
VSpace,
|
VSpace,
|
||||||
VTabItem,
|
|
||||||
VTabs,
|
|
||||||
} from "@halo-dev/components";
|
} from "@halo-dev/components";
|
||||||
|
import DefaultEditor from "@/components/editor/DefaultEditor.vue";
|
||||||
import PostSettingModal from "./components/PostSettingModal.vue";
|
import PostSettingModal from "./components/PostSettingModal.vue";
|
||||||
import PostPreviewModal from "./components/PostPreviewModal.vue";
|
import PostPreviewModal from "./components/PostPreviewModal.vue";
|
||||||
import AttachmentSelectorModal from "../attachments/components/AttachmentSelectorModal.vue";
|
|
||||||
import type { PostRequest } from "@halo-dev/api-client";
|
import type { PostRequest } from "@halo-dev/api-client";
|
||||||
import { computed, markRaw, nextTick, onMounted, ref, watch } from "vue";
|
import { computed, onMounted, ref } from "vue";
|
||||||
import cloneDeep from "lodash.clonedeep";
|
import cloneDeep from "lodash.clonedeep";
|
||||||
import { apiClient } from "@/utils/api-client";
|
import { apiClient } from "@/utils/api-client";
|
||||||
import { useRouteQuery } from "@vueuse/router";
|
import { useRouteQuery } from "@vueuse/router";
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
import {
|
|
||||||
allExtensions,
|
|
||||||
Extension,
|
|
||||||
RichTextEditor,
|
|
||||||
useEditor,
|
|
||||||
} from "@halo-dev/richtext-editor";
|
|
||||||
import ExtensionCharacterCount from "@tiptap/extension-character-count";
|
|
||||||
import { formatDatetime } from "@/utils/date";
|
|
||||||
import { useAttachmentSelect } from "../attachments/composables/use-attachment";
|
|
||||||
import MdiFileImageBox from "~icons/mdi/file-image-box";
|
|
||||||
import MdiFormatHeader1 from "~icons/mdi/format-header-1";
|
|
||||||
import MdiFormatHeader2 from "~icons/mdi/format-header-2";
|
|
||||||
import MdiFormatHeader3 from "~icons/mdi/format-header-3";
|
|
||||||
import MdiFormatHeader4 from "~icons/mdi/format-header-4";
|
|
||||||
import MdiFormatHeader5 from "~icons/mdi/format-header-5";
|
|
||||||
import MdiFormatHeader6 from "~icons/mdi/format-header-6";
|
|
||||||
|
|
||||||
const initialFormState: PostRequest = {
|
const initialFormState: PostRequest = {
|
||||||
post: {
|
post: {
|
||||||
|
@ -78,118 +56,11 @@ const formState = ref<PostRequest>(cloneDeep(initialFormState));
|
||||||
const settingModal = ref(false);
|
const settingModal = ref(false);
|
||||||
const previewModal = ref(false);
|
const previewModal = ref(false);
|
||||||
const saving = ref(false);
|
const saving = ref(false);
|
||||||
const extraActiveId = ref("toc");
|
|
||||||
const attachmentSelectorModal = ref(false);
|
|
||||||
|
|
||||||
const isUpdateMode = computed(() => {
|
const isUpdateMode = computed(() => {
|
||||||
return !!formState.value.post.metadata.creationTimestamp;
|
return !!formState.value.post.metadata.creationTimestamp;
|
||||||
});
|
});
|
||||||
|
|
||||||
interface HeadingNode {
|
|
||||||
id: string;
|
|
||||||
level: number;
|
|
||||||
text: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const headingIcons = {
|
|
||||||
1: markRaw(MdiFormatHeader1),
|
|
||||||
2: markRaw(MdiFormatHeader2),
|
|
||||||
3: markRaw(MdiFormatHeader3),
|
|
||||||
4: markRaw(MdiFormatHeader4),
|
|
||||||
5: markRaw(MdiFormatHeader5),
|
|
||||||
6: markRaw(MdiFormatHeader6),
|
|
||||||
};
|
|
||||||
|
|
||||||
const headingNodes = ref<HeadingNode[]>();
|
|
||||||
const selectedHeadingNode = ref<HeadingNode>();
|
|
||||||
const editor = useEditor({
|
|
||||||
content: formState.value.content.raw,
|
|
||||||
extensions: [
|
|
||||||
...allExtensions,
|
|
||||||
ExtensionCharacterCount,
|
|
||||||
Extension.create({
|
|
||||||
addGlobalAttributes() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
types: ["heading"],
|
|
||||||
attributes: {
|
|
||||||
id: {
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
autofocus: "start",
|
|
||||||
onUpdate: () => {
|
|
||||||
formState.value.content.raw = editor.value?.getHTML() + "";
|
|
||||||
nextTick(() => {
|
|
||||||
handleGenerateTableOfContent();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => formState.value.content.raw,
|
|
||||||
(newValue) => {
|
|
||||||
const isSame = editor.value?.getHTML() === newValue;
|
|
||||||
|
|
||||||
if (isSame) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.value?.commands.setContent(newValue as string, false);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const { onAttachmentSelect } = useAttachmentSelect(editor);
|
|
||||||
|
|
||||||
const handleGenerateTableOfContent = () => {
|
|
||||||
if (!editor.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const headings: HeadingNode[] = [];
|
|
||||||
const transaction = editor.value.state.tr;
|
|
||||||
|
|
||||||
editor.value.state.doc.descendants((node, pos) => {
|
|
||||||
if (node.type.name === "heading") {
|
|
||||||
const id = `heading-${headings.length + 1}`;
|
|
||||||
|
|
||||||
if (node.attrs.id !== id) {
|
|
||||||
transaction?.setNodeMarkup(pos, undefined, {
|
|
||||||
...node.attrs,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
headings.push({
|
|
||||||
level: node.attrs.level,
|
|
||||||
text: node.textContent,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
transaction.setMeta("addToHistory", false);
|
|
||||||
transaction.setMeta("preventUpdate", true);
|
|
||||||
|
|
||||||
editor.value.view.dispatch(transaction);
|
|
||||||
|
|
||||||
headingNodes.value = headings;
|
|
||||||
|
|
||||||
if (!selectedHeadingNode.value) {
|
|
||||||
selectedHeadingNode.value = headings[0];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectHeadingNode = (node: HeadingNode) => {
|
|
||||||
selectedHeadingNode.value = node;
|
|
||||||
document.getElementById(node.id)?.scrollIntoView({ behavior: "smooth" });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
try {
|
try {
|
||||||
saving.value = true;
|
saving.value = true;
|
||||||
|
@ -261,7 +132,6 @@ onMounted(async () => {
|
||||||
|
|
||||||
// fetch post content
|
// fetch post content
|
||||||
await handleFetchContent();
|
await handleFetchContent();
|
||||||
handleGenerateTableOfContent();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -273,10 +143,6 @@ onMounted(async () => {
|
||||||
@saved="onSettingSaved"
|
@saved="onSettingSaved"
|
||||||
/>
|
/>
|
||||||
<PostPreviewModal v-model:visible="previewModal" :post="formState.post" />
|
<PostPreviewModal v-model:visible="previewModal" :post="formState.post" />
|
||||||
<AttachmentSelectorModal
|
|
||||||
v-model:visible="attachmentSelectorModal"
|
|
||||||
@select="onAttachmentSelect"
|
|
||||||
/>
|
|
||||||
<VPageHeader title="文章">
|
<VPageHeader title="文章">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<IconBookRead class="mr-2 self-center" />
|
<IconBookRead class="mr-2 self-center" />
|
||||||
|
@ -305,167 +171,11 @@ onMounted(async () => {
|
||||||
</template>
|
</template>
|
||||||
</VPageHeader>
|
</VPageHeader>
|
||||||
<div class="editor border-t" style="height: calc(100vh - 3.5rem)">
|
<div class="editor border-t" style="height: calc(100vh - 3.5rem)">
|
||||||
<RichTextEditor
|
<DefaultEditor
|
||||||
v-if="editor"
|
v-model="formState.content.raw"
|
||||||
:editor="editor"
|
:owner="formState.post.spec.owner"
|
||||||
:additional-menu-items="[
|
:permalink="formState.post.status?.permalink"
|
||||||
{
|
:publish-time="formState.post.spec.publishTime"
|
||||||
type: 'button',
|
/>
|
||||||
icon: MdiFileImageBox,
|
|
||||||
title: 'SuperScript',
|
|
||||||
action: () => (attachmentSelectorModal = true),
|
|
||||||
isActive: () => false,
|
|
||||||
},
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<template #extra>
|
|
||||||
<div class="h-full w-72 overflow-y-auto border-l bg-white">
|
|
||||||
<VTabs v-model:active-id="extraActiveId" type="outline">
|
|
||||||
<VTabItem id="toc" label="大纲">
|
|
||||||
<div class="p-1 pt-0">
|
|
||||||
<ul class="space-y-1">
|
|
||||||
<li
|
|
||||||
v-for="(node, index) in headingNodes"
|
|
||||||
:key="index"
|
|
||||||
:class="[
|
|
||||||
{ 'bg-gray-100': node.id === selectedHeadingNode?.id },
|
|
||||||
]"
|
|
||||||
class="group cursor-pointer truncate rounded-base px-1.5 py-1 text-sm text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
|
||||||
@click="handleSelectHeadingNode(node)"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
:style="{
|
|
||||||
paddingLeft: `${(node.level - 1) * 0.8}rem`,
|
|
||||||
}"
|
|
||||||
class="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<component
|
|
||||||
:is="headingIcons[node.level]"
|
|
||||||
class="h-4 w-4 rounded-sm bg-gray-100 p-0.5 group-hover:bg-white"
|
|
||||||
:class="[
|
|
||||||
{ '!bg-white': node.id === selectedHeadingNode?.id },
|
|
||||||
]"
|
|
||||||
/>
|
|
||||||
<span class="flex-1 truncate">{{ node.text }}</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</VTabItem>
|
|
||||||
<VTabItem id="information" label="详情">
|
|
||||||
<div class="flex flex-col gap-2 p-1 pt-0">
|
|
||||||
<div class="grid grid-cols-2 gap-2">
|
|
||||||
<div
|
|
||||||
class="group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div
|
|
||||||
class="text-sm text-gray-500 group-hover:text-gray-900"
|
|
||||||
>
|
|
||||||
字符数
|
|
||||||
</div>
|
|
||||||
<div class="rounded bg-gray-200 p-0.5">
|
|
||||||
<IconCharacterRecognition
|
|
||||||
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-base font-medium text-gray-900">
|
|
||||||
{{ editor.storage.characterCount.characters() }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div
|
|
||||||
class="text-sm text-gray-500 group-hover:text-gray-900"
|
|
||||||
>
|
|
||||||
词数
|
|
||||||
</div>
|
|
||||||
<div class="rounded bg-gray-200 p-0.5">
|
|
||||||
<IconCharacterRecognition
|
|
||||||
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-base font-medium text-gray-900">
|
|
||||||
{{ editor.storage.characterCount.words() }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-2">
|
|
||||||
<div
|
|
||||||
class="group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div
|
|
||||||
class="text-sm text-gray-500 group-hover:text-gray-900"
|
|
||||||
>
|
|
||||||
发布时间
|
|
||||||
</div>
|
|
||||||
<div class="rounded bg-gray-200 p-0.5">
|
|
||||||
<IconCalendar
|
|
||||||
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-base font-medium text-gray-900">
|
|
||||||
{{
|
|
||||||
formatDatetime(formState.post.spec.publishTime) ||
|
|
||||||
"未发布"
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-1 gap-2">
|
|
||||||
<div
|
|
||||||
class="group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div
|
|
||||||
class="text-sm text-gray-500 group-hover:text-gray-900"
|
|
||||||
>
|
|
||||||
创建者
|
|
||||||
</div>
|
|
||||||
<div class="rounded bg-gray-200 p-0.5">
|
|
||||||
<IconUserFollow
|
|
||||||
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-base font-medium text-gray-900">
|
|
||||||
{{ formState.post.spec.owner }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-1 gap-2">
|
|
||||||
<div
|
|
||||||
class="group flex cursor-pointer flex-col gap-y-5 rounded-md bg-gray-100 px-1.5 py-1 transition-all"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div
|
|
||||||
class="text-sm text-gray-500 group-hover:text-gray-900"
|
|
||||||
>
|
|
||||||
访问链接
|
|
||||||
</div>
|
|
||||||
<div class="rounded bg-gray-200 p-0.5">
|
|
||||||
<IconLink
|
|
||||||
class="h-4 w-4 text-gray-600 group-hover:text-gray-900"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="text-sm text-gray-900 hover:text-blue-600">
|
|
||||||
{{ formState.post.status?.["permalink"] }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</VTabItem>
|
|
||||||
</VTabs>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</RichTextEditor>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
Loading…
Reference in New Issue