resources/js/wysiwyg-tinymce/drop-paste-handling.js
import {Clipboard} from '../services/clipboard.ts';
let wrap;
let draggedContentEditable;
function hasTextContent(node) {
return node && !!(node.textContent || node.innerText);
}
/**
* Upload an image file to the server
* @param {File} file
* @param {int} pageId
*/
async function uploadImageFile(file, pageId) {
if (file === null || file.type.indexOf('image') !== 0) {
throw new Error('Not an image file');
}
const remoteFilename = file.name || `image-${Date.now()}.png`;
const formData = new FormData();
formData.append('file', file, remoteFilename);
formData.append('uploaded_to', pageId);
const resp = await window.$http.post(window.baseUrl('/images/gallery'), formData);
return resp.data;
}
/**
* Handle pasting images from clipboard.
* @param {Editor} editor
* @param {WysiwygConfigOptions} options
* @param {ClipboardEvent|DragEvent} event
*/
function paste(editor, options, event) {
const clipboard = new Clipboard(event.clipboardData || event.dataTransfer);
// Don't handle the event ourselves if no items exist of contains table-looking data
if (!clipboard.hasItems() || clipboard.containsTabularData()) {
return;
}
const images = clipboard.getImages();
for (const imageFile of images) {
const id = `image-${Math.random().toString(16).slice(2)}`;
const loadingImage = window.baseUrl('/loading.gif');
event.preventDefault();
setTimeout(() => {
editor.insertContent(`<p><img src="${loadingImage}" id="${id}"></p>`);
uploadImageFile(imageFile, options.pageId).then(resp => {
const safeName = resp.name.replace(/"/g, '');
const newImageHtml = `<img src="${resp.thumbs.display}" alt="${safeName}" />`;
const newEl = editor.dom.create('a', {
target: '_blank',
href: resp.url,
}, newImageHtml);
editor.dom.replace(newEl, id);
}).catch(err => {
editor.dom.remove(id);
window.$events.error(err?.data?.message || options.translations.imageUploadErrorText);
console.error(err);
});
}, 10);
}
}
/**
* @param {Editor} editor
*/
function dragStart(editor) {
const node = editor.selection.getNode();
if (node.nodeName === 'IMG') {
wrap = editor.dom.getParent(node, '.mceTemp');
if (!wrap && node.parentNode.nodeName === 'A' && !hasTextContent(node.parentNode)) {
wrap = node.parentNode;
}
}
// Track dragged contenteditable blocks
if (node.hasAttribute('contenteditable') && node.getAttribute('contenteditable') === 'false') {
draggedContentEditable = node;
}
}
/**
* @param {Editor} editor
* @param {WysiwygConfigOptions} options
* @param {DragEvent} event
*/
function drop(editor, options, event) {
const {dom} = editor;
const rng = window.tinymce.dom.RangeUtils.getCaretRangeFromPoint(
event.clientX,
event.clientY,
editor.getDoc(),
);
// Template insertion
const templateId = event.dataTransfer && event.dataTransfer.getData('bookstack/template');
if (templateId) {
event.preventDefault();
window.$http.get(`/templates/${templateId}`).then(resp => {
editor.selection.setRng(rng);
editor.undoManager.transact(() => {
editor.execCommand('mceInsertContent', false, resp.data.html);
});
});
}
// Don't allow anything to be dropped in a captioned image.
if (dom.getParent(rng.startContainer, '.mceTemp')) {
event.preventDefault();
} else if (wrap) {
event.preventDefault();
editor.undoManager.transact(() => {
editor.selection.setRng(rng);
editor.selection.setNode(wrap);
dom.remove(wrap);
});
}
// Handle contenteditable section drop
if (!event.isDefaultPrevented() && draggedContentEditable) {
event.preventDefault();
editor.undoManager.transact(() => {
const selectedNode = editor.selection.getNode();
const range = editor.selection.getRng();
const selectedNodeRoot = selectedNode.closest('body > *');
if (range.startOffset > (range.startContainer.length / 2)) {
selectedNodeRoot.after(draggedContentEditable);
} else {
selectedNodeRoot.before(draggedContentEditable);
}
});
}
// Handle image insert
if (!event.isDefaultPrevented()) {
paste(editor, options, event);
}
wrap = null;
}
/**
* @param {Editor} editor
* @param {DragEvent} event
*/
function dragOver(editor, event) {
// This custom handling essentially emulates the default TinyMCE behaviour while allowing us
// to specifically call preventDefault on the event to allow the drop of custom elements.
event.preventDefault();
editor.focus();
const rangeUtils = window.tinymce.dom.RangeUtils;
const range = rangeUtils.getCaretRangeFromPoint(event.clientX ?? 0, event.clientY ?? 0, editor.getDoc());
editor.selection.setRng(range);
}
/**
* @param {Editor} editor
* @param {WysiwygConfigOptions} options
*/
export function listenForDragAndPaste(editor, options) {
editor.on('dragover', event => dragOver(editor, event));
editor.on('dragstart', () => dragStart(editor));
editor.on('drop', event => drop(editor, options, event));
editor.on('paste', event => paste(editor, options, event));
}