diff options
| author | Robot Odoo <xmo+roboto@odoo.com> | 2019-01-17 19:17:22 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2019-01-17 19:17:22 +0100 |
| commit | 22efdfbecc1e8c688000be98903a5a355d1ece8a (patch) | |
| tree | e097625171b2b67ed5f5288ced67446608b9f633 /addons/web_editor/static/src/js/editor | |
| parent | f50fbc6d168f0d17fa1e932b4ecc968a92b02b65 (diff) | |
| parent | eb0ea8efbe932c140302e9f8da33a839bcd73146 (diff) | |
[IMP] web_editor,*: Refactoring the wysiwyg editor and 'html' field
_Task #1878502_
- The refactoring allows for the update of the _Summernote_ library.
- A new structure was created by transforming all the plugins in the library using the odoo inheritance system. Plugins are easier to implement with the `AbstractPlugin` to add Odoo behaviors.
- The cleanup allowed us to remove some of the code that was in `web_editor` but only used in `website` or in `mass_mailing`. Some parts are still in `web_editor` but will be moved at a later time.
- The refactoring also had to be done in order to create a set of unit tests. Each behavior can be tested, including the behaviors performed as a consequence of keyboard interactions (for instance: Enter, Tab...).
- From now on, the methods of the library (in this case _Summernote_) can no longer be called by other modules or files. Only the `wysiwyg` widgets can access it, to simplify the updating process. The `wysiwyg` object serves as an interface. A lot of tests are done, ensuring the consistency of outside behavior.
- Depending on the options the snippets will be loaded or not, the editor will be in an iframe or not... all of this is transparent from the outside.
- Regarding iframes, all controllers related to editing have been removed: the new API no longer needs them. This speeds up loading, eases testing and removes complexity for the same features.
**PUBLIC FEATURES**
There are several public methods on the `Wysiwyg` class:
- `Wysiwyg.prepare (WidgetParent)` which returns a deferred resolved when the library (xml, lazy, assets...) is loaded.
- `Wysiwyg.getRange (DOM)` returns the range (selection in the dom)
- `Wysiwyg.setRange (startNode, startOffset, endNode, endOffset)` which creates a range (selection in the dom)
- `Wysiwyg.setRangeFromNode (DOM, options)` that creates a range from an element (option available to select all, start or end)
A jQuery selector was added: `:o_editable`, which indicates whether the current element is editable. That is, if it is contained in a tag with the attribute `contentEditable = "true"` or in a tag with the class `o_editable`.
Several methods are also present:
- `focusIn`: makes a focus and places the cursor at the beginning of the element
- `focusInEnd`: makes a focus and places the cursor at the end of the element
- `selectContent`: makes a focus and selects the content
**HTML FIELD**
The HTML field can receive different options:
- `style-inline:` {boolean} transforms a class into an inline style when saving and vice versa when reading.
- `no-attachment`: {boolean} prevents the use of attachments (in media dialog)
- `cssEdit`: {xml_id} to use a template containing the css to loaded in an iframe when editing
- `cssReadonly`: {xml_id} to use a template containing the css to load into an iframe when viewing in readonly
- `snippets`: {xml_id} snippets template (can be used with or without `cssEdit`)
- `wrapper`: {template} qweb static template (containing a tag: `id = "wrapper"`) that will include the content during editing (removed on save)
**MASS MAILING**
A widget was created for `mass mailing`. There are now two fields: `body_html` and `body_arch`.
`body_arch` contains the code with the class without conversion into inline style, useful when editing and one with the inline style that is visible in readonly mode and sent by email.
Advantage: no spreading errors, able to update css/theme, able to do more changes when converting to inline style so that a maximum of mail clients have an impeccable rendering.
**REMAINING KNOW ISSUES**
These will be fixed in a subsequent commit.
- A few committed changes made during the development of this refactoring were lost in the process and should be reapplied.
- Some minor (mostly range) issues remain with Firefox.
--
I confirm I have signed the CLA and read the PR guidelines at www.odoo.com/submit-pr
closes odoo/odoo#29775
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, -}; -}); |
