添加插入图片菜单
This commit is contained in:
@@ -52,6 +52,7 @@ import { CodeBlock } from "./menus/toolbar/base/CodeBlock.ts";
|
|||||||
import { Print } from "./menus/toolbar/base/Print.ts";
|
import { Print } from "./menus/toolbar/base/Print.ts";
|
||||||
|
|
||||||
import { Link } from "./menus/toolbar/insert/Link.ts";
|
import { Link } from "./menus/toolbar/insert/Link.ts";
|
||||||
|
import { Image } from "./menus/toolbar/insert/Image.ts";
|
||||||
|
|
||||||
// 注册组件
|
// 注册组件
|
||||||
defineCustomElement('uai-editor-header', Header);
|
defineCustomElement('uai-editor-header', Header);
|
||||||
@@ -105,3 +106,4 @@ defineCustomElement('uai-editor-base-menu-codeblock', CodeBlock);
|
|||||||
defineCustomElement('uai-editor-base-menu-print', Print);
|
defineCustomElement('uai-editor-base-menu-print', Print);
|
||||||
|
|
||||||
defineCustomElement('uai-editor-insert-menu-link', Link);
|
defineCustomElement('uai-editor-insert-menu-link', Link);
|
||||||
|
defineCustomElement('uai-editor-insert-menu-image', Image);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { EditorEvents } from "@tiptap/core";
|
|||||||
import { UAIEditorEventListener, UAIEditorOptions } from "../../../core/UAIEditor.ts";
|
import { UAIEditorEventListener, UAIEditorOptions } from "../../../core/UAIEditor.ts";
|
||||||
|
|
||||||
import menuIcon from "../../../assets/icons/menu.svg";
|
import menuIcon from "../../../assets/icons/menu.svg";
|
||||||
import { ScrollableDiv } from "./ScrollableDiv";
|
import { ScrollableDiv } from "./ScrollableDiv.ts";
|
||||||
|
|
||||||
import { FontSizeIncrease } from "../common/FontSizeIncrease.ts";
|
import { FontSizeIncrease } from "../common/FontSizeIncrease.ts";
|
||||||
import { FontSizeDecrease } from "../common/FontSizeDecrease.ts";
|
import { FontSizeDecrease } from "../common/FontSizeDecrease.ts";
|
||||||
@@ -46,7 +46,8 @@ import { BlockQuote } from "./base/BlockQuote.ts";
|
|||||||
import { CodeBlock } from "./base/CodeBlock.ts";
|
import { CodeBlock } from "./base/CodeBlock.ts";
|
||||||
import { Print } from "./base/Print.ts";
|
import { Print } from "./base/Print.ts";
|
||||||
|
|
||||||
import { Link } from "./insert/Link";
|
import { Link } from "./insert/Link.ts";
|
||||||
|
import { Image } from "./insert/Image.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 传统菜单栏
|
* 传统菜单栏
|
||||||
@@ -105,6 +106,7 @@ export class Classic extends HTMLElement implements UAIEditorEventListener {
|
|||||||
|
|
||||||
// 插入菜单
|
// 插入菜单
|
||||||
insertMenuLink!: Link;
|
insertMenuLink!: Link;
|
||||||
|
insertMenuImage!: Image;
|
||||||
|
|
||||||
constructor(defaultToolbarMenus: Record<string, any>[]) {
|
constructor(defaultToolbarMenus: Record<string, any>[]) {
|
||||||
super();
|
super();
|
||||||
@@ -277,6 +279,9 @@ export class Classic extends HTMLElement implements UAIEditorEventListener {
|
|||||||
|
|
||||||
this.insertMenuLink = new Link({ menuType: "button", enable: true, header: "classic", hideText: false });
|
this.insertMenuLink = new Link({ menuType: "button", enable: true, header: "classic", hideText: false });
|
||||||
this.eventComponents.push(this.insertMenuLink);
|
this.eventComponents.push(this.insertMenuLink);
|
||||||
|
|
||||||
|
this.insertMenuImage = new Image({ menuType: "button", enable: true, header: "classic", hideText: false });
|
||||||
|
this.eventComponents.push(this.insertMenuImage);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 创建基础菜单
|
* 创建基础菜单
|
||||||
@@ -355,5 +360,6 @@ export class Classic extends HTMLElement implements UAIEditorEventListener {
|
|||||||
group1.classList.add("uai-classic-virtual-group");
|
group1.classList.add("uai-classic-virtual-group");
|
||||||
this.classicMenuInsertGroup.appendChild(group1);
|
this.classicMenuInsertGroup.appendChild(group1);
|
||||||
group1.appendChild(this.insertMenuLink);
|
group1.appendChild(this.insertMenuLink);
|
||||||
|
group1.appendChild(this.insertMenuImage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,7 @@ import { EditorEvents } from "@tiptap/core";
|
|||||||
import { UAIEditorEventListener, UAIEditorOptions } from "../../../core/UAIEditor.ts";
|
import { UAIEditorEventListener, UAIEditorOptions } from "../../../core/UAIEditor.ts";
|
||||||
|
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { ScrollableDiv } from "./ScrollableDiv";
|
import { ScrollableDiv } from "./ScrollableDiv.ts";
|
||||||
import { Icons } from "../../Icons.ts";
|
import { Icons } from "../../Icons.ts";
|
||||||
|
|
||||||
import { FontSizeIncrease } from "../common/FontSizeIncrease.ts";
|
import { FontSizeIncrease } from "../common/FontSizeIncrease.ts";
|
||||||
@@ -48,7 +48,8 @@ import { CodeBlock } from "./base/CodeBlock.ts";
|
|||||||
|
|
||||||
import { Print } from "./base/Print.ts";
|
import { Print } from "./base/Print.ts";
|
||||||
|
|
||||||
import { Link } from "./insert/Link";
|
import { Link } from "./insert/Link.ts";
|
||||||
|
import { Image } from "./insert/Image.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 经典菜单栏
|
* 经典菜单栏
|
||||||
@@ -108,6 +109,7 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener {
|
|||||||
|
|
||||||
// 插入菜单
|
// 插入菜单
|
||||||
insertMenuLink!: Link;
|
insertMenuLink!: Link;
|
||||||
|
insertMenuImage!: Image;
|
||||||
|
|
||||||
constructor(defaultToolbarMenus: Record<string, any>[]) {
|
constructor(defaultToolbarMenus: Record<string, any>[]) {
|
||||||
super();
|
super();
|
||||||
@@ -300,6 +302,9 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener {
|
|||||||
|
|
||||||
this.insertMenuLink = new Link({ menuType: "button", enable: true, huge: true });
|
this.insertMenuLink = new Link({ menuType: "button", enable: true, huge: true });
|
||||||
this.eventComponents.push(this.insertMenuLink);
|
this.eventComponents.push(this.insertMenuLink);
|
||||||
|
|
||||||
|
this.insertMenuImage = new Image({ menuType: "button", enable: true, huge: true });
|
||||||
|
this.eventComponents.push(this.insertMenuImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -452,5 +457,6 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener {
|
|||||||
group1.classList.add("uai-ribbon-virtual-group");
|
group1.classList.add("uai-ribbon-virtual-group");
|
||||||
this.ribbonMenuInsertGroup.appendChild(group1);
|
this.ribbonMenuInsertGroup.appendChild(group1);
|
||||||
group1.appendChild(this.insertMenuLink);
|
group1.appendChild(this.insertMenuLink);
|
||||||
|
group1.appendChild(this.insertMenuImage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
// Copyright (c) 2024-present AI-Labs
|
||||||
|
|
||||||
|
// @ ts-nocheck
|
||||||
|
import { MenuButton, MenuButtonOptions } from "../../MenuButton.ts";
|
||||||
|
import icon from "../../../../assets/icons/image.svg";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { UAIEditorEventListener, UAIEditorOptions } from "../../../../core/UAIEditor.ts";
|
||||||
|
import { EditorEvents } from "@tiptap/core";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插入菜单:插入图片
|
||||||
|
*/
|
||||||
|
export class Image extends HTMLElement implements UAIEditorEventListener {
|
||||||
|
// 按钮选项
|
||||||
|
menuButtonOptions: MenuButtonOptions = {
|
||||||
|
menuType: "button",
|
||||||
|
enable: true,
|
||||||
|
icon: icon,
|
||||||
|
hideText: false,
|
||||||
|
text: t('insert.image'),
|
||||||
|
tooltip: t('insert.image'),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 功能按钮
|
||||||
|
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 = "image/*";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定义创建方法
|
||||||
|
* @param event
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
onCreate(event: EditorEvents["create"], options: UAIEditorOptions) {
|
||||||
|
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()
|
||||||
|
// this.fileInput.click();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定义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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@ import i18next from "i18next";
|
|||||||
import { zh } from "../i18n/zh.ts";
|
import { zh } from "../i18n/zh.ts";
|
||||||
import { Resource } from "i18next";
|
import { Resource } from "i18next";
|
||||||
import { allExtensions } from "./UAIExtensions.ts";
|
import { allExtensions } from "./UAIExtensions.ts";
|
||||||
|
import { Uploader } from "../utils/FileUploader.ts";
|
||||||
|
|
||||||
self.MonacoEnvironment = {
|
self.MonacoEnvironment = {
|
||||||
getWorker(_workerId, _label) {
|
getWorker(_workerId, _label) {
|
||||||
@@ -81,6 +82,12 @@ export type UAIEditorOptions = {
|
|||||||
theme?: "light" | "dark",
|
theme?: "light" | "dark",
|
||||||
lang?: string,
|
lang?: string,
|
||||||
i18n?: Record<string, Record<string, string>>,
|
i18n?: Record<string, Record<string, string>>,
|
||||||
|
image?: {
|
||||||
|
uploadUrl?: string,
|
||||||
|
uploadHeaders?: (() => Record<string, any>) | Record<string, any>,
|
||||||
|
uploadFormName?: string,
|
||||||
|
uploader?: Uploader,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -19,10 +19,12 @@ import { Underline } from "@tiptap/extension-underline";
|
|||||||
|
|
||||||
import BulletList from "../extensions/BulletList.ts";
|
import BulletList from "../extensions/BulletList.ts";
|
||||||
import FontSize from "../extensions/FontSize.ts";
|
import FontSize from "../extensions/FontSize.ts";
|
||||||
|
import Image from "../extensions/Image.ts";
|
||||||
import Indent from "../extensions/Indent.ts";
|
import Indent from "../extensions/Indent.ts";
|
||||||
import LineHeight from "../extensions/LineHeight.ts"
|
import LineHeight from "../extensions/LineHeight.ts"
|
||||||
import NodeAlign from "../extensions/NodeAlign.ts";
|
import NodeAlign from "../extensions/NodeAlign.ts";
|
||||||
import OrderedList from "../extensions/OrderedList.ts";
|
import OrderedList from "../extensions/OrderedList.ts";
|
||||||
|
import SelectFile from "../extensions/SelectFile.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 定义编辑器的所有自定义扩展组件
|
* 定义编辑器的所有自定义扩展组件
|
||||||
@@ -45,11 +47,15 @@ export const allExtensions = (uaiEditor: UAIEditor, _options: UAIEditorOptions):
|
|||||||
Highlight.configure({
|
Highlight.configure({
|
||||||
multicolor: true
|
multicolor: true
|
||||||
}),
|
}),
|
||||||
|
Image,
|
||||||
Indent,
|
Indent,
|
||||||
LineHeight,
|
LineHeight,
|
||||||
Link,
|
Link,
|
||||||
NodeAlign,
|
NodeAlign,
|
||||||
OrderedList,
|
OrderedList,
|
||||||
|
SelectFile.configure({
|
||||||
|
allowedMimeTypes: []
|
||||||
|
}),
|
||||||
Subscript,
|
Subscript,
|
||||||
Superscript,
|
Superscript,
|
||||||
TaskList,
|
TaskList,
|
||||||
|
|||||||
@@ -0,0 +1,160 @@
|
|||||||
|
// Copyright (c) 2024-present AI-Labs
|
||||||
|
|
||||||
|
import { NodeViewRendererProps } from '@tiptap/core'
|
||||||
|
import Image from '@tiptap/extension-image'
|
||||||
|
import { resize } from '../utils/resize.ts'
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
setImage: {
|
||||||
|
/**
|
||||||
|
* 设置图片
|
||||||
|
* @param options
|
||||||
|
* @param replace
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
setImage: (options: any, replace?: any) => ReturnType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default Image.extend({
|
||||||
|
atom: true,
|
||||||
|
addAttributes() {
|
||||||
|
return {
|
||||||
|
vnode: {
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
default: 'image',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
file: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
src: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
default: '500px',
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
default: 'auto',
|
||||||
|
},
|
||||||
|
left: {
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
top: {
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
angle: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
draggable: {
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
rotatable: {
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
equalProportion: {
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
flipX: {
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
flipY: {
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
uploaded: {
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
error: {
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
previewType: {
|
||||||
|
default: 'image',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
parseHTML() {
|
||||||
|
return [{ tag: 'img' }]
|
||||||
|
},
|
||||||
|
addNodeView() {
|
||||||
|
return (props: NodeViewRendererProps) => {
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const { src, width, height, nodeAlign, alt, flipX, flipY } = props.node.attrs;
|
||||||
|
container.classList.add(`uai-node-view`);
|
||||||
|
container.style.justifyContent = nodeAlign;
|
||||||
|
|
||||||
|
const wrapperStyle = width.indexOf("%") > 0 ? `style="width: ${width};"` : "";
|
||||||
|
|
||||||
|
var transform = "none";
|
||||||
|
|
||||||
|
if (flipX || flipY) {
|
||||||
|
transform = `rotateX(${flipX ? '180' : '0'}deg) rotateY(${flipY ? '180' : '0'}deg)`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.editor.isEditable) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="uai-resize-wrapper" ${wrapperStyle}>
|
||||||
|
<img alt="${alt}" src="${src}" style="width: ${width}; height: ${height || 'auto'}; transform: ${transform};" class="resize-obj">
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
return {
|
||||||
|
dom: container,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="uai-resize-wrapper" ${wrapperStyle}>
|
||||||
|
<div class="uai-resize">
|
||||||
|
<div class="uai-resize-btn-top-left" data-position="1" draggable="true"></div>
|
||||||
|
<div class="uai-resize-btn-top-center" data-position="2" draggable="true"></div>
|
||||||
|
<div class="uai-resize-btn-top-right" data-position="3" draggable="true"></div>
|
||||||
|
<div class="uai-resize-btn-left-center" data-position="4" draggable="true"></div>
|
||||||
|
<div class="uai-resize-btn-right-center" data-position="5" draggable="true"></div>
|
||||||
|
<div class="uai-resize-btn-bottom-left" data-position="6" draggable="true"></div>
|
||||||
|
<div class="uai-resize-btn-bottom-center" data-position="7" draggable="true"></div>
|
||||||
|
<div class="uai-resize-btn-bottom-right" data-position="8" draggable="true"></div>
|
||||||
|
</div>
|
||||||
|
<img alt="${alt}" src="${src}" style="width: ${width}; height: ${height || 'auto'}; transform: ${transform};" class="resize-obj">
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
resize(container, this.editor.view.dom, (attrs) => this.editor.commands.updateAttributes("image", attrs));
|
||||||
|
return {
|
||||||
|
dom: container,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addCommands() {
|
||||||
|
return {
|
||||||
|
setImage:
|
||||||
|
(
|
||||||
|
options: { src: string; alt?: string; title?: string },
|
||||||
|
replace?: boolean,
|
||||||
|
) =>
|
||||||
|
({ commands, editor }) => {
|
||||||
|
if (replace) {
|
||||||
|
return commands.insertContent({
|
||||||
|
type: this.name,
|
||||||
|
attrs: options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return commands.insertContentAt(editor.state.selection.anchor, {
|
||||||
|
type: this.name,
|
||||||
|
attrs: options,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
// Copyright (c) 2024-present AI-Labs
|
||||||
|
|
||||||
|
import { Node, mergeAttributes } from '@tiptap/core';
|
||||||
|
import { Plugin, PluginKey, TextSelection } from '@tiptap/pm/state';
|
||||||
|
import { Decoration, DecorationSet } from '@tiptap/pm/view';
|
||||||
|
import { InnerEditor } from '../core/UAIEditor.ts';
|
||||||
|
import { Base64Uploader } from '../utils/FileUploader.ts';
|
||||||
|
import { uuid } from '../utils/uuid.ts';
|
||||||
|
import { Icons } from '../components/Icons.ts';
|
||||||
|
|
||||||
|
export type DecorationWaitingAction = {
|
||||||
|
type: "add" | "remove";
|
||||||
|
id: string;
|
||||||
|
pos: number;
|
||||||
|
}
|
||||||
|
const key = new PluginKey("uai-waiting-plugin");
|
||||||
|
const actionKey = "uai_waiting_action";
|
||||||
|
|
||||||
|
export const createDecoration = (action: { pos: number, id: string }) => {
|
||||||
|
const placeholder = document.createElement("div");
|
||||||
|
placeholder.classList.add("uai-loader-placeholder");
|
||||||
|
placeholder.innerHTML = Icons.Loading;
|
||||||
|
return Decoration.widget(action.pos, placeholder, { id: action.id });
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectFileOptions {
|
||||||
|
allowedMimeTypes: string[],
|
||||||
|
HTMLAttributes: Record<string, any>,
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@tiptap/core' {
|
||||||
|
interface Commands<ReturnType> {
|
||||||
|
setFile: {
|
||||||
|
setFile: (options: any) => ReturnType
|
||||||
|
}
|
||||||
|
insertFile: {
|
||||||
|
insertFile: (options: any) => ReturnType
|
||||||
|
}
|
||||||
|
selectFiles: {
|
||||||
|
selectFiles: (type: any, autoType: any) => ReturnType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 指定可选文件的类型
|
||||||
|
*/
|
||||||
|
const mimeTypes: any = {
|
||||||
|
image: [
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/gif',
|
||||||
|
'image/webp',
|
||||||
|
'image/svg+xml',
|
||||||
|
'image/apng',
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否接受选择的文件类型
|
||||||
|
* @param options
|
||||||
|
* @param type
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const getAccept = (options: SelectFileOptions, type: string) => {
|
||||||
|
const accept = options.allowedMimeTypes
|
||||||
|
if (type === 'file' && accept.length === 0) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
if (!type || !['image', 'video', 'audio'].includes(type)) {
|
||||||
|
return accept.toString()
|
||||||
|
}
|
||||||
|
let acceptArray = [...accept]
|
||||||
|
if (acceptArray.includes(`${type}/*`) || accept.length === 0) {
|
||||||
|
acceptArray = mimeTypes[type]
|
||||||
|
} else if (acceptArray.filter((item) => item.startsWith(type)).length > 0) {
|
||||||
|
acceptArray = accept.filter((item: any) => mimeTypes[type].includes(item))
|
||||||
|
} else {
|
||||||
|
acceptArray = ['notAllow']
|
||||||
|
}
|
||||||
|
return acceptArray.length === 0 ? '' : acceptArray.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定义文件选择扩展
|
||||||
|
*/
|
||||||
|
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 }) =>
|
||||||
|
({ editor }) => {
|
||||||
|
const { type, name, size } = file;
|
||||||
|
let previewType: string | null = null;
|
||||||
|
const id = uuid();
|
||||||
|
let uploader = Base64Uploader;
|
||||||
|
let editorOptions = (editor as InnerEditor).uaiEditor.options;
|
||||||
|
const { state: { tr }, view } = this.editor;
|
||||||
|
if (!tr.selection.empty) tr.deleteSelection();
|
||||||
|
view.dispatch(tr.setMeta(actionKey, {
|
||||||
|
type: "add",
|
||||||
|
id,
|
||||||
|
pos: tr.selection.from,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 图片
|
||||||
|
if (type.startsWith('image/') && mimeTypes.image.includes(type)) {
|
||||||
|
previewType = 'image';
|
||||||
|
uploader = editorOptions.image?.uploader ?? Base64Uploader;
|
||||||
|
uploader(file, editorOptions.image?.uploadUrl, editorOptions.image?.uploadHeaders, editorOptions.image?.uploadFormName || "image")
|
||||||
|
.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:
|
||||||
|
(type, autoType = false) =>
|
||||||
|
({ editor }) => {
|
||||||
|
const accept = getAccept(this.options, type)
|
||||||
|
const fileInput = document.createElement("input");
|
||||||
|
fileInput.type = "file";
|
||||||
|
fileInput.multiple = true;
|
||||||
|
fileInput.accept = accept;
|
||||||
|
fileInput.click();
|
||||||
|
fileInput.addEventListener("change", () => {
|
||||||
|
for (const file of fileInput.files ?? []) {
|
||||||
|
editor.chain().focus().insertFile({ file, autoType }).run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addProseMirrorPlugins() {
|
||||||
|
const editor = this.editor;
|
||||||
|
return [
|
||||||
|
new Plugin({
|
||||||
|
key: key,
|
||||||
|
state: {
|
||||||
|
init: () => DecorationSet.empty,
|
||||||
|
apply: (tr, set) => {
|
||||||
|
const action = tr.getMeta(actionKey) as DecorationWaitingAction;
|
||||||
|
if (action) {
|
||||||
|
// update decorations position
|
||||||
|
let removed = false;
|
||||||
|
const newSet = set.map(tr.mapping, tr.doc, {
|
||||||
|
onRemove: (_) => {
|
||||||
|
removed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!removed) {
|
||||||
|
set = newSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add decoration
|
||||||
|
if (action.type === "add") {
|
||||||
|
set = set.add(tr.doc, [createDecoration(action)]);
|
||||||
|
}
|
||||||
|
// remove decoration
|
||||||
|
else if (action.type === "remove") {
|
||||||
|
set = set.remove(set.find(void 0, void 0,
|
||||||
|
spec => spec.id == action.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
props: {
|
||||||
|
decorations(state) {
|
||||||
|
return this.getState(state);
|
||||||
|
},
|
||||||
|
|
||||||
|
handlePaste: (_, event) => {
|
||||||
|
const items = Array.from(event.clipboardData?.items || []);
|
||||||
|
let isImagePasted = false;
|
||||||
|
let autoType = true;
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.type.indexOf("image") === 0) {
|
||||||
|
const file = item.getAsFile();
|
||||||
|
if (file) {
|
||||||
|
event.preventDefault();
|
||||||
|
isImagePasted = true;
|
||||||
|
|
||||||
|
editor.chain().focus().insertFile({ file, autoType }).run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isImagePasted;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleDOMEvents: {
|
||||||
|
drop(view, event) {
|
||||||
|
let autoType = true;
|
||||||
|
const hasFiles = event.dataTransfer &&
|
||||||
|
event.dataTransfer.files &&
|
||||||
|
event.dataTransfer.files.length
|
||||||
|
|
||||||
|
if (!hasFiles) return false
|
||||||
|
|
||||||
|
const images = Array
|
||||||
|
.from(event.dataTransfer.files)
|
||||||
|
.filter(file => (/image/i).test(file.type))
|
||||||
|
|
||||||
|
if (images.length === 0) return false
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
const { state: { tr, doc }, dispatch } = view
|
||||||
|
const coordinates = view.posAtCoords({ left: event.clientX, top: event.clientY })
|
||||||
|
dispatch(tr.setSelection(TextSelection.create(doc, coordinates!.pos)).scrollIntoView())
|
||||||
|
|
||||||
|
images.forEach(image => {
|
||||||
|
editor.chain().focus().insertFile({ image, autoType }).run();
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
transformPastedHTML(html) {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const document = parser.parseFromString(html, 'text/html');
|
||||||
|
const workspace = document.documentElement.querySelector('body');
|
||||||
|
if (workspace?.children) {
|
||||||
|
const imgNodes = document.documentElement.querySelectorAll('p > img');
|
||||||
|
for (const image of imgNodes) {
|
||||||
|
const imageParent = image.parentNode;
|
||||||
|
const position = Array.prototype.indexOf.call(workspace.children, imageParent);
|
||||||
|
image.parentElement!.prepend(image);
|
||||||
|
workspace.insertBefore(image, workspace.children[position]);
|
||||||
|
}
|
||||||
|
return workspace.innerHTML;
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
},
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user