添加插入文件功能

This commit is contained in:
wux_labs
2025-02-26 13:40:36 +08:00
parent 01db2bb19d
commit 52aacaaefb
16 changed files with 607 additions and 86 deletions
+12
View File
@@ -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",
+1
View File
@@ -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"
+6
View File
@@ -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);
}
}
+6
View File
@@ -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");
+6
View File
@@ -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");
+1 -1
View File
@@ -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);
}
}
+1 -11
View File
@@ -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();
}
})
+1 -1
View File
@@ -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();
}
})
}
+31
View File
@@ -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;
+181
View File
@@ -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,
})
},
}
},
})
+1 -1
View File
@@ -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";
+32 -64
View File
@@ -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
View File
@@ -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;
}
}
}
+54
View File
@@ -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
}