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