[FIX] web_editor: update summernote copy-paste feature

The copy-paste feature of summernote was messy and led most of the time
to breaking the template you were trying to edit. While it is difficult
to solve as it mostly comes from the lib itself and that the lib is in
a complicated state as Odoo had to overridde part of its internal code
for its own purposes, this commit hopes to solve the problem by updating
only the copy-paste feature to the latest summernote version.

Of course, as summernote changed a lot meanwhile, it was not possible to
just take the up-to-date summernote file. This commit refactores the
up-to-date summernote clipboard code so that it fits the old summernote
version Odoo is using.

Note: the main problems occurred while using Windows/Mac and not Linux.

opw-1837818
This commit is contained in:
qsm-odoo
2018-04-27 12:28:57 +02:00
parent 409970c188
commit ce6f8632cc
@@ -1,256 +1,41 @@
define([
'summernote/core/list',
'summernote/core/dom',
'summernote/core/key',
'summernote/core/agent',
'summernote/core/range'
'summernote/core/list',
'summernote/core/dom',
'summernote/core/key',
'summernote/core/agent',
'summernote/core/range'
], function (list, dom, key, agent, range) {
var Clipboard = function (handler) {
var $paste;
// ODOO override: use 0.8.10 version of this, adapted for the old summernote
// version odoo is using
var Clipboard = function (handler) {
/**
* paste by clipboard event
*
* @param {Event} event
*/
var pasteByEvent = function (event) {
if (["INPUT", "TEXTAREA"].indexOf(event.target.tagName) !== -1) {
// ODOO override: from old summernote version
return;
}
this.attach = function (layoutInfo) {
// [workaround] getting image from clipboard
// - IE11 and Firefox: CTRL+v hook
// - Webkit: event.clipboardData
if (agent.isMSIE && agent.browserVersion > 10) {
$paste = $('<div />').attr('contenteditable', true).css({
position : 'absolute',
left : -100000,
opacity : 0
});
var clipboardData = event.originalEvent.clipboardData;
var layoutInfo = dom.makeLayoutInfo(event.currentTarget || event.target);
var $editable = layoutInfo.editable();
layoutInfo.editable().on('keydown', function (e) {
if (e.ctrlKey && e.keyCode === key.code.V) {
handler.invoke('saveRange', layoutInfo.editable());
$paste.focus();
if (clipboardData && clipboardData.items && clipboardData.items.length) {
var item = list.head(clipboardData.items);
if (item.kind === 'file' && item.type.indexOf('image/') !== -1) {
handler.insertImages(layoutInfo, [item.getAsFile()]);
}
handler.invoke('editor.afterCommand', $editable);
}
};
setTimeout(function () {
pasteByHook(layoutInfo);
}, 0);
}
});
layoutInfo.editable().before($paste);
} else {
layoutInfo.editable().on('paste', pasteByEvent);
}
this.attach = function (layoutInfo) {
layoutInfo.editable().on('paste', pasteByEvent);
};
};
var pasteByHook = function (layoutInfo) {
var $editable = layoutInfo.editable();
var node = $paste[0].firstChild;
if (dom.isImg(node)) {
var dataURI = node.src;
var decodedData = atob(dataURI.split(',')[1]);
var array = new Uint8Array(decodedData.length);
for (var i = 0; i < decodedData.length; i++) {
array[i] = decodedData.charCodeAt(i);
}
var blob = new Blob([array], { type : 'image/png' });
blob.name = 'clipboard.png';
handler.invoke('restoreRange', $editable);
handler.invoke('focus', $editable);
handler.insertImages(layoutInfo, [blob]);
} else {
var pasteContent = $('<div />').html($paste.html()).html();
handler.invoke('restoreRange', $editable);
handler.invoke('focus', $editable);
if (pasteContent) {
handler.invoke('pasteHTML', $editable, pasteContent);
}
}
$paste.empty();
};
/**
* paste by clipboard event
*
* @param {Event} event
*/
var pasteByEvent = function (event) {
var clipboardData = event.originalEvent.clipboardData;
var layoutInfo = dom.makeLayoutInfo(event.currentTarget || event.target);
var $editable = layoutInfo.editable();
if (["INPUT", "TEXTAREA"].indexOf(event.target.tagName) !== -1) {
return;
}
if (clipboardData && clipboardData.items && clipboardData.items.length) {
var item = list.head(clipboardData.items);
if (item.kind === 'file' && item.type.indexOf('image/') !== -1) {
handler.insertImages(layoutInfo, [item.getAsFile()]);
}
handler.invoke('editor.afterCommand', $editable);
}
event.preventDefault();
var html = clipboardData.getData("text/html");
var $node = $('<div/>').html(html);
// if copying source did not provide html, default to plain text
if(!html) {
$node.text(clipboardData.getData("text/plain")).html(function(_, html){
return html.replace(/\r?\n/g,'<br>');
});
}
pasteContent($node, layoutInfo, $editable);
};
/*
remove undesirable tag
filter classes and style attributes
remove undesirable attributes
*/
var filter_tag = function ($nodes, $editable) {
return $nodes.each(function() {
var $node = $(this);
if ($node.attr('style')) {
var style = _.filter(_.compact($node.attr('style').split(/\s*;\s*/)), function (style) {
style = style.split(/\s*:\s*/);
return /width|height|color|background-color|font-weight|text-align|font-style|text-decoration/i.test(style[0]) &&
!(style[1] === 'initial' || style[1] === 'inherit' || $node.css(style[0]) === $editable.css(style[0]) ||
(style[0] === 'background-color' && style[1] === 'rgb(255, 255, 255)') ||
(style[0] === 'color' && style[1] === 'rgb(0, 0, 0)'));
}).join(';');
if (style.length) {
$node.attr('style', style);
} else {
$node.removeAttr('style');
}
}
if ($node.attr('class')) {
var classes = _.filter($node.attr('class').split(/\s+/), function (style) {
return /(^|\s)(fa|pull|text|bg)(\s|-|$)/.test(style);
}).join(' ');
if (classes.length) {
$node.attr('class', classes);
} else {
$node.removeAttr('class');
}
}
});
};
var pasteContent = function ($node, layoutInfo, $editable) {
$node.find('meta, script, style').remove();
filter_tag($node.find('*'), $editable).removeAttr('title', 'alt', 'id', 'contenteditable');
/*
remove unless span and unwant font
*/
$node.find('span, font').filter(':not([class]):not([style])').each(function () {
$(this).replaceWith($(this).contents());
});
$node.find('span + span').each(function () {
if (dom.isText(this.previousSibling)) {
if (dom.isVisibleText(this.previousSibling)) {
return;
} else { // keep space between 2 tags, but can merge the both tags
$(this).prev().append(this.previousSibling);
}
}
if ($(this).attr('class') === $(this).prev().attr('class') && $(this).attr('style') === $(this).prev().attr('style')) {
$(this).prev().append($(this).contents());
$(this).remove();
}
});
// remove empty table row and td
var $tdr;
while(($tdr = $node.find('tr:empty, td:empty, th:empty, tbody:empty, t-head:empty, table:empty')) && $tdr.length) {
$tdr.remove();
}
/*
reset architecture HTML node and add <p> tag
*/
var $arch = $('<div/>');
var $last = $arch;
$node.contents().each(function () {
if (dom.isBR(this)) {
$(this).remove();
$last = $('<p/>');
$arch.append($last);
} else if (/h[0-9]+|li|table|p/i.test(this.tagName)) {
$last = $('<p/>');
$arch.append(this).append($last);
} else if ($arch.is(':empty') && dom.isText(this)) {
$last = $('<p/>').append(this);
$arch.append($last);
} else if (this.nodeType !== Node.COMMENT_NODE) {
$last.append(this);
}
});
$arch.find(':not([class]):not([style]):empty, p:empty').remove();
/*
history
*/
$editable.data('NoteHistory').recordUndo($editable, "paste");
/*
remove selected content
*/
var r = range.create();
if (!r.isCollapsed()) {
r = r.deleteContents();
r.select();
}
// If only pasting a <p/> element in an unique <p/> element, only paste
// the <p/> element text
var $p = $arch.children('p');
var onlyAP = ($p.length === 1 && $arch.children().length === 1);
if (onlyAP) {
var $p1 = $(r.sc).closest('p');
var $p2 = $(r.ec).closest('p');
if ($p1.length && $p2.length && $p1[0] === $p2[0]) {
$arch.html($p.text());
}
}
/*
insert content
*/
var $nodes = $();
$editable.on('DOMNodeInserted', function (event) {
$nodes = $nodes.add(event.originalEvent.target);
});
window.document.execCommand('insertHTML', false, $arch.html());
$editable.off('DOMNodeInserted');
/*
clean insterted content
*/
var $span = $nodes.filter('span');
$span = $span.first().add($span.last());
$span = $span.add($span.prev('span'));
$span = $span.add($span.next('span'));
filter_tag($span, $editable);
$span.not('[span], [style]').each(function () {
_.each(this.childNodes, function (node) {
$(node.parentNode).after(node);
});
$(this).remove();
});
r = range.create();
if (!dom.isText(r.ec)) {
r = range.create(r.sc.childNodes[r.so], dom.nodeLength(r.sc.childNodes[r.so]));
}
r.clean().select();
$editable.trigger('content_changed');
};
};
return Clipboard;
return Clipboard;
});