]> BookStack Code Mirror - bookstack/blob - resources/js/wysiwyg-tinymce/drop-paste-handling.js
Editors: Added lexical editor for testing
[bookstack] / resources / js / wysiwyg-tinymce / drop-paste-handling.js
1 import {Clipboard} from '../services/clipboard';
2
3 let wrap;
4 let draggedContentEditable;
5
6 function hasTextContent(node) {
7     return node && !!(node.textContent || node.innerText);
8 }
9
10 /**
11  * Upload an image file to the server
12  * @param {File} file
13  * @param {int} pageId
14  */
15 async function uploadImageFile(file, pageId) {
16     if (file === null || file.type.indexOf('image') !== 0) {
17         throw new Error('Not an image file');
18     }
19
20     const remoteFilename = file.name || `image-${Date.now()}.png`;
21     const formData = new FormData();
22     formData.append('file', file, remoteFilename);
23     formData.append('uploaded_to', pageId);
24
25     const resp = await window.$http.post(window.baseUrl('/images/gallery'), formData);
26     return resp.data;
27 }
28
29 /**
30  * Handle pasting images from clipboard.
31  * @param {Editor} editor
32  * @param {WysiwygConfigOptions} options
33  * @param {ClipboardEvent|DragEvent} event
34  */
35 function paste(editor, options, event) {
36     const clipboard = new Clipboard(event.clipboardData || event.dataTransfer);
37
38     // Don't handle the event ourselves if no items exist of contains table-looking data
39     if (!clipboard.hasItems() || clipboard.containsTabularData()) {
40         return;
41     }
42
43     const images = clipboard.getImages();
44     for (const imageFile of images) {
45         const id = `image-${Math.random().toString(16).slice(2)}`;
46         const loadingImage = window.baseUrl('/loading.gif');
47         event.preventDefault();
48
49         setTimeout(() => {
50             editor.insertContent(`<p><img src="${loadingImage}" id="${id}"></p>`);
51
52             uploadImageFile(imageFile, options.pageId).then(resp => {
53                 const safeName = resp.name.replace(/"/g, '');
54                 const newImageHtml = `<img src="${resp.thumbs.display}" alt="${safeName}" />`;
55
56                 const newEl = editor.dom.create('a', {
57                     target: '_blank',
58                     href: resp.url,
59                 }, newImageHtml);
60
61                 editor.dom.replace(newEl, id);
62             }).catch(err => {
63                 editor.dom.remove(id);
64                 window.$events.error(err?.data?.message || options.translations.imageUploadErrorText);
65                 console.error(err);
66             });
67         }, 10);
68     }
69 }
70
71 /**
72  * @param {Editor} editor
73  */
74 function dragStart(editor) {
75     const node = editor.selection.getNode();
76
77     if (node.nodeName === 'IMG') {
78         wrap = editor.dom.getParent(node, '.mceTemp');
79
80         if (!wrap && node.parentNode.nodeName === 'A' && !hasTextContent(node.parentNode)) {
81             wrap = node.parentNode;
82         }
83     }
84
85     // Track dragged contenteditable blocks
86     if (node.hasAttribute('contenteditable') && node.getAttribute('contenteditable') === 'false') {
87         draggedContentEditable = node;
88     }
89 }
90
91 /**
92  * @param {Editor} editor
93  * @param {WysiwygConfigOptions} options
94  * @param {DragEvent} event
95  */
96 function drop(editor, options, event) {
97     const {dom} = editor;
98     const rng = window.tinymce.dom.RangeUtils.getCaretRangeFromPoint(
99         event.clientX,
100         event.clientY,
101         editor.getDoc(),
102     );
103
104     // Template insertion
105     const templateId = event.dataTransfer && event.dataTransfer.getData('bookstack/template');
106     if (templateId) {
107         event.preventDefault();
108         window.$http.get(`/templates/${templateId}`).then(resp => {
109             editor.selection.setRng(rng);
110             editor.undoManager.transact(() => {
111                 editor.execCommand('mceInsertContent', false, resp.data.html);
112             });
113         });
114     }
115
116     // Don't allow anything to be dropped in a captioned image.
117     if (dom.getParent(rng.startContainer, '.mceTemp')) {
118         event.preventDefault();
119     } else if (wrap) {
120         event.preventDefault();
121
122         editor.undoManager.transact(() => {
123             editor.selection.setRng(rng);
124             editor.selection.setNode(wrap);
125             dom.remove(wrap);
126         });
127     }
128
129     // Handle contenteditable section drop
130     if (!event.isDefaultPrevented() && draggedContentEditable) {
131         event.preventDefault();
132         editor.undoManager.transact(() => {
133             const selectedNode = editor.selection.getNode();
134             const range = editor.selection.getRng();
135             const selectedNodeRoot = selectedNode.closest('body > *');
136             if (range.startOffset > (range.startContainer.length / 2)) {
137                 selectedNodeRoot.after(draggedContentEditable);
138             } else {
139                 selectedNodeRoot.before(draggedContentEditable);
140             }
141         });
142     }
143
144     // Handle image insert
145     if (!event.isDefaultPrevented()) {
146         paste(editor, options, event);
147     }
148
149     wrap = null;
150 }
151
152 /**
153  * @param {Editor} editor
154  * @param {DragEvent} event
155  */
156 function dragOver(editor, event) {
157     // This custom handling essentially emulates the default TinyMCE behaviour while allowing us
158     // to specifically call preventDefault on the event to allow the drop of custom elements.
159     event.preventDefault();
160     editor.focus();
161     const rangeUtils = window.tinymce.dom.RangeUtils;
162     const range = rangeUtils.getCaretRangeFromPoint(event.clientX ?? 0, event.clientY ?? 0, editor.getDoc());
163     editor.selection.setRng(range);
164 }
165
166 /**
167  * @param {Editor} editor
168  * @param {WysiwygConfigOptions} options
169  */
170 export function listenForDragAndPaste(editor, options) {
171     editor.on('dragover', event => dragOver(editor, event));
172     editor.on('dragstart', () => dragStart(editor));
173     editor.on('drop', event => drop(editor, options, event));
174     editor.on('paste', event => paste(editor, options, event));
175 }