diff options
Diffstat (limited to 'addons/web_editor/static/src/js/editor')
| -rw-r--r-- | addons/web_editor/static/src/js/editor/editor.js | 286 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/editor/rte.js | 749 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/editor/rte.summernote.js | 1274 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/editor/snippets.editor.js | 1408 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/editor/snippets.options.js | 1317 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/editor/summernote.js | 2436 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/editor/transcoder.js | 395 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/editor/translator.js | 375 |
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(/"/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 = ' '; - } - - // 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, -}; -}); |
