odoo.define('web_editor.wysiwyg.plugin.unbreakable', function (require) {
'use strict';
var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
var registry = require('web_editor.wysiwyg.plugin.registry');
var dom = $.summernote.dom;
//--------------------------------------------------------------------------
// unbreakable node preventing editing
//--------------------------------------------------------------------------
var Unbreakable = AbstractPlugin.extend({
events: {
'wysiwyg.range .note-editable': '_onRange',
'summernote.mouseup': '_onMouseUp',
'summernote.keyup': '_onKeyup',
'summernote.keydown': '_onKeydown',
// 'summernote.focusnode': '_onFocusnode', => add this event to summernote.
},
initialize: function () {
var self = this;
this._super.apply(this, arguments);
setTimeout(function () {
self.secureArea();
self.context.invoke('HistoryPlugin.clear');
});
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Change the selection if it breaks an unbreakable node:
*
* .. code-block:: xml
*
content_1
content_2
content_3
content_4
content_5
content_6
content_7
content_8
content_9
content_10
content_11
content_12
* .. code-block: none
START END RESIZE START RESIZE END
content_1 content_1 content_3 content_3 (find the first allowed node)
content_1 content_2 content_3 content_3
content_1 content_3 content_3 -
content_3 content_3 - - (nothing to do)
content_3 content_8 - - (can remove unbreakable node)
content_3 content_4 - content_3
content_3 content_5 - #d (can select the entire unbreakable node)
content_5 content_8 content_6 content_6
content_5 content_7 #e #e (select the entire unbreakable node)
content_6 content_8 - content_6
content_7 content_8 - content_8
content_9 content_12 content_10 -
*
* @returns {WrappedRange}
*/
secureRange: function () {
var self = this;
var range = this.context.invoke('editor.createRange');
var isCollapsed = range.isCollapsed();
var needReselect = false;
var startPoint = range.getStartPoint();
var endPoint = range.getEndPoint();
// don't change the selection if the carret is just after a media in editable area
var prev;
if (
isCollapsed && startPoint.node.tagName && startPoint.node.childNodes[startPoint.offset] &&
(prev = dom.prevPoint(startPoint)) && dom.isMedia(prev.node) &&
this.options.isEditableNode(prev.node.parentNode)
) {
return range;
}
// move the start selection to an allowed node
var target = startPoint.node.childNodes[startPoint.offset] || startPoint.node;
if (startPoint.offset && startPoint.offset === dom.nodeLength(startPoint.node)) {
startPoint.node = this.context.invoke('HelperPlugin.lastLeaf', startPoint.node);
startPoint.offset = dom.nodeLength(startPoint.node);
}
if (!dom.isMedia(target) || !this.options.isEditableNode(target)) {
var afterEnd = false;
startPoint = dom.nextPointUntil(startPoint, function (point) {
if (point.node === endPoint.node && point.offset === endPoint.offset) {
afterEnd = true;
}
return self.options.isEditableNode(point.node) && dom.isVisiblePoint(point) || !point.node;
});
if (!startPoint || !startPoint.node) { // no allowed node, search the other way
afterEnd = false;
startPoint = dom.prevPointUntil(range.getStartPoint(), function (point) {
return self.options.isEditableNode(point.node) && dom.isVisiblePoint(point) || !point.node;
});
}
if (startPoint && !startPoint.node) {
startPoint = null;
}
if (afterEnd) {
isCollapsed = true;
}
}
if (startPoint && (startPoint.node !== range.sc || startPoint.offset !== range.so)) {
needReselect = true;
range.sc = startPoint.node;
range.so = startPoint.offset;
if (isCollapsed) {
range.ec = range.sc;
range.eo = range.so;
}
}
if (startPoint && !isCollapsed) { // mouse selection or key selection with shiftKey
var point = endPoint;
endPoint = false;
// if the start point was moved after the end point
var toCollapse = !dom.prevPointUntil(point, function (point) {
return point.node === range.sc && point.offset === range.so;
});
if (!toCollapse) {
// find the first allowed ancestor
var commonUnbreakableParent = dom.ancestor(range.sc, function (node) {
return !dom.isMedia(node) && self.options.isUnbreakableNode(node);
});
if (!commonUnbreakableParent) {
commonUnbreakableParent = this.editable;
}
var lastCheckedNode;
if (point.offset === dom.nodeLength(point.node)) {
point = dom.nextPoint(point);
}
// move the end selection to an allowed node in the first allowed ancestor
endPoint = dom.prevPointUntil(point, function (point) {
if (point.node === range.sc && point.offset === range.so) {
return true;
}
if (lastCheckedNode === point.node) {
return false;
}
// select the entirety of the unbreakable node
if (
point.node.tagName && point.offset &&
$.contains(commonUnbreakableParent, point.node) &&
self.options.isUnbreakableNode(point.node)
) {
return true;
}
var unbreakableParent = dom.ancestor(point.node, function (node) {
return !dom.isMedia(node) && self.options.isUnbreakableNode(node);
});
if (!unbreakableParent) {
unbreakableParent = self.editable;
}
if (commonUnbreakableParent !== unbreakableParent) {
lastCheckedNode = point.node;
return false;
}
lastCheckedNode = point.node;
if (!self.options.isEditableNode(point.node)) {
return false;
}
if (
(/\S|\u200B|\u00A0/.test(point.node.textContent) ||
dom.isMedia(point.node)) &&
dom.isVisiblePoint(point)
) {
return true;
}
if (dom.isText(point.node)) {
lastCheckedNode = point.node;
}
return false;
});
}
if (!endPoint) {
endPoint = range.getStartPoint();
}
if (endPoint.node !== range.ec || endPoint.offset !== range.eo) {
needReselect = true;
range.ec = endPoint.node;
range.eo = endPoint.offset;
}
}
if (needReselect) {
range = range.select();
this.context.invoke('editor.saveRange');
}
return range;
},
/**
* Apply contentEditable false on all media.
*
* @param {DOM} [node] default is editable area
*/
secureArea: function (node) {
this.$editable.find('o_not_editable').attr('contentEditable', 'false');
var medias = (function findMedia(node) {
var medias = [];
if (node.tagName !== 'IMG' && dom.isMedia(node)) {
medias.push(node);
} else {
$(node.childNodes).each(function () {
if (this.tagName) {
medias.push.apply(medias, findMedia(this));
}
});
}
return medias;
})(node || this.editable);
$(medias).addClass('o_fake_not_editable').attr('contentEditable', 'false');
$(medias).each(function () {
if (dom.isVideo(this) && !$(this).children('.o_fake_editable').length) {
// allow char insertion
$(this).prepend('
');
$(this).append('');
}
});
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Trigger a focusnode event when the focus enters another node.
*
* @param {DOM} node
*/
_focusNode: function (node) {
if (!node.tagName) {
node = node.parentNode;
}
if (this._focusedNode !== node) {
this._focusedNode = node;
this.context.triggerEvent('focusnode', node);
}
},
//--------------------------------------------------------------------------
// Handler
//--------------------------------------------------------------------------
/**
* Method called on wysiwyg.range event on the editable: secures the range, refocuses.
*/
_onRange: function () {
var range = this.secureRange();
this._focusNode(range.sc);
},
/**
* Method called on mouseup event: secures the range, refocuses.
*/
_onMouseUp: function () {
var range = this.secureRange();
this._focusNode(range.ec);
},
/**
* Method called on keydown event: prevents changes to unbreakable nodes.
*
* @param {SummernoteEvent} se
* @param {jQueryEvent} e
*/
_onKeydown: function (se, e) {
if (!e.key || (e.key.length !== 1 && e.keyCode !== 8 && e.keyCode !== 46)) {
return;
}
var range;
// for test tour, to trigger Keydown on target (instead of use Wysiwyg.setRange)
if (
e.target !== this._focusedNode &&
(this.editable === e.target || $.contains(this.editable, e.target))
) {
range = this.context.invoke('editor.createRange');
if (!$.contains(e.target, range.sc) && !$.contains(e.target, range.ec)) {
range = this.context.invoke('editor.setRange', e.target, 0);
range = range.normalize().select();
this.context.invoke('editor.saveRange');
this._focusNode(range.ec);
}
}
// rerange to prevent some edition.
// eg: if the user select with arraw and shifKey and keypress an other char
range = this.secureRange();
var target = range.getStartPoint();
if (e.keyCode === 8) { // backspace
if (!target || this.options.isUnbreakableNode(target.node)) {
e.preventDefault();
}
} else if (e.keyCode === 46) { // delete
target = dom.nextPointUntil(dom.nextPoint(target), dom.isVisiblePoint);
if (!target || this.options.isUnbreakableNode(target.node)) {
e.preventDefault();
}
}
},
/**
* Method called on keyup event: prevents selection of unbreakable nodes.
*
* @param {SummernoteEvent} se
* @param {jQueryEvent} se
*/
_onKeyup: function (se, e) {
if (e.keyCode < 37 || e.keyCode > 40) {
return;
}
var range;
if (e.keyCode === 37) { // left
range = this.secureRange();
this._focusNode(range.sc);
} else if (e.keyCode === 39) { // right
range = this.secureRange();
this._focusNode(range.ec);
} else if (e.keyCode === 38) { // up
range = this.secureRange();
this._focusNode(range.sc);
} else { // down
range = this.secureRange();
this._focusNode(range.ec);
}
},
});
registry.add('UnbreakablePlugin', Unbreakable);
return Unbreakable;
});