mirror of https://github.com/halo-dev/halo
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
parent
3b1200ff2e
commit
3ac09524e0
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -88,6 +88,8 @@ editor:
|
|||
toolbar_item:
|
||||
title: Format Brush
|
||||
cancel: Cancel Format Brush
|
||||
details:
|
||||
command_item: Details
|
||||
components:
|
||||
color_picker:
|
||||
more_color: More
|
||||
|
|
|
@ -88,6 +88,8 @@ editor:
|
|||
toolbar_item:
|
||||
title: 格式刷
|
||||
cancel: 取消格式刷
|
||||
details:
|
||||
command_item: 折叠内容
|
||||
components:
|
||||
color_picker:
|
||||
more_color: 更多颜色
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,3 +7,4 @@
|
|||
@use "gap-cursor.scss";
|
||||
@use "node-select.scss";
|
||||
@use "range-selection.scss";
|
||||
@use "details.scss";
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 () => {
|
||||
|
|
Loading…
Reference in New Issue