mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-07-30 04:23:11 +03:00
Lexical: Added drop/paste image handling
This commit is contained in:
158
resources/js/wysiwyg/services/drop-paste-handling.ts
Normal file
158
resources/js/wysiwyg/services/drop-paste-handling.ts
Normal file
@ -0,0 +1,158 @@
|
||||
import {
|
||||
$insertNodes,
|
||||
$isDecoratorNode, COMMAND_PRIORITY_HIGH, DROP_COMMAND,
|
||||
LexicalEditor,
|
||||
LexicalNode, PASTE_COMMAND
|
||||
} from "lexical";
|
||||
import {$insertNewBlockNodesAtSelection, $selectSingleNode} from "../utils/selection";
|
||||
import {$getNearestBlockNodeForCoords, $htmlToBlockNodes} from "../utils/nodes";
|
||||
import {Clipboard} from "../../services/clipboard";
|
||||
import {$createImageNode} from "../nodes/image";
|
||||
import {$createCustomParagraphNode} from "../nodes/custom-paragraph";
|
||||
import {$createLinkNode} from "@lexical/link";
|
||||
import {EditorImageData, uploadImageFile} from "../utils/images";
|
||||
import {EditorUiContext} from "../ui/framework/core";
|
||||
|
||||
function $getNodeFromMouseEvent(event: MouseEvent, editor: LexicalEditor): LexicalNode|null {
|
||||
const x = event.clientX;
|
||||
const y = event.clientY;
|
||||
const dom = document.elementFromPoint(x, y);
|
||||
if (!dom) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $getNearestBlockNodeForCoords(editor, event.clientX, event.clientY);
|
||||
}
|
||||
|
||||
function $insertNodesAtEvent(nodes: LexicalNode[], event: DragEvent, editor: LexicalEditor) {
|
||||
const positionNode = $getNodeFromMouseEvent(event, editor);
|
||||
|
||||
if (positionNode) {
|
||||
$selectSingleNode(positionNode);
|
||||
}
|
||||
|
||||
$insertNewBlockNodesAtSelection(nodes, true);
|
||||
|
||||
if (!$isDecoratorNode(positionNode) || !positionNode?.getTextContent()) {
|
||||
positionNode?.remove();
|
||||
}
|
||||
}
|
||||
|
||||
async function insertTemplateToEditor(editor: LexicalEditor, templateId: string, event: DragEvent) {
|
||||
const resp = await window.$http.get(`/templates/${templateId}`);
|
||||
const data = (resp.data || {html: ''}) as {html: string}
|
||||
const html: string = data.html || '';
|
||||
|
||||
editor.update(() => {
|
||||
const newNodes = $htmlToBlockNodes(editor, html);
|
||||
$insertNodesAtEvent(newNodes, event, editor);
|
||||
});
|
||||
}
|
||||
|
||||
function handleMediaInsert(data: DataTransfer, context: EditorUiContext): boolean {
|
||||
const clipboard = new Clipboard(data);
|
||||
let handled = false;
|
||||
|
||||
// Don't handle the event ourselves if no items exist of contains table-looking data
|
||||
if (!clipboard.hasItems() || clipboard.containsTabularData()) {
|
||||
return handled;
|
||||
}
|
||||
|
||||
const images = clipboard.getImages();
|
||||
if (images.length > 0) {
|
||||
handled = true;
|
||||
}
|
||||
|
||||
context.editor.update(async () => {
|
||||
for (const imageFile of images) {
|
||||
const loadingImage = window.baseUrl('/loading.gif');
|
||||
const loadingNode = $createImageNode(loadingImage);
|
||||
const imageWrap = $createCustomParagraphNode();
|
||||
imageWrap.append(loadingNode);
|
||||
$insertNodes([imageWrap]);
|
||||
|
||||
try {
|
||||
const respData: EditorImageData = await uploadImageFile(imageFile, context.options.pageId);
|
||||
const safeName = respData.name.replace(/"/g, '');
|
||||
context.editor.update(() => {
|
||||
const finalImage = $createImageNode(respData.thumbs?.display || '', {
|
||||
alt: safeName,
|
||||
});
|
||||
const imageLink = $createLinkNode(respData.url, {target: '_blank'});
|
||||
imageLink.append(finalImage);
|
||||
loadingNode.replace(imageLink);
|
||||
});
|
||||
} catch (err: any) {
|
||||
context.editor.update(() => {
|
||||
loadingNode.remove(false);
|
||||
});
|
||||
window.$events.error(err?.data?.message || context.options.translations.imageUploadErrorText);
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return handled;
|
||||
}
|
||||
|
||||
function createDropListener(context: EditorUiContext): (event: DragEvent) => boolean {
|
||||
const editor = context.editor;
|
||||
return (event: DragEvent): boolean => {
|
||||
// Template handling
|
||||
const templateId = event.dataTransfer?.getData('bookstack/template') || '';
|
||||
if (templateId) {
|
||||
insertTemplateToEditor(editor, templateId, event);
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
|
||||
// HTML contents drop
|
||||
const html = event.dataTransfer?.getData('text/html') || '';
|
||||
if (html) {
|
||||
editor.update(() => {
|
||||
const newNodes = $htmlToBlockNodes(editor, html);
|
||||
$insertNodesAtEvent(newNodes, event, editor);
|
||||
});
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (event.dataTransfer) {
|
||||
const handled = handleMediaInsert(event.dataTransfer, context);
|
||||
if (handled) {
|
||||
event.preventDefault();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
function createPasteListener(context: EditorUiContext): (event: ClipboardEvent) => boolean {
|
||||
return (event: ClipboardEvent) => {
|
||||
if (!event.clipboardData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const handled = handleMediaInsert(event.clipboardData, context);
|
||||
if (handled) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
return handled;
|
||||
};
|
||||
}
|
||||
|
||||
export function registerDropPasteHandling(context: EditorUiContext): () => void {
|
||||
const dropListener = createDropListener(context);
|
||||
const pasteListener = createPasteListener(context);
|
||||
|
||||
const unregisterDrop = context.editor.registerCommand(DROP_COMMAND, dropListener, COMMAND_PRIORITY_HIGH);
|
||||
const unregisterPaste = context.editor.registerCommand(PASTE_COMMAND, pasteListener, COMMAND_PRIORITY_HIGH);
|
||||
|
||||
return () => {
|
||||
unregisterDrop();
|
||||
unregisterPaste();
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user