diff --git a/src/components/index.ts b/src/components/index.ts index f14250a..ec89ebe 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -51,6 +51,8 @@ import { CodeBlock } from "./menus/toolbar/base/CodeBlock.ts"; import { Print } from "./menus/toolbar/base/Print.ts"; +import { Link } from "./menus/toolbar/insert/Link.ts"; + // 注册组件 defineCustomElement('uai-editor-header', Header); defineCustomElement('uai-editor-editor', Editor); @@ -101,3 +103,5 @@ defineCustomElement('uai-editor-base-menu-blockquote', BlockQuote); defineCustomElement('uai-editor-base-menu-codeblock', CodeBlock); defineCustomElement('uai-editor-base-menu-print', Print); + +defineCustomElement('uai-editor-insert-menu-link', Link); diff --git a/src/components/menus/toolbar/Classic.ts b/src/components/menus/toolbar/Classic.ts index 0dc384f..077a8a8 100644 --- a/src/components/menus/toolbar/Classic.ts +++ b/src/components/menus/toolbar/Classic.ts @@ -46,6 +46,8 @@ import { BlockQuote } from "./base/BlockQuote.ts"; import { CodeBlock } from "./base/CodeBlock.ts"; import { Print } from "./base/Print.ts"; +import { Link } from "./insert/Link"; + /** * 传统菜单栏 */ @@ -61,6 +63,10 @@ export class Classic extends HTMLElement implements UAIEditorEventListener { classicMenuBaseScrollable!: ScrollableDiv; classicMenuBaseGroup!: HTMLElement; + // 插入菜单容器 + classicMenuInsertScrollable!: ScrollableDiv; + classicMenuInsertGroup!: HTMLElement; + // 基础菜单 baseMenuUndo!: Undo; baseMenuRedo!: Redo; @@ -97,6 +103,9 @@ export class Classic extends HTMLElement implements UAIEditorEventListener { baseMenuCodeBlock!: CodeBlock; baseMenuPrint!: Print; + // 插入菜单 + insertMenuLink!: Link; + constructor(defaultToolbarMenus: Record[]) { super(); this.defaultToolbarMenus = defaultToolbarMenus; @@ -141,12 +150,17 @@ export class Classic extends HTMLElement implements UAIEditorEventListener { selectMuenus.addEventListener("change", () => { const menu = selectMuenus.selectedOptions[0].value; this.classicMenuBaseScrollable.style.display = "none"; + this.classicMenuInsertScrollable.style.display = "none"; if (menu === "base") { this.classicMenuBaseScrollable.style.display = "flex"; } + if (menu === "insert") { + this.classicMenuInsertScrollable.style.display = "flex"; + } }) // 创建分组菜单 this.createBaseMenu(event, options); + this.createInsertMenu(event, options); this.classicMenuBaseScrollable.style.display = "flex"; } @@ -260,6 +274,9 @@ export class Classic extends HTMLElement implements UAIEditorEventListener { this.baseMenuPrint = new Print({ menuType: "button", enable: true, header: "classic", hideText: false }); this.eventComponents.push(this.baseMenuPrint); + + this.insertMenuLink = new Link({ menuType: "button", enable: true, header: "classic", hideText: false }); + this.eventComponents.push(this.insertMenuLink); } /** * 创建基础菜单 @@ -320,4 +337,23 @@ export class Classic extends HTMLElement implements UAIEditorEventListener { this.classicMenuBaseGroup.appendChild(group4); group4.appendChild(this.baseMenuPrint); } + + /** + * 创建插入菜单 + * @param event + * @param options + */ + createInsertMenu(event: EditorEvents["create"], options: UAIEditorOptions) { + this.classicMenuInsertGroup = document.createElement("div"); + this.classicMenuInsertGroup.style.display = "flex"; + this.classicMenuInsertScrollable = new ScrollableDiv(this.classicMenuInsertGroup); + this.classicMenuInsertScrollable.style.display = "none"; + this.classicMenu.appendChild(this.classicMenuInsertScrollable); + this.classicMenuInsertScrollable.onCreate(event, options); + + const group1 = document.createElement("div"); + group1.classList.add("uai-classic-virtual-group"); + this.classicMenuInsertGroup.appendChild(group1); + group1.appendChild(this.insertMenuLink); + } } \ No newline at end of file diff --git a/src/components/menus/toolbar/Ribbon.ts b/src/components/menus/toolbar/Ribbon.ts index c04b02a..eabe5e8 100644 --- a/src/components/menus/toolbar/Ribbon.ts +++ b/src/components/menus/toolbar/Ribbon.ts @@ -48,6 +48,8 @@ import { CodeBlock } from "./base/CodeBlock.ts"; import { Print } from "./base/Print.ts"; +import { Link } from "./insert/Link"; + /** * 经典菜单栏 */ @@ -64,6 +66,10 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener { ribbonMenuBaseScrollable!: ScrollableDiv; ribbonMenuBaseGroup!: HTMLElement; + // 插入菜单容器 + ribbonMenuInsertScrollable!: ScrollableDiv; + ribbonMenuInsertGroup!: HTMLElement; + // 基础菜单 baseMenuUndo!: Undo; baseMenuRedo!: Redo; @@ -100,6 +106,9 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener { baseMenuCodeBlock!: CodeBlock; baseMenuPrint!: Print; + // 插入菜单 + insertMenuLink!: Link; + constructor(defaultToolbarMenus: Record[]) { super(); this.defaultToolbarMenus = defaultToolbarMenus; @@ -137,9 +146,13 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener { } tab.classList.add("active"); this.ribbonMenuBaseScrollable.style.display = "none"; + this.ribbonMenuInsertScrollable.style.display = "none"; if (menu.value === "base") { this.ribbonMenuBaseScrollable.style.display = "flex"; } + if (menu.value === "insert") { + this.ribbonMenuInsertScrollable.style.display = "flex"; + } }) ribbonTabs.appendChild(tab); }); @@ -158,6 +171,7 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener { // 创建分组菜单 this.createBaseMenu(event, options); + this.createInsertMenu(event, options); this.ribbonMenuBaseScrollable.style.display = "flex"; } @@ -283,6 +297,9 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener { this.baseMenuPrint = new Print({ menuType: "button", enable: true, huge: true, hideText: false }); this.eventComponents.push(this.baseMenuPrint); + + this.insertMenuLink = new Link({ menuType: "button", enable: true, huge: true }); + this.eventComponents.push(this.insertMenuLink); } /** @@ -417,4 +434,23 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener { this.ribbonMenuBaseGroup.appendChild(group5); group5.appendChild(this.baseMenuPrint); } + + /** + * 创建插入菜单 + * @param event + * @param options + */ + createInsertMenu(_event: EditorEvents["create"], _options: UAIEditorOptions) { + this.ribbonMenuInsertGroup = document.createElement("div"); + this.ribbonMenuInsertGroup.classList.add("uai-ribbon-container"); + this.ribbonMenuInsertGroup.style.display = "flex"; + this.ribbonMenuInsertScrollable = new ScrollableDiv(this.ribbonMenuInsertGroup); + this.ribbonMenuInsertScrollable.style.display = "none"; + this.ribbonScrollableContainer.appendChild(this.ribbonMenuInsertScrollable); + + const group1 = document.createElement("div"); + group1.classList.add("uai-ribbon-virtual-group"); + this.ribbonMenuInsertGroup.appendChild(group1); + group1.appendChild(this.insertMenuLink); + } } \ No newline at end of file diff --git a/src/components/menus/toolbar/insert/Link.ts b/src/components/menus/toolbar/insert/Link.ts new file mode 100644 index 0000000..fe984b5 --- /dev/null +++ b/src/components/menus/toolbar/insert/Link.ts @@ -0,0 +1,130 @@ +// Copyright (c) 2024-present AI-Labs + +// @ ts-nocheck +import { MenuButton, MenuButtonOptions } from "../../MenuButton.ts"; +import icon from "../../../../assets/icons/link.svg"; +import { t } from "i18next"; +import { UAIEditorEventListener, UAIEditorOptions, InnerEditor } from "../../../../core/UAIEditor.ts"; +import { EditorEvents } from "@tiptap/core"; +import { CenterModal } from "../../../modals/CenterModal.ts"; + +/** + * 插入菜单:插入链接 + */ +export class Link extends HTMLElement implements UAIEditorEventListener { + // 按钮选项 + menuButtonOptions: MenuButtonOptions = { + menuType: "button", + enable: true, + icon: icon, + hideText: false, + text: t('insert.link.text'), + tooltip: t('insert.link.text'), + } + + // 功能按钮 + menuButton: MenuButton; + // 模态框 + modal: CenterModal; + // 链接文本 + textInput!: HTMLInputElement; + // 链接地址 + hrefInput!: HTMLInputElement; + + constructor(options: MenuButtonOptions) { + super(); + + // 初始化功能按钮选项 + this.menuButtonOptions = { ...this.menuButtonOptions, ...options }; + + // 创建功能按钮 + this.menuButton = new MenuButton(this.menuButtonOptions); + + // 创建模态框 + this.modal = new CenterModal(t('insert.link.title')); + } + + /** + * 定义创建方法 + * @param event + * @param options + */ + onCreate(event: EditorEvents["create"], options: UAIEditorOptions) { + this.menuButton.onCreate(event, options); + this.appendChild(this.menuButton); + + // 定义模态框中链接文本输入 + const group1 = document.createElement("div"); + group1.classList.add("form-group"); + + // 链接文本输入框 + const textLabel = document.createElement("label") + textLabel.setAttribute("for", "textInput"); + textLabel.innerHTML = t('insert.link.hrefText'); + this.textInput = document.createElement("input"); + this.textInput.id = "textInput"; + this.textInput.classList.add("form-control"); + + group1.appendChild(textLabel); + group1.appendChild(this.textInput); + this.modal.modalBody.appendChild(group1); + + const group2 = document.createElement("div"); + group2.classList.add("form-group"); + + // 定义模态框中链接地址输入 + const hrefLabel = document.createElement("label") + hrefLabel.setAttribute("for", "hrefInput"); + hrefLabel.innerHTML = t('insert.link.href'); + this.hrefInput = document.createElement("input"); + this.hrefInput.id = "hrefInput"; + this.hrefInput.classList.add("form-control"); + + // 链接地址输入框 + group2.appendChild(hrefLabel); + group2.appendChild(this.hrefInput); + this.modal.modalBody.appendChild(group2); + + // 定义按钮点击事件,模态框中点确认时插入链接到文档 + this.modal.commit.addEventListener("click", () => { + event.editor.commands.setLink({ href: this.hrefInput.value, target: '_blank' }); + event.editor.chain().focus().insertContent(this.textInput.value).run(); + this.modal.hide(); + }); + // 定义按钮点击事件,弹出模态框并编辑 + this.addEventListener("click", () => { + if (this.menuButtonOptions.enable) { + const state = event.editor.state; + this.textInput.value = state.doc.textBetween(state.selection.from, state.selection.to); + const href = event.editor.getAttributes('link').href; + if (href) { + this.hrefInput.value = href; + } + this.modal.show(); + } + }) + } + + /** + * 定义Transaction监听方法 + * @param event + * @param options + */ + onTransaction(event: EditorEvents["transaction"], options: UAIEditorOptions) { + this.menuButton.onTransaction(event, options); + if (this.menuButton.menuButton) { + var disable = event.editor.isEditable; + this.onEditableChange(disable); + } + // 清空输入内容 + if (this.hrefInput) { + this.hrefInput.value = ""; + } + } + + onEditableChange(editable: boolean) { + this.menuButtonOptions.enable = editable; + this.menuButton.onEditableChange(editable); + } +} + diff --git a/src/components/modals/CenterModal.ts b/src/components/modals/CenterModal.ts new file mode 100644 index 0000000..4723ec6 --- /dev/null +++ b/src/components/modals/CenterModal.ts @@ -0,0 +1,74 @@ +// Copyright (c) 2024-present AI-Labs + +import { Modal } from "bootstrap"; + +import 'bootstrap/dist/css/bootstrap.css'; + +/** + * 居中的模态弹出框 + */ +export class CenterModal extends Modal { + + modalHeader!: HTMLElement; + modalBody!: HTMLElement; + modalFooter!: HTMLElement; + commit: HTMLButtonElement; + + constructor(title: string, options?: Partial) { + const modal = document.createElement("div"); + // 设置模态弹出框 + modal.classList.add("modal"); + modal.setAttribute("role", "dialog"); + modal.setAttribute("tabindex", "-1"); + modal.setAttribute("aria-hidden", "true"); + modal.setAttribute("aria-labelledby", "exampleModalCenterTitle"); + + // 模态弹出框居中样式 + const modalDialog = document.createElement("div"); + modalDialog.classList.add("modal-dialog"); + modalDialog.classList.add("modal-dialog-centered"); + modalDialog.setAttribute("role", "document"); + + // 模态弹出框内容 + const modalContent = document.createElement("div"); + modalContent.classList.add("modal-content"); + modalDialog.appendChild(modalContent); + + modal.appendChild(modalDialog); + super(modal, options); + + // 标题区 + this.modalHeader = document.createElement("div"); + this.modalHeader.classList.add("modal-header"); + this.modalHeader.innerHTML = `` + modalContent.appendChild(this.modalHeader); + + // 主体区 + this.modalBody = document.createElement("div"); + this.modalBody.classList.add("modal-body"); + modalContent.appendChild(this.modalBody); + + // 底部按钮区 + this.modalFooter = document.createElement("div"); + this.modalFooter.classList.add("modal-footer"); + modalContent.appendChild(this.modalFooter); + + // 关闭按钮 + const close = document.createElement("button"); + close.classList.add("btn"); + close.classList.add("btn-secondary"); + close.setAttribute("data-dismiss", "modal"); + close.innerHTML = "关闭"; + close.addEventListener("click", () => { + this.hide(); + }); + this.modalFooter.appendChild(close); + + // 确定按钮 + this.commit = document.createElement("button") + this.commit.classList.add("btn"); + this.commit.classList.add("btn-primary"); + this.commit.innerHTML = "确定"; + this.modalFooter.appendChild(this.commit); + } +} \ No newline at end of file diff --git a/src/core/UAIExtensions.ts b/src/core/UAIExtensions.ts index c046c6b..0ea7593 100644 --- a/src/core/UAIExtensions.ts +++ b/src/core/UAIExtensions.ts @@ -8,6 +8,7 @@ import { StarterKit } from "@tiptap/starter-kit"; import { Color } from "@tiptap/extension-color"; import { FontFamily } from "@tiptap/extension-font-family"; import { Highlight } from "@tiptap/extension-highlight"; +import { Link } from "@tiptap/extension-link"; import { Subscript } from "@tiptap/extension-subscript"; import { Superscript } from "@tiptap/extension-superscript"; import { TaskItem } from "@tiptap/extension-task-item"; @@ -15,7 +16,7 @@ import { TaskList } from "@tiptap/extension-task-list"; import { TextAlign } from "@tiptap/extension-text-align"; import { TextStyle } from "@tiptap/extension-text-style"; import { Underline } from "@tiptap/extension-underline"; -; + import BulletList from "../extensions/BulletList.ts"; import FontSize from "../extensions/FontSize.ts"; import Indent from "../extensions/Indent.ts"; @@ -46,6 +47,7 @@ export const allExtensions = (uaiEditor: UAIEditor, _options: UAIEditorOptions): }), Indent, LineHeight, + Link, NodeAlign, OrderedList, Subscript,