From e0cf01c8195a3b9221e20d1f23b23a5b5b5d98ba Mon Sep 17 00:00:00 2001 From: wux_labs Date: Fri, 20 Dec 2024 19:59:38 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=87=E6=A1=A3=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Editor.ts | 69 ++++++++ src/components/Footer.ts | 55 ++++++ src/components/Header.ts | 222 ++++++++++++++++++++++++ src/components/index.ts | 18 ++ src/components/menus/toolbar/Classic.ts | 84 +++++++++ src/components/menus/toolbar/Ribbon.ts | 108 ++++++++++++ src/utils/DefineCustomElement.ts | 12 ++ 7 files changed, 568 insertions(+) create mode 100644 src/components/Editor.ts create mode 100644 src/components/Footer.ts create mode 100644 src/components/Header.ts create mode 100644 src/components/index.ts create mode 100644 src/components/menus/toolbar/Classic.ts create mode 100644 src/components/menus/toolbar/Ribbon.ts create mode 100644 src/utils/DefineCustomElement.ts diff --git a/src/components/Editor.ts b/src/components/Editor.ts new file mode 100644 index 0000000..4a0e7eb --- /dev/null +++ b/src/components/Editor.ts @@ -0,0 +1,69 @@ +// Copyright (c) 2024-present AI-Labs + +// @ ts-nocheck +import { EditorEvents } from "@tiptap/core"; +import { UAIEditorEventListener, UAIEditorOptions } from "../core/UAIEditor.ts"; + +/** + * 编辑器界面,主要是用来组织编辑器页面布局 + */ +export class Editor extends HTMLElement implements UAIEditorEventListener { + // 主容器 + mainContainer!: HTMLElement; + // 分页容器 + pageContainer!: HTMLElement; + // 可缩放容器 + zoomableContainer!: HTMLElement; + // 缩放内容 + zoomableContent!: HTMLElement; + // 页面内容 + pageContent!: HTMLElement; + // 编辑器容器 + editorContainer!: HTMLElement; + + constructor() { + super(); + + this.pageContainer = document.createElement("div"); + this.pageContainer.classList.add("uai-page-container"); + this.appendChild(this.pageContainer); + + this.zoomableContainer = document.createElement("div"); + this.zoomableContainer.classList.add("uai-zoomable-container"); + this.pageContainer.appendChild(this.zoomableContainer); + + this.zoomableContent = document.createElement("div"); + this.zoomableContent.classList.add("uai-zoomable-content"); + this.zoomableContainer.appendChild(this.zoomableContent); + + this.pageContent = document.createElement("div"); + this.pageContent.classList.add("uai-page-content"); + this.zoomableContent.appendChild(this.pageContent); + + this.editorContainer = document.createElement("div"); + this.editorContainer.classList.add("uai-editor-container"); + this.pageContent.appendChild(this.editorContainer); + } + + /** + * 定义创建方法 + * @param event + * @param options + */ + onCreate(_event: EditorEvents["create"], _options: UAIEditorOptions) { + + } + + /** + * 定义Transaction监听方法 + * @param event + * @param options + */ + onTransaction(_event: EditorEvents["transaction"], _options: UAIEditorOptions) { + + } + + onEditableChange(_editable: boolean) { + + } +} \ No newline at end of file diff --git a/src/components/Footer.ts b/src/components/Footer.ts new file mode 100644 index 0000000..6481897 --- /dev/null +++ b/src/components/Footer.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2024-present AI-Labs + +// @ ts-nocheck +import { EditorEvents } from "@tiptap/core"; +import { UAIEditorEventListener, UAIEditorOptions } from "../core/UAIEditor.ts"; + +/** + * 编辑器底部状态栏 + */ +export class Footer extends HTMLElement implements UAIEditorEventListener { + container!: HTMLElement; + + constructor() { + super(); + } + + /** + * 定义创建方法 + * @param event + * @param options + */ + onCreate(event: EditorEvents["create"], options: UAIEditorOptions) { + this.container = document.createElement("div"); + this.container.classList.add("uai-footer"); + this.appendChild(this.container); + + // 状态栏 + const statusBar = document.createElement("div"); + statusBar.classList.add("uai-status-bar"); + this.container.appendChild(statusBar); + + // 状态栏左边区域 + const statusBarLeft = document.createElement("div"); + statusBarLeft.classList.add("uai-status-bar-left"); + statusBar.appendChild(statusBarLeft); + + // 状态栏右边区域 + const statusBarRight = document.createElement("div"); + statusBarRight.classList.add("uai-status-bar-right"); + statusBar.appendChild(statusBarRight); + } + + /** + * 定义Transaction监听方法 + * @param event + * @param options + */ + onTransaction(event: EditorEvents["transaction"], options: UAIEditorOptions) { + + } + + onEditableChange(_editable: boolean) { + + } +} \ No newline at end of file diff --git a/src/components/Header.ts b/src/components/Header.ts new file mode 100644 index 0000000..86bce45 --- /dev/null +++ b/src/components/Header.ts @@ -0,0 +1,222 @@ +// Copyright (c) 2024-present AI-Labs + +// @ ts-nocheck +import { t } from "i18next"; +import expandDownIcon from "../assets/icons/expand-down.svg"; +import toolbarRibbon from "../assets/icons/toolbar-ribbon.svg"; +import toolbarClassic from "../assets/icons/toolbar-classic.svg"; +import toolbarSource from "../assets/icons/toolbar-source.svg"; +import codeblock from "../assets/icons/codeblock.svg"; +import { Ribbon } from "./menus/toolbar/Ribbon"; +import { Classic } from "./menus/toolbar/Classic"; +import { InnerEditor, UAIEditorEventListener, UAIEditorOptions } from "../core/UAIEditor.ts"; +import { EditorEvents } from "@tiptap/core"; +import tippy, { Instance, Props } from "tippy.js"; + +/** + * 文档顶部菜单栏 + */ +export class Header extends HTMLElement implements UAIEditorEventListener { + toolbar!: HTMLElement; + container!: HTMLElement; + actions!: HTMLElement; + + defaultToolbarMenus!: Record[]; + + // 专业工具栏 + ribbonMenu!: Ribbon; + // 经典工具栏 + classicMenu!: Classic; + // 源码编辑器 + sourceMenu!: HTMLElement; + + // 菜单模式 + menuMode!: "ribbon" | "classic" | "source"; + tippyInstance!: Instance; + + constructor() { + super(); + + // 初始化所有菜单分组 + this.defaultToolbarMenus = [ + { label: t('toolbar.base'), value: 'base' }, + { label: t('toolbar.insert'), value: 'insert' }, + { label: t('toolbar.table'), value: 'table' }, + { label: t('toolbar.tools'), value: 'tools' }, + { label: t('toolbar.page'), value: 'page' }, + { label: t('toolbar.export'), value: 'export' }, + { label: t('toolbar.ai'), value: 'ai' }, + ]; + // 创建专业工具栏 + this.ribbonMenu = new Ribbon(this.defaultToolbarMenus); + // 创建经典工具栏 + this.classicMenu = new Classic(this.defaultToolbarMenus); + // 创建源码编辑器 + this.sourceMenu = document.createElement("div"); + this.sourceMenu.classList.add("uai-classic-scrollable-container"); + this.sourceMenu.innerHTML = ` ${t('toolbar.source')}`; + } + + /** + * 定义创建方法 + * @param event + * @param options + */ + onCreate(event: EditorEvents["create"], options: UAIEditorOptions) { + // 创建工具栏 + this.toolbar = document.createElement("div"); + this.toolbar.classList.add("uai-toolbar"); + this.appendChild(this.toolbar); + + this.container = document.createElement("div"); + this.container.classList.add("uai-toolbar-container"); + this.toolbar.appendChild(this.container); + + // 初始化专业工具栏 + this.ribbonMenu.onCreate(event, options); + // 初始化经典工具栏 + this.classicMenu.onCreate(event, options); + + this.container.appendChild(this.ribbonMenu); + this.container.appendChild(this.classicMenu); + this.container.appendChild(this.sourceMenu); + + // 添加工具栏切换菜单 + this.actions = document.createElement("div"); + this.actions.classList.add("uai-toolbar-actions"); + this.container.appendChild(this.actions); + + const actionsButton = document.createElement("div"); + actionsButton.classList.add("uai-toolbar-actions-button"); + this.actions.appendChild(actionsButton); + + const actionsButtonIcon = document.createElement("img"); + actionsButtonIcon.src = expandDownIcon; + actionsButtonIcon.width = 12; + actionsButton.appendChild(actionsButtonIcon); + + const actionsButtonText = document.createElement("span"); + actionsButtonText.classList.add("uai-button-text"); + actionsButtonText.innerHTML = ` ${t('toolbar.toggle')}`; + actionsButton.appendChild(actionsButtonText); + + // 工具栏切换菜单 + this.tippyInstance = tippy(actionsButton, { + content: this.createContainer(event, options), + placement: 'bottom', + trigger: 'click', + interactive: true, + arrow: false, + onShow: () => { + }, + onHidden: () => { + }, + }); + + if (options.header === "ribbon") { + this.switchRibbonMenu(event); + } else { + this.switchClassicMenu(event); + } + } + + /** + * 定义Transaction监听方法 + * @param event + * @param options + */ + onTransaction(event: EditorEvents["transaction"], options: UAIEditorOptions) { + this.ribbonMenu.onTransaction(event, options); + this.classicMenu.onTransaction(event, options); + } + + onEditableChange(editable: boolean) { + this.ribbonMenu.onEditableChange(editable); + this.classicMenu.onEditableChange(editable); + } + + /** + * 切换专业工具栏 + * @param event + */ + switchRibbonMenu(event: EditorEvents["create"]) { + this.ribbonMenu.style.display = "flex"; + this.classicMenu.style.display = "none"; + this.sourceMenu.style.display = "none"; + this.menuMode = "ribbon"; + (event.editor as InnerEditor).uaiEditor.switchEditor(); + } + + /** + * 切换经典工具栏 + * @param event + */ + switchClassicMenu(event: EditorEvents["create"]) { + this.ribbonMenu.style.display = "none"; + this.classicMenu.style.display = "flex"; + this.sourceMenu.style.display = "none"; + this.menuMode = "classic"; + (event.editor as InnerEditor).uaiEditor.switchEditor(); + } + + /** + * 切换源码编辑器 + * @param event + */ + switchSourceMenu(event: EditorEvents["create"]) { + this.ribbonMenu.style.display = "none"; + this.classicMenu.style.display = "none"; + this.sourceMenu.style.display = "flex"; + this.menuMode = "source"; + (event.editor as InnerEditor).uaiEditor.switchSource(); + } + + /** + * 创建工具栏切换菜单选项提示框 + * @param event + * @param options + * @returns + */ + createContainer(event: EditorEvents["create"], options: UAIEditorOptions) { + const container = document.createElement("div"); + container.classList.add("uai-popup-action-list"); + + // 专业工具栏菜单 + const ribbon = document.createElement("div"); + ribbon.classList.add("uai-popup-action-item"); + ribbon.innerHTML = ` ${t('toolbar.ribbon')}`; + // 点击切换到专业工具栏 + ribbon.addEventListener('click', () => { + this.switchRibbonMenu(event); + this.tippyInstance.hide(); + }); + container.appendChild(ribbon); + + // 经典工具栏菜单 + const classic = document.createElement("div"); + classic.classList.add("uai-popup-action-item"); + classic.innerHTML = ` ${t('toolbar.classic')}`; + // 点击切换到经典工具栏 + classic.addEventListener('click', () => { + this.switchClassicMenu(event); + this.tippyInstance.hide(); + }); + container.appendChild(classic); + + // 源码编辑器菜单 + const source = document.createElement("div"); + source.classList.add("uai-popup-action-item"); + source.innerHTML = ` ${t('toolbar.source')}`; + // 点击切换到源码编辑器 + source.addEventListener('click', () => { + this.switchSourceMenu(event); + this.tippyInstance.hide(); + }); + container.appendChild(source); + + return container; + } + + connectedCallback() { + } +} \ No newline at end of file diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..f4d1aed --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,18 @@ +// Copyright (c) 2024-present AI-Labs + +import { defineCustomElement } from "../utils/DefineCustomElement.ts"; +import { Header } from "../components/Header.ts"; +import { Editor } from "../components/Editor.ts"; +import { Footer } from "../components/Footer.ts"; + +import { Ribbon } from "./menus/toolbar/Ribbon.ts"; +import { Classic } from "./menus/toolbar/Classic.ts"; + + +// 注册组件 +defineCustomElement('uai-editor-header', Header); +defineCustomElement('uai-editor-editor', Editor); +defineCustomElement('uai-editor-footer', Footer); + +defineCustomElement('uai-editor-ribbon-menu', Ribbon); +defineCustomElement('uai-editor-classic-menu', Classic); diff --git a/src/components/menus/toolbar/Classic.ts b/src/components/menus/toolbar/Classic.ts new file mode 100644 index 0000000..a704d94 --- /dev/null +++ b/src/components/menus/toolbar/Classic.ts @@ -0,0 +1,84 @@ +// Copyright (c) 2024-present AI-Labs + +// @ ts-nocheck +import { EditorEvents } from "@tiptap/core"; +import { UAIEditorEventListener, UAIEditorOptions } from "../../../core/UAIEditor.ts"; + +import menuIcon from "../../../assets/icons/menu.svg"; + +/** + * 传统菜单栏 + */ +export class Classic extends HTMLElement implements UAIEditorEventListener { + defaultToolbarMenus!: Record[]; + eventComponents: UAIEditorEventListener[] = []; + + // 传统菜单栏容器 + classicMenu!: HTMLElement; + classicScrollableContainer!: HTMLElement; + + constructor(defaultToolbarMenus: Record[]) { + super(); + this.defaultToolbarMenus = defaultToolbarMenus; + // 初始化菜单 + this.initMenus(); + } + + /** + * 定义创建方法 + * @param event + * @param options + */ + onCreate(event: EditorEvents["create"], options: UAIEditorOptions) { + this.eventComponents.forEach(component => { + component.onCreate(event, options); + }) + // 创建菜单容器 + this.classicScrollableContainer = document.createElement("div"); + this.classicScrollableContainer.classList.add("uai-classic-scrollable-container"); + this.appendChild(this.classicScrollableContainer); + + this.classicMenu = document.createElement("div"); + this.classicMenu.classList.add("uai-classic-menu"); + this.classicScrollableContainer.appendChild(this.classicMenu); + + const selectDiv = document.createElement("div"); + selectDiv.classList.add("uai-classic-virtual-group"); + selectDiv.classList.add("uai-editor-menu-select-3"); + selectDiv.innerHTML = ``; + this.classicMenu.appendChild(selectDiv); + + const selectMuenus = document.createElement("select"); + selectDiv.appendChild(selectMuenus); + + this.defaultToolbarMenus.forEach(menu => { + const option = document.createElement("option"); + option.label = menu.label; + option.value = menu.value; + selectMuenus.options.add(option); + }); + } + + /** + * 定义Transaction监听方法 + * @param event + * @param options + */ + onTransaction(event: EditorEvents["transaction"], options: UAIEditorOptions) { + this.eventComponents.forEach(component => { + component.onTransaction(event, options); + }) + } + + onEditableChange(editable: boolean) { + this.eventComponents.forEach(component => { + component.onEditableChange(editable); + }) + } + + /** + * 初始化菜单 + */ + initMenus() { + } +} \ No newline at end of file diff --git a/src/components/menus/toolbar/Ribbon.ts b/src/components/menus/toolbar/Ribbon.ts new file mode 100644 index 0000000..f5f82b7 --- /dev/null +++ b/src/components/menus/toolbar/Ribbon.ts @@ -0,0 +1,108 @@ +// Copyright (c) 2024-present AI-Labs + +// @ ts-nocheck +import { EditorEvents } from "@tiptap/core"; +import { UAIEditorEventListener, UAIEditorOptions } from "../../../core/UAIEditor.ts"; + +import { t } from "i18next"; + +/** + * 经典菜单栏 + */ +export class Ribbon extends HTMLElement implements UAIEditorEventListener { + defaultToolbarMenus!: Record[]; + eventComponents: UAIEditorEventListener[] = []; + headingOptions: Record[] = []; + + // 经典菜单栏容器 + ribbonMenu!: HTMLElement; + ribbonScrollableContainer!: HTMLElement; + + constructor(defaultToolbarMenus: Record[]) { + super(); + this.defaultToolbarMenus = defaultToolbarMenus; + // 初始化菜单 + this.initMenus(); + } + + /** + * 定义创建方法 + * @param event + * @param options + */ + onCreate(event: EditorEvents["create"], options: UAIEditorOptions) { + this.eventComponents.forEach(component => { + component.onCreate(event, options); + }) + // 创建菜单容器 + this.ribbonMenu = document.createElement("div"); + this.ribbonMenu.classList.add("uai-ribbon-menu"); + this.appendChild(this.ribbonMenu); + + const ribbonTabs = document.createElement("div"); + ribbonTabs.classList.add("uai-ribbon-tabs"); + this.ribbonMenu.appendChild(ribbonTabs); + + // 添加菜单分组 + this.defaultToolbarMenus.forEach(menu => { + const tab = document.createElement("div"); + tab.classList.add("uai-ribbon-tabs-item"); + tab.innerHTML = menu.label; + // 添加事件,处理菜单分组切换、界面元素切换 + tab.addEventListener("click", () => { + for (var i = 0; i < ribbonTabs.children.length; i++) { + ribbonTabs.children[i].classList.remove("active"); + } + tab.classList.add("active"); + }) + ribbonTabs.appendChild(tab); + }); + ribbonTabs.children[0].classList.add("active") + + this.ribbonScrollableContainer = document.createElement("div"); + this.ribbonScrollableContainer.classList.add("uai-ribbon-scrollable-container"); + this.ribbonMenu.appendChild(this.ribbonScrollableContainer); + + this.headingOptions.push({ label: t('base.heading.paragraph'), desc: 'text', value: 'paragraph', element: document.createElement("div") }); + + for (const i of Array.from({ length: 6 }).keys()) { + const level = i + 1 + this.headingOptions.push({ label: `${t('base.heading.text')}`.replace("{level}", `${level}`), desc: `h${level}`, value: level, element: document.createElement("div") }) + } + } + + /** + * 定义Transaction监听方法 + * @param event + * @param options + */ + onTransaction(event: EditorEvents["transaction"], options: UAIEditorOptions) { + this.eventComponents.forEach(component => { + component.onTransaction(event, options); + }) + + this.headingOptions.forEach(headingOption => { + // 标题样式设置 + (headingOption.element as HTMLElement).classList.remove("active"); + if (headingOption.value === "paragraph" && event.editor.isActive('paragraph')) { + // 如果当前选中非标题,则正文选项被激活 + (headingOption.element as HTMLElement).classList.add("active"); + } else if (event.editor.isActive('heading', { level: headingOption.value })) { + // 如果当前选中是标题,则激活对应的标题选项 + (headingOption.element as HTMLElement).classList.add("active"); + } + }) + } + + onEditableChange(editable: boolean) { + this.eventComponents.forEach(component => { + component.onEditableChange(editable); + }) + } + + /** + * 初始化菜单 + */ + initMenus() { + } +} \ No newline at end of file diff --git a/src/utils/DefineCustomElement.ts b/src/utils/DefineCustomElement.ts new file mode 100644 index 0000000..8fcf74a --- /dev/null +++ b/src/utils/DefineCustomElement.ts @@ -0,0 +1,12 @@ +// Copyright (c) 2024-present AI-Labs + +/** + * 注册Web组件 + * @param name + * @param element + */ +export const defineCustomElement = (name: string, element: CustomElementConstructor) => { + if (!window.customElements.get(name)) { + window.customElements.define(name, element); + } +} \ No newline at end of file