添加基础菜单

This commit is contained in:
wux_labs
2025-02-18 09:14:21 +08:00
parent e0cf01c819
commit 7c78fb9e1a
11 changed files with 1009 additions and 0 deletions
+15
View File
@@ -8,6 +8,13 @@ import { Footer } from "../components/Footer.ts";
import { Ribbon } from "./menus/toolbar/Ribbon.ts";
import { Classic } from "./menus/toolbar/Classic.ts";
import { ScrollableDiv } from "./menus/toolbar/ScrollableDiv.ts";
import { MenuButton } from "./menus/MenuButton.ts";
import { Undo } from "./menus/toolbar/base/Undo.ts";
import { Redo } from "./menus/toolbar/base/Redo.ts";
import { FormatPainter } from "./menus/toolbar/base/FormatPainter.ts";
import { ClearFormat } from "./menus/toolbar/base/ClearFormat.ts";
// 注册组件
defineCustomElement('uai-editor-header', Header);
@@ -16,3 +23,11 @@ defineCustomElement('uai-editor-footer', Footer);
defineCustomElement('uai-editor-ribbon-menu', Ribbon);
defineCustomElement('uai-editor-classic-menu', Classic);
defineCustomElement('uai-editor-scrollable-div', ScrollableDiv);
defineCustomElement('uai-editor-menu-button', MenuButton);
defineCustomElement('uai-editor-base-menu-undo', Undo);
defineCustomElement('uai-editor-base-menu-redo', Redo);
defineCustomElement('uai-editor-base-menu-format-painter', FormatPainter);
defineCustomElement('uai-editor-base-menu-clear-format', ClearFormat);
+132
View File
@@ -0,0 +1,132 @@
// Copyright (c) 2024-present AI-Labs
// @ ts-nocheck
import { EditorEvents } from "@tiptap/core";
import { UAIEditorEventListener, UAIEditorOptions } from "../../core/UAIEditor.ts";
import tippy, { Instance, Props } from "tippy.js";
/**
* 菜单按钮选项
*/
export type MenuButtonOptions = {
menuType: "button",
enable: boolean,
className?: string,
header?: "ribbon" | "classic",
huge?: boolean,
icon?: string,
text?: string,
hideText?: boolean,
tooltip?: string,
shortcut?: string,
options?: string[] | number[]
}
/**
* 功能菜单
*/
export class MenuButton extends HTMLElement implements UAIEditorEventListener {
container!: HTMLElement;
menuButton!: HTMLElement;
menuButtonContent!: HTMLElement;
menuButtonArrow?: HTMLElement;
menuButtonOptions!: MenuButtonOptions;
tippyInstance!: Instance<Props>;
constructor(options: MenuButtonOptions) {
super();
// 初始化功能菜单选项
this.menuButtonOptions = options;
}
/**
* 定义创建方法
* @param event
* @param options
*/
onCreate(_event: EditorEvents["create"], _options: UAIEditorOptions) {
this.container = document.createElement("div");
this.container.classList.add("uai-menu-button-wrap");
this.appendChild(this.container);
// 根据不同的菜单类型绘制不同的用户界面
if (this.menuButtonOptions.menuType === "button") {
// 按钮
this.createMenuButton()
}
// 提示信息
var tipsContent = this.menuButtonOptions.tooltip;
if (tipsContent) {
if (this.menuButtonOptions.shortcut) {
tipsContent += ` (${this.menuButtonOptions.shortcut})`;
}
this.tippyInstance = tippy(this.menuButton, {
appendTo: "parent",
content: tipsContent,
theme: 'uai-tips',
placement: "top",
// arrow: false,
// interactive: true,
});
}
}
onTransaction(_event: EditorEvents["transaction"], _options: UAIEditorOptions) {
}
onEditableChange(editable: boolean) {
this.menuButtonOptions.enable = editable;
if (editable) {
this.menuButton.style.pointerEvents = "auto";
this.menuButton.style.opacity = "1";
this.style.cursor = "pointer";
} else {
this.menuButton.style.pointerEvents = "none";
this.menuButton.style.opacity = "0.3";
this.style.cursor = "not-allowed";
}
}
/**
* 创建按钮
*/
createMenuButton() {
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 = `<img src="${this.menuButtonOptions.icon}" width="${size}" />`
}
// 按钮文字描述
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);
}
}
}
+59
View File
@@ -5,6 +5,12 @@ import { EditorEvents } from "@tiptap/core";
import { UAIEditorEventListener, UAIEditorOptions } from "../../../core/UAIEditor.ts";
import menuIcon from "../../../assets/icons/menu.svg";
import { ScrollableDiv } from "./ScrollableDiv";
import { Redo } from "./base/Redo";
import { Undo } from "./base/Undo";
import { FormatPainter } from "./base/FormatPainter";
import { ClearFormat } from "./base/ClearFormat";
/**
* 传统菜单栏
@@ -17,6 +23,16 @@ export class Classic extends HTMLElement implements UAIEditorEventListener {
classicMenu!: HTMLElement;
classicScrollableContainer!: HTMLElement;
// 基础菜单容器
classicMenuBaseScrollable!: ScrollableDiv;
classicMenuBaseGroup!: HTMLElement;
// 基础菜单
baseMenuUndo!: Undo;
baseMenuRedo!: Redo;
baseMenuFormatPainter!: FormatPainter;
baseMenuClearFormat!: ClearFormat;
constructor(defaultToolbarMenus: Record<string, any>[]) {
super();
this.defaultToolbarMenus = defaultToolbarMenus;
@@ -57,6 +73,17 @@ export class Classic extends HTMLElement implements UAIEditorEventListener {
option.value = menu.value;
selectMuenus.options.add(option);
});
// 添加事件,处理菜单分组切换、界面元素切换
selectMuenus.addEventListener("change", () => {
const menu = selectMuenus.selectedOptions[0].value;
this.classicMenuBaseScrollable.style.display = "none";
if (menu === "base") {
this.classicMenuBaseScrollable.style.display = "flex";
}
})
// 创建分组菜单
this.createBaseMenu(event, options);
this.classicMenuBaseScrollable.style.display = "flex";
}
/**
@@ -80,5 +107,37 @@ export class Classic extends HTMLElement implements UAIEditorEventListener {
* 初始化菜单
*/
initMenus() {
this.baseMenuUndo = new Undo({ menuType: "button", enable: true, hideText: false });
this.eventComponents.push(this.baseMenuUndo);
this.baseMenuRedo = new Redo({ menuType: "button", enable: true });
this.eventComponents.push(this.baseMenuRedo);
this.baseMenuFormatPainter = new FormatPainter({ menuType: "button", enable: true });
this.eventComponents.push(this.baseMenuFormatPainter);
this.baseMenuClearFormat = new ClearFormat({ menuType: "button", enable: true });
this.eventComponents.push(this.baseMenuClearFormat);
}
/**
* 创建基础菜单
* @param event
* @param options
*/
createBaseMenu(event: EditorEvents["create"], options: UAIEditorOptions) {
this.classicMenuBaseGroup = document.createElement("div");
this.classicMenuBaseGroup.style.display = "flex";
this.classicMenuBaseScrollable = new ScrollableDiv(this.classicMenuBaseGroup);
this.classicMenuBaseScrollable.style.display = "none";
this.classicMenu.appendChild(this.classicMenuBaseScrollable);
this.classicMenuBaseScrollable.onCreate(event, options);
const group1 = document.createElement("div");
group1.classList.add("uai-classic-virtual-group");
this.classicMenuBaseGroup.appendChild(group1);
group1.appendChild(this.baseMenuUndo);
group1.appendChild(this.baseMenuRedo);
group1.appendChild(this.baseMenuFormatPainter);
group1.appendChild(this.baseMenuClearFormat);
}
}
+67
View File
@@ -5,6 +5,13 @@ import { EditorEvents } from "@tiptap/core";
import { UAIEditorEventListener, UAIEditorOptions } from "../../../core/UAIEditor.ts";
import { t } from "i18next";
import { ScrollableDiv } from "./ScrollableDiv";
import { Redo } from "./base/Redo";
import { Undo } from "./base/Undo";
import { FormatPainter } from "./base/FormatPainter";
import { ClearFormat } from "./base/ClearFormat";
/**
* 经典菜单栏
@@ -18,6 +25,17 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener {
ribbonMenu!: HTMLElement;
ribbonScrollableContainer!: HTMLElement;
// 基础菜单容器
ribbonMenuBaseScrollable!: ScrollableDiv;
ribbonMenuBaseGroup!: HTMLElement;
// 基础菜单
baseMenuUndo!: Undo;
baseMenuRedo!: Redo;
baseMenuFormatPainter!: FormatPainter;
baseMenuClearFormat!: ClearFormat;
constructor(defaultToolbarMenus: Record<string, any>[]) {
super();
this.defaultToolbarMenus = defaultToolbarMenus;
@@ -54,6 +72,10 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener {
ribbonTabs.children[i].classList.remove("active");
}
tab.classList.add("active");
this.ribbonMenuBaseScrollable.style.display = "none";
if (menu.value === "base") {
this.ribbonMenuBaseScrollable.style.display = "flex";
}
})
ribbonTabs.appendChild(tab);
});
@@ -69,6 +91,10 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener {
const level = i + 1
this.headingOptions.push({ label: `${t('base.heading.text')}`.replace("{level}", `${level}`), desc: `h${level}`, value: level, element: document.createElement("div") })
}
// 创建分组菜单
this.createBaseMenu(event, options);
this.ribbonMenuBaseScrollable.style.display = "flex";
}
/**
@@ -104,5 +130,46 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener {
* 初始化菜单
*/
initMenus() {
this.baseMenuUndo = new Undo({ menuType: "button", enable: true });
this.eventComponents.push(this.baseMenuUndo);
this.baseMenuRedo = new Redo({ menuType: "button", enable: true });
this.eventComponents.push(this.baseMenuRedo);
this.baseMenuFormatPainter = new FormatPainter({ menuType: "button", enable: true });
this.eventComponents.push(this.baseMenuFormatPainter);
this.baseMenuClearFormat = new ClearFormat({ menuType: "button", enable: true });
this.eventComponents.push(this.baseMenuClearFormat);
}
/**
* 创建基础菜单
* @param event
* @param options
*/
createBaseMenu(event: EditorEvents["create"], _options: UAIEditorOptions) {
this.ribbonMenuBaseGroup = document.createElement("div");
this.ribbonMenuBaseGroup.classList.add("uai-ribbon-container");
this.ribbonMenuBaseGroup.style.display = "flex";
this.ribbonMenuBaseScrollable = new ScrollableDiv(this.ribbonMenuBaseGroup);
this.ribbonMenuBaseScrollable.style.display = "none";
this.ribbonScrollableContainer.appendChild(this.ribbonMenuBaseScrollable);
const group1 = document.createElement("div");
group1.classList.add("uai-ribbon-virtual-group");
this.ribbonMenuBaseGroup.appendChild(group1);
const group1row1 = document.createElement("div");
group1row1.classList.add("uai-ribbon-virtual-group-row");
group1.appendChild(group1row1);
group1row1.appendChild(this.baseMenuUndo);
group1row1.appendChild(this.baseMenuRedo);
const group1row2 = document.createElement("div");
group1row2.classList.add("uai-ribbon-virtual-group-row");
group1.appendChild(group1row2);
group1row2.appendChild(this.baseMenuFormatPainter);
group1row2.appendChild(this.baseMenuClearFormat);
}
}
@@ -0,0 +1,89 @@
// Copyright (c) 2024-present AI-Labs
// @ ts-nocheck
import { EditorEvents } from "@tiptap/core";
import { UAIEditorEventListener, UAIEditorOptions } from "../../../core/UAIEditor.ts";
/**
* 可滚动容器
*/
export class ScrollableDiv extends HTMLElement implements UAIEditorEventListener {
private container: HTMLElement;
scrollLeftBtn: HTMLElement;
content: HTMLElement;
scrollRightBtn: HTMLElement;
private scrollAmount: number = 50; // 每次滚动的像素数
private transform: number = 0; // 当前transform值
constructor(content: HTMLElement) {
super();
this.content = content;
this.scrollLeftBtn = document.createElement("div");
this.scrollLeftBtn.classList.add("uai-scrollable-control-button");
this.scrollLeftBtn.innerHTML = "<strong><</strong>";
this.scrollLeftBtn.style.display = "none";
this.container = document.createElement("div");
this.container.appendChild(this.content);
// this.container.style.overflow = "hidden";
// this.container.style.position = "relative";
this.scrollRightBtn = document.createElement("div");
this.scrollRightBtn.classList.add("uai-scrollable-control-button");
this.scrollRightBtn.innerHTML = "<strong>></strong>";
this.scrollRightBtn.style.display = "none";
this.scrollLeftBtn.addEventListener('click', () => {
if (this.transform < 0) {
this.transform += Math.min(this.scrollAmount, 0 - this.transform);
}
this.content.style.transform = `translateX(${this.transform}px)`;
this.scrollButtonControl();
});
this.scrollRightBtn.addEventListener('click', () => {
const scrollWidth = this.content.scrollWidth + this.transform - this.scrollRightBtn.clientWidth - this.container.clientWidth;
if (scrollWidth > 0) {
this.transform -= Math.min(this.scrollAmount, scrollWidth);
}
this.content.style.transform = `translateX(${this.transform}px)`;
this.scrollButtonControl();
});
this.classList.add("uai-classic-scrollable-menu")
this.appendChild(this.scrollLeftBtn)
this.appendChild(this.container)
this.appendChild(this.scrollRightBtn)
}
onCreate(event: EditorEvents["create"], options: UAIEditorOptions) {
const resizeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
if (entry.target === this.parentElement) {
this.scrollButtonControl();
}
}
});
resizeObserver.observe(this.parentElement!);
}
onTransaction(event: EditorEvents["transaction"], options: UAIEditorOptions) {
}
onEditableChange(editable: boolean) {
}
scrollButtonControl() {
if (this.transform < 0) {
this.scrollLeftBtn.style.display = "flex";
} else {
this.scrollLeftBtn.style.display = "none";
}
if (this.parentElement!.scrollWidth + this.transform - this.scrollRightBtn.clientWidth < this.container.clientWidth) {
this.scrollRightBtn.style.display = "flex";
} else {
this.scrollRightBtn.style.display = "none";
}
// 触发宽度变化事件
}
}
@@ -0,0 +1,68 @@
// Copyright (c) 2024-present AI-Labs
// @ ts-nocheck
import { MenuButton, MenuButtonOptions } from "../../MenuButton.ts";
import icon from "../../../../assets/icons/clear-format.svg";
import { t } from "i18next";
import { UAIEditorEventListener, UAIEditorOptions } from "../../../../core/UAIEditor.ts";
import { EditorEvents } from "@tiptap/core";
/**
* 基础菜单:清除格式
*/
export class ClearFormat extends HTMLElement implements UAIEditorEventListener {
// 按钮选项
menuButtonOptions: MenuButtonOptions = {
menuType: "button",
enable: true,
icon: icon,
hideText: true,
text: t('base.clearFormat'),
tooltip: t('base.clearFormat'),
}
// 功能按钮
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().unsetAllMarks().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);
}
}
@@ -0,0 +1,72 @@
// Copyright (c) 2024-present AI-Labs
// @ ts-nocheck
import { MenuButton, MenuButtonOptions } from "../../MenuButton.ts";
import icon from "../../../../assets/icons/format-painter.svg";
import { t } from "i18next";
import { UAIEditorEventListener, UAIEditorOptions } from "../../../../core/UAIEditor.ts";
import { EditorEvents } from "@tiptap/core";
/**
* 基础菜单:格式刷
*/
export class FormatPainter extends HTMLElement implements UAIEditorEventListener {
// 按钮选项
menuButtonOptions: MenuButtonOptions = {
menuType: "button",
enable: true,
icon: icon,
hideText: true,
text: t('base.formatPainter.text'),
tooltip: t('base.formatPainter.text'),
}
// 功能按钮
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) {
// TODO
}
})
}
/**
* 定义Transaction监听方法
* @param event
* @param options
*/
onTransaction(event: EditorEvents["transaction"], options: UAIEditorOptions) {
this.menuButton.onTransaction(event, options);
if (this.menuButton.menuButton) {
var disable = !event.editor.state.selection.empty;
this.onEditableChange(disable);
}
}
onEditableChange(editable: boolean) {
this.menuButtonOptions.enable = editable;
this.menuButton.onEditableChange(editable);
}
}
+74
View File
@@ -0,0 +1,74 @@
// Copyright (c) 2024-present AI-Labs
// @ ts-nocheck
import { MenuButton, MenuButtonOptions } from "../../MenuButton.ts";
import icon from "../../../../assets/icons/redo.svg";
import { t } from "i18next";
import { UAIEditorEventListener, UAIEditorOptions } from "../../../../core/UAIEditor.ts";
import { EditorEvents } from "@tiptap/core";
/**
* 基础菜单:重做
*/
export class Redo extends HTMLElement implements UAIEditorEventListener {
// 按钮选项
menuButtonOptions: MenuButtonOptions = {
menuType: "button",
enable: true,
icon: icon,
hideText: true,
text: t('base.redo'),
tooltip: t('base.redo'),
shortcut: "Ctrl+Y / Ctrl+Shift+Z",
}
// 功能按钮
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().redo().run();
}
})
}
/**
* 定义Transaction监听方法
* @param event
* @param options
*/
onTransaction(event: EditorEvents["transaction"], options: UAIEditorOptions){
this.menuButton.onTransaction(event, options);
if (this.menuButton.menuButton) {
// 根据当前是否有可重做操作决定按钮是否可用
var disable = event.editor.can().chain().redo().run();
this.onEditableChange(disable);
}
}
onEditableChange(editable: boolean){
this.menuButtonOptions.enable = editable;
this.menuButton.onEditableChange(editable);
}
}
+74
View File
@@ -0,0 +1,74 @@
// Copyright (c) 2024-present AI-Labs
// @ ts-nocheck
import { MenuButton, MenuButtonOptions } from "../../MenuButton.ts";
import icon from "../../../../assets/icons/undo.svg";
import { t } from "i18next";
import { UAIEditorEventListener, UAIEditorOptions } from "../../../../core/UAIEditor.ts";
import { EditorEvents } from "@tiptap/core";
/**
* 基础菜单:撤销
*/
export class Undo extends HTMLElement implements UAIEditorEventListener {
// 按钮选项
menuButtonOptions: MenuButtonOptions = {
menuType: "button",
enable: true,
icon: icon,
hideText: true,
text: t('base.undo'),
tooltip: t('base.undo'),
shortcut: "Ctrl+Z",
}
// 功能按钮
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().undo().run();
}
})
}
/**
* 定义Transaction监听方法
* @param event
* @param options
*/
onTransaction(event: EditorEvents["transaction"], options: UAIEditorOptions){
this.menuButton.onTransaction(event, options);
if (this.menuButton.menuButton) {
// 根据当前是否有可撤销操作决定按钮是否可用
var disable = event.editor.can().chain().undo().run();
this.onEditableChange(disable);
}
}
onEditableChange(editable: boolean){
this.menuButtonOptions.enable = editable;
this.menuButton.onEditableChange(editable);
}
}