feat: add details extension for editor (#7594)

#### What type of PR is this?

/area ui
/area editor
/kind feature
/milestone 2.21.x

#### What this PR does / why we need it:

Add details supports for editor.

<img width="1021" alt="image" src="https://github.com/user-attachments/assets/63d61c49-e370-4a4a-ba14-865bce9afdbe" />

#### Which issue(s) this PR fixes:

Fixes https://github.com/halo-dev/halo/issues/3490

#### Special notes for your reviewer:

#### Does this PR introduce a user-facing change?

```release-note
为编辑器添加内容折叠功能
```
pull/7595/head v2.21.2
Ryan Wang 2025-06-26 21:59:23 +08:00 committed by GitHub
parent 3b1200ff2e
commit 3ac09524e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 204 additions and 0 deletions

View File

@ -46,6 +46,9 @@
"@tiptap/extension-code": "^2.11.2", "@tiptap/extension-code": "^2.11.2",
"@tiptap/extension-code-block": "^2.11.2", "@tiptap/extension-code-block": "^2.11.2",
"@tiptap/extension-color": "^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-document": "^2.11.2",
"@tiptap/extension-dropcursor": "^2.11.2", "@tiptap/extension-dropcursor": "^2.11.2",
"@tiptap/extension-hard-break": "^2.11.2", "@tiptap/extension-hard-break": "^2.11.2",

View File

@ -44,6 +44,7 @@ import {
ExtensionTrailingNode, ExtensionTrailingNode,
ExtensionUnderline, ExtensionUnderline,
ExtensionVideo, ExtensionVideo,
ExtensionDetails,
RichTextEditor, RichTextEditor,
useEditor, useEditor,
} from "../index"; } from "../index";
@ -113,6 +114,9 @@ const editor = useEditor({
ExtensionClearFormat, ExtensionClearFormat,
ExtensionFormatBrush, ExtensionFormatBrush,
ExtensionRangeSelection, ExtensionRangeSelection,
ExtensionDetails.configure({
persist: true,
}),
], ],
parseOptions: { parseOptions: {
preserveWhitespace: true, preserveWhitespace: true,

View File

@ -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<ExtensionOptions & DetailsOptions>({
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;

View File

@ -26,6 +26,7 @@ import ExtensionTable from "./table";
import ExtensionTaskList from "./task-list"; import ExtensionTaskList from "./task-list";
import ExtensionTextAlign from "./text-align"; import ExtensionTextAlign from "./text-align";
import ExtensionUnderline from "./underline"; import ExtensionUnderline from "./underline";
import ExtensionDetails from "./details";
// Custom extensions // Custom extensions
import { import {
@ -107,6 +108,7 @@ const allExtensions = [
ExtensionClearFormat, ExtensionClearFormat,
ExtensionFormatBrush, ExtensionFormatBrush,
ExtensionRangeSelection, ExtensionRangeSelection,
ExtensionDetails,
]; ];
export { export {
@ -156,6 +158,7 @@ export {
ExtensionTrailingNode, ExtensionTrailingNode,
ExtensionUnderline, ExtensionUnderline,
ExtensionVideo, ExtensionVideo,
ExtensionDetails,
RangeSelection, RangeSelection,
}; };

View File

@ -88,6 +88,8 @@ editor:
toolbar_item: toolbar_item:
title: Format Brush title: Format Brush
cancel: Cancel Format Brush cancel: Cancel Format Brush
details:
command_item: Details
components: components:
color_picker: color_picker:
more_color: More more_color: More

View File

@ -88,6 +88,8 @@ editor:
toolbar_item: toolbar_item:
title: 格式刷 title: 格式刷
cancel: 取消格式刷 cancel: 取消格式刷
details:
command_item: 折叠内容
components: components:
color_picker: color_picker:
more_color: 更多颜色 more_color: 更多颜色

View File

@ -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;
}
}
}

View File

@ -7,3 +7,4 @@
@use "gap-cursor.scss"; @use "gap-cursor.scss";
@use "node-select.scss"; @use "node-select.scss";
@use "range-selection.scss"; @use "range-selection.scss";
@use "details.scss";

View File

@ -477,6 +477,15 @@ importers:
'@tiptap/extension-color': '@tiptap/extension-color':
specifier: ^2.11.2 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))) 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': '@tiptap/extension-document':
specifier: ^2.11.2 specifier: ^2.11.2
version: 2.11.2(@tiptap/core@2.11.2(@tiptap/pm@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/core': ^2.7.0
'@tiptap/extension-text-style': ^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': '@tiptap/extension-document@2.11.2':
resolution: {integrity: sha512-/EZhIAN1x7DYgGM0xv7y7wo5ceBmHb0+rOIPuBerVFeTn+VcC3tST/Q64bdvcxgNe2E59Ti0CUdYEA51wc2u5Q==} resolution: {integrity: sha512-/EZhIAN1x7DYgGM0xv7y7wo5ceBmHb0+rOIPuBerVFeTn+VcC3tST/Q64bdvcxgNe2E59Ti0CUdYEA51wc2u5Q==}
peerDependencies: peerDependencies:
@ -15373,6 +15400,21 @@ snapshots:
'@tiptap/core': 2.11.2(@tiptap/pm@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-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))': '@tiptap/extension-document@2.11.2(@tiptap/core@2.11.2(@tiptap/pm@2.11.2))':
dependencies: dependencies:
'@tiptap/core': 2.11.2(@tiptap/pm@2.11.2) '@tiptap/core': 2.11.2(@tiptap/pm@2.11.2)

View File

@ -13,6 +13,7 @@ import {
ExtensionColumn, ExtensionColumn,
ExtensionColumns, ExtensionColumns,
ExtensionCommands, ExtensionCommands,
ExtensionDetails,
ExtensionDocument, ExtensionDocument,
ExtensionDraggable, ExtensionDraggable,
ExtensionDropcursor, ExtensionDropcursor,
@ -406,6 +407,9 @@ const presetExtensions = [
ExtensionClearFormat, ExtensionClearFormat,
ExtensionFormatBrush, ExtensionFormatBrush,
ExtensionRangeSelection, ExtensionRangeSelection,
ExtensionDetails.configure({
persist: true,
}),
]; ];
onMounted(async () => { onMounted(async () => {