添加插入文件功能
This commit is contained in:
Generated
+12
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<ReturnType> {
|
||||
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 = `<img src="${previewIcon}" width="16" />`;
|
||||
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<HTMLDivElement>('div[data-tippy-root]');
|
||||
if (tippyRoot) {
|
||||
tippyRoot.style.visibility = "hidden";
|
||||
}
|
||||
document.querySelector<HTMLDivElement>('.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 = `<img src="${downloadIcon}" width="16" />`;
|
||||
action.appendChild(download);
|
||||
|
||||
return {
|
||||
dom: wrapper,
|
||||
}
|
||||
}
|
||||
},
|
||||
addCommands() {
|
||||
return {
|
||||
setFile:
|
||||
(options) =>
|
||||
({ commands, editor }) => {
|
||||
return commands.insertContentAt(editor.state.selection.anchor, {
|
||||
type: this.name,
|
||||
attrs: options,
|
||||
})
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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<ReturnType> {
|
||||
setFile: {
|
||||
setFile: (options: any) => ReturnType
|
||||
}
|
||||
insertFile: {
|
||||
insertFile: (options: any) => ReturnType
|
||||
}
|
||||
@@ -89,60 +86,16 @@ const getAccept = (options: SelectFileOptions, type: string) => {
|
||||
* 定义文件选择扩展
|
||||
*/
|
||||
export default Node.create<SelectFileOptions>({
|
||||
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<SelectFileOptions>({
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
// 视频
|
||||
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<SelectFileOptions>({
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
// 音频
|
||||
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<SelectFileOptions>({
|
||||
},
|
||||
});
|
||||
});
|
||||
} 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<SelectFileOptions>({
|
||||
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<SelectFileOptions>({
|
||||
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<SelectFileOptions>({
|
||||
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
|
||||
|
||||
+102
-8
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
const fileTypes: Record<string, string[]> = {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user