diff --git a/ui/packages/editor/package.json b/ui/packages/editor/package.json index 53553a53f..876bee36a 100644 --- a/ui/packages/editor/package.json +++ b/ui/packages/editor/package.json @@ -46,6 +46,9 @@ "@tiptap/extension-code": "^2.11.2", "@tiptap/extension-code-block": "^2.11.2", "@tiptap/extension-color": "^2.11.2", + "@tiptap/extension-details": "^2.22.3", + "@tiptap/extension-details-content": "^2.22.3", + "@tiptap/extension-details-summary": "^2.22.3", "@tiptap/extension-document": "^2.11.2", "@tiptap/extension-dropcursor": "^2.11.2", "@tiptap/extension-hard-break": "^2.11.2", diff --git a/ui/packages/editor/src/dev/App.vue b/ui/packages/editor/src/dev/App.vue index f40319be0..5fa97191a 100644 --- a/ui/packages/editor/src/dev/App.vue +++ b/ui/packages/editor/src/dev/App.vue @@ -44,6 +44,7 @@ import { ExtensionTrailingNode, ExtensionUnderline, ExtensionVideo, + ExtensionDetails, RichTextEditor, useEditor, } from "../index"; @@ -113,6 +114,9 @@ const editor = useEditor({ ExtensionClearFormat, ExtensionFormatBrush, ExtensionRangeSelection, + ExtensionDetails.configure({ + persist: true, + }), ], parseOptions: { preserveWhitespace: true, diff --git a/ui/packages/editor/src/extensions/details/index.ts b/ui/packages/editor/src/extensions/details/index.ts new file mode 100644 index 000000000..f6bd1bc5f --- /dev/null +++ b/ui/packages/editor/src/extensions/details/index.ts @@ -0,0 +1,87 @@ +import type { ExtensionOptions } from "@/types"; +import TiptapDetails, { type DetailsOptions } from "@tiptap/extension-details"; +import TiptapDetailsContent from "@tiptap/extension-details-content"; +import TiptapDetailsSummary from "@tiptap/extension-details-summary"; +import type { Editor, Range } from "@/tiptap/vue-3"; +import { markRaw } from "vue"; +import ToolbarItem from "@/components/toolbar/ToolbarItem.vue"; +import { i18n } from "@/locales"; +import MdiExpandHorizontal from "~icons/mdi/expand-horizontal"; + +const getRenderContainer = (node: HTMLElement) => { + let container = node; + if (container.nodeName === "#text") { + container = node.parentElement as HTMLElement; + } + + while (container && container.dataset.type !== "details") { + container = container.parentElement as HTMLElement; + } + return container; +}; + +const Details = TiptapDetails.extend({ + addOptions() { + return { + ...this.parent?.(), + HTMLAttributes: { + class: "details", + }, + getCommandMenuItems() { + return { + priority: 160, + icon: markRaw(MdiExpandHorizontal), + title: "editor.extensions.details.command_item", + keywords: ["details"], + command: ({ editor, range }: { editor: Editor; range: Range }) => { + editor + .chain() + .focus() + .deleteRange(range) + .setDetails() + .updateAttributes("details", { open: true }) + .run(); + }, + }; + }, + getToolbarItems({ editor }: { editor: Editor }) { + return { + priority: 95, + component: markRaw(ToolbarItem), + props: { + editor, + isActive: editor.isActive("details"), + icon: markRaw(MdiExpandHorizontal), + title: i18n.global.t("editor.extensions.details.command_item"), + action: () => { + if (editor.isActive("details")) { + editor.chain().focus().unsetDetails().run(); + } else { + editor + .chain() + .focus() + .setDetails() + .updateAttributes("details", { open: true }) + .run(); + } + }, + }, + }; + }, + getDraggable() { + return { + getRenderContainer({ dom }: { dom: HTMLElement }) { + return { + el: getRenderContainer(dom), + }; + }, + }; + }, + }; + }, + addExtensions() { + return [TiptapDetailsSummary, TiptapDetailsContent]; + }, +}); + +export default Details; diff --git a/ui/packages/editor/src/extensions/index.ts b/ui/packages/editor/src/extensions/index.ts index 50309370b..7effdf509 100644 --- a/ui/packages/editor/src/extensions/index.ts +++ b/ui/packages/editor/src/extensions/index.ts @@ -26,6 +26,7 @@ import ExtensionTable from "./table"; import ExtensionTaskList from "./task-list"; import ExtensionTextAlign from "./text-align"; import ExtensionUnderline from "./underline"; +import ExtensionDetails from "./details"; // Custom extensions import { @@ -107,6 +108,7 @@ const allExtensions = [ ExtensionClearFormat, ExtensionFormatBrush, ExtensionRangeSelection, + ExtensionDetails, ]; export { @@ -156,6 +158,7 @@ export { ExtensionTrailingNode, ExtensionUnderline, ExtensionVideo, + ExtensionDetails, RangeSelection, }; diff --git a/ui/packages/editor/src/locales/en.yaml b/ui/packages/editor/src/locales/en.yaml index 06f532341..7009fe1a2 100644 --- a/ui/packages/editor/src/locales/en.yaml +++ b/ui/packages/editor/src/locales/en.yaml @@ -88,6 +88,8 @@ editor: toolbar_item: title: Format Brush cancel: Cancel Format Brush + details: + command_item: Details components: color_picker: more_color: More diff --git a/ui/packages/editor/src/locales/zh-CN.yaml b/ui/packages/editor/src/locales/zh-CN.yaml index 2247f70b1..4a7ab1960 100644 --- a/ui/packages/editor/src/locales/zh-CN.yaml +++ b/ui/packages/editor/src/locales/zh-CN.yaml @@ -88,6 +88,8 @@ editor: toolbar_item: title: 格式刷 cancel: 取消格式刷 + details: + command_item: 折叠内容 components: color_picker: more_color: 更多颜色 diff --git a/ui/packages/editor/src/styles/details.scss b/ui/packages/editor/src/styles/details.scss new file mode 100644 index 000000000..025d8ffc2 --- /dev/null +++ b/ui/packages/editor/src/styles/details.scss @@ -0,0 +1,56 @@ +.halo-rich-text-editor { + .details { + display: flex; + gap: 0.25rem; + margin: 1.5rem 0; + border: 1px solid theme("colors.gray.200"); + border-radius: 0.5rem; + padding: 0.5rem; + + summary { + all: unset; + font-weight: 700; + } + + > button { + align-items: center; + background: transparent; + border-radius: 4px; + display: flex; + font-size: 0.625rem; + height: 1.25rem; + justify-content: center; + line-height: 1; + margin-top: 0.1rem; + padding: 0; + width: 1.25rem; + + &:hover { + background-color: theme("colors.gray.200"); + } + + &::before { + content: "\25B6"; + } + } + + &.is-open > button::before { + transform: rotate(90deg); + } + + > div { + display: flex; + flex-direction: column; + gap: 1rem; + width: 100%; + + > [data-type="detailsContent"] > :last-child { + margin-bottom: 0.5rem; + } + } + + .details { + margin: 0.5rem 0; + } + } +} diff --git a/ui/packages/editor/src/styles/index.scss b/ui/packages/editor/src/styles/index.scss index 3d976f140..f11ada511 100644 --- a/ui/packages/editor/src/styles/index.scss +++ b/ui/packages/editor/src/styles/index.scss @@ -7,3 +7,4 @@ @use "gap-cursor.scss"; @use "node-select.scss"; @use "range-selection.scss"; +@use "details.scss"; diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index adb489850..029753998 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -477,6 +477,15 @@ importers: '@tiptap/extension-color': specifier: ^2.11.2 version: 2.11.2(@tiptap/core@2.11.2(@tiptap/pm@2.11.2))(@tiptap/extension-text-style@2.11.2(@tiptap/core@2.11.2(@tiptap/pm@2.11.2))) + '@tiptap/extension-details': + specifier: ^2.22.3 + version: 2.22.3(@tiptap/core@2.11.2(@tiptap/pm@2.11.2))(@tiptap/extension-text-style@2.11.2(@tiptap/core@2.11.2(@tiptap/pm@2.11.2))) + '@tiptap/extension-details-content': + specifier: ^2.22.3 + version: 2.22.3(@tiptap/core@2.11.2(@tiptap/pm@2.11.2))(@tiptap/extension-text-style@2.11.2(@tiptap/core@2.11.2(@tiptap/pm@2.11.2))) + '@tiptap/extension-details-summary': + specifier: ^2.22.3 + version: 2.22.3(@tiptap/core@2.11.2(@tiptap/pm@2.11.2))(@tiptap/extension-text-style@2.11.2(@tiptap/core@2.11.2(@tiptap/pm@2.11.2))) '@tiptap/extension-document': specifier: ^2.11.2 version: 2.11.2(@tiptap/core@2.11.2(@tiptap/pm@2.11.2)) @@ -4080,6 +4089,24 @@ packages: '@tiptap/core': ^2.7.0 '@tiptap/extension-text-style': ^2.7.0 + '@tiptap/extension-details-content@2.22.3': + resolution: {integrity: sha512-wlkF3Y+kdxg23xoKJFaLK+1yMJSyRkUGBQD8M+BtWrSDsB7ywz3ZnI2HiSiZQDEB5adksqEqD5psDIpuZwVYSQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/extension-text-style': ^2.7.0 + + '@tiptap/extension-details-summary@2.22.3': + resolution: {integrity: sha512-+ohQoRSDsUT43fi+BOrE6JTTnwY3Lg6tlt4FZA/hG5JoNnfmz4XzNpPRvrjgWAO+af/vxmF+OwaMMcTA/a6gTQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/extension-text-style': ^2.7.0 + + '@tiptap/extension-details@2.22.3': + resolution: {integrity: sha512-YYWpIpS0Ue7t/557S7AP+ZGpnC16few5Yrf8twV+VXsN7rxj8KsVqRO4cvX1cIqMEKPxzupsYs6KEkx/JUf1ng==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/extension-text-style': ^2.7.0 + '@tiptap/extension-document@2.11.2': resolution: {integrity: sha512-/EZhIAN1x7DYgGM0xv7y7wo5ceBmHb0+rOIPuBerVFeTn+VcC3tST/Q64bdvcxgNe2E59Ti0CUdYEA51wc2u5Q==} peerDependencies: @@ -15373,6 +15400,21 @@ snapshots: '@tiptap/core': 2.11.2(@tiptap/pm@2.11.2) '@tiptap/extension-text-style': 2.11.2(@tiptap/core@2.11.2(@tiptap/pm@2.11.2)) + '@tiptap/extension-details-content@2.22.3(@tiptap/core@2.11.2(@tiptap/pm@2.11.2))(@tiptap/extension-text-style@2.11.2(@tiptap/core@2.11.2(@tiptap/pm@2.11.2)))': + dependencies: + '@tiptap/core': 2.11.2(@tiptap/pm@2.11.2) + '@tiptap/extension-text-style': 2.11.2(@tiptap/core@2.11.2(@tiptap/pm@2.11.2)) + + '@tiptap/extension-details-summary@2.22.3(@tiptap/core@2.11.2(@tiptap/pm@2.11.2))(@tiptap/extension-text-style@2.11.2(@tiptap/core@2.11.2(@tiptap/pm@2.11.2)))': + dependencies: + '@tiptap/core': 2.11.2(@tiptap/pm@2.11.2) + '@tiptap/extension-text-style': 2.11.2(@tiptap/core@2.11.2(@tiptap/pm@2.11.2)) + + '@tiptap/extension-details@2.22.3(@tiptap/core@2.11.2(@tiptap/pm@2.11.2))(@tiptap/extension-text-style@2.11.2(@tiptap/core@2.11.2(@tiptap/pm@2.11.2)))': + dependencies: + '@tiptap/core': 2.11.2(@tiptap/pm@2.11.2) + '@tiptap/extension-text-style': 2.11.2(@tiptap/core@2.11.2(@tiptap/pm@2.11.2)) + '@tiptap/extension-document@2.11.2(@tiptap/core@2.11.2(@tiptap/pm@2.11.2))': dependencies: '@tiptap/core': 2.11.2(@tiptap/pm@2.11.2) diff --git a/ui/src/components/editor/DefaultEditor.vue b/ui/src/components/editor/DefaultEditor.vue index 9ee0c6d8d..6ca356f8a 100644 --- a/ui/src/components/editor/DefaultEditor.vue +++ b/ui/src/components/editor/DefaultEditor.vue @@ -13,6 +13,7 @@ import { ExtensionColumn, ExtensionColumns, ExtensionCommands, + ExtensionDetails, ExtensionDocument, ExtensionDraggable, ExtensionDropcursor, @@ -406,6 +407,9 @@ const presetExtensions = [ ExtensionClearFormat, ExtensionFormatBrush, ExtensionRangeSelection, + ExtensionDetails.configure({ + persist: true, + }), ]; onMounted(async () => {