summaryrefslogtreecommitdiff
path: root/addons/web_editor/static/src/js/wysiwyg/plugin
diff options
context:
space:
mode:
authorGorash <chm@odoo.com>2019-08-20 06:23:06 +0000
committerDavid Monjoie <dmo@odoo.com>2019-08-21 18:52:36 +0000
commit35b61822a8b12ebbdb455495479df6f2e13db03e (patch)
tree999c382ea06b53b8da8d7db2d82e671d831dd856 /addons/web_editor/static/src/js/wysiwyg/plugin
parentf14cecfc0a7f401533213aaa968ee233e19d1325 (diff)
[REV] wev_editor, website: revert saas-12.2 editor
This commit reverts the saas-12.2 editor by putting back the editor of Odoo 12. The saas-12.2 editor was an intermediate work between the previous editor of Odoo 12 and the new one slated for Odoo 13. It was unstable but it was supposed to be replaced by the new editor of version 13. However, since the new editor has been postponed to Odoo 14, the saas-12.2 one would have been staying in Odoo 13, which would have been a nightmare to maintain. To avoid this outcome, we chose to put back the 12.0 editor in its place. This has the particular advantage that Odoo 13 will share the same bugfixes as Odoo 12 and 11 as they all run under the same core editor, while the saas-12.2 one would have been an entirely different beast to maintain. This is a partial revert of f2969923. Part of PR 35677. Co-authored-by: Nicolas Bayet <nby@odoo.com> Co-authored-by: Antoine Guenet <age@odoo.com> Co-authored-by: Christophe Matthieu <chm@odoo.com> Co-authored-by: David Monjoie <dmo@odoo.com>
Diffstat (limited to 'addons/web_editor/static/src/js/wysiwyg/plugin')
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/abstract.js222
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/bullet.js571
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/buttons.js88
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/codeview.js60
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/dropzone.js229
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/editor.js189
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/font.js567
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/font_buttons.js31
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/help_dialog.js27
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/helper.js1948
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/hint.js139
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/history.js87
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/keyboard.js1237
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/link.js493
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/media.js1288
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/plugins.js78
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/table.js244
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/text.js811
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/toolbar.js57
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/transform.js85
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/plugin/unbreakable.js364
21 files changed, 0 insertions, 8815 deletions
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/abstract.js b/addons/web_editor/static/src/js/wysiwyg/plugin/abstract.js
deleted file mode 100644
index de776020a42..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/abstract.js
+++ /dev/null
@@ -1,222 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.abstract', function (require) {
-'use strict';
-
-var Class = require('web.Class');
-var mixins = require('web.mixins');
-var ServicesMixin = require('web.ServicesMixin');
-var wysiwygTranslation = require('web_editor.wysiwyg.translation');
-var wysiwygOptions = require('web_editor.wysiwyg.options');
-
-//--------------------------------------------------------------------------
-// AbstractPlugin for summernote module API
-//--------------------------------------------------------------------------
-
-var AbstractPlugin = Class.extend(mixins.EventDispatcherMixin, ServicesMixin).extend({
- /**
- * Use this prop if you want to extend a summernote plugin.
- */
- init: function (context) {
- var self = this;
- this._super.apply(this, arguments);
- this.setParent(context.options.parent);
- this.context = context;
-
- if (!this.context.invoke) {
- // for use outside of wysiwyg/summernote
- this.context.invoke = function () {};
- }
-
- this.$editable = context.layoutInfo.editable;
- this.editable = this.$editable[0];
- this.document = this.editable.ownerDocument;
- this.window = this.document.defaultView;
- this.summernote = this.window._summernoteSlave || $.summernote; // if the target is in iframe
- this.ui = this.summernote.ui;
- this.$editingArea = context.layoutInfo.editingArea;
- this.options = _.defaults(context.options || {}, wysiwygOptions);
- this.lang = wysiwygTranslation;
- this._addButtons();
- if (this.events) {
- this.events = _.clone(this.events);
- _.each(_.keys(this.events), function (key) {
- var value = self.events[key];
- if (typeof value === 'string') {
- value = self[value].bind(self);
- }
- if (key.indexOf('summernote.') === 0) {
- self.events[key] = value;
- } else {
- delete self.events[key];
- key = key.split(' ');
- if (key.length > 1) {
- self.context.layoutInfo.editor.on(key[0], key.slice(1).join(' '), value);
- } else {
- self.context.layoutInfo.editor.on(key, value);
- }
- }
- });
- }
- },
-
- //--------------------------------------------------------------------------
- // Public summernote module API
- //--------------------------------------------------------------------------
-
- shouldInitialize: function () {
- return true;
- },
- initialize: function () {},
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Override to add buttons.
- */
- _addButtons: function () {},
- /**
- * Creates a dropdown button with its contents and behavior.
- *
- * @param {str} optionName
- * @param {str} buttonIcon (ex.: 'note-icon-align')
- * @param {str} buttonTooltip (ex.: 'Align Paragraph')
- * @param {Object[]} values (ex.: [{value: 'padding-small', string: 'S'}])
- * @param {function} onclick
- */
- _createDropdownButton: function (optionName, buttonIcon, buttonTooltip, values, onclick) {
- var self = this;
-
- if (!onclick) {
- onclick = function (e) {
- var classNames = _.map(values, function (item) {
- return item.value;
- }).join(' ');
- var $target = $(self.context.invoke('editor.restoreTarget'));
- $target.removeClass(classNames);
- if ($(e.target).data('value')) {
- $target.addClass($(e.target).data('value'));
- }
- };
- }
- if (optionName) {
- this.context.memo('button.' + optionName, function () {
- return self._renderDropdownButton(buttonIcon, buttonTooltip, values, onclick);
- });
- } else {
- return this._renderDropdownButton(buttonIcon, buttonTooltip, values, onclick);
- }
- },
- /**
- * Creates a button to toggle a class.
- *
- * @param {str} optionName
- * @param {str} buttonIcon (ex.: 'note-icon-align')
- * @param {str} buttonTooltip (ex.: 'Align Paragraph')
- * @param {str} className
- */
- _createToggleButton: function (optionName, buttonIcon, buttonTooltip, className) {
- var self = this;
- return this._createButton(optionName, buttonIcon, buttonTooltip, function () {
- var $target = $(self.context.invoke('editor.restoreTarget'));
- $target.toggleClass(className);
- });
- },
- /**
- * Creates a button.
- *
- * @param {str} optionName
- * @param {str} buttonIcon (ex.: 'note-icon-align')
- * @param {str} buttonTooltip (ex.: 'Align Paragraph')
- * @param {function} onclick
- */
- _createButton: function (optionName, buttonIcon, buttonTooltip, onclick) {
- var self = this;
- if (optionName) {
- this.context.memo('button.' + optionName, function () {
- return self._renderButton(buttonIcon, buttonTooltip, onclick);
- });
- } else {
- return this._renderButton(buttonIcon, buttonTooltip, onclick);
- }
- },
- /**
- * Helper function to _createButton: renders the button.
- *
- * @param {str} buttonIcon (ex.: 'note-icon-align')
- * @param {str} buttonTooltip (ex.: 'Align Paragraph')
- * @param {function} onclick
- * @returns {JQuery}
- */
- _renderButton: function (buttonIcon, buttonTooltip, onclick) {
- var self = this;
- return this.context.invoke('buttons.button', {
- contents: buttonIcon.indexOf('<') === -1 ? this.ui.icon(buttonIcon) : buttonIcon,
- tooltip: buttonTooltip,
- click: function (e) {
- e.preventDefault();
- self.context.invoke('editor.beforeCommand');
- onclick(e);
- self.editable.normalize();
- self.context.invoke('editor.saveRange');
- self.context.invoke('editor.afterCommand');
- },
- }).render();
- },
- /**
- * Helper function to _createDropdownButton: renders the dropdown button.
- *
- * @param {str} buttonIcon (ex.: 'note-icon-align')
- * @param {str} buttonTooltip (ex.: 'Align Paragraph')
- * @param {Object[]} values (ex.: [{value: 'padding-small', string: 'S'}])
- * @param {function} onclick
- * @param {JQuery}
- */
- _renderDropdownButton: function (buttonIcon, buttonTooltip, values, onclick) {
- return this.ui.buttonGroup([
- this.context.invoke('buttons.button', {
- className: 'dropdown-toggle',
- contents: buttonIcon.indexOf('<') === -1 ?
- this.ui.dropdownButtonContents(this.ui.icon(buttonIcon), this.options) : buttonIcon,
- tooltip: buttonTooltip,
- data: {
- toggle: 'dropdown'
- }
- }),
- this.ui.dropdown({
- items: values,
- template: function (item) {
- return item.string;
- },
- click: this._wrapCommand(function (e) {
- e.preventDefault();
- onclick(e);
- }),
- })
- ]).render();
- },
- /**
- * Wraps a given function between common actions required
- * for history (undo/redo) and the maintenance of the DOM/range.
- *
- * @param {function} fn
- * @returns {any} the return of fn
- */
- _wrapCommand: function (fn) {
- var self = this;
- return function () {
- self.context.invoke('editor.restoreRange');
- self.context.invoke('editor.beforeCommand');
- var res = fn.apply(self, arguments);
- self.editable.normalize();
- self.context.invoke('editor.saveRange');
- self.context.invoke('editor.afterCommand');
- return res;
- };
- },
-
-});
-
-return AbstractPlugin;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/bullet.js b/addons/web_editor/static/src/js/wysiwyg/plugin/bullet.js
deleted file mode 100644
index 1d5c2d47a33..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/bullet.js
+++ /dev/null
@@ -1,571 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.bullet', function (require) {
-'use strict';
-
-var core = require('web.core');
-var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
-var wysiwygOptions = require('web_editor.wysiwyg.options');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-var wysiwygTranslation = require('web_editor.wysiwyg.translation');
-
-var _t = core._t;
-var dom = $.summernote.dom;
-
-wysiwygOptions.icons.checklist = 'fa fa-check-square';
-wysiwygOptions.keyMap.pc['CTRL+SHIFT+NUM9'] = 'insertCheckList';
-wysiwygOptions.keyMap.mac['CMD+SHIFT+NUM9'] = 'insertCheckList';
-wysiwygTranslation.lists.checklist = _t('Checklist');
-wysiwygTranslation.help.checklist = _t('Toggle checkbox list');
-
-
-var BulletPlugin = AbstractPlugin.extend({
- events: {
- 'summernote.mousedown': '_onMouseDown',
- },
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Insert an ordered list, an unordered list or a checklist.
- * If already in list, remove the list or convert it to the given type.
- *
- * @param {string('ol'|'ul'|'checklist')} type the type of list to insert
- * @returns {false|Node[]} contents of the ul/ol or content of the converted/removed list
- */
- insertList: function (type) {
- var range = this.context.invoke('editor.createRange');
- if (!range) {
- return;
- }
- var res;
- var start = range.getStartPoint();
- var end = range.getEndPoint();
-
- var isInList = dom.ancestor(range.sc, dom.isList);
- if (isInList) {
- res = this._convertList(false, [], start, end, type);
- } else {
- var ul = this._createList(type);
- res = ul ? [].slice.call(ul.children) : [];
- }
-
- var startLeaf = this.context.invoke('HelperPlugin.firstLeaf', start.node);
- var endLeaf = this.context.invoke('HelperPlugin.firstLeaf', end.node);
- range = this.context.invoke('editor.setRange', startLeaf, start.offset, endLeaf, end.offset);
- range.select();
- this.context.invoke('editor.saveRange');
-
- return res;
- },
- /**
- * Indent a node (list or format node).
- *
- * @returns {false|Node[]} contents of list/indented item
- */
- indent: function () {
- return this._indent();
- },
- /**
- * Outdent a node (list or format node).
- *
- * @returns {false|Node[]} contents of list/outdented item
- */
- outdent: function () {
- return this._indent(true);
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Add checklist buttons.
- */
- _addButtons: function () {
- var self = this;
- this._super();
- this.context.memo('help.checklist', this.lang.help.checklist);
- this._createButton('checklist', this.options.icons.checklist, this.lang.lists.checklist, function (e) {
- e.preventDefault();
- self.context.invoke('editor.insertCheckList');
- });
- },
- /**
- * Convert ul<->ol or remove ul/ol.
- *
- * @param {boolean} isWithinElem true if selection already inside the LI
- * @param {DOM[]} nodes selected nodes
- * @param {Object} startPoint
- * @param {Object} endPoint
- * @param {boolean} sorted
- * @returns {boolean} isWithinElem
- */
- _convertList: function (isWithinElem, nodes, startPoint, endPoint, sorted) {
- var ol = dom.ancestor(startPoint.node, dom.isList);
- var parent = ol.parentNode;
-
- // get selected lis
-
- var lis = [];
- var lisBefore = [];
- var lisAfter = [];
- _.each(ol.children, function (li) {
- if (!isWithinElem && (li === startPoint.node || $.contains(li, startPoint.node))) {
- isWithinElem = true;
- }
- if (isWithinElem) {
- lis.push(li);
- } else if (lis.length) {
- lisAfter.push(li);
- } else {
- lisBefore.push(li);
- }
- if (isWithinElem && (li === endPoint.node || $.contains(li, endPoint.node))) {
- isWithinElem = false;
- }
- });
-
- var res = lis;
-
- if (lisBefore.length) {
- var ulBefore = this.document.createElement(ol.tagName);
- ulBefore.className = ol.className;
-
- if (dom.isLi(ol.parentNode)) {
- var li = this.document.createElement('li');
- li.className = ol.parentNode.className;
- $(li).insertBefore(ol.parentNode);
- li.appendChild(ulBefore);
- } else {
- $(ulBefore).insertBefore(ol);
- }
-
- $(ulBefore).append(lisBefore);
- }
- if (lisAfter.length) {
- var ulAfter = this.document.createElement(ol.tagName);
- ulAfter.className = ol.className;
-
- if (dom.isLi(ol.parentNode)) {
- var li = this.document.createElement('li');
- li.className = ol.parentNode.className;
- $(li).insertAfter(ol.parentNode);
- li.appendChild(ulAfter);
- } else {
- $(ulAfter).insertAfter(ol);
- }
-
- $(ulAfter).append(lisAfter);
- }
-
- // convert ul<->ol or remove list
- var current = ol.tagName === 'UL' && ol.className.indexOf('o_checklist') !== -1 ? 'checklist' : ol.tagName.toLowerCase();
- if (current !== sorted) {
- // convert ul <-> ol
-
- var ul;
- $(ol).removeClass('o_checklist');
- if (sorted === 'checklist' && current === "ul") {
- ul = ol;
- } else if (sorted === 'ul' && current === 'checklist') {
- ul = ol;
- } else {
- ul = this.document.createElement(sorted === "ol" ? "ol" : "ul");
- ul.className = ol.className;
- $(ul).removeClass('o_checklist');
- $(ul).insertBefore(ol).append(lis);
- parent.removeChild(ol);
- }
- if (sorted === 'checklist') {
- $(ul).addClass('o_checklist');
- }
-
- this.context.invoke('HelperPlugin.deleteEdge', ul, 'next');
- this.context.invoke('HelperPlugin.deleteEdge', ul, 'prev');
-
- } else {
- // remove ol/ul
-
- if (dom.isLi(parent) || dom.isList(parent)) {
- if (dom.isLi(parent)) {
- ol = parent;
- parent = ol.parentNode;
- }
- $(lis).insertBefore(ol);
- } else {
- res = [];
- _.each(lis, function (li) {
- res.push.apply(res, li.childNodes);
- $(li.childNodes).insertBefore(ol);
- });
-
- // wrap in p
-
- var hasNode = _.find(res, function (node) {
- return node.tagName && node.tagName !== "BR" && !dom.isMedia(node);
- });
- if (!hasNode) {
- var p = this.document.createElement('p');
- $(p).insertBefore(ol).append(res);
- res = [p];
- }
- }
- parent.removeChild(ol);
-
- }
-
- nodes.push.apply(nodes, res);
-
- return isWithinElem;
- },
- /**
- * Create a list if allowed.
- *
- * @param {string('ol'|'ul'|'checklist')} type the type of list to insert
- * @returns {false|Node} the list, if any
- */
- _createList: function (type) {
- var nodes = this.context.invoke('HelperPlugin.getSelectedNodes');
- var formatNodes = this._filterEditableFormatNodes(nodes);
- if (!formatNodes.length) {
- return;
- }
-
- var ul = this._createListElement(type);
- $(formatNodes[0][0] || formatNodes[0]).before(ul);
- this._fillListElementWith(ul, formatNodes);
- this._deleteListElementEdges(ul);
-
- return ul;
- },
- /**
- * Create a list element of the given type and return it.
- *
- * @param {string('ol'|'ul'|'checklist')} type the type of list to insert
- * @returns {Node}
- */
- _createListElement: function (type) {
- var ul = this.document.createElement(type === "ol" ? "ol" : "ul");
- if (type === 'checklist') {
- ul.className = 'o_checklist';
- }
- return ul;
- },
- /**
- * Delete a list element's edges if necessary.
- *
- * @param {Node} ul
- */
- _deleteListElementEdges: function (ul) {
- this.context.invoke('HelperPlugin.deleteEdge', ul, 'next');
- this.context.invoke('HelperPlugin.deleteEdge', ul, 'prev');
- this.editable.normalize();
- },
- /**
- * Fill a list element with the nodes passed, wrapped in LIs.
- *
- * @param {Node} ul
- * @param {Node[]} nodes
- */
- _fillListElementWith: function (ul, nodes) {
- var self = this;
- _.each(nodes, function (node) {
- var li = self.document.createElement('li');
- $(li).append(node);
- ul.appendChild(li);
- });
- },
- /**
- * Filter the editable format ancestors of the given nodes
- * and fill or wrap them if needed for range selection.
- *
- * @param {Node[]} nodes
- * @returns {Node[]}
- */
- _filterEditableFormatNodes: function (nodes) {
- var self = this;
- var formatNodes = this.context.invoke('HelperPlugin.filterFormatAncestors', nodes);
- formatNodes = _.compact(_.map(formatNodes, function (node) {
- var ancestor = (!node.tagName || node.tagName === 'BR') && dom.ancestor(node, dom.isCell);
- if (ancestor && self.options.isEditableNode(ancestor)) {
- if (!ancestor.childNodes.length) {
- var br = self.document.createElement('br');
- ancestor.appendChild(br);
- }
- var p = self.document.createElement('p');
- $(p).append(ancestor.childNodes);
- ancestor.appendChild(p);
- return p;
- }
- return self.options.isEditableNode(node) && node || null;
- }));
- return formatNodes;
- },
- /**
- * Indent or outdent a format node.
- *
- * @param {bool} outdent true to outdent, false to indent
- * @returns {false|[]Node} indented nodes
- */
- _indent: function (outdent) {
- var range = this.context.invoke('editor.createRange');
- if (!range) {
- return;
- }
- // list groups shouldn't be outdented like regular lists
- if ($(dom.ancestor(range.sc, dom.isList)).hasClass('list-group')) {
- return;
- }
-
- var self = this;
- var nodes = [];
- var isWithinElem;
- var ancestor = range.commonAncestor() || this.editable;
- var $dom = $(ancestor);
-
- if (!dom.isList(ancestor)) {
- // to indent a selection, we indent the child nodes of the common
- // ancestor that contains this selection
- $dom = $(ancestor.tagName ? ancestor : ancestor.parentNode).children();
- }
-
- // if selection is inside indented contents and outdent is true, we can outdent this node
- var indentedContent = outdent && dom.ancestor(ancestor, function (node) {
- var style = dom.isCell(node) ? 'paddingLeft' : 'marginLeft';
- return node.tagName && !!parseFloat(node.style[style] || 0);
- });
-
- if (indentedContent) {
- $dom = $(indentedContent);
- } else {
- // if selection is inside a list, we indent its list items
- $dom = $(dom.ancestor(ancestor, dom.isList));
- if (!$dom.length) {
- // if the selection is contained in a single HTML node, we indent
- // the first ancestor 'content block' (P, H1, PRE, ...) or TD
- var nodes = this.context.invoke('HelperPlugin.getSelectedNodes');
- $dom = $(this._filterEditableFormatNodes(nodes));
- }
- }
-
- // if select tr, take the first td
- $dom = $dom.map(function () {
- return this.tagName === "TR" ? this.firstElementChild : this;
- });
-
- $dom.each(function () {
- if (isWithinElem || $.contains(this, range.sc)) {
- if (dom.isList(this)) {
- if (outdent) {
- var type = this.tagName === 'OL' ? 'ol' : (this.className && this.className.indexOf('o_checklist') !== -1 ? 'checklist' : 'ul');
- isWithinElem = self._convertList(isWithinElem, nodes, range.getStartPoint(), range.getEndPoint(), type);
- } else {
- isWithinElem = self._indentUL(isWithinElem, nodes, this, range.sc, range.ec);
- }
- } else if (self.context.invoke('HelperPlugin.isFormatNode', this) || dom.ancestor(this, dom.isCell)) {
- isWithinElem = self._indentFormatNode(outdent, isWithinElem, nodes, this, range.sc, range.ec);
- }
- }
- });
-
- if ($dom.parent().length) {
- var $parent = $dom.parent();
-
- // remove text nodes between lists
- var $ul = $parent.find('ul, ol');
- if (!$ul.length) {
- $ul = $(dom.ancestor(range.sc, dom.isList));
- }
- $ul.each(function () {
- var notWhitespace = /\S/;
- if (
- this.previousSibling &&
- this.previousSibling !== this.previousElementSibling &&
- !this.previousSibling.textContent.match(notWhitespace)
- ) {
- this.parentNode.removeChild(this.previousSibling);
- }
- if (
- this.nextSibling &&
- this.nextSibling !== this.nextElementSibling &&
- !this.nextSibling.textContent.match(notWhitespace)
- ) {
- this.parentNode.removeChild(this.nextSibling);
- }
- });
-
- // merge same ul or ol
- $ul.prev('ul, ol').each(function () {
- self.context.invoke('HelperPlugin.deleteEdge', this, 'next');
- });
-
- }
-
- range.normalize().select();
- this.context.invoke('editor.saveRange');
-
- return !!nodes.length && nodes;
- },
- /**
- * Indent several LIs in a list.
- *
- * @param {bool} isWithinElem true if selection already inside the LI
- * @param {Node[]} nodes
- * @param {Node} UL
- * @param {Node} start
- * @param {Node} end
- * @returns {bool} isWithinElem
- */
- _indentUL: function (isWithinElem, nodes, UL, start, end) {
- var next;
- var tagName = UL.tagName;
- var node = UL.firstChild;
- var ul = document.createElement(tagName);
- ul.className = UL.className;
- var flag;
-
- if (isWithinElem) {
- flag = true;
- }
-
- // create and fill ul into a li
- while (node) {
- if (flag || node === start || $.contains(node, start)) {
- isWithinElem = true;
- node.parentNode.insertBefore(ul, node);
- }
- next = node.nextElementSibling;
- if (isWithinElem) {
- ul.appendChild(node);
- nodes.push(node);
- }
- if (node === end || $.contains(node, end)) {
- isWithinElem = false;
- break;
- }
- node = next;
- }
-
- var temp;
- var prev = ul.previousElementSibling;
- if (
- prev && prev.tagName === "LI" &&
- (temp = prev.firstElementChild) && temp.tagName === tagName &&
- ((prev.firstElementChild || prev.firstChild) !== ul)
- ) {
- $(prev.firstElementChild || prev.firstChild).append($(ul).contents());
- $(ul).remove();
- ul = prev;
- ul.parentNode.removeChild(ul.nextElementSibling);
- }
- next = ul.nextElementSibling;
- if (
- next && next.tagName === "LI" &&
- (temp = next.firstElementChild) && temp.tagName === tagName &&
- (ul.firstElementChild !== next.firstElementChild)
- ) {
- $(ul.firstElementChild).append($(next.firstElementChild).contents());
- $(next.firstElementChild).remove();
- ul.parentNode.removeChild(ul.nextElementSibling);
- }
-
- // wrap in li
- var li = this.document.createElement('li');
- li.className = 'o_indent';
- $(ul).before(li);
- li.appendChild(ul);
-
- return isWithinElem;
- },
- /**
- * Outdent a container node.
- *
- * @param {Node} node
- * @returns {float} margin
- */
- _outdentContainer: function (node) {
- var style = dom.isCell(node) ? 'paddingLeft' : 'marginLeft';
- var margin = parseFloat(node.style[style] || 0) - 1.5;
- node.style[style] = margin > 0 ? margin + "em" : "";
- return margin;
- },
- /**
- * Indent a container node.
- *
- * @param {Node} node
- * @returns {float} margin
- */
- _indentContainer: function (node) {
- var style = dom.isCell(node) ? 'paddingLeft' : 'marginLeft';
- var margin = parseFloat(node.style[style] || 0) + 1.5;
- node.style[style] = margin + "em";
- return margin;
- },
- /**
- * Indent/outdent a format node.
- *
- * @param {bool} outdent true to outdent, false to indent
- * @param {bool} isWithinElem true if selection already inside the element
- * @param {DOM[]} nodes
- * @param {DOM} p
- * @param {DOM} start
- * @param {DOM} end
- * @returns {bool} isWithinElem
- */
- _indentFormatNode: function (outdent, isWithinElem, nodes, p, start, end) {
- if (p === start || $.contains(p, start) || $.contains(start, p)) {
- isWithinElem = true;
- }
- if (isWithinElem) {
- nodes.push(p);
- if (outdent) {
- this._outdentContainer(p);
- } else {
- this._indentContainer(p);
- }
- }
- if (p === end || $.contains(p, end) || $.contains(end, p)) {
- isWithinElem = false;
- }
- return isWithinElem;
- },
-
- //--------------------------------------------------------------------------
- // Handle
- //--------------------------------------------------------------------------
-
- /**
- * @param {SummernoteEvent} se
- * @param {jQueryEvent} e
- */
- _onMouseDown: function (se, e) {
- if (!dom.isLi(e.target) || !$(e.target).parent('ul.o_checklist').length || e.offsetX > 0) {
- return;
- }
- e.preventDefault();
- var checked = $(e.target).hasClass('o_checked');
- $(e.target).toggleClass('o_checked', !checked);
- var $sublevel = $(e.target).next('ul.o_checklist, li:has(> ul.o_checklist)').find('> li, ul.o_checklist > li');
- var $parents = $(e.target).parents('ul.o_checklist').map(function () {
- return this.parentNode.tagName === 'LI' ? this.parentNode : this;
- });
- if (checked) {
- $sublevel.removeClass('o_checked');
- $parents.prev('ul.o_checklist li').removeClass('o_checked');
- } else {
- $sublevel.addClass('o_checked');
- var $lis;
- do {
- $lis = $parents.not(':has(li:not(.o_checked))').prev('ul.o_checklist li:not(.o_checked)');
- $lis.addClass('o_checked');
- } while ($lis.length);
- }
- },
-});
-
-registry.add('BulletPlugin', BulletPlugin);
-
-return BulletPlugin;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/buttons.js b/addons/web_editor/static/src/js/wysiwyg/plugin/buttons.js
deleted file mode 100644
index 7059c448d81..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/buttons.js
+++ /dev/null
@@ -1,88 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.buttons', function (require) {
-'use strict';
-
-var Plugins = require('web_editor.wysiwyg.plugins');
-
-var dom = $.summernote.dom;
-
-Plugins.buttons.include({
- /**
- * Fix tooltip for 'option' buttons (summernote 0.8.9 bug)
- * Remove this once the library is updated to 0.8.10.
- *
- * @override
- */
- addToolbarButtons: function () {
- this.options.codeview = this.lang.options.codeview;
- this.options.help = this.lang.options.help;
- this.options.fullscreen = this.lang.options.fullscreen;
- this._super();
- },
- /**
- * Show current style (of start of selection) in magic wand dropdown.
- *
- * @param {JQuery Object} $container
- */
- updateActiveStyleDropdown: function ($container) {
- var self = this;
- var range = this.context.invoke('editor.createRange');
- var el = dom.ancestor(range.sc, function (n) {
- return n.tagName && self.options.styleTags.indexOf(n.tagName.toLowerCase()) !== -1;
- });
- if (el) {
- var tagName = el.tagName.toLowerCase();
- $container.find('.dropdown-style a').each(function (idx, item) {
- var $item = $(item);
- // always compare string to avoid creating another func.
- var isChecked = ($item.data('value') + '') === (tagName + '');
- $item.toggleClass('active', isChecked);
- });
- } else {
- var $item = $container.find('.dropdown-style a.active');
- $item.removeClass('active');
- }
- },
- /**
- * @override
- */
- updateCurrentStyle: function ($container) {
- this._super.apply(this, arguments);
-
- this.updateParaAlignIcon($container || this.$toolbar);
- this.updateActiveStyleDropdown($container || this.$toolbar);
- },
- /**
- * Show current paragraph alignment (of start of selection) on paragraph alignment dropdown.
- *
- * @param {JQuery Object} $container
- */
- updateParaAlignIcon: function ($container) {
- var range = this.context.invoke('editor.createRange');
- var $paraIcon = $container.find('.note-para .dropdown-toggle i');
- var el = dom.isText(range.sc) ? range.sc.parentNode : range.sc;
- if (el) {
- $paraIcon.removeClass();
- switch ($(el).css('text-align')) {
- case 'left':
- $paraIcon.addClass('note-icon-align-left');
- break;
- case 'center':
- $paraIcon.addClass('note-icon-align-center');
- break;
- case 'right':
- $paraIcon.addClass('note-icon-align-right');
- break;
- case 'justify':
- $paraIcon.addClass('note-icon-align-justify');
- break;
- default:
- $paraIcon.addClass('note-icon-align-left');
- break;
- }
- }
- },
-});
-
-return Plugins.buttons;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/codeview.js b/addons/web_editor/static/src/js/wysiwyg/plugin/codeview.js
deleted file mode 100644
index 01ffc52b205..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/codeview.js
+++ /dev/null
@@ -1,60 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.codeview', function (require) {
-'use strict';
-
-var core = require('web.core');
-var Plugins = require('web_editor.wysiwyg.plugins');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-
-var _t = core._t;
-
-
-var CodeviewPlugin = Plugins.codeview.extend({
- /**
- * @override
- */
- activate: function () {
- this._super();
- if (this.$codable.height() === 0) {
- this.$codable.height(180);
- }
- this.context.invoke('editor.hidePopover');
- this.context.invoke('editor.clearTarget');
- },
- /**
- * @override
- */
- deactivate: function () {
- if (
- this.context.invoke('HelperPlugin.hasJinja', this.context.invoke('code')) &&
- !this.isBeingDestroyed
- ) {
- var message = _t("Your code contains JINJA conditions.\nRemove them to return to WYSIWYG HTML edition.");
- this.do_warn(_t("Cannot edit HTML"), message);
- this.$codable.focus();
- return;
- }
- this._super();
- this.$editable.css('height', '');
- },
- /**
- * @override
- */
- destroy: function () {
- this.isBeingDestroyed = true;
- this._super();
- },
- /**
- * Force activation of the code view.
- */
- forceActivate: function () {
- if (!this.isActivated()) {
- this.activate();
- }
- },
-});
-
-registry.add('codeview', CodeviewPlugin);
-
-return CodeviewPlugin;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/dropzone.js b/addons/web_editor/static/src/js/wysiwyg/plugin/dropzone.js
deleted file mode 100644
index a88fc50fc7f..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/dropzone.js
+++ /dev/null
@@ -1,229 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.dropzone', function (require) {
-'use strict';
-
-var core = require('web.core');
-var Plugins = require('web_editor.wysiwyg.plugins');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-var utils = require('web.utils');
-
-var _t = core._t;
-var dom = $.summernote.dom;
-
-var DropzonePlugin = Plugins.dropzone.extend({
- //--------------------------------------------------------------------------
- // Public summernote module API
- //--------------------------------------------------------------------------
-
- /**
- * Disable Summernote's handling of drop events.
- */
- attachDragAndDropEvent: function () {
- this._super.apply(this, arguments);
- this.$dropzone.off('drop');
- this.$dropzone.on('dragenter', this._onDragenter.bind(this));
- this.$dropzone.on('drop', this._onDrop.bind(this));
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Clean up then drops HTML or plain text into the editor.
- *
- * @private
- * @param {String} html
- * @param {Boolean} textOnly true to allow only dropping plain text
- */
- _dropHTML: function (html, textOnly) {
- this.context.invoke('editor.beforeCommand');
-
- // Clean up
- var nodes = this.context.invoke('TextPlugin.prepareClipboardData', html);
-
- // Delete selection
- this.context.invoke('HelperPlugin.deleteSelection');
-
- // Insert the nodes
- this.context.invoke('TextPlugin.pasteNodes', nodes, textOnly);
- this.context.invoke('HelperPlugin.normalize');
- this.context.invoke('editor.saveRange');
-
- this.context.invoke('editor.afterCommand');
- },
- /**
- * Drop images into the editor: save them as attachments.
- *
- * @private
- * @param {File[]]} files (images only)
- */
- _dropImages: function (files) {
- var self = this;
- this.context.invoke('editor.beforeCommand');
- var range = this.context.invoke('editor.createRange');
-
- var spinners = [];
- var images = [];
- var defs = [];
- _.each(files, function (file) {
- // Add spinner
- var spinner = $('<span class="fa fa-spinner fa-spin">').attr('data-filename', file.name)[0];
- self.context.invoke('editor.hidePopover');
- if (range.sc.tagName) {
- if (range.so >= dom.nodeLength(range.sc)) {
- $(range.sc).append(spinner);
- } else {
- $(range.sc.childNodes[range.so]).before(spinner);
- }
- } else {
- range.sc.splitText(range.so);
- $(range.sc).after(spinner);
- }
- spinners.push(spinner);
-
- // save images as attachments
- var def = new Promise(function (resolve) {
- // Get image's Base64 string
- utils.getDataURLFromFile(file).then(function (result) {
- self._uploadImage(result, file.name).then(function (attachment) {
- // Make the HTML
- var image = self.document.createElement('img');
- image.setAttribute('style', 'width: 100%;');
- image.src = '/web/content/' + attachment.id + '/' + attachment.name;
- image.alt = attachment.name;
- $(spinner).replaceWith(image);
- images.push(image);
- resolve(image);
- $(image).trigger('dropped');
- });
- });
- });
- defs.push(def);
- });
-
- this.trigger_up('drop_images', {
- spinners: spinners,
- promises: defs,
- });
-
- Promise.all(defs).then(function () {
- var defs = [];
- $(images).each(function () {
- if (!this.height) {
- var def = new Promise(function (resolve) {
- $(this).one('load error abort', resolve);
- });
- defs.push(def);
- }
- });
- Promise.all(defs).then(function () {
- if (images.length === 1) {
- range = self.context.invoke('editor.setRange', _.last(images), 0);
- range.select();
- self.context.invoke('editor.saveRange');
- self.context.invoke('editor.afterCommand');
- self.context.invoke('MediaPlugin.updatePopoverAfterEdit', images[0]);
- } else {
- self.context.invoke('editor.afterCommand');
- }
- });
- });
- },
- /**
- * Upload an image from its Base64 representation.
- *
- * @private
- * @param {String} imageBase64
- * @param {String} fileName
- * @returns {Promise}
- */
- _uploadImage: function (imageBase64, fileName) {
- var options = {};
- this.trigger_up('getRecordInfo', {
- recordInfo: options,
- type: 'media',
- callback: function (recordInfo) {
- _.defaults(options, recordInfo);
- },
- });
-
- return this._rpc({
- route: '/web_editor/attachment/add_data',
- params: {
- 'data': imageBase64.split(';base64,').pop(),
- 'name': fileName,
- 'res_id': options.res_id || 0,
- 'res_model': options.res_model,
- },
- });
- },
- /**
- * @private
- * @param {JQueryEvent} e
- */
- _onDragenter: function (e) {
- var range = this.context.invoke('editor.createRange');
- if (!this.editable.contains(range.sc)) {
- var node = this.editable;
- if (node && node.firstChild) {
- node = node.firstChild;
- }
- range = this.context.invoke('editor.setRange', node, 0, node, 0);
- range.select();
- }
- },
- /**
- * @private
- * @param {JQueryEvent} e
- */
- _onDrop: function (e) {
- e.preventDefault();
-
- this.$editor.removeClass('dragover');
-
- if (this.options.disableDragAndDrop) {
- return;
- }
- var dataTransfer = e.originalEvent.dataTransfer;
-
- if (!this._canDropHere()) {
- this.context.invoke('HelperPlugin.notify', _t("Not a dropzone"), _t("Dropping is prohibited in this area."));
- return;
- }
-
- if (dataTransfer.getData('text/html')) {
- this._dropHTML(dataTransfer.getData('text/html'));
- return;
- }
- if (dataTransfer.files.length) {
- var images = [];
- _.each(dataTransfer.files, function (file) {
- if (file.type.indexOf('image') !== -1) {
- images.push(file);
- }
- });
- if (!images.length || images.length < dataTransfer.files.length) {
- this.context.invoke('HelperPlugin.notify', _t("Unsupported file type"), _t("Images are the only file types that can be dropped."));
- }
- if (images.length) {
- this._dropImages(images);
- }
- }
- },
- /**
- * Return true if dropping is allowed at the current range.
- *
- * @private
- * @returns {Boolean}
- */
- _canDropHere: function () {
- var range = this.context.invoke('editor.createRange');
- return this.options.isEditableNode(range.sc);
- },
-});
-
-registry.add('dropzone', DropzonePlugin);
-
-return DropzonePlugin;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/editor.js b/addons/web_editor/static/src/js/wysiwyg/plugin/editor.js
deleted file mode 100644
index 350a9158188..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/editor.js
+++ /dev/null
@@ -1,189 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.editor', function (require) {
-'use strict';
-
-var Plugins = require('web_editor.wysiwyg.plugins');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-var TablePlugin = require('web_editor.wysiwyg.plugin.table');
-
-
-var NewSummernoteEditor = Plugins.editor.extend({
- //--------------------------------------------------------------------------
- // Public summernote module API
- //--------------------------------------------------------------------------
-
- initialize: function () {
- var self = this;
- this.history = this.context.modules.HistoryPlugin;
- this.dropzone = this.context.modules.DropzonePlugin;
-
- this.insertTable = this.wrapCommand(this._insertTable.bind(this));
- this.insertOrderedList = this.wrapCommand(this._insertOrderedList.bind(this));
- this.insertUnorderedList = this.wrapCommand(this._insertUnorderedList.bind(this));
- this.insertCheckList = this.wrapCommand(this._insertCheckList.bind(this));
- this.indent = this.wrapCommand(this._indent.bind(this));
- this.outdent = this.wrapCommand(this._outdent.bind(this));
- this.table = new TablePlugin(this.context);
- this.hasFocus = this._hasFocus.bind(this);
-
- this._makeTextCommand('formatBlock');
- this._makeTextCommand('removeFormat');
- _.each('bold,italic,underline,strikethrough,superscript,subscript'.split(','), function (sCmd) {
- self._makeTextCommand('formatText', sCmd);
- });
- _.each('justifyLeft,justifyCenter,justifyRight,justifyFull'.split(','), function (sCmd) {
- self._makeTextCommand('formatBlockStyle', sCmd);
- });
-
- this._super();
- },
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Hide all popovers.
- */
- hidePopover: function () {
- this.context.invoke('MediaPlugin.hidePopovers');
- this.context.invoke('LinkPopover.hide');
- },
- /*
- * Focus the editor.
- */
- focus: function () {
- // [workaround] Screen will move when page is scolled in IE.
- // - do focus when not focused
- if (!this.hasFocus()) {
- var range = $.summernote.range.create();
- if (range) {
- $(range.sc).closest('[contenteditable]').focus();
- range.select();
- } else {
- this.$editable.focus();
- }
- }
- },
- /**
- * Fix double-undo (CTRL-Z) issue with Odoo integration.
- *
- * @override
- */
- undo: function () {
- this.createRange();
- this._super();
- },
- /**
- * Set the range at the given nodes and offsets.
- * If no `ec` is specified, the range is collapsed on start.
- * Note: Does NOT select the range.
- *
- * @param {Node} sc
- * @param {Number} so
- * @param {Node} [ec]
- * @param {Number} [eo]
- * @returns {Object} range
- */
- setRange: function (sc, so, ec, eo) {
- var range = this.createRange();
- range.sc = sc;
- range.so = so;
- range.ec = ec || sc;
- range.eo = eo || so;
- return range;
- },
- /**
- * Remove a link and preserve its text contents.
- */
- unlink: function () {
- this.beforeCommand();
- this.context.invoke('LinkPlugin.unlink');
- this.afterCommand();
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Returns true if editable area has focus.
- *
- * @private
- * @returns {Boolean}
- */
- _hasFocus: function () {
- return this.$editable.is(':focus') || !!this.$editable.find('[contenteditable]:focus').length;
- },
- /**
- * Indent a list or a format node.
- *
- * @private
- * @returns {false|Node[]} contents of list/indented item
- */
- _indent: function () {
- return this.context.invoke('BulletPlugin.indent');
- },
- /**
- * Insert a checklist.
- *
- * @private
- */
- _insertCheckList: function () {
- this.context.invoke('BulletPlugin.insertList', 'checklist');
- },
- /**
- * Insert an ordered list.
- *
- * @private
- */
- _insertOrderedList: function () {
- this.context.invoke('BulletPlugin.insertList', 'ol');
- },
- /**
- * Insert table (respecting unbreakable node rules).
- *
- * @private
- * @param {string} dim (eg: 3x3)
- */
- _insertTable: function (dim) {
- this.context.invoke('TablePlugin.insertTable', dim);
- },
- /**
- * Insert an ordered list.
- *
- * @private
- */
- _insertUnorderedList: function () {
- this.context.invoke('BulletPlugin.insertList', 'ul');
- },
- /**
- * Adds a TextPlugin command to the editor object.
- * The command will exist in the editor object under the name
- * arg if specified, or commandName otherwise.
- *
- * @param {String} commandName
- * @param {String} arg
- */
- _makeTextCommand: function (commandName, arg) {
- var self = this;
- this[arg || commandName] = self.wrapCommand(function (value) {
- self.context.invoke('TextPlugin.' + commandName, arg || value, value);
- });
- },
- /**
- * Outdent a list or a format node.
- *
- * @private
- * @returns {false|Node[]} contents of list/outdented item
- */
- _outdent: function () {
- return this.context.invoke('BulletPlugin.outdent');
- },
-});
-
-// Override Summernote default editor
-registry.add('editor', NewSummernoteEditor);
-
-return NewSummernoteEditor;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/font.js b/addons/web_editor/static/src/js/wysiwyg/plugin/font.js
deleted file mode 100644
index 0956b031747..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/font.js
+++ /dev/null
@@ -1,567 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.font', function (require) {
-'use strict';
-
-var core = require('web.core');
-var ColorpickerDialog = require('web.ColorpickerDialog');
-var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-var wysiwygTranslation = require('web_editor.wysiwyg.translation');
-var wysiwygOptions = require('web_editor.wysiwyg.options');
-
-var QWeb = core.qweb;
-var _t = core._t;
-
-var dom = $.summernote.dom;
-
-//--------------------------------------------------------------------------
-// Font (colorpicker & font-size)
-//--------------------------------------------------------------------------
-
-dom.isFont = function (node) {
- return node && node.tagName === "FONT" || dom.isIcon(node);
-};
-
-var FontPlugin = AbstractPlugin.extend({
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Creates both ColorPicker buttons (fore- and backcolors) containing their palettes.
- *
- * @returns {jQuery} the buttons' div.btn-group container
- */
- createColorPickerButtons: function () {
- var self = this;
- var $container = $('<div class="note-btn-group btn-group note-color"/>');
-
- // create and append the buttons
-
- this._createForeColorButton().appendTo($container);
- this._createBackColorButton().appendTo($container);
-
- // add event
-
- $container.on('mousedown', function () {
- self.context.invoke('editor.saveRange');
- });
- return $container;
- },
- /**
- * Takes the font size button created by summernote and override its
- * events so as to customize its behavior.
- *
- * @param {jQuery} $button the button, as created by summernote
- * @returns {jQuery} the overridden button
- */
- overrideFontSizeButton: function ($button) {
- var self = this;
- $button.click(function (e) {
- e.preventDefault();
- });
- $button.find('.dropdown-menu').off('click').on('mousedown', function (e) {
- e.preventDefault();
- self.context.invoke('editor.createRange');
- self.context.invoke('editor.beforeCommand');
- var $target = $(e.target);
- self.context.invoke('FontPlugin.changeFontSize', $target.closest('[data-value]').data('value'), $target);
- self.context.invoke('buttons.updateCurrentStyle');
- self.context.invoke('editor.saveRange');
- self.context.invoke('editor.afterCommand');
- });
- return $button;
- },
- /**
- * Creates a (fore- or back-) color palette.
- *
- * @param {string('foreColor'|'backColor')} eventName
- * @returns {jQuery}
- */
- createPalette: function (eventName) {
- var self = this;
- var colors = _.clone(this.options.colors);
- colors.splice(0, 1); // Ignore the summernote gray palette and use ours
- var $palette = $(QWeb.render('wysiwyg.plugin.font.colorPalette', {
- colors: colors,
- eventName: eventName,
- lang: this.lang,
- }));
- if (this.options.tooltip) {
- $palette.find('.note-color-btn').tooltip({
- container: this.options.container,
- trigger: 'hover',
- placement: 'bottom'
- });
- }
-
- // custom structure for the color picker and custom colorsin XML template
- var $clpicker = $(QWeb.render('web_editor.colorpicker'));
-
- var $buttons = $(_.map($clpicker.children(), function (group) {
- var $contents = $(group).contents();
- if (!$contents.length) {
- return '';
- }
- var $row = $("<div/>", {
- "class": "note-color-row mb8 clearfix",
- 'data-group': $(group).data('name'),
- }).append($contents);
- var $after_breaks = $row.find(".o_small + :not(.o_small)");
- if ($after_breaks.length === 0) {
- $after_breaks = $row.find(":nth-child(8n+9)");
- }
- $after_breaks.addClass("o_clear");
- return $row[0].outerHTML;
- }).join(""));
-
- $buttons.find('button').each(function () {
- var color = $(this).data('color');
- $(this).addClass('note-color-btn bg-' + color).attr('data-value', (eventName === 'backColor' ? 'bg-' : 'text-') + color);
- });
-
- $palette.find('.o_theme_color_placeholder').prepend($buttons.filter('[data-group="theme"]'));
- $palette.find('.o_transparent_color_placeholder').prepend($buttons.filter('[data-group="transparent_grayscale"]'));
- $palette.find('.o_common_color_placeholder').prepend($buttons.filter('[data-group="common"]'));
-
- $palette.off('click').on('mousedown', '.note-color-btn', function (e) {
- e.preventDefault();
- self.context.invoke('editor.createRange');
- self.context.invoke('editor.beforeCommand');
- var method = eventName === 'backColor' ? 'changeBgColor' : 'changeForeColor';
- var $target = $(e.target);
- self.context.invoke('FontPlugin.' + method, $target.closest('[data-value]').data('value'), $target);
- self.context.invoke('buttons.updateCurrentStyle');
- self.editable.normalize();
- self.context.invoke('editor.saveRange');
- self.context.invoke('editor.afterCommand');
- });
- $palette.on('mousedown', '.note-custom-color', this._onCustomColor.bind(this, eventName));
-
- return $palette;
- },
- /**
- * Change the selection's fore color.
- *
- * @param {string} color (hexadecimal or class name)
- */
- changeForeColor: function (color) {
- this._applyFont(color || 'text-undefined', null, null);
- },
- /**
- * Change the selection's background color.
- *
- * @param {string} color (hexadecimal or class name)
- */
- changeBgColor: function (color) {
- this._applyFont(null, color || 'bg-undefined', null);
- },
- /**
- * Change the selection's font size.
- *
- * @param {integer} fontsize
- */
- changeFontSize: function (fontsize) {
- this._applyFont(null, null, fontsize || 'inherit');
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Applies the given styles (fore- or backcolor, font size)
- * to a given <font> node.
- *
- * @private
- * @param {Node} node
- * @param {string} color (hexadecimal or class name)
- * @param {string} bgcolor (hexadecimal or class name)
- * @param {integer} size
- * @returns {Node} the <font> node
- */
- _applyStylesToFontNode: function (node, color, bgcolor, size) {
- var className = node.className.split(this.context.invoke('HelperPlugin.getRegex', 'space'));
- var k;
- if (color) {
- for (k = 0; k < className.length; k++) {
- if (className[k].length && className[k].slice(0, 5) === "text-") {
- className.splice(k, 1);
- k--;
- }
- }
- if (color === 'text-undefined') {
- node.className = className.join(" ");
- node.style.color = "inherit";
- } else if (color.indexOf('text-') !== -1) {
- node.className = className.join(" ") + " " + color;
- node.style.color = "inherit";
- } else {
- node.className = className.join(" ");
- node.style.color = color;
- }
- }
- if (bgcolor) {
- for (k = 0; k < className.length; k++) {
- if (className[k].length && className[k].slice(0, 3) === "bg-") {
- className.splice(k, 1);
- k--;
- }
- }
-
- if (bgcolor === 'bg-undefined') {
- node.className = className.join(" ");
- node.style.backgroundColor = "inherit";
- } else if (bgcolor.indexOf('bg-') !== -1) {
- node.className = className.join(" ") + " " + bgcolor;
- node.style.backgroundColor = "inherit";
- } else {
- node.className = className.join(" ");
- node.style.backgroundColor = bgcolor;
- }
- }
- if (size) {
- node.style.fontSize = "inherit";
- if (!isNaN(size) && Math.abs(parseInt(this.window.getComputedStyle(node).fontSize, 10) - size) / size > 0.05) {
- node.style.fontSize = size + "px";
- }
- }
- return node;
- },
- /**
- * Applies the given styles (fore- or backcolor, font size) to the selection.
- * If no text is selected, apply to the current text node, if any.
- *
- * @private
- * @param {string} color (hexadecimal or class name)
- * @param {string} bgcolor (hexadecimal or class name)
- * @param {integer} fontsize
- */
- _applyFont: function (color, bgcolor, size) {
- var self = this;
- var r = this.context.invoke('editor.createRange');
- if (!r || !this.$editable.has(r.sc).length || !this.$editable.has(r.ec).length) {
- return;
- }
- var target;
- var font;
- if (r.isCollapsed()) {
- if (dom.isIcon(r.sc)) {
- target = dom.lastAncestor(r.sc, dom.isIcon);
- } else {
- target = this.context.invoke('editor.restoreTarget');
- if (target && dom.isIcon(target)) {
- r = this.context.invoke('editor.setRange', target, 0);
- } else if (dom.isText(r.sc)) {
- font = dom.create("font");
- font.appendChild(this.document.createTextNode('\u200B'));
-
- var fontParent = dom.ancestor(r.sc, function (n) {
- return n.tagName === 'FONT';
- });
- var right;
- if (fontParent) {
- right = this.context.invoke('HelperPlugin.splitTree', fontParent, r.getStartPoint());
- } else {
- right = r.sc.splitText(r.so);
- }
- $(right).before(font);
- font = this._applyStylesToFontNode(font, color, bgcolor, size);
- r = this.context.invoke('editor.setRange', font, 1);
- r.select();
- return;
- }
- }
- }
-
- var startPoint = r.getStartPoint();
- var endPoint = r.getEndPoint();
- if (startPoint.node.tagName && startPoint.node.childNodes[startPoint.offset]) {
- startPoint.node = startPoint.node.childNodes[startPoint.offset];
- startPoint.offset = 0;
- }
- if (endPoint.node.tagName && endPoint.node.childNodes[endPoint.offset]) {
- endPoint.node = endPoint.node.childNodes[endPoint.offset];
- endPoint.offset = 0;
- }
- // get first and last point
- var ancestor;
- var node;
- if (!r.isCollapsed()) {
- if (endPoint.offset && endPoint.offset !== dom.nodeLength(endPoint.node)) {
- ancestor = dom.lastAncestor(endPoint.node, dom.isFont) || endPoint.node;
- dom.splitTree(ancestor, endPoint);
- }
- if (startPoint.offset && startPoint.offset !== dom.nodeLength(startPoint.node)) {
- ancestor = dom.lastAncestor(startPoint.node, dom.isFont) || startPoint.node;
- node = dom.splitTree(ancestor, startPoint);
- if (endPoint.node === startPoint.node) {
- endPoint.node = node;
- endPoint.offset = dom.nodeLength(node);
- }
- startPoint.node = node;
- startPoint.offset = 0;
- }
- }
- // get list of nodes to change
- var nodes = [];
- dom.walkPoint(startPoint, endPoint, function (point) {
- var node = point.node;
- if (((dom.isText(node) && self.context.invoke('HelperPlugin.isVisibleText', node)) || dom.isIcon(node)) &&
- (node !== endPoint.node || endPoint.offset)) {
- nodes.push(point.node);
- }
- });
- nodes = _.unique(nodes);
- // if fontawesome
- if (r.isCollapsed()) {
- nodes.push(startPoint.node);
- }
-
- // apply font: foreColor, backColor, size (the color can be use a class text-... or bg-...)
- var $font;
- var fonts = [];
- var style;
- var className;
- var i;
- if (color || bgcolor || size) {
- for (i = 0; i < nodes.length; i++) {
- node = nodes[i];
- font = dom.lastAncestor(node, dom.isFont);
- if (!font) {
- if (node.textContent.match(this.context.invoke('HelperPlugin.getRegex', 'startAndEndSpace'))) {
- node.textContent = node.textContent.replace(this.context.invoke('HelperPlugin.getRegex', 'startAndEndSpace', 'g'), '\u00A0');
- }
- font = dom.create("font");
- node.parentNode.insertBefore(font, node);
- font.appendChild(node);
- }
- fonts.push(font);
- this._applyStylesToFontNode(font, color, bgcolor, size);
- }
- }
- // remove empty values
- // we must remove the value in 2 steps (applay inherit then remove) because some
- // browser like chrome have some time an error for the rendering and/or keep inherit
- for (i = 0; i < fonts.length; i++) {
- font = fonts[i];
- if (font.style.color === "inherit") {
- font.style.color = "";
- }
- if (font.style.backgroundColor === "inherit") {
- font.style.backgroundColor = "";
- }
- if (font.style.fontSize === "inherit") {
- font.style.fontSize = "";
- }
- $font = $(font);
- if (font.style.color === '' && font.style.backgroundColor === '' && font.style.fontSize === '') {
- $font.removeAttr("style");
- }
- if (!font.className.length) {
- $font.removeAttr("class");
- }
- }
-
- // target the deepest node
- if (startPoint.node.tagName && !startPoint.offset) {
- startPoint.node = this.context.invoke('HelperPlugin.firstLeaf', startPoint.node.childNodes[startPoint.offset] || startPoint.node);
- startPoint.offset = 0;
- }
- if (endPoint.node.tagName && !endPoint.offset) {
- endPoint.node = this.context.invoke('HelperPlugin.firstLeaf', endPoint.node.childNodes[endPoint.offset] || endPoint.node);
- endPoint.offset = 0;
- }
-
- // select nodes to clean (to remove empty font and merge same nodes)
- nodes = [];
- dom.walkPoint(startPoint, endPoint, function (point) {
- nodes.push(point.node);
- });
- nodes = _.unique(nodes);
- // remove node without attributes (move content), and merge the same nodes
- for (i = 0; i < nodes.length; i++) {
- node = nodes[i];
- if (dom.isText(node) && !this.context.invoke('HelperPlugin.isVisibleText', node)) {
- continue;
- }
- font = dom.lastAncestor(node, dom.isFont);
- node = font || dom.ancestor(node, dom.isSpan);
- if (!node) {
- continue;
- }
- $font = $(node);
- className = this.context.invoke('HelperPlugin.orderClass', node);
- style = this.context.invoke('HelperPlugin.orderStyle', node);
- if (!className && !style) {
- $(node).before($(node).contents());
- if (endPoint.node === node) {
- endPoint = dom.prevPointUntil(endPoint, function (point) {
- return point.node !== node;
- });
- }
- $(node).remove();
-
- nodes.splice(i, 1);
- i--;
- continue;
- }
- var prev = font && font.previousSibling;
- while (prev && !font.tagName && !this.context.invoke('HelperPlugin.isVisibleText', prev)) {
- prev = prev.previousSibling;
- }
- if (prev &&
- font.tagName === prev.tagName &&
- className === this.context.invoke('HelperPlugin.orderClass', prev) && style === this.context.invoke('HelperPlugin.orderStyle', prev)) {
- $(prev).append($(font).contents());
- if (endPoint.node === font) {
- endPoint = dom.prevPointUntil(endPoint, function (point) {
- return point.node !== font;
- });
- }
- $(font).remove();
-
- nodes.splice(i, 1);
- i--;
- continue;
- }
- }
-
- // restore selection
- r = this.context.invoke('editor.setRange', startPoint.node, startPoint.offset, endPoint.node, endPoint.offset);
- r.normalize().select();
-
- if (target) {
- this.context.invoke('MediaPlugin.updatePopoverAfterEdit', target);
- }
- },
- /**
- * Creates the backcolor button containing its palette.
- *
- * @private
- * @returns {jQuery} the backcolor button
- */
- _createBackColorButton: function () {
- var $bgPalette = this.createPalette('backColor');
- $bgPalette.find("button:not(.note-color-btn)")
- .addClass("note-color-btn")
- .attr('data-event', 'backColor')
- .each(function () {
- var $el = $(this);
- var className = $el.hasClass('o_custom_color') ? $el.data('color') : 'bg-' + $el.data('color');
- $el.attr('data-value', className).addClass($el.hasClass('o_custom_color') ? '' : className);
- });
-
- var $bgContainer = $(QWeb.render('wysiwyg.plugin.font.paletteButton', {
- className: 'note-bg-color',
- icon: this.options.icons.bg,
- }));
- $bgContainer.find('.dropdown-menu').append($bgPalette);
- return $bgContainer;
- },
- /**
- * Creates the forecolor button containing its palette.
- *
- * @private
- * @returns {jQuery} the forecolor button
- */
- _createForeColorButton: function () {
- var $forePalette = this.createPalette('foreColor');
- $forePalette.find("button:not(.note-color-btn)")
- .addClass("note-color-btn")
- .attr('data-event', 'foreColor')
- .each(function () {
- var $el = $(this);
- var className = $el.hasClass('o_custom_color') ? $el.data('color') : 'text-' + $el.data('color');
- $el.attr('data-value', className).addClass($el.hasClass('o_custom_color') ? '' : 'bg-' + $el.data('color'));
- });
-
- var $foreContainer = $(QWeb.render('wysiwyg.plugin.font.paletteButton', {
- className: 'note-fore-color',
- icon: this.options.icons.fore,
- }));
- $foreContainer.find('.dropdown-menu').append($forePalette);
- return $foreContainer;
- },
- /**
- * Method called on custom color button click :
- * opens the color picker dialog and saves the chosen color on save.
- *
- * @private
- * @param {string} targetColor
- * @param {jQuery Event} ev
- */
- _onCustomColor: function (targetColor, ev) {
- ev.preventDefault();
- ev.stopPropagation();
-
- var self = this;
- var $button = $(ev.target).next('button');
- var target = this.context.invoke('editor.restoreTarget');
- var colorPickerDialog = new ColorpickerDialog(this, {});
-
- this.context.invoke('editor.saveRange');
- colorPickerDialog.on('colorpicker:saved', this, this._wrapCommand(function (ev) {
- $button = $button.clone().appendTo($button.parent());
- $button.show();
- $button.css('background-color', ev.data.cssColor);
- $button.attr('data-value', ev.data.cssColor);
- $button.data('value', ev.data.cssColor);
- $button.attr('title', ev.data.cssColor);
- self.context.invoke('editor.saveTarget', target);
- self.context.invoke('editor.restoreRange');
- $button.mousedown();
- }));
- colorPickerDialog.open();
- this.context.invoke('MediaPlugin.hidePopovers');
- },
-});
-
-_.extend(wysiwygOptions.icons, {
- fore: 'fa fa-font',
- bg: 'fa fa-paint-brush',
-});
-_.extend(wysiwygTranslation.color, {
- customColor: _t('Custom color'),
- fore: _t('Color'),
- bg: _t('Background color'),
-});
-
-registry.add('FontPlugin', FontPlugin);
-
-registry.addXmlDependency('/web_editor/static/src/xml/wysiwyg_colorpicker.xml');
-registry.addJob(function (wysiwyg) {
- if ('web_editor.colorpicker' in QWeb.templates) {
- return;
- }
-
- if (wysiwyg.isDestroyed()) {
- throw new Error('The Wysiwyg are destroyed before this loading');
- }
-
- var options = {};
- wysiwyg.trigger_up('getRecordInfo', {
- recordInfo: options,
- callback: function (recordInfo) {
- _.defaults(options, recordInfo);
- },
- });
- return wysiwyg._rpc({
- model: 'ir.ui.view',
- method: 'read_template',
- args: ['web_editor.colorpicker'],
- kwargs: {
- context: options.context,
- },
- }).then(function (template) {
- if (!/^<templates>/.test(template)) {
- template = _.str.sprintf('<templates>%s</templates>', template);
- }
- QWeb.add_template(template);
- });
-});
-
-
-return FontPlugin;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/font_buttons.js b/addons/web_editor/static/src/js/wysiwyg/plugin/font_buttons.js
deleted file mode 100644
index f30b16b8bfc..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/font_buttons.js
+++ /dev/null
@@ -1,31 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.font.buttons', function (require) {
-'use strict';
-
-var ButtonsPlugin = require('web_editor.wysiwyg.plugin.buttons');
-
-//--------------------------------------------------------------------------
-// override the ColorPicker button into the Toolbar and the font size button
-//--------------------------------------------------------------------------
-
-ButtonsPlugin.include({
- /**
- * @override
- */
- addToolbarButtons: function () {
- var self = this;
- this._super();
-
- this.context.memo('button.colorpicker', function () {
- return self.context.invoke('FontPlugin.createColorPickerButtons');
- });
-
- var fontsizeFunction = this.context.memo('button.fontsize');
- this.context.memo('button.fontsize', function () {
- return self.context.invoke('FontPlugin.overrideFontSizeButton', fontsizeFunction());
- });
- },
-});
-
-return ButtonsPlugin;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/help_dialog.js b/addons/web_editor/static/src/js/wysiwyg/plugin/help_dialog.js
deleted file mode 100644
index 2a28cbb221d..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/help_dialog.js
+++ /dev/null
@@ -1,27 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.HelpDialog', function (require) {
-'use strict';
-
-var Plugins = require('web_editor.wysiwyg.plugins');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-
-/**
- * Allows to customize link content and style.
- */
-var HelpDialog = Plugins.helpDialog.extend({
- /**
- * Restore the hidden close button.
- */
- showHelpDialog: function () {
- var self = this;
- return this._super().then(function () {
- self.$dialog.find('button.close span').attr('aria-hidden', 'false');
- });
- },
-});
-
-registry.add('helpDialog', HelpDialog);
-
-return {
- HelpDialog: HelpDialog,
-};
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/helper.js b/addons/web_editor/static/src/js/wysiwyg/plugin/helper.js
deleted file mode 100644
index d65c91aa682..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/helper.js
+++ /dev/null
@@ -1,1948 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.helper', function (require) {
-'use strict';
-
-var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
-var Dialog = require('web.Dialog');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-
-var dom = $.summernote.dom;
-
-
-var HelperPlugin = AbstractPlugin.extend({
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * This dictionary contains oft-used regular expressions,
- * for performance and readability purposes. It can be
- * accessed and extended by the getRegex() method.
- */
- regex: {
- char: {
- noflag: /\S|\u00A0|\u200B/,
- },
- emptyElemWithBR: {
- noflag: /^\s*<br\/?>\s*$/,
- },
- endInvisible: {
- noflag: /\u200B$/,
- },
- endNotChar: {
- noflag: /[^\S\u00A0\u200B]+$/,
- },
- endSingleSpace: {
- noflag: /[\S\u00A0\u200B]\s$/,
- },
- endSpace: {
- noflag: /\s+$/,
- },
- invisible: {
- noflag: /\u200B/,
- },
- jinja: {
- noflag: /(^|\n)\s*%\send|%\sset/,
- },
- notWhitespace: {
- noflag: /\S/,
- },
- onlyEmptySpace: {
- noflag: /^[\s\u00A0\u200B]*(<br>)?[\s\u00A0\u200B]*$/,
- },
- semicolon: {
- noflag: / ?; ?/,
- },
- space: {
- noflag: /\s+/,
- g: /\s+/g,
- },
- spaceOrNewline: {
- noflag: /[\s\n\r]+/,
- g: /[\s\n\r]+/g,
- },
- startAndEndInvisible: {
- noflag: /^\u200B|\u200B$/,
- g: /^\u200B|\u200B$/g,
- },
- startAndEndSpace: {
- noflag: /^\s+|\s+$/,
- g: /^\s+|\s+$/g,
- },
- startAndEndSemicolon: {
- noflag: /^ ?;? ?| ?;? ?$/,
- },
- startInvisible: {
- noflag: /^\u200B/,
- },
- startNotChar: {
- noflag: /^[^\S\u00A0\u200B]+/,
- },
- startSingleSpace: {
- noflag: /^\s[\S\u00A0\u200B]/,
- },
- startSpace: {
- noflag: /^\s+/,
- },
- },
-
- /**
- * Compares two nodes to see if they are similar.
- * "Similar" means that they have the same tag, styles, classes and attributes.
- *
- * @param {Node} node
- * @param {Node} otherNode
- * @returns {Boolean} true if the nodes are similar
- */
- compareNodes: function (node, otherNode) {
- if (!otherNode || !node) {
- return false;
- }
- if (node.tagName !== otherNode.tagName) {
- return false;
- }
- if (dom.isText(node)) {
- return true;
- }
- this.removeBlankAttrs(node);
- this.removeBlankAttrs(otherNode);
- this.orderClass(node);
- this.orderStyle(node);
- this.orderClass(otherNode);
- this.orderStyle(otherNode);
- var nodeAttributes = this._getAttrsNoID(node);
- var otherNodeAttributes = this._getAttrsNoID(otherNode);
- if (nodeAttributes.length !== otherNodeAttributes.length) {
- return false;
- }
- for (var i = 0; i < nodeAttributes.length; i++) {
- var attr = nodeAttributes[i];
- var otherAttr = otherNodeAttributes[i];
- if (attr.name !== otherAttr.name || attr.value !== otherAttr.value) {
- return false;
- }
- }
- return true;
- },
- /**
- * Returns the number of leading breakable space in the textNode.
- * Note: returns 0 if the node is not a textNode.
- *
- * @param {Node} node
- */
- countLeadingBreakableSpace: function (node) {
- if (!dom.isText(node)) {
- return 0;
- }
- var clone = $(node).clone()[0];
- var breakableSpace = this.removeExtremeBreakableSpace(clone, 0).start;
- return breakableSpace === 1 ? 0 : breakableSpace;
- },
- /**
- * Returns the number of trailing breakable space in the textNode.
- * Note: returns 0 if the node is not a textNode.
- *
- * @param {Node} node
- */
- countTrailingBreakableSpace: function (node) {
- if (!dom.isText(node)) {
- return 0;
- }
- var clone = $(node).clone()[0];
- var breakableSpace = this.removeExtremeBreakableSpace(clone, 0).end;
- return breakableSpace === 1 ? 0 : breakableSpace;
- },
- /**
- * Remove the dom between 2 points (respecting unbreakable rules).
- * Returns an object::
- *
- * {
- * node: pointA.node (updated if necessary),
- * offset: pointA.offset (updated if necessary),
- * changed: bool (whether changes were applied)
- * }
- *
- * @param {Object} pointA
- * @param {Node} pointA.node
- * @param {number} pointA.offset
- * @param {Object} pointB
- * @param {Node} pointB.node
- * @param {number} pointB.offset
- * @returns {Object} {node, offset, changed}
- */
- deleteBetween: function (pointA, pointB) {
- var self = this;
- if (pointB.node.childNodes[pointB.offset]) {
- var firstLeaf = this.firstLeaf(pointB.node.childNodes[pointB.offset]);
- pointB = this.makePoint(firstLeaf, 0);
- }
- if (pointB.node.tagName && pointB.node.tagName !== 'BR' && pointB.offset >= dom.nodeLength(pointB.node)) {
- pointB = dom.nextPoint(pointB);
- }
- var changed;
- var commonAncestor = dom.commonAncestor(pointA.node, pointB.node);
-
- var ecAncestor = dom.ancestor(pointB.node, function (node) {
- return node === commonAncestor || self.options.isUnbreakableNode(node.parentNode);
- });
- var next = this.splitTree(ecAncestor, pointB, {
- nextText: true,
- });
-
- var scAncestor = dom.ancestor(pointA.node, function (node) {
- return node === commonAncestor || self.options.isUnbreakableNode(node.parentNode);
- });
- if (dom.isIcon(pointA.node)) {
- pointA = dom.prevPoint(pointA);
- }
- this.splitTree(scAncestor, pointA, {
- nextText: true,
- });
- pointA.offset = dom.nodeLength(pointA.node);
-
- var nodes = [];
- dom.nextPointUntil(pointA, function (point) {
- if (point.node === next || !point.node) {
- return true;
- }
- if (dom.isText(point.node) && point.offset) {
- return;
- }
- var target = point.node.childNodes[point.offset] || point.node;
- if (target === pointA.node || $.contains(target, pointA.node) || target === next || $.contains(target, next)) {
- return;
- }
- if (
- nodes.indexOf(target) === -1 && !dom.ancestor(target, function (target) {
- return nodes.indexOf(target) !== -1;
- })
- ) {
- nodes.push(target);
- }
- });
- $(nodes).remove();
-
- changed = !!nodes.length;
- var toMerge = changed && pointA.node.parentNode !== next.parentNode;
-
-
- var point = this.makePoint(this.firstLeaf(next), 0);
- if (nodes.length > 1 || nodes.length && !dom.isText(nodes[0])) {
- point = this.removeEmptyInlineNodes(point);
- }
-
- // Remove whole li/ul/ol if deleted all contents of li/ul/ol
- var ul = dom.ancestor(next, function (n) {
- return n.tagName === 'UL' || n.tagName === 'OL';
- });
- if (ul && next[dom.isText(next) ? 'textContent' : 'innerHTML'] === '' && pointA.node !== next.previousSibling) {
- var toRemove = next;
- while (
- toRemove !== ul && toRemove.parentNode &&
- !this.options.isUnbreakableNode(toRemove.parentNode) &&
- this.isBlankNode(toRemove.parentNode)
- ) {
- toRemove = toRemove.parentNode;
- }
- $(toRemove).remove();
- }
- if (!$.contains(this.editable, pointA.node)) {
- pointA = point;
- }
-
- if (toMerge) {
- pointA = this.deleteEdge(pointA.node, 'next') || pointA;
- }
-
- return {
- node: pointA.node,
- offset: pointA.offset,
- changed: changed,
- };
- },
- /**
- * Remove the edge between a node and its sibling
- * (= merge the nodes, respecting unbreakable rules).
- *
- * @param {Node} node
- * @param {String('next'|'prev')} direction
- * @param {Boolean} doNotTryNonSim true to not try merging non-similar nodes
- * @returns {Object} {node, offset}
- */
- deleteEdge: function (node, direction, doNotTryNonSim) {
- var prevOrNext = direction === 'prev' ? 'previousSibling' : 'nextSibling';
- var result = false;
- var startN = node;
-
- if (node.tagName === 'BR' && node.nextSibling && !(dom.isText(node.nextSibling) && !this.isVisibleText(node.nextSibling))) {
- node = node.nextSibling;
- node = this.firstLeaf(node);
- }
-
- var nodes = [];
- var next;
- while (node && node !== this.editable && !this.options.isUnbreakableNode(node)) {
- nodes.push(node);
-
- next = node[prevOrNext];
- while (next && !next.tagName) {
- if (!this.getRegex('char').test(next.textContent)) {
- next = next[prevOrNext];
- continue;
- }
- break;
- }
-
- if (next) {
- break;
- }
- node = node.parentNode;
- }
-
- if (next && next.tagName === 'TABLE') {
- return this.makePoint(node, 0);
- }
-
- var ifBrRemovedAndMerge = !_.filter(nodes, this.isNodeBlockType.bind(this)).length;
- var brRemoved = false;
-
- var range = this.context.invoke('editor.createRange');
-
- var spaceToRemove = [];
- var offset;
- while ((node = nodes.pop())) {
- next = node[prevOrNext];
- while (next && !next.tagName) {
- if (!this.getRegex('char').test(next.textContent)) {
- spaceToRemove.push(next);
- next = next[prevOrNext];
- continue;
- }
- break;
- }
-
- // handle merge of text with p (useful in case of <li>text</li> with <li><p>text</p></li>)
- if (next && dom.isText(node) && this.isNodeBlockType(next)) {
- node = [next, next = node][0]; // swap node and next
- direction = direction === 'next' ? 'prev' : 'next'; // switch direction
- }
- if (next && dom.isText(next) && this.isNodeBlockType(node)) {
- if (this.isBlankNode(node, true)) {
- $(node).empty();
- }
- $(node)[direction === 'next' ? 'append' : 'prepend'](next);
- offset = direction === 'next' ? 0 : dom.nodeLength(next);
- if (dom.isText(next[prevOrNext])) {
- // merge text if needed
- var sib = next[prevOrNext];
- var firstText = (direction === 'next' ? sib : next).textContent;
- var secondText = (direction === 'next' ? next : sib).textContent;
- next.textContent = firstText + secondText;
- $(sib).remove();
- offset = firstText.length;
- }
- result = this.makePoint(next, offset);
- break;
- }
-
- if (
- !next ||
- !(node.tagName || next.tagName === 'BR') ||
- !next.tagName
- ) {
- continue;
- }
-
- if (!brRemoved && next.tagName === 'BR' && (!next[prevOrNext] || this.compareNodes(node, next[prevOrNext]))) {
- var newNext = next[prevOrNext];
- $(next).remove();
- next = newNext;
- offset = (next ? direction === 'prev' : direction === 'next') ? dom.nodeLength(next) : 0;
- result = this.makePoint(next || node, offset);
- if (!ifBrRemovedAndMerge) {
- continue;
- }
- brRemoved = true;
- ifBrRemovedAndMerge = false;
- }
-
- if (!this.compareNodes(node, next)) {
- continue;
- }
- _.each(spaceToRemove, function (space) {
- $(space).remove();
- });
- spaceToRemove = [];
- next = node[prevOrNext];
- var $next = $(next);
- if (next.tagName) {
- var textNode;
- var nextTextNode;
- var deep;
- if (direction === 'prev') {
- textNode = this.firstLeaf(node);
- if (!textNode.tagName && !dom.ancestor(textNode, dom.isPre)) {
- this.removeExtremeBreakableSpace(textNode);
- range.so = range.eo = 0;
- nextTextNode = this.lastLeaf(next);
- if (!nextTextNode.tagName && !dom.ancestor(nextTextNode, dom.isPre)) {
- this.removeExtremeBreakableSpace(nextTextNode);
- }
- }
- deep = this.lastLeaf(next);
- result = this.makePoint(deep, dom.nodeLength(deep));
- if (
- this.getRegex('char').test(node.textContent) || node.childElementCount > 1 ||
- node.firstElementChild && node.firstElementChild.tagName !== "BR"
- ) {
- $next.append($(node).contents());
- }
- $(node).remove();
- } else {
- nextTextNode = this.firstLeaf(next);
- if (!nextTextNode.tagName && !dom.ancestor(nextTextNode, dom.isPre)) {
- this.removeExtremeBreakableSpace(nextTextNode);
- textNode = this.lastLeaf(node);
- if (!textNode.tagName && !dom.ancestor(textNode, dom.isPre)) {
- this.removeExtremeBreakableSpace(textNode);
- range.so = range.eo = dom.nodeLength(node);
- }
- }
- if (node.innerHTML.trim() === '<br>') {
- $(node).contents().remove();
- }
- deep = this.lastLeaf(node);
- result = this.makePoint(deep, dom.nodeLength(deep));
- $(node).append($next.contents());
- $next.remove();
- }
- continue;
- } else if (!this.getRegex('char').test(next.textContent)) {
- result = this.makePoint(node, direction === 'prev' ? 0 : dom.nodeLength(node));
- $next.remove();
- continue;
- }
-
- break;
- }
-
- if (!result && startN && !doNotTryNonSim) {
- result = this.deleteNonSimilarEdge(startN, direction);
- }
-
- return result;
- },
- /**
- * Find and delete the previous/next non-similar edge if possible.
- * "Similar" means that they have the same tag, styles, classes and attributes.
- *
- * @param {Node} node
- * @param {String} direction 'prev' or 'next'
- * @returns {false|Object} {node, offset}
- */
- deleteNonSimilarEdge: function (node, direction) {
- var next = node[direction === 'next' ? 'nextSibling' : 'previousSibling'];
- while (
- next && dom.isText(next) &&
- this.getRegexBlank({
- space: true,
- invisible: true,
- }).test(next.textContent)
- ) {
- next = next[direction === 'next' ? 'nextSibling' : 'previousSibling'];
- }
-
- if (next) {
- return;
- }
-
- node = this.firstBlockAncestor(node);
-
- if (this.options.isUnbreakableNode(node)) {
- return;
- }
-
- var point = this.makePoint(node, direction === 'prev' ? 0 : dom.nodeLength(node));
- var otherBlock = this.findNextBlockToMerge(point.node, direction);
-
- if (!otherBlock) {
- return;
- }
-
- var blockToMergeFrom = direction === 'next' ? otherBlock : point.node;
- var blockToMergeInto = direction === 'next' ? point.node : otherBlock;
-
- // empty tag are removed
- if (
- this.getRegexBlank({
- space: true,
- newline: true,
- }).test(blockToMergeInto.textContent) &&
- !$(blockToMergeInto).find('.fa').length && $(blockToMergeInto).find('br').length <= 1
- ) {
- $(blockToMergeInto).remove();
- return this.makePoint(this.firstLeaf(blockToMergeFrom), 0);
- }
-
- // UL/OL can only have LI, UL or OR as direct children
- if (dom.isList(blockToMergeInto) && !dom.isLi(blockToMergeFrom) && !dom.isList(blockToMergeFrom)) {
- return;
- }
-
- return this.mergeNonSimilarBlocks(blockToMergeFrom, blockToMergeInto);
- },
- /**
- * Deletes the contents of the selected DOM.
- *
- * @returns {Boolean} true if there was a selection to delete
- */
- deleteSelection: function () {
- var range = this.context.invoke('editor.createRange');
- if (range.isCollapsed()) {
- return false;
- }
- var point = this.deleteBetween(range.getStartPoint(), range.getEndPoint());
- point = this.fillEmptyNode(point);
-
- range = this.context.invoke('editor.setRange', point.node, point.offset);
- range = range.select();
-
- this.editable.normalize();
- this.context.invoke('editor.saveRange');
-
- // remove tooltip when remove DOM nodes
- $('body > .tooltip').tooltip('hide');
-
- return true;
- },
- /**
- * Fill up an empty node so as to allow the carret to go inside it.
- * A block node will be filled with a <br>, with the offset before it.
- * An inline node will be filled with two zero-width spaces, with the offset in between the two.
- * Returns the given point (with the completed node and the updated offset).
- *
- * @param {Object} point {node, offset}
- * @returns {Object} {node, offset}
- */
- fillEmptyNode: function (point) {
- if (
- !point.node.tagName && this.getRegexBlank({
- space: true,
- invisible: true,
- nbsp: true,
- }).test(point.node.parentNode.innerHTML)
- ) {
- point.node = point.node.parentNode;
- point.offset = 0;
- }
- if (
- point.node.tagName && point.node.tagName !== 'BR' &&
- this.getRegexBlank({
- space: true,
- invisible: true,
- nbsp: true,
- }).test(point.node.innerHTML)
- ) {
- var text = this.document.createTextNode('');
- point.node.innerHTML = '';
- point.node.appendChild(text);
- point.node = text;
- point.offset = 0;
- }
- if (point.node.parentNode.innerHTML === '') {
- if (this.isNodeBlockType(point.node.parentNode)) {
- var node = point.node.parentNode;
- node.innerHTML = '<br/>';
- point.node = node.firstChild;
- point.offset = 0;
- } else {
- point.node.textContent = '\u200B\u200B';
- point.offset = 1;
- }
- }
- return point;
- },
- /**
- * Get the "format" ancestors list of nodes.
- * In this context, a "format" node is understood as
- * an editable block or an editable element expecting text
- * (eg.: p, h1, span).
- *
- * @param {Node[]} nodes
- * @returns {Node[]}
- */
- filterFormatAncestors: function (nodes) {
- var self = this;
- var selectedNodes = [];
- _.each(this.filterLeafChildren(nodes), function (node) {
- var ancestor = dom.ancestor(node, function (node) {
- return dom.isCell(node) || (
- !self.options.isUnbreakableNode(node) &&
- (self.isFormatNode(node) || self.isNodeBlockType(node))
- ) && self.editable !== node;
- });
- if (!ancestor) {
- ancestor = node;
- }
- if (dom.isCell(ancestor)) {
- ancestor = node;
- }
- if (ancestor && selectedNodes.indexOf(ancestor) === -1) {
- selectedNodes.push(ancestor);
- }
- });
- return selectedNodes;
- },
- /**
- * Get the "leaf" children of a list of nodes.
- * In this context, a "leaf" is understood as
- * either a text node or a node that doesn't expect text contents.
- *
- * @param {Node[]} nodes
- * @returns {Node[]}
- */
- filterLeafChildren: function (nodes) {
- var self = this;
- return _.compact(_.map(nodes, function (node) {
- if (node.firstChild) {
- node = node.firstChild;
- }
- if (
- node.tagName === "BR" ||
- self.isVisibleText(node) ||
- dom.isFont(node) ||
- dom.isImg(node) ||
- dom.isDocument(node)
- ) {
- return node;
- }
- }));
- },
- /**
- * Find the previous/next non-similar block to merge with.
- * "Similar" means that they have the same tag, styles, classes and attributes.
- *
- * @param {Node} node
- * @param {String} direction 'prev' or 'next
- * @returns {false|Node}
- */
- findNextBlockToMerge: function (node, direction) {
- var self = this;
- var startNode = node;
- var mergeableTags = this.options.styleTags.join(', ') + ', li';
- var blockToMerge = false;
-
- var li = dom.ancestor(node, function (n) {
- return n !== node && self.isNodeBlockType(n) || dom.isLi(n);
- });
- var nextElementSibling = direction === 'next' ? 'nextElementSibling' : 'previousElementSibling';
- li = li && dom.isLi(li) ? li : undefined;
- if (li) {
- if (li[nextElementSibling]) {
- node = li;
- } else {
- node = dom.ancestor(node, function (n) {
- return ((n.tagName === 'UL' || n.tagName === 'OL') && n[nextElementSibling] &&
- (direction === 'next' || dom.isList(n[nextElementSibling])));
- });
- }
- }
-
- if (!node || !node[nextElementSibling]) {
- return false;
- }
-
- node = node[nextElementSibling];
-
- var ulFoldedSnippetNode = dom.ancestor(node, function (n) {
- return $(n).hasClass('o_ul_folded');
- });
- var ulFoldedSnippetStartNode = dom.ancestor(startNode, function (n) {
- return $(n).hasClass('o_ul_folded');
- });
- if (
- (this.options.isUnbreakableNode(node) && (!ulFoldedSnippetNode || ulFoldedSnippetNode === this.editable)) &&
- this.options.isUnbreakableNode(startNode) && (!ulFoldedSnippetStartNode || ulFoldedSnippetStartNode === this.editable)
- ) {
- return false;
- }
-
- node = this.firstBlockAncestor(node);
-
- li = dom.isList(node) ? undefined : dom.ancestor(node, function (n) {
- return n !== node && self.isNodeBlockType(n) || dom.isLi(n);
- });
- li = li && dom.isLi(li) ? li : undefined;
- node = li || node;
-
- if (node.tagName === 'UL' || node.tagName === 'OL') {
- node = node[direction === 'next' ? 'firstElementChild' : 'lastElementChild'];
- }
-
- if (this.options.isUnbreakableNode(node)) {
- return false;
- }
-
- if (node === startNode || $(node).has(startNode).length || $(startNode).has(node).length) {
- return false;
- }
-
- var $mergeable = $(node).find('*').addBack()
- .filter(mergeableTags)
- .filter(function (i, n) {
- if (!(n.tagName === 'LI' && $(n).find(mergeableTags).length)) {
- return n;
- }
- });
- if ($mergeable.length) {
- blockToMerge = $mergeable[direction === 'next' ? 'first' : 'last']()[0] || false;
- }
-
- return blockToMerge;
- },
- /**
- * Get the first ancestor of a node, that is of block type (or itself).
- *
- * @param {Node} node
- * @returns {Node}
- */
- firstBlockAncestor: function (node) {
- var self = this;
- return dom.ancestor(node, function (n) {
- return self.isNodeBlockType(n);
- });
- },
- /**
- * Get the first leaf of a node, that is editable and not a media.
- * In this context, a leaf node is understood as a childless node.
- *
- * @param {Node} node
- * @returns {Node}
- */
- firstLeaf: function (node) {
- while (node.firstChild && !dom.isMedia(node) && this.options.isEditableNode(node)) {
- node = node.firstChild;
- }
- return node;
- },
- /**
- * Get the first leaf of a node, that is an element and not a BR.
- * In this context, a leaf node is understood as a childless node.
- *
- * @param {Node} node
- * @returns {Node} node
- */
- firstNonBRElementLeaf: function (node) {
- while (node.firstElementChild && node.firstElementChild.tagName !== 'BR') {
- node = node.firstElementChild;
- }
- return node;
- },
- /**
- * Returns the node targeted by a path
- *
- * @param {Object[]} path list of object (tagName, offset)
- * @returns {Node}
- */
- fromPath: function (path) {
- var node = this.editable;
- var to;
- path = path.slice();
- while ((to = path.shift())) {
- node = _.filter(node.childNodes, function (node) {
- return !to.tagName && node.tagName === 'BR' || node.tagName === to.tagName;
- })[to.offset];
- }
- return node;
- },
- /**
- * Returns (and creates if necessary) a regular expression.
- * If a regular expression with the given name exists, simply returns it.
- * Otherwise, creates a new one with the given name, exp and flag.
- *
- * @param {String} name
- * @param {String} [flag] optional
- * @param {String} [exp] optional
- * @returns {RegExp}
- */
- getRegex: function (name, flag, exp) {
- var flagName = flag || 'noflag';
- flag = flag || '';
- // If the regular expression exists, but not with this flag:
- // retrieve whichever version of it and apply the new flag to it,
- // then save that new version in the `regex` object.
- if (this.regex[name] && !this.regex[name][flagName]) {
- if (exp) {
- console.warn("A regular expression already exists with the name: " + name + ". The expression passed will be ignored.");
- }
- var firstVal = this.regex[name][Object.keys(this.regex[name])[0]];
- this.regex[name][flagName] = new RegExp(firstVal, flag);
- } else if (!this.regex[name]) {
- // If the regular expression does not exist:
- // save it into the `regex` object, with the name, expression
- // and flag passed as arguments (if any).
- if (!exp) {
- throw new Error("Cannot find a regular expression with the name " + name + ". Pass an expression to create it.");
- }
- this.regex[name] = {};
- this.regex[name][flagName] = new RegExp(exp, flag);
- }
- return this.regex[name][flagName];
- },
- /**
- * Returns (and creates if necessary) a regular expression
- * targetting a string made ONLY of some combination of the
- * characters enabled with options.
- * If a regular expression with the given options exists, simply returns it.
- * eg: getRegexBlank({space: true, nbsp: true}) => /^[\s\u00A0]*$/
- *
- * @param {Object} [options] optional
- * @param {Boolean} options.not ^ (not all that follows)
- * @param {Boolean} options.space \s (a whitespace)
- * @param {Boolean} options.notspace \S (not a whitespace)
- * @param {Boolean} options.nbsp \u00A0 (a non-breakable space)
- * @param {Boolean} options.invisible \u200B (a zero-width character)
- * @param {Boolean} options.newline \n|\r (a new line or a carriage return)
- * @param {Boolean} options.atLeastOne + (do not target blank strings)
- * @returns {RegExp}
- */
- getRegexBlank: function (options) {
- options = options || {};
- var charMap = {
- notspace: {
- name: 'NotSpace',
- exp: '\\S',
- },
- space: {
- name: 'Space',
- exp: '\\s',
- },
- nbsp: {
- name: 'Nbsp',
- exp: '\\u00A0',
- },
- invisible: {
- name: 'Invisible',
- exp: '\\u200B',
- },
- newline: {
- name: 'Newline',
- exp: '\\n\\r',
- },
- };
- var name = 'only';
- var exp = '';
- var atLeastOne = options.atLeastOne;
- options.atLeastOne = false;
-
- // Build the expression and its name
- if (options.not) {
- name += 'Not';
- exp += '^';
- options.not = false;
- }
- _.each(options, function (value, key) {
- if (value && charMap[key]) {
- name += charMap[key].name;
- exp += charMap[key].exp;
- }
- });
-
- exp = '^[' + exp + ']' + (atLeastOne ? '+' : '*') + '$';
- name += atLeastOne ? 'One' : '';
- return this.getRegex(name, undefined, exp);
- },
- /**
- * Returns a list of all selected nodes in the range.
- *
- * @returns {Node []}
- */
- getSelectedNodes: function () {
- var range = this.context.invoke('editor.createRange');
- if (!range.isCollapsed()) {
- if (range.so && !range.sc.tagName) {
- if (range.sc === range.ec) {
- range.sc = range.ec = range.sc.splitText(range.so);
- range.eo -= range.so;
- } else {
- range.sc = range.sc.splitText(range.so);
- }
- range.so = 0;
- }
- if (!range.ec.tagName && range.eo !== dom.nodeLength(range.ec) && !range.ec.tagName) {
- range.ec.splitText(range.eo);
- }
- range.select();
- }
- var res = [range.sc];
- var point = range.getStartPoint();
- var prevNode = range.sc;
- dom.nextPointUntil(point, function (pt) {
- if (pt.node !== prevNode) {
- var ok = true;
- // Return only the smallest traversed children
- _.each(res, function (n, i) {
- // If pt.node is a child of res[i], replace it
- if (dom.listAncestor(pt.node).indexOf(n) !== -1) {
- res[i] = pt.node;
- ok = false;
- } else if (dom.listAncestor(n).indexOf(pt.node) !== -1) {
- // Inversely, skip parents of res[i]
- ok = false;
- }
- });
- if (ok) {
- res.push(pt.node);
- }
- }
- prevNode = pt.node;
- return pt.node === range.ec;
- });
- return _.uniq(res);
- },
- /**
- * Returns a list of all selected text nodes in the range.
- *
- * @returns {Node []}
- */
- getSelectedText: function () {
- var self = this;
- var selectedText = _.filter(this.getSelectedNodes(), function (node) {
- node = self.firstLeaf(node);
- return self.isVisibleText(node);
- });
- return _.uniq(selectedText);
- },
- /**
- * Returns true if the value contains jinja logic.
- *
- * @returns {Boolean}
- */
- hasJinja: function (value) {
- return this.getRegex('jinja').test(value);
- },
- /**
- * Inserts a block node (respecting the rules of unbreakable nodes).
- * In order to insert the node, the DOM tree is split at the carret position.
- * If there is a selection, it is deleted first.
- *
- * @param {Node} node
- */
- insertBlockNode: function (node) {
- var self = this;
- var range = this.context.invoke('editor.createRange');
- range = range.deleteContents();
- var point = range.getStartPoint();
- var unbreakable = point.node;
- if (!this.options.isUnbreakableNode(point.node)) {
- unbreakable = dom.ancestor(point.node, function (node) {
- return self.options.isUnbreakableNode(node.parentNode) || node === self.editable;
- }) || point.node;
- }
-
- if (unbreakable === point.node && !point.offset && point.node.tagName !== 'P') {
- if (point.node.innerHTML === '<br>') {
- $(point.node.firstElementChild).remove();
- }
- if (point.node.tagName === "BR") {
- $(point.node).replaceWith(node);
- } else {
- point.node.append(node);
- }
- return;
- }
- if (!this.options.isUnbreakableNode(point.node)) {
- var tree = dom.splitTree(unbreakable, point, {
- isSkipPaddingBlankHTML: true,
- isNotSplitEdgePoint: true,
- });
- if ((!tree || $.contains(tree, range.sc)) && (point.offset || point.node.tagName)) {
- tree = tree || dom.ancestor(point.node, function (node) {
- return self.options.isUnbreakableNode(node.parentNode);
- });
- $(tree).after(node);
- } else {
- $(tree).before(node);
- }
- } else {
- // prevent unwrapped text in unbreakable
- if (dom.isText(unbreakable)) {
- $(unbreakable).wrap(this.document.createElement('p'));
- unbreakable.splitText(point.offset);
- unbreakable = unbreakable.parentNode;
- point.offset = 1;
- }
- $(unbreakable.childNodes[point.offset]).before(node);
- }
- if (range.sc.innerHTML === '<br>') {
- var clone = range.sc.cloneNode(true);
- if (node.previousSibling === range.sc) {
- $(node).after(clone);
- } else if (node.nextSibling === range.sc) {
- $(node).before(clone);
- }
- }
- },
- /**
- * Inserts a string as a text node in the DOM.
- * If the range is on a text node, splits the text node first.
- * Otherwise just inserts the text node.
- * Wraps it in a P if needed.
- *
- * @param {String} text
- */
- insertTextInline: function (text) {
- if (text === " ") {
- text = "\u00A0";
- }
- this.editable.normalize();
- this.deleteSelection();
-
- var range = this.context.invoke('editor.createRange');
-
-
- if (!range.sc.tagName && range.sc.textContent.match(/\S/)) {
- var before = range.sc.textContent.slice(0, range.so);
- var after = range.sc.textContent.slice(range.so);
-
- if (
- (before.length || after.length) &&
- (!before.length || before[before.length - 1] === ' ') &&
- (!after.length || after[0] === ' ')
- ) {
- var startSpace = this.getRegex('startSpace');
- var endSpace = this.getRegex('endSpace');
- before = before.replace(endSpace, '\u00A0');
- after = after.replace(startSpace, '\u00A0');
- range.sc.textContent = before + after;
- if (range.so > before.length) {
- range.so = range.eo = before.length;
- }
- range.select();
- }
- }
-
-
- range = range.deleteContents();
- range = this._standardizeRangeOnEdge(range);
- var textNode = this._insertTextNodeInEditableArea(range, text);
-
- // if the text node can't be inserted in the dom (not editable area) do nothing
- if (!textNode) {
- return;
- }
-
- this.secureExtremeSingleSpace(range.sc);
-
- this._wrapTextWithP(textNode);
-
- range = this.context.invoke('editor.setRange', textNode, text.length);
- range.select();
- this.context.invoke('editor.saveRange');
-
- // Clean up and make leading/trailing/multiple space visible
- this.normalize();
- range = this.context.invoke('editor.createRange');
- var reStartBlanks = this.getRegex('startBlanks', '', '^([\\s\\u00A0\\u200B]*)');
- var reEndBlanks = this.getRegex('endBlanks', '', '([\\s\\u00A0\\u200B]*)$');
- var reAllNBSP = /\u00A0/g;
- var reMultipleSpace = /(\s){2,}/;
-
- var textContent = range.sc.textContent;
- var startBlanks = range.sc.textContent.match(reStartBlanks)[0] || '';
- var endBlanks = range.sc.textContent.match(reEndBlanks)[0] || '';
- var trimmed = range.sc.textContent.replace(reStartBlanks, '').replace(reEndBlanks, '');
- // Remove the single inner nbsp's and replace the multiple inner spaces with nbsp's
- var cleanContents = trimmed.replace(reAllNBSP, ' ')
- .replace(reMultipleSpace, function (space) {
- return Array(space.length + 1).join('\u00A0');
- });
- // Keep the leading/trailing whitespace, nbsp's and zero-width chars
- range.sc.textContent = startBlanks + cleanContents + endBlanks;
-
- this._removeInvisibleChar(range);
-
- try {
- range.select();
- } catch (e) {
- /* Under certain unknown conditions, the range cannot be
- applied and triggered an error. In order to allow the user to
- keep editing, we need to prevent the dialog from showing, hence
- the use of `console.error`.
- If this error appears, the carret is moved to the beginning
- of the focused node.*/
- console.error(e);
- var log = [
- ['textContent', textContent],
- ['trimmed', trimmed],
- ['cleanContents', cleanContents],
- ];
- if (range) {
- log.push(
- ['start', range.sc.outerHTML || range.sc.textContent, range.so],
- ['end', range.ec.outerHTML || range.ec.textContent, range.eo],
- ['parent', range.sc.parentNode && range.sc.parentNode.outerHTML]);
- }
- console.error('Informations for debugging', log);
- }
- this.context.invoke('editor.saveRange');
- },
- /**
- * Returns true if the node is a text node containing nothing
- *
- * @param {Node} node
- * @returns {Boolean}
- */
- isBlankText: function (node) {
- return dom.isText(node) &&
- this.getRegexBlank({
- not: true,
- notspace: true,
- nbsp: true,
- invisible: true,
- })
- .test(node.textContent);
- },
- /**
- * Returns true if the node is blank.
- * In this context, a blank node is understood as
- * a node expecting text contents (or with children expecting text contents)
- * but without any.
- *
- * @param {Node} node
- * @param {Boolean} brIsBlank true to consider nodes with BR element
- * as ultimate only child to be blank
- * @returns {Boolean}
- */
- isBlankNode: function (node, brIsBlank) {
- var self = this;
- if (brIsBlank && dom.isBR(node)) {
- return true;
- }
- if (dom.isVoid(node) || dom.isIcon(node)) {
- return false;
- }
- if (this.getRegexBlank({
- space: true,
- }).test(node[dom.isText(node) ? 'textContent' : 'innerHTML'])) {
- return true;
- }
- var areAllChildrenBlank = _.all(node.childNodes, function (node) {
- return self.isBlankNode(node, brIsBlank);
- });
- if (node.childNodes.length && areAllChildrenBlank) {
- return true;
- }
- return false;
- },
- /**
- * Returns true if the point is on the left/right edge of the first
- * previous/next point with the given tag name (skips insignificant nodes).
- *
- * @param {Object} point
- * @param {String} tagName
- * @param {String('left'|'right')} side
- * @returns {Boolean}
- */
- isEdgeOfTag: function (point, tagName, side) {
- var method = side === 'left' ? 'isLeftEdgePoint' : 'isRightEdgePoint';
- var prevOrNext = side === 'left' ? 'prev' : 'next';
- var newPt;
- var first = true;
- while (point && point.node.tagName !== tagName) {
- newPt = this.skipNodes(point, prevOrNext, function (pt) {
- return pt.node.tagName === tagName && dom[method](pt);
- });
- if (newPt.node.tagName === tagName || newPt.node.tagName === 'BR') {
- point = newPt;
- break;
- }
- if (newPt === point && (!first || dom.isText(point.node) && !dom[method](point))) {
- break;
- }
- point = dom[prevOrNext + 'Point'](newPt);
- first = false;
- }
- if (!point) {
- return false;
- }
- var ancestor = dom.ancestor(point.node, function (n) {
- return n.tagName === tagName;
- });
- return !!(ancestor && dom[method + 'Of'](point, ancestor));
- },
- /**
- * Returns true if the node is a "format" node.
- * In this context, a "format" node is understood as
- * an editable block or an editable element expecting text
- * (eg.: p, h1, span).
- *
- * @param {Node} node
- * @returns {Boolean}
- */
- isFormatNode: function (node) {
- return node.tagName && this.options.styleTags.indexOf(node.tagName.toLowerCase()) !== -1;
- },
- /**
- * Returns true if the node is within a table.
- *
- * @param {Node} node
- * @returns {Boolean}
- */
- isInTable: function (node) {
- return !!dom.ancestor(node, function (n) {
- return n.tagName === 'TABLE';
- });
- },
- /**
- * Returns true if the point is on the left edge of a block node
- * (skips insignificant nodes).
- *
- * @param {Object} point
- * @returns {Boolean}
- */
- isLeftEdgeOfBlock: function (point) {
- point = this.skipNodes(point, 'prev');
- return dom.isLeftEdgePointOf(point, this.firstBlockAncestor(point.node));
- },
- /**
- * Returns true if the point is on the left edge of the first
- * previous point with the given tag name (skips insignificant nodes).
- *
- * @param {Object} point
- * @param {String} tagName
- * @returns {Boolean}
- */
- isLeftEdgeOfTag: function (point, tagName) {
- return this.isEdgeOfTag(point, tagName, 'left');
- },
- /**
- * Returns true if the node is a block.
- *
- * @param {Node} node
- * @returns {Boolean}
- */
- isNodeBlockType: function (node) {
- if (dom.isText(node)) {
- return false;
- }
- var display = this.window.getComputedStyle(node).display;
- // All inline elements have the word 'inline' in their display value, except 'contents'
- return display.indexOf('inline') === -1 && display !== 'contents';
- },
- /**
- * Returns true if the point is on the right edge of the first
- * next point with the given tag name (skips insignificant nodes).
- *
- * @param {Object} point
- * @param {String} tagName
- * @returns {Boolean}
- */
- isRightEdgeOfTag: function (point, tagName) {
- return this.isEdgeOfTag(point, tagName, 'right');
- },
- /**
- * Returns true if point should be ignored.
- * This is generally used for trying to figure out if the point is an edge point.
- *
- * @param {Object} point
- * @param {String} direction ('prev' or 'next')
- * @param {Object} options
- * @param {Boolean} options.noSkipBlankText true to not skip blank text
- * @param {Boolean} options.noSkipSingleBRs true to not skip single BRs
- * @param {Boolean} options.noSkipExtremeBreakableSpace true to not skip leading/trailing breakable space
- * @param {Boolean} options.noSkipParent true to not skip to leaf nodes or offset 0
- * @param {Boolean} options.noSkipSibling true to not skip if on edge and sibling is skippable
- * @returns {Boolean}
- */
- isSkippable: function (point, direction, options) {
- options = options || {};
- var isEdge = direction === 'prev' ? dom.isLeftEdgePoint(point) : dom.isRightEdgePoint(point);
-
- // skip blank text nodes
- if (
- !options.noSkipBlankText &&
- this.isBlankText(point.node)
- ) {
- return true;
- }
- // skip single BRs
- if (
- !options.noSkipSingleBRs &&
- point.node.tagName === 'BR' &&
- (!point.node.previousSibling || this.isBlankText(point.node.previousSibling)) &&
- (!point.node.nextSibling || this.isBlankText(point.node.nextSibling))
- ) {
- return true;
- }
- // skip leading/trailing breakable space
- if (
- !options.noSkipExtremeBreakableSpace &&
- (direction === 'prev' && !isEdge && point.offset <= this.countLeadingBreakableSpace(point.node) ||
- direction === 'next' && point.offset > dom.nodeLength(point.node) - this.countTrailingBreakableSpace(point.node))
- ) {
- return true;
- }
- // skip to leaf node or edge
- var node = direction === 'prev' ? point.node.childNodes[0] : point.node.childNodes[point.node.childNodes.length - 1];
- var offset = direction === 'prev' ? 0 : dom.nodeLength(node);
- if (
- !options.noSkipParent &&
- !isEdge && point.node.childNodes.length &&
- this.isSkippable(this.makePoint(node, offset), direction, options)
- ) {
- return true;
- }
- // skip if on edge and sibling is skippable
- var sibling = direction === 'prev' ? point.node.previousSibling : point.node.nextSibling;
- offset = direction === 'prev' ? 0 : dom.nodeLength(sibling);
- if (
- !options.noSkipSibling &&
- isEdge && sibling &&
- this.isSkippable(this.makePoint(sibling, offset), direction, _.defaults({
- noSkipSibling: true,
- }, options))
- ) {
- return true;
- }
- return false;
- },
- /**
- * Returns true if the node is a text node with visible text.
- *
- * @param {Node} node
- * @returns {Boolean}
- */
- isVisibleText: function (node) {
- return !node.tagName && this.getRegex('char').test(node.textContent);
- },
- /**
- * Get the last leaf of a node, that is editable and not a media.
- * In this context, a leaf node is understood as a childless node.
- *
- * @param {Node} node
- * @returns {Node}
- */
- lastLeaf: function (node) {
- while (node.lastChild && !dom.isMedia(node) && this.options.isEditableNode(node)) {
- node = node.lastChild;
- }
- return node;
- },
- /**
- * Returns a Point object from a node and an offset.
- *
- * @param {Node} node
- * @param {Number} offset
- */
- makePoint: function (node, offset) {
- return {
- node: node,
- offset: offset,
- };
- },
- /**
- * Merges mergeFromBlock into mergeIntoBlock, respecting the rules of unbreakable.
- *
- * @param {Node} mergeFromBlock block to merge from
- * @param {Node} mergeIntoBlock block to merge into
- * @returns {Object} {node, offset}
- */
- mergeNonSimilarBlocks: function (mergeFromBlock, mergeIntoBlock) {
- var point;
- var mergeableTags = this.options.styleTags.join(', ') + ', li';
- var $contents = $(mergeFromBlock).find('*').addBack()
- .filter(mergeableTags)
- .filter(function (i, n) {
- if (!(n.tagName === 'LI' && $(n).find(mergeableTags).length)) {
- return n;
- }
- }).contents();
- var containsUnbreakables = !!$contents.filter(this.options.isUnbreakable).length;
-
- if ($contents.length && !containsUnbreakables) {
- if (dom.isText($contents[0])) {
- this.removeExtremeBreakableSpace($contents[0]);
- }
- var $lastContents = $(mergeIntoBlock).contents().last();
- if (!($contents.length === 1 && $contents[0].tagName === 'BR')) {
- if (mergeIntoBlock.innerHTML.trim() === '<br>') {
- $(mergeIntoBlock).contents().remove();
- $(mergeIntoBlock).append($contents);
- $lastContents = false;
- } else {
- $lastContents.after($contents);
- }
- }
- while (mergeFromBlock.parentNode && this.isBlankNode(mergeFromBlock.parentNode, true)) {
- mergeFromBlock = mergeFromBlock.parentNode;
- }
- $(mergeFromBlock).remove();
-
- point = {};
- if ($lastContents && $lastContents.length) {
- point = this.makePoint($lastContents[0], dom.nodeLength($lastContents[0]));
- } else {
- point = this.makePoint($contents[0], 0);
- }
-
- point = this.deleteEdge(point.node, 'next', true) || point;
- }
- return point;
- },
- /**
- * Normalize the DOM and range.
- */
- normalize: function () {
- this.editable.normalize();
- var range = this.context.invoke('editor.createRange');
- var rangeN = range.normalize();
-
- // summernote's normalize function fails when br in text,
- // and targets the br instead of the point just after br.
- var point = rangeN.getStartPoint();
- if (point.node.tagName === "BR") {
- point = dom.nextPoint(point);
- }
- if (point.node.tagName && point.node.childNodes[point.offset]) {
- point = dom.nextPoint(point);
- }
- if (point.node.tagName === "BR") {
- point = dom.nextPoint(point);
- }
- if (point.node !== range.sc || point.offset !== range.so) {
- range = this.context.invoke('editor.setRange', point.node, point.offset);
- range.select();
- }
- },
- /**
- * Simulate a do_notify by notifying the user through a dialog.
- *
- * @param {String} title
- * @param {String} content
- */
- notify: function (title, content) {
- var $notif = $('<p>' + content + '</p>');
- new Dialog(this, {
- title: title,
- size: 'medium',
- $content: $notif,
- }).open();
- },
- /**
- * Return true if the `container` contains the `contained` and only
- * the `contained` (blank text nodes are ignored).
- *
- * @param {Node} container
- * @param {Node} contained
- * @returns {Boolean}
- */
- onlyContains: function (container, contained) {
- var self = this;
- if (!$.contains(container, contained)) {
- return false;
- }
- var $contents = $(container).contents();
- var otherContents = $contents.filter(function (index, node) {
- if (node === contained || dom.isText(node) && !self.isVisibleText(node)) {
- return false;
- }
- return true;
- });
- return !otherContents.length;
- },
- /**
- * Reorders the classes in the node's class attribute and returns it.
- *
- * @param {Node} node
- * @returns {String}
- */
- orderClass: function (node) {
- var className = node.getAttribute && node.getAttribute('class');
- if (!className) {
- return null;
- }
- className = className.replace(this.getRegex('spaceOrNewline', 'g'), ' ')
- .replace(this.getRegex('startAndEndSpace', 'g'), '')
- .replace(this.getRegex('space', 'g'), ' ');
- className = className.replace('o_default_snippet_text', '')
- .replace('o_checked', '');
- if (!className.length) {
- node.removeAttribute("class");
- return null;
- }
- className = className.split(" ");
- className.sort();
- className = className.join(" ");
- node.setAttribute('class', className);
- return className;
- },
- /**
- * Reorders the styles in the node's style attributes and returns it.
- *
- * @param {Node} node
- * @returns {String}
- */
- orderStyle: function (node) {
- var style = node.getAttribute('style');
- if (!style) {
- return null;
- }
- style = style.replace(this.getRegex('spaceOrNewline'), ' ')
- .replace(this.getRegex('startAndEndSemicolon', 'g'), '')
- .replace(this.getRegex('semicolon', 'g'), ';');
- if (!style.length) {
- node.removeAttribute("style");
- return null;
- }
- style = style.split(";");
- style.sort();
- style = style.join("; ") + ";";
- node.setAttribute('style', style);
- return style;
- },
- /**
- * Returns the path from the editable node to the given node.
- *
- * @param {Node} node
- * @returns {Object[]} list of objects (tagName, offset)
- */
- path: function (node) {
- var path = [];
- while (node && node !== this.editable) {
- var tagName = node.tagName;
- path.unshift({
- tagName: tagName,
- offset: _.filter(node.parentNode.childNodes, function (node) {
- return node.tagName === tagName;
- }).indexOf(node),
- });
- node = node.parentNode;
- }
- return path;
- },
- /**
- * Removes all attributes without a value from the given node.
- *
- * @param {Node} node
- * @returns {Node}
- */
- removeBlankAttrs: function (node) {
- _.each([].slice.call(node.attributes), function (attr) {
- if (!attr.value) {
- node.removeAttribute(attr.name);
- }
- });
- return node;
- },
- /**
- * Remove a node's direct blank siblings, if any.
- * Eg: Text<i></i>Node<b></b>Text => TextNodeText
- *
- * @param {Node} node
- */
- removeBlankSiblings: function (node) {
- var isAfterBlank = node.previousSibling && this.isBlankNode(node.previousSibling);
- if (isAfterBlank) {
- $(node.previousSibling).remove();
- }
- var isBeforeBlank = node.nextSibling && this.isBlankNode(node.nextSibling);
- if (isBeforeBlank) {
- $(node.nextSibling).remove();
- }
- },
- /**
- * Removes the block target and joins its siblings.
- *
- * @param {Node} target
- * @param {Boolean} doNotInsertP true to NOT fill an empty unbreakable with a p element.
- * @returns {Object} {node, offset}
- */
- removeBlockNode: function (target, doNotInsertP) {
- var self = this;
- var check = function (point) {
- if (point.node === target) {
- return false;
- }
- return !point.node || self.options.isEditableNode(point.node) &&
- (point.node.tagName === "BR" || self.isVisibleText(point.node));
- };
- var parent = target.parentNode;
- var offset = [].indexOf.call(parent.childNodes, target);
- var deleteEdge = 'next';
- var point = dom.prevPointUntil(this.makePoint(target, 0), check);
- if (!point || !point.node) {
- deleteEdge = 'prev';
- point = dom.nextPointUntil(this.makePoint(target, 0), check);
- }
-
- $(target).remove();
-
- if (
- point && (deleteEdge === 'prev' && point.offset) ||
- deleteEdge === 'next' && point.offset === dom.nodeLength(point.node)
- ) {
- point = this.deleteEdge(point.node, deleteEdge) || point;
- }
-
- $(parent).contents().filter(function () {
- return dom.isText(this) && self.getRegexBlank({
- atLeastOne: true,
- invisible: true,
- }).test(this.textContent);
- }).remove();
-
- var br;
- if (parent.innerHTML === '') {
- br = this.document.createElement('br');
- $(parent).append(br);
- point = this.makePoint(parent, 0);
- }
-
- if (!doNotInsertP && this.getRegexBlank({
- space: true,
- invisible: true,
- }).test(parent.innerHTML)) {
- br = this.document.createElement('br');
- if (this.options.isUnbreakableNode(parent) && parent.tagName !== "TD") {
- var p = this.document.createElement('p');
- $(p).append(br);
- $(parent).append(p);
- } else {
- $(parent).append(br);
- }
- point = this.makePoint(br.parentNode, 0);
- }
-
- if (point && point.node.tagName === "BR" && point.node.parentNode) {
- point = {
- node: point.node.parentNode,
- offset: [].indexOf.call(point.node.parentNode.childNodes, point.node),
- };
- }
-
- return point || this.makePoint(parent, offset);
- },
- /**
- * Removes the empty inline nodes around the point, and joins its siblings.
- *
- * @param {Object} point {node, offset}
- * @returns {Object} {node, offset}
- */
- removeEmptyInlineNodes: function (point) {
- var node = point.node;
- if (!point.node.tagName && !point.node.textContent.length) {
- node = node.parentNode;
- if ($(node).hasClass('o_default_snippet_text')) {
- // for default snippet value
- return point;
- }
- }
- var prev;
- var next;
- while (
- node.tagName !== 'BR' &&
- (node.tagName ? node.innerHTML : node.textContent) === '' &&
- !this.isNodeBlockType(node) &&
- this.options.isEditableNode(node.parentNode) &&
- (!node.attributes || !node.attributes.contenteditable) &&
- !dom.isMedia(node)
- ) {
- prev = node.previousSibling;
- next = node.nextSibling;
- point = {
- node: node.parentNode,
- offset: [].indexOf.call(node.parentNode.childNodes, node)
- };
- $(node).remove();
- node = point.node;
- }
- if (next && !next.tagName) {
- if (/^\s+[^\s<]/.test(next.textContent)) {
- next.textContent = next.textContent.replace(this.getRegex('startSpace'), '\u00A0');
- }
- }
- if (prev) {
- if (!prev.tagName) {
- if (/[^\s>]\s+$/.test(prev.textContent)) {
- prev.textContent = prev.textContent.replace(this.getRegex('endSpace'), ' ');
- }
- }
- point = this.makePoint(prev, dom.nodeLength(prev));
- }
- return point;
- },
- /**
- * Removes any amount of leading/trailing breakable space from a text node.
- * Returns how many characters were removed at the start
- * and at the end of the text node.
- *
- * @param {Node} textNode
- * @param {Boolean} secureExtremeties (defaults to true)
- * @returns {Object} removed {start, end}
- */
- removeExtremeBreakableSpace: function (textNode, secureExtremeties) {
- if (arguments.length === 1) {
- secureExtremeties = true;
- }
- if (secureExtremeties) {
- this.secureExtremeSingleSpace(textNode);
- }
- var removed = {
- start: 0,
- end: 0,
- };
- textNode.textContent = textNode.textContent.replace(this.getRegex('startNotChar'), function (toRemove) {
- removed.start = toRemove.length;
- return '';
- });
- textNode.textContent = textNode.textContent.replace(this.getRegex('endNotChar'), function (toRemove) {
- removed.end = toRemove.length;
- return '';
- });
- return removed;
- },
- /**
- * Makes the leading/trailing single space of a node non breakable (nbsp).
- *
- * @param {Node} node
- */
- secureExtremeSingleSpace: function (node) {
- if (this.getRegex('endSingleSpace').test(node.textContent)) {
- // if the text ends with a single space, make it insecable
- node.textContent = node.textContent.substr(0, node.textContent.length - 1) + '\u00A0';
- }
- if (this.getRegex('startSingleSpace').test(node.textContent)) {
- // if the text starts with a single space, make it insecable
- node.textContent = '\u00A0' + node.textContent.substr(1, node.textContent.length);
- }
- },
- /**
- * Skips points to ignore (generally for trying to figure out if edge point).
- * Returns the resulting point.
- *
- * @param {Object} point
- * @param {String} direction ('prev' or 'next')
- * @param {function} pred (extra condition to stop at)
- * @param {Object} options
- * @param {Boolean} options.noSkipBlankText true to not skip blank text
- * @param {Boolean} options.noSkipSingleBRs true to not skip single BRs
- * @param {Boolean} options.noSkipExtremeBreakableSpace true to not skip leading/trailing breakable space
- * @param {Boolean} options.noSkipParent true to not skip to leaf nodes or offset 0
- * @param {Boolean} options.noSkipSibling true to not skip if on edge and sibling is skippable
- * @returns {Object} {node, offset}
- */
- skipNodes: function (point, direction, pred, options) {
- var self = this;
- if (arguments.length === 3 && !_.isFunction(arguments[2])) {
- // allow for passing options and no pred function
- options = _.clone(pred);
- pred = null;
- }
- options = options || {};
- return dom[direction + 'PointUntil'](point, function (pt) {
- return !self.isSkippable(pt, direction, options) || pred && pred(pt);
- });
- },
- /**
- * Split the DOM tree at the node's start and end points.
- *
- * @param {Node} node
- */
- splitAtNodeEnds: function (node) {
- if (!node.parentNode) {
- return;
- }
- var startPoint = this.makePoint(node, 0);
- var endPoint = this.makePoint(node, dom.nodeLength(node));
- var splitOptions = {
- isSkipPaddingBlankHTML: true,
- };
- this.splitTree(node.parentNode, startPoint, splitOptions);
- this.splitTree(node.parentNode, endPoint, splitOptions);
- // Splitting at ends may create blank nodes (because of dom.splitTree) so let's clean it up:
- this.removeBlankSiblings(node.parentNode);
- },
- /**
- * Split the DOM tree at the point
- *
- * @param {Node} root - split root
- * @param {BoundaryPoint} point {node, offset}
- * @param {Object} [options]
- * @param {Boolean} [options.nextText] - default: false
- * @param {Boolean} [options.isSkipPaddingBlankHTML] - default: false
- * @param {Boolean} [options.isNotSplitEdgePoint] - default: false
- * @returns {Node} right node of boundary point
- */
- splitTree: function (root, point, options) {
- var nextText;
- if (options && options.nextText && !point.node.tagName) {
- nextText = point.node.splitText(point.offset);
- }
- var emptyText = false;
- if (!point.node.tagName && point.node.textContent === "") {
- emptyText = true;
- point.node.textContent = '\u200B';
- point.offset = 1;
- }
- var next = dom.splitTree(root, point, options);
- if (emptyText) {
- point.node.textContent = '';
- }
- var result = nextText || next || point.node;
- var att = nextText ? 'textContent' : 'innerHTML';
- if (/^\s+([^\s<])/.test(result[att])) {
- result[att] = result[att].replace(this.getRegex('startSpace'), '\u00A0');
- }
- return result;
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- _getAttrsNoID: function (node) {
- var nodeAttributes = [];
- for (var i = 0; i < node.attributes.length; i++) {
- if (node.attributes[i].name !== 'id') {
- nodeAttributes.push(node.attributes[i]);
- }
- }
- return nodeAttributes;
- },
- _insertTextNodeInEditableArea: function (range, text) {
- // try to insert the text node in editable area
- var textNode = this.document.createTextNode(text);
- if (this.options.isEditableNode(range.sc) && $(range.sc).closest('[contenteditable]').attr('contenteditable') === 'true') {
- if (dom.isText(range.sc) && this.isVisibleText(range.sc)) {
- // If range is on visible text: split the text at offset and insert the text node
- range.sc.splitText(range.so);
- range.so = range.eo = dom.nodeLength(range.sc);
- $(range.sc).after(textNode);
- } else if (dom.isBR(range.sc)) {
- if (range.sc.nextSibling && this.isVisibleText(range.sc.nextSibling)) {
- $(range.sc).before(textNode);
- } else {
- $(range.sc).replaceWith(textNode);
- }
- } else if (dom.isVoid(range.sc)) {
- $(range.sc).before(textNode);
- } else if (range.sc.childNodes[range.so]) {
- var node = range.sc.childNodes[range.so];
- if (dom.isBR(node)) {
- $(node).replaceWith(textNode);
- } else {
- $(node).before(textNode);
- }
- } else if (dom.isText(range.sc)) {
- $(range.sc).after(textNode);
- } else if (this.options.isUnbreakableNode(range.sc)) {
- $(range.sc).append(textNode);
- }
- }
- if (!textNode.parentNode && this.options.isEditableNode(range.sc.parentNode)) {
- $(range.sc).before(textNode);
- }
-
- return textNode.parentNode && textNode;
- },
- /**
- * Perform operations that are necessary after the insertion of a visible character:
- * adapt range for the presence of zero-width characters, move out of media, rerange.
- *
- * @private
- */
- _removeInvisibleChar: function (range) {
- if (range.sc.tagName || dom.ancestor(range.sc, dom.isAnchor)) {
- return true;
- }
- var needReselect = false;
- var fake = range.sc.parentNode;
- if ((fake.className || '').indexOf('o_fake_editable') !== -1 && dom.isMedia(fake)) {
- var $media = $(fake.parentNode);
- $media[fake.previousElementSibling ? 'after' : 'before'](fake.firstChild);
- needReselect = true;
- }
- if (range.sc.textContent.slice(range.so - 2, range.so - 1) === '\u200B') {
- range.sc.textContent = range.sc.textContent.slice(0, range.so - 2) + range.sc.textContent.slice(range.so - 1);
- range.so = range.eo = range.so - 1;
- needReselect = true;
- }
- if (range.sc.textContent.slice(range.so, range.so + 1) === '\u200B') {
- range.sc.textContent = range.sc.textContent.slice(0, range.so) + range.sc.textContent.slice(range.so + 1);
- needReselect = true;
- }
- if (needReselect) {
- range = range.normalize();
- }
- return range;
- },
- _standardizeRangeOnEdge: function (range) {
- var self = this;
-
- var invisible = this.document.createTextNode('\u200B');
-
- if (dom.isText(range.sc) && !this.isVisibleText(range.sc) && range.sc.nextSibling) {
- var firstLeafOfNext = this.firstLeaf(range.sc.nextSibling);
- range = this.context.invoke('editor.setRange', firstLeafOfNext, 0);
- }
-
- // Create empty text node to have a range into the node
- if (range.sc.tagName && !dom.isVoid(range.sc) && !range.sc.childNodes[range.so]) {
- $(range.sc).append(invisible);
- if (invisible.previousSibling && invisible.previousSibling.tagName === "BR") {
- invisible.previousSibling.remove();
- }
- range = this.context.invoke('editor.setRange', invisible, 0);
- }
-
- // On left edge of non-empty element: move before
- var siblings = range.sc.parentNode && range.sc.parentNode.childNodes;
- var isInEmptyElem = !siblings || !siblings.length || _.all(siblings, this.isBlankNode.bind(this));
- if (
- !range.so && !isInEmptyElem &&
- !(range.sc.previousSibling && range.sc.previousSibling.tagName === "BR") &&
- !this.options.isEditableNode(range.sc)
- ) {
- var point = range.getStartPoint();
- var newPoint = dom.prevPointUntil(point, function (pt) {
- return pt.node !== range.sc && !pt.node.tagName && !self.isBlankText(pt.node);
- });
- if (!newPoint || this.firstBlockAncestor(newPoint.node) !== this.firstBlockAncestor(point.node)) {
- range = this.context.invoke('editor.setRange', point.node, point.offset);
- } else {
- range = this.context.invoke('editor.setRange', newPoint.node, newPoint.offset);
- }
- }
-
- return range;
- },
- _wrapTextWithP: function (textNode) {
- if (dom.ancestor(textNode, dom.isAnchor)) {
- return;
- }
- var self = this;
- var isFormatNode = dom.ancestor(textNode, this.isFormatNode.bind(this));
- if (!isFormatNode) {
- var hasInlineParent = dom.ancestor(textNode.parentNode, function (node) {
- return !self.isNodeBlockType(node);
- });
- if (
- !hasInlineParent &&
- (textNode.tagName ||
- !(textNode.previousSibling && this.isVisibleText(textNode.previousSibling) ||
- textNode.nextSibling && this.isVisibleText(textNode.nextSibling))
- )
- ) {
- var blankP = this.document.createElement('p');
- $(textNode).after(blankP);
- $(blankP).prepend(textNode);
- }
- }
- },
-});
-
-registry.add('HelperPlugin', HelperPlugin);
-
-return HelperPlugin;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/hint.js b/addons/web_editor/static/src/js/wysiwyg/plugin/hint.js
deleted file mode 100644
index 708ac89333f..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/hint.js
+++ /dev/null
@@ -1,139 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.hint', function (require) {
-'use strict';
-
-var Plugins = require('web_editor.wysiwyg.plugins');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-
-var dom = $.summernote.dom;
-
-
-var HintPlugin = Plugins.hintPopover.extend({
- init: function (context) {
- context.options.hint = (context.options.hint || []).concat(this._hints());
- this._super.apply(this, arguments);
- },
-
- //--------------------------------------------------------------------------
- // Public summernote module API
- //--------------------------------------------------------------------------
-
- /**
- * Replace the current hint.
- */
- replace: function () {
- var self = this;
- var $item = this.$content.find('.note-hint-item.active');
- if ($item.length) {
- this.lastWordRange.select();
- this.context.invoke('HelperPlugin.deleteSelection');
- var range = this.context.invoke('editor.createRange');
-
- this.nodeFromItem($item).each(function () {
- $(range.sc).after(this);
- range = self.context.invoke('editor.setRange', this, dom.nodeLength(this));
- });
- range.select();
- this.context.invoke('editor.saveRange');
- this.lastWordRange = null;
- this.hide();
- this.context.triggerEvent('change', this.$editable.html(), this.$editable[0]);
- this.context.invoke('editor.focus');
- }
- },
- /**
- * @param {JQueryEvent} e
- */
- handleKeyup: function (e) {
- var self = this;
- if ([13, 38, 40].indexOf(e.keyCode) === -1) { // enter, up, down
- var wordRange = this.context.invoke('editor.createRange');
- var keyword_1 = wordRange.sc.textContent.slice(0, wordRange.so);
- if (this.hints.length && keyword_1) {
- this.$content.empty();
- this.$popover.hide();
- this.lastWordRange = wordRange;
- var hasMatch = false;
-
- // test all hints
- this.hints.forEach(function (hint, idx) {
- var match = keyword_1.match(hint.match);
- if (match) {
- hasMatch = true;
- wordRange.so = wordRange.eo - match[0].length;
- self.createGroup(idx, match[0]).appendTo(self.$content);
- }
- });
- if (!hasMatch) {
- return;
- }
-
- // select first .note-hint-item
- this.$content.find('.note-hint-item:first').addClass('active');
-
- // set position for popover after group is created
- var rect = wordRange.getClientRects()[0];
- var bnd = {
- top: rect.top + $(this.document).scrollTop(),
- left: rect.left + $(this.document).scrollLeft(),
- };
- this.$popover.css({
- left: bnd.left,
- top: bnd.top + (this.direction === 'top' ? -this.$popover.outerHeight() : (rect.bottom - rect.top)) - 5,
- });
- } else {
- this.hide();
- }
- }
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Get hint objects.
- *
- * @private
- * @returns {Object[]} hints
- */
- _hints: function () {
- var self = this;
- return [{
- className: 'o_hint_partner',
- match: /\B@(\w+(\s\w*)?)$/,
- search: function (keyword, callback) {
- self._rpc({
- model: 'res.partner',
- method: "search_read",
- fields: ['id', 'name', 'email'],
- domain: [
- ['name', 'ilike', keyword],
- ],
- limit: 10,
- }).then(callback);
- },
- template: function (partner) {
- return partner.name + (partner.email ? ' <i style="color: #999;">(' + partner.email + ')</i>' : '');
- },
- content: function (item) {
- return $(self.document.createTextNode('@' + item.name + '\u00A0'));
- },
- },
- {
- className: 'fa',
- match: /:([\-+\w]+)$/,
- search: function () {},
- template: function () {
- return '<span class="fa fa-star">\u200B</span>';
- },
- content: function () {}
- },
- ];
- },
-});
-
-registry.add('hintPopover', null);
-
-return HintPlugin;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/history.js b/addons/web_editor/static/src/js/wysiwyg/plugin/history.js
deleted file mode 100644
index addf6383783..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/history.js
+++ /dev/null
@@ -1,87 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.history', function (require) {
-'use strict';
-
-var Plugins = require('web_editor.wysiwyg.plugins');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-
-
-var HistoryPlugin = Plugins.history.extend({
- /**
- * Apply a snapshot.
- *
- * @override
- */
- applySnapshot: function () {
- this.context.invoke('MediaPlugin.hidePopovers');
- try {
- this._super.apply(this, arguments);
- } catch (e) {
- console.error(e);
- }
- this.context.invoke('editor.focus');
- var newRange = this.context.invoke('editor.createRange');
- if (!this.options.isEditableNode(newRange.sc)) {
- var firstEditableSel = '[contenteditable="true"]:not(":hidden")';
- var sc = this.$editable.find(firstEditableSel)[0];
- this.context.invoke('editor.setRange', sc, 0, sc, 0).select();
- }
- },
- /**
- * Clear the history.
- *
- * @override
- */
- clear: function () {
- this.stack = [];
- this.stackOffset = -1;
- this.recordUndo();
- },
- /**
- * Get the current history stack and the current offset.
- *
- * @returns {Object} {stack: Object, stackOffset: Integer}
- */
- getHistoryStep: function () {
- return {
- stack: this.stack,
- stackOffset: this.stackOffset,
- };
- },
- /**
- * Prevent errors with first snapshot.
- *
- * @override
- */
- makeSnapshot: function () {
- var rng = $.summernote.range.create(this.editable);
- var snapshot = this._super();
- if (rng.sc === this.editable || $(rng.sc).has(this.editable).length) {
- snapshot.bookmark.s.path = snapshot.bookmark.e.path = [0];
- snapshot.bookmark.s.offset = snapshot.bookmark.e.offset = 0;
- }
- return snapshot;
- },
- /**
- * @override
- */
- recordUndo: function () {
- if (!this.stack[this.stackOffset] || this.$editable.html() !== this.stack[this.stackOffset].contents) {
- this._super();
- }
- },
- /**
- * @override
- */
- undo: function () {
- if (this.stackOffset > 0) {
- this.stackOffset--;
- }
- this.applySnapshot(this.stack[this.stackOffset]);
- },
-});
-
-registry.add('HistoryPlugin', HistoryPlugin);
-
-return HistoryPlugin;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/keyboard.js b/addons/web_editor/static/src/js/wysiwyg/plugin/keyboard.js
deleted file mode 100644
index 9ac05220e45..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/keyboard.js
+++ /dev/null
@@ -1,1237 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.keyboard', function (require) {
-'use strict';
-
-var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-
-var dom = $.summernote.dom;
-dom.isAnchor = function (node) {
- return (node.tagName === 'A' || node.tagName === 'BUTTON' || $(node).hasClass('btn')) &&
- !$(node).hasClass('fa') && !$(node).hasClass('o_image');
-};
-var keycode = ({
- '8': 'BACKSPACE',
- '9': 'TAB',
- '13': 'ENTER',
- '46': 'DELETE',
-});
-
-var isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
-var iPhone = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
-
-var KeyboardPlugin = AbstractPlugin.extend({
- events: iPhone ?
- {
- 'DOMNodeInserted .note-editable': '_removeGarbageSpans',
- } :
- {
- 'summernote.keydown': '_onKeydown',
- 'DOMNodeInserted .note-editable': '_removeGarbageSpans',
- 'keypress .note-editable': '_onKeyPress',
- 'textInput .note-editable': '_onTextInput',
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Perform various DOM and range manipulations after a deletion:
- * - Rerange out of BR elements
- * - Clean the DOM at current range position
- *
- * @see _handleDeletion
- *
- * @private
- * @param {Object} range
- * @param {String('prev'|'next')} direction 'prev' to delete BEFORE the carret
- * @returns {Object} range
- */
- _afterDeletion: function (range, direction) {
- range = direction === 'prev' ? this._insertInvisibleCharAfterSingleBR(range) : range;
- range = this._rerangeOutOfBR(range, direction);
- range = this._cleanRangeAfterDeletion(range);
- return range;
- },
- /**
- * Perform operations that are necessary after the insertion of a visible character:
- * - Adapt range for the presence of zero-width characters
- * - Move out of media
- * - Rerange
- *
- * @private
- */
- _afterVisibleChar: function () {
- var range = this.context.invoke('editor.createRange');
- if (range.sc.tagName || dom.ancestor(range.sc, dom.isAnchor)) {
- return true;
- }
- var needReselect = false;
- var fake = range.sc.parentNode;
- if ((fake.className || '').indexOf('o_fake_editable') !== -1 && dom.isMedia(fake)) {
- var $media = $(fake.parentNode);
- $media[fake.previousElementSibling ? 'after' : 'before'](fake.firstChild);
- needReselect = true;
- }
- if (range.sc.textContent.slice(range.so - 2, range.so - 1) === '\u200B') {
- range.sc.textContent = range.sc.textContent.slice(0, range.so - 2) + range.sc.textContent.slice(range.so - 1);
- range.so = range.eo = range.so - 1;
- needReselect = true;
- }
- if (range.sc.textContent.slice(range.so, range.so + 1) === '\u200B') {
- range.sc.textContent = range.sc.textContent.slice(0, range.so) + range.sc.textContent.slice(range.so + 1);
- needReselect = true;
- }
- if (needReselect) {
- range.normalize().select();
- }
- },
- /**
- * Perform various DOM and range manipulations to prepare a deletion:
- * - Rerange within the element targeted by the range
- * - Slice the text content if necessary
- * - Move before an invisible BR if necessary
- * - Replace a media with an empty SPAN if necessary
- * - Change the direction of deletion if necessary
- * - Clean the DOM at range position if necessary
- *
- * @see _handleDeletion
- *
- * @private
- * @param {Object} range
- * @param {String('prev'|'next')} direction
- * @param {Boolean} didDeleteNodes true if nodes were already deleted prior to this call
- * @returns {Object} {didDeleteNodes: Boolean, range: Object, direction: String('prev'|next')}
- */
- _beforeDeletion: function (range, direction, didDeleteNodes) {
- var res = {
- range: range,
- direction: direction,
- didDeleteNodes: didDeleteNodes,
- };
-
- res.range = this._rerangeToOffsetChild(res.range, direction);
- res.range = this._sliceAndRerangeBeforeDeletion(res.range);
- res.range = direction === 'prev' ? this._moveBeforeInvisibleBR(res.range) : res.range;
-
- if (dom.isMedia(res.range.sc)) {
- var span = this._replaceMediaWithEmptySpan(res.range.sc);
- res.range = this.context.invoke('editor.setRange', span, 0);
- res.didDeleteNodes = true;
- return res;
- }
-
- if (res.didDeleteNodes) {
- res.direction = 'next';
- return res;
- }
-
- res.range = this._cleanRangeBeforeDeletion(res.range, direction);
-
- return res;
- },
- /**
- * Clean the DOM at range position after a deletion:
- * - Remove empty inline nodes
- * - Fill the current node if it's empty
- *
- * @private
- * @param {Object} range
- * @returns {Object} range
- */
- _cleanRangeAfterDeletion: function (range) {
- var point = range.getStartPoint();
-
- point = this.context.invoke('HelperPlugin.removeEmptyInlineNodes', point);
- point = this.context.invoke('HelperPlugin.fillEmptyNode', point);
- range = this.context.invoke('editor.setRange', point.node, point.offset);
- return range;
- },
- /**
- * Clean the DOM at range position:
- * - Remove all previous zero-width characters
- * - Remove leading/trailing breakable space
- *
- * @private
- * @param {Object} range
- * @param {String('prev'|'next')} direction
- * @returns {Object} range
- */
- _cleanRangeBeforeDeletion: function (range, direction) {
- if (direction === 'prev') {
- this._removeAllPreviousInvisibleChars(range);
- }
- range = this._removeExtremeBreakableSpaceAndRerange(range);
- return range;
- },
- /**
- * Get information on the range in order to perform a deletion:
- * - The point at which to delete, if any
- * - Whether the node contains a block
- * - The block to remove, if any
- *
- * @private
- * @param {Object} range
- * @param {String('prev'|'next')} direction
- * @param {Boolean} wasOnStartOfBR true if the requested deletion started at
- * the beginning of a BR element
- * @returns {Object} {
- * point: {false|Object},
- * hasBlock: {Boolean},
- * blockToRemove: {false|Node},
- * }
- */
- _getDeleteInfo: function (range, direction, wasOnStartOfBR) {
- var self = this;
- var hasBlock = false;
- var blockToRemove = false;
- var method = direction === 'prev' ? 'prevPointUntil' : 'nextPointUntil';
-
- var pt = range.getStartPoint();
- pt = dom[method](pt, function (point) {
- var isAtStartOfMedia = !point.offset && dom.isMedia(point.node);
- var isBRorHR = point.node.tagName === 'BR' || point.node.tagName === 'HR';
- var isRootBR = wasOnStartOfBR && point.node === range.sc;
- var isOnRange = range.ec === point.node && range.eo === point.offset;
-
- if (!point.offset && self.context.invoke('HelperPlugin.isNodeBlockType', point.node)) {
- hasBlock = true;
- if (blockToRemove) {
- return true;
- }
- }
-
- if (!blockToRemove && (isAtStartOfMedia || isBRorHR && !isRootBR)) {
- blockToRemove = point.node;
- return false;
- }
-
- if (isOnRange) {
- return false;
- }
-
- return self._isDeletableNode(point.node);
- });
-
- return {
- point: pt || false,
- hasBlock: hasBlock,
- blockToRemove: blockToRemove,
- };
- },
- /**
- * Handle deletion (BACKSPACE / DELETE).
- *
- * @private
- * @param {String('prev'|'next')} direction 'prev' to delete BEFORE the carret
- * @returns {Boolean} true if case handled
- */
- _handleDeletion: function (direction) {
- var range = this.context.invoke('editor.createRange');
- range = direction === 'prev' && this._replaceEmptyParentWithEmptyP(range);
- if (range) {
- range = this._afterDeletion(range, direction);
- range.select();
- return true;
- }
- var didDeleteNodes = this.context.invoke('HelperPlugin.deleteSelection');
- range = this.context.invoke('editor.createRange');
- var wasOnStartOfBR = direction === 'prev' && !range.so && range.sc.tagName === 'BR';
-
- this._removeNextEmptyUnbreakable(range.sc);
- var temp = this._beforeDeletion(range, direction, didDeleteNodes);
- didDeleteNodes = temp.didDeleteNodes;
- range = temp.range;
- direction = temp.direction;
-
- if (!didDeleteNodes) {
- var newRange = this._performDeletion(range, direction, wasOnStartOfBR);
- didDeleteNodes = newRange.so !== range.so || newRange.sc !== range.sc;
- range = newRange;
- }
-
- range = this._afterDeletion(range, direction);
-
- range = range.collapse(direction === 'prev').select();
- this.editable.normalize();
- return didDeleteNodes;
- },
- /**
- * Handle ENTER.
- *
- * @private
- * @returns {Boolean} true if case handled
- */
- _handleEnter: function () {
- var self = this;
- var range = this.context.invoke('editor.createRange');
-
- var ancestor = dom.ancestor(range.sc, function (node) {
- return dom.isLi(node) || self.options.isUnbreakableNode(node.parentNode) && node.parentNode !== self.editable ||
- self.context.invoke('HelperPlugin.isNodeBlockType', node) && !dom.ancestor(node, dom.isLi);
- });
-
- if (
- dom.isLi(ancestor) && !$(ancestor.parentNode).hasClass('list-group') &&
- this.context.invoke('HelperPlugin.getRegexBlank', {
- space: true,
- newline: true,
- }).test(ancestor.textContent) &&
- $(ancestor).find('br').length <= 1 &&
- !$(ancestor).find('.fa, img').length
- ) {
- // double enter in a list make oudent
- this.context.invoke('BulletPlugin.outdent');
- return true;
- }
-
- var btn = dom.ancestor(range.sc, function (n) {
- return $(n).hasClass('btn');
- });
-
- var point = range.getStartPoint();
-
- if (!point.node.tagName && this.options.isUnbreakableNode(point.node.parentNode)) {
- return this._handleShiftEnter();
- }
-
- if (point.node.tagName && point.node.childNodes[point.offset] && point.node.childNodes[point.offset].tagName === "BR") {
- point = dom.nextPoint(point);
- }
- if (point.node.tagName === "BR") {
- point = dom.nextPoint(point);
- }
-
- var next = this.context.invoke('HelperPlugin.splitTree', ancestor, point, {
- isSkipPaddingBlankHTML: !this.context.invoke('HelperPlugin.isNodeBlockType', point.node.parentNode) && !!point.node.parentNode.nextSibling
- });
- $(next).removeClass('o_checked');
- while (next.firstChild) {
- next = next.firstChild;
- }
-
- // if there is no block in the split parents, then we add a br between the two node
- var hasSplitBlock = false;
- var node = next;
- var lastChecked = node;
- while (node && node !== ancestor && node !== this.editable) {
- if (this.context.invoke('HelperPlugin.isNodeBlockType', node)) {
- hasSplitBlock = true;
- break;
- }
- lastChecked = node;
- node = node.parentNode;
- }
- if (!hasSplitBlock && lastChecked.tagName) {
- $(lastChecked).before(this.document.createElement('br'));
- }
-
- if (!next.tagName) {
- this.context.invoke('HelperPlugin.secureExtremeSingleSpace', next);
- }
- if (next.tagName !== "BR" && next.innerHTML === "") {
- next.innerHTML = '\u200B';
- }
- if (ancestor) {
- var firstChild = this.context.invoke('HelperPlugin.firstLeaf', ancestor);
- var lastChild = this.context.invoke('HelperPlugin.lastLeaf', ancestor);
- if (this.context.invoke('HelperPlugin.isBlankNode', ancestor)) {
- firstChild = dom.isText(firstChild) ? firstChild.parentNode : firstChild;
- $(firstChild).contents().remove();
- $(firstChild).append(this.document.createElement('br'));
- }
- if (lastChild.tagName === 'BR' && lastChild.previousSibling) {
- $(lastChild).after(this.document.createTextNode('\u200B'));
- }
- }
-
- // move to next editable area
- point = this.context.invoke('HelperPlugin.makePoint', next, 0);
- if (
- (point.node.tagName && point.node.tagName !== 'BR') ||
- !this.context.invoke('HelperPlugin.isVisibleText', point.node.textContent)
- ) {
- point = dom.nextPointUntil(point, function (pt) {
- if (pt.node === point.node) {
- return;
- }
- return (
- pt.node.tagName === "BR" ||
- self.context.invoke('HelperPlugin.isVisibleText', pt.node)
- ) &&
- self.options.isEditableNode(pt.node);
- });
- point = point || this.context.invoke('HelperPlugin.makePoint', next, 0);
- if (point.node.tagName === "BR") {
- point = dom.nextPoint(point);
- }
- }
-
- if (!hasSplitBlock && !point.node.tagName) {
- point.node.textContent = '\u200B' + point.node.textContent;
- point.offset = 1;
- }
-
- // if the left part of the split node ends with a space, replace that space with nbsp
- if (range.sc.textContent) {
- var endSpace = this.context.invoke('HelperPlugin.getRegex', 'endSpace');
- range.sc.textContent = range.sc.textContent.replace(endSpace,
- function (trailingSpaces) {
- return Array(trailingSpaces.length + 1).join('\u00A0');
- }
- );
- }
-
- // On buttons, we want to split the button and move to the beginning of it
- if (btn) {
- next = dom.ancestor(point.node, function (n) {
- return $(n).hasClass('btn');
- });
-
- // Force content in empty buttons, the carret can be moved there
- this.context.invoke('LinkPopover.hide');
- this.context.invoke('LinkPopover.fillEmptyLink', next, true);
- this.context.invoke('LinkPopover.fillEmptyLink', btn, true);
-
- // Move carret to the new button
- range = this.context.invoke('editor.setRange', next.firstChild, 0);
- range.select();
- } else {
- range = this.context.invoke('editor.setRange', point.node, point.offset);
- range.normalize().select();
- }
-
- return true;
- },
- /**
- * Handle SHIFT+ENTER.
- *
- * @private
- * @returns {Boolean} true if case handled
- */
- _handleShiftEnter: function () {
- var range = this.context.invoke('editor.createRange');
- var target = range.sc.childNodes[range.so] || range.sc;
- var before;
- if (target.tagName) {
- if (target.tagName === "BR") {
- before = target;
- } else if (target === range.sc) {
- if (range.so) {
- before = range.sc.childNodes[range.so - 1];
- } else {
- before = this.document.createTextNode('');
- $(range.sc).append(before);
- }
- }
- } else {
- before = target;
- var after = target.splitText(target === range.sc ? range.so : 0);
- if (
- !after.nextSibling && after.textContent === '' &&
- this.context.invoke('HelperPlugin.isNodeBlockType', after.parentNode)
- ) {
- after.textContent = '\u200B';
- }
- if (!after.tagName && (!after.previousSibling || after.previousSibling.tagName === "BR")) {
- after.textContent = after.textContent.replace(startSpace, '\u00A0');
- }
- }
-
- if (!before) {
- return true;
- }
-
- var br = this.document.createElement('br');
- $(before).after(br);
- var next = this.context.invoke('HelperPlugin.makePoint', br, 0);
- var startSpace = this.context.invoke('HelperPlugin.getRegex', 'startSpace');
-
- if (!before.tagName) {
- next = dom.nextPoint(next);
- var nextNode = this.context.invoke('HelperPlugin.firstLeaf', next.node.childNodes[next.offset] || next.node);
- if (!nextNode.tagName) {
- next.node = nextNode;
- next.offset = 0;
- }
- }
-
- if (
- next.node.tagName === "BR" && next.node.nextSibling &&
- !next.node.nextSibling.tagName && !dom.ancestor(next.node, dom.isPre)
- ) {
- next.node.nextSibling.textContent = next.node.nextSibling.textContent.replace(startSpace, '\u00A0');
- }
- if (
- !next.node.tagName &&
- (!next.node.previousSibling || next.node.previousSibling.tagName === "BR") &&
- !dom.ancestor(next.node, dom.isPre)
- ) {
- next.node.textContent = next.node.textContent.replace(startSpace, '\u00A0');
- }
-
- range = this.context.invoke('editor.setRange', next.node, next.offset);
- range.select();
-
- return true;
- },
- /**
- * Insert a zero-width character after a BR if the range is
- * at the beginning of an invisible text node
- * and after said single BR element.
- *
- * @private
- * @param {Object} range
- * @returns {Object} range
- */
- _insertInvisibleCharAfterSingleBR: function (range) {
- if (this._isAtStartOfInvisibleText(range) && this._isAfterSingleBR(range.sc)) {
- var invisibleChar = this.document.createTextNode('\u200B');
- $(range.sc.previousSibling).after(invisibleChar);
- range = this.context.invoke('editor.setRange', invisibleChar, 1);
- }
- return range;
- },
- /**
- * Return true if the node comes after a BR element.
- *
- * @private
- * @param {Node} node
- * @returns {Boolean}
- */
- _isAfterBR: function (node) {
- return node.previousSibling && node.previousSibling.tagName === 'BR';
- },
- /**
- * Return true if the range if positioned after a BR element that doesn't visually
- * show a new line in the DOM: a BR in an element that has only a BR, or text then a BR.
- * eg: <p><br></p> or <p>text<br></p>
- *
- * @private
- * @param {Object} range
- * @returns {Boolean}
- */
- _isAfterInvisibleBR: function (range) {
- return this._isAfterOnlyBR(range) || this._isAfterOnlyTextThenBR(range);
- },
- /**
- * Return true if the range is positioned on a text node, after an zero-width character.
- *
- * @private
- * @param {Object} range
- * @returns {Boolean}
- */
- _isAfterInvisibleChar: function (range) {
- return !range.sc.tagName && range.so && range.sc.textContent[range.so - 1] === '\u200B';
- },
- /**
- * Return true if the range is positioned on a text node, after an leading zero-width character.
- *
- * @private
- * @param {Object} range
- * @returns {Boolean}
- */
- _isAfterLeadingInvisibleChar: function (range) {
- return !range.sc.tagName && range.so === 1 && range.sc.textContent[0] === '\u200B';
- },
- /**
- * Return true if the range if positioned after a BR element in a node that has only a BR.
- * eg: <p><br></p>
- *
- * @private
- * @param {Object} range
- * @returns {Boolean}
- */
- _isAfterOnlyBR: function (range) {
- return this._hasOnlyBR(range.sc) && range.so === 1;
- },
- /**
- * Return true if the node has for only element child a BR element.
- *
- * @private
- * @param {Node} node
- * @returns {Boolean}
- */
- _hasOnlyBR: function (node) {
- return node.childElementCount === 1 && node.firstChild.tagName === 'BR';
- },
- /**
- * Return true if the range if positioned after a BR element in a node that has only text
- * and ends with a BR.
- * eg: <p>text<br></p>
- *
- * @private
- * @param {Object} range
- * @returns {Boolean}
- */
- _isAfterOnlyTextThenBR: function (range) {
- var hasTrailingBR = range.sc.lastChild && range.sc.lastChild.tagName === 'BR';
- if (!hasTrailingBR) {
- return false;
- }
- var hasOnlyTextThenBR = _.all(range.sc.childNodes, function (n) {
- return dom.isText(n) || n === range.sc.lastChild;
- });
- var isAfterTrailingBR = range.so === dom.nodeLength(range.sc);
- return hasOnlyTextThenBR && isAfterTrailingBR;
- },
- /**
- * Return true if the node is after a single BR.
- *
- * @private
- * @param {Node} node
- * @returns {Boolean}
- */
- _isAfterSingleBR: function (node) {
- var isPreviousAfterBR = node.previousSibling && this._isAfterBR(node.previousSibling);
- return this._isAfterBR(node) && !isPreviousAfterBR;
- },
- /**
- * Return true if the node comes after two BR elements.
- *
- * @private
- * @param {Node} node
- * @returns {Boolean}
- */
- _isAfterTwoBRs: function (node) {
- var isAfterBR = this._isAfterBR(node);
- var isPreviousSiblingAfterBR = node.previousSibling && this._isAfterBR(node.previousSibling);
- return isAfterBR && isPreviousSiblingAfterBR;
- },
- /**
- * Return true if the range is positioned at the start of an invisible text node.
- *
- * @private
- * @param {Object} range
- * @returns {Boolean}
- */
- _isAtStartOfInvisibleText: function (range) {
- return !range.so && dom.isText(range.sc) && !this.context.invoke('HelperPlugin.isVisibleText', range.sc);
- },
- /**
- * Return true if the range is positioned on a text node, before a trailing zero-width character.
- *
- * @private
- * @param {Object} range
- * @returns {Boolean}
- */
- _isBeforeTrailingInvisibleChar: function (range) {
- var isBeforeLastCharOfText = !range.sc.tagName && range.so === dom.nodeLength(range.sc) - 1;
- var isLastCharInvisible = range.sc.textContent.slice(range.so) === '\u200B';
- return isBeforeLastCharOfText && isLastCharInvisible;
- },
- /**
- * Return true if the node is deletable.
- *
- * @private
- * @param {Node} node
- * @return {Boolean}
- */
- _isDeletableNode: function (node) {
- var isVisibleText = this.context.invoke('HelperPlugin.isVisibleText', node);
- var isMedia = dom.isMedia(node);
- var isBR = node.tagName === 'BR';
- var isEditable = this.options.isEditableNode(node);
- return isEditable && (isVisibleText || isMedia || isBR);
- },
- /**
- * Return true if the range is positioned on an edge to delete, depending on the given direction.
- *
- * @private
- * @param {Object} range
- * @param {String('prev'|'next')} direction
- */
- _isOnEdgeToDelete: function (range, direction) {
- var isOnBR = range.sc.tagName === 'BR';
- var parentHasOnlyBR = range.sc.parentNode && range.sc.parentNode.innerHTML.trim() === "<br>";
- var isOnDirEdge;
- if (direction === 'next') {
- isOnDirEdge = range.so === dom.nodeLength(range.sc);
- } else {
- isOnDirEdge = range.so === 0;
- }
- return (!isOnBR || parentHasOnlyBR) && isOnDirEdge;
- },
- /**
- * Move the range before a BR if that BR doesn't visually show a new line in the DOM.
- * Return the new range.
- *
- * @private
- * @param {Object} range
- * @returns {Object} range
- */
- _moveBeforeInvisibleBR: function (range) {
- if (this._isAfterInvisibleBR(range)) {
- range.so -= 1;
- }
- return range;
- },
- /**
- * Perform a deletion in the given direction.
- * Note: This is where the actual deletion takes place.
- * It should be preceded by _beforeDeletion and
- * followed by _afterDeletion.
- *
- * @see _handleDeletion
- *
- * @private
- * @param {Object} range
- * @param {String('prev'|'next')} direction 'prev' to delete BEFORE the carret
- * @param {Boolean} wasOnStartOfBR true if the requested deletion started at
- * the beginning of a BR element
- * @returns {Object} range
- */
- _performDeletion: function (range, direction, wasOnStartOfBR) {
- var didDeleteNodes = false;
- if (this._isOnEdgeToDelete(range, direction)) {
- var rest = this.context.invoke('HelperPlugin.deleteEdge', range.sc, direction);
- didDeleteNodes = !!rest;
- if (didDeleteNodes) {
- range = this.context.invoke('editor.setRange', rest.node, rest.offset);
- return range;
- }
- }
-
- var deleteInfo = this._getDeleteInfo(range, direction, wasOnStartOfBR);
-
- if (!deleteInfo.point) {
- return range;
- }
-
- var point = deleteInfo.point;
- var blockToRemove = deleteInfo.blockToRemove;
- var hasBlock = deleteInfo.hasBlock;
-
- var isLonelyBR = blockToRemove && blockToRemove.tagName === 'BR' && this._hasOnlyBR(blockToRemove.parentNode);
- var isHR = blockToRemove && blockToRemove.tagName === "HR";
-
- if (blockToRemove && !isLonelyBR) {
- $(blockToRemove).remove();
- point = isHR ? this.context.invoke('HelperPlugin.deleteEdge', range.sc, direction) : point;
- didDeleteNodes = true;
- } else if (!hasBlock) {
- var isAtEndOfNode = point.offset === dom.nodeLength(point.node);
- var shouldMove = isAtEndOfNode || direction === 'next' && point.offset;
-
- point.offset = shouldMove ? point.offset - 1 : point.offset;
- point.node = this._removeCharAtOffset(point);
- didDeleteNodes = true;
-
- var isInPre = !!dom.ancestor(range.sc, dom.isPre);
- if (!isInPre) {
- this.context.invoke('HelperPlugin.secureExtremeSingleSpace', point.node);
- }
-
- if (direction === 'prev' && !point.offset && !this._isAfterBR(point.node)) {
- point.node = this._replaceLeadingSpaceWithSingleNBSP(point.node);
- }
- }
-
- if (didDeleteNodes) {
- range = this.context.invoke('editor.setRange', point.node, point.offset);
- }
- return range;
- },
- /**
- * Prevent the appearance of a text node with the editable DIV as direct parent:
- * wrap it in a p element.
- *
- * @private
- */
- _preventTextInEditableDiv: function () {
- var range = this.context.invoke('editor.createRange');
- while (
- dom.isText(this.editable.firstChild) &&
- !this.context.invoke('HelperPlugin.isVisibleText', this.editable.firstChild)
- ) {
- var node = this.editable.firstChild;
- if (node && node.parentNode) {
- node.parentNode.removeChild(node);
- }
- }
- var editableIsEmpty = !this.editable.childNodes.length;
- if (editableIsEmpty) {
- var p = this.document.createElement('p');
- p.innerHTML = '<br>';
- this.editable.appendChild(p);
- range = this.context.invoke('editor.setRange', p, 0);
- } else if (this.context.invoke('HelperPlugin.isBlankNode', this.editable.firstChild) && !range.sc.parentNode) {
- this.editable.firstChild.innerHTML = '<br/>';
- range = this.context.invoke('editor.setRange', this.editable.firstChild, 0);
- }
-
- range.select();
- },
- /**
- * Remove all invisible chars before the current range, that are adjacent to it,
- * then rerange.
- *
- * @private
- * @param {Object} range
- * @returns {Object} range
- */
- _removeAllPreviousInvisibleChars: function (range) {
- while (this._isAfterInvisibleChar(range)) {
- var text = range.sc.textContent;
- range.sc.textContent = text.slice(0, range.so - 1) + text.slice(range.so, text.length);
- range.so -= 1;
- }
- return range;
- },
- /**
- * Remove a char from a point's text node, at the point's offset.
- *
- * @private
- * @param {Object} point
- * @returns {Node}
- */
- _removeCharAtOffset: function (point) {
- var text = point.node.textContent;
- var startToOffset = text.slice(0, point.offset);
- var offsetToEnd = text.slice(point.offset + 1);
- point.node.textContent = startToOffset + offsetToEnd;
- return point.node;
- },
- /**
- * Remove any amount of leading/trailing breakable space at range position.
- * Then move the range and return it.
- *
- * @private
- * @param {Object} range
- * @returns {Object} range
- */
- _removeExtremeBreakableSpaceAndRerange: function (range) {
- var isInPre = !!dom.ancestor(range.sc, dom.isPre);
- if (!range.sc.tagName && !isInPre) {
- var changed = this.context.invoke('HelperPlugin.removeExtremeBreakableSpace', range.sc);
- range.so = range.eo = range.so > changed.start ? range.so - changed.start : 0;
- range.so = range.eo = range.so > dom.nodeLength(range.sc) ? dom.nodeLength(range.sc) : range.so;
- range.select();
- this.context.invoke('editor.saveRange');
- }
- return range;
- },
- /**
- * Patch for Google Chrome's contenteditable SPAN bug.
- *
- * @private
- * @param {jQueryEvent} e
- */
- _removeGarbageSpans: function (e) {
- if (e.target.className === "" && e.target.tagName == "SPAN" &&
- e.target.style.fontStyle === "inherit" &&
- e.target.style.fontVariantLigatures === "inherit" &&
- e.target.style.fontVariantCaps === "inherit") {
- var $span = $(e.target);
- $span.after($span.contents()).remove();
- }
- },
- /**
- * Remove the first unbreakable ancestor's next sibling if not a block and empty.
- *
- * @private
- * @param {Node} node
- */
- _removeNextEmptyUnbreakable: function (node) {
- var self = this;
- var unbreakable = dom.ancestor(node, this.options.isUnbreakableNode);
- if (unbreakable === this.editable) {
- return;
- }
- var nextUnbreakable = unbreakable && unbreakable.nextElementSibling;
- var isNextEmpty = nextUnbreakable && dom.isEmpty(nextUnbreakable) && !dom.isVoid(nextUnbreakable);
- var isNextContainsOnlyInvisibleText = nextUnbreakable && _.all($(nextUnbreakable).contents(), function (n) {
- return dom.isText(n) && !self.context.invoke('HelperPlugin.isVisibleText', n);
- });
- if (!dom.isBlock(nextUnbreakable) && (isNextEmpty || isNextContainsOnlyInvisibleText)) {
- $(nextUnbreakable).remove();
- }
- },
- /**
- * If the range's start container is empty and constitutes the only contents of its parent,
- * replace it with an empty p, then rerange.
- *
- * @private
- * @param {Object} range
- * @returns {Object|undefined} range
- */
- _replaceEmptyParentWithEmptyP: function (range) {
- var node = range.sc.childElementCount === 1 && range.sc.firstChild.tagName === 'BR' ? range.sc.firstChild : range.sc;
- if (node === this.editable || !node.parentNode || node.parentNode === this.editable) {
- return;
- }
- if (
- dom.isEmpty(node) &&
- this.context.invoke('HelperPlugin.onlyContains', node.parentNode, node) &&
- ['LI', 'P'].indexOf(node.parentNode.tagName) === -1
- ) {
- var emptyP = this.document.createElement('p');
- var br = this.document.createElement('br');
- $(emptyP).append(br);
- $(node.parentNode).before(emptyP).remove();
- range.sc = range.ec = br;
- range.so = range.eo = 0;
- return range.collapse(true);
- }
- return;
- },
- /**
- * Replace all leading space from a text node with one non-breakable space.
- *
- * @param {Node} node
- * @returns {Node} node
- */
- _replaceLeadingSpaceWithSingleNBSP: function (node) {
- var startSpace = this.context.invoke('HelperPlugin.getRegex', 'startSpace');
- node.textContent = node.textContent.replace(startSpace, '\u00A0');
- return node;
- },
- /**
- * Replace a media node with an empty SPAN and return that SPAN.
- *
- * @param {Node} media
- * @returns {Node} span
- */
- _replaceMediaWithEmptySpan: function (media) {
- var span = this.document.createElement('span');
- media = dom.ancestor(media, function (n) {
- return !n.parentNode || !dom.isMedia(n.parentNode);
- });
- $(media).replaceWith(span);
- return span;
- },
- /**
- * Move the (collapsed) range to get out of BR elements.
- *
- * @private
- * @param {Object} range
- * @returns {Object} range
- */
- _rerangeOutOfBR: function (range, direction) {
- range = this._rerangeToFirstNonBRElementLeaf(range);
- range = this._rerangeToNextNonBR(range, direction === 'next');
- return range;
- },
- /**
- * Move the (collapsed) range to the first leaf that is not a BR element.
- *
- * @private
- * @param {Object} range
- * @returns {Object} range
- */
- _rerangeToFirstNonBRElementLeaf: function (range) {
- var leaf = this.context.invoke('HelperPlugin.firstNonBRElementLeaf', range.sc);
- if (leaf !== range.sc) {
- range = this.context.invoke('editor.setRange', leaf, 0);
- }
- return range;
- },
- /**
- * Move the (collapsed) range to the next (or previous) node that is not a BR element.
- *
- * @private
- * @param {Object} range
- * @param {Boolean} previous true to move to the previous node
- * @returns {Object} range
- */
- _rerangeToNextNonBR: function (range, previous) {
- var point = range.getStartPoint();
- var method = previous ? 'prevPointUntil' : 'nextPointUntil';
- point = dom[method](point, function (pt) {
- return pt.node.tagName !== 'BR';
- });
- range = this.context.invoke('editor.setRange', point.node, point.offset);
- return range;
- },
- /**
- * Move the (collapsed) range to the child of the node at the current offset if possible.
- *
- * @private
- * @param {Object} range
- * @param {String('prev'|'next')} direction
- * @returns {Object} range
- */
- _rerangeToOffsetChild: function (range, direction) {
- if (range.sc.childNodes[range.so]) {
- var node;
- var offset;
- if (direction === 'prev' && range.so > 0) {
- node = range.sc.childNodes[range.so - 1];
- offset = dom.nodeLength(node);
- range = this.context.invoke('editor.setRange', node, offset);
- } else {
- node = range.sc.childNodes[range.so];
- offset = 0;
- range = this.context.invoke('editor.setRange', node, offset);
- }
- }
- return range;
- },
- /**
- * Select all the contents of the current unbreakable ancestor.
- */
- _selectAll: function () {
- var self = this;
- var range = this.context.invoke('editor.createRange');
- var unbreakable = dom.ancestor(range.sc, this.options.isUnbreakableNode);
- var $contents = $(unbreakable).contents();
- var startNode = $contents.length ? $contents[0] : unbreakable;
- var pointA = this.context.invoke('HelperPlugin.makePoint', startNode, 0);
- pointA = dom.nextPointUntil(pointA, function (point) {
- return self.context.invoke('HelperPlugin.isVisibleText', point.node);
- }) || pointA;
- var endNode = $contents.length ? $contents[$contents.length - 1] : unbreakable;
- var endOffset = $contents.length ? dom.nodeLength($contents[$contents.length - 1]) : 1;
- var pointB = this.context.invoke('HelperPlugin.makePoint', endNode, endOffset);
- pointB = dom.prevPointUntil(pointB, function (point) {
- return self.context.invoke('HelperPlugin.isVisibleText', point.node);
- }) || pointB;
- range.sc = pointA.node;
- range.so = pointA.offset;
- range.ec = pointB.node;
- range.eo = pointB.offset;
- range.select().normalize();
- },
- /**
- * Before a deletion, if necessary, slice the text content at range, then rerange.
- *
- * @param {Object} range
- * @returns {Object} range
- */
- _sliceAndRerangeBeforeDeletion: function (range) {
- if (this._isAfterLeadingInvisibleChar(range) && !this._isAfterTwoBRs(range.sc)) {
- range.sc.textContent = range.sc.textContent.slice(1);
- range.so = 0;
- }
- if (this._isBeforeTrailingInvisibleChar(range) && !this._isAfterBR(range.sc)) {
- range.sc.textContent = range.sc.textContent.slice(0, range.so);
- }
- return range;
- },
-
-
- //--------------------------------------------------------------------------
- // Handle
- //--------------------------------------------------------------------------
-
- /**
- * Customize handling of certain keydown events.
- *
- * @private
- * @param {SummernoteEvent} se
- * @param {jQueryEvent} e
- * @returns {Boolean} true if case handled
- */
- _onKeydown: function (se, e) {
- if (e.ctrlKey && e.key === 'a') {
- e.preventDefault();
- this._selectAll();
- return;
- }
- if (!keycode[e.keyCode]) {
- return;
- }
-
- this.context.invoke('editor.clearTarget');
- this.context.invoke('MediaPlugin.hidePopovers');
- this.context.invoke('editor.beforeCommand');
- var handled = false;
- switch (keycode[e.keyCode]) {
- case 'BACKSPACE':
- handled = this._onBackspace(e);
- break;
- case 'TAB':
- handled = this._onTab(e);
- break;
- case 'ENTER':
- handled = this._onEnter(e);
- break;
- case 'DELETE':
- handled = this._onDelete(e);
- break;
- }
- if (handled) {
- e.preventDefault();
- this._preventTextInEditableDiv();
- this.context.invoke('editor.saveRange');
- this.context.invoke('editor.afterCommand');
- }
- return handled;
- },
- /**
- * Handle BACKSPACE keydown event.
- *
- * @private
- * @param {jQueryEvent} e
- * @returns {Boolean} true if case is handled and event default must be prevented
- */
- _onBackspace: function (e) {
- var range = this.context.invoke('editor.createRange');
- var needOutdent = false;
-
- // Special cases
- if (range.isCollapsed()) {
-
- // Do nothing if on left edge of a table cell
- var point = range.getStartPoint();
- if (point.node.childNodes[point.offset]) {
- point.node = point.node.childNodes[point.offset];
- point.offset = dom.nodeLength(point.node);
- }
- if (this.context.invoke('HelperPlugin.isLeftEdgeOfTag', point, 'TD')) {
- return true;
- }
-
- // Outdent if on left edge of an indented block
- point = range.getStartPoint();
- var isIndented = !!dom.ancestor(point.node, function (n) {
- var style = dom.isCell(n) ? 'paddingLeft' : 'marginLeft';
- return n.tagName && !!parseFloat(n.style[style] || 0);
- });
- if (this.context.invoke('HelperPlugin.isLeftEdgeOfBlock', point)) {
- if (isIndented) {
- this.context.invoke('BulletPlugin.outdent');
- return true;
- }
- if (dom.ancestor(range.sc, dom.isLi)) {
- needOutdent = true;
- }
- }
- }
-
- var flag = this._handleDeletion('prev');
-
- if (!flag && needOutdent) {
- range.select();
- this.context.invoke('BulletPlugin.outdent');
- }
-
- return true;
- },
- /**
- * Handle DELETE keydown event.
- *
- * @private
- * @param {jQueryEvent} e
- * @returns {Boolean} true if case is handled and event default must be prevented
- */
- _onDelete: function (e) {
- var range = this.context.invoke('editor.createRange');
-
- // Special case
- if (range.isCollapsed()) {
- // Do nothing if on left edge of a table cell
- if (this.context.invoke('HelperPlugin.isRightEdgeOfTag', range.getStartPoint(), 'TD')) {
- return true;
- }
- }
-
- this._handleDeletion('next');
- return true;
- },
- /**
- * Handle ENTER keydown event.
- *
- * @private
- * @param {jQueryEvent} e
- * @returns {Boolean} true if case is handled and event default must be prevented
- */
- _onEnter: function (e) {
- this.context.invoke('HelperPlugin.deleteSelection');
- if (e.shiftKey) {
- this._handleShiftEnter();
- } else if (e.ctrlKey) {
- this.context.invoke('TextPlugin.insertHR');
- } else {
- this._handleEnter();
- }
- return true;
- },
- /**
- * Handle TAB keydown event.
- *
- * @private
- * @param {jQueryEvent} e
- * @returns {Boolean} true if case is handled and event default must be prevented
- */
- _onTab: function (e) {
- // If TAB not handled, prevent default and do nothing
- if (!this.options.keyMap.pc.TAB) {
- this.trigger_up('wysiwyg_blur', {
- key: 'TAB',
- keyCode: 9,
- shiftKey: e.shiftKey,
- });
- return true;
- }
- var range = this.context.invoke('editor.createRange');
- var point = range.getStartPoint();
- var startSpace = this.context.invoke('HelperPlugin.getRegex', 'startSpace');
-
- if (!range.isOnCell()) {
- // If on left edge point: indent/outdent
- if (!point.node.tagName) { // Clean up start spaces on textNode
- point.node.textContent.replace(startSpace, function (startSpaces) {
- point.offset = startSpaces.length === point.offset ? 0 : point.offset;
- return '';
- });
- }
- if (this.context.invoke('HelperPlugin.isLeftEdgeOfBlock', point) || dom.isEmpty(point.node)) {
- if (e.shiftKey) {
- this.context.invoke('BulletPlugin.outdent');
- } else {
- this.context.invoke('BulletPlugin.indent');
- }
- this.context.invoke('HelperPlugin.normalize');
- return true;
- }
- // Otherwise insert a tab or do nothing
- if (!e.shiftKey) {
- this.context.invoke('TextPlugin.insertTab');
- this.context.invoke('HelperPlugin.normalize');
- }
- return true;
- }
- // In table, on tab switch to next cell
- return false;
- },
- /**
- * Handle char input.
- *
- * @private
- * @param {jQueryEvent} e
- */
- _onKeyPress: function (e) {
- console.log(e.originalEvent);
- if (!isSafari || iPhone) {
- e.preventDefault();
- this.context.invoke('HelperPlugin.insertTextInline', e.key);
- }
- },
- /**
- * Handle char input.
- *
- * @private
- * @param {jQueryEvent} e
- */
- _onTextInput: function (e) {
- if (isSafari && !iPhone) {
- return;
- }
- e.originalEvent.preventDefault();
-
- var text = e.originalEvent.data;
- if (!text) {
- return;
- }
- if (text === 'Spacebar') {
- text = ' ';
- }
- this.context.invoke('HelperPlugin.insertTextInline', text);
- },
-});
-
-registry.add('KeyboardPlugin', KeyboardPlugin);
-
-return KeyboardPlugin;
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/link.js b/addons/web_editor/static/src/js/wysiwyg/plugin/link.js
deleted file mode 100644
index d8d261df980..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/link.js
+++ /dev/null
@@ -1,493 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.link', function (require) {
-'use strict';
-
-var core = require('web.core');
-var LinkDialog = require('wysiwyg.widgets.LinkDialog');
-var Plugins = require('web_editor.wysiwyg.plugins');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-
-var _t = core._t;
-var dom = $.summernote.dom;
-dom.isAnchor = function (node) {
- return (node.tagName === 'A' || node.tagName === 'BUTTON' || $(node).hasClass('btn')) &&
- !$(node).hasClass('fa') && !$(node).hasClass('o_image');
-};
-
-//--------------------------------------------------------------------------
-// link
-//--------------------------------------------------------------------------
-
-var LinkPlugin = Plugins.linkDialog.extend({
- events: {
- 'dblclick .note-editable': '_onDblclick',
- },
-
- //--------------------------------------------------------------------------
- // Public summernote module API
- //--------------------------------------------------------------------------
-
- /**
- * @param {Object} layoutInfo
- */
- show: function () {
- var self = this;
- var linkInfo = this.context.invoke('editor.getLinkInfo');
- this.context.invoke('editor.saveRange');
- return this.showLinkDialog(linkInfo)
- .then(function (linkInfo) {})
- .guardedCatch(function () {});
- },
- /**
- * @override
- * @param {Object} linkInfo
- * @returns {Promise}
- */
- showLinkDialog: function (linkInfo) {
- var self = this;
- this.context.invoke('editor.hidePopover');
- var media = this.context.invoke('editor.restoreTarget');
-
- if (linkInfo.range) {
- var r = linkInfo.range.getPoints();
-
- // move carret in icon, video...
- if (!media && (dom.ancestor(r.sc, dom.isImg) || dom.ancestor(r.sc, dom.isIcon))) {
- media = dom.ancestor(r.sc, dom.isImg) || dom.ancestor(r.sc, dom.isIcon);
- }
- // if select a text content in anchor then click on image then click on anchor button
- if (dom.isImg(media) || dom.isIcon(media)) {
- r.sc = media.parentNode;
- r.so = [].indexOf.call(media.parentNode.childNodes, media);
- r.ec = media;
- r.eo = dom.nodeLength(media);
-
- linkInfo.range.sc = r.sc;
- linkInfo.range.so = r.so;
- linkInfo.range.ec = r.ec;
- linkInfo.range.eo = r.eo;
- }
-
- linkInfo.isAnchor = linkInfo.range.isOnAnchor();
- linkInfo.className = linkInfo.isAnchor ? dom.ancestor(r.sc, dom.isAnchor).className : '';
- linkInfo.url = linkInfo.isAnchor ? ($(dom.ancestor(r.sc, dom.isAnchor)).attr('href') || '').replace(window.location.origin, '') : '';
-
- var nodes = [];
- if (linkInfo.isAnchor) {
- nodes = dom.ancestor(r.sc, dom.isAnchor).childNodes;
- } else if (!linkInfo.range.isCollapsed()) {
- if (dom.isImg(media) || dom.isIcon(media)) {
- nodes.push(media);
- } else {
- if (r.sc.tagName) {
- r.sc = (r.so ? r.sc.childNodes[r.so] : r.sc).firstChild || r.sc;
- r.so = 0;
- } else if (r.so !== r.sc.textContent.length) {
- if (r.sc === r.ec) {
- r.ec = r.sc = r.sc.splitText(r.so);
- r.eo -= r.so;
- } else {
- r.sc = r.sc.splitText(r.so);
- }
- r.so = 0;
- }
- if (r.ec.tagName) {
- r.ec = (r.eo ? r.ec.childNodes[r.eo - 1] : r.ec).lastChild || r.ec;
- r.eo = r.ec.textContent.length;
- } else if (r.eo !== r.ec.textContent.length) {
- r.ec.splitText(r.eo);
- }
-
- // browsers can't target a picture or void node
- if (dom.isVoid(r.sc) || dom.isImg(r.sc)) {
- r.so = dom.listPrev(r.sc).length - 1;
- r.sc = r.sc.parentNode;
- }
- if (dom.isBR(r.ec)) {
- r.eo = dom.listPrev(r.ec).length - 1;
- r.ec = r.ec.parentNode;
- } else if (dom.isVoid(r.ec) || dom.isImg(r.sc)) {
- r.eo = dom.listPrev(r.ec).length;
- r.ec = r.ec.parentNode;
- }
- linkInfo.range.sc = r.sc;
- linkInfo.range.so = r.so;
- linkInfo.range.ec = r.ec;
- linkInfo.range.eo = r.eo;
- linkInfo.range.select();
- this.context.invoke('editor.saveRange');
- linkInfo.range = this.context.invoke('editor.createRange');
-
- // search nodes to insert in the anchor
-
- var startPoint = this.context.invoke('HelperPlugin.makePoint', r.sc, r.so);
- var endPoint = this.context.invoke('HelperPlugin.makePoint', r.ec, r.eo);
- dom.walkPoint(startPoint, endPoint, function (point) {
- var node = point.node.childNodes && point.node.childNodes[point.offset] || point.node;
- nodes.push(node);
- });
-
- nodes = _.filter(_.uniq(nodes), function (node) {
- return nodes.indexOf(node.parentNode) === -1;
- });
- }
- }
-
- if (nodes.length > 0) {
- var text = "";
- linkInfo.images = [];
- for (var i = 0; i < nodes.length; i++) {
- if (dom.ancestor(nodes[i], dom.isImg)) {
- text += dom.ancestor(nodes[i], dom.isImg).outerHTML;
- } else if (dom.ancestor(nodes[i], dom.isIcon)) {
- text += dom.ancestor(nodes[i], dom.isIcon).outerHTML;
- } else if (!linkInfo.isAnchor && nodes[i].nodeType === 1) {
- // just use text nodes from listBetween
- } else if (!linkInfo.isAnchor && i === 0) {
- text += nodes[i].textContent;
- } else if (!linkInfo.isAnchor && i === nodes.length - 1) {
- text += nodes[i].textContent;
- } else {
- text += nodes[i].textContent;
- }
- }
- linkInfo.text = text.replace(this.context.invoke('HelperPlugin.getRegex', 'space', 'g'), ' ');
- }
-
- linkInfo.needLabel = !linkInfo.text.length;
- }
-
- var def = new Promise(function (resolve, reject) {
- var linkDialog = new LinkDialog(self.options.parent, {
- onClose: function () {
- setTimeout(function () {
- self.context.invoke('editor.focus');
- });
- }
- },
- _.omit(linkInfo, 'range')
- );
-
- linkDialog.on('save', self, self._wrapCommand(function (newLinkInfo) {
- var isCollapsed = linkInfo.range.isCollapsed();
- linkInfo.range.select();
- newLinkInfo.range = linkInfo.range;
-
- var $anchor;
- if (linkInfo.isAnchor) {
- $anchor = $(dom.ancestor(r.sc, dom.isAnchor));
- $anchor.css(newLinkInfo.style || {});
- if (newLinkInfo.isNewWindow) {
- $anchor.attr('target', '_blank');
- } else {
- $anchor.removeAttr('target');
- }
- } else {
- self.context.invoke('editor.saveRange');
- self.context.invoke('editor.createLink', newLinkInfo);
- var range = self.context.invoke('editor.createRange');
- var anchor = dom.ancestor(range.sc.childNodes[range.so] || range.sc, dom.isAnchor);
- $anchor = $(anchor);
- if (isCollapsed) {
- // move the range just after the link
- var point = dom.nextPoint({
- node: anchor,
- offset: dom.nodeLength(anchor),
- });
- range = self.context.invoke('editor.setRange', point.node, point.offset);
- range.select();
- } else {
- $anchor.selectContent();
- }
- }
- if ((dom.isImg(media) || dom.isIcon(media)) && !$anchor.find(media).length) {
- $(media).remove();
- }
- $anchor.attr('class', newLinkInfo.className);
- $anchor.attr('href', newLinkInfo.url);
- self.context.invoke('editor.saveRange');
- self.context.invoke('editor.saveTarget', $anchor[0]);
- self.context.triggerEvent('focusnode', $anchor[0]);
- resolve(linkInfo);
- }));
- linkDialog.on('closed', self, function () {
- this.context.invoke('editor.restoreRange');
- this.context.invoke('LinkPopover.update');
- reject();
- });
- linkDialog.open();
- });
- return def;
- },
- /**
- * Remove the current link, keep its contents.
- *
- * @override
- */
- unlink: function () {
- var rng = this.context.invoke('editor.createRange');
- var anchor = rng.sc;
- while (anchor && anchor.tagName !== 'A') {
- anchor = anchor.parentElement;
- }
- if (!anchor) {
- this.context.invoke('editor.hidePopover');
- return;
- }
- var startAndEndInvisible = this.context.invoke('HelperPlugin.getRegex', 'startAndEndInvisible', 'g');
- anchor.innerHTML = anchor.innerHTML.replace(startAndEndInvisible, '');
- var $contents = $(anchor).contents();
- $(anchor).before($contents).remove();
-
- this.context.invoke('editor.hidePopover');
-
- var start = $contents[0];
- var end = $contents.last()[0];
- rng = this.context.invoke('editor.setRange', start, 0, end, dom.nodeLength(end));
- rng.select();
- this.editable.normalize();
- this.context.invoke('editor.saveRange');
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * @override
- */
- _addButtons: function () {
- var self = this;
- this._super();
-
- this.context.memo('help.LinkPlugin.show', this.options.langInfo.help['linkDialog.show']);
-
- this.context.memo('button.linkPlugin', function () {
- return self.context.invoke('buttons.button', {
- contents: self.ui.icon(self.options.icons.link),
- tooltip: self.lang.link.link + self.context.invoke('buttons.representShortcut', 'LinkPlugin.show'),
- click: self.context.createInvokeHandler('LinkPlugin.show')
- }).render();
- });
- },
- /**
- * @param {jQueryEvent} e
- */
- _onDblclick: function (e) {
- if (dom.ancestor(e.target, dom.isAnchor) && !dom.isMedia(e.target)) {
- this.show();
- }
- },
-});
-
-var LinkPopover = Plugins.linkPopover.extend({
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Prevent links without text: replace empty text with the word "Label".
- *
- * @param {DOM} anchor
- * @param {bool} select
- */
- fillEmptyLink: function (anchor, select) {
- if (dom.isAnchor(anchor)) {
- var text = _t('Label');
- if (this.context.invoke('HelperPlugin.getRegex', 'onlyEmptySpace').test(anchor.innerHTML)) {
- $(anchor).contents().remove();
- $(anchor).append(this.document.createTextNode(text));
- }
- var rng = this.context.invoke('editor.createRange');
- if (select && anchor.innerHTML === text) {
- this._cleanLastLink();
- rng.sc = rng.ec = anchor.firstChild;
- rng.so = 0;
- rng.eo = dom.nodeLength(rng.sc);
- rng.select();
- this.context.invoke('editor.saveRange');
- }
- if (select) {
- this.lastAnchor = anchor;
- }
- }
- },
- /**
- * @override
- */
- hide: function () {
- this._cleanLastLink();
- this._super();
- },
- /**
- * @override
- */
- update: function () {
- var rng = this.context.invoke('editor.createRange');
- var anchor = dom.ancestor(rng.sc, dom.isAnchor);
- if (anchor && anchor === dom.ancestor(rng.ec, dom.isAnchor)) {
- anchor = dom.ancestor(rng.sc, dom.isAnchor);
- if (!$(anchor).is(':o_editable')) {
- this.hide();
- return;
- }
- } else {
- anchor = false;
- }
-
- if ($(anchor).data('toggle') === 'tab') {
- anchor = false;
- }
-
- if (!this.options.displayPopover(anchor)) {
- anchor = false;
- }
-
- if (anchor !== this.lastAnchor) {
- this._cleanLastLink();
- }
-
- if (!anchor) {
- this.hide();
- return;
- }
-
- if (dom.isAnchor(anchor)) {
- this.lastAnchor = anchor;
- }
-
-
- var $target = $(anchor);
- if (!$target.data('show_tooltip') && this.context.options.tooltip) {
- $target.data('show_tooltip', true);
- setTimeout(function () {
- $target.tooltip({
- title: _t('Double-click to edit'),
- trigger: 'manual',
- container: this.document.body,
- placement: 'top'
- }).tooltip('show');
- setTimeout(function () {
- $target.tooltip('dispose');
- }, 2000);
- }, 400);
- }
-
- var innerHTML = anchor.innerHTML.replace('&nbsp;', '\u00A0').replace('&#8203;', '\u200B');
-
- // prevent links without text
- this.fillEmptyLink(anchor, true);
-
- // add invisible char to prevent the carret to leave the link at the begin or end
- if (innerHTML[0] !== '\u200B') {
- var before = this.document.createTextNode('\u200B');
- $(anchor).prepend(before);
- }
- if (innerHTML[innerHTML.length - 1] !== '\u200B') {
- var after = this.document.createTextNode('\u200B');
- $(anchor).append(after);
- }
-
- anchor.normalize();
-
- var href = $(anchor).attr('href');
- this.$popover.find('a').attr('href', href).html(href);
-
- var pos = $(anchor).offset();
- var posContainer = $(this.options.container).offset();
- pos.left = pos.left - posContainer.left + 10;
- pos.top = pos.top - posContainer.top + $(anchor).outerHeight();
-
- this.$popover.css({
- display: 'block',
- left: pos.left,
- top: pos.top,
- });
-
- this.context.layoutInfo.editor.after(this.$popover);
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Clean the last link and prevent links without text.
- *
- * @private
- */
- _cleanLastLink: function () {
- if (this.lastAnchor) {
- this.fillEmptyLink(this.lastAnchor);
-
- var range = this.context.invoke('editor.createRange');
- var rangeChange;
- var innerHTML = this.lastAnchor.innerHTML.replace('&nbsp;', '\u00A0').replace('&#8203;', '\u200B');
- // prevent links without text
- if (
- this.$editable.has(this.lastAnchor).length &&
- this.context.invoke('HelperPlugin.getRegexBlank', {
- space: true,
- invisible: true,
- nbsp: true,
- }).test(innerHTML)
- ) {
- $(this.lastAnchor).contents().remove();
- $(this.lastAnchor).append(this.document.createTextNode(_t('Label')));
- range.sc = range.ec = this.lastAnchor.firstChild;
- range.so = 0;
- range.eo = dom.nodeLength(range.sc);
- rangeChange = true;
- }
- var firstChild = this.context.invoke('HelperPlugin.firstLeaf', this.lastAnchor);
- if (
- !firstChild.tagName &&
- this.context.invoke('HelperPlugin.getRegex', 'startInvisible').test(firstChild.textContent)
- ) {
- firstChild.textContent = firstChild.textContent.replace(this.context.invoke('HelperPlugin.getRegex', 'startInvisible'), '');
- if (range.sc === firstChild && range.so) {
- range.so -= 1;
- rangeChange = true;
- }
- if (range.ec === firstChild && range.eo) {
- range.eo -= 1;
- rangeChange = true;
- }
- }
- var lastChild = this.context.invoke('HelperPlugin.lastLeaf', this.lastAnchor);
- if (
- lastChild.textContent.length > 1 &&
- !lastChild.tagName &&
- this.context.invoke('HelperPlugin.getRegex', 'endInvisible').test(lastChild.textContent)
- ) {
- lastChild.textContent = lastChild.textContent.replace(this.context.invoke('HelperPlugin.getRegex', 'endInvisible'), '');
- if (range.sc === lastChild && range.so > dom.nodeLength(lastChild)) {
- range.so = dom.nodeLength(lastChild);
- rangeChange = true;
- }
- if (range.ec === lastChild && range.eo > dom.nodeLength(lastChild)) {
- range.eo = dom.nodeLength(lastChild);
- rangeChange = true;
- }
- }
- if (rangeChange) {
- range.select();
- this.context.invoke('editor.saveRange');
- }
- this.lastAnchor = null;
- }
- },
-});
-
-registry.add('LinkPlugin', LinkPlugin)
- .add('LinkPopover', LinkPopover)
- .add('linkDialog', null)
- .add('linkPopover', null)
- .add('autoLink', null);
-
-return {
- LinkPlugin: LinkPlugin,
- LinkPopover: LinkPopover,
-};
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/media.js b/addons/web_editor/static/src/js/wysiwyg/plugin/media.js
deleted file mode 100644
index b8b4a7582fe..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/media.js
+++ /dev/null
@@ -1,1288 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.media', function (require) {
-'use strict';
-
-var core = require('web.core');
-var weWidgets = require('wysiwyg.widgets');
-var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-var Plugins = require('web_editor.wysiwyg.plugins');
-var wysiwygTranslation = require('web_editor.wysiwyg.translation');
-var wysiwygOptions = require('web_editor.wysiwyg.options');
-
-var _t = core._t;
-
-var dom = $.summernote.dom;
-var ui = $.summernote.ui;
-
-//--------------------------------------------------------------------------
-// Media (for image, video, icon, document)
-//--------------------------------------------------------------------------
-
-/**
- * Return true if the node is a media (image, icon, document or video).
- *
- * @param {Node} node
- * @returns {Boolean}
- */
-dom.isMedia = function (node) {
- return dom.isImg(node) ||
- dom.isIcon(node) ||
- dom.isDocument(node) ||
- dom.isVideo(node);
-};
-
-var MediaPlugin = AbstractPlugin.extend({
- events: {
- 'summernote.mousedown': '_onMouseDown',
- 'summernote.keydown': '_onKeydown',
- 'summernote.keyup': '_onKeyup',
- 'summernote.scroll': '_onScroll',
- 'summernote.disable': '_onDisable',
- 'summernote.change': '_onChange',
- 'summernote.codeview.toggled': '_onToggled',
- 'dblclick .note-editable': '_onDblclick',
- },
-
- mousePosition: {},
- _modalOpen: false,
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Open the image dialog and listen to its saved/closed events.
- */
- showImageDialog: function () {
- if (this._modalOpen) {
- return;
- }
- this._modalOpen = true;
- this.context.invoke('editor.saveRange');
- var media = this.context.invoke('editor.restoreTarget');
-
- var $mediaParent = $(media).parent();
- if ($mediaParent.hasClass('media_iframe_video')) {
- media = $mediaParent[0];
- $mediaParent = $mediaParent.parent();
- }
-
- // If the image is for a field the model should handle the resize so we
- // use an arbitrary high size of 1920 instead of using the width of the
- // the element.
- var mediaWidth = $(media).parent().data('oeField') ? 1920 : $(media).width();
-
- var mediaDialog = new weWidgets.MediaDialog(this.options.parent, {
- mediaWidth: mediaWidth,
- onlyImages: $mediaParent.data('oeField') === 'image' || $mediaParent.data('oeType') === 'image',
- },
- $(media).clone()[0]
- );
-
- mediaDialog.on('save', this, function (element) {
- this.insertMedia(media, element);
- });
- mediaDialog.on('closed', this, function () {
- this.context.invoke('editor.restoreRange');
- this._modalOpen = false;
- });
- mediaDialog.open();
- },
- /**
- * Remove the current target media and hide its popover.
- */
- removeMedia: function () {
- this.context.invoke('editor.beforeCommand');
- var target = this.context.invoke('editor.restoreTarget');
- var point = this.context.invoke('HelperPlugin.removeBlockNode', target);
- var rng = this.context.invoke('editor.setRange', point.node, point.offset);
- rng.normalize().select();
- this.context.invoke('editor.saveRange');
- this.context.invoke('editor.clearTarget');
- this.hidePopovers();
- this.context.invoke('editor.afterCommand');
- },
- /**
- * Update the target media and its popover.
- *
- * @param {Node} target
- */
- update: function (target) {
- if (!target || !dom.isMedia(target)) {
- return;
- }
- if (!this.options.displayPopover(target)) {
- if (dom.isImg(target)) {
- this.context.invoke('HandlePlugin.update', target);
- }
- return;
- }
-
- this.lastPos = this.context.invoke('HelperPlugin.makePoint', target, $(target).offset());
-
- this.context.triggerEvent('focusnode', target);
-
- if (!dom.isMedia(target)) {
- return;
- }
-
- this.context.invoke('editor.saveTarget', target);
-
- var $target = $(target);
- if (!$target.data('show_tooltip') && this.context.options.tooltip) {
- $target.data('show_tooltip', true);
- setTimeout(function () {
- $target.tooltip({
- title: _t('Double-click to edit'),
- trigger: 'manual',
- container: this.document.body,
- }).tooltip('show');
- setTimeout(function () {
- $target.tooltip('dispose');
- }, 2000);
- }, 400);
- }
-
- if (dom.isImg(target)) {
- this.context.invoke('ImagePlugin.show', target, this.mousePosition);
- } else if (dom.isIcon(target)) {
- this.context.invoke('IconPlugin.show', target, this.mousePosition);
- } else if (dom.isVideo(target)) {
- this.context.invoke('VideoPlugin.show', target, this.mousePosition);
- } else if (dom.isDocument(target)) {
- this.context.invoke('DocumentPlugin.show', target, this.mousePosition);
- }
- },
- /**
- * Hide all open popovers.
- * Warning: removes the saved target.
- */
- hidePopovers: function () {
- var media = this.context.invoke('editor.restoreTarget');
- this.context.invoke('HandlePlugin.hide');
- this.context.invoke('ImagePlugin.hide');
- this.context.invoke('IconPlugin.hide');
- this.context.invoke('VideoPlugin.hide');
- this.context.invoke('DocumentPlugin.hide');
- this.context.invoke('editor.saveTarget', media);
- },
- /**
- * Insert or replace a media.
- *
- * @param {Node} previous the media to replace, if any
- * @param {Node} newMedia contains the media to insert
- */
- insertMedia: function (previous, newMedia) {
- if (!newMedia) {
- return;
- }
- this._wrapCommand(function () {
- this.$editable.focus();
- var rng = this.context.invoke('editor.createRange');
- var point;
-
- if (newMedia.tagName === "IMG") {
- $(newMedia).one('load error abort', this.updatePopoverAfterEdit.bind(this, newMedia));
- }
-
- if (previous) {
- this.context.invoke('editor.clearTarget');
- var start = previous.parentNode;
- rng = this.context.invoke('editor.setRange', start, _.indexOf(start.childNodes, previous));
- if (previous.tagName === newMedia.tagName) {
- // Eg: replace an image with an image -> reapply classes removed by `clear`
- var reFaIcons = /fa-(?!spin(\s|$))\S+/g; // do not keep fontawesome icons but keep fa-spin
- $(newMedia).addClass(previous.className.replace(reFaIcons, ''));
- }
-
- if (dom.isVideo(previous) || dom.isVideo(newMedia)) {
- var doNotInsertP = previous.tagName === newMedia.tagName;
- point = this.context.invoke('HelperPlugin.removeBlockNode', previous, doNotInsertP);
- if (!rng.sc.parentNode || !rng.sc.childNodes[rng.so]) {
- rng = this.context.invoke('editor.setRange', point.node, point.offset);
- }
- previous = null;
- }
- rng.select();
- this.hidePopovers();
- }
-
- if (dom.isVideo(newMedia)) {
- this.context.invoke('HelperPlugin.insertBlockNode', newMedia);
- } else {
- rng = this.context.invoke('editor.createRange');
- point = rng.getStartPoint();
- if (!rng.isCollapsed()) {
- point = this.context.invoke('HelperPlugin.deleteBetween', point, rng.getEndPoint());
- }
-
- if (point.node.tagName) {
- if (previous) {
- $(previous).replaceWith(newMedia);
- } else if (dom.isVoid(point.node)) {
- point.node.parentNode.insertBefore(newMedia, point.node);
- } else {
- var node = point.node.childNodes[point.offset];
- if (point.node.tagName === 'BR') {
- $(point.node).replaceWith(newMedia);
- } else if (node && node.tagName === 'BR') {
- $(node).replaceWith(newMedia);
- } else {
- point.node.insertBefore(newMedia, node || null);
- }
- }
- if (!this._isFakeNotEditable(newMedia)) {
- if (!newMedia.previousSibling) {
- $(newMedia).before(this.document.createTextNode('\u200B'), newMedia);
- }
- if (!newMedia.nextSibling) {
- $(newMedia).after(this.document.createTextNode('\u200B'), newMedia);
- }
- }
- } else {
- var next = this.document.createTextNode(point.node.textContent.slice(point.offset));
- point.node.textContent = point.node.textContent.slice(0, point.offset);
-
- $(point.node).after(next).after(newMedia);
- point.node.parentNode.normalize();
- if (!this._isFakeNotEditable(newMedia)) {
- if (!newMedia.previousSibling) {
- $(newMedia).before(this.document.createTextNode('\u200B'), newMedia);
- }
- if (!newMedia.nextSibling) {
- $(newMedia).after(this.document.createTextNode('\u200B'), newMedia);
- }
- }
- rng = this.context.invoke('editor.setRange', newMedia.nextSibling || newMedia, 0);
- rng.normalize().select();
- }
- }
- this.context.invoke('editor.saveRange');
- this.context.invoke('editor.saveTarget', newMedia);
- this.context.triggerEvent('focusnode', newMedia);
- this.context.invoke('UnbreakablePlugin.secureArea', newMedia);
-
- this.updatePopoverAfterEdit(newMedia);
- })();
- },
- /**
- * Update the media's popover and its position after editing the media.
- *
- * @param {Node} media
- */
- updatePopoverAfterEdit: function (media) {
- this.mousePosition = {
- pageX: $(media).offset().left + $(media).width() / 2,
- pageY: $(media).offset().top + $(media).height() / 2,
- };
- $(media).trigger('mousedown').trigger('mouseup');
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Add the media popovers' buttons:
- * - Replacement
- * - Removal
- * - Alignment
- * - Padding
- *
- * @override
- */
- _addButtons: function () {
- var self = this;
- this._super();
-
- this.context.memo('button.mediaPlugin', function () {
- return self.context.invoke('buttons.button', {
- contents: self.ui.icon(self.options.icons.picture),
- tooltip: self.lang.image.image,
- click: self.context.createInvokeHandler('MediaPlugin.showImageDialog')
- }).render();
- });
-
- this.context.memo('button.removePluginMedia', function () {
- return self.context.invoke('buttons.button', {
- contents: self.ui.icon(self.options.icons.trash),
- tooltip: self.lang.image.remove,
- click: self._wrapCommand(function () {
- this.context.invoke('MediaPlugin.removeMedia');
- })
- }).render();
- });
-
- _.each(['left', 'center', 'right', 'none'], function (align) {
- var alignName = _.str.camelize('align_' + align);
- self._createButton(alignName, self.options.icons[alignName], self.lang.image[alignName], function () {
- var $target = $(self.context.invoke('editor.restoreTarget'));
- $target.css('float', '').removeClass('mx-auto pull-right pull-left');
- if (align === 'center') {
- $target.addClass('mx-auto');
- } else if (align !== 'none') {
- $target.addClass('pull-' + align);
- }
- });
- });
-
- var padding = [null, 'padding-small', 'padding-medium', 'padding-large', 'padding-xl'];
- var zipped = _.zip(padding, this.lang.image.paddingList);
- var values = _.map(zipped, function (z) {
- return {
- value: z[0],
- string: z[1],
- };
- });
- this._createDropdownButton('padding', this.options.icons.padding, this.lang.image.padding, values);
- },
- /**
- * Return true if the node is a fake not-editable.
- *
- * @param {Node} node
- * @returns {Boolean}
- */
- _isFakeNotEditable: function (node) {
- var contentEditableAncestor = dom.ancestor(node, function (n) {
- return !!n.contentEditable && n.contentEditable !== 'inherit';
- });
- return !!contentEditableAncestor && contentEditableAncestor.contentEditable === 'false';
- },
- /**
- * Select the target media based on the
- * currently saved target or on the current range.
- *
- * @private
- * @param {Node} [target] optional
- * @returns {Node} target
- */
- _selectTarget: function (target) {
- if (!target) {
- target = this.context.invoke('editor.restoreTarget');
- }
-
- if (this.context.isDisabled()) {
- this.hidePopovers();
- this.context.invoke('editor.clearTarget');
- return target;
- }
- var range = this.context.invoke('editor.createRange');
- if (!target && range.isCollapsed()) {
- target = range.sc.childNodes[range.so] || range.sc;
- }
- if (!target || !dom.isMedia(target)) {
- this.hidePopovers();
- this.context.invoke('editor.clearTarget');
- return target;
- }
-
- while (target.parentNode && dom.isMedia(target.parentNode)) {
- target = target.parentNode;
- }
-
- if (!this.options.isEditableNode(target)) {
- if (!target.parentNode) {
- target = this.editable;
- }
- this.hidePopovers();
- this.context.invoke('editor.clearTarget');
- return target;
- }
-
- this.context.invoke('editor.saveTarget', target);
- this.context.triggerEvent('focusnode', target);
-
- return target;
- },
- /**
- * Select the target media on the right (or left)
- * of the currently selected target media.
- *
- * @private
- * @param {Node} target
- * @param {Boolean} left
- */
- _moveTargetSelection: function (target, left) {
- if (!target || !dom.isMedia(target)) {
- return;
- }
- var range = this.context.invoke('editor.createRange');
- var $contentEditable;
-
- if (
- range.sc.tagName && $.contains(target, range.sc) &&
- $(range.sc).hasClass('o_fake_editable') &&
- left === !range.sc.previousElementSibling
- ) {
- $contentEditable = $(range.sc).closest('[contentEditable]');
- if ($(target).closest('[contentEditable]')[0] !== $contentEditable[0]) {
- $contentEditable.focus();
- }
- this.context.invoke('editor.saveRange');
- return;
- }
-
- var next = this.context.invoke('HelperPlugin.makePoint', target, 0);
- if (left) {
- if (dom.isVideo(target)) {
- next = this.context.invoke('HelperPlugin.makePoint', target.firstElementChild, 0);
- } else {
- next = dom.prevPointUntil(next, function (point) {
- return point.node !== target && !$.contains(target, point.node);
- }) || next;
- }
- } else {
- if (dom.isVideo(target)) {
- next = this.context.invoke('HelperPlugin.makePoint', target.lastElementChild, 0);
- } else {
- next = dom.nextPointUntil(next, function (point) {
- return point.node !== target && !$.contains(target, point.node);
- }) || next;
- }
- }
-
- $contentEditable = $(next.node).closest('[contentEditable]');
- if ($(target).closest('[contentEditable]')[0] !== $contentEditable[0]) {
- // move the focus only if the new contentEditable is not the same (avoid scroll up)
- // (like in the case of a video, which uses two contentEditable in the media, so as to write text)
- $contentEditable.focus();
- }
-
- range = this.context.invoke('editor.setRange', next.node, next.offset);
- range.select();
-
- this.context.invoke('editor.saveRange');
- },
-
- //--------------------------------------------------------------------------
- // handle
- //--------------------------------------------------------------------------
-
- /**
- * @private
- */
- _onDisable: function () {
- this.hidePopovers();
- this.context.invoke('editor.clearTarget');
- },
- /**
- * @private
- * @param {jQueryEvent} e
- */
- _onDblclick: function (e) {
- if (dom.isMedia(e.target)) {
- var target = this._selectTarget(e.target);
- this._moveTargetSelection(target);
- this.showImageDialog();
- }
- },
- /**
- * @private
- **/
- _onKeydown: function () {
- this.context.invoke('editor.clearTarget');
- },
- /**
- * @private
- * @param {SummernoteEvent} se
- * @param {jQueryEvent} e
- */
- _onKeyup: function (se, e) {
- var target = this._selectTarget();
- var range = this.context.invoke('editor.createRange');
- if (e.keyCode === 37) {
- var point = dom.prevPoint(range.getStartPoint());
- if (dom.isMedia(point.node)) {
- target = point.node;
- }
- }
- this._moveTargetSelection(target, e.keyCode === 37);
- return this.update(target);
- },
- /**
- * @private
- */
- _onScroll: function () {
- var target = this._selectTarget();
- if (this.lastPos && this.lastPos.target === target && $(target).offset()) {
- var newTop = $(target).offset().top;
- var movement = this.lastPos.offset.top - newTop;
- if (movement && this.mousePosition) {
- this.mousePosition.pageY -= movement;
- }
- }
- return this.update(target);
- },
- /**
- * @private
- */
- _onChange: function () {
- var target = this._selectTarget();
- this._moveTargetSelection(target);
- if (!this.$editable.has(target).length) {
- return;
- }
- return this.update(target);
- },
- /**
- * @private
- * @param {SummernoteEvent} se
- * @param {jQueryEvent} e
- */
- _onMouseDown: function (se, e) {
- var target = this._selectTarget(e.target);
- if (target && dom.isMedia(target)) {
- var pos = $(target).offset();
-
- if (e.pageX) {
- this.mousePosition = {
- pageX: e.pageX,
- pageY: e.pageY,
- };
- } else {
- // for testing triggers
- this.mousePosition = {
- pageX: pos.left,
- pageY: pos.top,
- };
- }
-
- var width = $(target).width();
- // we put the cursor to the left if we click in the first tier of the media
- var left = this.mousePosition.pageX < (pos.left + width / 3);
- this._moveTargetSelection(target, left);
-
- this.update(target);
- e.preventDefault();
- } else {
- this.mousePosition = {};
- }
- },
- /**
- * @private
- */
- _onToggled: function () {
- this.update();
- },
-});
-
-_.extend(wysiwygOptions.icons, {
- alignCenter: 'note-icon-align-center',
- alignNone: wysiwygOptions.icons.alignJustify,
- picture: 'fa fa-file-image-o',
-});
-_.extend(wysiwygTranslation.image, {
- alignRight: wysiwygTranslation.image.floatRight,
- alignCenter: _t('Align center'),
- alignLeft: wysiwygTranslation.image.floatLeft,
- alignNone: wysiwygTranslation.image.floatNone,
-});
-
-//--------------------------------------------------------------------------
-// Abstract
-//--------------------------------------------------------------------------
-
-var AbstractMediaPlugin = AbstractPlugin.extend({
- targetType: null,
- initialize: function () {
- this._super.apply(this, arguments);
- this.$popover = this.ui.popover({
- className: 'note-' + this.targetType + '-popover',
- })
- .render().appendTo(this.options.container);
- var $content = this.$popover.find('.popover-content, .note-popover-content');
- this.context.invoke('buttons.build', $content, this.options.popover[this.targetType]);
- this.options.POPOVER_MARGIN = 15;
- },
- destroy: function () {
- this.$popover.remove();
- },
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Hide this popover.
- */
- hide: function () {
- this.$popover.hide();
- },
- /**
- * Show this popover.
- *
- * @param {Node} target
- * @param {Object} mousePosition {pageX: Number, pageY: Number}
- */
- show: function (target, mousePosition) {
- this._popoverPosition(target, mousePosition);
- ui.toggleBtnActive(this.$popover.find('a, button'), false);
- var $target = $(target);
-
- var float = $target.css('float');
- if (float === 'none' && $target.hasClass('mx-auto')) {
- float = 'center';
- }
- var floatIcon = this.options.icons[_.str.camelize('align_' + (float !== 'none' ? float : 'justify'))];
- ui.toggleBtnActive(this.$popover.find('.note-float button:has(.' + floatIcon + ')'), true);
-
- var padding = (($target.attr('class') || '').match(/(^| )(padding-[^\s]+)( |$)/) || ['fa-1x'])[2];
- ui.toggleBtnActive(this.$popover.find('.note-padding a:has(li[data-value="' + padding + '"])'), true);
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Takes the left and top position of the popover and an optional margin
- * and returns updated positions to force the popover to fit the editor container.
- *
- * @private
- * @param {Number} left
- * @param {Number} top
- * @param {Number} [margin] optional
- * @returns {Object} {left: Number, top: Number}
- */
- _popoverFitEditor: function (left, top, margin) {
- margin = margin || this.options.POPOVER_MARGIN;
-
- var $container = $(this.options.container);
- var containerWidth = $container.width();
- var containerHeight = $container.height();
-
- var popoverWidth = this.$popover.width();
- var popoverHeight = this.$popover.height();
-
- var isBeyondXBounds = left + popoverWidth >= containerWidth - margin;
- var isBeyondYBounds = top + popoverHeight >= containerHeight - margin;
- return {
- left: isBeyondXBounds ? containerWidth - popoverWidth - margin : left,
- top: isBeyondYBounds ? top = containerHeight - popoverHeight - margin : (top > 0 ? top : margin),
- };
- },
- /**
- * Update the position of the popover in CSS.
- *
- * @private
- * @param {Node} target
- * @param {Object} mousePosition {pageX: Number, pageY: Number}
- */
- _popoverPosition: function (target, mousePosition) {
- var pos = $(this.options.container).offset();
- pos.left = mousePosition.pageX - pos.left + this.options.POPOVER_MARGIN;
- pos.top = mousePosition.pageY - pos.top + this.options.POPOVER_MARGIN;
-
- var popoverPos = this._popoverFitEditor(pos.left, pos.top);
- this.$popover.css({
- display: 'block',
- left: popoverPos.left,
- top: popoverPos.top,
- });
- },
- /**
- * Override to return whether the target is a media (specific to its class) or not.
- *
- * @private
- * @param {Node} target
- */
- _isMedia: function (target) {},
-});
-
-//--------------------------------------------------------------------------
-// Image
-//--------------------------------------------------------------------------
-
-var ImagePlugin = AbstractMediaPlugin.extend({
- targetType: 'image',
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Open the crop image dialog and listen to its save/closed events.
- */
- cropImageDialog: function () {
- this.context.invoke('editor.saveRange');
-
- var media = this.context.invoke('editor.restoreTarget');
- var cropImageDialog = new weWidgets.CropImageDialog(this.options.parent, {},
- $(media).clone()[0]
- );
- cropImageDialog.on('save', this, function (img) {
- this.context.invoke('MediaPlugin.insertMedia', media, img);
- });
- cropImageDialog.on('closed', this, function () {
- this.context.invoke('editor.restoreRange');
- });
-
- cropImageDialog.open();
- },
- /**
- * Open the alt dialog (change image title and alt) and listen to its save/closed events.
- */
- altDialg: function () {
- this.context.invoke('editor.saveRange');
-
- var media = this.context.invoke('editor.restoreTarget');
- var altDialog = new weWidgets.AltDialog(this.options.parent, {},
- $(media).clone()[0]
- );
- altDialog.on('save', this, this._wrapCommand(function (img) {
- $(media).attr('alt', img.alt)
- .attr('title', img.title)
- .trigger('content_changed');
- }));
- altDialog.on('closed', this, function () {
- this.context.invoke('editor.restoreRange');
- });
-
- altDialog.open();
- },
- /**
- * Show the image target's popover.
- *
- * @override
- * @param {Node} target
- * @param {Object} mousePosition {pageX: Number, pageY: Number}
- */
- show: function (target, mousePosition) {
- var self = this;
- this._super.apply(this, arguments);
- var $target = $(target);
-
- this.context.invoke('HandlePlugin.update', target);
-
- _.each(this.options.icons.imageShape, function (icon, className) {
- var thisIconSel = '.note-imageShape button:has(.' +
- icon.replace(self.context.invoke('HelperPlugin.getRegex', 'space', 'g'), '.') +
- ')';
- ui.toggleBtnActive(self.$popover.find(thisIconSel), $target.hasClass(className));
- });
-
- var size = (($target.attr('style') || '').match(/width:\s*([0-9]+)%/i) || [])[1];
- ui.toggleBtnActive(this.$popover.find('.note-imagesize button:contains(' + (size ? size + '%' : this.lang.image.imageSizeAuto) + ')'), true);
-
- ui.toggleBtnActive(this.$popover.find('.note-cropImage button'), $target.hasClass('o_cropped_img_to_save'));
-
- // update alt button in popover
- if ($target.attr('alt')) {
- var $altLabel = $(this.altBtnPrefix).text(function (i, v) {
- var newText = v + '\u00A0' + $target.attr('alt');
- return $.trim(newText).substring(0, 30).trim(this) + "...";
- });
- this.$popover.find('.note-alt button').contents().replaceWith($altLabel);
- }
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Save cropped images.
- *
- * @private
- * @returns {Promise}
- */
- _saveCroppedImages: function () {
- var self = this;
- var defs = this.$editables.find('.o_cropped_img_to_save').map(function () {
- var $croppedImg = $(this);
- $croppedImg.removeClass('o_cropped_img_to_save');
- var resModel = $croppedImg.data('crop:resModel');
- var resID = $croppedImg.data('crop:resID');
- var cropID = $croppedImg.data('crop:id');
- var mimetype = $croppedImg.data('crop:mimetype');
- var originalSrc = $croppedImg.data('crop:originalSrc');
- var datas = $croppedImg.attr('src').split(',')[1];
- if (!cropID) {
- var name = originalSrc + '.crop';
- return self._rpc({
- model: 'ir.attachment',
- method: 'create',
- args: [{
- res_model: resModel,
- res_id: resID,
- name: name,
- datas: datas,
- mimetype: mimetype,
- url: originalSrc, // To save the original image that was cropped
- }],
- }).then(function (attachmentID) {
- return self._rpc({
- model: 'ir.attachment',
- method: 'generate_access_token',
- args: [
- [attachmentID],
- ],
- }).then(function (access_token) {
- $croppedImg.attr('src', '/web/image/' + attachmentID + '?access_token=' + access_token[0]);
- });
- });
- } else {
- return self._rpc({
- model: 'ir.attachment',
- method: 'write',
- args: [
- [cropID], {
- datas: datas,
- },
- ],
- });
- }
- }).get();
- return Promise.all(defs);
- },
- /**
- * Add the image popovers' buttons:
- * From _super:
- * - Replacement
- * - Removal
- * - Alignment
- * - Padding
- * From this override:
- * - Shape
- * - Crop
- * - Alt
- * - Size
- *
- * @private
- */
- _addButtons: function () {
- var self = this;
- this._super();
- // add all shape buttons if this option is active
- this.context.memo('button.imageShape', function () {
- var $el = $();
- _.each(['rounded', 'rounded-circle', 'shadow', 'img-thumbnail'], function (shape) {
- $el = $el.add(self._createToggleButton(null, self.options.icons.imageShape[shape], self.lang.image.imageShape[shape], shape));
- });
- return $el;
- });
- this.context.memo('button.cropImage', function () {
- return self.context.invoke('buttons.button', {
- contents: self.ui.icon(self.options.icons.cropImage),
- tooltip: self.lang.image.cropImage,
- click: self.context.createInvokeHandler('ImagePlugin.cropImageDialog')
- }).render();
- });
- this.altBtnPrefix = '<b>' + self.lang.image.alt + '</b>';
- this.context.memo('button.alt', function () {
- return self.context.invoke('buttons.button', {
- contents: self.altBtnPrefix,
- click: self.context.createInvokeHandler('ImagePlugin.altDialg')
- }).render();
- });
- this.context.memo('button.imageSizeAuto', function () {
- return self.context.invoke('buttons.button', {
- contents: '<span class="note-iconsize-10">' + self.lang.image.imageSizeAuto + '</span>',
- click: self._wrapCommand(function () {
- var target = this.context.invoke('editor.restoreTarget');
- $(target).css({
- width: '',
- height: ''
- });
- })
- }).render();
- });
- },
- /**
- * Return true if the target is an image.
- *
- * @override
- * @param {Node} target
- * @returns {Boolean} true if the target is an image
- */
- _isMedia: function (target) {
- return dom.isImg(target);
- },
-});
-
-_.extend(wysiwygOptions.icons, {
- padding: 'fa fa-plus-square-o',
- cropImage: 'fa fa-crop',
- imageShape: {
- rounded: 'fa fa-square',
- 'rounded-circle': 'fa fa-circle-o',
- shadow: 'fa fa-sun-o',
- 'img-thumbnail': 'fa fa-picture-o',
- },
-});
-_.extend(wysiwygTranslation.image, {
- padding: _t('Padding'),
- paddingList: [_t('None'), _t('Small'), _t('Medium'), _t('Large'), _t('Xl')],
- imageSizeAuto: _t('Auto'),
- cropImage: _t('Crop image'),
- imageShape: {
- rounded: _t('Shape: Rounded'),
- 'rounded-circle': _t('Shape: Circle'),
- shadow: _t('Shape: Shadow'),
- 'img-thumbnail': _t('Shape: Thumbnail'),
- },
- alt: _t('Description:'),
-});
-
-//--------------------------------------------------------------------------
-// Video
-//--------------------------------------------------------------------------
-
-/**
- * Return true if the node is a video.
- *
- * @param {Node} node
- * @returns {Boolean}
- */
-dom.isVideo = function (node) {
- node = node && !node.tagName ? node.parentNode : node;
- return (node.tagName === "IFRAME" || node.tagName === "DIV") &&
- (node.parentNode && node.parentNode.className && node.parentNode.className.indexOf('media_iframe_video') !== -1 ||
- node.className.indexOf('media_iframe_video') !== -1);
-};
-
-var VideoPlugin = AbstractMediaPlugin.extend({
- targetType: 'video',
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Show the video target's popover.
- *
- * @override
- * @param {Node} target
- * @param {Object} mousePosition {pageX: Number, pageY: Number}
- */
- show: function (target, mousePosition) {
- if (target.tagName === "DIV" && target.className.indexOf('css_editable_mode_display') !== -1) {
- target = target.parentNode;
- this.context.invoke('editor.saveTarget', target);
- }
- return this._super.apply(this, arguments);
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Return true if the target is a video.
- *
- * @override
- * @param {Node} target
- */
- _isMedia: function (target) {
- return dom.isVideo(target);
- },
-});
-
-//--------------------------------------------------------------------------
-// Icons: Icon Awsome (and other with themes)
-//--------------------------------------------------------------------------
-
-/**
- * Return true if the node is an icon.
- *
- * @param {Node} node
- * @returns {Boolean}
- */
-dom.isIcon = function (node) {
- node = node && !node.tagName ? node.parentNode : node;
- return node && node.className && node.className.indexOf(' fa-') !== -1;
-};
-
-var IconPlugin = AbstractMediaPlugin.extend({
- targetType: 'icon',
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Hide this icon's popover.
- *
- * @override
- */
- hide: function () {
- this._super();
- this.removeSelectedClass(this.lastIcon);
- },
- /**
- * Show the icon target's popover.
- *
- * @override
- * @param {Node} target
- */
- show: function (target) {
- this.removeSelectedClass(this.lastIcon);
- this.lastIcon = target;
-
- this._super.apply(this, arguments);
-
- var $target = $(target);
- ui.toggleBtnActive(this.$popover.find('.note-faSpin button'), $target.hasClass('fa-spin'));
- var faSize = parseInt((($target.attr('style') || '').match(/font-size:\s*([0-9](em|px))(;|$)/) || [])[1] || 0);
- if (!faSize) {
- faSize = (($target.attr('class') || '').match(/(^| )fa-([0-9])x( |$)/) || [])[2];
- }
- ui.toggleBtnActive(this.$popover.find('.note-faSize a[data-value="fa-' + faSize + 'x"]'), true);
- this.addSelectedClass(target);
- },
- /**
- * Add a class to the current target so as to show it's selected.
- *
- * @param {Node} target
- */
- addSelectedClass: function (target) {
- if (target) {
- $(target).addClass('o_we_selected_image');
- }
- },
- /**
- * Remove a class to the current target so as to show it's not selected.
- *
- * @param {Node} target
- */
- removeSelectedClass: function (target) {
- if (target) {
- $(target).removeClass('o_we_selected_image');
- }
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Add the icon popovers' buttons:
- * From _super:
- * - Replacement
- * - Removal
- * - Alignment
- * - Padding
- * From this override:
- * - Size
- * - Spin
- *
- * @private
- */
- _addButtons: function () {
- var self = this;
- this._super();
- var values = [{
- value: '',
- string: '1x',
- },
- {
- value: 'fa-2x',
- string: '2x',
- },
- {
- value: 'fa-3x',
- string: '3x',
- },
- {
- value: 'fa-4x',
- string: '4x',
- },
- {
- value: 'fa-5x',
- string: '5x',
- },
- ];
- var onFaSize = function (e) {
- var classNames = _.map(values, function (item) {
- return item.value;
- }).join(' ');
- var $target = $(self.context.invoke('editor.restoreTarget'));
- $target.removeClass(classNames);
- if ($(e.target).data('value')) {
- $target.addClass($(e.target).data('value'));
- $target.css('fontSize', '');
- }
- };
- this._createDropdownButton('faSize', this.options.icons.faSize, this.lang.image.faSize, values, onFaSize);
- this._createToggleButton('faSpin', this.options.icons.faSpin, this.lang.image.faSpin, 'fa-spin');
- },
- /**
- * Return true if the target is an icon.
- *
- * @override
- * @param {Node} target
- */
- _isMedia: function (target) {
- return dom.isIcon(target);
- },
- /**
- * Update the position of the popover in CSS.
- *
- * @override
- * @param {Node} target
- * @param {Object} mousePosition {pageX: Number, pageY: Number}
- */
- _popoverPosition: function (target, mousePosition) {
- var pos = $(target).offset();
- var posContainer = $(this.options.container).offset();
- pos.left = pos.left - posContainer.left + this.options.POPOVER_MARGIN + parseInt($(target).css('font-size')) + 10;
- pos.top = pos.top - posContainer.top + this.options.POPOVER_MARGIN;
-
- var popoverPos = this._popoverFitEditor(pos.left + 10, pos.top - 15);
- this.$popover.css({
- display: 'block',
- left: popoverPos.left,
- top: popoverPos.top,
- });
- },
-});
-_.extend(wysiwygOptions.icons, {
- faSize: 'fa fa-expand',
- faSpin: 'fa fa-refresh',
-});
-_.extend(wysiwygTranslation.image, {
- faSize: _t('Icon size'),
- faSpin: _t('Spin'),
-});
-
-//--------------------------------------------------------------------------
-// Media Document
-//--------------------------------------------------------------------------
-
-/**
- * Return true is the node is a document.
- * @param {Node} node
- * @returns {Boolean}
- */
-dom.isDocument = function (node) {
- node = node && !node.tagName ? node.parentNode : node;
- return node && (node.tagName === "A" && node.className.indexOf('o_image') !== -1);
-};
-
-var DocumentPlugin = AbstractMediaPlugin.extend({
- targetType: 'document',
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Return true if the target is an icon.
- *
- * @override
- * @param {Node} target
- */
- _isMedia: function (target) {
- return dom.isDocument(target);
- },
- /**
- * Update the position of the popover in CSS.
- *
- * @override
- * @param {Node} target
- * @param {Object} mousePosition {pageX: Number, pageY: Number}
- */
- _popoverPosition: function (target, mousePosition) {
- var pos = $(target).offset();
- var posContainer = $(this.options.container).offset();
- pos.left = pos.left - posContainer.left + this.options.POPOVER_MARGIN;
- pos.top = pos.top - posContainer.top + this.options.POPOVER_MARGIN;
-
- var popoverPos = this._popoverFitEditor(pos.left + 10, pos.top - 15);
- this.$popover.css({
- display: 'block',
- left: popoverPos.left,
- top: popoverPos.top,
- });
- },
-});
-
-//--------------------------------------------------------------------------
-// Handle (hover image)
-//--------------------------------------------------------------------------
-
-var HandlePlugin = Plugins.handle.extend({
- /**
- * Update the handle.
- *
- * @param {Node} target
- * @returns {Boolean}
- */
- update: function (target) {
- if (this.context.isDisabled()) {
- return false;
- }
- var isImage = dom.isImg(target);
- var $selection = this.$handle.find('.note-control-selection');
- this.context.invoke('imagePopover.update', target);
- if (!isImage) {
- return isImage;
- }
-
- var $target = $(target);
- var pos = $target.offset();
- var posContainer = $selection.closest('.note-handle').offset();
-
- // exclude margin
- var imageSize = {
- w: $target.outerWidth(false),
- h: $target.outerHeight(false)
- };
-
- $selection.css({
- display: 'block',
- left: pos.left - posContainer.left,
- top: pos.top - posContainer.top,
- width: imageSize.w,
- height: imageSize.h,
- }).data('target', $target); // save current target element.
-
- var src = $target.attr('src');
- var displayInfo = imageSize.w >= 170 || (imageSize.w >= 120 && imageSize.h >= 58) || (imageSize.w >= 80 && imageSize.h >= 76);
-
- var sizingText = '';
- if (displayInfo) {
- sizingText = imageSize.w + 'x' + imageSize.h;
- sizingText += ' (' + this.lang.image.original + ': ';
- } else if (src && imageSize.w >= 80 && imageSize.h >= 32) {
- displayInfo = true;
- sizingText = '(';
- }
-
- if (src) {
- var origImageObj = new Image();
- origImageObj.src = src;
- sizingText += origImageObj.width + 'x' + origImageObj.height + ')';
- }
-
- $selection.find('.note-control-selection-info').text(sizingText);
- this.context.invoke('editor.saveTarget', target);
-
- return isImage;
- },
-});
-
-//--------------------------------------------------------------------------
-// add to registry
-//--------------------------------------------------------------------------
-
-registry.add('MediaPlugin', MediaPlugin)
- .add('ImagePlugin', ImagePlugin)
- .add('VideoPlugin', VideoPlugin)
- .add('IconPlugin', IconPlugin)
- .add('DocumentPlugin', DocumentPlugin)
- .add('HandlePlugin', HandlePlugin);
-
-// modules to remove from summernote
-registry.add('imagePopover', null)
- .add('handle', null);
-
-return {
- MediaPlugin: MediaPlugin,
- ImagePlugin: ImagePlugin,
- VideoPlugin: VideoPlugin,
- IconPlugin: IconPlugin,
- DocumentPlugin: DocumentPlugin,
-};
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/plugins.js b/addons/web_editor/static/src/js/wysiwyg/plugin/plugins.js
deleted file mode 100644
index dfd5b109e74..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/plugins.js
+++ /dev/null
@@ -1,78 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugins', function (require) {
-'use strict';
-
-var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-var wysiwygOptions = require('web_editor.wysiwyg.options');
-
-
-var plugins = _.mapObject(wysiwygOptions.modules, function (Module, pluginName) {
- var prototype = {
- init: function () {
- var self = this;
- this._super.apply(this, arguments);
- var events = _.clone(this.events);
- this.summernote.options.modules[pluginName].apply(this, arguments);
- _.each(events, function (value, key) {
- self.events[key] = value;
- });
- },
- };
- _.each(Module.prototype, function (prop, name) {
- if (typeof prop === 'function') {
- prototype[name] = function () {
- return this.summernote.options.modules[pluginName].prototype[name].apply(this, arguments);
- };
- } else {
- prototype[name] = prop;
- }
- });
-
- var Plugin = AbstractPlugin.extend(prototype).extend({
- destroy: function () {
- if (this.shouldInitialize()) {
- this._super();
- }
- },
- });
-
- // override summernote default buttons
- registry.add(pluginName, Plugin);
-
- return Plugin;
-});
-
-// export table plugin to convert it in module (see editor)
-
-var $textarea = $('<textarea>');
-$('body').append($textarea);
-$textarea.summernote();
-var summernote = $textarea.data('summernote');
-
-_.each(['style', 'table', 'typing', 'bullet', 'history'], function (name) {
- var prototype = {};
- for (var k in summernote.modules.editor[name]) {
- prototype[k] = summernote.modules.editor[name][k];
- }
- plugins[name] = AbstractPlugin.extend(prototype);
-});
-
-var History = summernote.modules.editor.history.constructor;
-plugins.history.include({
- init: function (context) {
- this._super(context);
- History.call(this, this.$editable);
- },
-});
-
-try {
- $textarea.summernote('destroy');
-} catch (e) {
- summernote.layoutInfo.editor.remove();
-}
-$textarea.remove();
-
-
-return plugins;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/table.js b/addons/web_editor/static/src/js/wysiwyg/plugin/table.js
deleted file mode 100644
index b015faee43f..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/table.js
+++ /dev/null
@@ -1,244 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.table', function (require) {
-'use strict';
-
-var Plugins = require('web_editor.wysiwyg.plugins');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-
-var dom = $.summernote.dom;
-
-
-var TablePlugin = Plugins.table.extend({
-
- initialize: function () {
- this._super.apply(this, arguments);
- var self = this;
- // We need setTimeout to make sure to initialize after HelperPlugin and HistoryPlugin
- setTimeout(function () {
- // contentEditable fail for image and font in table
- // user must use right arrow the number of space but without feedback
- self.$editable.find('td:has(img, span.fa)').each(function () {
- if (this.firstChild && !this.firstChild.tagName) {
- var startSpace = self.context.invoke('HelperPlugin.getRegex', 'startSpace');
- this.firstChild.textContent = this.firstChild.textContent.replace(startSpace, ' ');
- }
- if (this.lastChild && !this.lastChild.tagName) {
- var endSpace = self.context.invoke('HelperPlugin.getRegex', 'endSpace');
- this.lastChild.textContent = this.lastChild.textContent.replace(endSpace, ' ');
- }
- });
- self.context.invoke('HistoryPlugin.clear');
- });
- },
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Add a new col and
- * wrap contents of the new cells in p elements.
- *
- * @see summernote library
- *
- * @override
- * @param {WrappedRange} rng
- * @param {String('left'|'right')} position
- */
- addCol: function (rng, position) {
- this._super.apply(this, arguments);
- var cell = dom.ancestor(rng.commonAncestor(), dom.isCell);
- var table = dom.ancestor(cell, function (n) {
- return n.tagName === 'TABLE';
- });
- var newColIndex = $(cell)[position === 'right' ? 'next' : 'prev']('td').index() + 1;
- $(table).find('td:nth-child(' + newColIndex + ')').contents().wrap('<p></p>');
- },
- /**
- * Add a new row and
- * wrap contents of the new cells in p elements.
- *
- * @see summernote library
- *
- * @override
- * @param {WrappedRange} rng
- * @param {String('top'|'bottom')} position
- */
- addRow: function (rng, position) {
- this._super.apply(this, arguments);
- var row = dom.ancestor(rng.commonAncestor(), dom.isCell).parentElement;
- $(row)[position === 'bottom' ? 'next' : 'prev']('tr').find('td').contents().wrap('<p></p>');
- },
- /**
- * Create empty table element and
- * wrap the contents of all cells in p elements.
- *
- * @see summernote library
- *
- * @override
- * @param {Number} rowCount
- * @param {Number} colCount
- * @returns {Node} table
- */
- createTable: function () {
- var table = this._super.apply(this, arguments);
- $(table).find('td').contents().wrap('<p></p>');
- return table;
- },
- /**
- * @see summernote library
- *
- * @override
- */
- deleteCol: function () {
- var range = this.context.invoke('editor.createRange');
-
- // Delete the last remaining column === delete the table
- var cell = dom.ancestor(range.commonAncestor(), dom.isCell);
- if (cell && !cell.previousElementSibling && !cell.nextElementSibling) {
- return this.deleteTable();
- }
- var neighbor = cell.previousElementSibling || cell.nextElementSibling;
-
- this._super.apply(this, arguments);
-
- // Put the range back on the previous or next cell after deleting
- // to allow chain-removing
- range = this.context.invoke('editor.createRange');
- if (range.sc.tagName !== 'TD' && neighbor) {
- range = this.context.invoke('editor.setRange', neighbor, 0);
- range.normalize().select();
- }
- },
- /**
- * @see summernote library
- *
- * @override
- */
- deleteRow: function () {
- var range = this.context.invoke('editor.createRange');
-
- // Delete the last remaining row === delete the table
- var row = dom.ancestor(range.commonAncestor(), function (n) {
- return n.tagName === 'TR';
- });
- if (row && !row.previousElementSibling && !row.nextElementSibling) {
- return this.deleteTable();
- }
- var neighbor = row.previousElementSibling || row.nextElementSibling;
-
- this._super.apply(this, arguments);
-
- // Put the range back on the previous or next row after deleting
- // to allow chain-removing
- range = this.context.invoke('editor.createRange');
- if (range.sc.tagName !== 'TR' && neighbor) {
- range = this.context.invoke('editor.setRange', neighbor, 0);
- range.normalize().select();
- }
- },
- /**
- * Delete the table in range.
- */
- deleteTable: function () {
- var range = this.context.invoke('editor.createRange');
- var cell = dom.ancestor(range.commonAncestor(), dom.isCell);
- var table = $(cell).closest('table')[0];
-
- var point = this.context.invoke('HelperPlugin.removeBlockNode', table);
- range = this.context.invoke('editor.setRange', point.node, point.offset);
- range.normalize().select();
- },
- /**
- * Insert a table.
- * Note: adds <p><br></p> before/after the table if the table
- * has nothing brefore/after it, so as to allow the carret to move there.
- *
- * @param {String} dim dimension of table (ex : "5x5")
- */
- insertTable: function (dim) {
- var dimension = dim.split('x');
- var table = this.createTable(dimension[0], dimension[1], this.options);
- this.context.invoke('HelperPlugin.insertBlockNode', table);
- var p;
- if (!table.previousElementSibling) {
- p = this.document.createElement('p');
- $(p).append(this.document.createElement('br'));
- $(table).before(p);
- }
- if (!table.nextElementSibling) {
- p = this.document.createElement('p');
- $(p).append(this.document.createElement('br'));
- $(table).after(p);
- }
- var range = this.context.invoke('editor.setRange', $(table).find('td')[0], 0);
- range.normalize().select();
- this.context.invoke('editor.saveRange');
- },
-});
-
-
-var TablePopover = Plugins.tablePopover.extend({
- events: _.defaults({
- 'summernote.scroll': '_onScroll',
- }, Plugins.tablePopover.prototype.events),
-
- /**
- * Update the table's popover and its position.
- *
- * @override
- * @param {Node} target
- * @returns {false|Node} the selected cell (on which to display the popover)
- */
- update: function (target) {
- if (!target || this.context.isDisabled()) {
- return false;
- }
- var cell = dom.ancestor(target, dom.isCell);
- if (!!cell && this.options.isEditableNode(cell)) {
- var pos = $(cell).offset();
- var posContainer = $(this.options.container).offset();
- pos.left = pos.left - posContainer.left + 10;
- pos.top = pos.top - posContainer.top + $(cell).outerHeight() - 4;
-
- this.lastPos = this.context.invoke('HelperPlugin.makePoint', target, $(target).offset());
-
- this.$popover.css({
- display: 'block',
- left: pos.left,
- top: pos.top,
- });
- } else {
- this.hide();
- }
- return cell;
- },
- /**
- * Update the target table and its popover.
- *
- * @private
- */
- _onScroll: function () {
- var range = this.context.invoke('editor.createRange');
- var target = dom.ancestor(range.sc, dom.isCell);
- if (!target || target === this.editable) {
- return;
- }
- if (this.lastPos && this.lastPos.target === target && $(target).offset()) {
- var newTop = $(target).offset().top;
- var movement = this.lastPos.offset.top - newTop;
- if (movement && this.mousePosition) {
- this.mousePosition.pageY -= movement;
- }
- }
- return this.update(target);
- },
-});
-
-
-registry.add('TablePlugin', TablePlugin);
-registry.add('TablePopover', TablePopover);
-registry.add('tablePopover', null);
-
-return TablePlugin;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/text.js b/addons/web_editor/static/src/js/wysiwyg/plugin/text.js
deleted file mode 100644
index 2b7d10d02fd..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/text.js
+++ /dev/null
@@ -1,811 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.text', function (require) {
-'use strict';
-
-var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-
-var dom = $.summernote.dom;
-dom.isAnchor = function (node) {
- return (node.tagName === 'A' || node.tagName === 'BUTTON' || $(node).hasClass('btn')) &&
- !$(node).hasClass('fa') && !$(node).hasClass('o_image');
-};
-
-
-var TextPlugin = AbstractPlugin.extend({
- events: {
- 'summernote.paste': '_onPaste',
- },
-
- // See: https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#removeformat-candidate
- formatTags: [
- 'abbr',
- 'acronym',
- 'b',
- 'bdi',
- 'bdo',
- 'big',
- 'blink',
- 'cite',
- 'code',
- 'dfn',
- 'em',
- 'font',
- 'i',
- 'ins',
- 'kbd',
- 'mark',
- 'nobr',
- 'q',
- 's',
- 'samp',
- 'small',
- 'span',
- 'strike',
- 'strong',
- 'sub',
- 'sup',
- 'tt',
- 'u',
- 'var',
- ],
- tab: '\u00A0\u00A0\u00A0\u00A0',
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Insert a Horizontal Rule element (hr).
- */
- insertHR: function () {
- var self = this;
- var hr = this.document.createElement('hr');
- this.context.invoke('HelperPlugin.insertBlockNode', hr);
- var point = this.context.invoke('HelperPlugin.makePoint', hr, 0);
- point = dom.nextPointUntil(point, function (pt) {
- return pt.node !== hr && !self.options.isUnbreakableNode(pt.node);
- }) || point;
- var range = $.summernote.range.create(point.node, point.offset);
- range.select();
- },
- /**
- * Insert a TAB (4 non-breakable spaces).
- */
- insertTab: function () {
- this.context.invoke('HelperPlugin.insertTextInline', this.tab);
- },
- /**
- * Paste nodes or their text content into the editor.
- *
- * @param {Node[]} nodes
- * @param {Boolean} textOnly true to allow only dropping plain text
- */
- pasteNodes: function (nodes, textOnly) {
- if (!nodes.length) {
- return;
- }
- nodes = textOnly ? this.document.createTextNode($(nodes).text()) : nodes;
- nodes = textOnly ? nodes : this._mergeAdjacentULs(nodes);
-
- var point = this._getPastePoint();
- // Prevent pasting HTML within a link:
- point = textOnly ? point : dom.nextPointUntil(point, this._isPointInAnchor.bind(this));
-
- this._insertNodesAt(nodes, point);
-
- var start = nodes[nodes.length - 1];
- this.context.invoke('editor.setRange', start, dom.nodeLength(start)).normalize().select();
- },
- /**
- * Prepare clipboard data for safe pasting into the editor.
- *
- * @see clipboardWhitelist
- * @see clipboardBlacklist
- *
- * @param {DOMString} clipboardData
- * @returns {Node[]}
- */
- prepareClipboardData: function (clipboardData) {
- var $html = $('<wrapper>' + clipboardData + '</wrapper>').contents();
- var $clipboardData = this._removeIllegalClipboardElements($html);
-
- var $all = $clipboardData.find('*').addBack();
- $all.filter('table').addClass('table table-bordered');
- this._wrapTDContents($all.filter('td'));
- this._fillEmptyBlocks($all);
- this._removeIllegalClipboardAttributes($all);
- $all.filter('a').removeClass();
- $all.filter('img').css('max-width', '100%');
-
- return $clipboardData.toArray();
- },
- /**
- * Format a 'format' block: change its tagName (eg: p -> h1).
- *
- * @param {string} tagName
- * P, H1, H2, H3, H4, H5, H6, BLOCKQUOTE, PRE
- */
- formatBlock: function (tagName) {
- var self = this;
- var r = this.context.invoke('editor.createRange');
- if (
- !r ||
- !this.$editable.has(r.sc).length ||
- !this.$editable.has(r.ec).length ||
- this.options.isUnbreakableNode(r.sc)
- ) {
- return;
- }
- var nodes = this.context.invoke('HelperPlugin.getSelectedNodes');
- nodes = this.context.invoke('HelperPlugin.filterFormatAncestors', nodes);
- if (!nodes.length) {
- var node = this.context.invoke('editor.createRange').sc;
- if (node.tagName === 'BR' || dom.isText(node)) {
- node = node.parentNode;
- }
- nodes = [node];
- }
- var changedNodes = [];
- _.each(nodes, function (node) {
- var newNode = self.document.createElement(tagName);
- $(newNode).append($(node).contents());
- var attributes = $(node).prop("attributes");
- _.each(attributes, function (attr) {
- $(newNode).attr(attr.name, attr.value);
- });
- $(node).replaceWith(newNode);
- changedNodes.push(newNode);
- });
-
- // Select all formatted nodes
- if (changedNodes.length) {
- var lastNode = changedNodes[changedNodes.length - 1];
- var startNode = changedNodes[0].firstChild || changedNodes[0];
- var endNode = lastNode.lastChild || lastNode;
- var range = this.context.invoke('editor.setRange', startNode, 0, endNode, dom.nodeLength(endNode));
- range.select();
- }
- },
- /**
- * Change the paragraph alignment of a 'format' block.
- *
- * @param {string} style
- * justifyLeft, justifyCenter, justifyRight, justifyFull
- */
- formatBlockStyle: function (style) {
- var self = this;
- var nodes = this.context.invoke('HelperPlugin.getSelectedNodes');
- nodes = this.context.invoke('HelperPlugin.filterFormatAncestors', nodes);
- var align = style === 'justifyLeft' ? 'left' :
- style === 'justifyCenter' ? 'center' :
- style === 'justifyRight' ? 'right' : 'justify';
- _.each(nodes, function (node) {
- if (dom.isText(node)) {
- return;
- }
- var textAlign = self.window.getComputedStyle(node).textAlign;
- if (align !== textAlign) {
- if (align !== self.window.getComputedStyle(node.parentNode).textAlign) {
- $(node).css('text-align', align);
- } else {
- $(node).css('text-align', '');
- }
- }
- });
- this.editable.normalize();
- },
- /**
- * (Un-)format text: make it bold, italic, ...
- *
- * @param {string} tagName
- * bold, italic, underline, strikethrough, superscript, subscript
- */
- formatText: function (tagName) {
- var self = this;
- var tag = {
- bold: 'B',
- italic: 'I',
- underline: 'U',
- strikethrough: 'S',
- superscript: 'SUP',
- subscript: 'SUB',
- } [tagName];
- if (!tag) {
- throw new Error(tagName);
- }
-
- var range = this.context.invoke('editor.createRange');
- if (!range || !this.$editable.has(range.sc).length || !this.$editable.has(range.ec).length) {
- return;
- }
- if (range.isCollapsed()) {
- var br;
- if (range.sc.tagName === 'BR') {
- br = range.sc;
- } else if (range.sc.firstChild && range.sc.firstChild.tagName === 'BR') {
- br = range.sc.firstChild;
- }
- if (br) {
- var emptyText = this.document.createTextNode('\u200B');
- $(br).before(emptyText).remove();
- range = this.context.invoke('editor.setRange', emptyText, 0, emptyText, 1);
- } else {
- var offset = range.so;
- this.context.invoke('HelperPlugin.insertTextInline', '\u200B');
- range = this.context.invoke('editor.createRange');
- range.so = offset;
- range.eo = offset + 1;
- }
- range.select();
- }
-
- var nodes = this.context.invoke('HelperPlugin.getSelectedNodes');
- var texts = this.context.invoke('HelperPlugin.filterLeafChildren', nodes);
- var formatted = this.context.invoke('HelperPlugin.filterFormatAncestors', nodes);
-
- var start = this.context.invoke('HelperPlugin.firstLeaf', nodes[0]);
- var end = this.context.invoke('HelperPlugin.lastLeaf', nodes[nodes.length - 1]);
-
- function containsOnlySelectedText(node) {
- return _.all(node.childNodes, function (n) {
- return _.any(texts, function (t) {
- return n === t && !(dom.isText(n) && n.textContent === '');
- }) && containsOnlySelectedText(n);
- });
- }
-
- function containsAllSelectedText(node) {
- return _.all(texts, function (t) {
- return _.any(node.childNodes, function (n) {
- return n === t && !(dom.isText(n) && n.textContent === '') || containsAllSelectedText(n);
- });
- });
- }
-
- var nodeAlreadyStyled = [];
- var toStyled = [];
- var notStyled = _.filter(texts, function (text, index) {
- if (toStyled.indexOf(text) !== -1 || nodeAlreadyStyled.indexOf(text) !== -1) {
- return;
- }
- nodeAlreadyStyled.push(text);
-
- end = text;
-
- var styled = dom.ancestor(text, function (node) {
- return node.tagName === tag;
- });
- if (styled) {
- if (
- !/^\u200B$/.test(text.textContent) &&
- containsAllSelectedText(styled) &&
- containsOnlySelectedText(styled)
- ) {
- // Unwrap all contents
- nodes = $(styled).contents();
- $(styled).before(nodes).remove();
- nodeAlreadyStyled.push.apply(nodeAlreadyStyled, nodes);
- end = _.last(nodeAlreadyStyled);
- } else {
- var options = {
- isSkipPaddingBlankHTML: true,
- isNotSplitEdgePoint: true,
- };
-
- if (
- nodeAlreadyStyled.indexOf(text.nextSibling) === -1 &&
- !dom.isRightEdgeOf(text, styled)
- ) {
- options.nextText = false;
- var point = self.context.invoke('HelperPlugin.makePoint', text, dom.nodeLength(text));
- if (dom.isMedia(text)) {
- point = dom.nextPoint(point);
- }
- var next = self.context.invoke('HelperPlugin.splitTree', styled, point, options);
- nodeAlreadyStyled.push(next);
- }
- if (
- nodeAlreadyStyled.indexOf(text.previousSibling) === -1 &&
- !dom.isLeftEdgeOf(text, styled)
- ) {
- options.nextText = true;
- var textPoint = self.context.invoke('HelperPlugin.makePoint', text, 0);
- text = self.context.invoke('HelperPlugin.splitTree', styled, textPoint, options);
- nodeAlreadyStyled.push(text);
- if (index === 0) {
- start = text;
- }
- end = text;
- }
-
- var toRemove = dom.ancestor(text, function (n) {
- return n.tagName === tag;
- });
- if (toRemove) {
- // Remove generated empty elements
- if (
- toRemove.nextSibling &&
- self.context.invoke('HelperPlugin.isBlankNode', toRemove.nextSibling)
- ) {
- $(toRemove.nextSibling).remove();
- }
- if (
- toRemove.previousSibling &&
- self.context.invoke('HelperPlugin.isBlankNode', toRemove.previousSibling)
- ) {
- $(toRemove.previousSibling).remove();
- }
-
- // Unwrap the element
- nodes = $(toRemove).contents();
- $(toRemove).before(nodes).remove();
- nodeAlreadyStyled.push.apply(nodeAlreadyStyled, nodes);
- end = _.last(nodeAlreadyStyled);
- }
- }
- }
-
- if (dom.ancestor(text, function (node) {
- return toStyled.indexOf(node) !== -1;
- })) {
- return;
- }
-
- var node = text;
- while (
- node && node.parentNode &&
- formatted.indexOf(node) === -1 &&
- formatted.indexOf(node.parentNode) === -1
- ) {
- node = node.parentNode;
- }
- if (node !== text) {
- if (containsAllSelectedText(node)) {
- toStyled.push.apply(toStyled, texts);
- node = text;
- } else if (!containsOnlySelectedText(node)) {
-
- node = text;
- }
- }
-
- if (toStyled.indexOf(node) === -1) {
- toStyled.push(node);
- }
- return !styled;
- });
-
- toStyled = _.uniq(toStyled);
-
- if (notStyled.length) {
- nodes = [];
- var toMerge = [];
- _.each(toStyled, function (node) {
- var next = true;
- if (node.nextSibling && node.nextSibling.tagName === tag) {
- $(node.nextSibling).prepend(node);
- next = false;
- }
- if (node.previousSibling && node.previousSibling.tagName === tag) {
- $(node.previousSibling).append(node);
- }
- if (node.parentNode && node.parentNode.tagName !== tag) {
- var styled = self.document.createElement(tag);
- if (node.tagName) {
- $(styled).append(node.childNodes);
- $(node).append(styled);
- } else {
- $(node).before(styled);
- styled.appendChild(node);
- }
- }
- // Add adjacent nodes with same tagName to list of nodes to merge
- if (
- node.parentNode && node.parentNode[next ? 'nextSibling' : 'previousSibling'] &&
- node.parentNode.tagName === node.parentNode[next ? 'nextSibling' : 'previousSibling'].tagName
- ) {
- toMerge.push(next ? node.parentNode : node.parentNode.previousSibling);
- }
- });
- // Merge what needs merging
- while (toMerge.length) {
- this.context.invoke('HelperPlugin.deleteEdge', toMerge.pop(), 'next');
- }
- }
-
- range = this.context.invoke('editor.setRange', start, 0, end, dom.nodeLength(end));
-
- if (range.sc === range.ec && range.sc.textContent === '\u200B') {
- range.so = range.eo = 1;
- }
-
- range.select();
- this.editable.normalize();
- },
- /**
- * Remove format on the current range.
- *
- * @see _isParentRemoveFormatCandidate
- */
- removeFormat: function () {
- this._selectCurrentIfCollapsed();
- var selectedText = this.context.invoke('HelperPlugin.getSelectedText');
- var selectedIcons = this.context.invoke('HelperPlugin.getSelectedNodes');
- selectedIcons = _.filter(selectedIcons, dom.isIcon);
- if (!selectedText.length && !selectedIcons.length) {
- return;
- }
- _.each(selectedIcons, this._removeIconFormat.bind(this));
- _.each(selectedText, this._removeTextFormat.bind(this));
- var startNode = selectedText[0];
- var endNode = selectedText[selectedText.length - 1];
- this.context.invoke('editor.setRange', startNode, 0, endNode, dom.nodeLength(endNode)).select();
- this.editable.normalize();
- this.context.invoke('editor.saveRange');
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Remove the non-whitelisted or blacklisted
- * top level elements from clipboard data.
- *
- * @see clipboardWhitelist
- * @see clipboardBlacklist
- *
- * @private
- * @param {JQuery} $clipboardData
- * @returns {Object} {$clipboardData: JQuery, didRemoveNodes: Boolean}
- */
- _cleanClipboardRoot: function ($clipboardData) {
- var didRemoveNodes = false;
- var whiteList = this._clipboardWhitelist();
- var blackList = this._clipboardBlacklist();
- var $fakeParent = $(this.document.createElement('div'));
- _.each($clipboardData, function (node) {
- var isWhitelisted = dom.isText(node) || $(node).filter(whiteList.join(',')).length;
- var isBlacklisted = $(node).filter(blackList.join(',')).length;
- if (!isWhitelisted || isBlacklisted) {
- $fakeParent.append(node.childNodes);
- didRemoveNodes = true;
- } else {
- $fakeParent.append(node);
- }
- });
- return {
- $clipboardData: $fakeParent.contents(),
- didRemoveNodes: didRemoveNodes,
- };
- },
- /**
- * Return a list of jQuery selectors for prohibited nodes on paste.
- *
- * @private
- * @returns {String[]}
- */
- _clipboardBlacklist: function () {
- return ['.Apple-interchange-newline'];
- },
- /**
- * Return a list of jQuery selectors for exclusively authorized nodes on paste.
- *
- * @private
- * @returns {String[]}
- */
- _clipboardWhitelist: function () {
- var listSels = ['ul', 'ol', 'li'];
- var styleSels = ['i', 'b', 'u', 'em', 'strong'];
- var tableSels = ['table', 'th', 'tbody', 'tr', 'td'];
- var miscSels = ['img', 'br', 'a', '.fa'];
- return this.options.styleTags.concat(listSels, styleSels, tableSels, miscSels);
- },
- /**
- * Return a list of attribute names that are exclusively authorized on paste.
- *
- * @private
- * @returns {String[]}
- */
- _clipboardWhitelistAttr: function () {
- return ['class', 'href', 'src'];
- },
- /**
- * Fill up empty block elements with BR elements so the carret can enter them.
- *
- * @private
- * @param {JQuery} $els
- */
- _fillEmptyBlocks: function ($els) {
- var self = this;
-
- $els.filter(function (i, n) {
- return self.context.invoke('HelperPlugin.isNodeBlockType', n) && !n.childNodes;
- }).append(this.document.createElement('br'));
- },
- /**
- * Get all non-whitelisted or blacklisted elements from clipboard data.
- *
- * @private
- * @param {JQuery} $clipboardData
- * @returns {JQuery}
- */
- _filterIllegalClipboardElements: function ($clipboardData) {
- return $clipboardData.find('*').addBack()
- .not(this._clipboardWhitelist().join(','))
- .addBack(this._clipboardBlacklist().join(','))
- .filter(function () {
- return !dom.isText(this);
- });
- },
- /**
- * Get a legal point to paste at, from the current range's start point.
- *
- * @private
- * @returns {Object} {node: Node, offset: Number}
- */
- _getPastePoint: function () {
- var point = this.context.invoke('editor.createRange').getStartPoint();
- var offsetChild = point.node.childNodes[point.offset];
- point = offsetChild ? this.context.invoke('HelperPlugin.makePoint', offsetChild, 0) : point;
- return dom.nextPointUntil(point, this._isPastePointLegal.bind(this));
- },
- /**
- * Insert nodes at a point. Insert them inline if the first node is inline
- * and pasting inline is legal at that point.
- *
- * @private
- * @param {Node[]} nodes
- * @param {Object} point {node: Node, offset: Number}
- */
- _insertNodesAt: function (nodes, point) {
- var canInsertInline = dom.isText(point.node) || point.node.tagName === 'BR' || dom.isMedia(point.node);
- var $fakeParent = $(this.document.createElement('div'));
- $fakeParent.append(nodes);
- if (dom.isInline(nodes[0]) && canInsertInline) {
- point.node = point.node.tagName ? point.node : point.node.splitText(point.offset);
- $(point.node).before($fakeParent.contents());
- } else {
- this.context.invoke('HelperPlugin.insertBlockNode', $fakeParent[0]);
- }
- $fakeParent.contents().unwrap();
- },
- /**
- * Return true if the parent of the given node is a removeFormat candidate:
- * - It is a removeFormat candidate as defined by W3C
- * - It is contained within the editable area
- * - It is not unbreakable
- *
- * @see formatTags the list of removeFormat candidates as defined by W3C
- *
- * @private
- * @param {Node} node
- */
- _isParentRemoveFormatCandidate: function (node) {
- var parent = node.parentNode;
- if (!parent) {
- return false;
- }
- var isEditableOrAbove = parent && (parent === this.editable || $.contains(parent, this.editable));
- var isUnbreakable = parent && this.options.isUnbreakableNode(parent);
- var isRemoveFormatCandidate = parent && parent.tagName && this.formatTags.indexOf(parent.tagName.toLowerCase()) !== -1;
- return parent && !isEditableOrAbove && !isUnbreakable && isRemoveFormatCandidate;
- },
- /**
- * Return true if it's legal to paste nodes at the given point:
- * if the point is not within a void node and the point is not unbreakable.
- *
- * @private
- * @param {Object} point {node: Node, offset: Number}
- * @returns {Boolean}
- */
- _isPastePointLegal: function (point) {
- var node = point.node;
- var isWithinVoid = false;
- if (node.parentNode) {
- isWithinVoid = dom.isVoid(node.parentNode) || $(node.parentNode).filter('.fa').length;
- }
- return !isWithinVoid && !this.options.isUnbreakableNode(point.node);
- },
- /**
- * @private
- * @param {Object} point {node: Node, offset: Number}
- * @returns {Boolean}
- */
- _isPointInAnchor: function (point) {
- var ancestor = dom.ancestor(point.node, dom.isAnchor);
- return !ancestor || ancestor === this.editable;
- },
- /**
- * Check a list of nodes and merges all adjacent ULs together:
- * [ul, ul, p, ul, ul] will return [ul, p, ul], with the li's of
- * nodes[1] and nodes[4] appended to nodes[0] and nodes[3].
- *
- * @private
- * @param {Node[]} nodes
- * @return {Node[]} the remaining, merged nodes
- */
- _mergeAdjacentULs: function (nodes) {
- var res = [];
- var prevNode;
- _.each(nodes, function (node) {
- prevNode = res[res.length - 1];
- if (prevNode && node.tagName === 'UL' && prevNode.tagName === 'UL') {
- $(prevNode).append(node.childNodes);
- } else {
- res.push(node);
- }
- });
- return res;
- },
- /**
- * Remove an icon's format (colors, font size).
- *
- * @private
- * @param {Node} icon
- */
- _removeIconFormat: function (icon) {
- $(icon).css({
- color: '',
- backgroundColor: '',
- fontSize: '',
- });
- var reColorClasses = /(^|\s+)(bg|text)-\S*|/g;
- icon.className = icon.className.replace(reColorClasses, '').trim();
- },
- /**
- * Remove non-whitelisted attributes from clipboard.
- *
- * @private
- * @param {JQuery} $els
- */
- _removeIllegalClipboardAttributes: function ($els) {
- var self = this;
- $els.each(function () {
- var $node = $(this);
- _.each(_.pluck(this.attributes, 'name'), function (attribute) {
- if (self._clipboardWhitelistAttr().indexOf(attribute) === -1) {
- $node.removeAttr(attribute);
- }
- });
- }).removeClass('o_editable o_not_editable');
- },
- /**
- * Remove non-whitelisted and blacklisted elements from clipboard data.
- *
- * @private
- * @param {JQuery} $clipboardData
- * @returns {JQuery}
- */
- _removeIllegalClipboardElements: function ($clipboardData) {
- var root = true;
- $clipboardData = $clipboardData.not('meta').not('style').not('script');
- var $badNodes = this._filterIllegalClipboardElements($clipboardData);
-
- do {
- if (root) {
- root = false;
- var cleanData = this._cleanClipboardRoot($clipboardData);
- $clipboardData = cleanData.$clipboardData;
- root = cleanData.didRemoveNodes;
- } else {
- this._removeNodesPreserveContents($badNodes);
- }
-
- $badNodes = this._filterIllegalClipboardElements($clipboardData);
- } while ($badNodes.length);
- return $clipboardData;
- },
- /**
- * Remove nodes from the DOM while preserving their contents if any.
- *
- * @private
- * @param {JQuery} $nodes
- */
- _removeNodesPreserveContents: function ($nodes) {
- var $contents = $nodes.contents();
- if ($contents.length) {
- $contents.unwrap();
- } else {
- $nodes.remove();
- }
- },
- /**
- * Remove a text node's format: remove its style parents (b, i, u, ...).
- *
- * @private
- * @param {Node} textNode
- */
- _removeTextFormat: function (textNode) {
- var node = textNode;
- while (this._isParentRemoveFormatCandidate(node)) {
- this.context.invoke('HelperPlugin.splitAtNodeEnds', node);
- $(node.parentNode).before(node).remove();
- }
- },
- /**
- * Select the whole current node if the range is collapsed
- *
- * @private
- */
- _selectCurrentIfCollapsed: function () {
- var range = this.context.invoke('editor.createRange');
- if (!range.isCollapsed()) {
- return;
- }
- this.context.invoke('editor.setRange', range.sc, 0, range.sc, dom.nodeLength(range.sc)).select();
- this.context.invoke('editor.saveRange');
- },
- /**
- * Prevent inline nodes directly in TDs by wrapping them in P elements.
- *
- * @private
- * @param {JQuery} $tds
- */
- _wrapTDContents: function ($tds) {
- var self = this;
- var $inlinesInTD = $tds.contents().filter(function () {
- return !self.context.invoke('HelperPlugin.isNodeBlockType', this);
- });
- var parentsOfInlinesInTD = [];
- _.each($inlinesInTD, function (n) {
- parentsOfInlinesInTD.push(self.context.invoke('HelperPlugin.firstBlockAncestor', n));
- });
- $($.unique(parentsOfInlinesInTD)).wrapInner(this.document.createElement('p'));
- },
-
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
-
- /**
- * Handle paste events to permit cleaning/sorting of the data before pasting.
- *
- * @private
- * @param {SummernoteEvent} se
- * @param {jQueryEvent} e
- */
- _onPaste: function (se, e) {
- var isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
- var iPhone = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
- if (isSafari && !iPhone) {
- return;
- }
- se.preventDefault();
- se.stopImmediatePropagation();
- e.preventDefault();
- e.stopImmediatePropagation();
-
- this.context.invoke('editor.beforeCommand');
-
- // Clean up
- var clipboardData = e.originalEvent.clipboardData.getData('text/html');
- if (clipboardData) {
- clipboardData = this.prepareClipboardData(clipboardData);
- } else {
- clipboardData = e.originalEvent.clipboardData.getData('text/plain');
- // get that text as an array of text nodes separated by <br> where needed
- var allNewlines = /\n/g;
- clipboardData = _.str.trim(clipboardData)
- .replace(/</g, '&lt;').replace(/>/g, '&gt;')
- .replace(allNewlines, '<br>');
- clipboardData = $('<p>' + clipboardData + '</p>').contents().toArray();
- }
-
- // Delete selection
- this.context.invoke('HelperPlugin.deleteSelection');
-
- // Insert the nodes
- this.pasteNodes(clipboardData);
- this.context.invoke('HelperPlugin.normalize');
- this.context.invoke('editor.saveRange');
-
- this.context.invoke('editor.afterCommand');
- },
-});
-
-registry.add('TextPlugin', TextPlugin);
-
-return TextPlugin;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/toolbar.js b/addons/web_editor/static/src/js/wysiwyg/plugin/toolbar.js
deleted file mode 100644
index bc7a9f7abbf..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/toolbar.js
+++ /dev/null
@@ -1,57 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.toolbar', function (require) {
-'use strict';
-
-var Plugins = require('web_editor.wysiwyg.plugins');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-
-var dom = $.summernote.dom;
-
-
-var ToolbarPlugin = Plugins.toolbar.extend({
- events: {
- 'summernote.mouseup': 'update',
- 'summernote.keyup': 'update',
- 'summernote.change': 'update',
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- initialize: function () {
- this._super();
- this.update();
- },
- /**
- * Update the toolbar (enabled/disabled).
- */
- update: function () {
- var $btn = this.$toolbar.children().not('.note-history, .note-view').find('button');
- var target = this.context.invoke('editor.restoreTarget') || this.context.invoke('editor.createRange').sc;
-
- if (!target || !$.contains(this.editable, target) || !this.options.isEditableNode(target)) {
- $btn.addClass('o_disabled');
- return;
- }
-
- $btn.removeClass('o_disabled');
-
- if (!target || !this.options.displayPopover(target)) {
- $btn.addClass('o_disabled');
- }
-
- if (dom.ancestor(target, dom.isMedia)) {
- this.$toolbar.children('.note-style, .note-font, .note-para, .note-table').addClass('o_disabled');
-
- $btn.find('.fa-file-image-o').parent().removeClass('o_disabled');
- if (dom.ancestor(target, dom.isFont)) {
- this.$toolbar.children('.note-color').removeClass('o_disabled');
-
- }
- }
- },
-});
-
-registry.add('toolbar', ToolbarPlugin);
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/transform.js b/addons/web_editor/static/src/js/wysiwyg/plugin/transform.js
deleted file mode 100644
index f7fa5b860f2..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/transform.js
+++ /dev/null
@@ -1,85 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.transform', function (require) {
-'use strict';
-
-var core = require('web.core');
-var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-var wysiwygTranslation = require('web_editor.wysiwyg.translation');
-var wysiwygOptions = require('web_editor.wysiwyg.options');
-
-var _t = core._t;
-
-
-var TransformPlugin = AbstractPlugin.extend({
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Manages transformations on a media.
- */
- transform: function () {
- var $image = $(this.context.invoke('editor.restoreTarget'));
-
- if ($image.data('transfo-destroy')) {
- $image.removeData('transfo-destroy');
- return;
- }
-
- $image.transfo(); // see web_editor/static/lib/jQuery.transfo.js
-
- var mouseup = function () {
- $('.note-popover button[data-event="transform"]').toggleClass('active', $image.is('[style*="transform"]'));
- };
- $(document).on('mouseup', mouseup);
-
- var mousedown = this._wrapCommand(function (event) {
- if (!$(event.target).closest('.transfo-container').length) {
- $image.transfo('destroy');
- $(document).off('mousedown', mousedown).off('mouseup', mouseup);
- }
- if ($(event.target).closest('.note-popover').length) {
- var transformStyles = self.context.invoke('HelperPlugin.getRegex', '', 'g', '[^;]*transform[\\w:]*;?');
- $image.data('transfo-destroy', true).attr('style', ($image.attr('style') || '').replace(transformStyles, ''));
- }
- });
- $(document).on('mousedown', mousedown);
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Adds the transform buttons.
- *
- * @override
- */
- _addButtons: function () {
- var self = this;
- this._super();
-
- this.context.memo('button.transform', function () {
- return self.context.invoke('buttons.button', {
- contents: self.ui.icon(self.options.icons.transform),
- tooltip: self.lang.image.transform,
- click: self.context.createInvokeHandler('TransformPlugin.transform'),
- }).render();
- });
- },
-
-});
-
-
-_.extend(wysiwygOptions.icons, {
- transform: 'fa fa-object-ungroup',
-});
-_.extend(wysiwygTranslation.image, {
- transform: _t('Transform the picture (click twice to reset transformation)'),
-});
-
-registry.add('TransformPlugin', TransformPlugin);
-
-return TransformPlugin;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/unbreakable.js b/addons/web_editor/static/src/js/wysiwyg/plugin/unbreakable.js
deleted file mode 100644
index 388a3b2b2c9..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/unbreakable.js
+++ /dev/null
@@ -1,364 +0,0 @@
-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
- *
- <unbreakable id="a">
- content_1
- <unbreakable id="b">content_2</unbreakable>
- <allow id="c">
- content_3
- <unbreakable id="d">content_4</unbreakable>
- <unbreakable id="e">
- content_5
- <allow id="f">content_6</allow>
- content_7
- </unbreakable>
- content_8
- </allow>
- <unbreakable id="f">content_9</unbreakable>
- <allow id="g">
- content_10
- <unbreakable id="h">content_11</unbreakable>
- content_12
- </allow>
- </unbreakable>
-
- * .. 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('<div class="o_fake_editable o_wysiwyg_to_remove" style="position: absolute;" contentEditable="true"/>');
- $(this).append('<div class="o_fake_editable o_wysiwyg_to_remove" style="position: absolute;" contentEditable="true"/>');
- }
- });
- },
-
- //--------------------------------------------------------------------------
- // 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;
-
-});