summaryrefslogtreecommitdiff
path: root/addons/web_editor/static/src/js/editor
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web_editor/static/src/js/editor')
-rw-r--r--addons/web_editor/static/src/js/editor/editor.js286
-rw-r--r--addons/web_editor/static/src/js/editor/rte.js749
-rw-r--r--addons/web_editor/static/src/js/editor/rte.summernote.js1274
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.editor.js1408
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.options.js1317
-rw-r--r--addons/web_editor/static/src/js/editor/summernote.js2436
-rw-r--r--addons/web_editor/static/src/js/editor/transcoder.js395
-rw-r--r--addons/web_editor/static/src/js/editor/translator.js375
8 files changed, 0 insertions, 8240 deletions
diff --git a/addons/web_editor/static/src/js/editor/editor.js b/addons/web_editor/static/src/js/editor/editor.js
deleted file mode 100644
index 63fa8c1c667..00000000000
--- a/addons/web_editor/static/src/js/editor/editor.js
+++ /dev/null
@@ -1,286 +0,0 @@
-odoo.define('web_editor.editor', function (require) {
-'use strict';
-
-var Dialog = require('web.Dialog');
-var Widget = require('web.Widget');
-var core = require('web.core');
-var rte = require('web_editor.rte');
-var snippetsEditor = require('web_editor.snippet.editor');
-
-var _t = core._t;
-
-var EditorMenuBar = Widget.extend({
- template: 'web_editor.editorbar',
- xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
- events: {
- 'click button[data-action=save]': '_onSaveClick',
- 'click button[data-action=cancel]': '_onCancelClick',
- },
- custom_events: {
- request_history_undo_record: '_onHistoryUndoRecordRequest',
- request_save: '_onSaveRequest',
- },
-
- /**
- * Initializes RTE and snippets menu.
- *
- * @constructor
- */
- init: function (parent) {
- var self = this;
- var res = this._super.apply(this, arguments);
- this.rte = new rte.Class(this);
- this.rte.on('rte:start', this, function () {
- self.trigger('rte:start');
- });
-
- // Snippets edition
- var $editable = this.rte.editable();
- window.__EditorMenuBar_$editable = $editable; // TODO remove this hack asap
- this.snippetsMenu = new snippetsEditor.Class(this, $editable);
-
- return res;
- },
- /**
- * @override
- */
- start: function () {
- var self = this;
- var defs = [this._super.apply(this, arguments)];
-
- core.bus.on('editor_save_request', this, this.save);
- core.bus.on('editor_discard_request', this, this.cancel);
-
- $('.dropdown-toggle').dropdown();
-
- $(document).on('keyup', function (event) {
- if ((event.keyCode === 8 || event.keyCode === 46)) {
- var $target = $(event.target).closest('.o_editable');
- if (!$target.is(':has(*:not(p):not(br))') && !$target.text().match(/\S/)) {
- $target.empty();
- }
- }
- });
- $(document).on('click', '.note-editable', function (ev) {
- ev.preventDefault();
- });
- $(document).on('submit', '.note-editable form .btn', function (ev) {
- ev.preventDefault(); // Disable form submition in editable mode
- });
- $(document).on('hide.bs.dropdown', '.dropdown', function (ev) {
- // Prevent dropdown closing when a contenteditable children is focused
- if (ev.originalEvent
- && $(ev.target).has(ev.originalEvent.target).length
- && $(ev.originalEvent.target).is('[contenteditable]')) {
- ev.preventDefault();
- }
- });
-
- this.rte.start();
-
- var flag = false;
- window.onbeforeunload = function (event) {
- if (rte.history.getEditableHasUndo().length && !flag) {
- flag = true;
- _.defer(function () { flag=false; });
- return _t('This document is not saved!');
- }
- };
-
- // Snippets menu
- defs.push(this.snippetsMenu.insertAfter(this.$el));
- this.rte.editable().find('*').off('mousedown mouseup click');
-
- return $.when.apply($, defs).then(function () {
- self.trigger_up('edit_mode');
- });
- },
- /**
- * @override
- */
- destroy: function () {
- this._super.apply(this, arguments);
- core.bus.off('editor_save_request', this, this._onSaveRequest);
- core.bus.off('editor_discard_request', this, this._onDiscardRequest);
- },
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Asks the user if he really wants to discard its changes (if there are
- * some of them), then simply reload the page if he wants to.
- *
- * @param {boolean} [reload=true]
- * true if the page has to be reloaded when the user answers yes
- * (do nothing otherwise but add this to allow class extension)
- * @returns {Deferred}
- */
- cancel: function (reload) {
- var self = this;
- var def = $.Deferred();
- if (!rte.history.getEditableHasUndo().length) {
- def.resolve();
- } else {
- var confirm = Dialog.confirm(this, _t("If you discard the current edition, all unsaved changes will be lost. You can cancel to return to the edition mode."), {
- confirm_callback: def.resolve.bind(def),
- });
- confirm.on('closed', def, def.reject);
- }
- return def.then(function () {
- if (reload !== false) {
- window.onbeforeunload = null;
- return self._reload();
- }
- });
- },
- /**
- * Asks the snippets to clean themself, then saves the page, then reloads it
- * if asked to.
- *
- * @param {boolean} [reload=true]
- * true if the page has to be reloaded after the save
- * @returns {Deferred}
- */
- save: function (reload) {
- var self = this;
- var defs = [];
- this.trigger_up('ready_to_save', {defs: defs});
- return $.when.apply($, defs).then(function () {
- self.snippetsMenu.cleanForSave();
- return self._saveCroppedImages();
- }).then(function () {
- return self.rte.save();
- }).then(function () {
- if (reload !== false) {
- return self._reload();
- }
- });
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Reloads the page in non-editable mode, with the right scrolling.
- *
- * @private
- * @returns {Deferred} (never resolved, the page is reloading anyway)
- */
- _reload: function () {
- window.location.hash = 'scrollTop=' + window.document.body.scrollTop;
- if (window.location.search.indexOf('enable_editor') >= 0) {
- window.location.href = window.location.href.replace(/&?enable_editor(=[^&]*)?/g, '');
- } else {
- window.location.reload(true);
- }
- return $.Deferred();
- },
- /**
- * @private
- */
- _saveCroppedImages: function () {
- var self = this;
- var defs = _.map(this.rte.editable().find('.o_cropped_img_to_save'), function (croppedImg) {
- var $croppedImg = $(croppedImg);
- $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_fname: 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}],
- });
- }
- });
- return $.when.apply($, defs);
- },
-
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
-
- /**
- * Called when the "Discard" button is clicked -> discards the changes.
- *
- * @private
- */
- _onCancelClick: function () {
- this.cancel();
- },
- /**
- * Called when an element askes to record an history undo -> records it.
- *
- * @private
- * @param {OdooEvent} ev
- */
- _onHistoryUndoRecordRequest: function (ev) {
- this.rte.historyRecordUndo(ev.data.$target, ev.data.event);
- },
- /**
- * Called when the "Save" button is clicked -> saves the changes.
- *
- * @private
- */
- _onSaveClick: function () {
- this.save();
- },
- /**
- * Called when a discard request is received -> discard the page content
- * changes.
- *
- * @private
- * @param {OdooEvent} ev
- */
- _onDiscardRequest: function (ev) {
- this.cancel(ev.data.reload).then(ev.data.onSuccess, ev.data.onFailure);
- },
- /**
- * Called when a save request is received -> saves the page content.
- *
- * @private
- * @param {OdooEvent} ev
- */
- _onSaveRequest: function (ev) {
- ev.stopPropagation();
- this.save(ev.data.reload).then(ev.data.onSuccess, ev.data.onFailure);
- },
-});
-
-return {
- Class: EditorMenuBar,
-};
-});
diff --git a/addons/web_editor/static/src/js/editor/rte.js b/addons/web_editor/static/src/js/editor/rte.js
deleted file mode 100644
index ba6e1fed7ec..00000000000
--- a/addons/web_editor/static/src/js/editor/rte.js
+++ /dev/null
@@ -1,749 +0,0 @@
-odoo.define('web_editor.rte', function (require) {
-'use strict';
-
-var base = require('web_editor.base');
-var concurrency = require('web.concurrency');
-var core = require('web.core');
-var Widget = require('web.Widget');
-var weContext = require('web_editor.context');
-var summernote = require('web_editor.summernote');
-var summernoteCustomColors = require('web_editor.rte.summernote_custom_colors');
-
-var _t = core._t;
-
-// Summernote Lib (neek change to make accessible: method and object)
-var dom = summernote.core.dom;
-var range = summernote.core.range;
-
-// Change History to have a global History for all summernote instances
-var History = function History($editable) {
- var aUndo = [];
- var pos = 0;
- var toSnap;
-
- this.makeSnap = function (event, rng) {
- rng = rng || range.create();
- var elEditable = $(rng && rng.sc).closest('.o_editable')[0];
- if (!elEditable) {
- return false;
- }
- return {
- event: event,
- editable: elEditable,
- contents: elEditable.innerHTML,
- bookmark: rng && rng.bookmark(elEditable),
- scrollTop: $(elEditable).scrollTop()
- };
- };
-
- this.applySnap = function (oSnap) {
- var $editable = $(oSnap.editable);
-
- if (document.documentMode) {
- $editable.removeAttr('contentEditable').removeProp('contentEditable');
- }
-
- $editable.trigger('content_will_be_destroyed');
- $editable.html(oSnap.contents).scrollTop(oSnap.scrollTop);
- $editable.trigger('content_was_recreated');
-
- $('.oe_overlay').remove();
- $('.note-control-selection').hide();
-
- $editable.trigger('content_changed');
-
- try {
- var r = oSnap.editable.innerHTML === '' ? range.create(oSnap.editable, 0) : range.createFromBookmark(oSnap.editable, oSnap.bookmark);
- r.select();
- } catch (e) {
- console.error(e);
- return;
- }
-
- $(document).trigger('click');
- $('.o_editable *').filter(function () {
- var $el = $(this);
- if ($el.data('snippet-editor')) {
- $el.removeData();
- }
- });
-
-
- _.defer(function () {
- var target = dom.isBR(r.sc) ? r.sc.parentNode : dom.node(r.sc);
- if (!target) {
- return;
- }
-
- $editable.trigger('applySnap');
-
- var evt = document.createEvent('MouseEvents');
- evt.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, target);
- target.dispatchEvent(evt);
-
- $editable.trigger('keyup');
- });
- };
-
- this.undo = function () {
- if (!pos) { return; }
- var _toSnap = toSnap;
- if (_toSnap) {
- this.saveSnap();
- }
- if (!aUndo[pos] && (!aUndo[pos] || aUndo[pos].event !== 'undo')) {
- var temp = this.makeSnap('undo');
- if (temp && (!pos || temp.contents !== aUndo[pos-1].contents)) {
- aUndo[pos] = temp;
- } else {
- pos--;
- }
- } else if (_toSnap) {
- pos--;
- }
- this.applySnap(aUndo[Math.max(--pos,0)]);
- while (pos && (aUndo[pos].event === 'blur' || (aUndo[pos+1].editable === aUndo[pos].editable && aUndo[pos+1].contents === aUndo[pos].contents))) {
- this.applySnap(aUndo[--pos]);
- }
- };
-
- this.hasUndo = function () {
- return (toSnap && (toSnap.event !== 'blur' && toSnap.event !== 'activate' && toSnap.event !== 'undo')) ||
- !!_.find(aUndo.slice(0, pos+1), function (undo) {
- return undo.event !== 'blur' && undo.event !== 'activate' && undo.event !== 'undo';
- });
- };
-
- this.getEditableHasUndo = function () {
- var editable = [];
- if ((toSnap && (toSnap.event !== 'blur' && toSnap.event !== 'activate' && toSnap.event !== 'undo'))) {
- editable.push(toSnap.editable);
- }
- _.each(aUndo.slice(0, pos+1), function (undo) {
- if (undo.event !== 'blur' && undo.event !== 'activate' && undo.event !== 'undo') {
- editable.push(undo.editable);
- }
- });
- return _.uniq(editable);
- };
-
- this.redo = function () {
- if (!aUndo[pos+1]) { return; }
- this.applySnap(aUndo[++pos]);
- while (aUndo[pos+1] && aUndo[pos].event === 'active') {
- this.applySnap(aUndo[pos++]);
- }
- };
-
- this.hasRedo = function () {
- return aUndo.length > pos+1;
- };
-
- this.recordUndo = function ($editable, event, internal_history) {
- var self = this;
- if (!$editable) {
- var rng = range.create();
- if (!rng) return;
- $editable = $(rng.sc).closest('.o_editable');
- }
-
- if (aUndo[pos] && (event === 'applySnap' || event === 'activate')) {
- return;
- }
-
- if (!internal_history) {
- if (!event || !toSnap || !aUndo[pos-1] || toSnap.event === 'activate') { // don't trigger change for all keypress
- setTimeout(function () {
- $editable.trigger('content_changed');
- },0);
- }
- }
-
- if (aUndo[pos]) {
- pos = Math.min(pos, aUndo.length);
- aUndo.splice(pos, aUndo.length);
- }
-
- // => make a snap when the user change editable zone (because: don't make snap for each keydown)
- if (toSnap && (toSnap.split || !event || toSnap.event !== event || toSnap.editable !== $editable[0])) {
- this.saveSnap();
- }
-
- if (pos && aUndo[pos-1].editable !== $editable[0]) {
- var snap = this.makeSnap('blur', range.create(aUndo[pos-1].editable, 0));
- pos++;
- aUndo.push(snap);
- }
-
- if (range.create()) {
- toSnap = self.makeSnap(event);
- } else {
- toSnap = false;
- }
- };
-
- this.splitNext = function () {
- if (toSnap) {
- toSnap.split = true;
- }
- };
-
- this.saveSnap = function () {
- if (toSnap) {
- if (!aUndo[pos]) {
- pos++;
- }
- aUndo.push(toSnap);
- delete toSnap.split;
- toSnap = null;
- }
- };
-};
-var history = new History();
-
-// jQuery extensions
-$.extend($.expr[':'], {
- o_editable: function (node, i, m) {
- while (node) {
- if (node.className && _.isString(node.className)) {
- if (node.className.indexOf('o_not_editable')!==-1 ) {
- return false;
- }
- if (node.className.indexOf('o_editable')!==-1 ) {
- return true;
- }
- }
- node = node.parentNode;
- }
- return false;
- },
-});
-$.fn.extend({
- focusIn: function () {
- if (this.length) {
- range.create(dom.firstChild(this[0]), 0).select();
- }
- return this;
- },
- focusInEnd: function () {
- if (this.length) {
- var last = dom.lastChild(this[0]);
- range.create(last, dom.nodeLength(last)).select();
- }
- return this;
- },
- selectContent: function () {
- if (this.length) {
- var next = dom.lastChild(this[0]);
- range.create(dom.firstChild(this[0]), 0, next, next.textContent.length).select();
- }
- return this;
- },
-});
-
-// RTE
-var RTEWidget = Widget.extend({
- /**
- * @constructor
- */
- init: function (parent, getConfig) {
- var self = this;
- this._super.apply(this, arguments);
-
- this.init_bootstrap_carousel = $.fn.carousel;
- this.edit_bootstrap_carousel = function () {
- var res = self.init_bootstrap_carousel.apply(this, arguments);
- // off bootstrap keydown event to remove event.preventDefault()
- // and allow to change cursor position
- $(this).off('keydown.bs.carousel');
- return res;
- };
-
- this._getConfig = getConfig || this._getDefaultConfig;
-
- base.computeFonts();
- },
- /**
- * @override
- */
- start: function () {
- var self = this;
-
- this.saving_mutex = new concurrency.Mutex();
-
- $.fn.carousel = this.edit_bootstrap_carousel;
-
- $(document).on('click.rte keyup.rte', function () {
- var current_range = {};
- try {
- current_range = range.create() || {};
- } catch (e) {
- // if range is on Restricted element ignore error
- }
- var $popover = $(current_range.sc).closest('[contenteditable]');
- var popover_history = ($popover.data()||{}).NoteHistory;
- if (!popover_history || popover_history === history) return;
- var editor = $popover.parent('.note-editor');
- $('button[data-event="undo"]', editor).attr('disabled', !popover_history.hasUndo());
- $('button[data-event="redo"]', editor).attr('disabled', !popover_history.hasRedo());
- });
- $(document).on('mousedown.rte activate.rte', this, this._onMousedown.bind(this));
- $(document).on('mouseup.rte', this, this._onMouseup.bind(this));
-
- $('.o_not_editable').attr('contentEditable', false);
-
- var $editable = this.editable();
-
- // When a undo/redo is performed, the whole DOM is changed so we have
- // to prepare for it (website will restart animations for example)
- // TODO should be better handled
- $editable.on('content_will_be_destroyed', function (ev) {
- self.trigger_up('content_will_be_destroyed', {
- $target: $(ev.currentTarget),
- });
- });
- $editable.on('content_was_recreated', function (ev) {
- self.trigger_up('content_was_recreated', {
- $target: $(ev.currentTarget),
- });
- });
-
- $editable.addClass('o_editable')
- .data('rte', this)
- .each(function () {
- var $node = $(this);
-
- // fallback for firefox iframe display:none see https://github.com/odoo/odoo/pull/22610
- var computedStyles = window.getComputedStyle(this) || window.parent.getComputedStyle(this);
- // add class to display inline-block for empty t-field
- if (computedStyles.display === 'inline' && $node.data('oe-type') !== 'image') {
- $node.addClass('o_is_inline_editable');
- }
- });
-
- // start element observation
- $(document).on('content_changed', '.o_editable', function (ev) {
- self.trigger_up('rte_change', {target: ev.target});
- if (!ev.__isDirtyHandled) {
- $(this).addClass('o_dirty');
- ev.__isDirtyHandled = true;
- }
- });
-
- $('#wrapwrap, .o_editable').on('click.rte', '*', this, this._onClick.bind(this));
-
- $('body').addClass('editor_enable');
-
- $(document.body)
- .tooltip({
- selector: '[data-oe-readonly]',
- container: 'body',
- trigger: 'hover',
- delay: { 'show': 1000, 'hide': 100 },
- placement: 'bottom',
- title: _t("Readonly field")
- })
- .on('click', function () {
- $(this).tooltip('hide');
- });
-
- $(document).trigger('mousedown');
- this.trigger('rte:start');
-
- return this._super.apply(this, arguments);
- },
- /**
- * @override
- */
- destroy: function () {
- this.cancel();
- this._super.apply(this, arguments);
- },
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Stops the RTE.
- */
- cancel: function () {
- if (this.$last) {
- this.$last.destroy();
- this.$last = null;
- }
-
- $.fn.carousel = this.init_bootstrap_carousel;
-
- $(document).off('.rte');
- $('#wrapwrap, .o_editable').off('.rte');
-
- $('.o_not_editable').removeAttr('contentEditable');
-
- $(document).off('click.rte keyup.rte mousedown.rte activate.rte mouseup.rte');
- $(document).off('content_changed').removeClass('o_is_inline_editable').removeData('rte');
- $(document).tooltip('dispose');
- $('body').removeClass('editor_enable');
- this.trigger('rte:stop');
- },
- /**
- * Returns the editable areas on the page.
- *
- * @returns {jQuery}
- */
- editable: function () {
- return $('#wrapwrap [data-oe-model]')
- .not('.o_not_editable')
- .filter(function () {
- return !$(this).closest('.o_not_editable').length;
- })
- .not('link, script')
- .not('[data-oe-readonly]')
- .not('img[data-oe-field="arch"], br[data-oe-field="arch"], input[data-oe-field="arch"]')
- .not('.oe_snippet_editor')
- .add('.o_editable');
- },
- /**
- * Records the current state of the given $target to be able to undo future
- * changes.
- *
- * @see History.recordUndo
- * @param {jQuery} $target
- * @param {string} event
- * @param {boolean} internal_history
- */
- historyRecordUndo: function ($target, event, internal_history) {
- $target = $($target);
- var rng = range.create();
- var $editable = $(rng && rng.sc).closest('.o_editable');
- if (!rng || !$editable.length) {
- $editable = $target.closest('.o_editable');
- rng = range.create($target.closest('*')[0],0);
- } else {
- rng = $editable.data('range') || rng;
- }
- try {
- // TODO this line might break for unknown reasons. I suppose that
- // the created range is an invalid one. As it might be tricky to
- // adapt that line and that it is not a critical one, temporary fix
- // is to ignore the errors that this generates.
- rng.select();
- } catch (e) {
- console.log('error', e);
- }
- history.recordUndo($editable, event, internal_history);
- },
- /**
- * Searches all the dirty element on the page and saves them one by one. If
- * one cannot be saved, this notifies it to the user and restarts rte
- * edition.
- *
- * @param {Object} [context] - the context to use for saving rpc, default to
- * the editor context found on the page
- * @return {Deferred} rejected if the save cannot be done
- */
- save: function (context) {
- var self = this;
-
- $('.o_editable')
- .destroy()
- .removeClass('o_editable o_is_inline_editable');
-
- var $dirty = $('.o_dirty');
- $dirty
- .removeAttr('contentEditable')
- .removeClass('o_dirty oe_carlos_danger o_is_inline_editable');
- var defs = _.map($dirty, function (el) {
- var $el = $(el);
-
- $el.find('[class]').filter(function () {
- if (!this.getAttribute('class').match(/\S/)) {
- this.removeAttribute('class');
- }
- });
-
- // TODO: Add a queue with concurrency limit in webclient
- // https://github.com/medikoo/deferred/blob/master/lib/ext/function/gate.js
- return self.saving_mutex.exec(function () {
- return self._saveElement($el, context || weContext.get())
- .then(function () {
- $el.removeClass('o_dirty');
- }, function (response) {
- // because ckeditor regenerates all the dom, we can't just
- // setup the popover here as everything will be destroyed by
- // the DOM regeneration. Add markings instead, and returns a
- // new rejection with all relevant info
- var id = _.uniqueId('carlos_danger_');
- $el.addClass('o_dirty oe_carlos_danger ' + id);
- var html = (response.data.exception_type === 'except_osv');
- if (html) {
- var msg = $('<div/>', {text: response.data.message}).html();
- var data = msg.substring(3, msg.length -2).split(/', u'/);
- response.data.message = '<b>' + data[0] + '</b>' + data[1];
- }
- $('.o_editable.' + id)
- .removeClass(id)
- .popover({
- html: html,
- trigger: 'hover',
- content: response.data.message,
- placement: 'auto top',
- })
- .popover('show');
- });
- });
- });
-
- return $.when.apply($, defs).then(function () {
- window.onbeforeunload = null;
- }, function (failed) {
- // If there were errors, re-enable edition
- self.cancel();
- self.start();
- });
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * When the users clicks on an editable element, this function allows to add
- * external behaviors.
- *
- * @private
- * @param {jQuery} $editable
- */
- _enableEditableArea: function ($editable) {
- if ($editable.data('oe-type') === "monetary") {
- $editable.attr('contenteditable', false);
- $editable.find('.oe_currency_value').attr('contenteditable', true);
- }
- if ($editable.is('[data-oe-model]') && !$editable.is('[data-oe-model="ir.ui.view"]') && !$editable.is('[data-oe-type="html"]')) {
- $editable.data('layoutInfo').popover().find('.btn-group:not(.note-history)').remove();
- }
- },
- /**
- * When an element enters edition, summernote is initialized on it. This
- * function returns the default configuration for the summernote instance.
- *
- * @see _getConfig
- * @private
- * @param {jQuery} $editable
- * @returns {Object}
- */
- _getDefaultConfig: function ($editable) {
- return {
- 'airMode' : true,
- 'focus': false,
- 'airPopover': [
- ['style', ['style']],
- ['font', ['bold', 'italic', 'underline', 'clear']],
- ['fontsize', ['fontsize']],
- ['color', ['color']],
- ['para', ['ul', 'ol', 'paragraph']],
- ['table', ['table']],
- ['insert', ['link', 'picture']],
- ['history', ['undo', 'redo']],
- ],
- 'styleWithSpan': false,
- 'inlinemedia' : ['p'],
- 'lang': 'odoo',
- 'onChange': function (html, $editable) {
- $editable.trigger('content_changed');
- },
- 'colors': summernoteCustomColors,
- };
- },
- /**
- * Gets jQuery cloned element with internal text nodes escaped for XML
- * storage.
- *
- * @private
- * @param {jQuery} $el
- * @return {jQuery}
- */
- _getEscapedElement: function ($el) {
- var escaped_el = $el.clone();
- var to_escape = escaped_el.find('*').addBack();
- to_escape = to_escape.not(to_escape.filter('object,iframe,script,style,[data-oe-model][data-oe-model!="ir.ui.view"]').find('*').addBack());
- to_escape.contents().each(function () {
- if (this.nodeType === 3) {
- this.nodeValue = $('<div />').text(this.nodeValue).html();
- }
- });
- return escaped_el;
- },
- /**
- * Saves one (dirty) element of the page.
- *
- * @private
- * @param {jQuery} $el - the element to save
- * @param {Object} context - the context to use for the saving rpc
- * @param {boolean} [withLang=false]
- * false if the lang must be omitted in the context (saving "master"
- * page element)
- */
- _saveElement: function ($el, context, withLang) {
- return this._rpc({
- model: 'ir.ui.view',
- method: 'save',
- args: [
- $el.data('oe-id'),
- this._getEscapedElement($el).prop('outerHTML'),
- $el.data('oe-xpath') || null,
- ],
- context: context,
- }, withLang ? undefined : {
- noContextKeys: 'lang',
- });
- },
-
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
-
- /**
- * Called when any editable element is clicked -> Prevents default browser
- * action for the element.
- *
- * @private
- * @param {Event} e
- */
- _onClick: function (e) {
- e.preventDefault();
- },
- /**
- * Called when the mouse is pressed on the document -> activate element
- * edition.
- *
- * @private
- * @param {Event} ev
- */
- _onMousedown: function (ev) {
- var $target = $(ev.target);
- var $editable = $target.closest('.o_editable');
-
- if (!$editable.length || $.summernote.core.dom.isContentEditableFalse($target)) {
- return;
- }
-
- // Removes strange _moz_abspos attribute when it appears. Cannot
- // find another solution which works in all cases. A grabber still
- // appears at the same time which I did not manage to remove.
- // TODO find a complete and better solution
- _.defer(function () {
- $editable.find('[_moz_abspos]').removeAttr('_moz_abspos');
- });
-
- if ($target.is('a')) {
- /**
- * Remove content editable everywhere and add it on the link only so that characters can be added
- * and removed at the start and at the end of it.
- */
- $target.attr('contenteditable', true);
- _.defer(function () {
- $editable.not($target).attr('contenteditable', false);
- $target.focus();
- });
-
- // Once clicked outside, remove contenteditable on link and reactive all
- $(document).on('mousedown.reactivate_contenteditable', function (e) {
- if ($target.is(e.target)) return;
- $target.removeAttr('contenteditable');
- $editable.attr('contenteditable', true);
- $(document).off('mousedown.reactivate_contenteditable');
- });
- }
-
- if (this && this.$last && (!$editable.length || this.$last[0] !== $editable[0])) {
- var $destroy = this.$last;
- history.splitNext();
-
- _.delay(function () {
- var id = $destroy.data('note-id');
- $destroy.destroy().removeData('note-id').removeAttr('data-note-id');
- $('#note-popover-'+id+', #note-handle-'+id+', #note-dialog-'+id+'').remove();
- }, 150); // setTimeout to remove flickering when change to editable zone (re-create an editor)
- this.$last = null;
- }
- if ($editable.length && (!this.$last || this.$last[0] !== $editable[0])) {
- $editable.summernote(this._getConfig($editable));
-
- $editable.data('NoteHistory', history);
- this.$last = $editable;
-
- // firefox & IE fix
- try {
- document.execCommand('enableObjectResizing', false, false);
- document.execCommand('enableInlineTableEditing', false, false);
- document.execCommand('2D-position', false, false);
- } catch (e) { /* */ }
- document.body.addEventListener('resizestart', function (evt) {evt.preventDefault(); return false;});
- document.body.addEventListener('movestart', function (evt) {evt.preventDefault(); return false;});
- document.body.addEventListener('dragstart', function (evt) {evt.preventDefault(); return false;});
-
- if (!range.create()) {
- $editable.focusIn();
- }
-
- if (dom.isImg($target[0])) {
- $target.trigger('mousedown'); // for activate selection on picture
- }
-
- this._enableEditableArea($editable);
- }
- },
- /**
- * Called when the mouse is unpressed on the document.
- *
- * @private
- * @param {Event} ev
- */
- _onMouseup: function (ev) {
- var $target = $(ev.target);
- var $editable = $target.closest('.o_editable');
-
- if (!$editable.length) {
- return;
- }
-
- var self = this;
- _.defer(function () {
- self.historyRecordUndo($target, 'activate', true);
- });
-
- // Browsers select different content from one to another after a
- // triple click (especially: if triple-clicking on a paragraph on
- // Chrome, blank characters of the element following the paragraph are
- // selected too)
- //
- // The triple click behavior is reimplemented for all browsers here
- if (ev.originalEvent.detail === 3) {
- // Select the whole content inside the deepest DOM element that was
- // triple-clicked
- range.create(ev.target, 0, ev.target, ev.target.childNodes.length).select();
- }
- },
-});
-
-return {
- Class: RTEWidget,
- history: history,
-};
-});
-
-odoo.define('web_editor.rte.summernote_custom_colors', function (require) {
-'use strict';
-
-return [
- ['#000000', '#424242', '#636363', '#9C9C94', '#CEC6CE', '#EFEFEF', '#F7F7F7', '#FFFFFF'],
- ['#FF0000', '#FF9C00', '#FFFF00', '#00FF00', '#00FFFF', '#0000FF', '#9C00FF', '#FF00FF'],
- ['#F7C6CE', '#FFE7CE', '#FFEFC6', '#D6EFD6', '#CEDEE7', '#CEE7F7', '#D6D6E7', '#E7D6DE'],
- ['#E79C9C', '#FFC69C', '#FFE79C', '#B5D6A5', '#A5C6CE', '#9CC6EF', '#B5A5D6', '#D6A5BD'],
- ['#E76363', '#F7AD6B', '#FFD663', '#94BD7B', '#73A5AD', '#6BADDE', '#8C7BC6', '#C67BA5'],
- ['#CE0000', '#E79439', '#EFC631', '#6BA54A', '#4A7B8C', '#3984C6', '#634AA5', '#A54A7B'],
- ['#9C0000', '#B56308', '#BD9400', '#397B21', '#104A5A', '#085294', '#311873', '#731842'],
- ['#630000', '#7B3900', '#846300', '#295218', '#083139', '#003163', '#21104A', '#4A1031']
-];
-});
diff --git a/addons/web_editor/static/src/js/editor/rte.summernote.js b/addons/web_editor/static/src/js/editor/rte.summernote.js
deleted file mode 100644
index 62586a53b5c..00000000000
--- a/addons/web_editor/static/src/js/editor/rte.summernote.js
+++ /dev/null
@@ -1,1274 +0,0 @@
-odoo.define('web_editor.rte.summernote', function (require) {
-'use strict';
-
-var ajax = require('web.ajax');
-var Class = require('web.Class');
-var core = require('web.core');
-var ColorpickerDialog = require('web.colorpicker');
-var mixins = require('web.mixins');
-var base = require('web_editor.base');
-var weContext = require('web_editor.context');
-var rte = require('web_editor.rte');
-var weWidgets = require('web_editor.widget');
-
-var QWeb = core.qweb;
-var _t = core._t;
-
-ajax.jsonRpc('/web/dataset/call', 'call', {
- 'model': 'ir.ui.view',
- 'method': 'read_template',
- 'args': ['web_editor.colorpicker', weContext.get()]
-}).done(function (data) {
- QWeb.add_template(data);
-});
-
-// Summernote Lib (neek change to make accessible: method and object)
-var dom = $.summernote.core.dom;
-var range = $.summernote.core.range;
-var eventHandler = $.summernote.eventHandler;
-var renderer = $.summernote.renderer;
-
-var tplButton = renderer.getTemplate().button;
-var tplIconButton = renderer.getTemplate().iconButton;
-var tplDropdown = renderer.getTemplate().dropdown;
-
-function _rgbToHex(cssColor) {
- var rgba = cssColor.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/);
- if (!rgba) {
- return cssColor;
- }
- if (rgba[4]) {
- return cssColor;
- }
- var hex = ColorpickerDialog.prototype.convertRgbToHex(
- parseInt(rgba[1]),
- parseInt(rgba[2]),
- parseInt(rgba[3])
- );
- if (!hex) {
- return cssColor; // TODO handle error
- }
- return hex.hex.toUpperCase();
-}
-
-// Update and change the popovers content, and add history button
-var fn_createPalette = renderer.createPalette;
-renderer.createPalette = function ($container, options) {
- fn_createPalette.call(this, $container, options);
-
- if (!QWeb.has_template('web_editor.colorpicker')) {
- return;
- }
-
- var $clpicker = $(QWeb.render('web_editor.colorpicker'));
-
- var groups;
- if ($clpicker.is("colorpicker")) {
- groups = _.map($clpicker.find('[data-name="theme"]'), function (el) {
- return $(el).find("button").empty();
- });
- } else {
- groups = [$clpicker.find("button").empty()];
- }
-
- var html = "<h6 class='mt-2'>" + _t("Theme colors") + "</h6>" + _.map(groups, function ($group) {
- var $row = $("<div/>", {"class": "note-color-row mb8"}).append($group);
- 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("") + "<h6 class='mt-2'>" + _t("Common colors") + "</h6>";
- var $palettes = $container.find(".note-color .note-color-palette");
- $palettes.prepend(html);
-
- // Find the custom colors which are used in the page and add them to the color palette
- var colors = [];
- var $editable = window.__EditorMenuBar_$editable || $();
- _.each($editable.find('[style*="color"]'), function (element) {
- if (element.style.color) {
- colors.push(element.style.color);
- }
- if (element.style.backgroundColor) {
- colors.push(element.style.backgroundColor);
- }
- });
-
- var $customColorPalettes = $container.find('.note-color .note-custom-color-palette').append($('<div/>', {class: "note-color-row"}));
- var $customColorRows = $customColorPalettes.find('.note-color-row');
- _.each(_.uniq(colors), function (color) {
- var hexColor = _rgbToHex(color);
- if (_.indexOf(_.flatten(options.colors), hexColor) < 0) {
- // Create button for used custom color for backColor and foreColor both and add them into palette
- $customColorRows.append('<button type="button" class="o_custom_color" data-color="' + color + '" style="background-color:' + color + ';" />');
- }
- });
-
- $palettes.push.apply($palettes, $customColorPalettes);
-
- var $fore = $palettes.filter(":even").find("button:not(.note-color-btn)").addClass("note-color-btn");
- var $bg = $palettes.filter(":odd").find("button:not(.note-color-btn)").addClass("note-color-btn");
- $fore.each(function () {
- var $el = $(this);
- var className = $el.hasClass('o_custom_color') ? $el.data('color') : 'text-' + $el.data('color');
- $el.attr('data-event', 'foreColor').attr('data-value', className).addClass($el.hasClass('o_custom_color') ? '' : 'bg-' + $el.data('color'));
- });
- $bg.each(function () {
- var $el = $(this);
- var className = $el.hasClass('o_custom_color') ? $el.data('color') : 'bg-' + $el.data('color');
- $el.attr('data-event', 'backColor').attr('data-value', className).addClass($el.hasClass('o_custom_color') ? '' : className);
- });
-};
-
-var fn_tplPopovers = renderer.tplPopovers;
-renderer.tplPopovers = function (lang, options) {
- var $popover = $(fn_tplPopovers.call(this, lang, options));
-
- var $imagePopover = $popover.find('.note-image-popover');
- var $linkPopover = $popover.find('.note-link-popover');
- var $airPopover = $popover.find('.note-air-popover');
-
- //////////////// image popover
-
- // add center button for images
- $(tplIconButton('fa fa-align-center', {
- title: _t('Center'),
- event: 'floatMe',
- value: 'center'
- })).insertAfter($imagePopover.find('[data-event="floatMe"][data-value="left"]'));
- $imagePopover.find('button[data-event="removeMedia"]').parent().remove();
- $imagePopover.find('button[data-event="floatMe"][data-value="none"]').remove();
-
- // padding button
- var $padding = $('<div class="btn-group"/>');
- $padding.insertBefore($imagePopover.find('.btn-group:first'));
- var dropdown_content = [
- '<li><a class="dropdown-item" data-event="padding" href="#" data-value="">'+_t('None')+'</a></li>',
- '<li><a class="dropdown-item" data-event="padding" href="#" data-value="small">'+_t('Small')+'</a></li>',
- '<li><a class="dropdown-item" data-event="padding" href="#" data-value="medium">'+_t('Medium')+'</a></li>',
- '<li><a class="dropdown-item" data-event="padding" href="#" data-value="large">'+_t('Large')+'</a></li>',
- '<li><a class="dropdown-item" data-event="padding" href="#" data-value="xl">'+_t('Xl')+'</a></li>',
- ];
- $(tplIconButton('fa fa-plus-square-o', {
- title: _t('Padding'),
- dropdown: tplDropdown(dropdown_content)
- })).appendTo($padding);
-
- // circle, boxed... options became toggled
- $imagePopover.find('[data-event="imageShape"]:not([data-value])').remove();
- var $button = $(tplIconButton('fa fa-sun-o', {
- title: _t('Shadow'),
- event: 'imageShape',
- value: 'shadow'
- })).insertAfter($imagePopover.find('[data-event="imageShape"][data-value="rounded-circle"]'));
-
- // add spin for fa
- var $spin = $('<div class="btn-group d-none only_fa"/>').insertAfter($button.parent());
- $(tplIconButton('fa fa-refresh', {
- title: _t('Spin'),
- event: 'imageShape',
- value: 'fa-spin'
- })).appendTo($spin);
-
- // resize for fa
- var $resizefa = $('<div class="btn-group d-none only_fa"/>')
- .insertAfter($imagePopover.find('.btn-group:has([data-event="resize"])'));
- for (var size=1; size<=5; size++) {
- $(tplButton('<span class="note-fontsize-10">'+size+'x</span>', {
- title: size+"x",
- event: 'resizefa',
- value: size+''
- })).appendTo($resizefa);
- }
- var $colorfa = $airPopover.find('.note-color').clone();
- $colorfa.find(".dropdown-menu").css('min-width', '172px');
- $resizefa.after($colorfa);
-
- // show dialog box and delete
- var $imageprop = $('<div class="btn-group"/>');
- $imageprop.appendTo($imagePopover.find('.popover-body'));
- $(tplIconButton('fa fa-file-image-o', {
- title: _t('Edit'),
- event: 'showImageDialog'
- })).appendTo($imageprop);
- $(tplIconButton('fa fa-trash-o', {
- title: _t('Remove'),
- event: 'delete'
- })).appendTo($imageprop);
-
- $(tplIconButton('fa fa-crop', {
- title: _t('Crop Image'),
- event: 'cropImage',
- })).insertAfter($imagePopover.find('[data-event="imageShape"][data-value="img-thumbnail"]'));
-
- $imagePopover.find('.popover-body').append($airPopover.find(".note-history").clone());
-
- $imagePopover.find('[data-event="showImageDialog"]').before($airPopover.find('[data-event="showLinkDialog"]').clone());
-
- var $alt = $('<div class="btn-group"/>');
- $alt.appendTo($imagePopover.find('.popover-body'));
- $alt.append('<button class="btn btn-secondary" data-event="alt"><strong>' + _t('Description') + ': </strong><span class="o_image_alt"/></button>');
-
- //////////////// link popover
-
- $linkPopover.find('.popover-body').append($airPopover.find(".note-history").clone());
-
- $linkPopover.find('button[data-event="showLinkDialog"] i').attr("class", "fa fa-link");
- $linkPopover.find('button[data-event="unlink"]').before($airPopover.find('button[data-event="showImageDialog"]').clone());
-
- //////////////// text/air popover
-
- //// highlight the text format
- $airPopover.find('.note-style .dropdown-toggle').on('mousedown', function () {
- var $format = $airPopover.find('[data-event="formatBlock"]');
- var node = range.create().sc;
- var formats = $format.map(function () { return $(this).data("value"); }).get();
- while (node && (!node.tagName || (!node.tagName || formats.indexOf(node.tagName.toLowerCase()) === -1))) {
- node = node.parentNode;
- }
- $format.removeClass('active');
- $format.filter('[data-value="'+(node ? node.tagName.toLowerCase() : "p")+'"]')
- .addClass("active");
- });
-
- //////////////// tooltip
-
- setTimeout(function () {
- $airPopover.add($linkPopover).add($imagePopover).find("button")
- .tooltip('dispose')
- .tooltip({
- container: 'body',
- trigger: 'hover',
- placement: 'bottom'
- }).on('click', function () {$(this).tooltip('hide');});
- });
-
- return $popover;
-};
-
-var fn_boutton_update = eventHandler.modules.popover.button.update;
-eventHandler.modules.popover.button.update = function ($container, oStyle) {
- // stop animation when edit content
- var previous = $(".note-control-selection").data('target');
- if (previous) {
- var $previous = $(previous);
- $previous.css({"-webkit-animation-play-state": "", "animation-play-state": "", "-webkit-transition": "", "transition": "", "-webkit-animation": "", "animation": ""});
- $previous.find('.o_we_selected_image').addBack('.o_we_selected_image').removeClass('o_we_selected_image');
- }
- // end
-
- fn_boutton_update.call(this, $container, oStyle);
-
- $container.find('.note-color').removeClass('d-none');
-
- if (oStyle.image) {
- $container.find('[data-event]').removeClass("active");
-
- $container.find('a[data-event="padding"][data-value="small"]').toggleClass("active", $(oStyle.image).hasClass("padding-small"));
- $container.find('a[data-event="padding"][data-value="medium"]').toggleClass("active", $(oStyle.image).hasClass("padding-medium"));
- $container.find('a[data-event="padding"][data-value="large"]').toggleClass("active", $(oStyle.image).hasClass("padding-large"));
- $container.find('a[data-event="padding"][data-value="xl"]').toggleClass("active", $(oStyle.image).hasClass("padding-xl"));
- $container.find('a[data-event="padding"][data-value=""]').toggleClass("active", !$container.find('li a.active[data-event="padding"]').length);
-
- $(oStyle.image).addClass('o_we_selected_image');
-
- if (dom.isImgFont(oStyle.image)) {
- $container.find('[data-event="customColor"][data-value="foreColor"]').attr('data-color', $(oStyle.image).css('color'));
- $container.find('[data-event="customColor"][data-value="backColor"]').attr('data-color', $(oStyle.image).css('background-color'));
- $container.find('.note-fore-color-preview > button').css('border-bottom-color', $(oStyle.image).css('color'));
- $container.find('.note-back-color-preview > button').css('border-bottom-color', $(oStyle.image).css('background-color'));
-
- $container.find('.btn-group:not(.only_fa):has(button[data-event="resize"],button[data-value="img-thumbnail"])').addClass('d-none');
- $container.find('.only_fa').removeClass('d-none');
- $container.find('button[data-event="resizefa"][data-value="2"]').toggleClass("active", $(oStyle.image).hasClass("fa-2x"));
- $container.find('button[data-event="resizefa"][data-value="3"]').toggleClass("active", $(oStyle.image).hasClass("fa-3x"));
- $container.find('button[data-event="resizefa"][data-value="4"]').toggleClass("active", $(oStyle.image).hasClass("fa-4x"));
- $container.find('button[data-event="resizefa"][data-value="5"]').toggleClass("active", $(oStyle.image).hasClass("fa-5x"));
- $container.find('button[data-event="resizefa"][data-value="1"]').toggleClass("active", !$container.find('.active[data-event="resizefa"]').length);
-
- $container.find('button[data-event="imageShape"][data-value="fa-spin"]').toggleClass("active", $(oStyle.image).hasClass("fa-spin"));
- $container.find('button[data-event="imageShape"][data-value="shadow"]').toggleClass("active", $(oStyle.image).hasClass("shadow"));
- $container.find('.btn-group:has(button[data-event="imageShape"])').removeClass("d-none");
-
- } else {
- $container.find('.d-none:not(.only_fa, .note-recent-color)').removeClass('d-none');
- $container.find('.only_fa').addClass('d-none');
- var width = ($(oStyle.image).attr('style') || '').match(/(^|;|\s)width:\s*([0-9]+%)/);
- if (width) {
- width = width[2];
- }
- $container.find('button[data-event="resize"][data-value="auto"]').toggleClass("active", width !== "100%" && width !== "50%" && width !== "25%");
- $container.find('button[data-event="resize"][data-value="1"]').toggleClass("active", width === "100%");
- $container.find('button[data-event="resize"][data-value="0.5"]').toggleClass("active", width === "50%");
- $container.find('button[data-event="resize"][data-value="0.25"]').toggleClass("active", width === "25%");
-
- $container.find('button[data-event="imageShape"][data-value="shadow"]').toggleClass("active", $(oStyle.image).hasClass("shadow"));
-
- if (!$(oStyle.image).is("img")) {
- $container.find('.btn-group:has(button[data-event="imageShape"])').addClass('d-none');
- }
-
- $container.find('.note-color').addClass('d-none');
- }
-
- $container.find('button[data-event="floatMe"][data-value="left"]').toggleClass("active", $(oStyle.image).hasClass("float-left"));
- $container.find('button[data-event="floatMe"][data-value="center"]').toggleClass("active", $(oStyle.image).hasClass("d-block mx-auto"));
- $container.find('button[data-event="floatMe"][data-value="right"]').toggleClass("active", $(oStyle.image).hasClass("float-right"));
-
- $(oStyle.image).trigger('attributes_change');
- } else {
- $container.find('[data-event="customColor"][data-value="foreColor"]').attr('data-color', oStyle.color);
- $container.find('[data-event="customColor"][data-value="backColor"]').attr('data-color', oStyle['background-color']);
- $container.find('.note-fore-color-preview > button').css('border-bottom-color', oStyle.color);
- $container.find('.note-back-color-preview > button').css('border-bottom-color', oStyle['background-color']);
- }
-};
-
-var fn_popover_update = eventHandler.modules.popover.update;
-eventHandler.modules.popover.update = function ($popover, oStyle, isAirMode) {
- var $imagePopover = $popover.find('.note-image-popover');
- var $linkPopover = $popover.find('.note-link-popover');
- var $airPopover = $popover.find('.note-air-popover');
-
- fn_popover_update.call(this, $popover, oStyle, isAirMode);
-
- if (oStyle.image) {
- if (oStyle.image.parentNode.className.match(/(^|\s)media_iframe_video(\s|$)/i)) {
- oStyle.image = oStyle.image.parentNode;
- }
- var alt = $(oStyle.image).attr("alt");
-
- $imagePopover.find('.o_image_alt').text( (alt || "").replace(/&quot;/g, '"') ).parent().toggle(oStyle.image.tagName === "IMG");
- $imagePopover.show();
-
- // for video tag (non-void) we select the range over the tag,
- // for other media types we get the first descendant leaf element
- var target_node = oStyle.image;
- if (!oStyle.image.className.match(/(^|\s)media_iframe_video(\s|$)/i)) {
- target_node = dom.firstChild(target_node);
- }
- range.createFromNode(target_node).select();
- // save range on the editor so it is not lost if restored
- eventHandler.modules.editor.saveRange(dom.makeLayoutInfo(target_node).editable());
- } else {
- $(".note-control-selection").hide();
- }
-
- if (oStyle.image || (oStyle.range && (!oStyle.range.isCollapsed() || (oStyle.range.sc.tagName && !dom.isAnchor(oStyle.range.sc)))) || (oStyle.image && !$(oStyle.image).closest('a').length)) {
- $linkPopover.hide();
- oStyle.anchor = false;
- }
-
- if (oStyle.image || oStyle.anchor || (oStyle.range && !$(oStyle.range.sc).closest('.note-editable').length)) {
- $airPopover.hide();
- } else {
- $airPopover.show();
- }
-};
-
-var fn_handle_update = eventHandler.modules.handle.update;
-eventHandler.modules.handle.update = function ($handle, oStyle, isAirMode) {
- fn_handle_update.call(this, $handle, oStyle, isAirMode);
- if (oStyle.image) {
- $handle.find('.note-control-selection').hide();
- }
-};
-
-// Hack for image and link editor
-function getImgTarget($editable) {
- var $handle = $editable ? dom.makeLayoutInfo($editable).handle() : undefined;
- return $(".note-control-selection", $handle).data('target');
-}
-eventHandler.modules.editor.padding = function ($editable, sValue) {
- var $target = $(getImgTarget($editable));
- var paddings = "small medium large xl".split(/\s+/);
- $editable.data('NoteHistory').recordUndo();
- if (sValue.length) {
- paddings.splice(paddings.indexOf(sValue),1);
- $target.toggleClass('padding-'+sValue);
- }
- $target.removeClass("padding-" + paddings.join(" padding-"));
-};
-eventHandler.modules.editor.resize = function ($editable, sValue) {
- var $target = $(getImgTarget($editable));
- $editable.data('NoteHistory').recordUndo();
- var width = ($target.attr('style') || '').match(/(^|;|\s)width:\s*([0-9]+)%/);
- if (width) {
- width = width[2]/100;
- }
- $target.css('width', (width !== sValue && sValue !== "auto") ? (sValue * 100) + '%' : '');
-};
-eventHandler.modules.editor.resizefa = function ($editable, sValue) {
- var $target = $(getImgTarget($editable));
- $editable.data('NoteHistory').recordUndo();
- $target.attr('class', $target.attr('class').replace(/\s*fa-[0-9]+x/g, ''));
- if (+sValue > 1) {
- $target.addClass('fa-'+sValue+'x');
- }
-};
-eventHandler.modules.editor.floatMe = function ($editable, sValue) {
- var $target = $(getImgTarget($editable));
- $editable.data('NoteHistory').recordUndo();
- switch (sValue) {
- case 'center': $target.toggleClass('d-block mx-auto').removeClass('float-right float-left'); break;
- case 'left': $target.toggleClass('float-left').removeClass('float-right d-block mx-auto'); break;
- case 'right': $target.toggleClass('float-right').removeClass('float-left d-block mx-auto'); break;
- }
-};
-eventHandler.modules.editor.imageShape = function ($editable, sValue) {
- var $target = $(getImgTarget($editable));
- $editable.data('NoteHistory').recordUndo();
- $target.toggleClass(sValue);
-};
-
-eventHandler.modules.linkDialog.showLinkDialog = function ($editable, $dialog, linkInfo) {
- $editable.data('range').select();
- $editable.data('NoteHistory').recordUndo();
-
- var def = new $.Deferred();
- core.bus.trigger('link_dialog_demand', {
- $editable: $editable,
- linkInfo: linkInfo,
- onSave: function (linkInfo) {
- linkInfo.range.select();
- $editable.data('range', linkInfo.range);
- def.resolve(linkInfo);
- $editable.trigger('keyup');
- $('.note-popover .note-link-popover').show();
- },
- onCancel: def.reject.bind(def),
- });
- return def;
-};
-eventHandler.modules.imageDialog.showImageDialog = function ($editable) {
- var r = $editable.data('range');
- if (r.sc.tagName && r.sc.childNodes.length) {
- r.sc = r.sc.childNodes[r.so];
- }
- var media = $(r.sc).parents().addBack().filter(function (i, el) {
- return dom.isImg(el);
- })[0];
- core.bus.trigger('media_dialog_demand', {
- $editable: $editable,
- media: media,
- options: {
- lastFilters: ['background'],
- onUpload: $editable.data('callbacks').onUpload,
- noVideos: $editable.data('oe-model') === "mail.compose.message",
- },
- });
- return new $.Deferred().reject();
-};
-$.summernote.pluginEvents.alt = function (event, editor, layoutInfo, sorted) {
- var $editable = layoutInfo.editable();
- var $selection = layoutInfo.handle().find('.note-control-selection');
- core.bus.trigger('alt_dialog_demand', {
- $editable: $editable,
- media: $selection.data('target'),
- });
-};
-$.summernote.pluginEvents.customColor = function (event, editor, layoutInfo, customColor) {
- var defaultColor = event.target.dataset.color;
- core.bus.trigger('color_picker_dialog_demand', {
- color: defaultColor === 'rgba(0, 0, 0, 0)' ? 'rgb(255, 0, 0)' : defaultColor,
- onSave: function (color) {
- var $palettes = $(event.currentTarget).find('.note-custom-color-palette > .note-color-row')
- .append(('<button type="button" class="note-color-btn" data-value="' + color + '" style="background-color:' + color + ';" />'));
- $palettes.filter(':odd').find('button:not([data-event])').attr('data-event', 'backColor');
- $palettes.filter(':even').find('button:not([data-event])').attr('data-event', 'foreColor');
- if (customColor === 'foreColor') {
- $(event.currentTarget).find('.note-fore-color-preview > button').css('border-bottom-color', color);
- $.summernote.pluginEvents.foreColor(event, editor, layoutInfo, color);
- } else {
- $(event.currentTarget).find('.note-back-color-preview > button').css('border-bottom-color', color);
- $.summernote.pluginEvents.backColor(event, editor, layoutInfo, color);
- }
- }
- });
-};
-$.summernote.pluginEvents.cropImage = function (event, editor, layoutInfo, sorted) {
- var $editable = layoutInfo.editable();
- var $selection = layoutInfo.handle().find('.note-control-selection');
- core.bus.trigger('crop_image_dialog_demand', {
- $editable: $editable,
- media: $selection.data('target'),
- });
-};
-
-// Utils
-var fn_is_void = dom.isVoid || function () {};
-dom.isVoid = function (node) {
- return fn_is_void(node) || dom.isImgFont(node) || (node && node.className && node.className.match(/(^|\s)media_iframe_video(\s|$)/i));
-};
-var fn_is_img = dom.isImg || function () {};
-dom.isImg = function (node) {
- return fn_is_img(node) || dom.isImgFont(node) || (node && (node.nodeName === "IMG" || (node.className && node.className.match(/(^|\s)(media_iframe_video|o_image)(\s|$)/i)) ));
-};
-var fn_is_forbidden_node = dom.isForbiddenNode || function () {};
-dom.isForbiddenNode = function (node) {
- if (node.tagName === "BR") {
- return false;
- }
- return fn_is_forbidden_node(node) || $(node).is(".media_iframe_video");
-};
-var fn_is_img_font = dom.isImgFont || function () {};
-dom.isImgFont = function (node) {
- if (fn_is_img_font(node)) return true;
-
- var nodeName = node && node.nodeName.toUpperCase();
- var className = (node && node.className || "");
- if (node && (nodeName === "SPAN" || nodeName === "I") && className.length) {
- var classNames = className.split(/\s+/);
- for (var k=0; k<base.fontIcons.length; k++) {
- if (_.intersection(base.fontIcons[k].alias, classNames).length) {
- return true;
- }
- }
- }
- return false;
-};
-var fn_is_font = dom.isFont; // re-overwrite font to include theme icons
-dom.isFont = function (node) {
- return fn_is_font(node) || dom.isImgFont(node);
-};
-
-var fn_visible = $.summernote.pluginEvents.visible;
-$.summernote.pluginEvents.visible = function (event, editor, layoutInfo) {
- var res = fn_visible.apply(this, arguments);
- var rng = range.create();
- if (!rng) return res;
- var $node = $(dom.node(rng.sc));
- if (($node.is('[data-oe-type="html"]') || $node.is('[data-oe-field="arch"]')) &&
- $node.hasClass("o_editable") &&
- !$node[0].children.length &&
- "h1 h2 h3 h4 h5 h6 p b bold i u code sup strong small pre th td span label".toUpperCase().indexOf($node[0].nodeName) === -1) {
- var p = $('<p><br/></p>')[0];
- $node.append( p );
- range.createFromNode(p.firstChild).select();
- }
- return res;
-};
-
-function prettify_html(html) {
- html = html.trim();
- var result = '',
- level = 0,
- get_space = function (level) {
- var i = level, space = '';
- while (i--) space += ' ';
- return space;
- },
- reg = /^<\/?(a|span|font|u|em|i|strong|b)(\s|>)/i,
- inline_level = Infinity,
- tokens = _.compact(_.flatten(_.map(html.split(/</), function (value) {
- value = value.replace(/\s+/g, ' ').split(/>/);
- value[0] = /\S/.test(value[0]) ? '<' + value[0] + '>' : '';
- return value;
- })));
-
- // reduce => merge inline style + text
-
- for (var i = 0, l = tokens.length; i < l; i++) {
- var token = tokens[i];
- var inline_tag = reg.test(token);
- var inline = inline_tag || inline_level <= level;
-
- if (token[0] === '<' && token[1] === '/') {
- if (inline_tag && inline_level === level) {
- inline_level = Infinity;
- }
- level--;
- }
-
- if (!inline && !/\S/.test(token)) {
- continue;
- }
- if (!inline || (token[1] !== '/' && inline_level > level)) {
- result += get_space(level);
- }
-
- if (token[0] === '<' && token[1] !== '/') {
- level++;
- if (inline_tag && inline_level > level) {
- inline_level = level;
- }
- }
-
- if (token.match(/^<(img|hr|br)/)) {
- level--;
- }
-
- // don't trim inline content (which could change appearance)
- if (!inline) {
- token = token.trim();
- }
-
- result += token.replace(/\s+/, ' ');
-
- if (inline_level > level) {
- result += '\n';
- }
- }
- return result;
-}
-
-/*
- * This override when clicking on the 'Code View' button has two aims:
- *
- * - have our own code view implementation for FieldTextHtml
- * - add an 'enable' paramater to call the function directly and allow us to
- * disable (false) or enable (true) the code view mode.
- */
-$.summernote.pluginEvents.codeview = function (event, editor, layoutInfo, enable) {
- if (layoutInfo === undefined) {
- return;
- }
- if (layoutInfo.toolbar) {
- // if editor inline (FieldTextHtmlSimple)
- var is_activated = $.summernote.eventHandler.modules.codeview.isActivated(layoutInfo);
- if (is_activated === enable) {
- return;
- }
- return eventHandler.modules.codeview.toggle(layoutInfo);
- } else {
- // if editor iframe (FieldTextHtml)
- var $editor = layoutInfo.editor();
- var $textarea = $editor.prev('textarea');
- if ($textarea.is('textarea') === enable) {
- return;
- }
-
- if (!$textarea.length) {
- // init and create texarea
- var html = prettify_html($editor.prop("innerHTML"));
- $editor.parent().css({
- 'position': 'absolute',
- 'top': 0,
- 'bottom': 0,
- 'left': 0,
- 'right': 0
- });
- $textarea = $('<textarea/>').css({
- 'margin': '0 -4px',
- 'padding': '0 4px',
- 'border': 0,
- 'top': '51px',
- 'left': '620px',
- 'width': '100%',
- 'font-family': 'sans-serif',
- 'font-size': '13px',
- 'height': '98%',
- 'white-space': 'pre',
- 'word-wrap': 'normal'
- }).val(html).data('init', html);
- $editor.before($textarea);
- $editor.hide();
- } else {
- // save changes
- $editor.prop('innerHTML', $textarea.val().replace(/\s*\n\s*/g, '')).trigger('content_changed');
- $textarea.remove();
- $editor.show();
- }
- }
-};
-
-// Fix ie and re-range to don't break snippet
-var last_div;
-var last_div_change;
-var last_editable;
-var initial_data = {};
-function reRangeSelectKey(event) {
- initial_data.range = null;
- if (event.shiftKey && event.keyCode >= 37 && event.keyCode <= 40 && !$(event.target).is("input, textarea, select")) {
- var r = range.create();
- if (r) {
- var rng = r.reRange(event.keyCode <= 38);
- if (r !== rng) {
- rng.select();
- }
- }
- }
-}
-function reRangeSelect(event, dx, dy) {
- var r = range.create();
- if (!r || r.isCollapsed()) return;
-
- // check if the user move the caret on up or down
- var data = r.reRange(dy < 0 || (dy === 0 && dx < 0));
-
- if (data.sc !== r.sc || data.so !== r.so || data.ec !== r.ec || data.eo !== r.eo) {
- setTimeout(function () {
- data.select();
- $(data.sc.parentNode).closest('.note-popover');
- },0);
- }
-
- $(data.sc).closest('.o_editable').data('range', r);
- return r;
-}
-function summernote_mouseup(event) {
- if ($(event.target).closest("#web_editor-top-navbar, .note-popover").length) {
- return;
- }
- // don't rerange if simple click
- if (initial_data.event) {
- var dx = event.clientX - (event.shiftKey && initial_data.rect ? initial_data.rect.left : initial_data.event.clientX);
- var dy = event.clientY - (event.shiftKey && initial_data.rect ? initial_data.rect.top : initial_data.event.clientY);
- if (10 < Math.pow(dx, 2)+Math.pow(dy, 2)) {
- reRangeSelect(event, dx, dy);
- }
- }
-
- if (!$(event.target).closest(".o_editable").length) {
- return;
- }
- if (!initial_data.range || !event.shiftKey) {
- setTimeout(function () {
- initial_data.range = range.create();
- },0);
- }
-}
-var remember_selection;
-function summernote_mousedown(event) {
- rte.history.splitNext();
-
- var $editable = $(event.target).closest(".o_editable, .note-editor");
- var r;
-
- if (document.documentMode) {
- summernote_ie_fix(event, function (node) { return node.tagName === "DIV" || node.tagName === "IMG" || (node.dataset && node.dataset.oeModel); });
- } else if (last_div && event.target !== last_div) {
- if (last_div.tagName === "A") {
- summernote_ie_fix(event, function (node) { return node.dataset && node.dataset.oeModel; });
- } else if ($editable.length) {
- if (summernote_ie_fix(event, function (node) { return node.tagName === "A"; })) {
- r = range.create();
- r.select();
- }
- }
- }
-
- // restore range if range lost after clicking on non-editable area
- try {
- r = range.create();
- } catch (e) {
- // If this code is running inside an iframe-editor and that the range
- // is outside of this iframe, this will fail as the iframe does not have
- // the permission to check the outside content this way. In that case,
- // we simply ignore the exception as it is as if there was no range.
- return;
- }
- var editables = $(".o_editable[contenteditable], .note-editable[contenteditable]");
- var r_editable = editables.has((r||{}).sc).addBack(editables.filter((r||{}).sc));
- if (!r_editable.closest('.note-editor').is($editable) && !r_editable.filter('.o_editable').is(editables)) {
- var saved_editable = editables.has((remember_selection||{}).sc);
- if ($editable.length && !saved_editable.closest('.o_editable, .note-editor').is($editable)) {
- remember_selection = range.create(dom.firstChild($editable[0]), 0);
- } else if (!saved_editable.length) {
- remember_selection = undefined;
- }
- if (remember_selection) {
- try {
- remember_selection.select();
- } catch (e) {
- console.warn(e);
- }
- }
- } else if (r_editable.length) {
- remember_selection = r;
- }
-
- initial_data.event = event;
-
- // keep selection when click with shift
- if (event.shiftKey && $editable.length) {
- if (initial_data.range) {
- initial_data.range.select();
- }
- var rect = r && r.getClientRects();
- initial_data.rect = rect && rect.length ? rect[0] : { top: 0, left: 0 };
- }
-}
-
-function summernote_ie_fix(event, pred) {
- var editable;
- var div;
- var node = event.target;
- while (node.parentNode) {
- if (!div && pred(node)) {
- div = node;
- }
- if (last_div !== node && (node.getAttribute('contentEditable')==='false' || node.className && (node.className.indexOf('o_not_editable') !== -1))) {
- break;
- }
- if (node.className && node.className.indexOf('o_editable') !== -1) {
- if (!div) {
- div = node;
- }
- editable = node;
- break;
- }
- node = node.parentNode;
- }
-
- if (!editable) {
- $(last_div_change).removeAttr("contentEditable").removeProp("contentEditable");
- $(last_editable).attr("contentEditable", "true").prop("contentEditable", "true");
- last_div_change = null;
- last_editable = null;
- return;
- }
-
- if (div === last_div) {
- return;
- }
-
- last_div = div;
-
- $(last_div_change).removeAttr("contentEditable").removeProp("contentEditable");
-
- if (last_editable !== editable) {
- if ($(editable).is("[contentEditable='true']")) {
- $(editable).removeAttr("contentEditable").removeProp("contentEditable");
- last_editable = editable;
- } else {
- last_editable = null;
- }
- }
- if (!$(div).attr("contentEditable") && !$(div).is("[data-oe-type='many2one'], [data-oe-type='contact']")) {
- $(div).attr("contentEditable", "true").prop("contentEditable", "true");
- last_div_change = div;
- } else {
- last_div_change = null;
- }
- return editable !== div ? div : null;
-}
-
-var fn_attach = eventHandler.attach;
-eventHandler.attach = function (oLayoutInfo, options) {
- fn_attach.call(this, oLayoutInfo, options);
-
- oLayoutInfo.editor().on('dragstart', 'img', function (e) { e.preventDefault(); });
- $(document).on('mousedown', summernote_mousedown).on('mouseup', summernote_mouseup);
- oLayoutInfo.editor().off('click').on('click', function (e) {e.preventDefault();}); // if the content editable is a link
-
- /**
- * Open Media Dialog on double click on an image/video/icon.
- * Shows a tooltip on click to say to the user he can double click.
- */
- create_dblclick_feature("img, .media_iframe_video, i.fa, span.fa, a.o_image", function () {
- eventHandler.modules.imageDialog.show(oLayoutInfo);
- });
-
- /**
- * Open Link Dialog on double click on a link/button.
- * Shows a tooltip on click to say to the user he can double click.
- */
- create_dblclick_feature("a[href], a.btn, button.btn", function () {
- eventHandler.modules.linkDialog.show(oLayoutInfo);
- });
-
- oLayoutInfo.editable().on('mousedown', function (e) {
- if (dom.isImg(e.target) && dom.isContentEditable(e.target)) {
- range.createFromNode(e.target).select();
- }
- });
- $(document).on("keyup", reRangeSelectKey);
-
- var clone_data = false;
-
- if (options.model) {
- oLayoutInfo.editable().data({'oe-model': options.model, 'oe-id': options.id});
- }
- if (options.getMediaDomain) {
- oLayoutInfo.editable().data('oe-media-domain', options.getMediaDomain);
- }
-
- var $node = oLayoutInfo.editor();
- if ($node.data('oe-model') || $node.data('oe-translation-id')) {
- $node.on('content_changed', function () {
- var $nodes = $('[data-oe-model], [data-oe-translation-id]')
- .filter(function () { return this !== $node[0];});
-
- if ($node.data('oe-model')) {
- $nodes = $nodes.filter('[data-oe-model="'+$node.data('oe-model')+'"]')
- .filter('[data-oe-id="'+$node.data('oe-id')+'"]')
- .filter('[data-oe-field="'+$node.data('oe-field')+'"]');
- }
- if ($node.data('oe-translation-id')) $nodes = $nodes.filter('[data-oe-translation-id="'+$node.data('oe-translation-id')+'"]');
- if ($node.data('oe-type')) $nodes = $nodes.filter('[data-oe-type="'+$node.data('oe-type')+'"]');
- if ($node.data('oe-expression')) $nodes = $nodes.filter('[data-oe-expression="'+$node.data('oe-expression')+'"]');
- if ($node.data('oe-xpath')) $nodes = $nodes.filter('[data-oe-xpath="'+$node.data('oe-xpath')+'"]');
- if ($node.data('oe-contact-options')) $nodes = $nodes.filter('[data-oe-contact-options="'+$node.data('oe-contact-options')+'"]');
-
- var nodes = $node.get();
-
- if ($node.data('oe-type') === "many2one") {
- $nodes = $nodes.add($('[data-oe-model]')
- .filter(function () { return this !== $node[0] && nodes.indexOf(this) === -1; })
- .filter('[data-oe-many2one-model="'+$node.data('oe-many2one-model')+'"]')
- .filter('[data-oe-many2one-id="'+$node.data('oe-many2one-id')+'"]')
- .filter('[data-oe-type="many2one"]'));
-
- $nodes = $nodes.add($('[data-oe-model]')
- .filter(function () { return this !== $node[0] && nodes.indexOf(this) === -1; })
- .filter('[data-oe-model="'+$node.data('oe-many2one-model')+'"]')
- .filter('[data-oe-id="'+$node.data('oe-many2one-id')+'"]')
- .filter('[data-oe-field="name"]'));
- }
-
- if (!clone_data) {
- clone_data = true;
- $nodes.html(this.innerHTML);
- clone_data = false;
- }
- });
- }
-
- var custom_toolbar = oLayoutInfo.toolbar ? oLayoutInfo.toolbar() : undefined;
- var $toolbar = $(oLayoutInfo.popover()).add(custom_toolbar);
- $('button[data-event="undo"], button[data-event="redo"]', $toolbar).attr('disabled', true);
-
- $(oLayoutInfo.editor())
- .add(oLayoutInfo.handle())
- .add(oLayoutInfo.popover())
- .add(custom_toolbar)
- .on('click content_changed', function () {
- $('button[data-event="undo"]', $toolbar).attr('disabled', !oLayoutInfo.editable().data('NoteHistory').hasUndo());
- $('button[data-event="redo"]', $toolbar).attr('disabled', !oLayoutInfo.editable().data('NoteHistory').hasRedo());
- });
-
- function create_dblclick_feature(selector, callback) {
- var show_tooltip = true;
-
- oLayoutInfo.editor().on("dblclick", selector, function (e) {
- var $target = $(e.target);
- if (!dom.isContentEditable($target)) {
- // Prevent edition of non editable parts
- return;
- }
-
- show_tooltip = false;
- callback();
- e.stopImmediatePropagation();
- });
-
- oLayoutInfo.editor().on("click", selector, function (e) {
- var $target = $(e.target);
- if (!dom.isContentEditable($target)) {
- // Prevent edition of non editable parts
- return;
- }
-
- show_tooltip = true;
- setTimeout(function () {
- if (!show_tooltip) return;
- $target.tooltip({title: _t('Double-click to edit'), trigger: 'manuel', container: 'body'}).tooltip('show');
- setTimeout(function () {
- $target.tooltip('dispose');
- }, 800);
- }, 400);
- });
- }
-};
-var fn_detach = eventHandler.detach;
-eventHandler.detach = function (oLayoutInfo, options) {
- fn_detach.call(this, oLayoutInfo, options);
- oLayoutInfo.editable().off('mousedown');
- oLayoutInfo.editor().off("dragstart");
- oLayoutInfo.editor().off('click');
- $(document).off('mousedown', summernote_mousedown);
- $(document).off('mouseup', summernote_mouseup);
- oLayoutInfo.editor().off("dblclick");
- $(document).off("keyup", reRangeSelectKey);
-};
-
-// Translation for odoo
-$.summernote.lang.odoo = {
- font: {
- bold: _t('Bold'),
- italic: _t('Italic'),
- underline: _t('Underline'),
- strikethrough: _t('Strikethrough'),
- subscript: _t('Subscript'),
- superscript: _t('Superscript'),
- clear: _t('Remove Font Style'),
- height: _t('Line Height'),
- name: _t('Font Family'),
- size: _t('Font Size')
- },
- image: {
- image: _t('File / Image'),
- insert: _t('Insert Image'),
- resizeFull: _t('Resize Full'),
- resizeHalf: _t('Resize Half'),
- resizeQuarter: _t('Resize Quarter'),
- floatLeft: _t('Float Left'),
- floatRight: _t('Float Right'),
- floatNone: _t('Float None'),
- dragImageHere: _t('Drag an image here'),
- selectFromFiles: _t('Select from files'),
- url: _t('Image URL'),
- remove: _t('Remove Image')
- },
- link: {
- link: _t('Link'),
- insert: _t('Insert Link'),
- unlink: _t('Unlink'),
- edit: _t('Edit'),
- textToDisplay: _t('Text to display'),
- url: _t('To what URL should this link go?'),
- openInNewWindow: _t('Open in new window')
- },
- video: {
- video: _t('Video'),
- videoLink: _t('Video Link'),
- insert: _t('Insert Video'),
- url: _t('Video URL?'),
- providers: _t('(YouTube, Vimeo, Vine, Instagram, DailyMotion or Youku)')
- },
- table: {
- table: _t('Table')
- },
- hr: {
- insert: _t('Insert Horizontal Rule')
- },
- style: {
- style: _t('Style'),
- normal: _t('Normal'),
- blockquote: _t('Quote'),
- pre: _t('Code'),
- small: _t('Small'),
- h1: _t('Header 1'),
- h2: _t('Header 2'),
- h3: _t('Header 3'),
- h4: _t('Header 4'),
- h5: _t('Header 5'),
- h6: _t('Header 6')
- },
- lists: {
- unordered: _t('Unordered list'),
- ordered: _t('Ordered list')
- },
- options: {
- help: _t('Help'),
- fullscreen: _t('Full Screen'),
- codeview: _t('Code View')
- },
- paragraph: {
- paragraph: _t('Paragraph'),
- outdent: _t('Outdent'),
- indent: _t('Indent'),
- left: _t('Align left'),
- center: _t('Align center'),
- right: _t('Align right'),
- justify: _t('Justify full')
- },
- color: {
- custom: _t('Custom Color'),
- background: _t('Background Color'),
- foreground: _t('Font Color'),
- transparent: _t('Transparent'),
- setTransparent: _t('None'),
- },
- shortcut: {
- shortcuts: _t('Keyboard shortcuts'),
- close: _t('Close'),
- textFormatting: _t('Text formatting'),
- action: _t('Action'),
- paragraphFormatting: _t('Paragraph formatting'),
- documentStyle: _t('Document Style')
- },
- history: {
- undo: _t('Undo'),
- redo: _t('Redo')
- }
-};
-
-//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
-
-/**
- * @todo get rid of this. This has been implemented as a fix to be able to
- * instantiate media, link and alt dialogs outside the main editor: in the
- * simple HTML fields and forum textarea.
- */
-var SummernoteManager = Class.extend(mixins.EventDispatcherMixin, {
- /**
- * @constructor
- */
- init: function (parent) {
- mixins.EventDispatcherMixin.init.call(this);
- this.setParent(parent);
-
- core.bus.on('alt_dialog_demand', this, this._onAltDialogDemand);
- core.bus.on('color_picker_dialog_demand', this, this._onColorPickerDialogDemand);
- core.bus.on('crop_image_dialog_demand', this, this._onCropImageDialogDemand);
- core.bus.on('link_dialog_demand', this, this._onLinkDialogDemand);
- core.bus.on('media_dialog_demand', this, this._onMediaDialogDemand);
- },
- /**
- * @override
- */
- destroy: function () {
- mixins.EventDispatcherMixin.destroy.call(this);
-
- core.bus.off('alt_dialog_demand', this, this._onAltDialogDemand);
- core.bus.off('color_picker_dialog_demand', this, this._onColorPickerDialogDemand);
- core.bus.off('crop_image_dialog_demand', this, this._onCropImageDialogDemand);
- core.bus.off('link_dialog_demand', this, this._onLinkDialogDemand);
- core.bus.off('media_dialog_demand', this, this._onMediaDialogDemand);
- },
-
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
-
- /**
- * Called when a demand to open a alt dialog is received on the bus.
- *
- * @private
- * @param {Object} data
- */
- _onAltDialogDemand: function (data) {
- if (data.__alreadyDone) {
- return;
- }
- data.__alreadyDone = true;
- var altDialog = new weWidgets.AltDialog(this,
- data.options || {},
- data.$editable,
- data.media
- );
- if (data.onSave) {
- altDialog.on('save', this, data.onSave);
- }
- if (data.onCancel) {
- altDialog.on('cancel', this, data.onCancel);
- }
- altDialog.open();
- },
-/**
- * Called when a demand to open a color picker dialog is received on the bus.
- *
- * @private
- * @param {Object} data
- */
- _onColorPickerDialogDemand: function (data) {
- if (data.__alreadyDone) {
- return;
- }
- data.__alreadyDone = true;
- var colorpicker = new ColorpickerDialog(this, {
- defaultColor: data.color,
- });
- if (data.onSave) {
- colorpicker.on('colorpicker:saved', this, function (ev) {
- data.onSave(ev.data.cssColor);
- });
- }
- colorpicker.open();
- },
- /**
- * Called when a demand to open a crop dialog is received on the bus.
- *
- * @private
- * @param {Object} data
- */
- _onCropImageDialogDemand: function (data) {
- if (data.__alreadyDone) {
- return;
- }
- data.__alreadyDone = true;
- var cropImageDialog = new weWidgets.CropImageDialog(this,
- _.extend({
- res_model: data.$editable.data('oe-model'),
- res_id: data.$editable.data('oe-id'),
- }, data.options || {}),
- data.$editable,
- data.media
- );
- if (data.onSave) {
- cropImageDialog.on('save', this, data.onSave);
- }
- if (data.onCancel) {
- cropImageDialog.on('cancel', this, data.onCancel);
- }
- cropImageDialog.open();
- },
- /**
- * Called when a demand to open a link dialog is received on the bus.
- *
- * @private
- * @param {Object} data
- */
- _onLinkDialogDemand: function (data) {
- if (data.__alreadyDone) {
- return;
- }
- data.__alreadyDone = true;
- var linkDialog = new weWidgets.LinkDialog(this,
- data.options || {},
- data.$editable,
- data.linkInfo
- );
- if (data.onSave) {
- linkDialog.on('save', this, data.onSave);
- }
- if (data.onCancel) {
- linkDialog.on('cancel', this, data.onCancel);
- }
- linkDialog.open();
- },
- /**
- * Called when a demand to open a media dialog is received on the bus.
- *
- * @private
- * @param {Object} data
- */
- _onMediaDialogDemand: function (data) {
- if (data.__alreadyDone) {
- return;
- }
- data.__alreadyDone = true;
-
- var mediaDialog = new weWidgets.MediaDialog(this,
- _.extend({
- res_model: data.$editable.data('oe-model'),
- res_id: data.$editable.data('oe-id'),
- domain: data.$editable.data('oe-media-domain'),
- }, data.options),
- data.$editable,
- data.media
- );
- if (data.onSave) {
- mediaDialog.on('save', this, data.onSave);
- }
- if (data.onCancel) {
- mediaDialog.on('cancel', this, data.onCancel);
- }
- mediaDialog.open();
- },
-});
-/**
- * @todo cannot do this without include because it would make a loop in the
- * JS module dependencies otherwise.
- */
-rte.Class.include({
- /**
- * @override
- */
- start: function () {
- this._summernoteManager = new SummernoteManager(this);
- return this._super.apply(this, arguments);
- },
- /**
- * @override
- */
- cancel: function () {
- this._super.apply(this, arguments);
- this._summernoteManager.destroy();
- },
-});
-return SummernoteManager;
-});
diff --git a/addons/web_editor/static/src/js/editor/snippets.editor.js b/addons/web_editor/static/src/js/editor/snippets.editor.js
deleted file mode 100644
index fe8876468b5..00000000000
--- a/addons/web_editor/static/src/js/editor/snippets.editor.js
+++ /dev/null
@@ -1,1408 +0,0 @@
-odoo.define('web_editor.snippet.editor', function (require) {
-'use strict';
-
-var core = require('web.core');
-var dom = require('web.dom');
-var Widget = require('web.Widget');
-var options = require('web_editor.snippets.options');
-
-var _t = core._t;
-
-var globalSelector = {
- closest: function () { return $(); },
- all: function () { return $(); },
- is: function () { return false; },
-};
-
-/**
- * Management of the overlay and option list for a snippet.
- */
-var SnippetEditor = Widget.extend({
- template: 'web_editor.snippet_overlay',
- xmlDependencies: ['/web_editor/static/src/xml/snippets.xml'],
- events: {
- 'click .oe_snippet_parent': '_onParentButtonClick',
- 'click .oe_snippet_clone': '_onCloneClick',
- 'click .oe_snippet_remove': '_onRemoveClick',
- },
- custom_events: {
- cover_update: '_onCoverUpdate',
- option_update: '_onOptionUpdate',
- },
-
- /**
- * @constructor
- * @param {Widget} parent
- * @param {Element} target
- * @param templateOptions
- */
- init: function (parent, target, templateOptions) {
- this._super.apply(this, arguments);
- this.$target = $(target);
- this.$target.data('snippet-editor', this);
- this.templateOptions = templateOptions;
- },
- /**
- * @override
- */
- start: function () {
- var self = this;
- var defs = [this._super.apply(this, arguments)];
-
- // Hide parent button if no parent
- var $parent = globalSelector.closest(this.$target.parent());
- if (!$parent.length) {
- this.$('.oe_snippet_parent').remove();
- }
-
- // Initialize the associated options (see snippets.options.js)
- defs.push(this._initializeOptions());
-
- // Initialize move/clone/remove buttons
- if (!this.$target.parent().is(':o_editable')) {
- this.$el.find('.oe_snippet_move, .oe_snippet_clone, .oe_snippet_remove').remove();
- } else {
- this.dropped = false;
- this.$el.draggable({
- greedy: true,
- appendTo: 'body',
- cursor: 'move',
- handle: '.oe_snippet_move',
- cursorAt: {
- left: 18,
- top: 14
- },
- helper: function () {
- var $clone = $(this).clone().css({width: '24px', height: '24px', border: 0});
- $clone.find('.oe_overlay_options >:not(:contains(.oe_snippet_move)), .o_handle').remove();
- $clone.find(':not(.glyphicon)').css({position: 'absolute', top: 0, left: 0});
- $clone.appendTo('body').removeClass('d-none');
- return $clone;
- },
- start: _.bind(self._onDragAndDropStart, self),
- stop: _.bind(self._onDragAndDropStop, self)
- });
- }
-
- return $.when.apply($, defs);
- },
- /**
- * @override
- */
- destroy: function () {
- this.cleanForSave();
- this._super.apply(this, arguments);
- this.$target.removeData('snippet-editor');
- },
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Notifies all the associated snippet options that the snippet has just
- * been dropped in the page.
- */
- buildSnippet: function () {
- for (var i in this.styles) {
- this.styles[i].onBuilt();
- }
- },
- /**
- * Notifies all the associated snippet options that the template which
- * contains the snippet is about to be saved.
- */
- cleanForSave: function () {
- if (this.isDestroyed()) {
- return;
- }
- _.each(this.styles, function (option) {
- option.cleanForSave();
- });
- },
- /**
- * Makes the editor overlay cover the associated snippet.
- */
- cover: function () {
- var offset = this.$target.offset();
- var manipulatorOffset = this.$el.parent().offset();
- offset.top -= manipulatorOffset.top;
- offset.left -= manipulatorOffset.left;
- this.$el.css({
- width: this.$target.outerWidth(),
- left: offset.left,
- top: offset.top,
- });
- this.$('.o_handles').css('height', this.$target.outerHeight());
- this.$el.toggleClass('o_top_cover', offset.top < 15);
- },
- /**
- * Removes the associated snippet from the DOM and destroys the associated
- * editor (itself).
- */
- removeSnippet: function () {
- this.toggleFocus(false);
-
- this.trigger_up('call_for_each_child_snippet', {
- $snippet: this.$target,
- callback: function (editor, $snippet) {
- for (var i in editor.styles) {
- editor.styles[i].onRemove();
- }
- },
- });
-
- var $parent = this.$target.parent();
- this.$target.find('*').andSelf().tooltip('dispose');
- this.$target.remove();
- this.$el.remove();
-
- var node = $parent[0];
- if (node && node.firstChild) {
- $.summernote.core.dom.removeSpace(node, node.firstChild, 0, node.lastChild, 1);
- if (!node.firstChild.tagName && node.firstChild.textContent === ' ') {
- node.removeChild(node.firstChild);
- }
- }
-
- if ($parent.closest(':data("snippet-editor")').length) {
- while (!$parent.data('snippet-editor')) {
- var $nextParent = $parent.parent();
- if ($parent.children().length === 0 && $parent.text().trim() === '' && !$parent.hasClass('oe_structure')) {
- $parent.remove();
- }
- $parent = $nextParent;
- }
- if ($parent.children().length === 0 && $parent.text().trim() === '' && !$parent.hasClass('oe_structure')) {
- _.defer(function () {
- $parent.data('snippet-editor').removeSnippet();
- });
- }
- }
-
- // clean editor if they are image or table in deleted content
- $('.note-control-selection').hide();
- $('.o_table_handler').remove();
-
- this.trigger_up('snippet_removed');
- this.destroy();
- },
- /**
- * Displays/Hides the editor overlay and notifies the associated snippet
- * options. Note: when it is displayed, this is here that the parent
- * snippet options are moved to the editor overlay.
- *
- * @param {boolean} focus - true to display, false to hide
- */
- toggleFocus: function (focus) {
- var do_action = (focus ? _do_action_focus : _do_action_blur);
-
- // Attach own and parent options on the current overlay
- var $style_button = this.$el.find('.oe_options');
- var $ul = $style_button.find('.dropdown-menu:first');
- var $headers = $ul.find('.dropdown-header:data(editor)');
- _.each($headers, (function (el) {
- var $el = $(el);
- var styles = _.values($el.data('editor').styles);
- if ($el.data('editor') !== this) {
- styles = _.filter(styles, function (option) { return !option.preventChildPropagation; });
- }
-
- var count = 0;
- _.each(_.sortBy(styles, '__order').reverse(), function (style) {
- if (do_action(style, $el)) {
- count++;
- }
- });
- $el.toggleClass('d-none', count === 0);
- }).bind(this));
-
- // Activate the overlay
- $style_button.toggleClass('d-none', $ul.children(':not(.o_main_header):not(.dropdown-divider):not(.d-none)').length === 0);
- this.cover();
- this.$el.toggleClass('oe_active', !!focus);
-
- function _do_action_focus(style, $dest) {
- style.$el.insertAfter($dest);
- style.onFocus();
- return (style.$el.length > 0);
- }
- function _do_action_blur(style, $dest) {
- style.$el.detach();
- style.onBlur();
- return false;
- }
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * DOMElements have a default name which appears in the overlay when they
- * are being edited. This method retrieves this name; it can be defined
- * directly in the DOM thanks to the `data-name` attribute.
- *
- * @private
- */
- _getName: function () {
- if (this.$target.data('name') !== undefined) {
- return this.$target.data('name');
- }
- if (this.$target.parent('.row').length) {
- return _t("Column");
- }
- return _t("Block");
- },
- /**
- * Instantiates the snippet's options.
- *
- * @private
- */
- _initializeOptions: function () {
- var self = this;
- var $styles = this.$el.find('.oe_options');
- var $ul = $styles.find('.dropdown-menu:first');
- this.styles = {};
- this.selectorSiblings = [];
- this.selectorChildren = [];
-
- var i = 0;
- $ul.append($('<div/>', {class: 'dropdown-header o_main_header', text: this._getName()}).data('editor', this));
- var defs = _.map(this.templateOptions, function (val, option_id) {
- if (!val.selector.is(self.$target)) {
- return;
- }
- if (val['drop-near']) self.selectorSiblings.push(val['drop-near']);
- if (val['drop-in']) self.selectorChildren.push(val['drop-in']);
-
- var optionName = val.option;
- var $el = val.$el.children().clone(true).addClass('snippet-option-' + optionName);
- var option = new (options.registry[optionName] || options.Class)(
- self,
- val.base_target ? self.$target.find(val.base_target).eq(0) : self.$target,
- self.$el,
- val.data
- );
- self.styles[optionName || _.uniqueId('option')] = option;
- option.__order = i++;
- return option.attachTo($el);
- });
- $ul.append($('<div/>', {class: 'dropdown-divider mt-2'}));
-
- var $parents = this.$target.parents();
- _.each($parents, function (parent) {
- var parentEditor = $(parent).data('snippet-editor');
- if (parentEditor) {
- for (var styleName in parentEditor.styles) {
- if (!parentEditor.styles[styleName].preventChildPropagation) {
- $ul.append($('<div/>', {class: 'dropdown-header o_parent_editor_header', text: parentEditor._getName()}).data('editor', parentEditor));
- break;
- }
- }
- }
- });
-
- if (!this.selectorSiblings.length && !this.selectorChildren.length) {
- this.$el.find('.oe_snippet_move, .oe_snippet_clone').addClass('d-none');
- }
-
- this.$el.find('[data-toggle="dropdown"]').dropdown();
-
- return $.when.apply($, defs);
- },
-
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
-
- /**
- * Called when the 'clone' button is clicked.
- *
- * @private
- * @param {Event} ev
- */
- _onCloneClick: function (ev) {
- ev.preventDefault();
- var $clone = this.$target.clone(false);
-
- this.trigger_up('request_history_undo_record', {$target: this.$target});
-
- this.$target.after($clone);
- this.trigger_up('call_for_each_child_snippet', {
- $snippet: $clone,
- callback: function (editor, $snippet) {
- for (var i in editor.styles) {
- editor.styles[i].onClone({
- isCurrent: ($snippet.is($clone)),
- });
- }
- },
- });
- this.trigger_up('snippet_cloned', {$target: $clone});
- },
- /**
- * Called when the overlay dimensions/positions should be recomputed.
- *
- * @private
- */
- _onCoverUpdate: function () {
- this.cover();
- },
- /**
- * Called when the snippet is starting to be dragged thanks to the 'move'
- * button.
- *
- * @private
- */
- _onDragAndDropStart: function () {
- var self = this;
- self.size = {
- width: self.$target.width(),
- height: self.$target.height()
- };
- self.$target.after('<div class="oe_drop_clone" style="display: none;"/>');
- self.$target.detach();
- self.$el.addClass('d-none');
-
- var $selectorSiblings;
- for (var i = 0 ; i < self.selectorSiblings.length ; i++) {
- if (!$selectorSiblings) $selectorSiblings = self.selectorSiblings[i].all();
- else $selectorSiblings = $selectorSiblings.add(self.selectorSiblings[i].all());
- }
- var $selectorChildren;
- for (i = 0 ; i < self.selectorChildren.length ; i++) {
- if (!$selectorChildren) $selectorChildren = self.selectorChildren[i].all();
- else $selectorChildren = $selectorChildren.add(self.selectorChildren[i].all());
- }
-
- this.trigger_up('go_to_parent', {$snippet: this.$target});
- this.trigger_up('activate_insertion_zones', {
- $selectorSiblings: $selectorSiblings,
- $selectorChildren: $selectorChildren,
- });
-
- $('body').addClass('move-important');
-
- $('.oe_drop_zone').droppable({
- over: function () {
- $('.oe_drop_zone.hide').removeClass('hide');
- $(this).addClass('hide').first().after(self.$target);
- self.dropped = true;
- },
- out: function () {
- $(this).removeClass('hide');
- self.$target.detach();
- self.dropped = false;
- },
- });
- },
- /**
- * Called when the snippet is dropped after being dragged thanks to the
- * 'move' button.
- *
- * @private
- */
- _onDragAndDropStop: function () {
- var self = this;
-
- $('.oe_drop_zone').droppable('destroy').remove();
-
- var prev = this.$target.first()[0].previousSibling;
- var next = this.$target.last()[0].nextSibling;
- var $parent = this.$target.parent();
-
- var $clone = $('.oe_drop_clone');
- if (prev === $clone[0]) {
- prev = $clone[0].previousSibling;
- } else if (next === $clone[0]) {
- next = $clone[0].nextSibling;
- }
- $clone.after(this.$target);
-
- this.$el.removeClass('d-none');
- $('body').removeClass('move-important');
- $clone.remove();
-
- if (this.dropped) {
- this.trigger_up('request_history_undo_record', {$target: this.$target});
-
- if (prev) {
- this.$target.insertAfter(prev);
- } else if (next) {
- this.$target.insertBefore(next);
- } else {
- $parent.prepend(this.$target);
- }
-
- for (var i in this.styles) {
- this.styles[i].onMove();
- }
- }
-
- self.trigger_up('drag_and_drop_stop', {
- $snippet: self.$target,
- });
- },
- /**
- * Called when a child editor/option asks for another option to perform a
- * specific action/react to a specific event.
- *
- * @private
- * @param {OdooEvent} ev
- */
- _onOptionUpdate: function (ev) {
- // If multiple option names are given, we suppose it should not be
- // propagated to parent editor
- if (ev.data.optionNames) {
- ev.stopPropagation();
- var self = this;
- _.each(ev.data.optionNames, function (name) {
- var option = self.styles[name];
- if (option) {
- option.notify(ev.data.name, ev.data.data);
- }
- });
- }
- // If one option name is given, we suppose it should be handle by the
- // first parent editor which can do it
- if (ev.data.optionName) {
- var option = this.styles[ev.data.optionName];
- if (option) {
- ev.stopPropagation();
- option.notify(ev.data.name, ev.data.data);
- }
- }
- },
- /**
- * Called when the 'parent' button is clicked.
- *
- * @private
- * @param {Event} ev
- */
- _onParentButtonClick: function (ev) {
- ev.preventDefault();
- this.trigger_up('go_to_parent', {
- $snippet: this.$target,
- });
- },
- /**
- * Called when the 'remove' button is clicked.
- *
- * @private
- * @param {Event} ev
- */
- _onRemoveClick: function (ev) {
- ev.preventDefault();
- this.trigger_up('request_history_undo_record', {$target: this.$target});
- this.removeSnippet();
- },
-});
-
-/**
- * Management of drag&drop menu and snippet related behaviors in the page.
- */
-var SnippetsMenu = Widget.extend({
- id: 'oe_snippets',
- activeSnippets: [],
- custom_events: {
- activate_insertion_zones: '_onActivateInsertionZones',
- call_for_each_child_snippet: '_onCallForEachChildSnippet',
- deactivate_snippet: '_onDeactivateSnippet',
- drag_and_drop_stop: '_onDragAndDropStop',
- go_to_parent: '_onGoToParent',
- remove_snippet: '_onRemoveSnippet',
- snippet_removed: '_onSnippetRemoved',
- },
-
- /**
- * @constructor
- */
- init: function (parent, $editable) {
- this._super.apply(this, arguments);
-
- this.$editable = $editable;
- this.$activeSnippet = false;
- this.snippetEditors = [];
- },
- /**
- * @override
- */
- start: function () {
- var self = this;
- var defs = [this._super.apply(this, arguments)];
- var $document = $(document);
- var $window = $(window);
-
- // Fetch snippet templates and compute it
- var url = this._getSnippetURL();
- defs.push(this._rpc({route: url}).then(function (html) {
- return self._computeSnippetTemplates(html);
- }));
-
- // Prepare snippets editor environment
- this.$snippetEditorArea = $('<div/>', {
- id: 'oe_manipulators',
- }).insertAfter(this.$el);
-
- // Active snippet editor on click in the page
- var lastClickedElement;
- $document.on('click.snippets_menu', '*', function (ev) {
- var srcElement = ev.srcElement || (ev.originalEvent && (ev.originalEvent.originalTarget || ev.originalEvent.target) || ev.target);
- if (lastClickedElement === srcElement || !srcElement) {
- return;
- }
- lastClickedElement = srcElement;
- _.defer(function () {
- lastClickedElement = false;
- });
-
- var $target = $(srcElement);
- if ($target.closest('.oe_overlay, .note-popover').length) {
- return;
- }
- self._activateSnippet($target);
- });
-
- core.bus.on('deactivate_snippet', this, this._onDeactivateSnippet);
- core.bus.on('snippet_editor_clean_for_save', this, this._onCleanForSaveDemand);
-
- // Some summernote customization
- var _isNotBreakable = $.summernote.core.dom.isNotBreakable;
- $.summernote.core.dom.isNotBreakable = function (node) {
- return _isNotBreakable(node) || $(node).is('div') || globalSelector.is($(node));
- };
-
- // Adapt overlay covering when the window is resized / content changes
- var debouncedCoverUpdate = _.debounce(function () {
- self._updateCurrentSnippetEditorOverlay();
- }, 200);
- $window.on('resize.snippets_menu', debouncedCoverUpdate);
- $window.on('content_changed.snippets_menu', debouncedCoverUpdate);
-
- // On keydown add a class on the active overlay to hide it and show it
- // again when the mouse moves
- $document.on('keydown.snippets_menu', function () {
- if (self.$activeSnippet && self.$activeSnippet.data('snippet-editor')) {
- self.$activeSnippet.data('snippet-editor').$el.addClass('o_keypress');
- }
- });
- $document.on('mousemove.snippets_menu', function () {
- if (self.$activeSnippet && self.$activeSnippet.data('snippet-editor')) {
- self.$activeSnippet.data('snippet-editor').$el.removeClass('o_keypress');
- }
- });
-
- // Auto-selects text elements with a specific class and remove this
- // on text changes
- $document.on('click.snippets_menu', '.o_default_snippet_text', function (ev) {
- $(ev.target).selectContent();
- });
- $document.on('keyup.snippets_menu', function () {
- var r = $.summernote.core.range.create();
- $(r && r.sc).closest('.o_default_snippet_text').removeClass('o_default_snippet_text');
- });
-
- return $.when.apply($, defs).then(function () {
- // Trigger a resize event once entering edit mode as the snippets
- // menu will take part of the screen width (delayed because of
- // animation). (TODO wait for real animation end)
- setTimeout(function () {
- $window.trigger('resize');
- }, 1000);
- });
- },
- /**
- * @override
- */
- destroy: function () {
- this._super.apply(this, arguments);
- this.$snippetEditorArea.remove();
- $(window).off('.snippets_menu');
- $(document).off('.snippets_menu');
- core.bus.off('deactivate_snippet', this, this._onDeactivateSnippet);
- core.bus.off('snippet_editor_clean_for_save', this, this._onCleanForSaveDemand);
- },
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Prepares the page so that it may be saved:
- * - Asks the snippet editors to clean their associated snippet
- * - Remove the 'contentEditable' attributes
- */
- cleanForSave: function () {
- this.trigger_up('ready_to_clean_for_save');
- this._destroyEditors();
-
- this.$editable.find('[contentEditable]')
- .removeAttr('contentEditable')
- .removeProp('contentEditable');
-
- this.$editable.find('.o_we_selected_image')
- .removeClass('o_we_selected_image');
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Creates drop zones in the DOM (locations where snippets may be dropped).
- * Those locations are determined thanks to the two types of given DOM.
- *
- * @private
- * @param {jQuery} [$selectorSiblings]
- * elements which must have siblings drop zones
- * @param {jQuery} [$selectorChildren]
- * elements which must have child drop zones between each of existing
- * child
- */
- _activateInsertionZones: function ($selectorSiblings, $selectorChildren) {
- var zone_template = $('<div/>', {
- class: 'oe_drop_zone oe_insert',
- });
-
- if ($selectorChildren) {
- $selectorChildren.each(function () {
- var $zone = $(this);
- var css = window.getComputedStyle(this);
- var parentCss = window.getComputedStyle($zone.parent()[0]);
- var float = css.float || css.cssFloat;
- var parentDisplay = parentCss.display;
- var parentFlex = parentCss.flexDirection;
- var $drop = zone_template.clone();
-
- $zone.append($drop);
- var node = $drop[0].previousSibling;
- var test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === 'BR'));
- if (test) {
- $drop.addClass('oe_vertical').css({
- height: parseInt(window.getComputedStyle($zone[0]).lineHeight),
- float: 'none',
- display: 'inline-block',
- });
- } else if (float === 'left' || float === 'right' || (parentDisplay === 'flex' && parentFlex === 'row')) {
- $drop.addClass('oe_vertical').css('height', Math.max(Math.min($zone.outerHeight(), $zone.children().last().outerHeight()), 30));
- }
-
- $drop = $drop.clone();
-
- $zone.prepend($drop);
- node = $drop[0].nextSibling;
- test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === 'BR'));
- if (test) {
- $drop.addClass('oe_vertical').css({
- height: parseInt(window.getComputedStyle($zone[0]).lineHeight),
- float: 'none',
- display: 'inline-block'
- });
- } else if (float === 'left' || float === 'right' || (parentDisplay === 'flex' && parentFlex === 'row')) {
- $drop.addClass('oe_vertical').css('height', Math.max(Math.min($zone.outerHeight(), $zone.children().first().outerHeight()), 30));
- }
- if (test) {
- $drop.css({'float': 'none', 'display': 'inline-block'});
- }
- });
-
- // add children near drop zone
- $selectorSiblings = $(_.uniq(($selectorSiblings || $()).add($selectorChildren.children()).get()));
- }
-
- if ($selectorSiblings) {
- $selectorSiblings.filter(':not(.oe_drop_zone):not(.oe_drop_clone)').each(function () {
- var $zone = $(this);
- var $drop;
- var css = window.getComputedStyle(this);
- var parentCss = window.getComputedStyle($zone.parent()[0]);
- var float = css.float || css.cssFloat;
- var parentDisplay = parentCss.display;
- var parentFlex = parentCss.flexDirection;
-
- if ($zone.prev('.oe_drop_zone:visible').length === 0) {
- $drop = zone_template.clone();
- if (float === 'left' || float === 'right' || (parentDisplay === 'flex' && parentFlex === 'row')) {
- $drop.addClass('oe_vertical').css('height', Math.max(Math.min($zone.outerHeight(), $zone.prev().outerHeight() || Infinity), 30));
- }
- $zone.before($drop);
- }
- if ($zone.next('.oe_drop_zone:visible').length === 0) {
- $drop = zone_template.clone();
- if (float === 'left' || float === 'right' || (parentDisplay === 'flex' && parentFlex === 'row')) {
- $drop.addClass('oe_vertical').css('height', Math.max(Math.min($zone.outerHeight(), $zone.next().outerHeight() || Infinity), 30));
- }
- $zone.after($drop);
- }
- });
- }
-
- var count;
- var $zones;
- do {
- count = 0;
- $zones = this.$editable.find('.oe_drop_zone > .oe_drop_zone').remove(); // no recursive zones
- count += $zones.length;
- $zones.remove();
- } while (count > 0);
-
- // Cleaning consecutive zone and up zones placed between floating or
- // inline elements. We do not like these kind of zones.
- $zones = this.$editable.find('.oe_drop_zone:not(.oe_vertical)');
- $zones.each(function () {
- var zone = $(this);
- var prev = zone.prev();
- var next = zone.next();
- // remove consecutive zone
- if (prev.is('.oe_drop_zone') || next.is('.oe_drop_zone')) {
- zone.remove();
- return;
- }
- var float_prev = prev.css('float') || 'none';
- var float_next = next.css('float') || 'none';
- var disp_prev = prev.css('display') || null;
- var disp_next = next.css('display') || null;
- if ((float_prev === 'left' || float_prev === 'right')
- && (float_next === 'left' || float_next === 'right')) {
- zone.remove();
- } else if (disp_prev !== null && disp_next !== null
- && disp_prev !== 'block' && disp_next !== 'block') {
- zone.remove();
- }
- });
- },
- /**
- * Disable the overlay editor of the active snippet and activate the new one
- * if given.
- * Note 1: if the snippet editor associated to the given snippet is not
- * created yet, this method will create it.
- * Note 2: if the given DOM element is not a snippet (no editor option), the
- * first parent which is one is used instead.
- *
- * @param {jQuery|false} $snippet
- * The DOM element whose editor need to be enabled. Only disable the
- * current one if false is given.
- * @returns {Deferred<SnippetEditor>}
- * (might be async when an editor must be created)
- */
- _activateSnippet: function ($snippet) {
- if ($snippet) {
- if (!globalSelector.is($snippet)) {
- $snippet = globalSelector.closest($snippet);
- }
- if (this.$activeSnippet && this.$activeSnippet[0] === $snippet[0]) {
- return $.when($snippet.data('snippet-editor'));
- }
- }
- var editor = null;
- if (this.$activeSnippet) {
- editor = this.$activeSnippet.data('snippet-editor');
- if (editor) {
- editor.toggleFocus(false);
- }
- this.$activeSnippet = false;
- }
- if ($snippet && $snippet.length) {
- var self = this;
- return this._createSnippetEditor($snippet).then(function (editor) {
- self.$activeSnippet = $snippet;
- editor.toggleFocus(true);
- return editor;
- });
- }
- return $.when(editor);
- },
- /**
- * @private
- */
- _destroyEditors: function () {
- _.each(this.snippetEditors, function (snippetEditor) {
- snippetEditor.destroy();
- });
- },
- /**
- * Updates the cover dimensions of the current snippet editor.
- *
- * @private
- */
- _updateCurrentSnippetEditorOverlay: function () {
- if (this.$activeSnippet && this.$activeSnippet.data('snippet-editor')) {
- this.$activeSnippet.data('snippet-editor').cover();
- }
- },
- /**
- * Calls a given callback 'on' the given snippet and all its child ones if
- * any (DOM element with options).
- *
- * Note: the method creates the snippet editors if they do not exist yet.
- *
- * @private
- * @param {jQuery} $snippet
- * @param {function} callback
- * Given two arguments: the snippet editor associated to the snippet
- * being managed and the DOM element of this snippet.
- * @returns {Deferred} (might be async if snippet editors need to be created
- * and/or the callback is async)
- */
- _callForEachChildSnippet: function ($snippet, callback) {
- var self = this;
- var defs = _.map($snippet.add(globalSelector.all($snippet)), function (el) {
- var $snippet = $(el);
- return self._createSnippetEditor($snippet).then(function (editor) {
- if (editor) {
- return callback.call(self, editor, $snippet);
- }
- });
- });
- return $.when.apply($, defs);
- },
- /**
- * Creates and returns a set of helper functions which can help finding
- * snippets in the DOM which match some parameters (typically parameters
- * given by a snippet option). The functions are:
- *
- * - `is`: to determine if a given DOM is a snippet that matches the
- * parameters
- *
- * - `closest`: find closest parent (or itself) of a given DOM which is a
- * snippet that matches the parameters
- *
- * - `all`: find all snippets in the DOM that match the parameters
- *
- * See implementation for function details.
- *
- * @private
- * @param {string} include
- * jQuery selector that DOM elements must match to be considered as
- * potential snippet.
- * @param {string} exclude
- * jQuery selector that DOM elements must *not* match the be
- * considered as potential snippet.
- * @param {string|false} target
- * jQuery selector that at least one child of a DOM element must
- * match to that DOM element be considered as a potential snippet.
- * @param {boolean} noCheck
- * true if DOM elements which are technically not in an editable
- * environment may be considered.
- * @param {boolean} isChildren
- * when the DOM elements must be in an editable environment to be
- * considered (@see noCheck), this is true if the DOM elements'
- * parent must also be in an editable environment to be considered.
- */
- _computeSelectorFunctions : function (include, exclude, target, noCheck, isChildren) {
- var self = this;
-
- // Convert the selector for elements to include into a list
- var selectorList = _.compact(include.split(/\s*,\s*/));
-
- // Convert the selector for elements to exclude into a list
- var excludeList = _.compact(exclude.split(/\s*,\s*/));
- excludeList.push('.o_snippet_not_selectable');
-
- // Prepare the condition that will be added to each subselector for
- // elements to include: 'not the elements to exclude and only the
- // editable ones if needed'
- var selectorConditions = _.map(excludeList, function (exc) {
- return ':not(' + exc + ')';
- }).join('');
- if (target) {
- selectorConditions += ':has(' + target + ')';
- }
- if (!noCheck) {
- selectorConditions = ':o_editable' + selectorConditions;
- }
-
- // (Re)join the subselectors
- var selector =_.map(selectorList, function (s) {
- return s + selectorConditions;
- }).join(', ');
-
- // Prepare the functions
- var functions = {
- is: function ($from) {
- return $from.is(selector);
- },
- };
- if (noCheck) {
- functions.closest = function ($from, parentNode) {
- return $from.closest(selector, parentNode);
- };
- functions.all = function ($from) {
- return $from ? dom.cssFind($from, selector) : $(selector);
- };
- } else {
- functions.closest = function ($from, parentNode) {
- var parents = self.$editable.get();
- return $from.closest(selector, parentNode).filter(function () {
- var node = this;
- while (node.parentNode) {
- if (parents.indexOf(node)!==-1) {
- return true;
- }
- node = node.parentNode;
- }
- return false;
- });
- };
- functions.all = isChildren ? function ($from) {
- return dom.cssFind($from || self.$editable, selector);
- } : function ($from) {
- $from = $from || self.$editable;
- return $from.filter(selector).add(dom.cssFind($from, selector));
- };
- }
- return functions;
- },
- /**
- * Processes the given snippet template to register snippet options, creates
- * draggable thumbnail, etc.
- *
- * @private
- * @param {string} html
- */
- _computeSnippetTemplates: function (html) {
- var self = this;
- var $html = $(html);
- var $scroll = $html.siblings('#o_scroll');
-
- $html.find('[data-oe-type="snippet"]').each(function () {
- $(this).children()
- .attr('data-oe-type', 'snippet')
- .attr('data-oe-thumbnail', $(this).data('oe-thumbnail'));
- });
-
- this.templateOptions = [];
- var selectors = [];
- var $styles = $html.find('[data-selector]');
- $styles.each(function () {
- var $style = $(this);
- var selector = $style.data('selector');
- var exclude = $style.data('exclude') || '';
- var target = $style.data('target');
- var noCheck = $style.data('no-check');
- var option_id = $style.data('js');
- var option = {
- 'option': option_id,
- 'base_selector': selector,
- 'base_exclude': exclude,
- 'base_target': target,
- 'selector': self._computeSelectorFunctions(selector, exclude, target, noCheck),
- '$el': $style,
- 'drop-near': $style.data('drop-near') && self._computeSelectorFunctions($style.data('drop-near'), '', false, noCheck, true),
- 'drop-in': $style.data('drop-in') && self._computeSelectorFunctions($style.data('drop-in'), '', false, noCheck),
- 'data': $style.data(),
- };
- self.templateOptions.push(option);
- selectors.push(option.selector);
- });
- $styles.addClass('d-none');
-
- globalSelector.closest = function ($from) {
- var $temp;
- var $target;
- for (var i = 0, len = selectors.length ; i < len ; i++) {
- $temp = selectors[i].closest($from, $target && $target[0]);
- if ($temp.length) {
- $target = $temp;
- }
- }
- return $target || $();
- };
- globalSelector.all = function ($from) {
- var $target = $();
- for (var i = 0, len = selectors.length ; i < len ; i++) {
- $target = $target.add(selectors[i].all($from));
- }
- return $target;
- };
- globalSelector.is = function ($from) {
- for (var i = 0, len = selectors.length ; i < len ; i++) {
- if (selectors[i].is($from)) {
- return true;
- }
- }
- return false;
- };
-
- this.$snippets = $scroll.find('.o_panel_body').children()
- .addClass('oe_snippet')
- .each(function () {
- var $snippet = $(this);
- var name = $snippet.attr('name');
- var $sbody = $snippet.children(':not(.oe_snippet_thumbnail)').addClass('oe_snippet_body');
-
- // Associate in-page snippets to their name
- if ($sbody.length) {
- var snippetClasses = $sbody.attr('class').match(/s_[^ ]+/g);
- if (snippetClasses && snippetClasses.length) {
- snippetClasses = '.' + snippetClasses.join('.');
- }
- $(snippetClasses).data('name', name);
- $sbody.data('name', name);
- }
-
- // Create the thumbnail
- if ($snippet.find('.oe_snippet_thumbnail').length) {
- return; // Compatibility with elements which do not use 't-snippet'
- }
- var $thumbnail = $(_.str.sprintf(
- '<div class="oe_snippet_thumbnail">'+
- '<div class="oe_snippet_thumbnail_img" style="background-image: url(%s);"/>'+
- '<span class="oe_snippet_thumbnail_title">%s</span>'+
- '</div>',
- $snippet.find('[data-oe-thumbnail]').data('oeThumbnail'),
- name
- ));
- $snippet.prepend($thumbnail);
-
- // Create the install button (t-install feature) if necessary
- var moduleID = $snippet.data('moduleId');
- if (moduleID) {
- $snippet.addClass('o_snippet_install');
- var $installBtn = $('<a/>', {
- class: 'btn btn-primary o_install_btn',
- target: '_blank',
- href: '/web#id=' + moduleID + '&view_type=form&model=ir.module.module&action=base.open_module_tree',
- text: _t("Install"),
- });
- $thumbnail.append($installBtn);
- }
- })
- .not('[data-module-id]');
-
- // Hide scroll if no snippets defined
- if (!this.$snippets.length) {
- this.$el.detach();
- }
- $('body').toggleClass('editor_has_snippets', this.$snippets.length > 0);
-
- // Register the text nodes that needs to be auto-selected on click
- this._registerDefaultTexts();
-
- // Remove branding from template
- _.each($html.find('[data-oe-model], [data-oe-type]'), function (el) {
- for (var k = 0 ; k < el.attributes.length ; k++) {
- if (el.attributes[k].name.indexOf('data-oe-') === 0) {
- $(el).removeAttr(el.attributes[k].name);
- k--;
- }
- }
- });
-
- // Force non editable part to contentEditable=false
- $html.find('.o_not_editable').attr('contentEditable', false);
-
- // Add the computed template and make elements draggable
- this.$el.html($html);
- this._makeSnippetDraggable(this.$snippets);
- this._disableUndroppableSnippets();
- },
- /**
- * Creates a snippet editor to associated to the given snippet. If the given
- * snippet already has a linked snippet editor, the function only returns
- * that one.
- * The function also instantiates a snippet editor for all snippet parents
- * as a snippet editor must be able to display the parent snippet options.
- *
- * @private
- * @param {jQuery} $snippet
- * @returns {Deferred<SnippetEditor>}
- */
- _createSnippetEditor: function ($snippet) {
- var self = this;
- var snippetEditor = $snippet.data('snippet-editor');
- if (snippetEditor) {
- return $.when(snippetEditor);
- }
-
- var def;
- var $parent = globalSelector.closest($snippet.parent());
- if ($parent.length) {
- def = this._createSnippetEditor($parent);
- }
-
- return $.when(def).then(function (parentEditor) {
- snippetEditor = new SnippetEditor(parentEditor || self, $snippet, self.templateOptions);
- self.snippetEditors.push(snippetEditor);
- return snippetEditor.appendTo(self.$snippetEditorArea);
- }).then(function () {
- return snippetEditor;
- });
- },
- /**
- * There may be no location where some snippets might be dropped. This mades
- * them appear disabled in the menu.
- *
- * @todo make them undraggable
- * @private
- */
- _disableUndroppableSnippets: function () {
- var self = this;
- var cache = {};
- this.$snippets.each(function () {
- var $snippet = $(this);
- var $snippet_body = $snippet.find('.oe_snippet_body');
-
- var check = false;
- _.each(self.templateOptions, function (option, k) {
- if (check || !($snippet_body.is(option.base_selector) && !$snippet_body.is(option.base_exclude))) return;
-
- cache[k] = cache[k] || {
- 'drop-near': option['drop-near'] ? option['drop-near'].all().length : 0,
- 'drop-in': option['drop-in'] ? option['drop-in'].all().length : 0
- };
- check = (cache[k]['drop-near'] || cache[k]['drop-in']);
- });
-
- $snippet.toggleClass('o_disabled', !check);
- });
- },
- /**
- * Returns the URL where to find the snippets template. This URL might have
- * been set in the global 'snippetsURL' variable, otherwise this function
- * returns a default one.
- *
- * @private
- * @returns {string}
- */
- _getSnippetURL: function () {
- return odoo.snippetsURL || '/web_editor/snippets';
- },
- /**
- * Make given snippets be draggable/droppable thanks to their thumbnail.
- *
- * @private
- * @param {jQuery} $snippets
- */
- _makeSnippetDraggable: function ($snippets) {
- var self = this;
- var $tumb = $snippets.find('.oe_snippet_thumbnail_img:first');
- var left = $tumb.outerWidth()/2;
- var top = $tumb.outerHeight()/2;
- var $toInsert, dropped, $snippet;
-
- $snippets.draggable({
- greedy: true,
- helper: 'clone',
- zIndex: '1000',
- appendTo: 'body',
- cursor: 'move',
- handle: '.oe_snippet_thumbnail',
- distance: 30,
- cursorAt: {
- left: left,
- top: top,
- },
- start: function () {
- dropped = false;
- $snippet = $(this);
- var $base_body = $snippet.find('.oe_snippet_body');
- var $selectorSiblings = $();
- var $selectorChildren = $();
- var temp = self.templateOptions;
- for (var k in temp) {
- if ($base_body.is(temp[k].base_selector) && !$base_body.is(temp[k].base_exclude)) {
- if (temp[k]['drop-near']) {
- if (!$selectorSiblings) $selectorSiblings = temp[k]['drop-near'].all();
- else $selectorSiblings = $selectorSiblings.add(temp[k]['drop-near'].all());
- }
- if (temp[k]['drop-in']) {
- if (!$selectorChildren) $selectorChildren = temp[k]['drop-in'].all();
- else $selectorChildren = $selectorChildren.add(temp[k]['drop-in'].all());
- }
- }
- }
-
- $toInsert = $base_body.clone().data('name', $base_body.data('name'));
-
- if (!$selectorSiblings.length && !$selectorChildren.length) {
- console.warn($snippet.find('.oe_snippet_thumbnail_title').text() + " have not insert action: data-drop-near or data-drop-in");
- return;
- }
-
- self._activateSnippet(false);
- self._activateInsertionZones($selectorSiblings, $selectorChildren);
-
- $('.oe_drop_zone').droppable({
- over: function () {
- if (!dropped) {
- dropped = true;
- $(this).first().after($toInsert).addClass('d-none');
- }
- },
- out: function () {
- var prev = $toInsert.prev();
- if (this === prev[0]) {
- dropped = false;
- $toInsert.detach();
- $(this).removeClass('d-none');
- }
- },
- });
- },
- stop: function (ev, ui) {
- $toInsert.removeClass('oe_snippet_body');
-
- if (!dropped && ui.position.top > 3 && ui.position.left + 50 > self.$el.outerWidth()) {
- var $el = $.nearest({x: ui.position.left, y: ui.position.top}, '.oe_drop_zone').first();
- if ($el.length) {
- $el.after($toInsert);
- dropped = true;
- }
- }
-
- self.$editable.find('.oe_drop_zone').droppable('destroy').remove();
-
- if (dropped) {
- var prev = $toInsert.first()[0].previousSibling;
- var next = $toInsert.last()[0].nextSibling;
-
- if (prev) {
- $toInsert.detach();
- self.trigger_up('request_history_undo_record', {$target: $(prev)});
- $toInsert.insertAfter(prev);
- } else if (next) {
- $toInsert.detach();
- self.trigger_up('request_history_undo_record', {$target: $(next)});
- $toInsert.insertBefore(next);
- } else {
- var $parent = $toInsert.parent();
- $toInsert.detach();
- self.trigger_up('request_history_undo_record', {$target: $parent});
- $parent.prepend($toInsert);
- }
-
- $toInsert.closest('.o_editable').trigger('content_changed');
-
- var $target = $toInsert;
-
- _.defer(function () {
- self.trigger_up('snippet_dropped', {$target: $target});
- self._disableUndroppableSnippets();
-
- self._callForEachChildSnippet($target, function (editor, $snippet) {
- _.defer(function () {
- editor.buildSnippet();
- });
- }).then(function () {
- $target.closest('.o_editable').trigger('content_changed');
- self._activateSnippet($target);
- });
- });
- } else {
- $toInsert.remove();
- }
- },
- });
- },
- /**
- * Adds the 'o_default_snippet_text' class on nodes which contain only
- * non-empty text nodes. Those nodes are then auto-selected by the editor
- * when they are clicked.
- *
- * @private
- * @param {jQuery} [$in] - the element in which to search, default to the
- * snippet bodies in the menu
- */
- _registerDefaultTexts: function ($in) {
- if ($in === undefined) {
- $in = this.$snippets.find('.oe_snippet_body');
- }
-
- $in.find('*').addBack()
- .contents()
- .filter(function () {
- return this.nodeType === 3 && this.textContent.match(/\S/);
- }).parent().addClass('o_default_snippet_text');
- },
-
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
-
- /**
- * Called when a child editor asks for insertion zones to be enabled.
- *
- * @private
- * @param {OdooEvent} ev
- */
- _onActivateInsertionZones: function (ev) {
- this._activateInsertionZones(ev.data.$selectorSiblings, ev.data.$selectorChildren);
- },
- /**
- * Called when a child editor asks to operate some operation on all child
- * snippet of a DOM element.
- *
- * @private
- * @param {OdooEvent} ev
- */
- _onCallForEachChildSnippet: function (ev) {
- this._callForEachChildSnippet(ev.data.$snippet, ev.data.callback);
- },
- /**
- * Called when asked to clean the DOM for save. Should technically not be
- * used but used by tests.
- *
- * @private
- */
- _onCleanForSaveDemand: function (ev) {
- this.cleanForSave();
- },
- /**
- * Called when a child editor asks to deactivate the current snippet
- * overlay.
- *
- * @private
- */
- _onDeactivateSnippet: function () {
- this._activateSnippet(false);
- },
- /**
- * Called when a snippet has moved in the page.
- *
- * @private
- * @param {OdooEvent} ev
- */
- _onDragAndDropStop: function (ev) {
- this._destroyEditors();
- this._activateSnippet(ev.data.$snippet);
- },
- /**
- * Called when a snippet editor asked to disable itself and to enable its
- * parent instead.
- *
- * @private
- * @param {OdooEvent} ev
- */
- _onGoToParent: function (ev) {
- ev.stopPropagation();
- this._activateSnippet(ev.data.$snippet.parent());
- },
- /**
- * @private
- * @param {OdooEvent} ev
- */
- _onRemoveSnippet: function (ev) {
- ev.stopPropagation();
- this._createSnippetEditor(ev.data.$snippet).then(function (editor) {
- editor.removeSnippet();
- });
- },
- /**
- * Called when a snippet is removed -> checks if there is draggable snippets
- * to enable/disable as the DOM changed.
- *
- * @private
- */
- _onSnippetRemoved: function () {
- this._disableUndroppableSnippets();
- },
-});
-
-return {
- Class: SnippetsMenu,
- Editor: SnippetEditor,
- globalSelector: globalSelector,
-};
-});
diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js
deleted file mode 100644
index 97d3777b95f..00000000000
--- a/addons/web_editor/static/src/js/editor/snippets.options.js
+++ /dev/null
@@ -1,1317 +0,0 @@
-odoo.define('web_editor.snippets.options', function (require) {
-'use strict';
-
-var core = require('web.core');
-var Dialog = require('web.Dialog');
-var Widget = require('web.Widget');
-var summernoteCustomColors = require('web_editor.rte.summernote_custom_colors');
-var weWidgets = require('web_editor.widget');
-
-var qweb = core.qweb;
-var _t = core._t;
-
-/**
- * Handles a set of options for one snippet. The registry returned by this
- * module contains the names of the specialized SnippetOption which can be
- * referenced thanks to the data-js key in the web_editor options template.
- */
-var SnippetOption = Widget.extend({
- events: {
- 'mouseenter': '_onLinkEnter',
- 'mouseenter .dropdown-item': '_onLinkEnter',
- 'click': '_onLinkClick',
- 'mouseleave': '_onMouseleave',
- 'mouseleave .dropdown-item': '_onMouseleave',
- },
- /**
- * When editing a snippet, its options are shown alongside the ones of its
- * parent snippets. The parent options are only shown if the following flag
- * is set to false (default).
- */
- preventChildPropagation: false,
-
- /**
- * The option `$el` is supposed to be the associated DOM element in the
- * options dropdown. The option controls another DOM element: the snippet it
- * customizes, which can be found at `$target`. Access to the whole edition
- * overlay is possible with `$overlay` (this is not recommended though).
- *
- * @constructor
- */
- init: function (parent, $target, $overlay, data) {
- this._super.apply(this, arguments);
- this.$target = $target;
- this.$overlay = $overlay;
- this.data = data;
- this.__methodNames = [];
- },
- /**
- * Called when the option is initialized (i.e. the parent edition overlay is
- * shown for the first time).
- *
- * @override
- */
- start: function () {
- this._setActive();
- return this._super.apply(this, arguments);
- },
- /**
- * Called when the parent edition overlay is covering the associated snippet
- * (the first time, this follows the call to the @see start method).
- *
- * @abstract
- */
- onFocus : function () {},
- /**
- * Called when the parent edition overlay is covering the associated snippet
- * for the first time, when it is a new snippet dropped from the d&d snippet
- * menu. Note: this is called after the start and onFocus methods.
- *
- * @abstract
- */
- onBuilt: function () {},
- /**
- * Called when the parent edition overlay is removed from the associated
- * snippet (another snippet enters edition for example).
- *
- * @abstract
- */
- onBlur : function () {},
- /**
- * Called when the associated snippet is the result of the cloning of
- * another snippet (so `this.$target` is a cloned element).
- *
- * @abstract
- * @param {Object} options
- * @param {boolean} options.isCurrent
- * true if the associated snippet is a clone of the main element that
- * was cloned (so not a clone of a child of this main element that
- * was cloned)
- */
- onClone: function (options) {},
- /**
- * Called when the associated snippet is moved to another DOM location.
- *
- * @abstract
- */
- onMove: function () {},
- /**
- * Called when the associated snippet is about to be removed from the DOM.
- *
- * @abstract
- */
- onRemove: function () {},
- /**
- * Called when the template which contains the associated snippet is about
- * to be saved.
- *
- * @abstract
- */
- cleanForSave: function () {},
-
- //--------------------------------------------------------------------------
- // Options
- //--------------------------------------------------------------------------
-
- /**
- * Default option method which allows to select one and only one class in
- * the option classes set and set it on the associated snippet. The common
- * case is having a subdropdown with each item having a `data-select-class`
- * value allowing to choose the associated class.
- *
- * @param {boolean|string} previewMode
- * - truthy if the option is enabled for preview or if leaving it (in
- * that second case, the value is 'reset')
- * - false if the option should be activated for good
- * @param {*} value - the class to activate ($opt.data('selectClass'))
- * @param {jQuery} $opt - the related DOMElement option
- */
- selectClass: function (previewMode, value, $opt) {
- var $group = $opt && $opt.closest('.dropdown-submenu');
- if (!$group || !$group.length) {
- $group = this.$el;
- }
- var $lis = $group.find('[data-select-class]').addBack('[data-select-class]');
- var classes = $lis.map(function () {return $(this).data('selectClass');}).get().join(' ');
-
- this.$target.removeClass(classes);
- if (value) {
- this.$target.addClass(value);
- }
- },
- /**
- * Default option method which allows to select one or multiple classes in
- * the option classes set and set it on the associated snippet. The common
- * case is having a subdropdown with each item having a `data-toggle-class`
- * value allowing to toggle the associated class.
- *
- * @see this.selectClass
- */
- toggleClass: function (previewMode, value, $opt) {
- var $lis = this.$el.find('[data-toggle-class]').addBack('[data-toggle-class]');
- var classes = $lis.map(function () {return $(this).data('toggleClass');}).get().join(' ');
- var activeClasses = $lis.filter('.active, :has(.active)').map(function () {return $(this).data('toggleClass');}).get().join(' ');
-
- this.$target.removeClass(classes).addClass(activeClasses);
- if (value && previewMode !== 'reset') {
- this.$target.toggleClass(value);
- }
- },
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Override the helper method to search inside the $target element instead
- * of the dropdown item element.
- *
- * @override
- */
- $: function () {
- return this.$target.find.apply(this.$target, arguments);
- },
- /**
- * Sometimes, options may need to notify other options, even in parent
- * editors. This can be done thanks to the 'option_update' event, which
- * will then be handled by this function.
- *
- * @param {string} name - an identifier for a type of update
- * @param {*} data
- */
- notify: function (name, data) {
- if (name === 'target') {
- this.setTarget(data);
- }
- },
- /**
- * Sometimes, an option is binded on an element but should in fact apply on
- * another one. For example, elements which contain slides: we want all the
- * per-slide options to be in the main menu of the whole snippet. This
- * function allows to set the option's target.
- *
- * @param {jQuery} $target - the new target element
- */
- setTarget: function ($target) {
- this.$target = $target;
- this._setActive();
- this.$target.trigger('snippet-option-change', [this]);
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Reactivate the options that were activated before previews.
- */
- _reset: function () {
- var self = this;
- var $actives = this.$el.find('.active').addBack('.active');
- _.each($actives, function (activeElement) {
- var $activeElement = $(activeElement);
- self.__methodNames = _.without.apply(_, [self.__methodNames].concat(_.keys($activeElement.data())));
- self._select('reset', $activeElement);
- });
- _.each(this.__methodNames, function (methodName) {
- self[methodName]('reset');
- });
- this.__methodNames = [];
- },
- /**
- * Activates the option associated to the given DOM element.
- *
- * @private
- * @param {boolean|string} previewMode
- * - truthy if the option is enabled for preview or if leaving it (in
- * that second case, the value is 'reset')
- * - false if the option should be activated for good
- * @param {jQuery} $opt - the related DOMElement option
- */
- _select: function (previewMode, $opt) {
- var self = this;
-
- // Options can say they respond to strong choice
- if (previewMode && ($opt.data('noPreview') || $opt.parent().data('noPreview'))) {
- return;
- }
- // If it is not preview mode, the user selected the option for good
- // (so record the action)
- if (!previewMode) {
- this._reset();
- this.trigger_up('request_history_undo_record', {$target: this.$target});
- }
-
- // Search for methods (data-...) (i.e. data-toggle-class) on the
- // selected (sub)option and its parents
- var el = $opt[0];
- var methods = [];
- do {
- methods.push([el, el.dataset]);
- el = el.parentNode;
- } while (this.$el.parent().has(el).length);
-
- // Call the found method in the right order (parents -> child)
- _.each(methods.reverse(), function (data) {
- var $el = $(data[0]);
- var methods = data[1];
-
- _.each(methods, function (value, methodName) {
- if (self[methodName]) {
- if (previewMode === true) {
- self.__methodNames.push(methodName);
- }
- self[methodName](previewMode, value, $el);
- }
- });
- });
- this.__methodNames = _.uniq(this.__methodNames);
-
- if (!previewMode) {
- this._setActive();
- }
- },
- /**
- * Tweaks the option DOM elements to show the selected value according to
- * the state of the $target the option customizes.
- *
- * @todo should be extendable in a more easy way
- * @private
- */
- _setActive: function () {
- var self = this;
- this.$el.find('[data-toggle-class]')
- .addBack('[data-toggle-class]')
- .removeClass('active')
- .filter(function () {
- var className = $(this).data('toggleClass');
- return !className || self.$target.hasClass(className);
- })
- .addClass('active');
-
- _processSelectClassElements(this.$el);
- _.each(this.$el.find('.dropdown-menu'), function (group) {
- _processSelectClassElements($(group).children());
- });
-
- function _processSelectClassElements($elements) {
- var maxNbClasses = -1;
- $elements.filter('[data-select-class]')
- .removeClass('active')
- .filter(function () {
- var className = $(this).data('selectClass');
- var nbClasses = className ? className.split(' ').length : 0;
- if (nbClasses >= maxNbClasses && (!className || self.$target.hasClass(className))) {
- maxNbClasses = nbClasses;
- return true;
- }
- return false;
- })
- .last()
- .addClass('active');
- }
- },
-
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
-
- /**
- * Called when a option link is entered -> activates the related option in
- * preview mode.
- *
- * @private
- * @param {Event} ev
- */
- _onLinkEnter: function (ev) {
- var $opt = $(ev.target).closest('.dropdown-item');
- if (!$opt.length) {
- return;
- }
-
- var $dsmenu = $opt.parent('.dropdown-submenu');
- if ($dsmenu.length) {
- var $menu = $dsmenu.children('.dropdown-menu'); // FIXME
- if ($menu.length) {
- var menuRightPosition = $dsmenu.offset().left + $dsmenu.outerWidth() + $menu.outerWidth();
- $menu.toggleClass('o_open_to_left', menuRightPosition > $(window).outerWidth());
- }
- }
-
- if (!$opt.is(':hasData')) {
- return;
- }
- this.__click = false;
- this._select(true, $opt);
- this.$target.trigger('snippet-option-preview', [this]);
- },
- /**
- * Called when an option link is clicked -> activates the related option.
- *
- * @private
- * @param {Event} ev
- */
- _onLinkClick: function (ev) {
- var $opt = $(ev.target).closest('.dropdown-item');
- if (!$opt.length || !$opt.is(':hasData')) {
- return;
- }
-
- ev.preventDefault();
- this.__click = true;
- this._select(false, $opt);
- this.$target.trigger('snippet-option-change', [this]);
- },
- /**
- * Called when an option link/menu is left -> reactivate the options that
- * were activated before previews.
- *
- * @private
- */
- _onMouseleave: function () {
- if (this.__click) {
- return;
- }
- this._reset();
- },
-});
-
-//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
-
-/**
- * The registry object contains the list of available options.
- */
-var registry = {};
-
-registry.sizing = SnippetOption.extend({
- preventChildPropagation: true,
-
- /**
- * @override
- */
- start: function () {
- var self = this;
- var def = this._super.apply(this, arguments);
-
- this.$handles = this.$overlay.find('.o_handle');
- var resizeValues = this._getSize();
- _.each(resizeValues, function (value, key) {
- self.$handles.filter('.' + key).toggleClass('readonly', !value);
- });
-
- this.$handles.on('mousedown', function (ev) {
- ev.preventDefault();
-
- var $handle = $(ev.currentTarget);
-
- var compass = false;
- var XY = false;
- if ($handle.hasClass('n')) {
- compass = 'n';
- XY = 'Y';
- } else if ($handle.hasClass('s')) {
- compass = 's';
- XY = 'Y';
- } else if ($handle.hasClass('e')) {
- compass = 'e';
- XY = 'X';
- } else if ($handle.hasClass('w')) {
- compass = 'w';
- XY = 'X';
- }
-
- var resize = resizeValues[compass];
- if (!resize) {
- return;
- }
-
- var current = 0;
- var cssProperty = resize[2];
- var cssPropertyValue = parseInt(self.$target.css(cssProperty));
- _.each(resize[0], function (val, key) {
- if (self.$target.hasClass(val)) {
- current = key;
- } else if (resize[1][key] === cssPropertyValue) {
- current = key;
- }
- });
- var begin = current;
- var beginClass = self.$target.attr('class');
- var regClass = new RegExp('\\s*' + resize[0][begin].replace(/[-]*[0-9]+/, '[-]*[0-9]+'), 'g');
-
- var cursor = $handle.css('cursor') + '-important';
- var $body = $(document.body);
- $body.addClass(cursor);
-
- var xy = ev['page' + XY];
- var body_mousemove = function (ev) {
- ev.preventDefault();
-
- var dd = ev['page' + XY] - xy + resize[1][begin];
- var next = current + (current+1 === resize[1].length ? 0 : 1);
- var prev = current ? (current-1) : 0;
-
- var change = false;
- if (dd > (2*resize[1][next] + resize[1][current])/3) {
- self.$target.attr('class', (self.$target.attr('class')||'').replace(regClass, ''));
- self.$target.addClass(resize[0][next]);
- current = next;
- change = true;
- }
- if (prev !== current && dd < (2*resize[1][prev] + resize[1][current])/3) {
- self.$target.attr('class', (self.$target.attr('class')||'').replace(regClass, ''));
- self.$target.addClass(resize[0][prev]);
- current = prev;
- change = true;
- }
-
- if (change) {
- self._onResize(compass, beginClass, current);
- self.trigger_up('cover_update');
- $handle.addClass('o_active');
- }
- };
- var body_mouseup = function () {
- $body.off('mousemove', body_mousemove);
- $body.off('mouseup', body_mouseup);
- $body.removeClass(cursor);
- $handle.removeClass('o_active');
-
- // Highlights the previews for a while
- var $handlers = self.$overlay.find('.o_handle');
- $handlers.addClass('o_active').delay(300).queue(function () {
- $handlers.removeClass('o_active').dequeue();
- });
-
- if (begin === current) {
- return;
- }
- setTimeout(function () {
- self.trigger_up('request_history_undo_record', {
- $target: self.$target,
- event: 'resize_' + XY,
- });
- }, 0);
- };
- $body.on('mousemove', body_mousemove);
- $body.on('mouseup', body_mouseup);
- });
-
- return def;
- },
- /**
- * @override
- */
- onFocus: function () {
- this._onResize();
- },
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * @override
- */
- setTarget: function () {
- this._super.apply(this, arguments);
- this._onResize();
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Returns an object mapping one or several cardinal direction (n, e, s, w)
- * to an Array containing:
- * 1) A list of classes to toggle when using this cardinal direction
- * 2) A list of values these classes are supposed to set on a given CSS prop
- * 3) The mentioned CSS prop
- *
- * Note: this object must also be saved in this.grid before being returned.
- *
- * @abstract
- * @private
- * @returns {Object}
- */
- _getSize: function () {},
- /**
- * Called when the snippet is being resized and its classes changes.
- *
- * @private
- * @param {string} [compass] - resize direction ('n', 's', 'e' or 'w')
- * @param {string} [beginClass] - attributes class at the beginning
- * @param {integer} [current] - current increment in this.grid
- */
- _onResize: function (compass, beginClass, current) {
- var self = this;
-
- // Adapt the resize handles according to the classes and dimensions
- var resizeValues = this._getSize();
- var $handles = this.$overlay.find('.o_handle');
- _.each(resizeValues, function (resizeValue, direction) {
- var classes = resizeValue[0];
- var values = resizeValue[1];
- var cssProperty = resizeValue[2];
-
- var $handle = $handles.filter('.' + direction);
-
- var current = 0;
- var cssPropertyValue = parseInt(self.$target.css(cssProperty));
- _.each(classes, function (className, key) {
- if (self.$target.hasClass(className)) {
- current = key;
- } else if (values[key] === cssPropertyValue) {
- current = key;
- }
- });
-
- $handle.toggleClass('o_handle_start', current === 0);
- $handle.toggleClass('o_handle_end', current === classes.length - 1);
- });
-
- // Adapt the handles to fit the left, top and bottom sizes
- var ml = this.$target.css('margin-left');
- this.$overlay.find('.o_handle.w').css({
- width: ml,
- left: '-' + ml,
- });
- _.each(this.$overlay.find(".o_handle.n, .o_handle.s"), function (handle) {
- var $handle = $(handle);
- var direction = $handle.hasClass('n') ? 'top': 'bottom';
- $handle.height(self.$target.css('padding-' + direction));
- });
- },
-});
-
-/**
- * Handles the edition of padding-top and padding-bottom.
- */
-registry.sizing_y = registry.sizing.extend({
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * @override
- */
- _getSize: function () {
- var nClass = 'pt';
- var nProp = 'padding-top';
- var sClass = 'pb';
- var sProp = 'padding-bottom';
- if (this.$target.is('hr')) {
- nClass = 'mt';
- nProp = 'margin-top';
- sClass = 'mb';
- sProp = 'margin-bottom';
- }
-
- var grid = [];
- for (var i = 0 ; i <= 256/8 ; i++) {
- grid.push(i * 8);
- }
- grid.splice(1, 0, 4);
- this.grid = {
- n: [_.map(grid, function (v) { return nClass + v; }), grid, nProp],
- s: [_.map(grid, function (v) { return sClass + v; }), grid, sProp],
- };
- return this.grid;
- },
-});
-
-/**
- * Handles the edition of snippet's background color classes.
- */
-registry.colorpicker = SnippetOption.extend({
- xmlDependencies: ['/web_editor/static/src/xml/snippets.xml'],
- events: _.extend({}, SnippetOption.prototype.events || {}, {
- 'click .colorpicker button': '_onColorButtonClick',
- 'mouseenter .colorpicker button': '_onColorButtonEnter',
- 'mouseleave .colorpicker button': '_onColorButtonLeave',
- 'click .note-color-reset': '_onColorResetButtonClick',
- }),
- colorPrefix: 'bg-',
-
- /**
- * @override
- */
- start: function () {
- var self = this;
- var res = this._super.apply(this, arguments);
-
- if (this.data.colorPrefix) {
- this.colorPrefix = this.data.colorPrefix;
- }
-
- if (!this.$el.find('.colorpicker').length) {
- // TODO remove old UI's code that survived
- var $pt = $(qweb.render('web_editor.snippet.option.colorpicker'));
- var $clpicker = $(qweb.render('web_editor.colorpicker'));
-
- _.each($clpicker.find('.o_colorpicker_section'), function (elem) {
- $(elem).prepend("<div class='text-muted mt8'>" + elem.dataset.display + "</div>");
- });
-
- // Retrieve excluded palettes list
- var excluded = [];
- if (this.data.paletteExclude) {
- excluded = this.data.paletteExclude.replace(/ /g, '').split(',');
- }
- // Apply a custom title if specified
- if (this.data.paletteTitle) {
- $pt.find('.note-palette-title').text(this.data.paletteTitle);
- }
-
- // Remove excluded palettes
- _.each(excluded, function (exc) {
- $clpicker.find('[data-name="' + exc + '"]').remove();
- });
-
- // Add common colors to palettes if not excluded
- if (!('common' in excluded)) {
- var $commonColorSection = $clpicker.find('[data-name="common"]');
- _.each(summernoteCustomColors, function (colorRow, i) {
- var $div = $('<div/>', {class: 'clearfix'}).appendTo($commonColorSection);
- if (i === 0) {
- // Ignore the summernote gray palette and use ours
- return;
- }
- _.each(colorRow, function (color) {
- $div.append('<button class="o_custom_color" style="background-color: ' + color + '" />');
- });
- });
- }
-
- $pt.find('.o_colorpicker_section_tabs').append($clpicker);
- this.$el.find('.dropdown-menu').append($pt);
- }
-
- var classes = [];
- this.$el.find('.colorpicker button').each(function () {
- var $color = $(this);
- var color = $color.data('color');
- if (!color) {
- return;
- }
-
- $color.addClass('bg-' + color);
- var className = self.colorPrefix + color;
- if (self.$target.hasClass(className)) {
- $color.addClass('selected');
- }
- classes.push(className);
- });
- this.classes = classes.join(' ');
-
- return res;
- },
-
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
-
- /**
- * Called when a color button is clicked -> confirm the preview.
- *
- * @private
- * @param {Event} ev
- */
- _onColorButtonClick: function (ev) {
- this.$el.find('.colorpicker button.selected').removeClass('selected');
- $(ev.currentTarget).addClass('selected');
- this.$target.closest('.o_editable').trigger('content_changed');
- this.$target.trigger('background-color-event', false);
- },
- /**
- * Called when a color button is entered -> preview the background color.
- *
- * @private
- * @param {Event} ev
- */
- _onColorButtonEnter: function (ev) {
- this.$target.removeClass(this.classes);
- var color = $(ev.currentTarget).data('color');
- if (color) {
- this.$target.addClass(this.colorPrefix + color);
- } else if ($(ev.target).hasClass('o_custom_color')) {
- this.$target
- .removeClass(this.classes)
- .css('background-color', ev.currentTarget.style.backgroundColor);
- }
- this.$target.trigger('background-color-event', true);
- },
- /**
- * Called when a color button is left -> cancel the preview.
- *
- * @private
- * @param {Event} ev
- */
- _onColorButtonLeave: function (ev) {
- this.$target.removeClass(this.classes);
- this.$target.css('background-color', '');
- var $selected = this.$el.find('.colorpicker button.selected');
- if ($selected.length) {
- if ($selected.data('color')) {
- this.$target.addClass(this.colorPrefix + $selected.data('color'));
- } else {
- this.$target.css('background-color', $selected.css('background-color'));
- }
- }
- this.$target.trigger('background-color-event', 'reset');
- },
- /**
- * Called when the color reset button is clicked -> remove all background
- * color classes.
- *
- * @private
- */
- _onColorResetButtonClick: function () {
- this.$target.removeClass(this.classes).css('background-color', '');
- this.$target.trigger('content_changed');
- this.$el.find('.colorpicker button.selected').removeClass('selected');
- },
-});
-
-/**
- * Handles the edition of snippet's background image.
- */
-registry.background = SnippetOption.extend({
- /**
- * @override
- */
- start: function () {
- var res = this._super.apply(this, arguments);
- this.bindBackgroundEvents();
- this.__customImageSrc = this._getSrcFromCssValue();
- return res;
- },
-
- //--------------------------------------------------------------------------
- // Options
- //--------------------------------------------------------------------------
-
- /**
- * Handles a background change.
- *
- * @see this.selectClass for parameters
- */
- background: function (previewMode, value, $opt) {
- if (previewMode === 'reset' && value === undefined) {
- // No background has been selected and we want to reset back to the
- // original custom image
- this._setCustomBackground(this.__customImageSrc);
- return;
- }
-
- if (value && value.length) {
- this.$target.css('background-image', 'url(\'' + value + '\')');
- this.$target.removeClass('oe_custom_bg').addClass('oe_img_bg');
- } else {
- this.$target.css('background-image', '');
- this.$target.removeClass('oe_img_bg oe_custom_bg');
- }
- },
- /**
- * @override
- */
- selectClass: function (previewMode, value, $opt) {
- this.background(previewMode, '', $opt);
- this._super(previewMode, value ? (value + ' oe_img_bg') : value, $opt);
- },
- /**
- * Opens a media dialog to add a custom background image.
- *
- * @see this.selectClass for parameters
- */
- chooseImage: function (previewMode, value, $opt) {
- // Put fake image in the DOM, edit it and use it as background-image
- var $image = $('<img/>', {class: 'd-none', src: value}).appendTo(this.$target);
-
- var $editable = this.$target.closest('.o_editable');
- var _editor = new weWidgets.MediaDialog(this, {
- onlyImages: true,
- firstFilters: ['background'],
- res_model: $editable.data('oe-model'),
- res_id: $editable.data('oe-id'),
- }, null, $image[0]).open();
-
- _editor.on('save', this, function () {
- this._setCustomBackground($image.attr('src'));
- });
- _editor.on('closed', this, function () {
- $image.remove();
- });
- },
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Attaches events so that when a background-color is set, the background
- * image is removed.
- */
- bindBackgroundEvents: function () {
- if (this.$target.is('.parallax, .s_parallax_bg')) {
- return;
- }
- this.$target.off('.background-option')
- .on('background-color-event.background-option', (function (e, previewMode) {
- e.stopPropagation();
- if (e.currentTarget !== e.target) return;
- if (previewMode === false) {
- this.__customImageSrc = undefined;
- }
- this.background(previewMode);
- }).bind(this));
- },
- /**
- * @override
- */
- setTarget: function () {
- this._super.apply(this, arguments);
- // TODO should be automatic for all options as equal to the start method
- this.bindBackgroundEvents();
- this.__customImageSrc = this._getSrcFromCssValue();
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Returns the src value from a css value related to a background image
- * (e.g. "url('blabla')" => "blabla" / "none" => "").
- *
- * @private
- * @param {string} value
- * @returns {string}
- */
- _getSrcFromCssValue: function (value) {
- if (value === undefined) {
- value = this.$target.css('background-image');
- }
- return value.replace(/url\(['"]*|['"]*\)|^none$/g, '');
- },
- /**
- * @override
- */
- _setActive: function () {
- this._super.apply(this, arguments);
-
- var src = this._getSrcFromCssValue();
- this.$el.find('[data-background]')
- .removeClass('active')
- .filter(function () {
- var bgOption = $(this).data('background');
- return (bgOption === '' && src === '' || bgOption !== '' && src.indexOf(bgOption) >= 0);
- })
- .addClass('active');
-
- this.$el.find('[data-choose-image]').toggleClass('active', this.$target.hasClass('oe_custom_bg'));
- },
- /**
- * Sets the given value as custom background image.
- *
- * @private
- * @param {string} value
- */
- _setCustomBackground: function (value) {
- this.__customImageSrc = value;
- this.background(false, this.__customImageSrc);
- this.$target.addClass('oe_custom_bg');
- this._setActive();
- this.$target.trigger('snippet-option-change', [this]);
- },
-});
-
-/**
- * Handles the edition of snippet's background image position.
- */
-registry.background_position = SnippetOption.extend({
- xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
-
- /**
- * @override
- */
- start: function () {
- this._super.apply(this, arguments);
- var self = this;
- this.$target.on('snippet-option-change', function () {
- self.onFocus();
- });
- },
- /**
- * @override
- */
- onFocus: function () {
- this.$el.toggleClass('d-none', this.$target.css('background-image') === 'none');
- },
-
- //--------------------------------------------------------------------------
- // Options
- //--------------------------------------------------------------------------
-
- /**
- * Opens a Dialog to edit the snippet's backgroung image position.
- *
- * @see this.selectClass for parameters
- */
- backgroundPosition: function (previewMode, value, $opt) {
- var self = this;
-
- this.previous_state = [this.$target.attr('class'), this.$target.css('background-size'), this.$target.css('background-position')];
-
- this.bg_pos = self.$target.css('background-position').split(' ');
- this.bg_siz = self.$target.css('background-size').split(' ');
-
- this.modal = new Dialog(null, {
- title: _t("Background Image Sizing"),
- $content: $(qweb.render('web_editor.dialog.background_position')),
- buttons: [
- {text: _t("Ok"), classes: 'btn-primary', close: true, click: _.bind(this._saveChanges, this)},
- {text: _t("Discard"), close: true, click: _.bind(this._discardChanges, this)},
- ],
- }).open();
-
- this.modal.opened().then(function () {
- // Fetch data form $target
- var value = ((self.$target.hasClass('o_bg_img_opt_contain'))? 'contain' : ((self.$target.hasClass('o_bg_img_opt_custom'))? 'custom' : 'cover'));
- self.modal.$('> label > input[value=' + value + ']').prop('checked', true);
-
- if (self.$target.hasClass('o_bg_img_opt_repeat')) {
- self.modal.$('#o_bg_img_opt_contain_repeat').prop('checked', true);
- self.modal.$('#o_bg_img_opt_custom_repeat').val('o_bg_img_opt_repeat');
- } else if (self.$target.hasClass('o_bg_img_opt_repeat_x')) {
- self.modal.$('#o_bg_img_opt_custom_repeat').val('o_bg_img_opt_repeat_x');
- } else if (self.$target.hasClass('o_bg_img_opt_repeat_y')) {
- self.modal.$('#o_bg_img_opt_custom_repeat').val('o_bg_img_opt_repeat_y');
- }
-
- if (self.bg_pos.length > 1) {
- self.bg_pos = {
- x: self.bg_pos[0],
- y: self.bg_pos[1],
- };
- self.modal.$('#o_bg_img_opt_custom_pos_x').val(self.bg_pos.x.replace('%', ''));
- self.modal.$('#o_bg_img_opt_custom_pos_y').val(self.bg_pos.y.replace('%', ''));
- }
- if (self.bg_siz.length > 1) {
- self.modal.$('#o_bg_img_opt_custom_size_x').val(self.bg_siz[0].replace('%', ''));
- self.modal.$('#o_bg_img_opt_custom_size_y').val(self.bg_siz[1].replace('%', ''));
- }
-
- // Focus Point
- self.$focus = self.modal.$('.o_focus_point');
- self._updatePosInformation();
-
- var img_url = /\(['"]?([^'"]+)['"]?\)/g.exec(self.$target.css('background-image'));
- img_url = (img_url && img_url[1]) || '';
- var $img = $('<img/>', {class: 'img img-fluid', src: img_url});
- $img.on('load', function () {
- self._bindImageEvents($img);
- });
- $img.prependTo(self.modal.$('.o_bg_img_opt_object'));
-
- // Bind events
- self.modal.$el.on('change', '> label > input', function (e) {
- self.modal.$('> .o_bg_img_opt').addClass('o_hidden')
- .filter('[data-value=' + e.target.value + ']')
- .removeClass('o_hidden');
- });
- self.modal.$el.on('change', 'input, select', function (e) {
- self._saveChanges();
- });
- self.modal.$('> label > input:checked').trigger('change');
- });
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Bind events on the given image so that the users can adapt the focus
- * point.
- *
- * @private
- * @param {jQuery} $img
- */
- _bindImageEvents: function ($img) {
- var self = this;
-
- var mousedown = false;
- $img.on('mousedown', function (e) {
- mousedown = true;
- });
- $img.on('mousemove', function (e) {
- if (mousedown) {
- _update(e);
- }
- });
- $img.on('mouseup', function (e) {
- self.$focus.addClass('o_with_transition');
- _update(e);
- setTimeout(function () {
- self.$focus.removeClass('o_with_transition');
- }, 200);
- mousedown = false;
- });
-
- function _update(e) {
- var posX = e.pageX - $(e.target).offset().left;
- var posY = e.pageY - $(e.target).offset().top;
- self.bg_pos = {
- x: clipValue(posX/$img.width()*100).toFixed(2) + '%',
- y: clipValue(posY/$img.height()*100).toFixed(2) + '%',
- };
- self._updatePosInformation();
- self._saveChanges();
-
- function clipValue(value) {
- return Math.max(0, Math.min(value, 100));
- }
- }
- },
- /**
- * Removes all option-related classes and style on the target element.
- *
- * @private
- */
- _clean: function () {
- this.$target.removeClass('o_bg_img_opt_contain o_bg_img_opt_custom o_bg_img_opt_repeat o_bg_img_opt_repeat_x o_bg_img_opt_repeat_y')
- .css({
- 'background-size': '',
- 'background-position': '',
- });
- },
- /**
- * Restores the target style before last edition made with the option.
- *
- * @private
- */
- _discardChanges: function () {
- this._clean();
- if (this.previous_state) {
- this.$target.addClass(this.previous_state[0]).css({
- 'background-size': this.previous_state[1],
- 'background-position': this.previous_state[2],
- });
- }
- },
- /**
- * Updates the visual representation of the chosen background position.
- *
- * @private
- */
- _updatePosInformation: function () {
- this.modal.$('.o_bg_img_opt_ui_info .o_x').text(this.bg_pos.x);
- this.modal.$('.o_bg_img_opt_ui_info .o_y').text(this.bg_pos.y);
- this.$focus.css({
- left: this.bg_pos.x,
- top: this.bg_pos.y,
- });
- },
- /**
- * Updates the target element to match the chosen options.
- *
- * @private
- */
- _saveChanges: function () {
- this._clean();
-
- var bg_img_size = this.modal.$('> :not(label):not(.o_hidden)').data('value') || 'cover';
- switch (bg_img_size) {
- case 'cover':
- this.$target.css('background-position', this.bg_pos.x + ' ' + this.bg_pos.y);
- break;
- case 'contain':
- this.$target.addClass('o_bg_img_opt_contain');
- this.$target.toggleClass('o_bg_img_opt_repeat', this.modal.$('#o_bg_img_opt_contain_repeat').prop('checked'));
- break;
- case 'custom':
- this.$target.addClass('o_bg_img_opt_custom');
- var sizeX = this.modal.$('#o_bg_img_opt_custom_size_x').val();
- var sizeY = this.modal.$('#o_bg_img_opt_custom_size_y').val();
- var posX = this.modal.$('#o_bg_img_opt_custom_pos_x').val();
- var posY = this.modal.$('#o_bg_img_opt_custom_pos_y').val();
- this.$target.addClass(this.modal.$('#o_bg_img_opt_custom_repeat').val())
- .css({
- 'background-size': ((sizeX)? sizeX + '%' : 'auto') + ' ' + ((sizeY)? sizeY + '%' : 'auto'),
- 'background-position': ((posX)? posX + '%' : 'auto') + ' ' + ((posY)? posY + '%' : 'auto'),
- });
- break;
- }
- },
-});
-
-/**
- * Allows to replace a text value with the name of a database record.
- * @todo replace this mechanism with real backend m2o field ?
- */
-registry.many2one = SnippetOption.extend({
- xmlDependencies: ['/web_editor/static/src/xml/snippets.xml'],
- /**
- * @override
- */
- start: function () {
- var self = this;
-
- this.Model = this.$target.data('oe-many2one-model');
- this.ID = +this.$target.data('oe-many2one-id');
-
- // create search button and bind search bar
- this.$btn = $(qweb.render('web_editor.many2one.button'))
- .insertAfter(this.$overlay.find('.oe_options'));
-
- this.$ul = this.$btn.find('ul');
- this.$search = this.$ul.find('li:first');
- this.$search.find('input').on('mousedown click mouseup keyup keydown', function (e) {
- e.stopPropagation();
- });
-
- // move menu item
- setTimeout(function () {
- if (self.$overlay.find('.oe_options').hasClass('d-none')) {
- self.$btn.css('height', '0').find('> a').addClass('d-none');
- self.$ul.show().css({
- 'top': '-24px', 'margin': '0', 'padding': '2px 0', 'position': 'relative'
- });
- } else {
- self.$btn.find('a').on('click', function (e) {
- self._clear();
- });
- }
- },0);
-
- // bind search input
- this.$search.find('input')
- .focus()
- .on('keyup', function (e) {
- self._findExisting($(this).val());
- });
-
- // bind result
- this.$ul.on('click', 'li:not(:first) a', function (e) {
- self._selectRecord($(e.currentTarget));
- });
-
- return this._super.apply(this, arguments);
- },
- /**
- * @override
- */
- onFocus: function () {
- this.$target.attr('contentEditable', 'false');
- this._clear();
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Removes the input value and suggestions.
- *
- * @private
- */
- _clear: function () {
- var self = this;
- this.$search.siblings().remove();
- self.$search.find('input').val('');
- setTimeout(function () {
- self.$search.find('input').focus();
- }, 0);
- },
- /**
- * Find existing record with the given name and suggest them.
- *
- * @private
- * @param {string} name
- * @returns {Deferred}
- */
- _findExisting: function (name) {
- var self = this;
- var domain = [];
- if (!name || !name.length) {
- self.$search.siblings().remove();
- return;
- }
- if (isNaN(+name)) {
- if (this.Model !== 'res.partner') {
- domain.push(['name', 'ilike', name]);
- } else {
- domain.push('|', ['name', 'ilike', name], ['email', 'ilike', name]);
- }
- } else {
- domain.push(['id', '=', name]);
- }
-
- return this._rpc({
- model: this.Model,
- method: 'search_read',
- args: [domain, this.Model === 'res.partner' ? ['name', 'display_name', 'city', 'country_id'] : ['name', 'display_name']],
- kwargs: {
- order: [{name: 'name', asc: false}],
- limit: 5,
- },
- }).then(function (result) {
- self.$search.siblings().remove();
- self.$search.after(qweb.render('web_editor.many2one.search',{contacts:result}));
- });
- },
- /**
- * Selects the given suggestion and displays it the proper way.
- *
- * @private
- * @param {jQuery} $li
- */
- _selectRecord: function ($li) {
- var self = this;
-
- this.ID = +$li.data('id');
- this.$target.attr('data-oe-many2one-id', this.ID).data('oe-many2one-id', this.ID);
-
- this.trigger_up('request_history_undo_record', {$target: this.$target});
- this.$target.trigger('content_changed');
-
- if (self.$target.data('oe-type') === 'contact') {
- $('[data-oe-contact-options]')
- .filter('[data-oe-model="'+self.$target.data('oe-model')+'"]')
- .filter('[data-oe-id="'+self.$target.data('oe-id')+'"]')
- .filter('[data-oe-field="'+self.$target.data('oe-field')+'"]')
- .filter('[data-oe-contact-options!="'+self.$target.data('oe-contact-options')+'"]')
- .add(self.$target)
- .attr('data-oe-many2one-id', self.ID).data('oe-many2one-id', self.ID)
- .each(function () {
- var $node = $(this);
- var options = $node.data('oe-contact-options');
- self._rpc({
- model: 'ir.qweb.field.contact',
- method: 'get_record_to_html',
- args: [[self.ID]],
- kwargs: {
- options: options,
- },
- }).then(function (html) {
- $node.html(html);
- });
- });
- } else {
- self.$target.html($li.data('name'));
- }
-
- _.defer(function () {
- self.trigger_up('deactivate_snippet');
- });
- }
-});
-
-return {
- Class: SnippetOption,
- registry: registry,
-};
-});
diff --git a/addons/web_editor/static/src/js/editor/summernote.js b/addons/web_editor/static/src/js/editor/summernote.js
deleted file mode 100644
index 36f5becce06..00000000000
--- a/addons/web_editor/static/src/js/editor/summernote.js
+++ /dev/null
@@ -1,2436 +0,0 @@
-odoo.define('web_editor.summernote', function (require) {
-'use strict';
-
-var core = require('web.core');
-require('summernote/summernote'); // wait that summernote is loaded
-
-var _t = core._t;
-
-// Summernote Lib (neek hack to make accessible: method and object)
-// var agent = $.summernote.core.agent;
-var dom = $.summernote.core.dom;
-var range = $.summernote.core.range;
-var list = $.summernote.core.list;
-var key = $.summernote.core.key;
-var eventHandler = $.summernote.eventHandler;
-var editor = eventHandler.modules.editor;
-var renderer = $.summernote.renderer;
-var options = $.summernote.options;
-
-// Browser-unify execCommand
-var oldJustify = {};
-_.each(['Left', 'Right', 'Full', 'Center'], function (align) {
- oldJustify[align] = editor['justify' + align];
- editor['justify' + align] = function ($editable, value) {
- // Before calling the standard function, check all elements which have
- // an 'align' attribute and mark them with their value
- var $align = $editable.find('[align]');
- _.each($align, function (el) {
- var $el = $(el);
- $el.data('__align', $el.attr('align'));
- });
-
- // Call the standard function
- oldJustify[align].apply(this, arguments);
-
- // Then:
-
- // Remove the text-align of elements which lost the 'align' attribute
- var $newAlign = $editable.find('[align]');
- $align.not($newAlign).css('text-align', '');
-
- // Transform the 'align' attribute into the 'text-align' css
- // property for elements which received the 'align' attribute or whose
- // 'align' attribute changed
- _.each($newAlign, function (el) {
- var $el = $(el);
-
- var oldAlignValue = $align.data('__align');
- var alignValue = $el.attr('align');
- if (oldAlignValue === alignValue) {
- // If the element already had an 'align' attribute and that it
- // did not changed, do nothing (compatibility)
- return;
- }
-
- $el.removeAttr('align');
- $el.css('text-align', alignValue);
-
- // Note the first step (removing the text-align of elemnts which
- // lost the 'align' attribute) is kinda the same as this one, but
- // this one handles the elements which have been edited with chrome
- // or with this new system
- $el.find('*').css('text-align', '');
- });
-
- // Unmark the elements
- $align.removeData('__align');
- };
-});
-
-
-// Add methods to summernote
-
-dom.hasContentAfter = function (node) {
- var next;
- if (dom.isEditable(node)) return;
- while (node.nextSibling) {
- next = node.nextSibling;
- if (next.tagName || dom.isVisibleText(next) || dom.isBR(next)) return next;
- node = next;
- }
-};
-dom.hasContentBefore = function (node) {
- var prev;
- if (dom.isEditable(node)) return;
- while (node.previousSibling) {
- prev = node.previousSibling;
- if (prev.tagName || dom.isVisibleText(prev) || dom.isBR(prev)) return prev;
- node = prev;
- }
-};
-dom.ancestorHaveNextSibling = function (node, pred) {
- pred = pred || dom.hasContentAfter;
- while (!dom.isEditable(node) && (!node.nextSibling || !pred(node))) { node = node.parentNode; }
- return node;
-};
-dom.ancestorHavePreviousSibling = function (node, pred) {
- pred = pred || dom.hasContentBefore;
- while (!dom.isEditable(node) && (!node.previousSibling || !pred(node))) { node = node.parentNode; }
- return node;
-};
-dom.nextElementSibling = function (node) {
- while (node) {
- node = node.nextSibling;
- if (node && node.tagName) {
- break;
- }
- }
- return node;
-};
-dom.previousElementSibling = function (node) {
- while (node) {
- node = node.previousSibling;
- if (node && node.tagName) {
- break;
- }
- }
- return node;
-};
-dom.lastChild = function (node) {
- while (node.lastChild) { node = node.lastChild; }
- return node;
-};
-dom.firstChild = function (node) {
- while (node.firstChild) { node = node.firstChild; }
- return node;
-};
-dom.lastElementChild = function (node, deep) {
- node = deep ? dom.lastChild(node) : node.lastChild;
- return !node || node.tagName ? node : dom.previousElementSibling(node);
-};
-dom.firstElementChild = function (node, deep) {
- node = deep ? dom.firstChild(node) : node.firstChild;
- return !node || node.tagName ? node : dom.nextElementSibling(node);
-};
-dom.isEqual = function (prev, cur) {
- if (prev.tagName !== cur.tagName) {
- return false;
- }
- if ((prev.attributes ? prev.attributes.length : 0) !== (cur.attributes ? cur.attributes.length : 0)) {
- return false;
- }
-
- function strip(text) {
- return text && text.replace(/^\s+|\s+$/g, '').replace(/\s+/g, ' ');
- }
- var att, att2;
- loop_prev:
- for (var a in prev.attributes) {
- att = prev.attributes[a];
- for (var b in cur.attributes) {
- att2 = cur.attributes[b];
- if (att.name === att2.name) {
- if (strip(att.value) !== strip(att2.value)) return false;
- continue loop_prev;
- }
- }
- return false;
- }
- return true;
-};
-dom.hasOnlyStyle = function (node) {
- for (var i = 0; i < node.attributes.length; i++) {
- var attr = node.attributes[i];
- if (attr.attributeName !== 'style') {
- return false;
- }
- }
- return true;
-};
-dom.hasProgrammaticStyle = function (node) {
- var styles = ["float", "display", "position", "top", "left", "right", "bottom"];
- for (var i = 0; i < node.style.length; i++) {
- var style = node.style[i];
- if (styles.indexOf(style) !== -1) {
- return true;
- }
- }
- return false;
-};
-dom.mergeFilter = function (prev, cur, parent) {
- // merge text nodes
- if (prev && (dom.isText(prev) || (['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'LI', 'P'].indexOf(prev.tagName) !== -1 && prev !== cur.parentNode)) && dom.isText(cur)) {
- return true;
- }
- if (prev && prev.tagName === "P" && dom.isText(cur)) {
- return true;
- }
- if (prev && dom.isText(cur) && !dom.isVisibleText(cur) && (dom.isText(prev) || dom.isVisibleText(prev))) {
- return true;
- }
- if (prev && !dom.isBR(prev) && dom.isEqual(prev, cur) &&
- ((prev.tagName && dom.getComputedStyle(prev).display === "inline" &&
- cur.tagName && dom.getComputedStyle(cur).display === "inline"))) {
- return true;
- }
- if (dom.isEqual(parent, cur) &&
- ((parent.tagName && dom.getComputedStyle(parent).display === "inline" &&
- cur.tagName && dom.getComputedStyle(cur).display === "inline"))) {
- return true;
- }
- if (parent && cur.tagName === "FONT" && (!cur.firstChild || (!cur.attributes.getNamedItem('style') && !cur.className.length))) {
- return true;
- }
- // On backspace, webkit browsers create a <span> with a bunch of
- // inline styles "remembering" where they come from.
- // chances are we had e.g.
- // <p>foo</p>
- // <p>bar</p>
- // merged the lines getting this in webkit
- // <p>foo<span>bar</span></p>
- if (parent && cur.tagName === "SPAN" && dom.hasOnlyStyle(cur) && !dom.hasProgrammaticStyle(cur)) {
- return true;
- }
-};
-dom.doMerge = function (prev, cur) {
- if (prev.tagName) {
- if (prev.childNodes.length && !prev.textContent.match(/\S/) && dom.firstElementChild(prev) && dom.isBR(dom.firstElementChild(prev))) {
- prev.removeChild(dom.firstElementChild(prev));
- }
- if (cur.tagName) {
- while (cur.firstChild) {
- prev.appendChild(cur.firstChild);
- }
- cur.parentNode.removeChild(cur);
- } else {
- prev.appendChild(cur);
- }
- } else {
- if (cur.tagName) {
- var deep = cur;
- while (deep.tagName && deep.firstChild) {deep = deep.firstChild;}
- prev.appendData(deep.textContent);
- cur.parentNode.removeChild(cur);
- } else {
- prev.appendData(cur.textContent);
- cur.parentNode.removeChild(cur);
- }
- }
-};
-dom.merge = function (node, begin, so, end, eo, mergeFilter, all) {
- mergeFilter = mergeFilter || dom.mergeFilter;
- var _merged = false;
- var add = all || false;
-
- if (!begin) {
- begin = node;
- while (begin.firstChild) {begin = begin.firstChild;}
- so = 0;
- } else if (begin.tagName && begin.childNodes[so]) {
- begin = begin.childNodes[so];
- so = 0;
- }
- if (!end) {
- end = node;
- while (end.lastChild) {end = end.lastChild;}
- eo = end.textContent.length-1;
- } else if (end.tagName && end.childNodes[so]) {
- end = end.childNodes[so];
- so = 0;
- }
-
- begin = dom.firstChild(begin);
- if (dom.isText(begin) && so > begin.textContent.length) {
- so = 0;
- }
- end = dom.firstChild(end);
- if (dom.isText(end) && eo > end.textContent.length) {
- eo = 0;
- }
-
- function __merge(node) {
- var merged = false;
- var prev;
- for (var k=0; k<node.childNodes.length; k++) {
- var cur = node.childNodes[k];
-
- if (cur === begin) {
- if (!all) add = true;
- }
-
- __merge(cur);
- dom.orderClass(dom.node(cur));
-
- if (!add || !cur) continue;
- if (cur === end) {
- if (!all) add = false;
- }
-
- // create the first prev value
- if (!prev) {
- if (mergeFilter.call(dom, prev, cur, node)) {
- prev = prev || cur.previousSibling;
- dom.moveTo(cur, cur.parentNode, cur);
- k--;
- } else {
- prev = cur;
- }
- continue;
- } else if (mergeFilter.call(dom, null, cur, node)) { // merge with parent
- prev = prev || cur.previousSibling;
- dom.moveTo(cur, cur.parentNode, cur);
- k--;
- continue;
- }
-
- // merge nodes
- if (mergeFilter.call(dom, prev, cur, node)) {
- var p = prev;
- var c = cur;
- // compute prev/end and offset
- if (prev.tagName) {
- if (cur.tagName) {
- if (cur === begin) begin = prev;
- if (cur === end) end = prev;
- }
- } else {
- if (cur.tagName) {
- var deep = cur;
- while (deep.tagName && deep.lastChild) {deep = deep.lastChild;}
- if (deep === begin) {
- so += prev.textContent.length;
- begin = prev;
- }
- if (deep === end) {
- eo += prev.textContent.length;
- end = prev;
- }
- } else {
- // merge text nodes
- if (cur === begin) {
- so += prev.textContent.length;
- begin = prev;
- }
- if (cur === end) {
- eo += prev.textContent.length;
- end = prev;
- }
- }
- }
-
- dom.doMerge(p, c);
-
- merged = true;
- k--;
- continue;
- }
-
- prev = cur;
- }
-
- // an other loop to merge the new shibbing nodes
- if (merged) {
- _merged = true;
- __merge(node);
- }
- }
- if (node) {
- __merge(node);
- }
-
- return {
- merged: _merged,
- sc: begin,
- ec: end,
- so: so,
- eo: eo
- };
-};
-dom.autoMerge = function (target, previous) {
- var node = dom.lastChild(target);
- var nodes = [];
- var temp;
-
- while (node) {
- nodes.push(node);
- temp = (previous ? dom.hasContentBefore(node) : dom.hasContentAfter(node));
- if (temp) {
- if (!dom.isText(node) && !dom.isMergable(node) && temp.tagName !== node.tagName) {
- nodes = [];
- }
- break;
- }
- node = node.parentNode;
- }
-
- while (nodes.length) {
- node = nodes.pop();
- if (node && (temp = (previous ? dom.hasContentBefore(node) : dom.hasContentAfter(node))) &&
- temp.tagName === node.tagName &&
- !dom.isText(node) &&
- dom.isMergable(node) &&
- !dom.isNotBreakable(node) && !dom.isNotBreakable(previous ? dom.previousElementSibling(node) : dom.nextElementSibling(node))) {
-
- if (previous) {
- dom.doMerge(temp, node);
- } else {
- dom.doMerge(node, temp);
- }
- }
- }
-};
-dom.removeSpace = function (node, begin, so, end, eo) {
- var removed = false;
- var add = node === begin;
-
- if (node === begin && begin === end && dom.isBR(node)) {
- return {
- removed: removed,
- sc: begin,
- ec: end,
- so: so,
- eo: eo
- };
- }
-
- (function __remove_space(node) {
- if (!node) return;
- var t_begin, t_end;
- for (var k=0; k<node.childNodes.length; k++) {
- var cur = node.childNodes[k];
-
- if (cur === begin) add = true;
-
- if (cur.tagName && cur.tagName !== "SCRIPT" && cur.tagName !== "STYLE" && dom.getComputedStyle(cur).whiteSpace !== "pre") {
- __remove_space(cur);
- }
-
- if (!add) continue;
- if (cur === end) add = false;
-
- // remove begin empty text node
- if (node.childNodes.length > 1 && dom.isText(cur) && !dom.isVisibleText(cur)) {
- removed = true;
- if (cur === begin) {
- t_begin = dom.hasContentBefore(dom.ancestorHavePreviousSibling(cur));
- if (t_begin) {
- so = 0;
- begin = dom.lastChild(t_begin);
- }
- }
- if (cur === end) {
- t_end = dom.hasContentAfter(dom.ancestorHaveNextSibling(cur));
- if (t_end) {
- eo = 1;
- end = dom.firstChild(t_end);
- if (dom.isText(end)) {
- eo = end.textContent.length;
- }
- }
- }
- cur.parentNode.removeChild(cur);
- begin = dom.lastChild(begin);
- end = dom.lastChild(end);
- k--;
- continue;
- }
-
- // convert HTML space
- if (dom.isText(cur)) {
- var text;
- var temp;
- var _temp;
- var exp1 = /[\t\n\r ]+/g;
- var exp2 = /(?!([ ]|\u00A0)|^)\u00A0(?!([ ]|\u00A0)|$)/g;
- if (cur === begin) {
- temp = cur.textContent.substr(0, so);
- _temp = temp.replace(exp1, ' ').replace(exp2, ' ');
- so -= temp.length - _temp.length;
- }
- if (cur === end) {
- temp = cur.textContent.substr(0, eo);
- _temp = temp.replace(exp1, ' ').replace(exp2, ' ');
- eo -= temp.length - _temp.length;
- }
- text = cur.textContent.replace(exp1, ' ').replace(exp2, ' ');
- removed = removed || cur.textContent.length !== text.length;
- cur.textContent = text;
- }
- }
- })(node);
-
- return {
- removed: removed,
- sc: begin,
- ec: end,
- so: !dom.isBR(begin) && so > 0 ? so : 0,
- eo: dom.isBR(end) ? 0 : eo
- };
-};
-dom.removeBetween = function (sc, so, ec, eo, towrite) {
- var text;
- if (ec.tagName) {
- if (ec.childNodes[eo]) {
- ec = ec.childNodes[eo];
- eo = 0;
- } else {
- ec = dom.lastChild(ec);
- eo = dom.nodeLength(ec);
- }
- }
- if (sc.tagName) {
- sc = sc.childNodes[so] || dom.firstChild(ec);
- so = 0;
- if (!dom.hasContentBefore(sc) && towrite) {
- sc.parentNode.insertBefore(document.createTextNode('\u00A0'), sc);
- }
- }
- if (!eo && sc !== ec) {
- ec = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(ec)) || ec);
- eo = ec.textContent.length;
- }
-
- var ancestor = dom.commonAncestor(sc.tagName ? sc.parentNode : sc, ec.tagName ? ec.parentNode : ec) || dom.ancestor(sc, dom.isEditable);
-
- if (!dom.isContentEditable(ancestor)) {
- return {
- sc: sc,
- so: so,
- ec: sc,
- eo: eo
- };
- }
-
- if (ancestor.tagName) {
- var ancestor_sc = sc;
- var ancestor_ec = ec;
- while (ancestor !== ancestor_sc && ancestor !== ancestor_sc.parentNode) { ancestor_sc = ancestor_sc.parentNode; }
- while (ancestor !== ancestor_ec && ancestor !== ancestor_ec.parentNode) { ancestor_ec = ancestor_ec.parentNode; }
-
-
- var node = dom.node(sc);
- if (!dom.isNotBreakable(node) && !dom.isVoid(sc)) {
- sc = dom.splitTree(ancestor_sc, {'node': sc, 'offset': so});
- }
- var before = dom.hasContentBefore(dom.ancestorHavePreviousSibling(sc));
-
- var after;
- if (ec.textContent.slice(eo, Infinity).match(/\S|\u00A0/)) {
- after = dom.splitTree(ancestor_ec, {'node': ec, 'offset': eo});
- } else {
- after = dom.hasContentAfter(dom.ancestorHaveNextSibling(ec));
- }
-
- var nodes = dom.listBetween(sc, ec);
-
- var ancestor_first_last = function (node) {
- return node === before || node === after;
- };
-
- for (var i=0; i<nodes.length; i++) {
- if (!dom.ancestor(nodes[i], ancestor_first_last) && !$.contains(nodes[i], before) && !$.contains(nodes[i], after) && !dom.isEditable(nodes[i])) {
- nodes[i].parentNode.removeChild(nodes[i]);
- }
- }
-
- if (dom.listAncestor(after).length <= dom.listAncestor(before).length) {
- sc = dom.lastChild(before || ancestor);
- so = dom.nodeLength(sc);
- } else {
- sc = dom.firstChild(after);
- so = 0;
- }
-
- if (dom.isVoid(node)) {
- // we don't need to append a br
- } else if (towrite && !node.firstChild && node.parentNode && !dom.isNotBreakable(node)) {
- var br = $("<br/>")[0];
- node.appendChild(sc);
- sc = br;
- so = 0;
- } else if (!ancestor.children.length && !ancestor.textContent.match(/\S|\u00A0/)) {
- sc = $("<br/>")[0];
- so = 0;
- $(ancestor).prepend(sc);
- } else if (dom.isText(sc)) {
- text = sc.textContent.replace(/[ \t\n\r]+$/, '\u00A0');
- so = Math.min(so, text.length);
- sc.textContent = text;
- }
- } else {
- text = ancestor.textContent;
- ancestor.textContent = text.slice(0, so) + text.slice(eo, Infinity).replace(/^[ \t\n\r]+/, '\u00A0');
- }
-
- eo = so;
- if (!dom.isBR(sc) && !dom.isVisibleText(sc) && !dom.isText(dom.hasContentBefore(sc)) && !dom.isText(dom.hasContentAfter(sc))) {
- ancestor = dom.node(sc);
- text = document.createTextNode('\u00A0');
- $(sc).before(text);
- sc = text;
- so = 0;
- eo = 1;
- }
-
- var parentNode = sc && sc.parentNode;
- if (parentNode && sc.tagName === 'BR') {
- sc = parentNode;
- ec = parentNode;
- }
-
- return {
- sc: sc,
- so: so,
- ec: sc,
- eo: eo
- };
-};
-dom.indent = 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;
-};
-dom.outdent = 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;
-};
-dom.scrollIntoViewIfNeeded = function (node) {
- node = dom.node(node);
-
- var $span;
- if (dom.isBR(node)) {
- $span = $('<span/>').text('\u00A0');
- $(node).after($span);
- node = $span[0];
- }
-
- if (node.scrollIntoViewIfNeeded) {
- node.scrollIntoViewIfNeeded(false);
- } else {
- var offsetParent = node.offsetParent;
- while (offsetParent) {
- var elY = 0;
- var elH = node.offsetHeight;
- var parent = node;
-
- while (offsetParent && parent) {
- elY += node.offsetTop;
-
- // get if a parent have a scrollbar
- parent = node.parentNode;
- while (parent !== offsetParent &&
- (parent.tagName === "BODY" || ["auto", "scroll"].indexOf(dom.getComputedStyle(parent).overflowY) === -1)) {
- parent = parent.parentNode;
- }
- node = parent;
-
- if (parent !== offsetParent) {
- elY -= parent.offsetTop;
- parent = null;
- }
-
- offsetParent = node.offsetParent;
- }
-
- if ((node.tagName === "BODY" || ["auto", "scroll"].indexOf(dom.getComputedStyle(node).overflowY) !== -1) &&
- (node.scrollTop + node.clientHeight) < (elY + elH)) {
- node.scrollTop = (elY + elH) - node.clientHeight;
- }
- }
- }
-
- if ($span) {
- $span.remove();
- }
-
- return;
-};
-dom.moveTo = function (node, target, before) {
- var nodes = [];
- while (node.firstChild) {
- nodes.push(node.firstChild);
- if (before) {
- target.insertBefore(node.firstChild, before);
- } else {
- target.appendChild(node.firstChild);
- }
- }
- node.parentNode.removeChild(node);
- return nodes;
-};
-dom.isMergable = function (node) {
- return node.tagName && "h1 h2 h3 h4 h5 h6 p b bold i u code sup strong small li a ul ol font".indexOf(node.tagName.toLowerCase()) !== -1;
-};
-dom.isSplitable = function (node) {
- return node.tagName && "h1 h2 h3 h4 h5 h6 p b bold i u code sup strong small li a font".indexOf(node.tagName.toLowerCase()) !== -1;
-};
-dom.isRemovableEmptyNode = function (node) {
- return "h1 h2 h3 h4 h5 h6 p b bold i u code sup strong small li a ul ol font span br".indexOf(node.tagName.toLowerCase()) !== -1;
-};
-dom.isForbiddenNode = function (node) {
- return node.tagName === "BR" || $(node).is(".fa, img");
-};
-/**
- * @todo 'so' and 'eo' were added as a bugfix and are not given everytime. They
- * however should be as the function may be wrong without them (for example,
- * when asking the list between an element and its parent, as there is no path
- * from the beginning of the former to the beginning of the later).
- */
-dom.listBetween = function (sc, ec, so, eo) {
- var nodes = [];
- var ancestor = dom.commonAncestor(sc, ec);
- dom.walkPoint({'node': sc, 'offset': so || 0}, {'node': ec, 'offset': eo || 0}, function (point) {
- if (ancestor !== point.node || ancestor === sc || ancestor === ec) {
- nodes.push(point.node);
- }
- });
- return list.unique(nodes);
-};
-dom.isNotBreakable = function (node) {
- // avoid triple click => crappy dom
- return !dom.isText(node) && !dom.isBR(dom.firstChild(node)) && dom.isVoid(dom.firstChild(node));
-};
-dom.isContentEditable = function (node) {
- return $(node).closest('[contenteditable]').prop('contenteditable') === 'true';
-};
-dom.isContentEditableFalse = function (node) {
- return $(node).closest('[contenteditable]').prop('contenteditable') === 'false';
-};
-dom.isFont = function (node) {
- var nodeName = node && node.nodeName.toUpperCase();
- return node && (nodeName === "FONT" ||
- (nodeName === "SPAN" && (
- node.className.match(/(^|\s)fa(\s|$)/i) ||
- node.className.match(/(^|\s)(text|bg)-/i) ||
- (node.attributes.style && node.attributes.style.value.match(/(^|\s)(color|background-color|font-size):/i)))) );
-};
-dom.isVisibleText = function (textNode) {
- return !!textNode.textContent.match(/\S|\u00A0/);
-};
-var old_isVisiblePoint = dom.isVisiblePoint;
-dom.isVisiblePoint = function (point) {
- return point.node.nodeType !== 8 && old_isVisiblePoint.apply(this, arguments);
-};
-dom.orderStyle = function (node) {
- var style = node.getAttribute('style');
- if (!style) return null;
- style = style.replace(/[\s\n\r]+/, ' ').replace(/^ ?;? ?| ?;? ?$/g, '').replace(/ ?; ?/g, ';');
- if (!style.length) {
- node.removeAttribute("style");
- return null;
- }
- style = style.split(";");
- style.sort();
- style = style.join("; ")+";";
- node.setAttribute('style', style);
- return style;
-};
-dom.orderClass = function (node) {
- var className = node.getAttribute && node.getAttribute('class');
- if (!className) return null;
- className = className.replace(/[\s\n\r]+/, ' ').replace(/^ | $/g, '').replace(/ +/g, ' ');
- if (!className.length) {
- node.removeAttribute("class");
- return null;
- }
- className = className.split(" ");
- className.sort();
- className = className.join(" ");
- node.setAttribute('class', className);
- return className;
-};
-dom.node = function (node) {
- return dom.isText(node) ? node.parentNode : node;
-};
-dom.moveContent = function (from, to) {
- if (from === to) {
- return;
- }
- if (from.parentNode === to) {
- while (from.lastChild) {
- dom.insertAfter(from.lastChild, from);
- }
- } else {
- while (from.firstChild && from.firstChild !== to) {
- to.appendChild(from.firstChild);
- }
- }
-};
-dom.getComputedStyle = function (node) {
- return node.nodeType === Node.COMMENT_NODE ? {} : window.getComputedStyle(node);
-};
-
-//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
-
-range.WrappedRange.prototype.reRange = function (keep_end, isNotBreakable) {
- var sc = this.sc;
- var so = this.so;
- var ec = this.ec;
- var eo = this.eo;
- isNotBreakable = isNotBreakable || dom.isNotBreakable;
-
- // search the first snippet editable node
- var start = keep_end ? ec : sc;
- while (start) {
- if (isNotBreakable(start, sc, so, ec, eo)) {
- break;
- }
- start = start.parentNode;
- }
-
- // check if the end caret have the same node
- var lastFilterEnd;
- var end = keep_end ? sc : ec;
- while (end) {
- if (start === end) {
- break;
- }
- if (isNotBreakable(end, sc, so, ec, eo)) {
- lastFilterEnd = end;
- }
- end = end.parentNode;
- }
- if (lastFilterEnd) {
- end = lastFilterEnd;
- }
- if (!end) {
- end = document.getElementsByTagName('body')[0];
- }
-
- // if same node, keep range
- if (start === end || !start) {
- return this;
- }
-
- // reduce or extend the range to don't break a isNotBreakable area
- if ($.contains(start, end)) {
-
- if (keep_end) {
- sc = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(end)) || sc);
- so = sc.textContent.length;
- } else if (!eo) {
- ec = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(end)) || ec);
- eo = ec.textContent.length;
- } else {
- ec = dom.firstChild(dom.hasContentAfter(dom.ancestorHaveNextSibling(end)) || ec);
- eo = 0;
- }
- } else {
-
- if (keep_end) {
- sc = dom.firstChild(start);
- so = 0;
- } else {
- ec = dom.lastChild(start);
- eo = ec.textContent.length;
- }
- }
-
- return new range.WrappedRange(sc, so, ec, eo);
-};
-/**
- * Returns the image the range is in or matches (if any, false otherwise).
- *
- * @todo this implementation may not cover all corner cases but should do the
- * trick for all reproductible ones
- * @returns {DOMElement|boolean}
- */
-range.WrappedRange.prototype.isOnImg = function () {
- // If not a selection but a cursor position, just check if a point's
- // ancestor is an image or not
- if (this.sc === this.ec && this.so === this.eo) {
- return dom.ancestor(this.sc, dom.isImg);
- }
-
- var startPoint = {node: this.sc, offset: this.so};
- var endPoint = {node: this.ec, offset: this.eo};
-
- var nb = 0;
- var image;
- var textNode;
- dom.walkPoint(startPoint, endPoint, function (point) {
- // If the element has children (not a text node and not empty node),
- // the element cannot be considered as selected (these children will
- // be processed to determine that)
- if (dom.hasChildren(point.node)) {
- return;
- }
-
- // Check if an ancestor of the current point is an image
- var pointImg = dom.ancestor(point.node, dom.isImg);
- var isText = dom.isText(point.node);
-
- // Check if a visible element is selected, i.e.
- // - If an ancestor of the current is an image we did not see yet
- // - If the point is not in a br or a text (so a node with no children)
- // - If the point is in a non empty text node we already saw
- if (pointImg ?
- (image !== pointImg) :
- ((!dom.isBR(point.node) && !isText) || (textNode === point.node && point.node.textContent.match(/\S|\u00A0/)))) {
- nb++;
- }
-
- // If an ancestor of the current point is an image, then save it as the
- // image we are looking for
- if (pointImg) {
- image = pointImg;
- }
- // If the current point is a text node save it as the last text node
- // seen (if we see it again, this might mean it is selected)
- if (isText) {
- textNode = point.node;
- }
- });
-
- return nb === 1 && image;
-};
-range.WrappedRange.prototype.deleteContents = function (towrite) {
- if (this.sc === this.ec && this.so === this.eo) {
- return this;
- }
-
- var r;
- var image = this.isOnImg();
- if (image) {
- // If the range matches/is in an image, then the image is to be removed
- // and the cursor moved to its previous position
- var parentNode = image.parentNode;
- var index = _.indexOf(parentNode.childNodes, image);
- parentNode.removeChild(image);
- r = new range.WrappedRange(parentNode, index, parentNode, index);
- } else {
- r = dom.removeBetween(this.sc, this.so, this.ec, this.eo, towrite);
- }
-
- $(dom.node(r.sc)).trigger("click"); // trigger click to disable and reanable editor and image handler
- return new range.WrappedRange(r.sc, r.so, r.ec, r.eo);
-};
-range.WrappedRange.prototype.clean = function (mergeFilter, all) {
- var node = dom.node(this.sc === this.ec ? this.sc : this.commonAncestor());
- node = node || $(this.sc).closest('[contenteditable]')[0];
- if (node.childNodes.length <=1) {
- return this;
- }
-
- var merge = dom.merge(node, this.sc, this.so, this.ec, this.eo, mergeFilter, all);
- var rem = dom.removeSpace(node.parentNode, merge.sc, merge.so, merge.ec, merge.eo);
-
- if (merge.merged || rem.removed) {
- return range.create(rem.sc, rem.so, rem.ec, rem.eo);
- }
- return this;
-};
-range.WrappedRange.prototype.remove = function (mergeFilter) {
-};
-range.WrappedRange.prototype.isOnCellFirst = function () {
- var node = dom.ancestor(this.sc, function (node) {return ["LI", "DIV", "TD","TH"].indexOf(node.tagName) !== -1;});
- return node && ["TD","TH"].indexOf(node.tagName) !== -1;
-};
-range.WrappedRange.prototype.isContentEditable = function () {
- return dom.isContentEditable(this.sc) && (this.sc === this.ec || dom.isContentEditable(this.ec));
-};
-
-//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
-
-renderer.tplButtonInfo.fontsize = function (lang, options) {
- var items = options.fontSizes.reduce(function (memo, v) {
- return memo + '<a data-event="fontSize" href="#" class="dropdown-item" data-value="' + v + '">' +
- '<i class="fa fa-check"></i> ' + v +
- '</a>';
- }, '');
-
- var sLabel = '<span class="note-current-fontsize">11</span>';
- return renderer.getTemplate().button(sLabel, {
- title: lang.font.size,
- dropdown: '<div class="dropdown-menu">' + items + '</div>'
- });
-};
-
-renderer.tplButtonInfo.color = function (lang, options) {
- var foreColorButtonLabel = '<i class="' + options.iconPrefix + options.icons.color.recent + '"></i>';
- var backColorButtonLabel = '<i class="' + options.iconPrefix + 'paint-brush"></i>';
- // TODO Remove recent color button if possible.
- // It is still put to avoid JS errors when clicking other buttons as the
- // editor still expects it to exist.
- var recentColorButton = renderer.getTemplate().button(foreColorButtonLabel, {
- className: 'note-recent-color d-none',
- title: lang.color.foreground,
- event: 'color',
- value: '{"backColor":"#B35E9B"}'
- });
- var foreColorItems = [
- '<li><div class="btn-group flex-column">',
- '<div class="note-color-palette" data-target-event="foreColor"></div>',
- '<h6 class="note-custom-color mt8" data-event="customColor" data-value="foreColor" title="' + lang.color.custom + '">',
- lang.color.custom + '</h6>',
- '<div class="note-custom-color-palette" data-target-event="foreColor"></div>',
- '</div></li>',
- ];
- var backColorItems = [
- '<li><div class="btn-group flex-column">',
- '<div class="note-color-reset" data-event="backColor" data-value="inherit" title="' + lang.color.transparent + '">',
- lang.color.setTransparent + '</div>',
- '<div class="note-color-palette" data-target-event="backColor"></div>',
- '<h6 class="note-custom-color mt8" data-event="customColor" data-value="backColor" title="' + lang.color.custom + '">',
- lang.color.custom + '</h6>',
- '<div class="note-custom-color-palette" data-target-event="backColor"></div>',
- '</div></li>',
- ];
- var foreColorButton = renderer.getTemplate().button(foreColorButtonLabel, {
- className: 'note-fore-color-preview mx-1',
- title: lang.color.foreground,
- dropdown: renderer.getTemplate().dropdown(foreColorItems)
- });
- var backColorButton = renderer.getTemplate().button(backColorButtonLabel, {
- className: 'note-back-color-preview mx-1',
- title: lang.color.background,
- dropdown: renderer.getTemplate().dropdown(backColorItems)
- });
- return recentColorButton + foreColorButton + backColorButton;
-},
-
-//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
-
-key.nameFromCode[46] = 'DELETE';
-key.nameFromCode[27] = 'ESCAPE';
-
-options.keyMap.pc['BACKSPACE'] = 'backspace';
-options.keyMap.pc['DELETE'] = 'delete';
-options.keyMap.pc['ENTER'] = 'enter';
-options.keyMap.pc['ESCAPE'] = 'cancel';
-options.keyMap.mac['SHIFT+TAB'] = 'untab';
-options.keyMap.pc['UP'] = 'up';
-options.keyMap.pc['DOWN'] = 'down';
-
-options.keyMap.mac['BACKSPACE'] = 'backspace';
-options.keyMap.mac['DELETE'] = 'delete';
-options.keyMap.mac['ENTER'] = 'enter';
-options.keyMap.mac['ESCAPE'] = 'cancel';
-options.keyMap.mac['UP'] = 'up';
-options.keyMap.mac['DOWN'] = 'down';
-
-options.styleTags = ['p', 'pre', 'small', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote'];
-
-$.summernote.pluginEvents.insertTable = function (event, editor, layoutInfo, sDim) {
- var $editable = layoutInfo.editable();
- var dimension = sDim.split('x');
- var r = range.create();
- if (!r) return;
- r = r.deleteContents(true);
-
- var table = editor.table.createTable(dimension[0], dimension[1]);
- var parent = r.sc;
- while (dom.isText(parent.parentNode) || dom.isRemovableEmptyNode(parent.parentNode)) {
- parent = parent.parentNode;
- }
- var node = dom.splitTree(parent, {'node': r.sc, 'offset': r.so}) || r.sc;
- node.parentNode.insertBefore(table, node);
-
- if ($(node).text() === '' || node.textContent === '\u00A0') {
- node.parentNode.removeChild(node);
- }
-
- editor.afterCommand($editable);
- event.preventDefault();
- return false;
-};
-$.summernote.pluginEvents.tab = function (event, editor, layoutInfo, outdent) {
- var $editable = layoutInfo.editable();
- $editable.data('NoteHistory').recordUndo($editable, 'tab');
- var r = range.create();
- outdent = outdent || false;
- event.preventDefault();
-
- if (r && (dom.ancestor(r.sc, dom.isCell) || dom.ancestor(r.ec, dom.isCell))) {
- if (r.isCollapsed() && r.isOnCell() && r.isOnCellFirst()) {
- var td = dom.ancestor(r.sc, dom.isCell);
- if (!outdent && !dom.nextElementSibling(td) && !dom.nextElementSibling(td.parentNode)) {
- var last = dom.lastChild(td);
- range.create(last, dom.nodeLength(last), last, dom.nodeLength(last)).select();
- $.summernote.pluginEvents.enter(event, editor, layoutInfo);
- } else if (outdent && !dom.previousElementSibling(td) && !$(td.parentNode).text().match(/\S/)) {
- $.summernote.pluginEvents.backspace(event, editor, layoutInfo);
- } else {
- editor.table.tab(r, outdent);
- }
- } else {
- $.summernote.pluginEvents.indent(event, editor, layoutInfo, outdent);
- }
- } else if (r && r.isCollapsed()) {
- if (!r.sc.textContent.slice(0,r.so).match(/\S/) && r.isOnList()) {
- if (outdent) {
- $.summernote.pluginEvents.outdent(event, editor, layoutInfo);
- } else {
- $.summernote.pluginEvents.indent(event, editor, layoutInfo);
- }
- } else {
- var next;
- if (!outdent) {
- if (dom.isText(r.sc)) {
- next = r.sc.splitText(r.so);
- } else {
- next = document.createTextNode('');
- $(r.sc.childNodes[r.so]).before(next);
- }
- editor.typing.insertTab($editable, r, options.tabsize);
- r = range.create(next, 0, next, 0);
- r = dom.merge(r.sc.parentNode, r.sc, r.so, r.ec, r.eo, null, true);
- range.create(r.sc, r.so, r.ec, r.eo).select();
- } else {
- r = dom.merge(r.sc.parentNode, r.sc, r.so, r.ec, r.eo, null, true);
- r = range.create(r.sc, r.so, r.ec, r.eo);
- if (r.sc.splitText) {
- next = r.sc.splitText(r.so);
- r.sc.textContent = r.sc.textContent.replace(/(\u00A0)+$/g, '');
- next.textContent = next.textContent.replace(/^(\u00A0)+/g, '');
- range.create(r.sc, r.sc.textContent.length, r.sc, r.sc.textContent.length).select();
- }
- }
- }
- }
- return false;
-};
-$.summernote.pluginEvents.untab = function (event, editor, layoutInfo) {
- return $.summernote.pluginEvents.tab(event, editor, layoutInfo, true);
-};
-$.summernote.pluginEvents.up = function (event, editor, layoutInfo) {
- var r = range.create();
- var node = dom.firstChild(r.sc.childNodes[r.so] || r.sc);
- if (!r.isOnCell()) {
- return;
- }
- // check if an ancestor between node and cell has content before
- var ancestor = dom.ancestor(node, function (ancestorNode) {
- return dom.hasContentBefore(ancestorNode) || dom.isCell(ancestorNode);
- });
- if (!dom.isCell(ancestor) && (!dom.isBR(dom.hasContentBefore(ancestor)) || !dom.isText(node) || dom.isVisibleText(node) || dom.hasContentBefore(dom.hasContentBefore(ancestor)))) {
- return;
- }
- event.preventDefault();
- var td = dom.ancestor(r.sc, dom.isCell);
- var tr = td.parentNode;
- var target = tr.previousElementSibling && tr.previousElementSibling.children[_.indexOf(tr.children, td)];
- if (!target) {
- target = (dom.ancestorHavePreviousSibling(tr) || tr).previousSibling;
- }
- if (target) {
- range.create(dom.lastChild(target), dom.lastChild(target).textContent.length).select();
- }
-};
-$.summernote.pluginEvents.down = function (event, editor, layoutInfo) {
- var r = range.create();
- var node = dom.firstChild(r.sc.childNodes[r.so] || r.sc);
- if (!r.isOnCell()) {
- return;
- }
- // check if an ancestor between node and cell has content after
- var ancestor = dom.ancestor(node, function (ancestorNode) {
- return dom.hasContentAfter(ancestorNode) || dom.isCell(ancestorNode);
- });
- if (!dom.isCell(ancestor) && (!dom.isBR(dom.hasContentAfter(ancestor)) || !dom.isText(node) || dom.isVisibleText(node) || dom.hasContentAfter(dom.hasContentAfter(ancestor)))) {
- return;
- }
- event.preventDefault();
- var td = dom.ancestor(r.sc, dom.isCell);
- var tr = td.parentNode;
- var target = tr.nextElementSibling && tr.nextElementSibling.children[_.indexOf(tr.children, td)];
- if (!target) {
- target = (dom.ancestorHaveNextSibling(tr) || tr).nextSibling;
- }
- if (target) {
- range.create(dom.firstChild(target), 0).select();
- }
-};
-$.summernote.pluginEvents.enter = function (event, editor, layoutInfo) {
- var $editable = layoutInfo.editable();
- $editable.data('NoteHistory').recordUndo($editable, 'enter');
-
- var r = range.create();
- if (!r.isContentEditable()) {
- event.preventDefault();
- return false;
- }
- if (!r.isCollapsed()) {
- r = r.deleteContents();
- r.select();
- }
-
- var br = $("<br/>")[0];
-
- var node;
- var $node;
- var $clone;
- var contentBefore = r.sc.textContent.slice(0,r.so).match(/\S|\u00A0/);
- if (!contentBefore && dom.isText(r.sc)) {
- node = r.sc.previousSibling;
- while (!contentBefore && node && dom.isText(node)) {
- contentBefore = dom.isVisibleText(node);
- node = node.previousSibling;
- }
- }
-
- node = dom.node(r.sc);
- var exist = r.sc.childNodes[r.so] || r.sc;
- exist = dom.isVisibleText(exist) || dom.isBR(exist) ? exist : dom.hasContentAfter(exist) || (dom.hasContentBefore(exist) || exist);
-
- // table: add a tr
- var td = dom.ancestor(node, dom.isCell);
- if (td && !dom.nextElementSibling(node) && !dom.nextElementSibling(td) && !dom.nextElementSibling(td.parentNode) && (!dom.isText(r.sc) || !r.sc.textContent.slice(r.so).match(/\S|\u00A0/))) {
- $node = $(td.parentNode);
- $clone = $node.clone();
- $clone.children().html(dom.blank);
- $node.after($clone);
- node = dom.firstElementChild($clone[0]) || $clone[0];
- range.create(node, 0, node, 0).select();
- dom.scrollIntoViewIfNeeded(br);
- event.preventDefault();
- return false;
- }
-
- var last = node;
- while (node && dom.isSplitable(node) && !dom.isList(node)) {
- last = node;
- node = node.parentNode;
- }
-
- if (last === node && !dom.isBR(node)) {
- node = r.insertNode(br, true);
- if (isFormatNode(last.firstChild) && $(last).closest(options.styleTags.join(',')).length) {
- dom.moveContent(last.firstChild, last);
- last.removeChild(last.firstChild);
- }
- do {
- node = dom.hasContentAfter(node);
- } while (node && dom.isBR(node));
-
- // create an other br because the user can't see the new line with only br in a block
- if (!node && (!br.nextElementSibling || !dom.isBR(br.nextElementSibling))) {
- $(br).before($("<br/>")[0]);
- }
- node = br.nextSibling || br;
- } else if (last === node && dom.isBR(node)) {
- $(node).after(br);
- node = br;
- } else if (!r.so && r.isOnList() && !r.sc.textContent.length && !dom.ancestor(r.sc, dom.isLi).nextElementSibling) {
- // double enter on the end of a list = new line out of the list
- $('<p></p>').append(br).insertAfter(dom.ancestor(r.sc, dom.isList));
- node = br;
- } else if (dom.isBR(exist) && $(r.sc).closest('blockquote, pre').length && !dom.hasContentAfter($(exist.parentNode).closest('blockquote *, pre *').length ? exist.parentNode : exist)) {
- // double enter on the end of a blockquote & pre = new line out of the list
- $('<p></p>').append(br).insertAfter($(r.sc).closest('blockquote, pre'));
- node = br;
- } else if (dom.isEditable(dom.node(r.sc))) {
- // if we are directly in an editable, only SHIFT + ENTER should add a newline
- node = null;
- } else if (last === r.sc) {
- if (dom.isBR(last)) {
- last = last.parentNode;
- }
- $node = $(last);
- $clone = $node.clone().text("");
- $node.after($clone);
- node = dom.node(dom.firstElementChild($clone[0]) || $clone[0]);
- $(node).html(br);
- node = br;
- } else {
- node = dom.splitTree(last, {'node': r.sc, 'offset': r.so}) || r.sc;
- if (!contentBefore) {
- var cur = dom.node(dom.lastChild(node.previousSibling));
- if (!dom.isBR(cur)) {
- $(cur).html(br);
- }
- }
- if (!dom.isVisibleText(node)) {
- node = dom.firstChild(node);
- $(dom.node( dom.isBR(node) ? node.parentNode : node )).html(br);
- node = br;
- }
- }
-
- if (node) {
- node = dom.firstChild(node);
- if (dom.isBR(node)) {
- range.createFromNode(node).select();
- } else {
- range.create(node,0).select();
- }
- dom.scrollIntoViewIfNeeded(node);
- }
- event.preventDefault();
- return false;
-};
-$.summernote.pluginEvents.visible = function (event, editor, layoutInfo) {
- var $editable = layoutInfo.editable();
- $editable.data('NoteHistory').recordUndo($editable, "visible");
-
- var r = range.create();
- if (!r) return;
-
- if (!r.isCollapsed()) {
- if ((dom.isCell(dom.node(r.sc)) || dom.isCell(dom.node(r.ec))) && dom.node(r.sc) !== dom.node(r.ec)) {
- remove_table_content(r);
- r = range.create(r.ec, 0);
- }
- r.select();
- }
-
- // don't write in forbidden tag (like span for font awsome)
- var node = dom.firstChild(r.sc.tagName && r.so ? r.sc.childNodes[r.so] || r.sc : r.sc);
- while (node.parentNode) {
- if (dom.isForbiddenNode(node)) {
- var text = node.previousSibling;
- if (text && dom.isText(text) && dom.isVisibleText(text)) {
- range.create(text, text.textContent.length, text, text.textContent.length).select();
- } else {
- text = node.parentNode.insertBefore(document.createTextNode( "." ), node);
- range.create(text, 1, text, 1).select();
- setTimeout(function () {
- var text = range.create().sc;
- text.textContent = text.textContent.replace(/^./, '');
- range.create(text, text.textContent.length, text, text.textContent.length).select();
- },0);
- }
- break;
- }
- node = node.parentNode;
- }
-
- return true;
-};
-
-function remove_table_content(r) {
- var nodes = dom.listBetween(r.sc, r.ec, r.so, r.eo);
- if (dom.isText(r.sc)) {
- r.sc.textContent = r.sc.textContent.slice(0, r.so);
- }
- if (dom.isText(r.ec)) {
- r.ec.textContent = r.ec.textContent.slice(r.eo);
- }
- for (var i in nodes) {
- var node = nodes[i];
- if (node === r.sc || node === r.ec || $.contains(node, r.sc) || $.contains(node, r.ec)) {
- continue;
- } else if (dom.isCell(node)) {
- $(node).html("<br/>");
- } else if (node.parentNode) {
- do {
- var parent = node.parentNode;
- parent.removeChild(node);
- node = parent;
- } while (!dom.isVisibleText(node) && !dom.firstElementChild(node) &&
- !dom.isCell(node) &&
- node.parentNode && !$(node.parentNode).hasClass('o_editable'));
- }
- }
- return false;
-}
-
-$.summernote.pluginEvents.delete = function (event, editor, layoutInfo) {
- var $editable = layoutInfo.editable();
- $editable.data('NoteHistory').recordUndo($editable, "delete");
-
- var r = range.create();
- if (!r) return;
- if (!r.isContentEditable()) {
- event.preventDefault();
- return false;
- }
- if (!r.isCollapsed()) {
- if (dom.isCell(dom.node(r.sc)) || dom.isCell(dom.node(r.ec))) {
- remove_table_content(r);
- range.create(r.ec, 0).select();
- } else {
- r = r.deleteContents();
- r.select();
- }
- event.preventDefault();
- return false;
- }
-
- var target = r.ec;
- var offset = r.eo;
- if (target.tagName && target.childNodes[offset]) {
- target = target.childNodes[offset];
- offset = 0;
- }
-
- var node = dom.node(target);
- var data = dom.merge(node, target, offset, target, offset, null, true);
- data = dom.removeSpace(node.parentNode, data.sc, data.so, data.ec, data.eo);
- r = range.create(data.sc, data.so);
- r.select();
- target = r.sc;
- offset = r.so;
-
- while (!dom.hasContentAfter(node) && !dom.hasContentBefore(node) && !dom.isImg(node)) {node = node.parentNode;}
-
- var contentAfter = target.textContent.slice(offset,Infinity).match(/\S|\u00A0/);
- var content = target.textContent.replace(/[ \t\r\n]+$/, '');
- var temp;
- var temp2;
- var next;
-
- // media
- if (dom.isImg(node) || (!contentAfter && dom.isImg(dom.hasContentAfter(node)))) {
- var parent;
- var index;
- if (!dom.isImg(node)) {
- node = dom.hasContentAfter(node);
- }
- while (dom.isImg(node)) {
- parent = node.parentNode;
- index = dom.position(node);
- if (index>0) {
- next = node.previousSibling;
- r = range.create(next, next.textContent.length);
- } else {
- r = range.create(parent, 0);
- }
- if (!dom.hasContentAfter(node) && !dom.hasContentBefore(node)) {
- parent.appendChild($('<br/>')[0]);
- }
- parent.removeChild(node);
- node = parent;
- r.select();
- }
- }
- // empty tag
- else if (!content.length && target.tagName && dom.isRemovableEmptyNode(dom.isBR(target) ? target.parentNode : target)) {
- if (node === $editable[0] || $.contains(node, $editable[0])) {
- event.preventDefault();
- return false;
- }
- var before = false;
- next = dom.hasContentAfter(dom.ancestorHaveNextSibling(node));
- if (!dom.isContentEditable(next)) {
- before = true;
- next = dom.hasContentBefore(dom.ancestorHavePreviousSibling(node));
- }
- dom.removeSpace(next.parentNode, next, 0, next, 0); // clean before jump for not select invisible space between 2 tag
- next = dom.firstChild(next);
- node.parentNode.removeChild(node);
- range.create(next, before ? next.textContent.length : 0).select();
- }
- // normal feature if same tag and not the end
- else if (contentAfter) {
- return true;
- }
- // merge with the next text node
- else if (dom.isText(target) && (temp = dom.hasContentAfter(target)) && dom.isText(temp)) {
- return true;
- }
- //merge with the next block
- else if ((temp = dom.ancestorHaveNextSibling(target)) &&
- !r.isOnCell() &&
- dom.isMergable(temp) &&
- dom.isMergable(temp2 = dom.hasContentAfter(temp)) &&
- temp.tagName === temp2.tagName &&
- (temp.tagName !== "LI" || !$('ul,ol', temp).length) && (temp2.tagName !== "LI" || !$('ul,ol', temp2).length) && // protect li
- !dom.isNotBreakable(temp) &&
- !dom.isNotBreakable(temp2)) {
- dom.autoMerge(target, false);
- next = dom.firstChild(dom.hasContentAfter(dom.ancestorHaveNextSibling(target)));
- if (dom.isBR(next)) {
- if (dom.position(next) === 0) {
- range.create(next.parentNode, 0).select();
- }
- else {
- range.create(next.previousSibling, next.previousSibling.textContent.length).select();
- }
- next.parentNode.removeChild(next);
- } else {
- range.create(next, 0).select();
- }
- }
- // jump to next node for delete
- else if ((temp = dom.ancestorHaveNextSibling(target)) && (temp2 = dom.hasContentAfter(temp)) && dom.isContentEditable(temp2)) {
-
- dom.removeSpace(temp2.parentNode, temp2, 0, temp, 0); // clean before jump for not select invisible space between 2 tag
- temp2 = dom.firstChild(temp2);
-
- r = range.create(temp2, 0);
- r.select();
-
- if ((dom.isText(temp) || dom.getComputedStyle(temp).display === "inline") && (dom.isText(temp2) || dom.getComputedStyle(temp2).display === "inline")) {
- if (dom.isText(temp2)) {
- temp2.textContent = temp2.textContent.replace(/^\s*\S/, '');
- } else {
- $.summernote.pluginEvents.delete(event, editor, layoutInfo);
- }
- }
- }
-
- $(dom.node(r.sc)).trigger("click"); // trigger click to disable and reanable editor and image handler
- event.preventDefault();
- return false;
-};
-$.summernote.pluginEvents.backspace = function (event, editor, layoutInfo) {
- var $editable = layoutInfo.editable();
- $editable.data('NoteHistory').recordUndo($editable, "backspace");
-
- var r = range.create();
- if (!r) return;
- if (!r.isContentEditable()) {
- event.preventDefault();
- return false;
- }
- if (!r.isCollapsed()) {
- if (dom.isCell(dom.node(r.sc)) || dom.isCell(dom.node(r.ec))) {
- remove_table_content(r);
- range.create(r.sc, dom.nodeLength(r.sc)).select();
- } else {
- r = r.deleteContents();
- r.select();
- }
- event.preventDefault();
- return false;
- }
-
- var target = r.sc;
- var offset = r.so;
- if (target.tagName && target.childNodes[offset]) {
- target = target.childNodes[offset];
- offset = 0;
- }
-
- var node = dom.node(target);
- var data = dom.merge(node, target, offset, target, offset, null, true);
- data = dom.removeSpace(node.parentNode, data.sc, data.so, data.ec, data.eo);
- r = dom.isVoid(data.sc) ? range.createFromNode(data.sc) : range.create(data.sc, data.so);
- r.select();
- target = r.sc;
- offset = r.so;
- if (target.tagName && target.childNodes[offset]) {
- target = target.childNodes[offset];
- offset = 0;
- node = dom.node(target);
- }
-
- while (node.parentNode && !dom.hasContentAfter(node) && !dom.hasContentBefore(node) && !dom.isImg(node)) {node = node.parentNode;}
-
- var contentBefore = target.textContent.slice(0,offset).match(/\S|\u00A0/);
- var content = target.textContent.replace(/[ \t\r\n]+$/, '');
- var temp;
- var temp2;
- var prev;
-
- // delete media
- if (dom.isImg(node) || (!contentBefore && dom.isImg(dom.hasContentBefore(node)))) {
- if (!dom.isImg(node)) {
- node = dom.hasContentBefore(node);
- }
- range.createFromNode(node).select();
- $.summernote.pluginEvents.delete(event, editor, layoutInfo);
- }
- // table tr td
- else if (r.isOnCell() && !offset && (target === (temp = dom.ancestor(target, dom.isCell)) || target === temp.firstChild || (dom.isText(temp.firstChild) && !dom.isVisibleText(temp.firstChild) && target === temp.firstChild.nextSibling))) {
- if (dom.previousElementSibling(temp)) {
- var td = dom.previousElementSibling(temp);
- node = td.lastChild || td;
- } else {
- var tr = temp.parentNode;
- var prevTr = dom.previousElementSibling(tr);
- if (!$(temp.parentNode).text().match(/\S|\u00A0/)) {
- if (prevTr) {
- node = dom.lastChild(dom.lastElementChild(prevTr));
- } else {
- node = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(tr)) || $editable.get(0));
- }
- $(tr).empty();
- if (!$(tr).closest('table').has('td, th').length) {
- $(tr).closest('table').remove();
- }
- $(tr).remove();
- range.create(node, node.textContent.length, node, node.textContent.length).select();
- } else {
- node = dom.lastElementChild(prevTr).lastChild || dom.lastElementChild(prevTr);
- }
- }
- if (dom.isBR(node)) {
- range.createFromNode(node).select();
- } else {
- range.create(node, dom.nodeLength(node)).select();
- }
- }
- // empty tag
- else if (!content.length && target.tagName && dom.isRemovableEmptyNode(target)) {
- if (node === $editable[0] || $.contains(node, $editable[0])) {
- event.preventDefault();
- return false;
- }
- var before = true;
- prev = dom.hasContentBefore(dom.ancestorHavePreviousSibling(node));
- if (!dom.isContentEditable(prev)) {
- before = false;
- prev = dom.hasContentAfter(dom.ancestorHaveNextSibling(node));
- }
- dom.removeSpace(prev.parentNode, prev, 0, prev, 0); // clean before jump for not select invisible space between 2 tag
- prev = dom.lastChild(prev);
- node.parentNode.removeChild(node);
- range.createFromNode(prev).select();
- range.create(prev, before ? prev.textContent.length : 0).select();
- }
- // normal feature if same tag and not the begin
- else if (contentBefore) {
- return true;
- }
- // merge with the previous text node
- else if (dom.isText(target) && (temp = dom.hasContentBefore(target)) && (dom.isText(temp) || dom.isBR(temp))) {
- return true;
- }
- //merge with the previous block
- else if ((temp = dom.ancestorHavePreviousSibling(target)) &&
- dom.isMergable(temp) &&
- dom.isMergable(temp2 = dom.hasContentBefore(temp)) &&
- temp.tagName === temp2.tagName &&
- (temp.tagName !== "LI" || !$('ul,ol', temp).length) && (temp2.tagName !== "LI" || !$('ul,ol', temp2).length) && // protect li
- !dom.isNotBreakable(temp) &&
- !dom.isNotBreakable(temp2)) {
- prev = dom.firstChild(target);
- dom.autoMerge(target, true);
- range.create(prev, 0).select();
- }
- // jump to previous node for delete
- else if ((temp = dom.ancestorHavePreviousSibling(target)) && (temp2 = dom.hasContentBefore(temp)) && dom.isContentEditable(temp2)) {
-
- dom.removeSpace(temp2.parentNode, temp2, 0, temp, 0); // clean before jump for not select invisible space between 2 tag
- temp2 = dom.lastChild(temp2);
-
- r = range.create(temp2, temp2.textContent.length, temp2, temp2.textContent.length);
- r.select();
-
- if ((dom.isText(temp) || dom.getComputedStyle(temp).display === "inline") && (dom.isText(temp2) || dom.getComputedStyle(temp2).display === "inline")) {
- if (dom.isText(temp2)) {
- temp2.textContent = temp2.textContent.replace(/\S\s*$/, '');
- } else {
- $.summernote.pluginEvents.backspace(event, editor, layoutInfo);
- }
- }
- }
-
- r = range.create();
- if (r) {
- $(dom.node(r.sc)).trigger("click"); // trigger click to disable and reanable editor and image handler
- dom.scrollIntoViewIfNeeded(r.sc.parentNode.previousElementSibling || r.sc);
- }
-
- event.preventDefault();
- return false;
-};
-
-function isFormatNode(node) {
- return node.tagName && options.styleTags.indexOf(node.tagName.toLowerCase()) !== -1;
-}
-
-$.summernote.pluginEvents.insertUnorderedList = function (event, editor, layoutInfo, sorted) {
- var $editable = layoutInfo.editable();
- $editable.data('NoteHistory').recordUndo($editable);
-
- var parent;
- var r = range.create();
- if (!r) return;
- var node = r.sc;
- while (node && node !== $editable[0]) {
-
- parent = node.parentNode;
- if (node.tagName === (sorted ? "UL" : "OL")) {
-
- var ul = document.createElement(sorted ? "ol" : "ul");
- ul.className = node.className;
- parent.insertBefore(ul, node);
- while (node.firstChild) {
- ul.appendChild(node.firstChild);
- }
- parent.removeChild(node);
- r.select();
- return;
-
- } else if (node.tagName === (sorted ? "OL" : "UL")) {
-
- var lis = [];
- for (var i=0; i<node.children.length; i++) {
- lis.push(node.children[i]);
- }
-
- if (parent.tagName === "LI") {
- node = parent;
- parent = node.parentNode;
- _.each(lis, function (li) {
- parent.insertBefore(li, node);
- });
- } else {
- _.each(lis, function (li) {
- while (li.firstChild) {
- parent.insertBefore(li.firstChild, node);
- }
- });
- }
-
- parent.removeChild(node);
- r.select();
- return;
-
- }
- node = parent;
- }
-
- var p0 = r.sc;
- while (p0 && p0.parentNode && p0.parentNode !== $editable[0] && !isFormatNode(p0)) {
- p0 = p0.parentNode;
- }
- if (!p0) return;
- var p1 = r.ec;
- while (p1 && p1.parentNode && p1.parentNode !== $editable[0] && !isFormatNode(p1)) {
- p1 = p1.parentNode;
- }
- if (!p0.parentNode || p0.parentNode !== p1.parentNode) {
- return;
- }
-
- parent = p0.parentNode;
- ul = document.createElement(sorted ? "ol" : "ul");
- parent.insertBefore(ul, p0);
- var childNodes = parent.childNodes;
- var brs = [];
- var begin = false;
- for (i = 0; i < childNodes.length; i++) {
- if (begin && dom.isBR(childNodes[i])) {
- parent.removeChild(childNodes[i]);
- i--;
- }
- if ((!dom.isText(childNodes[i]) && !isFormatNode(childNodes[i])) || (!ul.firstChild && childNodes[i] !== p0) ||
- $.contains(ul, childNodes[i]) || (dom.isText(childNodes[i]) && !childNodes[i].textContent.match(/\S|u00A0/))) {
- continue;
- }
- begin = true;
- var li = document.createElement('li');
- ul.appendChild(li);
- li.appendChild(childNodes[i]);
- if (li.firstChild === p1) {
- break;
- }
- i--;
- }
- if (dom.isBR(childNodes[i])) {
- parent.removeChild(childNodes[i]);
- }
-
- for (i = 0; i < brs.length ; i++) {
- parent.removeChild(brs[i]);
- }
- r.clean().select();
- event.preventDefault();
- return false;
-};
-$.summernote.pluginEvents.insertOrderedList = function (event, editor, layoutInfo) {
- return $.summernote.pluginEvents.insertUnorderedList(event, editor, layoutInfo, true);
-};
-$.summernote.pluginEvents.indent = function (event, editor, layoutInfo, outdent) {
- var $editable = layoutInfo.editable();
- $editable.data('NoteHistory').recordUndo($editable);
- var r = range.create();
- if (!r) return;
-
- var flag = false;
- function indentUL(UL, start, end) {
- var next;
- var tagName = UL.tagName;
- var node = UL.firstChild;
- var ul = document.createElement(tagName);
- var li = document.createElement("li");
- li.style.listStyle = "none";
- li.appendChild(ul);
-
- if (flag) {
- flag = 1;
- }
-
- // create and fill ul into a li
- while (node) {
- if (flag === 1 || node === start || $.contains(node, start)) {
- flag = true;
- node.parentNode.insertBefore(li, node);
- }
- next = dom.nextElementSibling(node);
- if (flag) {
- ul.appendChild(node);
- }
- if (node === end || $.contains(node, end)) {
- flag = false;
- break;
- }
- node = next;
- }
-
- var temp;
- var prev = dom.previousElementSibling(li);
- if (prev && prev.tagName === "LI" && (temp = dom.firstElementChild(prev)) && temp.tagName === tagName && ((dom.firstElementChild(prev) || prev.firstChild) !== ul)) {
- dom.doMerge(dom.firstElementChild(prev) || prev.firstChild, ul);
- li = prev;
- li.parentNode.removeChild(dom.nextElementSibling(li));
- }
- next = dom.nextElementSibling(li);
- if (next && next.tagName === "LI" && (temp = dom.firstElementChild(next)) && temp.tagName === tagName && (dom.firstElementChild(li) !== dom.firstElementChild(next))) {
- dom.doMerge(dom.firstElementChild(li), dom.firstElementChild(next));
- li.parentNode.removeChild(dom.nextElementSibling(li));
- }
- }
- function outdenttUL(UL, start, end) {
- var next;
- var node = UL.firstChild;
- var parent = UL.parentNode;
- var li = UL.parentNode.tagName === "LI" ? UL.parentNode : UL;
- var ul = UL.parentNode.tagName === "LI" ? UL.parentNode.parentNode : UL.parentNode;
- start = dom.ancestor(start, dom.isLi);
- end = dom.ancestor(end, dom.isLi);
-
- if (ul.tagName !== "UL" && ul.tagName !== "OL") return;
-
- // create and fill ul into a li
- while (node) {
- if (node === start || $.contains(node, start)) {
- flag = true;
- if (dom.previousElementSibling(node) && li.tagName === "LI") {
- li = dom.splitTree(li, dom.prevPoint({'node': node, 'offset': 0}));
- }
- }
- next = dom.nextElementSibling(node);
- if (flag) {
- ul = node.parentNode;
- li.parentNode.insertBefore(node, li);
- if (!ul.children.length) {
- if (ul.parentNode.tagName === "LI") {
- ul = ul.parentNode;
- }
- ul.parentNode.removeChild(ul);
- }
- }
-
- if (node === end || $.contains(node, end)) {
- flag = false;
- break;
- }
- node = next;
- }
-
- dom.merge(parent, start, 0, end, 1, null, true);
- }
- function indentOther(p, start, end) {
- if (p === start || $.contains(p, start) || $.contains(start, p)) {
- flag = true;
- }
- if (flag) {
- if (outdent) {
- dom.outdent(p);
- } else {
- dom.indent(p);
- }
- }
- if (p === end || $.contains(p, end) || $.contains(end, p)) {
- flag = false;
- }
- }
-
- var ancestor = r.commonAncestor();
- 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 = $(dom.node(ancestor)).children();
- }
- if (!$dom.not('br').length) {
- // if selection is inside a list, we indent its list items
- $dom = $(dom.ancestor(r.sc, 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
- $dom = $(r.sc).closest(options.styleTags.join(',')+',td');
- }
- }
-
- // if select tr, take the first td
- $dom = $dom.map(function () { return this.tagName === "TR" ? dom.firstElementChild(this) : this; });
-
- $dom.each(function () {
- if (flag || $.contains(this, r.sc)) {
- if (dom.isList(this)) {
- if (outdent) {
- outdenttUL(this, r.sc, r.ec);
- } else {
- indentUL(this, r.sc, r.ec);
- }
- } else if (isFormatNode(this) || dom.ancestor(this, dom.isCell)) {
- indentOther(this, r.sc, r.ec);
- }
- }
- });
-
- if ($dom.length) {
- var $parent = $dom.parent();
-
- // remove text nodes between lists
- var $ul = $parent.find('ul, ol');
- if (!$ul.length) {
- $ul = $(dom.ancestor(r.sc, dom.isList));
- }
- $ul.each(function () {
- if (this.previousSibling &&
- this.previousSibling !== dom.previousElementSibling(this) &&
- !this.previousSibling.textContent.match(/\S/)) {
- this.parentNode.removeChild(this.previousSibling);
- }
- if (this.nextSibling &&
- this.nextSibling !== dom.nextElementSibling(this) &&
- !this.nextSibling.textContent.match(/\S/)) {
- this.parentNode.removeChild(this.nextSibling);
- }
- });
-
- // merge same ul or ol
- r = dom.merge($parent[0], r.sc, r.so, r.ec, r.eo, function (prev, cur) {
- if (prev && dom.isList(prev) && dom.isEqual(prev, cur)) {
- return true;
- }
- }, true);
- range.create(r.sc, r.so, r.ec, r.eo).select();
- }
- event.preventDefault();
- return false;
-};
-$.summernote.pluginEvents.outdent = function (event, editor, layoutInfo) {
- return $.summernote.pluginEvents.indent(event, editor, layoutInfo, true);
-};
-
-$.summernote.pluginEvents.formatBlock = function (event, editor, layoutInfo, sTagName) {
- $.summernote.pluginEvents.applyFont(event, editor, layoutInfo, null, null, "Default");
- var $editable = layoutInfo.editable();
- $editable.data('NoteHistory').recordUndo($editable);
- event.preventDefault();
-
- var r = range.create();
- if (!r) {
- return;
- }
- // select content since container (that firefox selects) may be removed
- if (r.so === 0) {
- r.sc = dom.firstChild(r.sc);
- }
- if (dom.nodeLength(r.ec) >= r.eo) {
- r.ec = dom.lastChild(r.ec);
- r.eo = dom.nodeLength(r.ec);
- }
- r = range.create(r.sc, r.so, r.ec, r.eo);
- r.reRange().select();
-
- if (sTagName === "blockquote" || sTagName === "pre") {
- sTagName = $.summernote.core.agent.isMSIE ? '<' + sTagName + '>' : sTagName;
- document.execCommand('FormatBlock', false, sTagName);
- return;
- }
-
- // fix by odoo because if you select a style in a li with no p tag all the ul is wrapped by the style tag
- var nodes = dom.listBetween(r.sc, r.ec, r.so, r.eo);
- for (var i=0; i<nodes.length; i++) {
- if (dom.isBR(nodes[i]) || (dom.isText(nodes[i]) && dom.isVisibleText(nodes[i])) || dom.isB(nodes[i]) || dom.isU(nodes[i]) || dom.isS(nodes[i]) || dom.isI(nodes[i]) || dom.isFont(nodes[i])) {
- var ancestor = dom.ancestor(nodes[i], isFormatNode);
- if ($(ancestor).parent().is('blockquote')) {
- // firefox may wrap formatting block in blockquote
- $(ancestor).unwrap();
- }
- if (!ancestor) {
- dom.wrap(nodes[i], sTagName);
- } else if (ancestor.tagName.toLowerCase() !== sTagName) {
- var tag = document.createElement(sTagName);
- ancestor.parentNode.insertBefore(tag, ancestor);
- dom.moveContent(ancestor, tag);
- if (ancestor.className) {
- tag.className = ancestor.className;
- }
- ancestor.parentNode.removeChild(ancestor);
- }
- }
- }
- r.select();
-};
-$.summernote.pluginEvents.removeFormat = function (event, editor, layoutInfo, value) {
- var $editable = layoutInfo.editable();
- $editable.data('NoteHistory').recordUndo($editable);
- var r = range.create();
- if (!r) return;
- var node = range.create().sc.parentNode;
- document.execCommand('removeFormat');
- document.execCommand('removeFormat');
- r = range.create();
- if (!r) return;
- r = dom.merge(node, r.sc, r.so, r.ec, r.eo, null, true);
- range.create(r.sc, r.so, r.ec, r.eo).select();
- event.preventDefault();
- return false;
-};
-
-eventHandler.modules.editor.undo = function ($popover) {
- if (!$popover.attr('disabled')) $popover.data('NoteHistory').undo();
-};
-eventHandler.modules.editor.redo = function ($popover) {
- if (!$popover.attr('disabled')) $popover.data('NoteHistory').redo();
-};
-
-// Get color and background color of node to update recent color button
-var fn_from_node = eventHandler.modules.editor.style.fromNode;
-eventHandler.modules.editor.style.fromNode = function ($node) {
- var styleInfo = fn_from_node.apply(this, arguments);
- styleInfo['color'] = $node.css('color');
- styleInfo['background-color'] = $node.css('background-color');
- return styleInfo;
-};
-
-// use image toolbar if current range is on image
-var fn_editor_currentstyle = eventHandler.modules.editor.currentStyle;
-eventHandler.modules.editor.currentStyle = function (target) {
- var styleInfo = fn_editor_currentstyle.apply(this, arguments);
- // with our changes for inline editor, the targeted element could be a button of the editor
- if (!styleInfo.image || !dom.isEditable(styleInfo.image)) {
- styleInfo.image = undefined;
- var r = range.create();
- if (r)
- styleInfo.image = r.isOnImg();
- }
- // Fix when the target is a link: the text-align buttons state should
- // indicate the alignment of the link in the parent, not the text inside
- // the link (which is not possible to customize with summernote). Summernote fixed
- // this in their newest version... by just not showing the active button
- // for alignments.
- if (styleInfo.anchor) {
- styleInfo['text-align'] = $(styleInfo.anchor).parent().css('text-align');
- }
- return styleInfo;
-};
-
-options.fontSizes = [_t('Default'), 8, 9, 10, 11, 12, 14, 18, 24, 36, 48, 62];
-$.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color, bgcolor, size) {
- var r = range.create();
- if (!r) return;
- var startPoint = r.getStartPoint();
- var endPoint = r.getEndPoint();
-
- if (r.isCollapsed() && !dom.isFont(r.sc)) {
- return {
- sc: startPoint.node,
- so: startPoint.offset,
- ec: endPoint.node,
- offset: endPoint.offset
- };
- }
-
- 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 (endPoint.offset && endPoint.offset !== dom.nodeLength(endPoint.node)) {
- ancestor = dom.ancestor(endPoint.node, dom.isFont) || endPoint.node;
- dom.splitTree(ancestor, endPoint);
- }
- if (startPoint.offset && startPoint.offset !== dom.nodeLength(startPoint.node)) {
- ancestor = dom.ancestor(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) && dom.isVisibleText(node)) ||
- (dom.isFont(node) && !dom.isVisibleText(node))) &&
- (node !== endPoint.node || endPoint.offset)) {
-
- nodes.push(point.node);
-
- }
- });
- nodes = list.unique(nodes);
-
- // If ico fa
- if (r.isCollapsed()) {
- nodes.push(startPoint.node);
- }
-
- // apply font: foreColor, backColor, size (the color can be use a class text-... or bg-...)
- var font, $font, fonts = [], className;
- var i;
- if (color || bgcolor || size) {
- for (i=0; i<nodes.length; i++) {
- node = nodes[i];
-
- font = dom.ancestor(node, dom.isFont);
- if (!font) {
- if (node.textContent.match(/^[ ]|[ ]$/)) {
- node.textContent = node.textContent.replace(/^[ ]|[ ]$/g, '\u00A0');
- }
-
- font = dom.create("font");
- node.parentNode.insertBefore(font, node);
- font.appendChild(node);
- }
-
- fonts.push(font);
-
- className = font.className.split(/\s+/);
-
- 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.indexOf('text-') !== -1) {
- font.className = className.join(" ") + " " + color;
- font.style.color = "inherit";
- } else {
- font.className = className.join(" ");
- font.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.indexOf('bg-') !== -1) {
- font.className = className.join(" ") + " " + bgcolor;
- font.style.backgroundColor = "inherit";
- } else {
- font.className = className.join(" ");
- font.style.backgroundColor = bgcolor;
- }
- }
- if (size) {
- font.style.fontSize = "inherit";
- if (!isNaN(size) && Math.abs(parseInt(dom.getComputedStyle(font).fontSize, 10)-size)/size > 0.05) {
- font.style.fontSize = size + "px";
- }
- }
- }
- }
-
- // 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.backgroundColor === "inherit") {
- font.style.backgroundColor = "";
- }
- if (font.style.color === "inherit") {
- font.style.color = "";
- }
- if (font.style.fontSize === "inherit") {
- font.style.fontSize = "";
- }
-
- $font = $(font);
-
- if (!$font.css("color") && !$font.css("background-color") && !$font.css("font-size")) {
- $font.removeAttr("style");
- }
- if (!font.className.length) {
- $font.removeAttr("class");
- }
- }
-
- // select nodes to clean (to remove empty font and merge same nodes)
- nodes = [];
- dom.walkPoint(startPoint, endPoint, function (point) {
- nodes.push(point.node);
- });
- nodes = list.unique(nodes);
-
- function remove(node, to) {
- if (node === endPoint.node) {
- endPoint = dom.prevPoint(endPoint);
- }
- if (to) {
- dom.moveContent(node, to);
- }
- dom.remove(node);
- }
-
- // remove node without attributes (move content), and merge the same nodes
- var className2, style, style2;
- for (i=0; i<nodes.length; i++) {
- node = nodes[i];
-
- if ((dom.isText(node) || dom.isBR(node)) && !dom.isVisibleText(node)) {
- remove(node);
- nodes.splice(i,1);
- i--;
- continue;
- }
-
- font = dom.ancestor(node, dom.isFont);
- node = font || dom.ancestor(node, dom.isSpan);
-
- if (!node) {
- continue;
- }
-
- $font = $(node);
- className = dom.orderClass(node);
- style = dom.orderStyle(node);
-
- if (!className && !style) {
- remove(node, node.parentNode);
- nodes.splice(i,1);
- i--;
- continue;
- }
-
- if (i>0 && (font = dom.ancestor(nodes[i-1], dom.isFont))) {
- className2 = font.getAttribute('class');
- style2 = font.getAttribute('style');
- if (node !== font && className === className2 && style === style2) {
- remove(node, font);
- nodes.splice(i,1);
- i--;
- continue;
- }
- }
- }
-
- range.create(startPoint.node, startPoint.offset, endPoint.node, endPoint.offset).select();
-};
-$.summernote.pluginEvents.fontSize = function (event, editor, layoutInfo, value) {
- var $editable = layoutInfo.editable();
- event.preventDefault();
- $.summernote.pluginEvents.applyFont(event, editor, layoutInfo, null, null, value);
- editor.afterCommand($editable);
-};
-$.summernote.pluginEvents.color = function (event, editor, layoutInfo, sObjColor) {
- var oColor = JSON.parse(sObjColor);
- var foreColor = oColor.foreColor, backColor = oColor.backColor;
-
- if (foreColor) { $.summernote.pluginEvents.foreColor(event, editor, layoutInfo, foreColor); }
- if (backColor) { $.summernote.pluginEvents.backColor(event, editor, layoutInfo, backColor); }
-};
-$.summernote.pluginEvents.foreColor = function (event, editor, layoutInfo, foreColor) {
- var $editable = layoutInfo.editable();
- $.summernote.pluginEvents.applyFont(event, editor, layoutInfo, foreColor, null, null);
- editor.afterCommand($editable);
-};
-$.summernote.pluginEvents.backColor = function (event, editor, layoutInfo, backColor) {
- var $editable = layoutInfo.editable();
- var r = range.create();
- if (!r) return;
- if (r.isCollapsed() && r.isOnCell()) {
- var cell = dom.ancestor(r.sc, dom.isCell);
- cell.className = cell.className.replace(new RegExp('(^|\\s+)bg-[^\\s]+(\\s+|$)', 'gi'), '');
- cell.style.backgroundColor = "";
- if (backColor.indexOf('bg-') !== -1) {
- cell.className += ' ' + backColor;
- } else if (backColor !== 'inherit') {
- cell.style.backgroundColor = backColor;
- }
- return;
- }
- $.summernote.pluginEvents.applyFont(event, editor, layoutInfo, null, backColor, null);
- editor.afterCommand($editable);
-};
-
-options.onCreateLink = function (sLinkUrl) {
- if (sLinkUrl.indexOf('mailto:') === 0 || sLinkUrl.indexOf('tel:') === 0) {
- // pass
- } else if (sLinkUrl.indexOf('@') !== -1 && sLinkUrl.indexOf(':') === -1) {
- sLinkUrl = 'mailto:' + sLinkUrl;
- } else if (sLinkUrl.indexOf('://') === -1 && sLinkUrl[0] !== '/'
- && sLinkUrl[0] !== '#' && sLinkUrl.slice(0, 2) !== '${') {
- sLinkUrl = 'http://' + sLinkUrl;
- }
- return sLinkUrl;
-};
-
-function summernote_table_scroll(event) {
- var r = range.create();
- if (r && r.isOnCell()) {
- $('.o_table_handler').remove();
- }
-}
-function summernote_table_update(oStyle) {
- var r = range.create();
- if (!oStyle.range || !r || !r.isOnCell() || !r.isOnCellFirst()) {
- $('.o_table_handler').remove();
- return;
- }
- var table = dom.ancestor(oStyle.range.sc, dom.isTable);
- if (!table) { // if the editable tag is inside the table
- return;
- }
- var $editable = $(table).closest('.o_editable');
-
- $('.o_table_handler').remove();
-
- var $dels = $();
- var $adds = $();
- var $tds = $('tr:first', table).children();
- $tds.each(function () {
- var $td = $(this);
- var pos = $td.offset();
-
- var $del = $('<span class="o_table_handler fa fa-minus-square"/>').appendTo('body');
- $del.data('td', this);
- $dels = $dels.add($del);
- $del.css({
- left: ((pos.left + $td.outerWidth()/2)-6) + "px",
- top: (pos.top-6) + "px"
- });
-
- var $add = $('<span class="o_table_handler fa fa-plus-square"/>').appendTo('body');
- $add.data('td', this);
- $adds = $adds.add($add);
- $add.css({
- left: (pos.left-6) + "px",
- top: (pos.top-6) + "px"
- });
- });
-
- var $last = $tds.last();
- var pos = $last.offset();
- var $add = $('<span class="o_table_handler fa fa-plus-square"/>').appendTo('body');
- $adds = $adds.add($add);
- $add.css({
- left: (pos.left+$last.outerWidth()-6) + "px",
- top: (pos.top-6) + "px"
- });
-
- var $table = $(table);
- $dels.data('table', table).on('mousedown', function (event) {
- var td = $(this).data('td');
- $editable.data('NoteHistory').recordUndo($editable);
-
- var newTd;
- if ($(td).siblings().length) {
- var eq = $(td).index();
- $table.find('tr').each(function () {
- $('> td:eq('+eq+')', this).remove();
- });
- newTd = $table.find('tr:first > td:eq('+eq+'), tr:first > td:last').first();
- } else {
- var prev = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling($table[0])));
- $table.remove();
- $('.o_table_handler').remove();
- r = range.create(prev, prev.textContent.length);
- r.select();
- $(r.sc).trigger('mouseup');
- return;
- }
-
- $('.o_table_handler').remove();
- range.create(newTd[0], 0, newTd[0], 0).select();
- newTd.trigger('mouseup');
- });
- $adds.data('table', table).on('mousedown', function (event) {
- var td = $(this).data('td');
- $editable.data('NoteHistory').recordUndo($editable);
-
- var newTd;
- if (td) {
- var eq = $(td).index();
- $table.find('tr').each(function () {
- $('td:eq('+eq+')', this).before('<td>'+dom.blank+'</td>');
- });
- newTd = $table.find('tr:first td:eq('+eq+')');
- } else {
- $table.find('tr').each(function () {
- $(this).append('<td>'+dom.blank+'</td>');
- });
- newTd = $table.find('tr:first td:last');
- }
-
- $('.o_table_handler').remove();
- range.create(newTd[0], 0, newTd[0], 0).select();
- newTd.trigger('mouseup');
- });
-
- $dels.css({
- 'position': 'absolute',
- 'cursor': 'pointer',
- 'background-color': '#fff',
- 'color': '#ff0000'
- });
- $adds.css({
- 'position': 'absolute',
- 'cursor': 'pointer',
- 'background-color': '#fff',
- 'color': '#00ff00'
- });
-}
-var fn_popover_update = eventHandler.modules.popover.update;
-eventHandler.modules.popover.update = function ($popover, oStyle, isAirMode) {
- fn_popover_update.call(this, $popover, oStyle, isAirMode);
- if ((isAirMode ? $popover : $popover.parent()).find('.note-table').length) {
- summernote_table_update(oStyle);
- }
-};
-
-var fn_attach = eventHandler.attach;
-eventHandler.attach = function (oLayoutInfo, options) {
- var $editable = oLayoutInfo.editor().hasClass('note-editable') ? oLayoutInfo.editor() : oLayoutInfo.editor().find('.note-editable');
- fn_attach.call(this, oLayoutInfo, options);
- $editable.on("scroll", summernote_table_scroll);
-};
-var fn_detach = eventHandler.detach;
-eventHandler.detach = function (oLayoutInfo, options) {
- var $editable = oLayoutInfo.editor().hasClass('note-editable') ? oLayoutInfo.editor() : oLayoutInfo.editor().find('.note-editable');
- fn_detach.call(this, oLayoutInfo, options);
- $editable.off("scroll", summernote_table_scroll);
- $('.o_table_handler').remove();
-};
-
-options.icons.image.image = "file-image-o";
-$.summernote.lang['en-US'].image.image = "File / Image";
-
-return $.summernote;
-});
diff --git a/addons/web_editor/static/src/js/editor/transcoder.js b/addons/web_editor/static/src/js/editor/transcoder.js
deleted file mode 100644
index 85903f6b4eb..00000000000
--- a/addons/web_editor/static/src/js/editor/transcoder.js
+++ /dev/null
@@ -1,395 +0,0 @@
-odoo.define('web_editor.transcoder', function (require) {
-'use strict';
-
-var base = require('web_editor.base');
-
-var rulesCache = [];
-
-/**
- * Returns the css rules which applies on an element, tweaked so that they are
- * browser/mail client ok.
- *
- * @param {DOMElement} a
- * @returns {Object} css property name -> css property value
- */
-function getMatchedCSSRules(a) {
- var i, r, k;
- if (!rulesCache.length) {
- var sheets = document.styleSheets;
- for (i = sheets.length-1 ; i >= 0 ; i--) {
- var rules;
- // try...catch because browser may not able to enumerate rules for cross-domain sheets
- try {
- rules = sheets[i].rules || sheets[i].cssRules;
- } catch (e) {
- console.warn("Can't read the css rules of: " + sheets[i].href, e);
- continue;
- }
- if (rules) {
- for (r = rules.length-1; r >= 0; r--) {
- var selectorText = rules[r].selectorText;
- if (selectorText &&
- rules[r].cssText &&
- selectorText !== '*' &&
- selectorText.indexOf(':hover') === -1 &&
- selectorText.indexOf(':before') === -1 &&
- selectorText.indexOf(':after') === -1 &&
- selectorText.indexOf(':active') === -1 &&
- selectorText.indexOf(':link') === -1 &&
- selectorText.indexOf('::') === -1 &&
- selectorText.indexOf("'") === -1) {
- var st = selectorText.split(/\s*,\s*/);
- for (k = 0 ; k < st.length ; k++) {
- rulesCache.push({ 'selector': st[k], 'style': rules[r].style });
- }
- }
- }
- }
- }
- rulesCache.reverse();
- }
-
- var css = [];
- var style;
- a.matches = a.matches || a.webkitMatchesSelector || a.mozMatchesSelector || a.msMatchesSelector || a.oMatchesSelector;
- for (r = 0; r < rulesCache.length; r++) {
- if (a.matches(rulesCache[r].selector)) {
- style = rulesCache[r].style;
- if (style.parentRule) {
- var style_obj = {};
- var len;
- for (k = 0, len = style.length ; k < len ; k++) {
- if (style[k].indexOf('animation') !== -1) {
- continue;
- }
- style_obj[style[k]] = style[style[k].replace(/-(.)/g, function (a, b) { return b.toUpperCase(); })];
- if (new RegExp(style[k] + '\s*:[^:;]+!important' ).test(style.cssText)) {
- style_obj[style[k]] += ' !important';
- }
- }
- rulesCache[r].style = style = style_obj;
- }
- css.push([rulesCache[r].selector, style]);
- }
- }
-
- function specificity(selector) {
- // http://www.w3.org/TR/css3-selectors/#specificity
- var a = 0;
- selector = selector.replace(/#[a-z0-9_-]+/gi, function () { a++; return ''; });
- var b = 0;
- selector = selector.replace(/(\.[a-z0-9_-]+)|(\[.*?\])/gi, function () { b++; return ''; });
- var c = 0;
- selector = selector.replace(/(^|\s+|:+)[a-z0-9_-]+/gi, function (a) { if (a.indexOf(':not(')===-1) c++; return ''; });
- return a*100 + b*10 + c;
- }
- css.sort(function (a, b) { return specificity(a[0]) - specificity(b[0]); });
-
- style = {};
- _.each(css, function (v,k) {
- _.each(v[1], function (v,k) {
- if (v && _.isString(v) && k.indexOf('-webkit') === -1 && (!style[k] || style[k].indexOf('important') === -1 || v.indexOf('important') !== -1)) {
- style[k] = v;
- }
- });
- });
-
- _.each(style, function (v,k) {
- if (v.indexOf('important') !== -1) {
- style[k] = v.slice(0, v.length-11);
- }
- });
-
- if (style.display === 'block') {
- delete style.display;
- }
-
- // The css generates all the attributes separately and not in simplified form.
- // In order to have a better compatibility (outlook for example) we simplify the css tags.
- // e.g. border-left-style: none; border-bottom-s .... will be simplified in border-style = none
- _.each([
- {property: 'margin'},
- {property: 'padding'},
- {property: 'border', propertyEnd: '-style', defaultValue: 'none'},
- ], function (propertyInfo) {
- var p = propertyInfo.property;
- var e = propertyInfo.propertyEnd || '';
- var defVal = propertyInfo.defaultValue || 0;
-
- if (style[p+'-top'+e] || style[p+'-right'+e] || style[p+'-bottom'+e] || style[p+'-left'+e]) {
- if (style[p+'-top'+e] === style[p+'-right'+e] && style[p+'-top'+e] === style[p+'-bottom'+e] && style[p+'-top'+e] === style[p+'-left'+e]) {
- // keep => property: [top/right/bottom/left value];
- style[p+e] = style[p+'-top'+e];
- }
- else {
- // keep => property: [top value] [right value] [bottom value] [left value];
- style[p+e] = (style[p+'-top'+e] || defVal) + ' ' + (style[p+'-right'+e] || defVal) + ' ' + (style[p+'-bottom'+e] || defVal) + ' ' + (style[p+'-left'+e] || defVal);
- if (style[p+e].indexOf('inherit') !== -1 || style[p+e].indexOf('initial') !== -1) {
- // keep => property-top: [top value]; property-right: [right value]; property-bottom: [bottom value]; property-left: [left value];
- delete style[p+e];
- return;
- }
- }
- delete style[p+'-top'+e];
- delete style[p+'-right'+e];
- delete style[p+'-bottom'+e];
- delete style[p+'-left'+e];
- }
- });
-
- if (style['border-bottom-left-radius']) {
- style['border-radius'] = style['border-bottom-left-radius'];
- delete style['border-bottom-left-radius'];
- delete style['border-bottom-right-radius'];
- delete style['border-top-left-radius'];
- delete style['border-top-right-radius'];
- }
-
- // if the border styling is initial we remove it to simplify the css tags for compatibility.
- // Also, since we do not send a css style tag, the initial value of the border is useless.
- _.each(_.keys(style), function (k) {
- if (k.indexOf('border') !== -1 && style[k] === 'initial') {
- delete style[k];
- }
- });
-
- // text-decoration rule is decomposed in -line, -color and -style. This is
- // however not supported by many browser/mail clients and the editor does
- // not allow to change -color and -style rule anyway
- if (style['text-decoration-line']) {
- style['text-decoration'] = style['text-decoration-line'];
- delete style['text-decoration-line'];
- delete style['text-decoration-color'];
- delete style['text-decoration-style'];
- }
-
- // text-align inheritance does not seem to get past <td> elements on some
- // mail clients
- if (style['text-align'] === 'inherit') {
- var $el = $(a).parent();
- do {
- var align = $el.css('text-align');
- if (_.indexOf(['left', 'right', 'center', 'justify'], align) >= 0) {
- style['text-align'] = align;
- break;
- }
- $el = $el.parent();
- } while (!$el.is('html'));
- }
-
- return style;
-}
-
-/**
- * Converts font icons to images.
- *
- * @param {jQuery} $editable - the element in which the font icons have to be
- * converted to images
- */
-function fontToImg($editable) {
- $editable.find('.fa').each(function () {
- var $font = $(this);
- var icon, content;
- _.find(base.fontIcons, function (font) {
- return _.find(base.getCssSelectors(font.parser), function (data) {
- if ($font.is(data.selector.replace(/::?before/g, ''))) {
- icon = data.names[0].split('-').shift();
- content = data.css.match(/content:\s*['"]?(.)['"]?/)[1];
- return true;
- }
- });
- });
- if (content) {
- var color = $font.css('color').replace(/\s/g, '');
- $font.replaceWith($('<img/>', {
- src: _.str.sprintf('/web_editor/font_to_img/%s/%s/%s', content.charCodeAt(0), window.encodeURI(color), Math.max(1, $font.height())),
- 'data-class': $font.attr('class'),
- 'data-style': $font.attr('style'),
- class: $font.attr('class').replace(new RegExp('(^|\\s+)' + icon + '(-[^\\s]+)?', 'gi'), ''), // remove inline font-awsome style
- style: $font.attr('style'),
- }).css({height: 'auto', width: 'auto'}));
- } else {
- $font.remove();
- }
- });
-}
-
-/**
- * Converts images which were the result of a font icon convertion to a font
- * icon again.
- *
- * @param {jQuery} $editable - the element in which the images will be converted
- * back to font icons
- */
-function imgToFont($editable) {
- $editable.find('img[src*="/web_editor/font_to_img/"]').each(function () {
- var $img = $(this);
- $img.replaceWith($('<span/>', {
- class: $img.data('class'),
- style: $img.data('style')
- }));
- });
-}
-
-/*
- * Utility function to apply function over descendants elements
- *
- * This is needed until the following issue of jQuery is solved:
- * https://github.com./jquery/sizzle/issues/403
- *
- * @param {Element} node The root Element node
- * @param {Function} func The function applied over descendants
- */
-function applyOverDescendants(node, func) {
- node = node.firstChild;
- while (node) {
- if (node.nodeType === 1) {
- func(node);
- applyOverDescendants(node, func);
- }
- var $node = $(node);
- if (node.nodeName === 'A' && $node.hasClass('btn') && !$node.children().length && $(node).parents('.o_outlook_hack').length) {
- node = $(node).parents('.o_outlook_hack')[0];
- }
- else if (node.nodeName === 'IMG' && $node.parent('p').hasClass('o_outlook_hack')) {
- node = $node.parent()[0];
- }
- node = node.nextSibling;
- }
-}
-
-/**
- * Converts css style to inline style (leave the classes on elements but forces
- * the style they give as inline style).
- *
- * @param {jQuery} $editable
- */
-function classToStyle($editable) {
- if (!rulesCache.length) {
- getMatchedCSSRules($editable[0]);
- }
- applyOverDescendants($editable[0], function (node) {
- var $target = $(node);
- var css = getMatchedCSSRules(node);
- var style = $target.attr('style') || '';
- _.each(css, function (v,k) {
- if (!(new RegExp('(^|;)\s*' + k).test(style))) {
- style = k+':'+v+';'+style;
- }
- });
- if (_.isEmpty(style)) {
- $target.removeAttr('style');
- } else {
- $target.attr('style', style);
- }
- // Apple Mail
- if (node.nodeName === 'TD' && !node.childNodes.length) {
- node.innerHTML = '&nbsp;';
- }
-
- // Outlook
- if (node.nodeName === 'A' && $target.hasClass('btn') && !$target.hasClass('btn-link') && !$target.children().length) {
- var $hack = $('<table class="o_outlook_hack" style="display: inline-table;vertical-align:middle"><tr><td></td></tr></table>');
- $hack.find('td')
- .attr('height', $target.outerHeight())
- .css({
- 'text-align': $target.parent().css('text-align'),
- 'margin': $target.css('padding'),
- 'border-radius': $target.css('border-radius'),
- 'background-color': $target.css('background-color'),
- });
- $target.after($hack);
- $target.appendTo($hack.find('td'));
- // the space add a line when it's a table but it's invisible when it's a link
- node = $hack[0].previousSibling;
- if (node && node.nodeType === Node.TEXT_NODE && !node.textContent.match(/\S/)) {
- $(node).remove();
- }
- node = $hack[0].nextSibling;
- if (node && node.nodeType === Node.TEXT_NODE && !node.textContent.match(/\S/)) {
- $(node).remove();
- }
- }
- else if (node.nodeName === 'IMG' && $target.is('.mx-auto.d-block')) {
- $target.wrap('<p class="o_outlook_hack" style="text-align:center;margin:0"/>');
- }
- });
-}
-
-/**
- * Removes the inline style which is not necessary (because, for example, a
- * class on an element will induce the same style).
- *
- * @param {jQuery} $editable
- */
-function styleToClass($editable) {
- // Outlook revert
- $editable.find('.o_outlook_hack').each(function () {
- $(this).after($('a,img', this));
- }).remove();
-
- getMatchedCSSRules($editable[0]);
-
- var $c = $('<span/>').appendTo(document.body);
-
- applyOverDescendants($editable[0], function (node) {
- var $target = $(node);
- var css = getMatchedCSSRules(node);
- var style = '';
- _.each(css, function (v,k) {
- if (!(new RegExp('(^|;)\s*' + k).test(style))) {
- style = k+':'+v+';'+style;
- }
- });
- css = ($c.attr('style', style).attr('style') || '').split(/\s*;\s*/);
- style = $target.attr('style') || '';
- _.each(css, function (v) {
- style = style.replace(v, '');
- });
- style = style.replace(/;+(\s;)*/g, ';').replace(/^;/g, '');
- if (style !== '') {
- $target.attr('style', style);
- } else {
- $target.removeAttr('style');
- }
- });
- $c.remove();
-}
-
-/**
- * Converts css display for attachment link to real image.
- * Without this post process, the display depends on the css and the picture
- * does not appear when we use the html without css (to send by email for e.g.)
- *
- * @param {jQuery} $editable
- */
-function attachmentThumbnailToLinkImg($editable) {
- $editable.find('a[href*="/web/content/"][data-mimetype]:empty').each(function () {
- var $link = $(this);
- var $img = $('<img/>')
- .attr('src', $link.css('background-image').replace(/(^url\(['"])|(['"]\)$)/g, ''))
- .css('height', Math.max(1, $link.height()) + 'px')
- .css('width', Math.max(1, $link.width()) + 'px');
- $link.append($img);
- });
-}
-
-/**
- * Revert attachmentThumbnailToLinkImg changes
- *
- * @see attachmentThumbnailToLinkImg
- * @param {jQuery} $editable
- */
-function linkImgToAttachmentThumbnail($editable) {
- $editable.find('a[href*="/web/content/"][data-mimetype] > img').remove();
-}
-
-return {
- fontToImg: fontToImg,
- imgToFont: imgToFont,
- classToStyle: classToStyle,
- styleToClass: styleToClass,
- attachmentThumbnailToLinkImg: attachmentThumbnailToLinkImg,
- linkImgToAttachmentThumbnail: linkImgToAttachmentThumbnail,
-};
-});
diff --git a/addons/web_editor/static/src/js/editor/translator.js b/addons/web_editor/static/src/js/editor/translator.js
deleted file mode 100644
index c6cadb40562..00000000000
--- a/addons/web_editor/static/src/js/editor/translator.js
+++ /dev/null
@@ -1,375 +0,0 @@
-odoo.define('web_editor.translate', function (require) {
-'use strict';
-
-var core = require('web.core');
-var Dialog = require('web.Dialog');
-var localStorage = require('web.local_storage');
-var Widget = require('web.Widget');
-var weContext = require('web_editor.context');
-var rte = require('web_editor.rte');
-var weWidgets = require('web_editor.widget');
-var Dialog = require('web.Dialog');
-
-var _t = core._t;
-
-var localStorageNoDialogKey = 'website_translator_nodialog';
-
-var RTETranslatorWidget = rte.Class.extend({
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * If the element holds a translation, saves it. Otherwise, fallback to the
- * standard saving but with the lang kept.
- *
- * @override
- */
- _saveElement: function ($el, context, withLang) {
- var self = this;
- if ($el.data('oe-translation-id')) {
- return this._rpc({
- model: 'ir.translation',
- method: 'save_html',
- args: [
- [+$el.data('oe-translation-id')],
- this._getEscapedElement($el).html()
- ],
- context: context,
- }).fail(function (error) {
- Dialog.alert(null, error.data.message);
- });
- }
- return this._super($el, context, withLang === undefined ? true : withLang);
- },
-});
-
-var AttributeTranslateDialog = weWidgets.Dialog.extend({
- /**
- * @constructor
- */
- init: function (parent, options, node) {
- this._super(parent, _.extend({
- title: _t("Translate Attribute"),
- buttons: [
- {text: _t("Close"), classes: 'btn-primary', click: this.save}
- ],
- }, options || {}));
- this.$target = $(node);
- this.translation = $(node).data('translation');
- },
- /**
- * @override
- */
- start: function () {
- var self = this;
- var $group = $('<div/>', {class: 'form-group'}).appendTo(this.$el);
- _.each(this.translation, function (node, attr) {
- var $node = $(node);
- var $label = $('<label class="col-form-label"></label>').text(attr);
- var $input = $('<input class="form-control"/>').val($node.html());
- $input.on('change keyup', function () {
- var value = $input.val();
- $node.html(value).trigger('change', node);
- $node.data('$node').attr($node.data('attribute'), value).trigger('translate');
- self.trigger_up('rte_change', {target: node});
- });
- $group.append($label).append($input);
- });
- return this._super.apply(this, arguments);
- }
-});
-
-var TranslatorInfoDialog = Dialog.extend({
- template: 'web_editor.TranslatorInfoDialog',
- xmlDependencies: Dialog.prototype.xmlDependencies.concat(
- ['/web_editor/static/src/xml/translator.xml']
- ),
-
- /**
- * @constructor
- */
- init: function (parent, options) {
- this._super(parent, _.extend({
- title: _t("Translation Info"),
- buttons: [
- {text: _t("Ok, never show me this again"), classes: 'btn-primary', close: true, click: this._onStrongOk.bind(this)},
- {text: _t("Ok"), close: true}
- ],
- }, options || {}));
- },
-
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
-
- /**
- * Called when the "strong" ok is clicked -> adapt localstorage to make sure
- * the dialog is never displayed again.
- *
- * @private
- */
- _onStrongOk: function () {
- localStorage.setItem(localStorageNoDialogKey, true);
- },
-});
-
-var TranslatorMenuBar = Widget.extend({
- template: 'web_editor.editorbar',
- xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
- events: {
- 'click [data-action="save"]': '_onSaveClick',
- 'click [data-action="cancel"]': '_onCancelClick',
- },
- custom_events: {
- 'rte_change': '_onRTEChange',
- },
-
- /**
- * @constructor
- */
- init: function (parent, $target, lang) {
- this._super.apply(this, arguments);
-
- var $edit = $target.find('[data-oe-translation-id], [data-oe-model][data-oe-id][data-oe-field]');
- $edit.filter(':has([data-oe-translation-id], [data-oe-model][data-oe-id][data-oe-field])').attr('data-oe-readonly', true);
- this.$target = $edit.not('[data-oe-readonly]');
- var attrs = ['placeholder', 'title', 'alt'];
- _.each(attrs, function (attr) {
- $target.find('['+attr+'*="data-oe-translation-id="]').filter(':empty, input, select, textarea, img').each(function () {
- var $node = $(this);
- var translation = $node.data('translation') || {};
- var trans = $node.attr(attr);
- var match = trans.match(/<span [^>]*data-oe-translation-id="([0-9]+)"[^>]*>(.*)<\/span>/);
- var $trans = $(trans).addClass('d-none o_editable o_editable_translatable_attribute').appendTo('body');
- $trans.data('$node', $node).data('attribute', attr);
- translation[attr] = $trans[0];
- $node.attr(attr, match[2]);
-
- var select2 = $node.data('select2');
- if (select2) {
- select2.blur();
- $node.on('translate', function () {
- select2.blur();
- });
- $node = select2.container.find('input');
- }
- $node.addClass('o_translatable_attribute').data('translation', translation);
- });
- });
- this.$target_attr = $target.find('.o_translatable_attribute');
- this.$target_attribute = $('.o_editable_translatable_attribute');
-
- this.lang = lang || weContext.get().lang;
-
- this.rte = new RTETranslatorWidget(this, this._getRTEConfig);
- },
- /**
- * @override
- */
- start: function () {
- this.$('#web_editor-toolbars').remove();
-
- var flag = false;
- window.onbeforeunload = function (event) {
- if ($('.o_editable.o_dirty').length && !flag) {
- flag = true;
- setTimeout(function () {
- flag = false;
- }, 0);
- return _t('This document is not saved!');
- }
- };
- this.$target.addClass('o_editable');
- this.rte.start();
- this.translations = [];
- this._markTranslatableNodes();
- this.$el.show(); // TODO seems useless
- this.trigger('edit');
-
- if (!localStorage.getItem(localStorageNoDialogKey)) {
- new TranslatorInfoDialog(this).open();
- }
-
- return this._super.apply(this, arguments);
- },
- /**
- * @override
- */
- destroy: function () {
- this._cancel();
- this._super.apply(this, arguments);
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Leaves translation mode by hiding the translator bar.
- *
- * @todo should be destroyed?
- * @private
- */
- _cancel: function () {
- var self = this;
- this.rte.cancel();
- this.$target.each(function () {
- $(this).html(self._getTranlationObject(this).value);
- });
- this._unmarkTranslatableNode();
- this.trigger('cancel');
- this.$el.hide();
- window.onbeforeunload = null;
- },
- /**
- * Returns the RTE summernote configuration for translation mode.
- *
- * @private
- * @param {jQuery} $editable
- */
- _getRTEConfig: function ($editable) {
- return {
- airMode : true,
- focus: false,
- airPopover: $editable.data('oe-model') ? [
- ['history', ['undo', 'redo']],
- ] : [
- ['font', ['bold', 'italic', 'underline', 'clear']],
- ['fontsize', ['fontsize']],
- ['color', ['color']],
- ['history', ['undo', 'redo']],
- ],
- styleWithSpan: false,
- inlinemedia : ['p'],
- lang: 'odoo',
- onChange: function (html, $editable) {
- $editable.trigger('content_changed');
- },
- };
- },
- /**
- * @private
- */
- _getTranlationObject: function (node) {
- var $node = $(node);
- var id = +$node.data('oe-translation-id');
- if (!id) {
- id = $node.data('oe-model')+','+$node.data('oe-id')+','+$node.data('oe-field');
- }
- var trans = _.find(this.translations, function (trans) {
- return trans.id === id;
- });
- if (!trans) {
- this.translations.push(trans = {'id': id});
- }
- return trans;
- },
- /**
- * @private
- */
- _markTranslatableNodes: function () {
- var self = this;
- this.$target.add(this.$target_attribute).each(function () {
- var $node = $(this);
- var trans = self._getTranlationObject(this);
- trans.value = (trans.value ? trans.value : $node.html() ).replace(/[ \t\n\r]+/, ' ');
- });
- this.$target.parent().prependEvent('click.translator', function (ev) {
- if (ev.ctrlKey || !$(ev.target).is(':o_editable')) {
- return;
- }
- ev.preventDefault();
- ev.stopPropagation();
- });
-
- // attributes
-
- this.$target_attr.each(function () {
- var $node = $(this);
- var translation = $node.data('translation');
- _.each(translation, function (node, attr) {
- var trans = self._getTranlationObject(node);
- trans.value = (trans.value ? trans.value : $node.html() ).replace(/[ \t\n\r]+/, ' ');
- $node.attr('data-oe-translation-state', (trans.state || 'to_translate'));
- });
- });
-
- this.$target_attr.prependEvent('mousedown.translator click.translator mouseup.translator', function (ev) {
- if (ev.ctrlKey) {
- return;
- }
- ev.preventDefault();
- ev.stopPropagation();
- if (ev.type !== 'mousedown') {
- return;
- }
-
- new AttributeTranslateDialog(self, {}, ev.target).open();
- });
- },
- /**
- * Saves the translation and reloads the page to leave edit mode.
- *
- * @private
- * @returns {Deferred} (never resolved as the page is reloading anyway)
- */
- _save: function () {
- return this.rte.save(weContext.get({lang: this.lang})).then(function () {
- window.location.href = window.location.href.replace(/&?edit_translations(=[^&]*)?/g, '');
- return $.Deferred();
- });
- },
- /**
- * @private
- */
- _unmarkTranslatableNode: function () {
- this.$target.removeClass('o_editable').removeAttr('contentEditable');
- this.$target.parent().off('.translator');
- this.$target_attr.off('.translator');
- },
-
- //--------------------------------------------------------------------------
- // Handlers
- //--------------------------------------------------------------------------
-
- /**
- * Called when the "cancel" button is clicked -> undo changes and leaves
- * edition.
- *
- * @private
- */
- _onCancelClick: function () {
- this._cancel();
- },
- /**
- * Called when text is edited -> make sure text is not messed up and mark
- * the element as dirty.
- *
- * @private
- * @param {OdooEvent} ev
- */
- _onRTEChange: function (ev) {
- var $node = $(ev.data.target);
- $node.find('p').each(function () { // remove <p/> element which might have been inserted because of copy-paste
- var $p = $(this);
- $p.after($p.html()).remove();
- });
- var trans = this._getTranlationObject($node[0]);
- $node.toggleClass('o_dirty', trans.value !== $node.html().replace(/[ \t\n\r]+/, ' '));
- },
- /**
- * Called when the "save" button is clicked -> saves the translations.
- *
- * @private
- */
- _onSaveClick: function () {
- this._save();
- },
-});
-
-return {
- Class: TranslatorMenuBar,
-};
-});