From 52131519e309d5d6b4d79a497d5ac18a1bca40ad Mon Sep 17 00:00:00 2001 From: wux_labs Date: Mon, 23 Dec 2024 21:43:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=88=97=E8=A1=A8=E6=A0=B7?= =?UTF-8?q?=E5=BC=8F=E5=8F=8A=E7=BC=A9=E8=BF=9B=E8=8F=9C=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/index.ts | 14 ++ src/components/menus/MenuButton.ts | 52 +++++- src/components/menus/toolbar/Classic.ts | 55 +++++- src/components/menus/toolbar/Ribbon.ts | 58 +++++- .../menus/toolbar/base/BulletList.ts | 164 +++++++++++++++++ src/components/menus/toolbar/base/Indent.ts | 69 +++++++ .../menus/toolbar/base/LineHeight.ts | 120 ++++++++++++ .../menus/toolbar/base/OrderedList.ts | 173 ++++++++++++++++++ src/components/menus/toolbar/base/Outdent.ts | 69 +++++++ src/components/menus/toolbar/base/TaskList.ts | 97 ++++++++++ src/core/UAIExtensions.ts | 8 + src/extensions/BulletList.ts | 23 +++ src/extensions/Indent.ts | 147 +++++++++++++++ src/extensions/LineHeight.ts | 76 ++++++++ src/extensions/OrderedList.ts | 23 +++ 15 files changed, 1135 insertions(+), 13 deletions(-) create mode 100644 src/components/menus/toolbar/base/BulletList.ts create mode 100644 src/components/menus/toolbar/base/Indent.ts create mode 100644 src/components/menus/toolbar/base/LineHeight.ts create mode 100644 src/components/menus/toolbar/base/OrderedList.ts create mode 100644 src/components/menus/toolbar/base/Outdent.ts create mode 100644 src/components/menus/toolbar/base/TaskList.ts create mode 100644 src/extensions/BulletList.ts create mode 100644 src/extensions/Indent.ts create mode 100644 src/extensions/LineHeight.ts create mode 100644 src/extensions/OrderedList.ts diff --git a/src/components/index.ts b/src/components/index.ts index 2b80a87..2ac7942 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -33,6 +33,13 @@ import { ClearFormat } from "./menus/toolbar/base/ClearFormat.ts"; import { FontFamily } from "./menus/toolbar/base/FontFamily.ts"; import { FontSize } from "./menus/toolbar/base/FontSize.ts"; +import { OrderedList } from "./menus/toolbar/base/OrderedList.ts"; +import { BulletList } from "./menus/toolbar/base/BulletList.ts"; +import { TaskList } from "./menus/toolbar/base/TaskList.ts"; +import { Indent } from "./menus/toolbar/base/Indent.ts"; +import { Outdent } from "./menus/toolbar/base/Outdent.ts"; +import { LineHeight } from "./menus/toolbar/base/LineHeight.ts"; + // 注册组件 defineCustomElement('uai-editor-header', Header); defineCustomElement('uai-editor-editor', Editor); @@ -65,3 +72,10 @@ defineCustomElement('uai-editor-base-menu-clear-format', ClearFormat); defineCustomElement('uai-editor-base-menu-font-family', FontFamily); defineCustomElement('uai-editor-base-menu-font-size', FontSize); + +defineCustomElement('uai-editor-base-menu-ordered-list', OrderedList); +defineCustomElement('uai-editor-base-menu-bullet-list', BulletList); +defineCustomElement('uai-editor-base-menu-task-list', TaskList); +defineCustomElement('uai-editor-base-menu-indent', Indent); +defineCustomElement('uai-editor-base-menu-outdent', Outdent); +defineCustomElement('uai-editor-base-menu-lineheight', LineHeight); diff --git a/src/components/menus/MenuButton.ts b/src/components/menus/MenuButton.ts index 008bdb9..d508736 100644 --- a/src/components/menus/MenuButton.ts +++ b/src/components/menus/MenuButton.ts @@ -10,7 +10,7 @@ import { Icons } from "../Icons.ts"; * 菜单按钮选项 */ export type MenuButtonOptions = { - menuType: "button" | "select" | "color", + menuType: "button" | "select" | "color" | "popup", enable: boolean, className?: string, header?: "ribbon" | "classic", @@ -64,6 +64,10 @@ export class MenuButton extends HTMLElement implements UAIEditorEventListener { // 颜色选择 this.createMenuColor() } + if (this.menuButtonOptions.menuType === "popup") { + // 弹出框 + this.createMenuPopup() + } // 提示信息 var tipsContent = this.menuButtonOptions.tooltip; @@ -189,4 +193,50 @@ export class MenuButton extends HTMLElement implements UAIEditorEventListener { this.menuButtonArrow.innerHTML = Icons.ArrowDown; this.menuButton.appendChild(this.menuButtonArrow); } + + /** + * 创建弹出框 + */ + createMenuPopup() { + var size = 16; + this.menuButton = document.createElement("div"); + this.menuButton.classList.add("uai-menu-button"); + if (this.menuButtonOptions.huge) { + // 大按钮大小 + this.menuButton.classList.add("huge"); + size = 28; + } else { + // 小按钮大小 + size = 16; + } + // 传统样式设置,文字在图标右侧 + if (this.menuButtonOptions.header === "classic") { + this.menuButton.classList.add("classic-text"); + } + this.container.appendChild(this.menuButton); + + // 按钮内容 + this.menuButtonContent = document.createElement("div"); + this.menuButtonContent.classList.add("uai-button-content"); + this.menuButton.appendChild(this.menuButtonContent); + + // 按钮图标 + if (this.menuButtonOptions.icon) { + this.menuButtonContent.innerHTML = `` + } + + // 按钮文字描述 + if (!this.menuButtonOptions.hideText && this.menuButtonOptions.text) { + const menuButtonText = document.createElement("span"); + menuButtonText.classList.add("uai-button-text"); + menuButtonText.innerHTML = this.menuButtonOptions.text; + this.menuButtonContent.appendChild(menuButtonText); + } + + // 按钮弹出框三角箭头 + this.menuButtonArrow = document.createElement("div"); + this.menuButtonArrow.classList.add("uai-button-icon-arrow"); + this.menuButtonArrow.innerHTML = Icons.ArrowDown; + this.menuButton.appendChild(this.menuButtonArrow); + } } \ No newline at end of file diff --git a/src/components/menus/toolbar/Classic.ts b/src/components/menus/toolbar/Classic.ts index c52ba74..4ebc2f7 100644 --- a/src/components/menus/toolbar/Classic.ts +++ b/src/components/menus/toolbar/Classic.ts @@ -20,13 +20,21 @@ import { Superscript } from "../common/Superscript.ts"; import { FontColor } from "../common/FontColor.ts"; import { Highlight } from "../common/Highlight.ts"; -import { Redo } from "./base/Redo"; -import { Undo } from "./base/Undo"; -import { FormatPainter } from "./base/FormatPainter"; -import { ClearFormat } from "./base/ClearFormat"; +import { Redo } from "./base/Redo.ts"; +import { Undo } from "./base/Undo.ts"; -import { FontFamily } from "./base/FontFamily"; -import { FontSize } from "./base/FontSize"; +import { FormatPainter } from "./base/FormatPainter.ts"; +import { ClearFormat } from "./base/ClearFormat.ts"; + +import { FontFamily } from "./base/FontFamily.ts"; +import { FontSize } from "./base/FontSize.ts"; + +import { OrderedList } from "./base/OrderedList.ts"; +import { BulletList } from "./base/BulletList.ts"; +import { TaskList } from "./base/TaskList.ts"; +import { Indent } from "./base/Indent.ts"; +import { Outdent } from "./base/Outdent.ts"; +import { LineHeight } from "./base/LineHeight.ts"; /** * 传统菜单栏 @@ -63,6 +71,13 @@ export class Classic extends HTMLElement implements UAIEditorEventListener { baseMenuFontColor!: FontColor; baseMenuHighlight!: Highlight; + baseMenuOrderedList!: OrderedList; + baseMenuBulletList!: BulletList; + baseMenuTaskList!: TaskList; + baseMenuIndent!: Indent; + baseMenuOutdent!: Outdent; + baseMenuLineHeight!: LineHeight; + constructor(defaultToolbarMenus: Record[]) { super(); this.defaultToolbarMenus = defaultToolbarMenus; @@ -184,6 +199,24 @@ export class Classic extends HTMLElement implements UAIEditorEventListener { this.baseMenuHighlight = new Highlight({ menuType: "color", enable: true }); this.eventComponents.push(this.baseMenuHighlight); + + this.baseMenuOrderedList = new OrderedList({ menuType: "popup", enable: true }); + this.eventComponents.push(this.baseMenuOrderedList); + + this.baseMenuBulletList = new BulletList({ menuType: "popup", enable: true }); + this.eventComponents.push(this.baseMenuBulletList); + + this.baseMenuTaskList = new TaskList({ menuType: "popup", enable: true }); + this.eventComponents.push(this.baseMenuTaskList); + + this.baseMenuIndent = new Indent({ menuType: "button", enable: true }); + this.eventComponents.push(this.baseMenuIndent); + + this.baseMenuOutdent = new Outdent({ menuType: "button", enable: true }); + this.eventComponents.push(this.baseMenuOutdent); + + this.baseMenuLineHeight = new LineHeight({ menuType: "popup", enable: true }); + this.eventComponents.push(this.baseMenuLineHeight); } /** * 创建基础菜单 @@ -221,5 +254,15 @@ export class Classic extends HTMLElement implements UAIEditorEventListener { group2.appendChild(this.baseMenuSuperscript); group2.appendChild(this.baseMenuFontColor); group2.appendChild(this.baseMenuHighlight); + + const group3 = document.createElement("div"); + group3.classList.add("uai-classic-virtual-group"); + this.classicMenuBaseGroup.appendChild(group3); + group3.appendChild(this.baseMenuOrderedList); + group3.appendChild(this.baseMenuBulletList); + group3.appendChild(this.baseMenuTaskList); + group3.appendChild(this.baseMenuIndent); + group3.appendChild(this.baseMenuOutdent); + group3.appendChild(this.baseMenuLineHeight); } } \ No newline at end of file diff --git a/src/components/menus/toolbar/Ribbon.ts b/src/components/menus/toolbar/Ribbon.ts index a46151b..9eb2b81 100644 --- a/src/components/menus/toolbar/Ribbon.ts +++ b/src/components/menus/toolbar/Ribbon.ts @@ -20,14 +20,21 @@ import { Superscript } from "../common/Superscript.ts"; import { FontColor } from "../common/FontColor.ts"; import { Highlight } from "../common/Highlight.ts"; -import { Redo } from "./base/Redo"; -import { Undo } from "./base/Undo"; +import { Redo } from "./base/Redo.ts"; +import { Undo } from "./base/Undo.ts"; -import { FormatPainter } from "./base/FormatPainter"; -import { ClearFormat } from "./base/ClearFormat"; +import { FormatPainter } from "./base/FormatPainter.ts"; +import { ClearFormat } from "./base/ClearFormat.ts"; -import { FontFamily } from "./base/FontFamily"; -import { FontSize } from "./base/FontSize"; +import { FontFamily } from "./base/FontFamily.ts"; +import { FontSize } from "./base/FontSize.ts"; + +import { OrderedList } from "./base/OrderedList.ts"; +import { BulletList } from "./base/BulletList.ts"; +import { TaskList } from "./base/TaskList.ts"; +import { Indent } from "./base/Indent.ts"; +import { Outdent } from "./base/Outdent.ts"; +import { LineHeight } from "./base/LineHeight.ts"; /** * 经典菜单栏 @@ -65,6 +72,13 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener { baseMenuFontColor!: FontColor; baseMenuHighlight!: Highlight; + baseMenuOrderedList!: OrderedList; + baseMenuBulletList!: BulletList; + baseMenuTaskList!: TaskList; + baseMenuIndent!: Indent; + baseMenuOutdent!: Outdent; + baseMenuLineHeight!: LineHeight; + constructor(defaultToolbarMenus: Record[]) { super(); this.defaultToolbarMenus = defaultToolbarMenus; @@ -206,6 +220,24 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener { this.baseMenuHighlight = new Highlight({ menuType: "color", enable: true }); this.eventComponents.push(this.baseMenuHighlight); + + this.baseMenuOrderedList = new OrderedList({ menuType: "popup", enable: true }); + this.eventComponents.push(this.baseMenuOrderedList); + + this.baseMenuBulletList = new BulletList({ menuType: "popup", enable: true }); + this.eventComponents.push(this.baseMenuBulletList); + + this.baseMenuTaskList = new TaskList({ menuType: "popup", enable: true }); + this.eventComponents.push(this.baseMenuTaskList); + + this.baseMenuIndent = new Indent({ menuType: "button", enable: true }); + this.eventComponents.push(this.baseMenuIndent); + + this.baseMenuOutdent = new Outdent({ menuType: "button", enable: true }); + this.eventComponents.push(this.baseMenuOutdent); + + this.baseMenuLineHeight = new LineHeight({ menuType: "popup", enable: true }); + this.eventComponents.push(this.baseMenuLineHeight); } /** @@ -260,5 +292,19 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener { group2row2.appendChild(this.baseMenuSuperscript); group2row2.appendChild(this.baseMenuFontColor); group2row2.appendChild(this.baseMenuHighlight); + + const group3 = document.createElement("div"); + group3.classList.add("uai-ribbon-virtual-group"); + this.ribbonMenuBaseGroup.appendChild(group3); + + const group3row1 = document.createElement("div"); + group3row1.classList.add("uai-ribbon-virtual-group-row"); + group3.appendChild(group3row1); + group3row1.appendChild(this.baseMenuOrderedList); + group3row1.appendChild(this.baseMenuBulletList); + group3row1.appendChild(this.baseMenuTaskList); + group3row1.appendChild(this.baseMenuIndent); + group3row1.appendChild(this.baseMenuOutdent); + group3row1.appendChild(this.baseMenuLineHeight); } } \ No newline at end of file diff --git a/src/components/menus/toolbar/base/BulletList.ts b/src/components/menus/toolbar/base/BulletList.ts new file mode 100644 index 0000000..d86f0af --- /dev/null +++ b/src/components/menus/toolbar/base/BulletList.ts @@ -0,0 +1,164 @@ +// Copyright (c) 2024-present AI-Labs + +// @ ts-nocheck +import { MenuButton, MenuButtonOptions } from "../../MenuButton.ts"; + +import icon0 from "../../../../assets/icons/bullet-list.svg"; +import icon1 from "../../../../assets/icons/bullet-list-disc.svg"; +import icon2 from "../../../../assets/icons/bullet-list-circle.svg"; +import icon3 from "../../../../assets/icons/bullet-list-square.svg"; + +import { t } from "i18next"; +import { UAIEditorEventListener, UAIEditorOptions } from "../../../../core/UAIEditor.ts"; +import { EditorEvents } from "@tiptap/core"; +import tippy, { Instance, Props } from "tippy.js"; + +/** + * 基础菜单:无序列表 + */ +export class BulletList extends HTMLElement implements UAIEditorEventListener { + // 按钮选项 + menuButtonOptions: MenuButtonOptions = { + menuType: "popup", + enable: true, + icon: icon0, + hideText: true, + text: t('list.bullet.text'), + tooltip: t('list.bullet.text'), + shortcut: "Ctrl+Shift+8", + } + + // 功能按钮 + menuButton: MenuButton; + // 选项提示实例 + tippyInstance!: Instance; + + constructor(options: MenuButtonOptions) { + super(); + + // 初始化功能按钮选项 + this.menuButtonOptions = { ...this.menuButtonOptions, ...options }; + + // 创建功能按钮 + this.menuButton = new MenuButton(this.menuButtonOptions); + } + + /** + * 定义创建方法 + * @param event + * @param options + */ + onCreate(event: EditorEvents["create"], options: UAIEditorOptions) { + this.menuButton.onCreate(event, options); + + this.appendChild(this.menuButton); + + // 定义按钮点击事件,切换无序列表状态 + this.menuButton.menuButtonContent.addEventListener("click", () => { + if (this.menuButtonOptions.enable) { + const listType = 'disc'; + if (event.editor.isActive('bulletList')) { + // 设置无序列表 + if (event.editor.getAttributes('bulletList').listType === listType) { + event.editor.chain().focus().toggleBulletList().run(); + } else { + event.editor.chain().focus().updateAttributes('bulletList', { listType }).run(); + } + } else { + // 切换无序列表 + event.editor.chain().focus().toggleBulletList().updateAttributes('bulletList', { listType }).run(); + } + } + }) + + // 下拉选择 + if (this.menuButtonOptions.enable && this.menuButton.menuButtonArrow) { + this.tippyInstance = tippy(this.menuButton.menuButtonArrow, { + content: this.createContainer(event, options), + appendTo: 'parent', + placement: 'bottom', + trigger: 'click', + interactive: true, + arrow: false, + onShow: () => { + this.menuButton.tippyInstance?.disable(); + }, + onHidden: () => { + this.menuButton.tippyInstance?.enable(); + }, + }); + } + } + + /** + * 定义Transaction监听方法 + * @param event + * @param options + */ + onTransaction(event: EditorEvents["transaction"], options: UAIEditorOptions) { + this.menuButton.onTransaction(event, options); + } + + onEditableChange(editable: boolean) { + this.menuButtonOptions.enable = editable; + this.menuButton.onEditableChange(editable); + } + + /** + * 创建下拉选项容器 + * @param event + * @param options + * @returns + */ + createContainer(event: EditorEvents["create"], options: UAIEditorOptions) { + const container = document.createElement("div"); + container.classList.add("uai-ordered-list"); + + const group = document.createElement("div"); + group.classList.add("uai-ordered-list-group"); + + // 定义下拉选项 + const listOptions = [ + { label: t('list.bullet.disc'), value: 'disc', icon: icon1 }, + { label: t('list.bullet.circle'), value: 'circle', icon: icon2 }, + { label: t('list.bullet.square'), value: 'square', icon: icon3 } + ] + + // 添加下拉选项 + listOptions.forEach(option => { + const item = document.createElement("div"); + item.classList.add("uai-ordered-list-item"); + item.innerHTML = `` + + tippy(item, { + appendTo: item, + content: option.label, + theme: 'uai-tips', + placement: "top", + arrow: true, + interactive: true, + }); + + // 下拉选项添加点击事件,设置对应的无序列表样式 + item.addEventListener("click", () => { + const listType = option.value; + if (event.editor.isActive('bulletList')) { + if (event.editor.getAttributes('bulletList').listType === listType) { + event.editor.chain().focus().toggleBulletList().run(); + } else { + event.editor.chain().focus().updateAttributes('bulletList', { listType }).run(); + } + } else { + event.editor.chain().focus().toggleBulletList().updateAttributes('bulletList', { listType }).run(); + } + this.tippyInstance.hide(); + }) + + group.appendChild(item); + }) + + container.appendChild(group); + return container; + } +} + diff --git a/src/components/menus/toolbar/base/Indent.ts b/src/components/menus/toolbar/base/Indent.ts new file mode 100644 index 0000000..489cd2a --- /dev/null +++ b/src/components/menus/toolbar/base/Indent.ts @@ -0,0 +1,69 @@ +// Copyright (c) 2024-present AI-Labs + +// @ ts-nocheck +import { MenuButton, MenuButtonOptions } from "../../MenuButton.ts"; +import icon from "../../../../assets/icons/indent.svg"; +import { t } from "i18next"; +import { UAIEditorEventListener, UAIEditorOptions } from "../../../../core/UAIEditor.ts"; +import { EditorEvents } from "@tiptap/core"; + +/** + * 基础菜单:向右缩进 + */ +export class Indent extends HTMLElement implements UAIEditorEventListener { + // 按钮选项 + menuButtonOptions: MenuButtonOptions = { + menuType: "button", + enable: true, + icon: icon, + hideText: true, + text: t('base.indent'), + tooltip: t('base.indent'), + shortcut: "Tab", + } + + // 功能按钮 + menuButton: MenuButton; + + constructor(options: MenuButtonOptions) { + super(); + + // 初始化功能按钮选项 + this.menuButtonOptions = { ...this.menuButtonOptions, ...options }; + + // 创建功能按钮 + this.menuButton = new MenuButton(this.menuButtonOptions); + } + + /** + * 定义创建方法 + * @param event + * @param options + */ + onCreate(event: EditorEvents["create"], options: UAIEditorOptions){ + this.menuButton.onCreate(event, options); + this.appendChild(this.menuButton); + + // 定义按钮点击事件,设置当前内容向右缩进 + this.addEventListener("click", ()=> { + if(this.menuButtonOptions.enable) { + event.editor.chain().focus().indent().run(); + } + }) + } + + /** + * 定义Transaction监听方法 + * @param event + * @param options + */ + onTransaction(event: EditorEvents["transaction"], options: UAIEditorOptions){ + this.menuButton.onTransaction(event, options); + } + + onEditableChange(editable: boolean){ + this.menuButtonOptions.enable = editable; + this.menuButton.onEditableChange(editable); + } +} + diff --git a/src/components/menus/toolbar/base/LineHeight.ts b/src/components/menus/toolbar/base/LineHeight.ts new file mode 100644 index 0000000..5fd7303 --- /dev/null +++ b/src/components/menus/toolbar/base/LineHeight.ts @@ -0,0 +1,120 @@ +// Copyright (c) 2024-present AI-Labs + +// @ ts-nocheck +import { MenuButton, MenuButtonOptions } from "../../MenuButton.ts"; + +import icon from "../../../../assets/icons/line-height.svg"; + +import { t } from "i18next"; +import { UAIEditorEventListener, UAIEditorOptions } from "../../../../core/UAIEditor.ts"; +import { EditorEvents } from "@tiptap/core"; +import tippy, { Instance, Props } from "tippy.js"; + +/** + * 基础菜单:行高 + */ +export class LineHeight extends HTMLElement implements UAIEditorEventListener { + // 按钮选项 + menuButtonOptions: MenuButtonOptions = { + menuType: "popup", + enable: true, + icon: icon, + hideText: true, + text: t('base.lineHeight.text'), + tooltip: t('base.lineHeight.text'), + } + + // 功能按钮 + menuButton: MenuButton; + // 提示实例 + tippyInstance!: Instance; + + constructor(options: MenuButtonOptions) { + super(); + + // 初始化功能按钮选项 + this.menuButtonOptions = { ...this.menuButtonOptions, ...options }; + + // 创建功能按钮 + this.menuButton = new MenuButton(this.menuButtonOptions); + } + + /** + * 定义创建方法 + * @param event + * @param options + */ + onCreate(event: EditorEvents["create"], options: UAIEditorOptions) { + this.menuButton.onCreate(event, options); + + this.appendChild(this.menuButton); + + // 定义按钮点击事件,设置当前内容的行高为默认行高 + this.menuButton.menuButtonContent.addEventListener("click", () => { + if (this.menuButtonOptions.enable) { + const height = options.dicts?.lineHeights?.[0]; + if (height) { + event.editor.chain().focus().setLineHeight(+height.value).run(); + } + } + }) + + // 添加可选的行高信息 + if (this.menuButtonOptions.enable && this.menuButton.menuButtonArrow) { + this.tippyInstance = tippy(this.menuButton.menuButtonArrow, { + content: this.createContainer(event, options), + placement: 'bottom', + trigger: 'click', + interactive: true, + arrow: false, + onShow: () => { + this.menuButton.tippyInstance?.disable(); + }, + onHidden: () => { + this.menuButton.tippyInstance?.enable(); + }, + }); + } + } + + /** + * 定义Transaction监听方法 + * @param event + * @param options + */ + onTransaction(event: EditorEvents["transaction"], options: UAIEditorOptions) { + this.menuButton.onTransaction(event, options); + } + + onEditableChange(editable: boolean) { + this.menuButtonOptions.enable = editable; + this.menuButton.onEditableChange(editable); + } + + /** + * 创建所有行高选项 + * @param event + * @param options + * @returns + */ + createContainer(event: EditorEvents["create"], options: UAIEditorOptions) { + const container = document.createElement("div"); + container.classList.add("uai-popup-action-list"); + + // 设置所有可选行高 + options.dicts?.lineHeights?.forEach(height => { + const item = document.createElement("div"); + item.classList.add("uai-popup-action-item"); + item.innerHTML = `${height.label}`; + // 添加点击事件,设置内容行高 + item.addEventListener('click', () => { + event.editor.chain().focus().setLineHeight(+height.value).run(); + this.tippyInstance.hide(); + }); + container.appendChild(item); + }) + + return container; + } +} + diff --git a/src/components/menus/toolbar/base/OrderedList.ts b/src/components/menus/toolbar/base/OrderedList.ts new file mode 100644 index 0000000..5a426ef --- /dev/null +++ b/src/components/menus/toolbar/base/OrderedList.ts @@ -0,0 +1,173 @@ +// Copyright (c) 2024-present AI-Labs + +// @ ts-nocheck +import { MenuButton, MenuButtonOptions } from "../../MenuButton.ts"; + +import icon0 from "../../../../assets/icons/ordered-list.svg"; +import icon1 from "../../../../assets/icons/ordered-list-decimal.svg"; +import icon2 from "../../../../assets/icons/ordered-list-decimal-leading-zero.svg"; +import icon3 from "../../../../assets/icons/ordered-list-lower-roman.svg"; +import icon4 from "../../../../assets/icons/ordered-list-upper-roman.svg"; +import icon5 from "../../../../assets/icons/ordered-list-lower-latin.svg"; +import icon6 from "../../../../assets/icons/ordered-list-upper-latin.svg"; +import icon7 from "../../../../assets/icons/ordered-list-trad-chinese-informal.svg"; +import icon8 from "../../../../assets/icons/ordered-list-simp-chinese-formal.svg"; + +import { t } from "i18next"; +import { UAIEditorEventListener, UAIEditorOptions } from "../../../../core/UAIEditor.ts"; +import { EditorEvents } from "@tiptap/core"; +import tippy, { Instance, Props } from "tippy.js"; + +/** + * 基础菜单:有序列表 + */ +export class OrderedList extends HTMLElement implements UAIEditorEventListener { + // 按钮选项 + menuButtonOptions: MenuButtonOptions = { + menuType: "popup", + enable: true, + icon: icon0, + hideText: true, + text: t('list.ordered.text'), + tooltip: t('list.ordered.text'), + shortcut: "Ctrl+Shift+7", + } + + // 功能按钮 + menuButton: MenuButton; + // 选项提示实例 + tippyInstance!: Instance; + + constructor(options: MenuButtonOptions) { + super(); + + // 初始化功能按钮选项 + this.menuButtonOptions = { ...this.menuButtonOptions, ...options }; + + // 创建功能按钮 + this.menuButton = new MenuButton(this.menuButtonOptions); + } + + /** + * 定义创建方法 + * @param event + * @param options + */ + onCreate(event: EditorEvents["create"], options: UAIEditorOptions) { + this.menuButton.onCreate(event, options); + + this.appendChild(this.menuButton); + + // 定义按钮点击事件,切换有序列表状态 + this.menuButton.menuButtonContent.addEventListener("click", () => { + if (this.menuButtonOptions.enable) { + const listType = 'decimal'; + if (event.editor.isActive('orderedList')) { + // 设置有序列表 + if (event.editor.getAttributes('orderedList').type === listType) { + event.editor.chain().focus().toggleOrderedList().run(); + } else { + event.editor.chain().focus().updateAttributes('orderedList', { "type": listType }).run(); + } + } else { + // 切换有序列表 + event.editor.chain().focus().toggleOrderedList().updateAttributes('orderedList', { "type": listType }).run(); + } + } + }) + + // 下拉选择 + if (this.menuButtonOptions.enable && this.menuButton.menuButtonArrow) { + this.tippyInstance = tippy(this.menuButton.menuButtonArrow, { + content: this.createContainer(event, options), + placement: 'bottom', + trigger: 'click', + interactive: true, + arrow: false, + onShow: () => { + this.menuButton.tippyInstance?.disable(); + }, + onHidden: () => { + this.menuButton.tippyInstance?.enable(); + }, + }) + } + } + + /** + * 定义Transaction监听方法 + * @param event + * @param options + */ + onTransaction(event: EditorEvents["transaction"], options: UAIEditorOptions) { + this.menuButton.onTransaction(event, options); + } + + onEditableChange(editable: boolean) { + this.menuButtonOptions.enable = editable; + this.menuButton.onEditableChange(editable); + } + + /** + * 创建下拉选项容器 + * @param event + * @param options + * @returns + */ + createContainer(event: EditorEvents["create"], options: UAIEditorOptions) { + const container = document.createElement("div"); + container.classList.add("uai-ordered-list"); + + const group = document.createElement("div"); + group.classList.add("uai-ordered-list-group"); + + // 定义下拉选项 + const listOptions = [ + { label: t('list.ordered.decimal'), value: 'decimal', icon: icon1 }, + { label: t('list.ordered.decimalLeadingZero'), value: 'decimal-leading-zero', icon: icon2 }, + { label: t('list.ordered.lowerRoman'), value: 'lower-roman', icon: icon3 }, + { label: t('list.ordered.upperRoman'), value: 'upper-roman', icon: icon4 }, + { label: t('list.ordered.lowerLatin'), value: 'lower-latin', icon: icon5 }, + { label: t('list.ordered.upperLatin'), value: 'upper-latin', icon: icon6 }, + { label: t('list.ordered.tradChineseInformal'), value: 'trad-chinese-informal', icon: icon7 }, + { label: t('list.ordered.simpChineseFormal'), value: 'simp-chinese-formal', icon: icon8 }, + ] + + // 添加下拉选项 + listOptions.forEach(option => { + const item = document.createElement("div"); + item.classList.add("uai-ordered-list-item"); + item.innerHTML = `` + + tippy(item, { + appendTo: item, + content: option.label, + theme: 'uai-tips', + placement: "top", + arrow: true, + interactive: true, + }); + + // 下拉选项添加点击事件,设置对应的有序列表样式 + item.addEventListener("click", () => { + const listType = option.value; + if (event.editor.isActive('orderedList')) { + if (event.editor.getAttributes('orderedList').type === listType) { + event.editor.chain().focus().toggleOrderedList().run(); + } else { + event.editor.chain().focus().updateAttributes('orderedList', { "type": listType }).run(); + } + } else { + event.editor.chain().focus().toggleOrderedList().updateAttributes('orderedList', { "type": listType }).run(); + } + this.tippyInstance.hide(); + }) + + group.appendChild(item); + }) + + container.appendChild(group); + return container + } +} + diff --git a/src/components/menus/toolbar/base/Outdent.ts b/src/components/menus/toolbar/base/Outdent.ts new file mode 100644 index 0000000..09c8302 --- /dev/null +++ b/src/components/menus/toolbar/base/Outdent.ts @@ -0,0 +1,69 @@ +// Copyright (c) 2024-present AI-Labs + +// @ ts-nocheck +import { MenuButton, MenuButtonOptions } from "../../MenuButton.ts"; +import icon from "../../../../assets/icons/outdent.svg"; +import { t } from "i18next"; +import { UAIEditorEventListener, UAIEditorOptions } from "../../../../core/UAIEditor.ts"; +import { EditorEvents } from "@tiptap/core"; + +/** + * 基础菜单:向左缩进 + */ +export class Outdent extends HTMLElement implements UAIEditorEventListener { + // 按钮选项 + menuButtonOptions: MenuButtonOptions = { + menuType: "button", + enable: true, + icon: icon, + hideText: true, + text: t('base.outdent'), + tooltip: t('base.outdent'), + shortcut: "Shift+Tab", + } + + // 功能按钮 + menuButton: MenuButton; + + constructor(options: MenuButtonOptions) { + super(); + + // 初始化功能按钮选项 + this.menuButtonOptions = { ...this.menuButtonOptions, ...options }; + + // 创建功能按钮 + this.menuButton = new MenuButton(this.menuButtonOptions); + } + + /** + * 定义创建方法 + * @param event + * @param options + */ + onCreate(event: EditorEvents["create"], options: UAIEditorOptions){ + this.menuButton.onCreate(event, options); + this.appendChild(this.menuButton); + + // 定义按钮点击事件,设置当前内容向左缩进 + this.addEventListener("click", ()=> { + if(this.menuButtonOptions.enable) { + event.editor.chain().focus().outdent().run(); + } + }) + } + + /** + * 定义Transaction监听方法 + * @param event + * @param options + */ + onTransaction(event: EditorEvents["transaction"], options: UAIEditorOptions){ + this.menuButton.onTransaction(event, options); + } + + onEditableChange(editable: boolean){ + this.menuButtonOptions.enable = editable; + this.menuButton.onEditableChange(editable); + } +} + diff --git a/src/components/menus/toolbar/base/TaskList.ts b/src/components/menus/toolbar/base/TaskList.ts new file mode 100644 index 0000000..0be65c9 --- /dev/null +++ b/src/components/menus/toolbar/base/TaskList.ts @@ -0,0 +1,97 @@ +// Copyright (c) 2024-present AI-Labs + +// @ ts-nocheck +import { MenuButton, MenuButtonOptions } from "../../MenuButton.ts"; + +import icon from "../../../../assets/icons/task-list.svg"; + +import { t } from "i18next"; +import { UAIEditorEventListener, UAIEditorOptions } from "../../../../core/UAIEditor.ts"; +import { EditorEvents } from "@tiptap/core"; +import tippy, { Instance, Props } from "tippy.js"; + +/** + * 基础菜单:任务列表 + */ +export class TaskList extends HTMLElement implements UAIEditorEventListener { + // 按钮选项 + menuButtonOptions: MenuButtonOptions = { + menuType: "popup", + enable: true, + icon: icon, + hideText: true, + text: t('list.task.text'), + tooltip: t('list.task.text'), + shortcut: "Ctrl+Shift+9", + } + + // 功能按钮 + menuButton: MenuButton; + // 选项提示实例 + tippyInstance!: Instance; + + constructor(options: MenuButtonOptions) { + super(); + + // 初始化功能按钮选项 + this.menuButtonOptions = { ...this.menuButtonOptions, ...options }; + + // 创建功能按钮 + this.menuButton = new MenuButton(this.menuButtonOptions); + } + + /** + * 定义创建方法 + * @param event + * @param options + */ + onCreate(event: EditorEvents["create"], options: UAIEditorOptions) { + this.menuButton.onCreate(event, options); + + this.appendChild(this.menuButton); + + this.menuButton.menuButtonContent.addEventListener("click", () => { + if (this.menuButtonOptions.enable) { + event.editor.chain().focus().toggleTaskList().run(); + } + }) + + if (this.menuButtonOptions.enable && this.menuButton.menuButtonArrow) { + this.tippyInstance = tippy(this.menuButton.menuButtonArrow, { + content: this.createContainer(event, options), + placement: 'bottom', + trigger: 'click', + interactive: true, + arrow: false, + onShow: () => { + this.menuButton.tippyInstance?.disable(); + }, + onHidden: () => { + this.menuButton.tippyInstance?.enable(); + }, + }) + } + } + + /** + * 定义Transaction监听方法 + * @param event + * @param options + */ + onTransaction(event: EditorEvents["transaction"], options: UAIEditorOptions) { + this.menuButton.onTransaction(event, options); + } + + onEditableChange(editable: boolean) { + this.menuButtonOptions.enable = editable; + this.menuButton.onEditableChange(editable); + } + + createContainer(event: EditorEvents["create"], options: UAIEditorOptions) { + const container = document.createElement("div"); + container.classList.add("uai-ordered-list"); + + return container + } +} + diff --git a/src/core/UAIExtensions.ts b/src/core/UAIExtensions.ts index cf0ae09..5a4ff9e 100644 --- a/src/core/UAIExtensions.ts +++ b/src/core/UAIExtensions.ts @@ -14,6 +14,10 @@ import { TextStyle } from "@tiptap/extension-text-style"; import { Underline } from "@tiptap/extension-underline"; import FontSize from "../extensions/FontSize.ts"; +import Indent from "../extensions/Indent.ts"; +import LineHeight from "../extensions/LineHeight.ts"; +import BulletList from "../extensions/BulletList.ts"; +import OrderedList from "../extensions/OrderedList.ts"; /** * 定义编辑器的所有自定义扩展组件 @@ -27,6 +31,7 @@ export const allExtensions = (uaiEditor: UAIEditor, _options: UAIEditorOptions): bulletList: false, orderedList: false, }), + BulletList, Color, FontFamily, FontSize.configure({ @@ -35,6 +40,9 @@ export const allExtensions = (uaiEditor: UAIEditor, _options: UAIEditorOptions): Highlight.configure({ multicolor: true }), + Indent, + LineHeight, + OrderedList, Subscript, Superscript, TextStyle, diff --git a/src/extensions/BulletList.ts b/src/extensions/BulletList.ts new file mode 100644 index 0000000..09a4c40 --- /dev/null +++ b/src/extensions/BulletList.ts @@ -0,0 +1,23 @@ +// Copyright (c) 2024-present AI-Labs + +import BulletList from '@tiptap/extension-bullet-list' + +// https://www.npmjs.com/package/tiptap-extension-bullet-list +export default BulletList.extend({ + content: 'listItem*', + addAttributes() { + return { + ...this.parent?.(), + listType: { + default: 'disc', + parseHTML: (element) => + element.style.getPropertyValue('list-style-type') || 'disc', + renderHTML: ({ listType }) => { + return { + style: `list-style-type: ${listType}`, + } + }, + }, + } + }, +}) diff --git a/src/extensions/Indent.ts b/src/extensions/Indent.ts new file mode 100644 index 0000000..25ee06c --- /dev/null +++ b/src/extensions/Indent.ts @@ -0,0 +1,147 @@ +// Copyright (c) 2024-present AI-Labs + +import { Command, Extension } from '@tiptap/core'; +import { AllSelection, TextSelection, Transaction } from 'prosemirror-state'; + +export interface IndentOptions { + types: string[]; + minLevel: number; + maxLevel: number; +} + +declare module '@tiptap/core' { + interface Commands { + indent: { + /** + * 向右缩进 + * @returns + */ + indent: () => ReturnType; + /** + * 向左缩进 + * @returns + */ + outdent: () => ReturnType; + }; + } +} + +/** + * 缩进样式前缀 + */ +const classAttrPrefix = 'indent-'; + +export default Extension.create({ + name: 'indent', + + priority: 99, + + addOptions() { + return { + types: ['heading', 'listItem', 'taskItem', 'paragraph'], + minLevel: 0, + maxLevel: 8, + } + }, + + addGlobalAttributes() { + return [ + { + types: this.options.types, + attributes: { + indent: { + default: 0, + parseHTML: element => { + let level = 0 + for (const className of element.classList) { + if (className.startsWith(classAttrPrefix)) { + level = +(className.split("-")[1]) + break + } + } + return level && level > this.options.minLevel ? level : null; + }, + + renderHTML: attributes => { + if (!attributes.indent) { + return {} + } + + return { + class: `${classAttrPrefix}${attributes.indent}`, + } + }, + }, + }, + }, + ]; + }, + + addCommands() { + const setNodeIndentMarkup = (tr: Transaction, pos: number, delta: number): Transaction => { + const node = tr?.doc?.nodeAt(pos); + + if (node) { + const nextLevel = (node.attrs.indent || 0) + delta; + const { minLevel, maxLevel } = this.options; + const indent = nextLevel < minLevel ? minLevel : nextLevel > maxLevel ? maxLevel : nextLevel; + + if (indent !== node.attrs.indent) { + const { indent: oldIndent, ...currentAttrs } = node.attrs; + const nodeAttrs = indent > minLevel ? { ...currentAttrs, indent } : currentAttrs; + return tr.setNodeMarkup(pos, node.type, nodeAttrs, node.marks); + } + } + return tr; + }; + + const updateIndentLevel = (tr: Transaction, delta: number): Transaction => { + const { doc, selection } = tr; + + if (doc && selection && (selection instanceof TextSelection || selection instanceof AllSelection)) { + const { from, to } = selection; + doc.nodesBetween(from, to, (node, pos) => { + if (this.options.types.includes(node.type.name)) { + tr = setNodeIndentMarkup(tr, pos, delta); + return false; + } + + return true; + }); + } + + return tr; + }; + + const applyIndent: (direction: number) => () => Command = + direction => + () => + ({ tr, state, dispatch }) => { + const { selection } = state; + tr = tr.setSelection(selection); + tr = updateIndentLevel(tr, direction); + + if (tr.docChanged) { + dispatch?.(tr); + return true; + } + + return false; + }; + return { + indent: applyIndent(1), + outdent: applyIndent(-1), + }; + }, + + addKeyboardShortcuts() { + return { + Tab: () => { + return this.editor.commands.indent(); + }, + 'Shift-Tab': () => { + return this.editor.commands.outdent(); + }, + }; + }, +}); diff --git a/src/extensions/LineHeight.ts b/src/extensions/LineHeight.ts new file mode 100644 index 0000000..53dbd7b --- /dev/null +++ b/src/extensions/LineHeight.ts @@ -0,0 +1,76 @@ +// Copyright (c) 2024-present AI-Labs + +import { Extension } from '@tiptap/core' + +export interface LineHeightOptions { + types: string[]; + defaultLineHeight: number; +} + +declare module '@tiptap/core' { + interface Commands { + setLineHeight: { + /** + * 设置行高 + * @param lineHeight + * @returns + */ + setLineHeight: (lineHeight: number) => ReturnType + } + unsetLineHeight: { + /** + * 取消设置行高 + * @returns + */ + unsetLineHeight: () => ReturnType + } + } +} + +export default Extension.create({ + name: 'lineHeight', + addOptions() { + return { + types: ["heading", "paragraph"], + defaultLineHeight: 1.5, + } + }, + addGlobalAttributes() { + return [ + { + types: this.options.types, + attributes: { + lineHeight: { + default: this.options.defaultLineHeight, + parseHTML: (element) => + element.style.lineHeight || this.options.defaultLineHeight, + renderHTML: (attributes) => { + if (attributes.lineHeight === this.options.defaultLineHeight) { + return {} + } + return { style: `line-height: ${attributes.lineHeight}` } + }, + }, + }, + }, + ] + }, + addCommands() { + return { + setLineHeight: + (lineHeight) => + ({ commands }) => { + return this.options.types.every((type: string) => + commands.updateAttributes(type, { lineHeight: lineHeight }), + ) + }, + unsetLineHeight: + () => + ({ commands }) => { + return this.options.types.every((type: string) => + commands.resetAttributes(type, 'lineHeight'), + ) + }, + } + }, +}) diff --git a/src/extensions/OrderedList.ts b/src/extensions/OrderedList.ts new file mode 100644 index 0000000..8bf1be7 --- /dev/null +++ b/src/extensions/OrderedList.ts @@ -0,0 +1,23 @@ +// Copyright (c) 2024-present AI-Labs + +import OrderedList from '@tiptap/extension-ordered-list' + +// https://www.npmjs.com/package/tiptap-extension-ordered-list +export default OrderedList.extend({ + content: 'listItem*', + addAttributes() { + return { + ...this.parent?.(), + listType: { + default: 'decimal', + parseHTML: (element) => + element.style.getPropertyValue('list-style-type') || 'decimal', + renderHTML: ({ listType }) => { + return { + style: `list-style-type: ${listType}`, + } + }, + }, + } + }, +})