diff --git a/src/components/index.ts b/src/components/index.ts index 28c63a6..b05c9c6 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -54,6 +54,7 @@ import { Print } from "./menus/toolbar/base/Print.ts"; import { Link } from "./menus/toolbar/insert/Link.ts"; import { Image } from "./menus/toolbar/insert/Image.ts"; import { Video } from "./menus/toolbar/insert/Video.ts"; +import { Audio } from "./menus/toolbar/insert/Audio.ts"; // 注册组件 defineCustomElement('uai-editor-header', Header); @@ -109,3 +110,4 @@ defineCustomElement('uai-editor-base-menu-print', Print); defineCustomElement('uai-editor-insert-menu-link', Link); defineCustomElement('uai-editor-insert-menu-image', Image); defineCustomElement('uai-editor-insert-menu-video', Video); +defineCustomElement('uai-editor-insert-menu-audio', Audio); diff --git a/src/components/menus/toolbar/Classic.ts b/src/components/menus/toolbar/Classic.ts index 093d751..472ffd9 100644 --- a/src/components/menus/toolbar/Classic.ts +++ b/src/components/menus/toolbar/Classic.ts @@ -49,6 +49,7 @@ import { Print } from "./base/Print.ts"; import { Link } from "./insert/Link.ts"; import { Image } from "./insert/Image.ts"; import { Video } from "./insert/Video.ts"; +import { Audio } from "./insert/Audio.ts"; /** * 传统菜单栏 @@ -109,6 +110,7 @@ export class Classic extends HTMLElement implements UAIEditorEventListener { insertMenuLink!: Link; insertMenuImage!: Image; insertMenuVideo!: Video; + insertMenuAudio!: Audio; constructor(defaultToolbarMenus: Record[]) { super(); @@ -287,6 +289,9 @@ export class Classic extends HTMLElement implements UAIEditorEventListener { this.insertMenuVideo = new Video({ menuType: "button", enable: true, header: "classic", hideText: false }); this.eventComponents.push(this.insertMenuVideo); + + this.insertMenuAudio = new Audio({ menuType: "button", enable: true, header: "classic", hideText: false }); + this.eventComponents.push(this.insertMenuAudio); } /** * 创建基础菜单 @@ -367,5 +372,6 @@ export class Classic extends HTMLElement implements UAIEditorEventListener { group1.appendChild(this.insertMenuLink); group1.appendChild(this.insertMenuImage); group1.appendChild(this.insertMenuVideo); + group1.appendChild(this.insertMenuAudio); } } \ No newline at end of file diff --git a/src/components/menus/toolbar/Ribbon.ts b/src/components/menus/toolbar/Ribbon.ts index fdf43d7..39780e4 100644 --- a/src/components/menus/toolbar/Ribbon.ts +++ b/src/components/menus/toolbar/Ribbon.ts @@ -51,6 +51,7 @@ import { Print } from "./base/Print.ts"; import { Link } from "./insert/Link.ts"; import { Image } from "./insert/Image.ts"; import { Video } from "./insert/Video.ts"; +import { Audio } from "./insert/Audio.ts"; /** * 经典菜单栏 @@ -112,6 +113,7 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener { insertMenuLink!: Link; insertMenuImage!: Image; insertMenuVideo!: Video; + insertMenuAudio!: Audio; constructor(defaultToolbarMenus: Record[]) { super(); @@ -310,6 +312,9 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener { this.insertMenuVideo = new Video({ menuType: "button", enable: true, huge: true }); this.eventComponents.push(this.insertMenuVideo); + + this.insertMenuAudio = new Audio({ menuType: "button", enable: true, huge: true }); + this.eventComponents.push(this.insertMenuAudio); } /** @@ -464,5 +469,6 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener { group1.appendChild(this.insertMenuLink); group1.appendChild(this.insertMenuImage); group1.appendChild(this.insertMenuVideo); + group1.appendChild(this.insertMenuAudio); } } \ No newline at end of file diff --git a/src/components/menus/toolbar/insert/Audio.ts b/src/components/menus/toolbar/insert/Audio.ts new file mode 100644 index 0000000..a060fd3 --- /dev/null +++ b/src/components/menus/toolbar/insert/Audio.ts @@ -0,0 +1,72 @@ +// Copyright (c) 2024-present AI-Labs + +// @ ts-nocheck +import { MenuButton, MenuButtonOptions } from "../../MenuButton.ts"; +import icon from "../../../../assets/icons/audio.svg"; +import { t } from "i18next"; +import { UAIEditorEventListener, UAIEditorOptions } from "../../../../core/UAIEditor.ts"; +import { EditorEvents } from "@tiptap/core"; + +/** + * 插入菜单:插入音频 + */ +export class Audio extends HTMLElement implements UAIEditorEventListener { + // 按钮选项 + menuButtonOptions: MenuButtonOptions = { + menuType: "button", + enable: true, + icon: icon, + hideText: false, + text: t('insert.audio'), + tooltip: t('insert.audio'), + } + + // 功能按钮 + 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().selectFiles('audio', true).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.isEditable; + this.onEditableChange(disable); + } + } + + onEditableChange(editable: boolean) { + this.menuButtonOptions.enable = editable; + this.menuButton.onEditableChange(editable); + } +} + diff --git a/src/core/UAIEditor.ts b/src/core/UAIEditor.ts index a0f326f..14c7808 100644 --- a/src/core/UAIEditor.ts +++ b/src/core/UAIEditor.ts @@ -94,6 +94,12 @@ export type UAIEditorOptions = { uploadFormName?: string, uploader?: Uploader, }, + audio?: { + uploadUrl?: string, + uploadHeaders?: (() => Record) | Record, + uploadFormName?: string, + uploader?: Uploader, + }, } /** diff --git a/src/core/UAIExtensions.ts b/src/core/UAIExtensions.ts index ed1042c..3db110b 100644 --- a/src/core/UAIExtensions.ts +++ b/src/core/UAIExtensions.ts @@ -26,6 +26,7 @@ import NodeAlign from "../extensions/NodeAlign.ts"; import OrderedList from "../extensions/OrderedList.ts"; import SelectFile from "../extensions/SelectFile.ts"; import Video from "../extensions/Video.ts"; +import Audio from "../extensions/Audio.ts"; /** * 定义编辑器的所有自定义扩展组件 @@ -39,6 +40,7 @@ export const allExtensions = (uaiEditor: UAIEditor, _options: UAIEditorOptions): bulletList: false, orderedList: false, }), + Audio, BulletList, Color, FontFamily, diff --git a/src/extensions/Audio.ts b/src/extensions/Audio.ts new file mode 100644 index 0000000..27f38b4 --- /dev/null +++ b/src/extensions/Audio.ts @@ -0,0 +1,85 @@ +// Copyright (c) 2024-present AI-Labs + +import { mergeAttributes, Node } from '@tiptap/core' +import { resize } from '../utils/resize.ts' + +declare module '@tiptap/core' { + interface Commands { + setAudio: { + setAudio: (options: any) => ReturnType + } + } +} +export default Node.create({ + name: 'audio', + group: 'block', + atom: true, + addAttributes() { + return { + vnode: { + default: true, + }, + id: { + default: null, + }, + file: { + default: null, + }, + name: { + default: null, + }, + size: { + default: null, + }, + src: { + default: null, + }, + controls: { + default: true, + }, + uploaded: { + default: false, + }, + previewType: { + default: 'audio', + }, + } + }, + parseHTML() { + return [{ tag: 'audio' }] + }, + renderHTML({ HTMLAttributes }) { + return ['audio', mergeAttributes(HTMLAttributes)] + }, + addCommands() { + return { + setAudio: + (options) => + ({ commands, editor }) => { + return commands.insertContentAt(editor.state.selection.anchor, { + type: this.name, + attrs: options, + }) + }, + } + }, + addNodeView() { + return (props) => { + const container = document.createElement('div'); + const { nodeAlign, src } = props.node.attrs; + container.classList.add(`uai-node-view`); + container.style.justifyContent = nodeAlign; + container.innerHTML = ` +
+
+
+ +
+ ` + resize(container, this.editor.view.dom, (attrs) => this.editor.commands.updateAttributes("audio", attrs)); + return { + dom: container + } + } + }, +}) diff --git a/src/extensions/SelectFile.ts b/src/extensions/SelectFile.ts index 2b6f2a9..71e8971 100644 --- a/src/extensions/SelectFile.ts +++ b/src/extensions/SelectFile.ts @@ -55,6 +55,7 @@ const mimeTypes: any = { 'image/apng', ], video: ['video/mp4', 'video/webm', 'video/ogg'], + audio: ['audio/mp3', 'audio/wav', 'audio/ogg', 'audio/aac', 'audio/flac'], } /** @@ -193,6 +194,26 @@ export default Node.create({ }); }); } + // 音频 + if (type.startsWith('audio/') && mimeTypes.audio.includes(type)) { + previewType = 'audio'; + uploader = editorOptions.audio?.uploader ?? Base64Uploader; + uploader(file, editorOptions.audio?.uploadUrl, editorOptions.audio?.uploadHeaders, editorOptions.audio?.uploadFormName || "audio") + .then(json => { + view.dispatch(tr.setMeta(actionKey, { type: "remove", id })); + this.editor.commands.insertContentAt(tr.selection.from, { + type: autoType ? (previewType ?? 'file') : 'file', + attrs: { + [previewType === 'file' ? 'url' : 'src']: json.data.src, + name, + type, + size, + file, + previewType, + }, + }); + }); + } return true; }, selectFiles: