From 91b20549fe9c877fa6badaf21e9d926e956f32db Mon Sep 17 00:00:00 2001 From: LIlGG <1103069291@qq.com> Date: Thu, 1 Aug 2024 16:08:38 +0800 Subject: [PATCH 1/8] refactor: editor code block to be extensible by plugins --- ui/packages/editor/package.json | 3 - ui/packages/editor/src/dev/App.vue | 5 +- .../code-block/CodeBlockViewRenderer.vue | 87 +++- .../src/extensions/code-block/Select.vue | 226 ++++++++++ .../src/extensions/code-block/code-block.ts | 52 ++- .../editor/src/extensions/code-block/index.ts | 2 +- .../src/extensions/code-block/lowlight.ts | 8 - ui/packages/editor/src/extensions/index.ts | 12 +- ui/packages/editor/src/index.ts | 1 - ui/packages/editor/src/styles/base.scss | 4 + ui/pnpm-lock.yaml | 64 +-- ui/src/components/editor/DefaultEditor.vue | 388 +++++++++--------- .../editor/composables/use-extension.ts | 94 +++++ 13 files changed, 639 insertions(+), 307 deletions(-) create mode 100644 ui/packages/editor/src/extensions/code-block/Select.vue delete mode 100644 ui/packages/editor/src/extensions/code-block/lowlight.ts create mode 100644 ui/src/components/editor/composables/use-extension.ts diff --git a/ui/packages/editor/package.json b/ui/packages/editor/package.json index e122b8408b..ab752a3ab6 100644 --- a/ui/packages/editor/package.json +++ b/ui/packages/editor/package.json @@ -47,7 +47,6 @@ "@tiptap/extension-bullet-list": "^2.5.1", "@tiptap/extension-code": "^2.5.1", "@tiptap/extension-code-block": "^2.5.1", - "@tiptap/extension-code-block-lowlight": "^2.5.1", "@tiptap/extension-color": "^2.5.1", "@tiptap/extension-document": "^2.5.1", "@tiptap/extension-dropcursor": "^2.5.1", @@ -80,9 +79,7 @@ "@tiptap/vue-3": "^2.5.1", "floating-vue": "^5.2.2", "github-markdown-css": "^5.2.0", - "highlight.js": "11.8.0", "linkifyjs": "^4.1.3", - "lowlight": "^3.0.0", "scroll-into-view-if-needed": "^3.1.0", "tippy.js": "^6.3.7" }, diff --git a/ui/packages/editor/src/dev/App.vue b/ui/packages/editor/src/dev/App.vue index b0540671a4..f40319be0e 100644 --- a/ui/packages/editor/src/dev/App.vue +++ b/ui/packages/editor/src/dev/App.vue @@ -45,7 +45,6 @@ import { ExtensionUnderline, ExtensionVideo, RichTextEditor, - lowlight, useEditor, } from "../index"; @@ -99,9 +98,7 @@ const editor = useEditor({ ExtensionVideo, ExtensionAudio, ExtensionCommands, - ExtensionCodeBlock.configure({ - lowlight, - }), + ExtensionCodeBlock, ExtensionIframe, ExtensionColor, ExtensionFontSize, diff --git a/ui/packages/editor/src/extensions/code-block/CodeBlockViewRenderer.vue b/ui/packages/editor/src/extensions/code-block/CodeBlockViewRenderer.vue index e71adcf620..1646d6dc79 100644 --- a/ui/packages/editor/src/extensions/code-block/CodeBlockViewRenderer.vue +++ b/ui/packages/editor/src/extensions/code-block/CodeBlockViewRenderer.vue @@ -1,40 +1,83 @@ + + diff --git a/ui/packages/editor/src/extensions/code-block/code-block.ts b/ui/packages/editor/src/extensions/code-block/code-block.ts index cb55deab15..3b6ec1f1f3 100644 --- a/ui/packages/editor/src/extensions/code-block/code-block.ts +++ b/ui/packages/editor/src/extensions/code-block/code-block.ts @@ -19,17 +19,12 @@ import { type Range, } from "@/tiptap/vue-3"; import { deleteNode } from "@/utils"; -import type { CodeBlockLowlightOptions } from "@tiptap/extension-code-block-lowlight"; -import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight"; import { markRaw } from "vue"; import MdiCodeBracesBox from "~icons/mdi/code-braces-box"; import CodeBlockViewRenderer from "./CodeBlockViewRenderer.vue"; - -export interface CustomCodeBlockLowlightOptions - extends CodeBlockLowlightOptions { - lowlight: any; - defaultLanguage: string | null | undefined; -} +import TiptapCodeBlock, { + type CodeBlockOptions, +} from "@tiptap/extension-code-block"; declare module "@/tiptap" { interface Commands { @@ -97,9 +92,23 @@ const getRenderContainer = (node: HTMLElement) => { return container; }; -export default CodeBlockLowlight.extend< - CustomCodeBlockLowlightOptions & CodeBlockLowlightOptions ->({ +export interface ExtensionCodeBlockOptions extends CodeBlockOptions { + /** + * Used for language list + * + * @default [] + */ + languages: Array | ((state: EditorState) => Array); + + /** + * Used for theme list + * + * @default [] + */ + themes?: Array | ((state: EditorState) => Array); +} + +export default TiptapCodeBlock.extend({ allowGapCursor: true, // It needs to have a higher priority than range-selection, // otherwise the Mod-a shortcut key will be overridden. @@ -121,6 +130,18 @@ export default CodeBlockLowlight.extend< return {}; }, }, + theme: { + default: null, + parseHTML: (element) => element.getAttribute("theme") || null, + renderHTML: (attributes) => { + if (attributes.theme) { + return { + theme: attributes.theme, + }; + } + return {}; + }, + }, }; }, @@ -197,7 +218,7 @@ export default CodeBlockLowlight.extend< if (this.editor.isActive("codeBlock")) { const { tr, selection } = this.editor.state; const codeBlack = findParentNode( - (node) => node.type.name === CodeBlockLowlight.name + (node) => node.type.name === TiptapCodeBlock.name )(selection); if (!codeBlack) { return false; @@ -218,9 +239,12 @@ export default CodeBlockLowlight.extend< addNodeView() { return VueNodeViewRenderer(CodeBlockViewRenderer); }, + addOptions() { return { ...this.parent?.(), + languages: [], + themes: [], getToolbarItems({ editor }: { editor: Editor }) { return { priority: 160, @@ -265,7 +289,7 @@ export default CodeBlockLowlight.extend< return { pluginKey: "codeBlockBubbleMenu", shouldShow: ({ state }: { state: EditorState }) => { - return isActive(state, CodeBlockLowlight.name); + return isActive(state, TiptapCodeBlock.name); }, getRenderContainer: (node: HTMLElement) => { return getRenderContainer(node); @@ -277,7 +301,7 @@ export default CodeBlockLowlight.extend< icon: markRaw(MdiDeleteForeverOutline), title: i18n.global.t("editor.common.button.delete"), action: ({ editor }: { editor: Editor }) => - deleteNode(CodeBlockLowlight.name, editor), + deleteNode(TiptapCodeBlock.name, editor), }, }, ], diff --git a/ui/packages/editor/src/extensions/code-block/index.ts b/ui/packages/editor/src/extensions/code-block/index.ts index d231f847f0..07f0a6d411 100644 --- a/ui/packages/editor/src/extensions/code-block/index.ts +++ b/ui/packages/editor/src/extensions/code-block/index.ts @@ -1,2 +1,2 @@ export { default as ExtensionCodeBlock } from "./code-block"; -export { default as lowlight } from "./lowlight"; +export * from "./code-block"; diff --git a/ui/packages/editor/src/extensions/code-block/lowlight.ts b/ui/packages/editor/src/extensions/code-block/lowlight.ts deleted file mode 100644 index 6512cc45ac..0000000000 --- a/ui/packages/editor/src/extensions/code-block/lowlight.ts +++ /dev/null @@ -1,8 +0,0 @@ -import dart from "highlight.js/lib/languages/dart"; -import xml from "highlight.js/lib/languages/xml"; -import { common, createLowlight } from "lowlight"; - -const lowlight = createLowlight(common); -lowlight.register("html", xml); -lowlight.register("dart", dart); -export default lowlight; diff --git a/ui/packages/editor/src/extensions/index.ts b/ui/packages/editor/src/extensions/index.ts index b47c95026c..83cd830982 100644 --- a/ui/packages/editor/src/extensions/index.ts +++ b/ui/packages/editor/src/extensions/index.ts @@ -29,7 +29,10 @@ import ExtensionTextAlign from "./text-align"; import ExtensionUnderline from "./underline"; // Custom extensions -import { ExtensionCodeBlock, lowlight } from "@/extensions/code-block"; +import { + ExtensionCodeBlock, + type ExtensionCodeBlockOptions, +} from "@/extensions/code-block"; import { ExtensionCommands } from "../extensions/commands-menu"; import ExtensionAudio from "./audio"; import ExtensionClearFormat from "./clear-format"; @@ -91,9 +94,7 @@ const allExtensions = [ ExtensionCommands.configure({ suggestion: {}, }), - ExtensionCodeBlock.configure({ - lowlight, - }), + ExtensionCodeBlock, ExtensionIframe, ExtensionVideo, ExtensionAudio, @@ -156,5 +157,6 @@ export { ExtensionVideo, RangeSelection, allExtensions, - lowlight, }; + +export type { ExtensionCodeBlockOptions }; diff --git a/ui/packages/editor/src/index.ts b/ui/packages/editor/src/index.ts index 444579db3f..9f30abe184 100644 --- a/ui/packages/editor/src/index.ts +++ b/ui/packages/editor/src/index.ts @@ -1,6 +1,5 @@ import "floating-vue/dist/style.css"; import "github-markdown-css/github-markdown-light.css"; -import "highlight.js/styles/github.css"; import type { App, Plugin } from "vue"; import { RichTextEditor } from "./components"; import "./styles/index.scss"; diff --git a/ui/packages/editor/src/styles/base.scss b/ui/packages/editor/src/styles/base.scss index 7c0543db59..c591fc182b 100644 --- a/ui/packages/editor/src/styles/base.scss +++ b/ui/packages/editor/src/styles/base.scss @@ -83,6 +83,10 @@ display: initial; } } + + .v-popper__arrow-container { + display: none; + } } .v-popper--theme-tooltip { diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 03a9e165b9..2fc474e821 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -470,9 +470,6 @@ importers: '@tiptap/extension-code-block': specifier: ^2.5.1 version: 2.5.1(@tiptap/core@2.5.1(@tiptap/pm@2.5.1))(@tiptap/pm@2.5.1) - '@tiptap/extension-code-block-lowlight': - specifier: ^2.5.1 - version: 2.5.1(@tiptap/core@2.5.1(@tiptap/pm@2.5.1))(@tiptap/extension-code-block@2.5.1(@tiptap/core@2.5.1(@tiptap/pm@2.5.1))(@tiptap/pm@2.5.1))(@tiptap/pm@2.5.1) '@tiptap/extension-color': specifier: ^2.5.1 version: 2.5.1(@tiptap/core@2.5.1(@tiptap/pm@2.5.1))(@tiptap/extension-text-style@2.5.1(@tiptap/core@2.5.1(@tiptap/pm@2.5.1))) @@ -569,15 +566,9 @@ importers: github-markdown-css: specifier: ^5.2.0 version: 5.2.0 - highlight.js: - specifier: 11.8.0 - version: 11.8.0 linkifyjs: specifier: ^4.1.3 version: 4.1.3 - lowlight: - specifier: ^3.0.0 - version: 3.0.0 scroll-into-view-if-needed: specifier: ^3.1.0 version: 3.1.0 @@ -3643,13 +3634,6 @@ packages: '@tiptap/core': ^2.5.1 '@tiptap/pm': ^2.5.1 - '@tiptap/extension-code-block-lowlight@2.5.1': - resolution: {integrity: sha512-iP2WqtSjKaCQVYWCCoL0JsjHhBFyK41wxoDomfbDgC4/NpcQP8xF1ooUWXZ3BRx33gcDSgPosCBBVuOTjR59Ew==} - peerDependencies: - '@tiptap/core': ^2.5.1 - '@tiptap/extension-code-block': ^2.5.1 - '@tiptap/pm': ^2.5.1 - '@tiptap/extension-code-block@2.5.1': resolution: {integrity: sha512-NlmThqc4mU9n9wsBRVbmTsJ+SrwueV/DzjEElJ/y8c9hDHnc6Z1SLMOF7OwdajO46E4LaFFH+p/PqU1fhirMqA==} peerDependencies: @@ -3911,9 +3895,6 @@ packages: '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} - '@types/hast@3.0.1': - resolution: {integrity: sha512-hs/iBJx2aydugBQx5ETV3ZgeSS0oIreQrFJ4bjBl0XvM4wAmDjFEALY7p0rTSLt2eL+ibjRAAs9dTPiCLtmbqQ==} - '@types/http-cache-semantics@4.0.4': resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} @@ -4049,9 +4030,6 @@ packages: '@types/unist@2.0.10': resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==} - '@types/unist@3.0.0': - resolution: {integrity: sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==} - '@types/uuid@9.0.7': resolution: {integrity: sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==} @@ -5539,9 +5517,6 @@ packages: resolution: {integrity: sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ==} hasBin: true - devlop@1.1.0: - resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -6570,10 +6545,6 @@ packages: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true - highlight.js@11.8.0: - resolution: {integrity: sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==} - engines: {node: '>=12.0.0'} - hookable@5.4.2: resolution: {integrity: sha512-6rOvaUiNKy9lET1X0ECnyZ5O5kSV0PJbtA5yZUgdEF7fGJEVwSLSislltyt7nFwVVALYHQJtfGeAR2Y0A0uJkg==} @@ -7443,9 +7414,6 @@ packages: resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - lowlight@3.0.0: - resolution: {integrity: sha512-kedX6yxvgak8P4LGh3vKRDQuMbVcnP+qRuDJlve2w+mNJAbEhEQPjYCp9QJnpVL5F2aAAVjeIzzrbQZUKHiDJw==} - lru-cache@10.1.0: resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==} engines: {node: 14 || >=16.14} @@ -10112,8 +10080,8 @@ packages: vue-component-type-helpers@2.0.19: resolution: {integrity: sha512-cN3f1aTxxKo4lzNeQAkVopswuImUrb5Iurll9Gaw5cqpnbTAxtEMM1mgi6ou4X79OCyqYv1U1mzBHJkzmiK82w==} - vue-component-type-helpers@2.0.26: - resolution: {integrity: sha512-sO9qQ8oC520SW6kqlls0iqDak53gsTVSrYylajgjmkt1c0vcgjsGSy1KzlDrbEx8pm02IEYhlUkU5hCYf8rwtg==} + vue-component-type-helpers@2.0.29: + resolution: {integrity: sha512-58i+ZhUAUpwQ+9h5Hck0D+jr1qbYl4voRt5KffBx8qzELViQ4XdT/Tuo+mzq8u63teAG8K0lLaOiL5ofqW38rg==} vue-demi@0.13.11: resolution: {integrity: sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==} @@ -14487,7 +14455,7 @@ snapshots: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.4.27(typescript@5.4.5) - vue-component-type-helpers: 2.0.26 + vue-component-type-helpers: 2.0.29 transitivePeerDependencies: - encoding - supports-color @@ -14573,12 +14541,6 @@ snapshots: '@tiptap/core': 2.5.1(@tiptap/pm@2.5.1) '@tiptap/pm': 2.5.1 - '@tiptap/extension-code-block-lowlight@2.5.1(@tiptap/core@2.5.1(@tiptap/pm@2.5.1))(@tiptap/extension-code-block@2.5.1(@tiptap/core@2.5.1(@tiptap/pm@2.5.1))(@tiptap/pm@2.5.1))(@tiptap/pm@2.5.1)': - dependencies: - '@tiptap/core': 2.5.1(@tiptap/pm@2.5.1) - '@tiptap/extension-code-block': 2.5.1(@tiptap/core@2.5.1(@tiptap/pm@2.5.1))(@tiptap/pm@2.5.1) - '@tiptap/pm': 2.5.1 - '@tiptap/extension-code-block@2.5.1(@tiptap/core@2.5.1(@tiptap/pm@2.5.1))(@tiptap/pm@2.5.1)': dependencies: '@tiptap/core': 2.5.1(@tiptap/pm@2.5.1) @@ -14841,10 +14803,6 @@ snapshots: dependencies: '@types/node': 18.19.34 - '@types/hast@3.0.1': - dependencies: - '@types/unist': 3.0.0 - '@types/http-cache-semantics@4.0.4': {} '@types/http-errors@2.0.4': {} @@ -14969,8 +14927,6 @@ snapshots: '@types/unist@2.0.10': {} - '@types/unist@3.0.0': {} - '@types/uuid@9.0.7': {} '@types/web-bluetooth@0.0.20': {} @@ -16780,10 +16736,6 @@ snapshots: transitivePeerDependencies: - supports-color - devlop@1.1.0: - dependencies: - dequal: 2.0.3 - didyoumean@1.2.2: {} diff-sequences@29.4.3: {} @@ -18092,8 +18044,6 @@ snapshots: he@1.2.0: {} - highlight.js@11.8.0: {} - hookable@5.4.2: {} hookable@5.5.3: {} @@ -19015,12 +18965,6 @@ snapshots: lowercase-keys@3.0.0: {} - lowlight@3.0.0: - dependencies: - '@types/hast': 3.0.1 - devlop: 1.1.0 - highlight.js: 11.8.0 - lru-cache@10.1.0: {} lru-cache@4.1.5: @@ -21989,7 +21933,7 @@ snapshots: vue-component-type-helpers@2.0.19: {} - vue-component-type-helpers@2.0.26: {} + vue-component-type-helpers@2.0.29: {} vue-demi@0.13.11(vue@3.4.27(typescript@5.3.3)): dependencies: diff --git a/ui/src/components/editor/DefaultEditor.vue b/ui/src/components/editor/DefaultEditor.vue index ab0e722e90..b2cdbb4685 100644 --- a/ui/src/components/editor/DefaultEditor.vue +++ b/ui/src/components/editor/DefaultEditor.vue @@ -48,8 +48,7 @@ import { RichTextEditor, ToolbarItem, ToolboxItem, - lowlight, - type AnyExtension, + type Extensions, } from "@halo-dev/richtext-editor"; // ui custom extension import { i18n } from "@/locales"; @@ -98,6 +97,7 @@ import { UiExtensionVideo, } from "./extensions"; import { getContents } from "./utils/attachment"; +import { useExtension } from "./composables/use-extension"; const { t } = useI18n(); const { currentUserHasPermission } = usePermission(); @@ -190,8 +190,196 @@ const handleCloseAttachmentSelectorModal = () => { attachmentOptions.value = initAttachmentOptions; }; +const { filterDuplicateExtensions } = useExtension(); + +const presetExtension = [ + ExtensionBlockquote, + ExtensionBold, + ExtensionBulletList, + ExtensionCode, + ExtensionDocument, + ExtensionDropcursor.configure({ + width: 2, + class: "dropcursor", + color: "skyblue", + }), + ExtensionGapcursor, + ExtensionHardBreak, + ExtensionHeading, + ExtensionHistory, + ExtensionHorizontalRule, + ExtensionItalic, + ExtensionOrderedList, + ExtensionStrike, + ExtensionText, + UiExtensionImage.configure({ + inline: true, + allowBase64: false, + HTMLAttributes: { + loading: "lazy", + }, + uploadImage: props.uploadImage, + }), + ExtensionTaskList, + ExtensionLink.configure({ + autolink: false, + openOnClick: false, + }), + ExtensionTextAlign.configure({ + types: ["heading", "paragraph"], + }), + ExtensionUnderline, + ExtensionTable.configure({ + resizable: true, + }), + ExtensionSubscript, + ExtensionSuperscript, + ExtensionPlaceholder.configure({ + placeholder: t( + "core.components.default_editor.extensions.placeholder.options.placeholder" + ), + }), + ExtensionHighlight, + ExtensionCommands, + ExtensionCodeBlock, + ExtensionIframe, + UiExtensionVideo.configure({ + uploadVideo: props.uploadImage, + }), + UiExtensionAudio.configure({ + uploadAudio: props.uploadImage, + }), + ExtensionCharacterCount, + ExtensionFontSize, + ExtensionColor, + ExtensionIndent, + Extension.create({ + addGlobalAttributes() { + return [ + { + types: ["heading"], + attributes: { + id: { + default: null, + }, + }, + }, + ]; + }, + }), + Extension.create({ + addOptions() { + // If user has no permission to view attachments, return + if (!currentUserHasPermission(["system:attachments:view"])) { + return this; + } + + return { + getToolboxItems({ editor }: { editor: Editor }) { + return [ + { + priority: 0, + component: markRaw(ToolboxItem), + props: { + editor, + icon: markRaw(IconFolder), + title: i18n.global.t( + "core.components.default_editor.toolbox.attachment" + ), + action: () => { + editor.commands.openAttachmentSelector((attachment) => { + editor + .chain() + .focus() + .insertContent(getContents(attachment)) + .run(); + }); + return true; + }, + }, + }, + ]; + }, + getToolbarItems({ editor }: { editor: Editor }) { + return { + priority: 1000, + component: markRaw(ToolbarItem), + props: { + editor, + isActive: showSidebar.value, + icon: markRaw(RiLayoutRightLine), + title: i18n.global.t( + "core.components.default_editor.toolbox.show_hide_sidebar" + ), + action: () => { + showSidebar.value = !showSidebar.value; + }, + }, + }; + }, + }; + }, + addCommands() { + return { + openAttachmentSelector: (callback, options) => () => { + if (options) { + attachmentOptions.value = options; + } + attachmentSelectorModal.value = true; + attachmentResult.updateAttachment = ( + attachments: AttachmentLike[] + ) => { + callback(attachments); + }; + return true; + }, + }; + }, + }), + ExtensionDraggable, + ExtensionColumns, + ExtensionColumn, + ExtensionNodeSelected, + ExtensionTrailingNode, + Extension.create({ + addProseMirrorPlugins() { + return [ + new Plugin({ + key: new PluginKey("get-heading-id"), + props: { + decorations: (state) => { + const headings: HeadingNode[] = []; + const { doc } = state; + doc.descendants((node) => { + if (node.type.name === ExtensionHeading.name) { + headings.push({ + level: node.attrs.level, + text: node.textContent, + id: node.attrs.id, + }); + } + }); + headingNodes.value = headings; + if (!selectedHeadingNode.value) { + selectedHeadingNode.value = headings[0]; + } + return DecorationSet.empty; + }, + }, + }), + ]; + }, + }), + ExtensionListKeymap, + UiExtensionUpload, + ExtensionSearchAndReplace, + ExtensionClearFormat, + ExtensionFormatBrush, + ExtensionRangeSelection, +]; + onMounted(async () => { - const extensionsFromPlugins: AnyExtension[] = []; + const extensionsFromPlugins: Extensions = []; for (const pluginModule of pluginModules) { const callbackFunction = @@ -214,196 +402,14 @@ onMounted(async () => { emit("update", html); }, 250); + const extension = filterDuplicateExtensions([ + ...presetExtension, + ...extensionsFromPlugins, + ]); + editor.value = new Editor({ content: props.raw, - extensions: [ - ExtensionBlockquote, - ExtensionBold, - ExtensionBulletList, - ExtensionCode, - ExtensionDocument, - ExtensionDropcursor.configure({ - width: 2, - class: "dropcursor", - color: "skyblue", - }), - ExtensionGapcursor, - ExtensionHardBreak, - ExtensionHeading, - ExtensionHistory, - ExtensionHorizontalRule, - ExtensionItalic, - ExtensionOrderedList, - ExtensionStrike, - ExtensionText, - UiExtensionImage.configure({ - inline: true, - allowBase64: false, - HTMLAttributes: { - loading: "lazy", - }, - uploadImage: props.uploadImage, - }), - ExtensionTaskList, - ExtensionLink.configure({ - autolink: false, - openOnClick: false, - }), - ExtensionTextAlign.configure({ - types: ["heading", "paragraph"], - }), - ExtensionUnderline, - ExtensionTable.configure({ - resizable: true, - }), - ExtensionSubscript, - ExtensionSuperscript, - ExtensionPlaceholder.configure({ - placeholder: t( - "core.components.default_editor.extensions.placeholder.options.placeholder" - ), - }), - ExtensionHighlight, - ExtensionCommands, - ExtensionCodeBlock.configure({ - lowlight, - }), - ExtensionIframe, - UiExtensionVideo.configure({ - uploadVideo: props.uploadImage, - }), - UiExtensionAudio.configure({ - uploadAudio: props.uploadImage, - }), - ExtensionCharacterCount, - ExtensionFontSize, - ExtensionColor, - ExtensionIndent, - ...extensionsFromPlugins, - Extension.create({ - addGlobalAttributes() { - return [ - { - types: ["heading"], - attributes: { - id: { - default: null, - }, - }, - }, - ]; - }, - }), - Extension.create({ - addOptions() { - // If user has no permission to view attachments, return - if (!currentUserHasPermission(["system:attachments:view"])) { - return this; - } - - return { - getToolboxItems({ editor }: { editor: Editor }) { - return [ - { - priority: 0, - component: markRaw(ToolboxItem), - props: { - editor, - icon: markRaw(IconFolder), - title: i18n.global.t( - "core.components.default_editor.toolbox.attachment" - ), - action: () => { - editor.commands.openAttachmentSelector((attachment) => { - editor - .chain() - .focus() - .insertContent(getContents(attachment)) - .run(); - }); - return true; - }, - }, - }, - ]; - }, - getToolbarItems({ editor }: { editor: Editor }) { - return { - priority: 1000, - component: markRaw(ToolbarItem), - props: { - editor, - isActive: showSidebar.value, - icon: markRaw(RiLayoutRightLine), - title: i18n.global.t( - "core.components.default_editor.toolbox.show_hide_sidebar" - ), - action: () => { - showSidebar.value = !showSidebar.value; - }, - }, - }; - }, - }; - }, - addCommands() { - return { - openAttachmentSelector: (callback, options) => () => { - if (options) { - attachmentOptions.value = options; - } - attachmentSelectorModal.value = true; - attachmentResult.updateAttachment = ( - attachments: AttachmentLike[] - ) => { - callback(attachments); - }; - return true; - }, - }; - }, - }), - ExtensionDraggable, - ExtensionColumns, - ExtensionColumn, - ExtensionNodeSelected, - ExtensionTrailingNode, - Extension.create({ - addProseMirrorPlugins() { - return [ - new Plugin({ - key: new PluginKey("get-heading-id"), - props: { - decorations: (state) => { - const headings: HeadingNode[] = []; - const { doc } = state; - doc.descendants((node) => { - if (node.type.name === ExtensionHeading.name) { - headings.push({ - level: node.attrs.level, - text: node.textContent, - id: node.attrs.id, - }); - } - }); - headingNodes.value = headings; - if (!selectedHeadingNode.value) { - selectedHeadingNode.value = headings[0]; - } - return DecorationSet.empty; - }, - }, - }), - ]; - }, - }), - ExtensionListKeymap, - UiExtensionUpload, - ExtensionSearchAndReplace, - ExtensionClearFormat, - ExtensionFormatBrush, - ExtensionRangeSelection, - ], + extensions: extension, parseOptions: { preserveWhitespace: true, }, diff --git a/ui/src/components/editor/composables/use-extension.ts b/ui/src/components/editor/composables/use-extension.ts new file mode 100644 index 0000000000..d1e4411ba3 --- /dev/null +++ b/ui/src/components/editor/composables/use-extension.ts @@ -0,0 +1,94 @@ +import { + getExtensionField, + type AnyConfig, + type AnyExtension, + type Extensions, +} from "@halo-dev/richtext-editor"; + +export function useExtension() { + const filterDuplicateExtensions = (extensions: Extensions | undefined) => { + if (!extensions) { + return; + } + + const resolvedExtensions = sort(flatten(extensions)); + + const map = new Map(); + + resolvedExtensions.forEach((extension) => { + const key = `${extension.type}-${extension.name}`; + if (map.has(key)) { + console.warn( + `Duplicate found for Extension, type: ${extension.type}, name: ${extension.name}. Keeping the later one.` + ); + } + map.set(key, extension); + }); + + return Array.from(map.values()); + }; + + /** + * Create a flattened array of extensions by traversing the `addExtensions` field. + * @param extensions An array of Tiptap extensions + * @returns A flattened array of Tiptap extensions + */ + const flatten = (extensions: Extensions): Extensions => { + return ( + extensions + .map((extension) => { + const context = { + name: extension.name, + options: extension.options, + storage: extension.storage, + }; + + const addExtensions = getExtensionField( + extension, + "addExtensions", + context + ); + + if (addExtensions) { + return [extension, ...flatten(addExtensions())]; + } + + return extension; + }) + // `Infinity` will break TypeScript so we set a number that is probably high enough + .flat(10) + ); + }; + + /** + * Sort extensions by priority. + * @param extensions An array of Tiptap extensions + * @returns A sorted array of Tiptap extensions by priority + */ + const sort = (extensions: Extensions): Extensions => { + const defaultPriority = 100; + + return extensions.sort((a, b) => { + const priorityA = + getExtensionField(a, "priority") || + defaultPriority; + const priorityB = + getExtensionField(b, "priority") || + defaultPriority; + + if (priorityA > priorityB) { + return -1; + } + + if (priorityA < priorityB) { + return 1; + } + + return 0; + }); + }; + + return { + filterDuplicateExtensions, + }; +} From 252b7e87a5d615e12f139fe7dcadbb74117ed4e5 Mon Sep 17 00:00:00 2001 From: LIlGG <1103069291@qq.com> Date: Thu, 1 Aug 2024 16:13:35 +0800 Subject: [PATCH 2/8] upgrade pnpm lock --- ui/pnpm-lock.yaml | 104 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 72 insertions(+), 32 deletions(-) diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 2ab1d81702..0d12dc2463 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -468,16 +468,8 @@ importers: specifier: ^2.5.7 version: 2.5.7(@tiptap/core@2.5.7(@tiptap/pm@2.5.7)) '@tiptap/extension-code-block': -<<<<<<< HEAD - specifier: ^2.5.1 - version: 2.5.1(@tiptap/core@2.5.1(@tiptap/pm@2.5.1))(@tiptap/pm@2.5.1) -======= specifier: ^2.5.7 version: 2.5.7(@tiptap/core@2.5.7(@tiptap/pm@2.5.7))(@tiptap/pm@2.5.7) - '@tiptap/extension-code-block-lowlight': - specifier: ^2.5.7 - version: 2.5.7(@tiptap/core@2.5.7(@tiptap/pm@2.5.7))(@tiptap/extension-code-block@2.5.7(@tiptap/core@2.5.7(@tiptap/pm@2.5.7))(@tiptap/pm@2.5.7))(@tiptap/pm@2.5.7) ->>>>>>> 58fe8728447cb0adfac5c70c10773219350c7b50 '@tiptap/extension-color': specifier: ^2.5.7 version: 2.5.7(@tiptap/core@2.5.7(@tiptap/pm@2.5.7))(@tiptap/extension-text-style@2.5.7(@tiptap/core@2.5.7(@tiptap/pm@2.5.7))) @@ -598,7 +590,7 @@ importers: version: 16.2.1(typescript@5.4.5) vite-plugin-dts: specifier: ^3.9.1 - version: 3.9.1(@types/node@20.14.2)(rollup@4.17.2)(typescript@5.4.5)(vite@5.2.11(@types/node@20.14.2)(less@4.2.0)(sass@1.60.0)(terser@5.31.0)) + version: 3.9.1(@types/node@18.13.0)(rollup@4.17.2)(typescript@5.4.5)(vite@5.2.11(@types/node@18.13.0)(less@4.2.0)(sass@1.60.0)(terser@5.31.0)) packages/shared: dependencies: @@ -614,7 +606,7 @@ importers: devDependencies: vite-plugin-dts: specifier: ^3.9.1 - version: 3.9.1(@types/node@20.14.2)(rollup@4.17.2)(typescript@5.4.5)(vite@5.2.11(@types/node@20.14.2)(less@4.2.0)(sass@1.60.0)(terser@5.31.0)) + version: 3.9.1(@types/node@18.13.0)(rollup@4.17.2)(typescript@5.4.5)(vite@5.2.11(@types/node@18.13.0)(less@4.2.0)(sass@1.60.0)(terser@5.31.0)) packages/ui-plugin-bundler-kit: dependencies: @@ -3642,20 +3634,8 @@ packages: '@tiptap/core': ^2.5.7 '@tiptap/pm': ^2.5.7 -<<<<<<< HEAD - '@tiptap/extension-code-block@2.5.1': - resolution: {integrity: sha512-NlmThqc4mU9n9wsBRVbmTsJ+SrwueV/DzjEElJ/y8c9hDHnc6Z1SLMOF7OwdajO46E4LaFFH+p/PqU1fhirMqA==} -======= - '@tiptap/extension-code-block-lowlight@2.5.7': - resolution: {integrity: sha512-/pk/gM1CgUeCzFzbKBGG33uVsbRxi4d7g9/saI/0PUY3XN9nze8G0WHTH706pjRevMH0KZjgGLbSabxMIx5oAg==} - peerDependencies: - '@tiptap/core': ^2.5.7 - '@tiptap/extension-code-block': ^2.5.7 - '@tiptap/pm': ^2.5.7 - '@tiptap/extension-code-block@2.5.7': resolution: {integrity: sha512-Nm1Lx+4CxE3q817WcQXR2Vp1ntG4Let7TRpuuI9YUGBmq/7OSm1N+Y7wdRrUcUV9fHCHQFPWQvEv1uW0kFavng==} ->>>>>>> 58fe8728447cb0adfac5c70c10773219350c7b50 peerDependencies: '@tiptap/core': ^2.5.7 '@tiptap/pm': ^2.5.7 @@ -13105,6 +13085,14 @@ snapshots: '@types/react': 18.2.41 react: 18.2.0 + '@microsoft/api-extractor-model@7.28.13(@types/node@18.13.0)': + dependencies: + '@microsoft/tsdoc': 0.14.2 + '@microsoft/tsdoc-config': 0.16.2 + '@rushstack/node-core-library': 4.0.2(@types/node@18.13.0) + transitivePeerDependencies: + - '@types/node' + '@microsoft/api-extractor-model@7.28.13(@types/node@20.14.2)': dependencies: '@microsoft/tsdoc': 0.14.2 @@ -13113,6 +13101,24 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@microsoft/api-extractor@7.43.0(@types/node@18.13.0)': + dependencies: + '@microsoft/api-extractor-model': 7.28.13(@types/node@18.13.0) + '@microsoft/tsdoc': 0.14.2 + '@microsoft/tsdoc-config': 0.16.2 + '@rushstack/node-core-library': 4.0.2(@types/node@18.13.0) + '@rushstack/rig-package': 0.5.2 + '@rushstack/terminal': 0.10.0(@types/node@18.13.0) + '@rushstack/ts-command-line': 4.19.1(@types/node@18.13.0) + lodash: 4.17.21 + minimatch: 3.0.8 + resolve: 1.22.8 + semver: 7.5.4 + source-map: 0.6.1 + typescript: 5.4.2 + transitivePeerDependencies: + - '@types/node' + '@microsoft/api-extractor@7.43.0(@types/node@20.14.2)': dependencies: '@microsoft/api-extractor-model': 7.28.13(@types/node@20.14.2) @@ -13765,6 +13771,17 @@ snapshots: '@rushstack/eslint-patch@1.3.2': {} + '@rushstack/node-core-library@4.0.2(@types/node@18.13.0)': + dependencies: + fs-extra: 7.0.1 + import-lazy: 4.0.0 + jju: 1.4.0 + resolve: 1.22.8 + semver: 7.5.4 + z-schema: 5.0.4 + optionalDependencies: + '@types/node': 18.13.0 + '@rushstack/node-core-library@4.0.2(@types/node@20.14.2)': dependencies: fs-extra: 7.0.1 @@ -13781,6 +13798,13 @@ snapshots: resolve: 1.22.8 strip-json-comments: 3.1.1 + '@rushstack/terminal@0.10.0(@types/node@18.13.0)': + dependencies: + '@rushstack/node-core-library': 4.0.2(@types/node@18.13.0) + supports-color: 8.1.1 + optionalDependencies: + '@types/node': 18.13.0 + '@rushstack/terminal@0.10.0(@types/node@20.14.2)': dependencies: '@rushstack/node-core-library': 4.0.2(@types/node@20.14.2) @@ -13788,6 +13812,15 @@ snapshots: optionalDependencies: '@types/node': 20.14.2 + '@rushstack/ts-command-line@4.19.1(@types/node@18.13.0)': + dependencies: + '@rushstack/terminal': 0.10.0(@types/node@18.13.0) + '@types/argparse': 1.0.38 + argparse: 1.0.10 + string-argv: 0.3.1 + transitivePeerDependencies: + - '@types/node' + '@rushstack/ts-command-line@4.19.1(@types/node@20.14.2)': dependencies: '@rushstack/terminal': 0.10.0(@types/node@20.14.2) @@ -14508,17 +14541,7 @@ snapshots: '@tiptap/core': 2.5.7(@tiptap/pm@2.5.7) '@tiptap/pm': 2.5.7 -<<<<<<< HEAD - '@tiptap/extension-code-block@2.5.1(@tiptap/core@2.5.1(@tiptap/pm@2.5.1))(@tiptap/pm@2.5.1)': -======= - '@tiptap/extension-code-block-lowlight@2.5.7(@tiptap/core@2.5.7(@tiptap/pm@2.5.7))(@tiptap/extension-code-block@2.5.7(@tiptap/core@2.5.7(@tiptap/pm@2.5.7))(@tiptap/pm@2.5.7))(@tiptap/pm@2.5.7)': - dependencies: - '@tiptap/core': 2.5.7(@tiptap/pm@2.5.7) - '@tiptap/extension-code-block': 2.5.7(@tiptap/core@2.5.7(@tiptap/pm@2.5.7))(@tiptap/pm@2.5.7) - '@tiptap/pm': 2.5.7 - '@tiptap/extension-code-block@2.5.7(@tiptap/core@2.5.7(@tiptap/pm@2.5.7))(@tiptap/pm@2.5.7)': ->>>>>>> 58fe8728447cb0adfac5c70c10773219350c7b50 dependencies: '@tiptap/core': 2.5.7(@tiptap/pm@2.5.7) '@tiptap/pm': 2.5.7 @@ -21678,6 +21701,23 @@ snapshots: - supports-color - terser + vite-plugin-dts@3.9.1(@types/node@18.13.0)(rollup@4.17.2)(typescript@5.4.5)(vite@5.2.11(@types/node@18.13.0)(less@4.2.0)(sass@1.60.0)(terser@5.31.0)): + dependencies: + '@microsoft/api-extractor': 7.43.0(@types/node@18.13.0) + '@rollup/pluginutils': 5.1.0(rollup@4.17.2) + '@vue/language-core': 1.8.27(typescript@5.4.5) + debug: 4.3.4(supports-color@8.1.1) + kolorist: 1.8.0 + magic-string: 0.30.10 + typescript: 5.4.5 + vue-tsc: 1.8.27(typescript@5.4.5) + optionalDependencies: + vite: 5.2.11(@types/node@18.13.0)(less@4.2.0)(sass@1.60.0)(terser@5.31.0) + transitivePeerDependencies: + - '@types/node' + - rollup + - supports-color + vite-plugin-dts@3.9.1(@types/node@20.14.2)(rollup@2.79.1)(typescript@5.4.5)(vite@5.2.11(@types/node@20.14.2)(less@4.2.0)(sass@1.60.0)(terser@5.31.0)): dependencies: '@microsoft/api-extractor': 7.43.0(@types/node@20.14.2) From 794bf6372348e27645392efe8ccc30bedf809d78 Mon Sep 17 00:00:00 2001 From: LIlGG <1103069291@qq.com> Date: Thu, 1 Aug 2024 16:52:34 +0800 Subject: [PATCH 3/8] optimize the dropdown menu --- .../src/extensions/code-block/Select.vue | 26 +++++++------------ 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/ui/packages/editor/src/extensions/code-block/Select.vue b/ui/packages/editor/src/extensions/code-block/Select.vue index 0a1deddbc9..6b6b54daf0 100644 --- a/ui/packages/editor/src/extensions/code-block/Select.vue +++ b/ui/packages/editor/src/extensions/code-block/Select.vue @@ -50,7 +50,7 @@ const handleInputFocus = () => { isFocus.value = true; setTimeout(() => { handleScrollIntoView(); - }); + }, 50); }; const handleInputBlur = () => { @@ -101,6 +101,9 @@ watch( if (newValue) { selectedOption.value = props.options.find((option) => option.value === newValue) || null; + selectedIndex.value = props.options.findIndex( + (option) => option.value === newValue + ); } }, { @@ -109,27 +112,17 @@ watch( ); watch( - [filterOptions, selectedOption], + selectedIndex, () => { - if (filterOptions.value.length > 0) { - selectedIndex.value = filterOptions.value.findIndex( - (option) => option.value === value.value - ); - } else { - selectedIndex.value = -1; - } + setTimeout(() => { + handleScrollIntoView(); + }); }, { immediate: true, } ); -watch(selectedIndex, () => { - setTimeout(() => { - handleScrollIntoView(); - }); -}); - const handleScrollIntoView = () => { if (selectedIndex.value === -1) { return; @@ -153,6 +146,7 @@ const handleScrollIntoView = () => { :auto-hide="false" :distance="0" :container="container || 'body'" + :popper-class="[containerClass]" >
{