diff options
| author | Christophe Simonis <chs@odoo.com> | 2019-09-13 18:07:59 +0200 |
|---|---|---|
| committer | Christophe Simonis <chs@odoo.com> | 2019-09-13 18:07:59 +0200 |
| commit | 6c948c36bbf72dd896e0bff03aa7fbddadcbb0cd (patch) | |
| tree | f8620905c011735d8a3c340c46d1417a54a75972 /addons/web_editor/static/src/js | |
| parent | 48503fcf08eac6fed12e080d4f501f9bbc8d257d (diff) | |
| parent | 197474e5b9ad43ab2d587a98136a1a809ce9a2e7 (diff) | |
[MERGE] forward port branch saas-12.5 up to 197474e5b9a
Diffstat (limited to 'addons/web_editor/static/src/js')
6 files changed, 597 insertions, 434 deletions
diff --git a/addons/web_editor/static/src/js/editor/editor.js b/addons/web_editor/static/src/js/editor/editor.js index 07947db56e4..31c31e5ce6a 100644 --- a/addons/web_editor/static/src/js/editor/editor.js +++ b/addons/web_editor/static/src/js/editor/editor.js @@ -106,7 +106,9 @@ var EditorMenuBar = Widget.extend({ // Snippets menu if (self.snippetsMenu) { - defs.push(this.snippetsMenu.insertAfter(this.$el)); + // TODO improve this, the editor can be relocated so here, + // 'insertAfter(this.$el)' won't work. + defs.push(this.snippetsMenu.appendTo(this.$el.parent())); } this.rte.editable().find('*').off('mousedown mouseup click'); diff --git a/addons/web_editor/static/src/js/editor/snippets.editor.js b/addons/web_editor/static/src/js/editor/snippets.editor.js index 4fba95f2490..c9571f59c51 100644 --- a/addons/web_editor/static/src/js/editor/snippets.editor.js +++ b/addons/web_editor/static/src/js/editor/snippets.editor.js @@ -1,6 +1,7 @@ odoo.define('web_editor.snippet.editor', function (require) { 'use strict'; +var concurrency = require('web.concurrency'); var core = require('web.core'); var dom = require('web.dom'); var Widget = require('web.Widget'); @@ -10,9 +11,9 @@ var Wysiwyg = require('web_editor.wysiwyg'); var _t = core._t; var globalSelector = { - closest: function () { return $(); }, - all: function () { return $(); }, - is: function () { return false; }, + closest: () => $(), + all: () => $(), + is: () => false, }; /** @@ -21,22 +22,15 @@ var globalSelector = { 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', - 'mousedown .oe_options [data-toggle="dropdown"]:first': '_onOpenCusomize', - }, custom_events: { - cover_update: '_onCoverUpdate', - option_update: '_onOptionUpdate', + 'option_update': '_onOptionUpdate', }, /** * @constructor * @param {Widget} parent * @param {Element} target - * @param templateOptions + * @param {Object} templateOptions * @param {jQuery} $editable * @param {Object} options */ @@ -49,58 +43,75 @@ var SnippetEditor = Widget.extend({ this.$target = $(target); this.$target.data('snippet-editor', this); this.templateOptions = templateOptions; + this.isTargetParentEditable = false; + this.isTargetMovable = false; }, /** * @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()); + var $customize = this._customize$Elements[this._customize$Elements.length - 1]; + + this.isTargetParentEditable = this.$target.parent().is(':o_editable'); + this.isTargetMovable = this.isTargetParentEditable && this.isTargetMovable; // Initialize move/clone/remove buttons - if (!this.$target.is(':o_editable')) { - this.$el.find('.oe_snippet_move, .oe_snippet_clone, .oe_snippet_remove').remove(); - } else { + if (this.isTargetMovable) { this.dropped = false; this.$el.draggable({ greedy: true, appendTo: this.$body, cursor: 'move', - handle: '.oe_snippet_move', + handle: '.o_move_handle', 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(self.$body).removeClass('d-none'); + helper: () => { + var $clone = this.$el.clone().css({width: '24px', height: '24px', border: 0}); + $clone.appendTo(this.$body).removeClass('d-none'); return $clone; }, - start: _.bind(self._onDragAndDropStart, self), - stop: _.bind(self._onDragAndDropStop, self) + start: this._onDragAndDropStart.bind(this), + stop: this._onDragAndDropStop.bind(this), }); + } else { + this.$('.o_move_handle').addClass('d-none'); + $customize.find('.oe_snippet_clone').addClass('d-none'); } - this.$target.on('transitionstart.snippet_editor, animationstart.snippet_editor', function () { - self._targetIsAnimated = true; - }); - this.$target.on('transitionend.snippet_editor, animationend.snippet_editor', function () { - self._targetIsAnimated = false; - if (self.$el.is('.oe_active')) { - self.cover(); - } + if (!this.isTargetParentEditable) { + $customize.find('.oe_snippet_remove').addClass('d-none'); + } + + var _animationsCount = 0; + var postAnimationCover = _.throttle(() => this.cover(), 100); + this.$target.on('transitionstart.snippet_editor, animationstart.snippet_editor', () => { + // We cannot rely on the fact each transition/animation start will + // trigger a transition/animation end as the element may be removed + // from the DOM before or it could simply be an infinite animation. + // + // By simplicity, for each start, we add a delayed operation that + // will decrease the animation counter after a fixed duration and + // do the post animation cover if none is registered anymore. + _animationsCount++; + setTimeout(() => { + if (!--_animationsCount) { + postAnimationCover(); + } + }, 500); // This delay have to be huge enough to take care of long + // animations which will not trigger an animation end event + // but if it is too small for some, this is the job of the + // animation creator to manually ask for a re-cover }); + // On top of what is explained above, do the post animation cover for + // each detected transition/animation end so that the user does not see + // a flickering when not needed. + this.$target.on('transitionend.snippet_editor, animationend.snippet_editor', postAnimationCover); return Promise.all(defs); }, @@ -143,9 +154,7 @@ var SnippetEditor = Widget.extend({ * Makes the editor overlay cover the associated snippet. */ cover: function () { - if (this._targetIsAnimated) { - // Do not cover a target being animated, it will be covered once the - // animation is completed. + if (!this.isShown() || !this.$target.length) { return; } var offset = this.$target.offset(); @@ -161,6 +170,12 @@ var SnippetEditor = Widget.extend({ this.$el.toggleClass('o_top_cover', offset.top < this.$editable.offset().top); }, /** + * @return {boolean} + */ + isShown: function () { + return this.$el && this.$el.parent().length && this.$el.hasClass('oe_active'); + }, + /** * Removes the associated snippet from the DOM and destroys the associated * editor (itself). */ @@ -176,6 +191,7 @@ var SnippetEditor = Widget.extend({ }, }); + this.trigger_up('go_to_parent', {$snippet: this.$target}); var $parent = this.$target.parent(); this.$target.find('*').addBack().tooltip('dispose'); this.$target.remove(); @@ -189,17 +205,17 @@ var SnippetEditor = Widget.extend({ } if ($parent.closest(':data("snippet-editor")').length) { - while (!$parent.data('snippet-editor')) { + var editor = $parent.data('snippet-editor'); + while (!editor) { var $nextParent = $parent.parent(); - if ($parent.children().length === 0 && $parent.text().trim() === '' && !$parent.hasClass('oe_structure')) { + if (isEmptyAndRemovable($parent)) { $parent.remove(); } $parent = $nextParent; + editor = $parent.data('snippet-editor'); } - if ($parent.children().length === 0 && $parent.text().trim() === '' && !$parent.hasClass('oe_structure')) { - _.defer(function () { - $parent.data('snippet-editor').removeSnippet(); - }); + if (isEmptyAndRemovable($parent, editor)) { + setTimeout(() => editor.removeSnippet()); } } @@ -210,6 +226,12 @@ var SnippetEditor = Widget.extend({ this.trigger_up('snippet_removed'); this.destroy(); $parent.trigger('content_changed'); + + function isEmptyAndRemovable($el, editor) { + editor = editor || $el.data('snippet-editor'); + return $el.children().length === 0 && $el.text().trim() === '' + && !$el.hasClass('oe_structure') && (!editor || editor.isTargetParentEditable); + } }, /** * Displays/Hides the editor overlay and notifies the associated snippet @@ -217,51 +239,63 @@ var SnippetEditor = Widget.extend({ * snippet options are moved to the editor overlay. * * @param {boolean} focus - true to display, false to hide + * @param {boolean} [previewMode=false] */ - toggleFocus: function (focus) { + toggleFocus: function (focus, previewMode) { if (!this.$el) { return; } - 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)); + if (previewMode) { + // In preview mode, the sticky classes are left untouched, we only + // add/remove the preview class when toggling/untoggling + this.$el.toggleClass('o_we_overlay_preview', focus); + } else { + // In non preview mode, the preview class is always removed, and the + // sticky class is added/removed when toggling/untoggling + this.$el.removeClass('o_we_overlay_preview'); + this.$el.toggleClass('o_we_overlay_sticky', focus); + } - // Activate the overlay - $style_button.toggleClass('d-none', $ul.children(':not(.o_main_header):not(.dropdown-divider):not(.d-none)').length === 0); + focus = this.$el.hasClass('o_we_overlay_sticky') ? true : focus; + + // Show/hide overlay in preview mode or not + this.$el.toggleClass('oe_active', focus); this.cover(); - this.$el.toggleClass('oe_active', !!focus); - if (focus) { - this.trigger_up('snippet_focused'); + // In non-preview mode, update the options panel if necessary + if (previewMode) { + return; } - - function _do_action_focus(style, $dest) { - style.$el.insertAfter($dest); - style.onFocus(); - return (style.$el.length > 0); + var lastIndex = this._customize$Elements.length - 1; + var optionsAlreadyShown = !!this._customize$Elements[lastIndex].parent().length; + if (optionsAlreadyShown === focus) { + return; } - function _do_action_blur(style, $dest) { - style.$el.detach(); - style.onBlur(); - return false; + this.trigger_up('update_customize_elements', { + customize$Elements: focus ? this._customize$Elements : [], + }); + this._customize$Elements.forEach(($el, i) => { + var editor = $el.data('editor'); + var styles = _.values(editor.styles); + $el.toggleClass('d-none', styles.length === 0); + _.sortBy(styles, '__order').reverse().forEach(style => { + if (focus) { + style.$el.appendTo($el); + style.onFocus(); + } else { + style.$el.detach(); + style.onBlur(); + } + }); + }); + }, + /** + * @param {boolean} [isTextEdition=false] + */ + toggleTextEdition: function (isTextEdition) { + if (this.$el) { + this.$el.toggleClass('o_keypress', !!isTextEdition && this.isShown()); } }, @@ -291,53 +325,61 @@ var SnippetEditor = Widget.extend({ * @private */ _initializeOptions: function () { - var self = this; - var $styles = this.$el.find('.oe_options'); - var $ul = $styles.find('.dropdown-menu:first'); + this._customize$Elements = []; this.styles = {}; this.selectorSiblings = []; this.selectorChildren = []; + var $element = this.$target.parent(); + while ($element.length) { + var parentEditor = $element.data('snippet-editor'); + if (parentEditor) { + this._customize$Elements = this._customize$Elements + .concat(parentEditor._customize$Elements); + break; + } + $element = $element.parent(); + } + + var $optionsSection = $(core.qweb.render('web_editor.customize_block_options_section', { + name: this._getName(), + })).data('editor', this); + $optionsSection.on('mouseover', this._onOptionsSectionMouseOver.bind(this)); + $optionsSection.on('mouseleave', this._onOptionsSectionMouseLeave.bind(this)); + $optionsSection.on('click', this._onOptionsSectionClick.bind(this)); + $optionsSection.on('click', '.oe_snippet_clone', this._onCloneClick.bind(this)); + $optionsSection.on('click', '.oe_snippet_remove', this._onRemoveClick.bind(this)); + this._customize$Elements.push($optionsSection); + 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)) { + var defs = _.map(this.templateOptions, val => { + if (!val.selector.is(this.$target)) { return; } - if (val['drop-near']) self.selectorSiblings.push(val['drop-near']); - if (val['drop-in']) self.selectorChildren.push(val['drop-in']); + if (val['drop-near']) { + this.selectorSiblings.push(val['drop-near']); + } + if (val['drop-in']) { + this.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, + this, + val.base_target ? this.$target.find(val.base_target).eq(0) : this.$target, + this.$el, val.data, - self.options + this.options ); - self.styles[optionName || _.uniqueId('option')] = option; + this.styles[optionName || _.uniqueId('option')] = option; option.__order = i++; - return option.attachTo($el); + var $optionSection = $(core.qweb.render('web_editor.customize_block_option')); + $optionSection.append($el); + return option.attachTo($optionSection); }); - $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.isTargetMovable = (this.selectorSiblings.length > 0 || this.selectorChildren.length > 0); this.$el.find('[data-toggle="dropdown"]').dropdown(); @@ -356,7 +398,7 @@ var SnippetEditor = Widget.extend({ */ _onCloneClick: function (ev) { ev.preventDefault(); - this.trigger_up('cover_will_change'); + ev.stopPropagation(); this.trigger_up('snippet_will_be_cloned', {$target: this.$target}); @@ -378,15 +420,6 @@ var SnippetEditor = Widget.extend({ $clone.trigger('content_changed'); }, /** - * Called when the overlay dimensions/positions should be recomputed. - * - * @private - */ - _onCoverUpdate: function () { - this.trigger_up('cover_will_change'); - this.cover(); - }, - /** * Called when the snippet is starting to be dragged thanks to the 'move' * button. * @@ -404,14 +437,20 @@ var SnippetEditor = Widget.extend({ 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()); + 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()); + 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}); @@ -434,8 +473,6 @@ var SnippetEditor = Widget.extend({ self.dropped = false; }, }); - - this.trigger_up('cover_will_change'); }, /** * Called when the snippet is dropped after being dragged thanks to the @@ -446,8 +483,6 @@ var SnippetEditor = Widget.extend({ * @param {Object} ui */ _onDragAndDropStop: function (ev, ui) { - this.$editable.find('.oe_drop_zone').droppable('destroy').remove(); - // TODO lot of this is duplicated code of the d&d feature of snippets if (!this.dropped) { var $el = $.nearest({x: ui.position.left, y: ui.position.top}, '.oe_drop_zone', {container: document.body}).first(); @@ -457,6 +492,8 @@ var SnippetEditor = Widget.extend({ } } + this.$editable.find('.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(); @@ -498,6 +535,33 @@ var SnippetEditor = Widget.extend({ }); }, /** + * @private + */ + _onOptionsSectionMouseOver: function (ev) { + this.trigger_up('activate_snippet', { + $snippet: this.$target, + previewMode: true, + }); + }, + /** + * @private + */ + _onOptionsSectionMouseLeave: function (ev) { + this.trigger_up('activate_snippet', { + $snippet: false, + previewMode: true, + }); + }, + /** + * @private + */ + _onOptionsSectionClick: function (ev) { + this.trigger_up('activate_snippet', { + $snippet: this.$target, + previewMode: false, + }); + }, + /** * Called when a child editor/option asks for another option to perform a * specific action/react to a specific event. * @@ -505,7 +569,6 @@ var SnippetEditor = Widget.extend({ * @param {OdooEvent} ev */ _onOptionUpdate: function (ev) { - this.trigger_up('cover_will_change'); // If multiple option names are given, we suppose it should not be // propagated to parent editor if (ev.data.optionNames) { @@ -529,28 +592,6 @@ var SnippetEditor = Widget.extend({ } }, /** - * Called when the user opens the customize menu. - * - * @private - * @param {OdooEvent} ev - */ - _onOpenCusomize: function (ev) { - this.trigger_up('request_history_undo_record', {$target: this.$target}); - }, - - /** - * 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 @@ -558,7 +599,7 @@ var SnippetEditor = Widget.extend({ */ _onRemoveClick: function (ev) { ev.preventDefault(); - this.trigger_up('cover_will_change'); + ev.stopPropagation(); this.trigger_up('request_history_undo_record', {$target: this.$target}); this.removeSnippet(); }, @@ -570,16 +611,21 @@ var SnippetEditor = Widget.extend({ var SnippetsMenu = Widget.extend({ id: 'oe_snippets', cacheSnippetTemplate: {}, - activeSnippets: [], + events: { + 'click we-collapse-area > we-toggler': '_onCollapseTogglerClick', + }, 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', - reload_snippet_dropzones: '_disableUndroppableSnippets', + 'activate_insertion_zones': '_onActivateInsertionZones', + 'activate_snippet': '_onActivateSnippet', + 'call_for_each_child_snippet': '_onCallForEachChildSnippet', + 'cover_update': '_onOverlaysCoverUpdate', + 'deactivate_snippet': '_onDeactivateSnippet', + 'drag_and_drop_stop': '_onDragAndDropStop', + 'go_to_parent': '_onGoToParent', + 'remove_snippet': '_onRemoveSnippet', + 'snippet_removed': '_onSnippetRemoved', + 'reload_snippet_dropzones': '_disableUndroppableSnippets', + 'update_customize_elements': '_onUpdateCustomizeElements', }, /** @@ -607,8 +653,8 @@ var SnippetsMenu = Widget.extend({ if (!this.options.snippets) { this.options.snippets = 'web_editor.snippets'; } - this.$activeSnippet = false; this.snippetEditors = []; + this._activateSnippetMutex = new concurrency.Mutex(); this.setSelectorEditableArea(options.$el, options.selectorEditableArea); }, @@ -616,7 +662,6 @@ var SnippetsMenu = Widget.extend({ * @override */ start: function () { - var self = this; var defs = [this._super.apply(this, arguments)]; this.ownerDocument = this.$el[0].ownerDocument; this.$document = $(this.ownerDocument); @@ -625,8 +670,12 @@ var SnippetsMenu = Widget.extend({ // Fetch snippet templates and compute it - defs.push(this.loadSnippets().then(function (html) { - return self._computeSnippetTemplates(html); + defs.push(this.loadSnippets().then(html => { + return this._computeSnippetTemplates(html); + }).then(() => { + this.customizePanel = document.createElement('div'); + this.customizePanel.classList.add('o_we_customize_panel', 'd-none'); + this.$el.append(this.customizePanel); })); // Prepare snippets editor environment @@ -635,45 +684,52 @@ var SnippetsMenu = Widget.extend({ }).insertAfter(this.$el); // Active snippet editor on click in the page - var lastClickedElement; - this.$document.on('click.snippets_menu', '*', function (ev) { + var lastElement; + this.$document.on('click.snippets_menu', '*', ev => { var srcElement = ev.target || (ev.originalEvent && (ev.originalEvent.target || ev.originalEvent.originalTarget)) || ev.srcElement; - if (lastClickedElement === srcElement || !srcElement) { + if (!srcElement || lastElement === srcElement) { return; } - lastClickedElement = srcElement; + lastElement = srcElement; _.defer(function () { - lastClickedElement = false; + lastElement = false; }); var $target = $(srcElement); - if ($target.closest('.oe_overlay, .note-popover').length) { + if ($target.closest('#snippets_menu').length) { + this._activateSnippet(false); return; } - self._activateSnippet($target); + if (!$target.closest('body > *').length) { + return; + } + if ($target.closest('#web_editor-top-edit, #oe_snippets, #oe_manipulators, .o_technical_modal, .oe_drop_zone').length) { + return; + } + this._activateSnippet($target); }); core.bus.on('deactivate_snippet', this, this._onDeactivateSnippet); core.bus.on('snippet_editor_clean_for_save', this, this._onCleanForSaveDemand); // Adapt overlay covering when the window is resized / content changes - var debouncedCoverUpdate = _.debounce(function () { - self.updateCurrentSnippetEditorOverlay(); - }, 200); + var debouncedCoverUpdate = _.throttle(() => { + this.updateCurrentSnippetEditorOverlay(); + }, 50); this.$window.on('resize.snippets_menu', debouncedCoverUpdate); this.$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 - this.$document.on('keydown.snippets_menu', function () { - if (self.$activeSnippet && self.$activeSnippet.data('snippet-editor')) { - self.$activeSnippet.data('snippet-editor').$el.addClass('o_keypress'); - } + this.$document.on('keydown.snippets_menu', () => { + this.snippetEditors.forEach(editor => { + editor.toggleTextEdition(true); + }); }); - this.$document.on('mousemove.snippets_menu, mousedown.snippets_menu', function () { - if (self.$activeSnippet && self.$activeSnippet.data('snippet-editor')) { - self.$activeSnippet.data('snippet-editor').$el.removeClass('o_keypress'); - } + this.$document.on('mousemove.snippets_menu, mousedown.snippets_menu', () => { + this.snippetEditors.forEach(editor => { + editor.toggleTextEdition(false); + }); }); // Auto-selects text elements with a specific class and remove this @@ -688,12 +744,19 @@ var SnippetsMenu = Widget.extend({ $(range && range.sc).closest('.o_default_snippet_text').removeClass('o_default_snippet_text'); }); - return Promise.all(defs).then(function () { + return Promise.all(defs).then(() => { + this.$('[data-title]').tooltip({ + delay: 0, + title: function () { + return this.classList.contains('active') ? false : this.dataset.title; + }, + }); + // 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 () { - self.$window.trigger('resize'); + setTimeout(() => { + this.$window.trigger('resize'); }, 1000); }); }, @@ -776,11 +839,9 @@ var SnippetsMenu = Widget.extend({ * Updates the cover dimensions of the current snippet editor. */ updateCurrentSnippetEditorOverlay: function () { - if (this.$activeSnippet && this.$activeSnippet.data('snippet-editor')) { - this.$activeSnippet.data('snippet-editor').cover(); - } this.snippetEditors = _.filter(this.snippetEditors, function (snippetEditor) { if (snippetEditor.$target.closest('body').length) { + snippetEditor.cover(); return true; } snippetEditor.destroy(); @@ -804,9 +865,6 @@ var SnippetsMenu = Widget.extend({ */ _activateInsertionZones: function ($selectorSiblings, $selectorChildren) { var self = this; - var zone_template = $('<div/>', { - class: 'oe_drop_zone oe_insert', - }); function isFullWidth($elem) { return $elem.parent().width() === $elem.outerWidth(true); @@ -815,49 +873,50 @@ var SnippetsMenu = Widget.extend({ if ($selectorChildren) { $selectorChildren.each(function () { var $zone = $(this); + var style; + var vertical; + var node; var css = self.window.getComputedStyle(this); var parentCss = self.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')); + style = {}; + vertical = false; + node = $zone[0].lastChild; + var test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === 'BR')); if (test) { - $drop.addClass('oe_vertical').css({ - height: parseInt(self.window.getComputedStyle($zone[0]).lineHeight), - float: 'none', - display: 'inline-block', - }); + vertical = true; + style['float'] = 'none'; + style['height'] = parseInt(self.window.getComputedStyle($zone[0]).lineHeight) + 'px'; + style['display'] = 'inline-block'; } else if (float === 'left' || float === 'right' || (parentDisplay === 'flex' && parentFlex === 'row')) { - $drop.css('float', float); + style['float'] = float; if (!isFullWidth($zone)) { - $drop.addClass('oe_vertical').css('height', Math.max(Math.min($zone.outerHeight(), $zone.children().last().outerHeight()), 30)); + vertical = true; + style['height'] = Math.max($zone.outerHeight(), 30) + 'px'; } } + self._insertDropzone($('<we-hook/>').appendTo($zone), vertical, style); - $drop = $drop.clone(); - - $zone.prepend($drop); - node = $drop[0].nextSibling; - test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === 'BR')); + style = {}; + vertical = false; + node = $zone[0].firstChild; + test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === 'BR')); if (test) { - $drop.addClass('oe_vertical').css({ - height: parseInt(self.window.getComputedStyle($zone[0]).lineHeight), - float: 'none', - display: 'inline-block' - }); + vertical = true; + style['float'] = 'none'; + style['height'] = parseInt(self.window.getComputedStyle($zone[0]).lineHeight) + 'px'; + style['display'] = 'inline-block'; } else if (float === 'left' || float === 'right' || (parentDisplay === 'flex' && parentFlex === 'row')) { - $drop.css('float', float); + style['float'] = float; if (!isFullWidth($zone)) { - $drop.addClass('oe_vertical').css('height', Math.max(Math.min($zone.outerHeight(), $zone.children().first().outerHeight()), 30)); + vertical = true; + style['height'] = Math.max($zone.outerHeight(), 30) + 'px'; } } - if (test) { - $drop.css({'float': 'none', 'display': 'inline-block'}); - } + self._insertDropzone($('<we-hook/>').prependTo($zone), vertical, style); }); // add children near drop zone @@ -867,7 +926,8 @@ var SnippetsMenu = Widget.extend({ if ($selectorSiblings) { $selectorSiblings.filter(':not(.oe_drop_zone):not(.oe_drop_clone)').each(function () { var $zone = $(this); - var $drop; + var style; + var vertical; var css = self.window.getComputedStyle(this); var parentCss = self.window.getComputedStyle($zone.parent()[0]); var float = css.float || css.cssFloat; @@ -875,24 +935,28 @@ var SnippetsMenu = Widget.extend({ var parentFlex = parentCss.flexDirection; if ($zone.prev('.oe_drop_zone:visible').length === 0) { - $drop = zone_template.clone(); + style = {}; + vertical = false; if (float === 'left' || float === 'right' || (parentDisplay === 'flex' && parentFlex === 'row')) { - $drop.css('float', float); + style['float'] = float; if (!isFullWidth($zone)) { - $drop.addClass('oe_vertical').css('height', Math.max(Math.min($zone.outerHeight(), $zone.prev().outerHeight() || Infinity), 30)); + vertical = true; + style['height'] = Math.max($zone.outerHeight(), 30) + 'px'; } } - $zone.before($drop); + self._insertDropzone($('<we-hook/>').insertBefore($zone), vertical, style); } if ($zone.next('.oe_drop_zone:visible').length === 0) { - $drop = zone_template.clone(); + style = {}; + vertical = false; if (float === 'left' || float === 'right' || (parentDisplay === 'flex' && parentFlex === 'row')) { - $drop.css('float', float); + style['float'] = float; if (!isFullWidth($zone)) { - $drop.addClass('oe_vertical').css('height', Math.max(Math.min($zone.outerHeight(), $zone.next().outerHeight() || Infinity), 30)); + vertical = true; + style['height'] = Math.max($zone.outerHeight(), 30) + 'px'; } } - $zone.after($drop); + self._insertDropzone($('<we-hook/>').insertAfter($zone), vertical, style); } }); } @@ -918,15 +982,15 @@ var SnippetsMenu = Widget.extend({ 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')) { + var floatPrev = prev.css('float') || 'none'; + var floatNext = next.css('float') || 'none'; + var dispPrev = prev.css('display') || null; + var dispNext = next.css('display') || null; + if ((floatPrev === 'left' || floatPrev === 'right') + && (floatNext === 'left' || floatNext === 'right')) { zone.remove(); - } else if (disp_prev !== null && disp_next !== null - && disp_prev.indexOf('inline') >= 0 && disp_next.indexOf('inline') >= 0) { + } else if (dispPrev !== null && dispNext !== null + && dispPrev.indexOf('inline') >= 0 && dispNext.indexOf('inline') >= 0) { zone.remove(); } }); @@ -940,38 +1004,32 @@ var SnippetsMenu = Widget.extend({ * 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. + * The DOM element whose editor (and its parent ones) need to be + * enabled. Only disable the current one if false is given. + * @param {boolean} [previewMode=false] * @returns {Promise<SnippetEditor>} * (might be async when an editor must be created) */ - _activateSnippet: function ($snippet) { - if ($snippet) { - if (!globalSelector.is($snippet)) { + _activateSnippet: function ($snippet, previewMode) { + return this._activateSnippetMutex.exec(() => { + // First disable all editors if necessary + this.snippetEditors.forEach(editor => { + editor.toggleFocus(false, previewMode); + }); + // Take the first parent of the provided DOM (or itself) which + // should have an associated snippet editor and create + enable it. + if ($snippet && $snippet.length) { $snippet = globalSelector.closest($snippet); + if (!$snippet.length) { + return Promise.resolve(null); + } + return this._createSnippetEditor($snippet).then(editor => { + editor.toggleFocus(true, previewMode); + return editor; + }); } - if (this.$activeSnippet && this.$activeSnippet[0] === $snippet[0]) { - return Promise.resolve($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; - this.trigger_up('activate_snippet', $snippet); - return this._createSnippetEditor($snippet).then(function (editor) { - self.$activeSnippet = $snippet; - editor.toggleFocus(true); - return editor; - }); - } - return Promise.resolve(editor); + return Promise.resolve(null); + }); }, /** * @private @@ -1040,7 +1098,7 @@ var SnippetsMenu = Widget.extend({ * 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) { + _computeSelectorFunctions: function (include, exclude, target, noCheck, isChildren) { var self = this; // Convert the selector for elements to include into a list @@ -1064,7 +1122,7 @@ var SnippetsMenu = Widget.extend({ } // (Re)join the subselectors - var selector =_.map(selectorList, function (s) { + var selector = _.map(selectorList, function (s) { return s + selectorConditions; }).join(', '); @@ -1087,7 +1145,7 @@ var SnippetsMenu = Widget.extend({ return $from.closest(selector, parentNode).filter(function () { var node = this; while (node.parentNode) { - if (parents.indexOf(node)!==-1) { + if (parents.indexOf(node) !== -1) { return true; } node = node.parentNode; @@ -1131,9 +1189,9 @@ var SnippetsMenu = Widget.extend({ var exclude = $style.data('exclude') || ''; var target = $style.data('target'); var noCheck = $style.data('no-check'); - var option_id = $style.data('js'); + var optionID = $style.data('js'); var option = { - 'option': option_id, + 'option': optionID, 'base_selector': selector, 'base_exclude': exclude, 'base_target': target, @@ -1151,7 +1209,7 @@ var SnippetsMenu = Widget.extend({ globalSelector.closest = function ($from) { var $temp; var $target; - for (var i = 0, len = selectors.length ; i < len ; i++) { + for (var i = 0, len = selectors.length; i < len; i++) { $temp = selectors[i].closest($from, $target && $target[0]); if ($temp.length) { $target = $temp; @@ -1161,13 +1219,13 @@ var SnippetsMenu = Widget.extend({ }; globalSelector.all = function ($from) { var $target = $(); - for (var i = 0, len = selectors.length ; i < len ; i++) { + 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++) { + for (var i = 0, len = selectors.length; i < len; i++) { if (selectors[i].is($from)) { return true; } @@ -1188,8 +1246,8 @@ var SnippetsMenu = Widget.extend({ if (snippetClasses && snippetClasses.length) { snippetClasses = '.' + snippetClasses.join('.'); } - $(snippetClasses).data('name', name); - $sbody.data('name', name); + var $els = $(snippetClasses).not('[data-name]').add($sbody); + $els.attr('data-name', name).data('name', name); } // Create the thumbnail @@ -1197,9 +1255,9 @@ var SnippetsMenu = Widget.extend({ 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 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 @@ -1231,7 +1289,7 @@ var SnippetsMenu = Widget.extend({ // Remove branding from template _.each($html.find('[data-oe-model], [data-oe-type]'), function (el) { - for (var k = 0 ; k < el.attributes.length ; k++) { + 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--; @@ -1296,11 +1354,13 @@ var SnippetsMenu = Widget.extend({ var cache = {}; this.$snippets.each(function () { var $snippet = $(this); - var $snippet_body = $snippet.find('.oe_snippet_body'); + var $snippetBody = $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; + if (check || !($snippetBody.is(option.base_selector) && !$snippetBody.is(option.base_exclude))) { + return; + } cache[k] = cache[k] || { 'drop-near': option['drop-near'] ? option['drop-near'].all().length : 0, @@ -1313,6 +1373,26 @@ var SnippetsMenu = Widget.extend({ }); }, /** + * Creates a dropzone element and inserts it by replacing the given jQuery + * location. This allows to add data on the dropzone depending on the hook + * environment. + * + * @private + * @param {jQuery} $hook + * @param {boolean} [vertical=false] + * @param {Object} [style] + */ + _insertDropzone: function ($hook, vertical, style) { + var $dropzone = $('<div/>', { + 'class': 'oe_drop_zone oe_insert' + (vertical ? ' oe_vertical' : ''), + }); + if (style) { + $dropzone.css(style); + } + $hook.replaceWith($dropzone); + return $dropzone; + }, + /** * Make given snippets be draggable/droppable thanks to their thumbnail. * * @private @@ -1321,8 +1401,8 @@ var SnippetsMenu = Widget.extend({ _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 left = $tumb.outerWidth() / 2; + var top = $tumb.outerHeight() / 2; var $toInsert, dropped, $snippet; $snippets.draggable({ @@ -1339,12 +1419,12 @@ var SnippetsMenu = Widget.extend({ start: function () { dropped = false; $snippet = $(this); - var $base_body = $snippet.find('.oe_snippet_body'); + var $baseBody = $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 ($baseBody.is(temp[k].base_selector) && !$baseBody.is(temp[k].base_exclude)) { if (temp[k]['drop-near']) { $selectorSiblings = $selectorSiblings.add(temp[k]['drop-near'].all()); } @@ -1354,7 +1434,7 @@ var SnippetsMenu = Widget.extend({ } } - $toInsert = $base_body.clone().data('name', $base_body.data('name')); + $toInsert = $baseBody.clone(); 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"); @@ -1382,8 +1462,6 @@ var SnippetsMenu = Widget.extend({ } }, }); - - self.trigger_up('cover_will_change'); }, stop: function (ev, ui) { $toInsert.removeClass('oe_snippet_body'); @@ -1427,9 +1505,6 @@ var SnippetsMenu = Widget.extend({ editor.buildSnippet(); }).then(function () { $target.trigger('content_changed'); - if ($target.closest('body').length) { // can be destroyed (eg in test) - self._activateSnippet($target); - } }); }); } else { @@ -1473,6 +1548,15 @@ var SnippetsMenu = Widget.extend({ this._activateInsertionZones(ev.data.$selectorSiblings, ev.data.$selectorChildren); }, /** + * Called when a child editor asks to deactivate the current snippet + * overlay. + * + * @private + */ + _onActivateSnippet: function (ev) { + this._activateSnippet(ev.data.$snippet, ev.data.previewMode); + }, + /** * Called when a child editor asks to operate some operation on all child * snippet of a DOM element. * @@ -1492,6 +1576,24 @@ var SnippetsMenu = Widget.extend({ this.cleanForSave(); }, /** + * @private + * @param {*} ev + */ + _onCollapseTogglerClick: function (ev) { + this.$('we-collapse-area > we-toggler').not(ev.currentTarget).removeClass('active'); + ev.currentTarget.classList.toggle('active'); + }, + /** + * Called when the overlay dimensions/positions should be recomputed. + * + * @private + */ + _onOverlaysCoverUpdate: function () { + this.snippetEditors.forEach(editor => { + editor.cover(); + }); + }, + /** * Called when a child editor asks to deactivate the current snippet * overlay. * @@ -1540,6 +1642,23 @@ var SnippetsMenu = Widget.extend({ _onSnippetRemoved: function () { this._disableUndroppableSnippets(); }, + /** + * @private + * @param {OdooEvent} ev + */ + _onUpdateCustomizeElements: function (ev) { + while (this.customizePanel.firstChild) { + this.customizePanel.removeChild(this.customizePanel.firstChild); + } + ev.data.customize$Elements.forEach($el => { + this.customizePanel.appendChild($el[0]); + }); + var customize = !!ev.data.customize$Elements.length; + this.$('#o_scroll').toggleClass('d-none', customize); + this.$('.o_we_add_snippet_btn').toggleClass('active', !customize); + this.customizePanel.classList.toggle('d-none', !customize); + this.$('.o_we_customize_snippet_btn').toggleClass('active', customize); + }, }); return { diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js index 1278d70afda..f124d1841f0 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -18,23 +18,16 @@ var _t = core._t; var SnippetOption = Widget.extend({ events: { 'mouseenter': '_onLinkEnter', - 'mouseenter a': '_onLinkEnter', + 'mouseenter we-button': '_onLinkEnter', 'click': '_onLinkClick', - 'click a': '_onLinkClick', + 'click we-button': '_onLinkClick', 'mouseleave': '_onMouseleave', - 'mouseleave a': '_onMouseleave', - 'mouseleave .dropdown-menu': '_onMouseleave', + 'mouseleave we-button': '_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 + * The option `$el` is supposed to be the associated DOM UI element. + * 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). * @@ -65,7 +58,7 @@ var SnippetOption = Widget.extend({ * * @abstract */ - onFocus : function () {}, + 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 @@ -80,7 +73,7 @@ var SnippetOption = Widget.extend({ * * @abstract */ - onBlur : function () {}, + onBlur: function () {}, /** * Called when the associated snippet is the result of the cloning of * another snippet (so `this.$target` is a cloned element). @@ -120,7 +113,7 @@ var SnippetOption = Widget.extend({ /** * 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` + * case is having a sub-collapse with each item having a `data-select-class` * value allowing to choose the associated class. * * @param {boolean|string} previewMode @@ -131,12 +124,14 @@ var SnippetOption = Widget.extend({ * @param {jQuery} $opt - the related DOMElement option */ selectClass: function (previewMode, value, $opt) { - var $group = $opt && $opt.parents('.dropdown-submenu').last(); + var $group = $opt && $opt.parents('we-collapse-area').last(); 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(' '); + var $lis = $group.find('[data-select-class]'); + var classes = $lis.map(function () { + return $(this).data('selectClass'); + }).get().join(' '); this.$target.removeClass(classes); if (value) { @@ -146,15 +141,19 @@ var SnippetOption = Widget.extend({ /** * 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` + * case is having a sub-collapse 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(' '); + var $lis = this.$el.find('[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') { @@ -168,7 +167,7 @@ var SnippetOption = Widget.extend({ /** * Override the helper method to search inside the $target element instead - * of the dropdown item element. + * of the UI item element. * * @override */ @@ -211,7 +210,7 @@ var SnippetOption = Widget.extend({ */ _reset: function () { var self = this; - var $actives = this.$el.find('.active').addBack('.active'); + var $actives = this.$el.find('we-button.active'); _.each($actives, function (activeElement) { var $activeElement = $(activeElement); self.__methodNames = _.without.apply(_, [self.__methodNames].concat(_.keys($activeElement.data()))); @@ -233,8 +232,6 @@ var SnippetOption = Widget.extend({ * @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; @@ -244,7 +241,6 @@ var SnippetOption = Widget.extend({ if (!previewMode) { this._reset(); this.trigger_up('request_history_undo_record', {$target: this.$target}); - this.$target.trigger('content_changed'); } // Search for methods (data-...) (i.e. data-toggle-class) on the @@ -257,16 +253,16 @@ var SnippetOption = Widget.extend({ } while (this.$el.parent().has(el).length); // Call the found method in the right order (parents -> child) - _.each(methods.reverse(), function (data) { + methods.reverse().forEach(data => { var $el = $(data[0]); var methods = data[1]; - _.each(methods, function (value, methodName) { - if (self[methodName]) { + Object.keys(methods).forEach(methodName => { + if (this[methodName]) { if (previewMode === true) { - self.__methodNames.push(methodName); + this.__methodNames.push(methodName); } - self[methodName](previewMode, value, $el); + this[methodName](previewMode, methods[methodName], $el); } }); }); @@ -275,6 +271,8 @@ var SnippetOption = Widget.extend({ if (!previewMode) { this._setActive(); } + + this.$target.trigger('content_changed'); }, /** * Tweaks the option DOM elements to show the selected value according to @@ -286,7 +284,6 @@ var SnippetOption = Widget.extend({ _setActive: function () { var self = this; this.$el.find('[data-toggle-class]') - .addBack('[data-toggle-class]') .removeClass('active') .filter(function () { var className = $(this).data('toggleClass'); @@ -295,25 +292,20 @@ var SnippetOption = Widget.extend({ .addClass('active'); // Get submenus which are not inside submenus - var $submenus = this.$el.find('.dropdown-submenu') - .addBack('.dropdown-submenu') - .not('.dropdown-submenu .dropdown-submenu'); + var $submenus = this.$el.find('we-collapse-area') + .not('we-collapse-area we-collapse-area'); // Add unique active class for each submenu active item _.each($submenus, function (submenu) { - var $elements = _getSelectClassElements($(submenu)); + var $elements = $(submenu).find('[data-select-class]'); _processSelectClassElements($elements); }); // Add unique active class for out-of-submenu active item - var $externalElements = _getSelectClassElements(this.$el) - .not('.dropdown-submenu *, .dropdown-submenu'); + var $externalElements = this.$el.find('[data-select-class]') + .not('we-collapse-area *, we-collapse-area'); _processSelectClassElements($externalElements); - function _getSelectClassElements($el) { - return $el.find('[data-select-class]') - .addBack('[data-select-class]'); - } function _processSelectClassElements($elements) { var maxNbClasses = -1; $elements.removeClass('active') @@ -343,20 +335,11 @@ var SnippetOption = Widget.extend({ * @param {Event} ev */ _onLinkEnter: function (ev) { - var $opt = $(ev.target).closest('.dropdown-item'); + var $opt = $(ev.target).closest('we-button'); 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; } @@ -371,7 +354,7 @@ var SnippetOption = Widget.extend({ * @param {Event} ev */ _onLinkClick: function (ev) { - var $opt = $(ev.target).closest('.dropdown-item'); + var $opt = $(ev.target).closest('we-button'); if (ev.isDefaultPrevented() || !$opt.length || !$opt.is(':hasData')) { return; } @@ -403,8 +386,6 @@ var SnippetOption = Widget.extend({ var registry = {}; registry.sizing = SnippetOption.extend({ - preventChildPropagation: true, - /** * @override */ @@ -413,11 +394,8 @@ registry.sizing = SnippetOption.extend({ 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); - }); + var resizeValues = this._getSize(); this.$handles.on('mousedown', function (ev) { ev.preventDefault(); @@ -463,22 +441,22 @@ registry.sizing = SnippetOption.extend({ $body.addClass(cursor); var xy = ev['page' + XY]; - var body_mousemove = function (ev) { + var bodyMouseMove = 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 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, '')); + 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, '')); + 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; @@ -490,9 +468,9 @@ registry.sizing = SnippetOption.extend({ $handle.addClass('o_active'); } }; - var body_mouseup = function () { - $body.off('mousemove', body_mousemove); - $body.off('mouseup', body_mouseup); + var bodyMouseUp = function () { + $body.off('mousemove', bodyMouseMove); + $body.off('mouseup', bodyMouseUp); $body.removeClass(cursor); $handle.removeClass('o_active'); @@ -512,8 +490,8 @@ registry.sizing = SnippetOption.extend({ }); }, 0); }; - $body.on('mousemove', body_mousemove); - $body.on('mouseup', body_mouseup); + $body.on('mousemove', bodyMouseMove); + $body.on('mouseup', bodyMouseUp); }); return def; @@ -522,8 +500,19 @@ registry.sizing = SnippetOption.extend({ * @override */ onFocus: function () { + var resizeValues = this._getSize(); + _.each(resizeValues, (value, key) => { + this.$handles.filter('.' + key).toggleClass('readonly', !value); + }); + this._onResize(); }, + /** + * @override + */ + onBlur: function () { + this.$handles.addClass('readonly'); + }, //-------------------------------------------------------------------------- // Public @@ -596,6 +585,9 @@ registry.sizing = SnippetOption.extend({ width: ml, left: '-' + ml, }); + this.$overlay.find('.o_handle.e').css({ + width: 0, + }); _.each(this.$overlay.find(".o_handle.n, .o_handle.s"), function (handle) { var $handle = $(handle); var direction = $handle.hasClass('n') ? 'top': 'bottom'; @@ -607,7 +599,7 @@ registry.sizing = SnippetOption.extend({ /** * Handles the edition of padding-top and padding-bottom. */ -registry.sizing_y = registry.sizing.extend({ +registry['sizing_y'] = registry.sizing.extend({ //-------------------------------------------------------------------------- // Private @@ -629,13 +621,13 @@ registry.sizing_y = registry.sizing.extend({ } var grid = []; - for (var i = 0 ; i <= 256/8 ; i++) { + 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], + n: [grid.map(v => nClass + v), grid, nProp], + s: [grid.map(v => sClass + v), grid, sProp], }; return this.grid; }, @@ -671,7 +663,7 @@ registry.colorpicker = SnippetOption.extend({ 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>"); + $(elem).prepend("<div>" + elem.dataset.display + "</div>"); }); // Retrieve excluded palettes list @@ -705,7 +697,7 @@ registry.colorpicker = SnippetOption.extend({ } $pt.find('.o_colorpicker_section_tabs').append($clpicker); - this.$el.find('.dropdown-menu').append($pt); + this.$el.find('we-collapse').append($pt); } var classes = []; @@ -775,7 +767,7 @@ registry.colorpicker = SnippetOption.extend({ if ($selected.length) { if ($selected.data('color')) { this.$target.addClass(this.colorPrefix + $selected.data('color')); - } else { + } else if ($selected.hasClass('o_custom_color')) { this.$target.css('background-color', $selected.css('background-color')); } } @@ -851,10 +843,10 @@ registry.background = SnippetOption.extend({ 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'), + 'onlyImages': true, + 'firstFilters': ['background'], + 'res_model': $editable.data('oe-model'), + 'res_id': $editable.data('oe-id'), }, $image[0]).open(); _editor.on('save', this, function () { @@ -881,7 +873,9 @@ registry.background = SnippetOption.extend({ this.$target.off('.background-option') .on('background-color-event.background-option', (function (e, previewMode) { e.stopPropagation(); - if (e.currentTarget !== e.target) return; + if (e.currentTarget !== e.target) { + return; + } if (previewMode === false) { this.__customImageSrc = undefined; } @@ -943,7 +937,7 @@ registry.background = SnippetOption.extend({ _setCustomBackground: function (value) { this.__customImageSrc = value; this.background(false, this.__customImageSrc); - this.$target.addClass('oe_custom_bg'); + this.$target.toggleClass('oe_custom_bg', !!value); this._setActive(); this.$target.trigger('snippet-option-change', [this]); }, @@ -952,7 +946,7 @@ registry.background = SnippetOption.extend({ /** * Handles the edition of snippet's background image position. */ -registry.background_position = SnippetOption.extend({ +registry['background_position'] = SnippetOption.extend({ xmlDependencies: ['/web_editor/static/src/xml/editor.xml'], /** @@ -984,10 +978,10 @@ registry.background_position = SnippetOption.extend({ 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.previousState = [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.bgPos = self.$target.css('background-position').split(' '); + this.bgSize = self.$target.css('background-size').split(' '); this.modal = new Dialog(null, { title: _t("Background Image Sizing"), @@ -1000,7 +994,7 @@ registry.background_position = SnippetOption.extend({ 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')); + 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')) { @@ -1012,26 +1006,26 @@ registry.background_position = SnippetOption.extend({ 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], + if (self.bgPos.length > 1) { + self.bgPos = { + x: self.bgPos[0], + y: self.bgPos[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('%', '')); + self.modal.$('#o_bg_img_opt_custom_pos_x').val(self.bgPos.x.replace('%', '')); + self.modal.$('#o_bg_img_opt_custom_pos_y').val(self.bgPos.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('%', '')); + if (self.bgSize.length > 1) { + self.modal.$('#o_bg_img_opt_custom_size_x').val(self.bgSize[0].replace('%', '')); + self.modal.$('#o_bg_img_opt_custom_size_y').val(self.bgSize[1].replace('%', '')); } // Focus Point - self.$focus = self.modal.$('.o_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}); + var imgURL = /\(['"]?([^'"]+)['"]?\)/g.exec(self.$target.css('background-image')); + imgURL = (imgURL && imgURL[1]) || ''; + var $img = $('<img/>', {class: 'img img-fluid', src: imgURL}); $img.on('load', function () { self._bindImageEvents($img); }); @@ -1085,9 +1079,9 @@ registry.background_position = SnippetOption.extend({ 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.bgPos = { + x: clipValue(posX / $img.width() * 100).toFixed(2) + '%', + y: clipValue(posY / $img.height() * 100).toFixed(2) + '%', }; self._updatePosInformation(); self._saveChanges(); @@ -1116,10 +1110,10 @@ registry.background_position = SnippetOption.extend({ */ _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], + if (this.previousState) { + this.$target.addClass(this.previousState[0]).css({ + 'background-size': this.previousState[1], + 'background-position': this.previousState[2], }); } }, @@ -1129,11 +1123,11 @@ registry.background_position = SnippetOption.extend({ * @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.modal.$('.o_bg_img_opt_ui_info .o_x').text(this.bgPos.x); + this.modal.$('.o_bg_img_opt_ui_info .o_y').text(this.bgPos.y); this.$focus.css({ - left: this.bg_pos.x, - top: this.bg_pos.y, + left: this.bgPos.x, + top: this.bgPos.y, }); }, /** @@ -1144,10 +1138,10 @@ registry.background_position = SnippetOption.extend({ _saveChanges: function () { this._clean(); - var bg_img_size = this.modal.$('> :not(label):not(.o_hidden)').data('value') || 'cover'; - switch (bg_img_size) { + var bgImgSize = this.modal.$('> :not(label):not(.o_hidden)').data('value') || 'cover'; + switch (bgImgSize) { case 'cover': - this.$target.css('background-position', this.bg_pos.x + ' ' + this.bg_pos.y); + this.$target.css('background-position', this.bgPos.x + ' ' + this.bgPos.y); break; case 'contain': this.$target.addClass('o_bg_img_opt_contain'); @@ -1161,8 +1155,8 @@ registry.background_position = SnippetOption.extend({ 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'), + 'background-size': (sizeX ? sizeX + '%' : 'auto') + ' ' + (sizeY ? sizeY + '%' : 'auto'), + 'background-position': (posX ? posX + '%' : 'auto') + ' ' + (posY ? posY + '%' : 'auto'), }); break; } @@ -1191,7 +1185,7 @@ registry.many2one = SnippetOption.extend({ // create search button and bind search bar this.$btn = $(qweb.render('web_editor.many2one.button')) - .insertAfter(this.$overlay.find('.oe_options')); + .prependTo(this.$el); this.$ul = this.$btn.find('ul'); this.$search = this.$ul.find('li:first'); @@ -1201,17 +1195,10 @@ registry.many2one = SnippetOption.extend({ // 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); + self.$btn.find('a').on('click', function (e) { + self._clear(); + }); + }, 0); // bind search input this.$search.find('input') @@ -1288,7 +1275,7 @@ registry.many2one = SnippetOption.extend({ }, }).then(function (result) { self.$search.siblings().remove(); - self.$search.after(qweb.render('web_editor.many2one.search',{contacts:result})); + self.$search.after(qweb.render('web_editor.many2one.search', {contacts: result})); }); }, /** @@ -1308,10 +1295,10 @@ registry.many2one = SnippetOption.extend({ 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')+'"]') + .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 () { diff --git a/addons/web_editor/static/src/js/frontend/loader.js b/addons/web_editor/static/src/js/frontend/loader.js new file mode 100644 index 00000000000..9c6cf0ef984 --- /dev/null +++ b/addons/web_editor/static/src/js/frontend/loader.js @@ -0,0 +1,28 @@ +odoo.define('web_editor.loader', function (require) { +'use strict'; + +var Wysiwyg = require('web_editor.wysiwyg.root'); + +function load(parent, textarea, options) { + var loading = textarea.nextElementSibling; + if (loading && !loading.classList.contains('o_wysiwyg_loading')) { + loading = null; + } + + if (!textarea.value.match(/\S/)) { + textarea.value = '<p><br/></p>'; + } + + var wysiwyg = new Wysiwyg(parent, options); + return wysiwyg.attachTo(textarea).then(() => { + if (loading) { + loading.parentNode.removeChild(loading); + } + return wysiwyg; + }); +} + +return { + load: load, +}; +}); diff --git a/addons/web_editor/static/src/js/frontend/loader_loading.js b/addons/web_editor/static/src/js/frontend/loader_loading.js new file mode 100644 index 00000000000..b5156944470 --- /dev/null +++ b/addons/web_editor/static/src/js/frontend/loader_loading.js @@ -0,0 +1,30 @@ +(function () { +'use strict'; + +/** + * This file makes sure textarea elements with a specific editor class are + * tweaked as soon as the DOM is ready so that they appear to be loading. + * + * They must then be loaded using standard Odoo modules system. In particular, + * @see web_editor.loader + */ + +document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('textarea.o_wysiwyg_loader').forEach(textarea => { + var wrapper = document.createElement('div'); + wrapper.classList.add('position-relative', 'o_wysiwyg_wrapper'); + + var loadingElement = document.createElement('div'); + loadingElement.classList.add('o_wysiwyg_loading'); + var loadingIcon = document.createElement('i'); + loadingIcon.classList.add('text-600', 'text-center', + 'fa', 'fa-circle-o-notch', 'fa-spin', 'fa-2x'); + loadingElement.appendChild(loadingIcon); + wrapper.appendChild(loadingElement); + + textarea.parentNode.insertBefore(wrapper, textarea); + wrapper.insertBefore(textarea, loadingElement); + }); +}); + +})(); diff --git a/addons/web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js b/addons/web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js index 64b7c6a8f8c..c0709a2c90e 100644 --- a/addons/web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js +++ b/addons/web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js @@ -23,9 +23,6 @@ Wysiwyg.include({ init: function (parent, options) { this._super.apply(this, arguments); if (this.options.inIframe) { - if (!this.options.iframeCssAssets) { - this.options.iframeCssAssets = 'web_editor.wysiwyg_iframe_css_assets'; - } this._onUpdateIframeId = 'onLoad_' + this.id; } }, @@ -133,4 +130,4 @@ Wysiwyg.include({ }, }); -});
\ No newline at end of file +}); |
