BookStack/resources/js/wysiwyg/services/drop-paste-handling.ts
Dan Brown fd07aa0f05
Lexical: Further fixes
- Improved node resizer positioning to be more accurate
- Fixed drop handling not running within editor margin space
- Made media dom update smarter to reduce reloads
- Fixed media alignment, broken due to added wrapper
2024-09-09 12:28:01 +01:00

163 lines
5.6 KiB
TypeScript

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();
event.stopPropagation();
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();
event.stopPropagation();
return true;
}
if (event.dataTransfer) {
const handled = handleMediaInsert(event.dataTransfer, context);
if (handled) {
event.preventDefault();
event.stopPropagation();
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);
context.scrollDOM.addEventListener('drop', dropListener);
return () => {
unregisterDrop();
unregisterPaste();
context.scrollDOM.removeEventListener('drop', dropListener);
};
}