添加插入图片菜单
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 { Link } from "./menus/toolbar/insert/Link.ts";
|
||||
import { Image } from "./menus/toolbar/insert/Image.ts";
|
||||
|
||||
// 注册组件
|
||||
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-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 menuIcon from "../../../assets/icons/menu.svg";
|
||||
import { ScrollableDiv } from "./ScrollableDiv";
|
||||
import { ScrollableDiv } from "./ScrollableDiv.ts";
|
||||
|
||||
import { FontSizeIncrease } from "../common/FontSizeIncrease.ts";
|
||||
import { FontSizeDecrease } from "../common/FontSizeDecrease.ts";
|
||||
@@ -46,7 +46,8 @@ import { BlockQuote } from "./base/BlockQuote.ts";
|
||||
import { CodeBlock } from "./base/CodeBlock.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;
|
||||
insertMenuImage!: Image;
|
||||
|
||||
constructor(defaultToolbarMenus: Record<string, any>[]) {
|
||||
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.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");
|
||||
this.classicMenuInsertGroup.appendChild(group1);
|
||||
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 { t } from "i18next";
|
||||
import { ScrollableDiv } from "./ScrollableDiv";
|
||||
import { ScrollableDiv } from "./ScrollableDiv.ts";
|
||||
import { Icons } from "../../Icons.ts";
|
||||
|
||||
import { FontSizeIncrease } from "../common/FontSizeIncrease.ts";
|
||||
@@ -48,7 +48,8 @@ import { CodeBlock } from "./base/CodeBlock.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;
|
||||
insertMenuImage!: Image;
|
||||
|
||||
constructor(defaultToolbarMenus: Record<string, any>[]) {
|
||||
super();
|
||||
@@ -300,6 +302,9 @@ export class Ribbon extends HTMLElement implements UAIEditorEventListener {
|
||||
|
||||
this.insertMenuLink = new Link({ menuType: "button", enable: true, huge: true });
|
||||
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");
|
||||
this.ribbonMenuInsertGroup.appendChild(group1);
|
||||
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 { Resource } from "i18next";
|
||||
import { allExtensions } from "./UAIExtensions.ts";
|
||||
import { Uploader } from "../utils/FileUploader.ts";
|
||||
|
||||
self.MonacoEnvironment = {
|
||||
getWorker(_workerId, _label) {
|
||||
@@ -81,6 +82,12 @@ export type UAIEditorOptions = {
|
||||
theme?: "light" | "dark",
|
||||
lang?: 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 FontSize from "../extensions/FontSize.ts";
|
||||
import Image from "../extensions/Image.ts";
|
||||
import Indent from "../extensions/Indent.ts";
|
||||
import LineHeight from "../extensions/LineHeight.ts"
|
||||
import NodeAlign from "../extensions/NodeAlign.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({
|
||||
multicolor: true
|
||||
}),
|
||||
Image,
|
||||
Indent,
|
||||
LineHeight,
|
||||
Link,
|
||||
NodeAlign,
|
||||
OrderedList,
|
||||
SelectFile.configure({
|
||||
allowedMimeTypes: []
|
||||
}),
|
||||
Subscript,
|
||||
Superscript,
|
||||
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