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; });