diff --git a/package-lock.json b/package-lock.json index 322596c..9c07550 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "markdown-it-container": "^4.0.0", "monaco-editor": "^0.52.2", "openai": "^4.77.0", + "pretty-bytes": "^6.1.1", "tippy.js": "^6.3.7", "turndown": "^7.2.0", "vite-plugin-svg-icons": "^2.0.1" @@ -5153,6 +5154,17 @@ "posthtml-render": "^1.0.6" } }, + "node_modules/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prosemirror-changeset": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.2.1.tgz", diff --git a/package.json b/package.json index 8025043..50e503e 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "markdown-it-container": "^4.0.0", "monaco-editor": "^0.52.2", "openai": "^4.77.0", + "pretty-bytes": "^6.1.1", "tippy.js": "^6.3.7", "turndown": "^7.2.0", "vite-plugin-svg-icons": "^2.0.1" diff --git a/src/components/index.ts b/src/components/index.ts index a858b51..ca177df 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -63,6 +63,7 @@ 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"; +import { File } from "./menus/toolbar/insert/File.ts"; import { HardBreak } from "./menus/toolbar/insert/HardBreak.ts"; import { Emoji } from "./menus/toolbar/insert/Emoji.ts"; import { Symbol } from "./menus/toolbar/insert/Symbol.ts"; @@ -103,6 +104,8 @@ import { VideoBubbleMenu } from "./menus/bubble/VideoBubbleMenu.ts"; import { AudioBubbleMenu } from "./menus/bubble/AudioBubbleMenu.ts"; +import { FileBubbleMenu } from "./menus/bubble/FileBubbleMenu.ts"; + import { CharacterCount } from "./menus/statusbar/CharacterCount.ts"; import { Fullscreen } from "./menus/statusbar/Fullscreen.ts"; @@ -172,6 +175,7 @@ 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); +defineCustomElement('uai-editor-insert-menu-file', File); defineCustomElement('uai-editor-insert-menu-hard-break', HardBreak); defineCustomElement('uai-editor-insert-menu-emoji', Emoji); defineCustomElement('uai-editor-insert-menu-symbol', Symbol); @@ -218,6 +222,8 @@ defineCustomElement('uai-editor-bubble-menu-video', VideoBubbleMenu); defineCustomElement('uai-editor-bubble-menu-audio', AudioBubbleMenu); +defineCustomElement('uai-editor-bubble-menu-file', FileBubbleMenu); + defineCustomElement('uai-editor-ai-action-request-action', AIRequestAction); defineCustomElement('uai-editor-ai-action-chat-response-action', AIChatResponseAction); defineCustomElement('uai-editor-ai-action-image-response-action', AIImageResponseAction); diff --git a/src/components/menus/bubble/FileBubbleMenu.ts b/src/components/menus/bubble/FileBubbleMenu.ts new file mode 100644 index 0000000..4fbcde9 --- /dev/null +++ b/src/components/menus/bubble/FileBubbleMenu.ts @@ -0,0 +1,89 @@ +// Copyright (c) 2024-present AI-Labs + +// @ ts-nocheck +import { EditorEvents } from "@tiptap/core"; + +import { UAIEditorEventListener, UAIEditor, UAIEditorOptions } from "../../../core/UAIEditor.ts"; + +import { AlignLeft } from "../common/AlignLeft.ts"; +import { AlignCenter } from "../common/AlignCenter.ts"; +import { AlignRight } from "../common/AlignRight.ts"; +import { NodeDelete } from "../common/NodeDelete.ts"; + +/** + * 定义文件内容浮动菜单 + */ +export class FileBubbleMenu extends HTMLElement implements UAIEditorEventListener { + uaiEditor!: UAIEditor; + + // 左对齐 + baseMenuAlignLeft!: AlignLeft; + // 居中对齐 + baseMenuAlignCenter!: AlignCenter; + // 右对齐 + baseMenuAlignRight!: AlignRight; + // 删除 + commonMenuNodeDelete!: NodeDelete; + + constructor(uaiEditor: UAIEditor) { + super(); + this.uaiEditor = uaiEditor; + this.initMenus(); + + // 定义菜单容器 + const container = document.createElement("div"); + container.classList.add("uai-bubble-menu-container"); + + // 定义菜单组 + const group1 = document.createElement("div"); + group1.classList.add("uai-bubble-menu-virtual-group"); + container.appendChild(group1); + // 添加对齐功能 + group1.appendChild(this.baseMenuAlignLeft); + group1.appendChild(this.baseMenuAlignCenter); + group1.appendChild(this.baseMenuAlignRight); + + const group2 = document.createElement("div"); + group2.classList.add("uai-bubble-menu-virtual-group"); + container.appendChild(group2); + + // 定义菜单组 + const group3 = document.createElement("div"); + group3.classList.add("uai-bubble-menu-virtual-group"); + container.appendChild(group3); + // 添加删除功能 + group3.appendChild(this.commonMenuNodeDelete); + + this.appendChild(container); + } + + onCreate(event: EditorEvents["create"], options: UAIEditorOptions) { + + } + + onTransaction(event: EditorEvents["transaction"], options: UAIEditorOptions) { + + } + + onEditableChange(editable: boolean) { + + } + + /** + * 初始化功能菜单 + */ + initMenus() { + this.baseMenuAlignLeft = new AlignLeft({ menuType: "button", enable: true }); + this.uaiEditor.eventComponents.push(this.baseMenuAlignLeft); + + this.baseMenuAlignCenter = new AlignCenter({ menuType: "button", enable: true }); + this.uaiEditor.eventComponents.push(this.baseMenuAlignCenter); + + this.baseMenuAlignRight = new AlignRight({ menuType: "button", enable: true }); + this.uaiEditor.eventComponents.push(this.baseMenuAlignRight); + + this.commonMenuNodeDelete = new NodeDelete({ menuType: "button", enable: true }); + this.uaiEditor.eventComponents.push(this.commonMenuNodeDelete); + + } +} \ No newline at end of file diff --git a/src/components/menus/toolbar/Classic.ts b/src/components/menus/toolbar/Classic.ts index 114024e..a2483f1 100644 --- a/src/components/menus/toolbar/Classic.ts +++ b/src/components/menus/toolbar/Classic.ts @@ -50,6 +50,7 @@ import { Link } from "./insert/Link.ts"; import { Image } from "./insert/Image.ts"; import { Video } from "./insert/Video.ts"; import { Audio } from "./insert/Audio.ts"; +import { File } from "./insert/File.ts"; import { HardBreak } from "./insert/HardBreak.ts"; import { Emoji } from "./insert/Emoji.ts"; import { Symbol } from "./insert/Symbol.ts"; @@ -162,6 +163,7 @@ export class Classic extends HTMLElement implements UAIEditorEventListener { insertMenuImage!: Image; insertMenuVideo!: Video; insertMenuAudio!: Audio; + insertMenuFile!: File; insertMenuHardBreak!: HardBreak; insertMenuEmoji!: Emoji; insertMenuSymbol!: Symbol; @@ -403,6 +405,9 @@ export class Classic extends HTMLElement implements UAIEditorEventListener { this.insertMenuAudio = new Audio({ menuType: "button", enable: true, header: "classic", hideText: false }); this.eventComponents.push(this.insertMenuAudio); + this.insertMenuFile = new File({ menuType: "button", enable: true, header: "classic", hideText: false }); + this.eventComponents.push(this.insertMenuFile); + this.insertMenuHardBreak = new HardBreak({ menuType: "button", enable: true, header: "classic", hideText: false }); this.eventComponents.push(this.insertMenuHardBreak); @@ -555,6 +560,7 @@ export class Classic extends HTMLElement implements UAIEditorEventListener { group1.appendChild(this.insertMenuImage); group1.appendChild(this.insertMenuVideo); group1.appendChild(this.insertMenuAudio); + group1.appendChild(this.insertMenuFile); const group2 = document.createElement("div"); group2.classList.add("uai-classic-virtual-group"); diff --git a/src/components/menus/toolbar/Ribbon.ts b/src/components/menus/toolbar/Ribbon.ts index c3beb1e..523d81a 100644 --- a/src/components/menus/toolbar/Ribbon.ts +++ b/src/components/menus/toolbar/Ribbon.ts @@ -53,6 +53,7 @@ import { Link } from "./insert/Link.ts"; import { Image } from "./insert/Image.ts"; import { Video } from "./insert/Video.ts"; import { Audio } from "./insert/Audio.ts"; +import { File } from "./insert/File.ts"; import { HardBreak } from "./insert/HardBreak.ts"; import { Emoji } from "./insert/Emoji.ts"; import { Symbol } from "./insert/Symbol.ts"; @@ -164,6 +165,7 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener { insertMenuImage!: Image; insertMenuVideo!: Video; insertMenuAudio!: Audio; + insertMenuFile!: File; insertMenuHardBreak!: HardBreak; insertMenuEmoji!: Emoji; insertMenuSymbol!: Symbol; @@ -425,6 +427,9 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener { this.insertMenuAudio = new Audio({ menuType: "button", enable: true, huge: true }); this.eventComponents.push(this.insertMenuAudio); + this.insertMenuFile = new File({ menuType: "button", enable: true, huge: true }); + this.eventComponents.push(this.insertMenuFile); + this.insertMenuHardBreak = new HardBreak({ menuType: "button", enable: true, huge: true }); this.eventComponents.push(this.insertMenuHardBreak); @@ -651,6 +656,7 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener { group1.appendChild(this.insertMenuImage); group1.appendChild(this.insertMenuVideo); group1.appendChild(this.insertMenuAudio); + group1.appendChild(this.insertMenuFile); const group2 = document.createElement("div"); group2.classList.add("uai-ribbon-virtual-group"); diff --git a/src/components/menus/toolbar/insert/Audio.ts b/src/components/menus/toolbar/insert/Audio.ts index 903bebc..d24a133 100644 --- a/src/components/menus/toolbar/insert/Audio.ts +++ b/src/components/menus/toolbar/insert/Audio.ts @@ -49,7 +49,7 @@ export class Audio extends HTMLElement implements UAIEditorEventListener { // 定义按钮点击事件,插入音频 this.addEventListener("click", () => { if (this.menuButtonOptions.enable) { - event.editor.chain().focus().selectFiles('audio', true).run() + event.editor.chain().focus().selectFiles('audio', true).run(); } }) } diff --git a/src/components/menus/toolbar/insert/File.ts b/src/components/menus/toolbar/insert/File.ts new file mode 100644 index 0000000..58dba08 --- /dev/null +++ b/src/components/menus/toolbar/insert/File.ts @@ -0,0 +1,83 @@ +// Copyright (c) 2024-present AI-Labs + +// @ ts-nocheck +import { EditorEvents } from "@tiptap/core"; + +import { t } from "i18next"; + +import { UAIEditorEventListener, UAIEditorOptions } from "../../../../core/UAIEditor.ts"; +import { MenuButton, MenuButtonOptions } from "../../MenuButton.ts"; + +import icon from "../../../../assets/icons/file.svg"; + +/** + * 插入菜单:插入文件 + */ +export class File extends HTMLElement implements UAIEditorEventListener { + // 按钮选项 + menuButtonOptions: MenuButtonOptions = { + menuType: "button", + enable: true, + icon: icon, + hideText: false, + text: t('insert.file'), + tooltip: t('insert.file'), + } + + // 功能按钮 + menuButton: MenuButton; + // 文件选择 + fileInput: HTMLInputElement; + + constructor(options: MenuButtonOptions) { + super(); + + // 初始化功能按钮选项 + this.menuButtonOptions = { ...this.menuButtonOptions, ...options }; + + // 创建功能按钮 + this.menuButton = new MenuButton(this.menuButtonOptions); + + // 初始化文件选择 + this.fileInput = document.createElement("input"); + this.fileInput.type = "file"; + this.fileInput.multiple = true; + this.fileInput.accept = "*/*"; + } + + /** + * 定义创建方法 + * @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('file', false).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/components/menus/toolbar/insert/Image.ts b/src/components/menus/toolbar/insert/Image.ts index c7230e0..8a3fda6 100644 --- a/src/components/menus/toolbar/insert/Image.ts +++ b/src/components/menus/toolbar/insert/Image.ts @@ -54,20 +54,10 @@ export class Image extends HTMLElement implements UAIEditorEventListener { this.menuButton.onCreate(event, options); this.appendChild(this.menuButton); - // this.fileInput.addEventListener("change", () => { - // const files = this.fileInput.files; - // if (files && files.length > 0) { - // for (let file of files) { - // event.editor.commands.uploadImage(file); - // } - // } - // (this.fileInput as any).value = ""; - // }); - // 定义按钮点击事件,插入图片 this.addEventListener("click", () => { if (this.menuButtonOptions.enable) { - event.editor.chain().focus().selectFiles('image', true).run() + event.editor.chain().focus().selectFiles('image', true).run(); // this.fileInput.click(); } }) diff --git a/src/components/menus/toolbar/insert/Video.ts b/src/components/menus/toolbar/insert/Video.ts index d0d58ee..1b4a053 100644 --- a/src/components/menus/toolbar/insert/Video.ts +++ b/src/components/menus/toolbar/insert/Video.ts @@ -49,7 +49,7 @@ export class Video extends HTMLElement implements UAIEditorEventListener { // 定义按钮点击事件,插入视频 this.addEventListener("click", () => { if (this.menuButtonOptions.enable) { - event.editor.chain().focus().selectFiles('video', true).run() + event.editor.chain().focus().selectFiles('video', true).run(); } }) } diff --git a/src/core/UAIExtensions.ts b/src/core/UAIExtensions.ts index ca4f216..a249cc7 100644 --- a/src/core/UAIExtensions.ts +++ b/src/core/UAIExtensions.ts @@ -29,6 +29,7 @@ import { UAIEditor, UAIEditorOptions } from "./UAIEditor"; import Audio from "../extensions/Audio.ts"; import BulletList from "../extensions/BulletList.ts"; import FontSize from "../extensions/FontSize.ts"; +import File from "../extensions/File.ts"; import Image from "../extensions/Image.ts"; import Indent from "../extensions/Indent.ts"; import LineHeight from "../extensions/LineHeight.ts" @@ -48,6 +49,7 @@ import { TextSelectionBubbleMenu } from "../components/menus/bubble/TextSelectio import { ImageBubbleMenu } from "../components/menus/bubble/ImageBubbleMenu.ts"; import { VideoBubbleMenu } from "../components/menus/bubble/VideoBubbleMenu.ts"; import { AudioBubbleMenu } from "../components/menus/bubble/AudioBubbleMenu.ts"; +import { FileBubbleMenu } from "../components/menus/bubble/FileBubbleMenu.ts"; /** * 创建浮动菜单功能 @@ -194,6 +196,33 @@ const createAudioBubbleMenu = (uaiEditor: UAIEditor) => { }) } +/** + * 创建文件内容的浮动菜单 + * @param uaiEditor + * @returns + */ +const createFileBubbleMenu = (uaiEditor: UAIEditor) => { + const container = new FileBubbleMenu(uaiEditor); + + return createBubbleMenu("fileBubbleMenu", { + pluginKey: 'fileBubbleMenu', + element: container, + tippyOptions: { + appendTo: uaiEditor.editor.editorContainer, + arrow: false, + interactive: true, + hideOnClick: false, + placement: 'top', + }, + shouldShow: ({ editor }) => { + if (!editor.isEditable) { + return false; + } + return editor.isActive("file") + } + }) +} + /** * 定义编辑器的所有自定义扩展组件 * @param uaiEditor @@ -214,6 +243,7 @@ export const allExtensions = (uaiEditor: UAIEditor, _options: UAIEditorOptions): appId: process.env.TIPTAP_APP_ID, token: process.env.TIPTAP_JWT_TOKEN, }), + File, FontFamily, FontSize.configure({ @@ -276,6 +306,7 @@ export const allExtensions = (uaiEditor: UAIEditor, _options: UAIEditorOptions): createImageBubbleMenu(uaiEditor), createVideoBubbleMenu(uaiEditor), createAudioBubbleMenu(uaiEditor), + createFileBubbleMenu(uaiEditor), ]; return extensions; diff --git a/src/extensions/File.ts b/src/extensions/File.ts new file mode 100644 index 0000000..8f349c0 --- /dev/null +++ b/src/extensions/File.ts @@ -0,0 +1,181 @@ +// Copyright (c) 2024-present AI-Labs + +// @ ts-nocheck +import { Node, mergeAttributes } from '@tiptap/core'; +import { t } from 'i18next'; + +import prettyBytes from 'pretty-bytes'; + +import { FullScreenModal } from '../components/modals/FullScreenModal.ts'; +import { getFileIcon } from '../utils/File.ts'; + +import downloadIcon from "../assets/icons/download.svg"; +import previewIcon from "../assets/icons/view.svg"; + +declare module '@tiptap/core' { + interface Commands { + setFile: { + setFile: (options: any) => ReturnType + } + } +} + +/** + * 定义文件选择扩展 + */ +export default Node.create({ + name: 'file', + group: 'block', + addAttributes() { + return { + vnode: { + default: true, + }, + id: { + default: null, + }, + file: { + default: null, + }, + url: { + default: null, + }, + name: { + default: null, + }, + type: { + default: null, + }, + size: { + default: null, + }, + uploaded: { + default: false, + }, + previewType: { + default: null, + }, + width: { + default: '500px', + }, + height: { + default: 'auto', + }, + } + }, + parseHTML() { + return [{ tag: 'file' }] + }, + renderHTML({ HTMLAttributes }) { + return [ + 'file', + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), + ] + }, + addNodeView() { + return (props) => { + const attrs = props.node.attrs; + const { nodeAlign, margin } = attrs; + + const wrapper = document.createElement('div'); + wrapper.classList.add("uai-node-view"); + wrapper.style.justifyContent = nodeAlign; + const marginTop = margin?.top && margin?.top !== '' ? `${margin.top}px` : undefined; + if (marginTop) { + wrapper.style.marginTop = marginTop; + } + const marginBottom = margin?.bottom && margin?.bottom !== '' ? `${margin.bottom}px` : undefined; + if (marginBottom) { + wrapper.style.marginBottom = marginBottom; + } + const container = document.createElement('div'); + container.classList.add("uai-node-container"); + container.classList.add("hover-shadow"); + container.classList.add("uai-select-outline"); + container.classList.add("uai-node-file"); + wrapper.appendChild(container); + + const icon = document.createElement('div'); + icon.classList.add("uai-file-icon"); + container.appendChild(icon); + const img = document.createElement('img'); + img.classList.add("icon-file"); + img.src = `https://unpkg.com/@uai-team/uai-resources@latest/dist/static/images/${getFileIcon(attrs.name)}.svg`; + icon.appendChild(img); + + const info = document.createElement('div'); + info.classList.add("uai-file-info"); + container.appendChild(info); + const name = document.createElement('div'); + name.classList.add("uai-file-name"); + name.title = attrs.name ?? t('file.unknownName'); + name.innerHTML = attrs.name ?? t('file.unknownName'); + info.appendChild(name); + const meta = document.createElement('div'); + meta.classList.add("uai-file-meta"); + meta.innerHTML = attrs.size ? prettyBytes(attrs.size) : t('file.unknownSize'); + info.appendChild(meta); + + const action = document.createElement('div'); + action.classList.add("uai-file-action"); + container.appendChild(action); + + const preview = document.createElement('div'); + preview.classList.add("uai-action-item"); + preview.title = t('file.preview'); + preview.innerHTML = ``; + preview.addEventListener("click", () => { + if (['image', 'video', 'audio'].includes(attrs.previewType)) { + props.editor.commands.insertContent({ + type: attrs.previewType, + attrs: { + ...attrs, + src: attrs.url, + }, + }); + } else if (["application/pdf"].includes(attrs.previewType)) { + const modal = new FullScreenModal(attrs.name ?? t('file.unknownName')); + const body = document.createElement('div'); + body.classList.add("uai-file-preview-modal-body"); + const iframe = document.createElement('iframe'); + iframe.src = attrs.url; + body.appendChild(iframe); + modal.modalBody.appendChild(body); + modal.show(); + const tippyRoot = document.querySelector('div[data-tippy-root]'); + if (tippyRoot) { + tippyRoot.style.visibility = "hidden"; + } + document.querySelector('.tippy-box')?.setAttribute("data-state", "hidden"); + tippyRoot?.parentElement?.removeChild(tippyRoot); + } + }); + action.appendChild(preview); + + const download = document.createElement('a'); + download.classList.add("uai-action-item"); + download.title = t('file.downlod'); + download.href = attrs.url; + download.target = "_blank"; + download.download = attrs.name; + download.innerHTML = ``; + action.appendChild(download); + + return { + dom: wrapper, + } + } + }, + addCommands() { + return { + setFile: + (options) => + ({ commands, editor }) => { + return commands.insertContentAt(editor.state.selection.anchor, { + type: this.name, + attrs: options, + }) + }, + } + }, +}) diff --git a/src/extensions/Image.ts b/src/extensions/Image.ts index 4ffd3fe..f07963b 100644 --- a/src/extensions/Image.ts +++ b/src/extensions/Image.ts @@ -98,7 +98,7 @@ export default Image.extend({ container.classList.add(`uai-node-view`); container.style.justifyContent = nodeAlign; - const wrapperStyle = width.indexOf("%") > 0 ? `style="width: ${width};"` : ""; + const wrapperStyle = `${width}`.indexOf("%") > 0 ? `style="width: ${width};"` : ""; var transform = "none"; diff --git a/src/extensions/SelectFile.ts b/src/extensions/SelectFile.ts index a8faa15..40d4833 100644 --- a/src/extensions/SelectFile.ts +++ b/src/extensions/SelectFile.ts @@ -1,7 +1,7 @@ // Copyright (c) 2024-present AI-Labs // @ ts-nocheck -import { Node, mergeAttributes } from '@tiptap/core'; +import { Node } from '@tiptap/core'; import { Plugin, PluginKey, TextSelection } from '@tiptap/pm/state'; import { Decoration, DecorationSet } from '@tiptap/pm/view'; @@ -32,9 +32,6 @@ export interface SelectFileOptions { declare module '@tiptap/core' { interface Commands { - setFile: { - setFile: (options: any) => ReturnType - } insertFile: { insertFile: (options: any) => ReturnType } @@ -89,60 +86,16 @@ const getAccept = (options: SelectFileOptions, type: string) => { * 定义文件选择扩展 */ export default Node.create({ - name: 'file', - group: 'block', - addAttributes() { - return { - vnode: { - default: true, - }, - id: { - default: null, - }, - file: { - default: null, - }, - url: { - default: null, - }, - name: { - default: null, - }, - type: { - default: null, - }, - size: { - default: null, - }, - uploaded: { - default: false, - }, - previewType: { - default: null, - }, - width: { - default: null, - }, - height: { - default: 200, - }, - } - }, - parseHTML() { - return [{ tag: 'file' }] - }, - renderHTML({ HTMLAttributes }) { - return [ - 'file', - mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), - ] - }, addCommands() { return { insertFile: - ({ file, autoType }) => + ({ file, type, autoType }) => ({ editor }) => { - const { type, name, size } = file; + const { name, size } = file; + const fileType = file.type; + if (autoType) { + type = fileType; + } let previewType: string | null = null; const id = uuid(); let uploader = Base64Uploader; @@ -174,9 +127,8 @@ export default Node.create({ }, }); }); - } - // 视频 - if (type.startsWith('video/') && mimeTypes.video.includes(type)) { + } else if (type.startsWith('video/') && mimeTypes.video.includes(type)) { + // 视频 previewType = 'video'; uploader = editorOptions.video?.uploader ?? Base64Uploader; @@ -195,9 +147,8 @@ export default Node.create({ }, }); }); - } - // 音频 - if (type.startsWith('audio/') && mimeTypes.audio.includes(type)) { + } else 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") @@ -215,6 +166,21 @@ export default Node.create({ }, }); }); + } else { + previewType = "file"; + // 插入节点 + 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']: URL.createObjectURL(file), + name, + type: fileType, + size, + file, + previewType: Object.keys(mimeTypes).includes(fileType.split("/")[0]) ? fileType.split("/")[0] : fileType, + }, + }) } return true; }, @@ -229,7 +195,7 @@ export default Node.create({ fileInput.click(); fileInput.addEventListener("change", () => { for (const file of fileInput.files ?? []) { - editor.chain().focus().insertFile({ file, autoType }).run(); + editor.chain().focus().insertFile({ file, type, autoType }).run(); } }); return true; @@ -282,13 +248,14 @@ export default Node.create({ let isImagePasted = false; let autoType = true; for (const item of items) { - if (item.type.indexOf("image") === 0) { + const type = item.type; + if (type.indexOf("image") === 0) { const file = item.getAsFile(); if (file) { event.preventDefault(); isImagePasted = true; - editor.chain().focus().insertFile({ file, autoType }).run(); + editor.chain().focus().insertFile({ file, type, autoType }).run(); } } } @@ -316,8 +283,9 @@ export default Node.create({ const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY }) dispatch(tr.setSelection(TextSelection.create(doc, coordinates!.pos)).scrollIntoView()) + const type = "image"; images.forEach(image => { - editor.chain().focus().insertFile({ image, autoType }).run(); + editor.chain().focus().insertFile({ image, type, autoType }).run(); }) return true diff --git a/src/styles/uaieditor.less b/src/styles/uaieditor.less index 8df5caf..bb123a8 100644 --- a/src/styles/uaieditor.less +++ b/src/styles/uaieditor.less @@ -204,14 +204,15 @@ } // &:not(:last-child) { - &::before { - content: ''; - display: block; - height: 18px; - width: 1px; - background-color: var(--uai-border-color-light); - margin: 0 10px; - } + &::before { + content: ''; + display: block; + height: 18px; + width: 1px; + background-color: var(--uai-border-color-light); + margin: 0 10px; + } + // } &:first-child::before { @@ -2371,4 +2372,97 @@ height: 0; position: absolute; border: none; +} + +.uai-node-view { + .uai-node-file { + display: inline-flex; + align-items: center; + padding: 12px; + outline: solid 1px var(--uai-content-node-border); + overflow: hidden; + background-color: var(--uai-color-white); + border-radius: var(--uai-content-node-radius); + + .uai-file-icon { + width: 32px; + height: 32px; + margin-right: 8px; + flex: 1; + + .icon-file { + width: 32px; + display: block; + } + } + + .uai-file-info { + width: 240px; + + .uai-file-name { + font-size: 12px; + font-weight: 500; + line-height: 1.2; + text-overflow: ellipsis; + overflow: hidden; + word-break: break-all; + white-space: nowrap; + width: 100%; + padding-right: 10px; + box-sizing: border-box; + } + + .uai-file-meta { + font-size: 12px; + color: var(--uai-text-color-light); + line-height: 1; + margin-top: 6px; + } + } + + .uai-file-action { + flex: 1; + display: flex; + align-items: center; + color: var(--uai-text-color-light); + gap: 5px; + + .uai-action-item { + font-size: 18px; + display: flex; + align-items: center; + justify-content: center; + height: 32px; + width: 32px; + background-color: var(--uai-color-white); + box-sizing: border-box; + cursor: pointer; + border-radius: 50%; + color: var(--uai-text-color-light); + + &:hover { + border: solid 1px var(--uai-primary-color); + color: var(--uai-primary-color); + } + + .loading { + animation: turn 1s linear infinite; + } + } + } + } +} + +.uai-file-preview-modal { + padding: 0 !important; + + &-body { + iframe { + display: block; + width: 100%; + height: calc(100vh - 100px); + border: solid 1px var(--uai-border-color-light); + box-sizing: border-box; + } + } } \ No newline at end of file diff --git a/src/utils/File.ts b/src/utils/File.ts new file mode 100644 index 0000000..d655b54 --- /dev/null +++ b/src/utils/File.ts @@ -0,0 +1,54 @@ +const fileTypes: Record = { + ai: ['ai', 'eps'], + app: ['app'], + axure: ['rp'], + book: ['mobi', 'oeb', 'lit', 'xeb', 'ebx', 'rb', 'pdb', 'epub', 'azw3', 'hlp', 'chm', 'wdl', 'ceb', 'abm', 'pdg', 'caj'], + css: ['css', 'less', 'sass'], + dmg: ['dmg'], + excel: ['csv', 'fods', 'ods', 'ots', 'xls', 'xlsm', 'xlsx', 'xlt', 'xltm', 'xltx', 'et', 'ett'], + exe: ['exe'], + font: ['eot', 'otf', 'fon', 'font', 'ttf', 'ttc', 'woff', 'woff2'], + html: ['htm', 'html', 'mht'], + img: ['png', 'bmp', 'jpg', 'jpeg', 'gif', 'webp', 'tga', 'exif', 'fpx', 'svg', 'hdri', 'raw', 'ico', 'jfif', 'dib', 'pbm', 'pgm', 'ppm', 'rgb'], + java: ['jar', 'java'], + js: ['js', 'jsx', 'ts', 'tsx'], + json: ['json'], + keynote: ['key'], + md: ['md', 'markdown'], + music: ['au', 'aif', 'aiff', 'aifc', 'rmi', 'mp3', 'mid', 'cda', 'wav', 'wma', 'ra', 'ram', 'snd', 'mida', 'ogg', 'ape', 'flac', 'aac'], + numbers: ['numbers'], + pages: ['pages'], + pdf: ['pdf'], + php: ['php'], + pkg: ['pkg'], + ppt: ['fodp', 'odp', 'otp', 'pot', 'potm', 'potx', 'pps', 'ppsm', 'ppsx', 'ppt', 'pptm', 'pptx', 'dps', 'dpt', 'wpp'], + psd: ['psd'], + python: ['python', 'py'], + sh: ['sh'], + sketch: ['sketch'], + sql: ['sql'], + text: ['text', 'txt'], + video: ['mp4', 'avi', 'mov', 'rmvb', 'rm', 'flv', 'mpeg', 'wmv', 'mkv'], + vue: ['vue'], + word: ['doc', 'docm', 'docx', 'dot', 'dotm', 'dotx', 'epub', 'fodt', 'odt', 'ott', 'rtf', 'wps', 'wpt'], + xmind: ['xmind'], + zip: ['zip', 'rar', 'tar', 'gz', 'gzip', 'uue', 'bz2', 'iso', '7z', 'z', 'ace', 'lzh', 'arj', 'cab'], +} + +export const getFileExtname = (filename: string) => { + const splitFileName = filename?.split('.') + return splitFileName ? splitFileName[splitFileName.length - 1] : undefined +} + +export const getFileIcon = (filename: string) => { + let iconName = 'common' + const extname = getFileExtname(filename) + if (extname) { + for (const type of Object.keys(fileTypes)) { + if (fileTypes[type].includes(extname)) { + iconName = type + } + } + } + return iconName +}