[FIX] web_editor: properly drop images in editor

This addresses issues with image dropping and pasting.

1. The code to compute the position of the drop was not always run so
that resulted in a sometimes wrong position (dropping at current range
instead).
2. Images dropped internally within a same document should not be
converted to base64.
3. Dropping an image should apply to it the `img-fluid` class so as not
to break the document's layout.

task-2954499

closes odoo/odoo#105137

X-original-commit: 9c258bf18c17a55f1d22c8a7d1b74f5dd51ee73d
Signed-off-by: David Monjoie (dmo) <dmo@odoo.com>
This commit is contained in:
Antoine Guenet
2022-11-07 13:50:10 +01:00
parent 6509afcae5
commit 82a2b7ff15
@@ -66,7 +66,7 @@ import {
hasTableSelection,
pxToFloat,
parseHTML,
splitTextNode,
splitTextNode
} from './utils/utils.js';
import { editorCommands } from './commands/commands.js';
import { Powerbox } from './powerbox/Powerbox.js';
@@ -185,15 +185,9 @@ function defaultOptions(defaultObject, object) {
return newObject;
}
function getImageFiles(dataTransfer) {
let files;
if (!dataTransfer.items) {
files = [...dataTransfer.items]
.filter(item => item.kind === 'file' && item.type.includes('image/'))
.map((item) => item.getAsFile());
} else {
files = [...dataTransfer.files];
}
return files || [];
return [...dataTransfer.items]
.filter(item => item.kind === 'file' && item.type.includes('image/'))
.map((item) => item.getAsFile());
}
function getImageUrl (file) {
return new Promise((resolve, reject) => {
@@ -588,6 +582,7 @@ export class OdooEditor extends EventTarget {
this.addDomListener(this.editable, 'mouseup', this._onMouseup);
this.addDomListener(this.editable, 'mousemove', this._onMousemove);
this.addDomListener(this.editable, 'paste', this._onPaste);
this.addDomListener(this.editable, 'dragstart', this._onDragStart);
this.addDomListener(this.editable, 'drop', this._onDrop);
this.addDomListener(this.document, 'copy', this._onClipboardCopy);
@@ -3934,15 +3929,18 @@ export class OdooEditor extends EventTarget {
* @param {File[]} imageFiles
*/
addImagesFiles(imageFiles) {
const promises = [];
for (const imageFile of imageFiles) {
const imageNode = document.createElement('img');
imageNode.style.width = '100%';
imageNode.classList.add('img-fluid');
imageNode.dataset.fileName = imageFile.name;
getImageUrl(imageFile).then((url)=> {
promises.push(getImageUrl(imageFile).then(url => {
imageNode.src = url;
this.execCommand('insert', imageNode);
});
return imageNode.outerHTML;
}));
}
return Promise.all(promises).then(html => html.join(''));
}
/**
* Handle safe pasting of html or plain text into the editor.
@@ -3972,10 +3970,10 @@ export class OdooEditor extends EventTarget {
if (fragment.hasChildNodes()) {
this.execCommand('insert', fragment);
}
} else if (files.length) {
this.addImagesFiles(files).then(html => this.execCommand('insert', this._prepareClipboardData(html)));
} else if (clipboardHtml) {
this.execCommand('insert', this._prepareClipboardData(clipboardHtml));
} else if (files.length) {
this.addImagesFiles(files);
} else {
const text = ev.clipboardData.getData('text/plain');
let splitAroundUrl = [text];
@@ -4143,18 +4141,17 @@ export class OdooEditor extends EventTarget {
this.historyStep();
}
}
_onDragStart(ev) {
if (ev.target.nodeName === 'IMG') {
ev.dataTransfer.setData('text/plain', `oid:${ev.target.oid}`);
}
}
/**
* Handle safe dropping of html into the editor.
*/
_onDrop(ev) {
ev.preventDefault();
const imageFiles = getImageFiles(ev.dataTransfer);
if (imageFiles.length) {
this.addImagesFiles(imageFiles);
return;
}
const sel = this.document.getSelection();
let isInEditor = false;
let ancestor = sel.anchorNode;
@@ -4164,21 +4161,36 @@ export class OdooEditor extends EventTarget {
}
ancestor = ancestor.parentNode;
}
const transferItem = [...(ev.originalEvent || ev).dataTransfer.items].find(
const dataTransfer = (ev.originalEvent || ev).dataTransfer;
const imageOidMatch = (dataTransfer.getData('text') || '').match('oid:(.*)');
const imageOid = imageOidMatch && imageOidMatch[1];
const image = imageOid && [...this.editable.querySelectorAll('*')].find(
node => node.oid === imageOid,
);
const fileTransferItems = getImageFiles(dataTransfer);
const htmlTransferItem = [...dataTransfer.items].find(
item => item.type === 'text/html',
);
if (transferItem) {
transferItem.getAsString(pastedText => {
if (isInEditor && !sel.isCollapsed) {
this.deleteRange(sel);
}
if (this.document.caretPositionFromPoint) {
const range = this.document.caretPositionFromPoint(ev.clientX, ev.clientY);
setSelection(range.offsetNode, range.offset);
} else if (this.document.caretRangeFromPoint) {
const range = this.document.caretRangeFromPoint(ev.clientX, ev.clientY);
setSelection(range.startContainer, range.startOffset);
}
if (image || fileTransferItems.length || htmlTransferItem) {
if (this.document.caretPositionFromPoint) {
const range = this.document.caretPositionFromPoint(ev.clientX, ev.clientY);
setSelection(range.offsetNode, range.offset);
} else if (this.document.caretRangeFromPoint) {
const range = this.document.caretRangeFromPoint(ev.clientX, ev.clientY);
setSelection(range.startContainer, range.startOffset);
}
}
if (image) {
image.classList.toggle('img-fluid', true);
const html = image.outerHTML;
image.remove();
this.execCommand('insert', this._prepareClipboardData(html));
} else if (fileTransferItems.length) {
this.addImagesFiles(fileTransferItems).then(html => {
this.execCommand('insert', this._prepareClipboardData(html));
});
} else if (htmlTransferItem) {
htmlTransferItem.getAsString(pastedText => {
this.execCommand('insert', this._prepareClipboardData(pastedText));
});
}