4 $isDecoratorNode, COMMAND_PRIORITY_HIGH, DROP_COMMAND,
6 LexicalNode, PASTE_COMMAND
8 import {$insertNewBlockNodesAtSelection, $selectSingleNode} from "../utils/selection";
9 import {$getNearestBlockNodeForCoords, $htmlToBlockNodes} from "../utils/nodes";
10 import {Clipboard} from "../../services/clipboard";
11 import {$createImageNode} from "@lexical/rich-text/LexicalImageNode";
12 import {$createLinkNode} from "@lexical/link";
13 import {EditorImageData, uploadImageFile} from "../utils/images";
14 import {EditorUiContext} from "../ui/framework/core";
16 function $getNodeFromMouseEvent(event: MouseEvent, editor: LexicalEditor): LexicalNode|null {
17 const x = event.clientX;
18 const y = event.clientY;
19 const dom = document.elementFromPoint(x, y);
24 return $getNearestBlockNodeForCoords(editor, event.clientX, event.clientY);
27 function $insertNodesAtEvent(nodes: LexicalNode[], event: DragEvent, editor: LexicalEditor) {
28 const positionNode = $getNodeFromMouseEvent(event, editor);
31 $selectSingleNode(positionNode);
34 $insertNewBlockNodesAtSelection(nodes, true);
36 if (!$isDecoratorNode(positionNode) || !positionNode?.getTextContent()) {
37 positionNode?.remove();
41 async function insertTemplateToEditor(editor: LexicalEditor, templateId: string, event: DragEvent) {
42 const resp = await window.$http.get(`/templates/${templateId}`);
43 const data = (resp.data || {html: ''}) as {html: string}
44 const html: string = data.html || '';
47 const newNodes = $htmlToBlockNodes(editor, html);
48 $insertNodesAtEvent(newNodes, event, editor);
52 function handleMediaInsert(data: DataTransfer, context: EditorUiContext): boolean {
53 const clipboard = new Clipboard(data);
56 // Don't handle the event ourselves if no items exist of contains table-looking data
57 if (!clipboard.hasItems() || clipboard.containsTabularData()) {
61 const images = clipboard.getImages();
62 if (images.length > 0) {
66 context.editor.update(async () => {
67 for (const imageFile of images) {
68 const loadingImage = window.baseUrl('/loading.gif');
69 const loadingNode = $createImageNode(loadingImage);
70 const imageWrap = $createParagraphNode();
71 imageWrap.append(loadingNode);
72 $insertNodes([imageWrap]);
75 const respData: EditorImageData = await uploadImageFile(imageFile, context.options.pageId);
76 const safeName = respData.name.replace(/"/g, '');
77 context.editor.update(() => {
78 const finalImage = $createImageNode(respData.thumbs?.display || '', {
81 const imageLink = $createLinkNode(respData.url, {target: '_blank'});
82 imageLink.append(finalImage);
83 loadingNode.replace(imageLink);
86 context.editor.update(() => {
87 loadingNode.remove(false);
89 window.$events.error(err?.data?.message || context.options.translations.imageUploadErrorText);
98 function handleImageLinkInsert(data: DataTransfer, context: EditorUiContext): boolean {
99 const regex = /https?:\/\/([^?#]*?)\.(png|jpeg|jpg|gif|webp|bmp|avif)/i
100 const text = data.getData('text/plain');
101 if (text && regex.test(text)) {
102 context.editor.update(() => {
103 const image = $createImageNode(text);
104 $insertNodes([image]);
113 function createDropListener(context: EditorUiContext): (event: DragEvent) => boolean {
114 const editor = context.editor;
115 return (event: DragEvent): boolean => {
117 const templateId = event.dataTransfer?.getData('bookstack/template') || '';
119 insertTemplateToEditor(editor, templateId, event);
120 event.preventDefault();
121 event.stopPropagation();
125 // HTML contents drop
126 const html = event.dataTransfer?.getData('text/html') || '';
128 editor.update(() => {
129 const newNodes = $htmlToBlockNodes(editor, html);
130 $insertNodesAtEvent(newNodes, event, editor);
132 event.preventDefault();
133 event.stopPropagation();
137 if (event.dataTransfer) {
138 const handled = handleMediaInsert(event.dataTransfer, context);
140 event.preventDefault();
141 event.stopPropagation();
150 function createPasteListener(context: EditorUiContext): (event: ClipboardEvent) => boolean {
151 return (event: ClipboardEvent) => {
152 if (!event.clipboardData) {
157 handleImageLinkInsert(event.clipboardData, context) ||
158 handleMediaInsert(event.clipboardData, context);
161 event.preventDefault();
168 export function registerDropPasteHandling(context: EditorUiContext): () => void {
169 const dropListener = createDropListener(context);
170 const pasteListener = createPasteListener(context);
172 const unregisterDrop = context.editor.registerCommand(DROP_COMMAND, dropListener, COMMAND_PRIORITY_HIGH);
173 const unregisterPaste = context.editor.registerCommand(PASTE_COMMAND, pasteListener, COMMAND_PRIORITY_HIGH);
174 context.scrollDOM.addEventListener('drop', dropListener);
179 context.scrollDOM.removeEventListener('drop', dropListener);