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 | |
| parent | 48503fcf08eac6fed12e080d4f501f9bbc8d257d (diff) | |
| parent | 197474e5b9ad43ab2d587a98136a1a809ce9a2e7 (diff) | |
[MERGE] forward port branch saas-12.5 up to 197474e5b9a
Diffstat (limited to 'addons/web_editor/static/src')
14 files changed, 1165 insertions, 1286 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 +}); diff --git a/addons/web_editor/static/src/scss/web_editor.backend.scss b/addons/web_editor/static/src/scss/web_editor.backend.scss index ecbe42721f3..385bb370fd7 100644 --- a/addons/web_editor/static/src/scss/web_editor.backend.scss +++ b/addons/web_editor/static/src/scss/web_editor.backend.scss @@ -3,7 +3,7 @@ word-wrap: break-word; .note-editable { - min-height: 180px; + min-height: 330px; font: inherit !important; font-family: inherit !important; line-height: inherit !important; @@ -23,13 +23,16 @@ width: 100%; margin: 0; padding: 0; - border: 0; ul > li > p { margin: 0px; } min-height: 300px; min-height: -webkit-calc(100vh - 170px); min-height: calc(100vh - 170px); + + &.o_readonly { + border: none; + } } .rounded { border-radius: .25rem !important; diff --git a/addons/web_editor/static/src/scss/web_editor.frontend.scss b/addons/web_editor/static/src/scss/web_editor.frontend.scss index 7df7cacccb4..58a314bb7ad 100644 --- a/addons/web_editor/static/src/scss/web_editor.frontend.scss +++ b/addons/web_editor/static/src/scss/web_editor.frontend.scss @@ -3,3 +3,14 @@ transform: none !important; } } + +.o_wysiwyg_loader { + @extend .form-control:disabled; + pointer-events: none; + min-height: 100px; + color: transparent; +} +.o_wysiwyg_loading { + @include o-position-absolute($top: 50%, $left: 50%); + transform: translate(-50%, -50%) +} diff --git a/addons/web_editor/static/src/scss/web_editor.variables.scss b/addons/web_editor/static/src/scss/web_editor.variables.scss index e317fe7616b..0ab8dc07b87 100644 --- a/addons/web_editor/static/src/scss/web_editor.variables.scss +++ b/addons/web_editor/static/src/scss/web_editor.variables.scss @@ -2,14 +2,22 @@ /// This files regroups the variables and mixins which are specific to the editor. /// -$o-we-font-family: Roboto, 'Montserrat', "Segoe UI","Helvetica Neue",Helvetica,Arial,sans-serif !default; +$o-we-font-family: Roboto, 'Montserrat', 'Segoe UI', 'Helvetica Neue', Helvetica, Arial, sans-serif !default; + +$o-we-font-size: 13px; +$o-we-bg-color: #F7F7F7 !default; +$o-we-bg-color-dark: #a7a7a7 !default; +$o-we-color: #212629 !default; +$o-we-border-width: 1px !default; +$o-we-border-color: rgba(0, 0, 0, 0.125) !default; +$o-we-dropdown-bg-color: #FFFFFF; +$o-we-dropdown-color: inherit; $o-we-color-dark: #2c2c36 !default; $o-we-color-darker: darken($o-we-color-dark, 2%) !default; $o-we-color-divider: #33363e !default; $o-we-color-light: #4e525b !default; -$o-we-color-normal: #50545d !default; -$o-we-color-paper: #fcfcfc !default; +$o-we-color-normal: #3D4047 !default; $o-we-color-text-normal: #999999 !default; $o-we-color-text-light: #d4d5d7 !default; $o-we-color-text-lighter: #ebecee !default; @@ -18,36 +26,36 @@ $o-we-color-warning: #f0ad4e !default; $o-we-color-success: #40ad67 !default; $o-we-color-info: #6999a8 !default; -$o-we-sidebar-width: 210px !default; +$o-we-sidebar-width: 230px !default; -$o-we-tab-active-bg: #3D4048 !default; +$o-we-handles-offset-to-hide: 10000px !default; +$o-we-handles-btn-size: 18px !default; +$o-we-handles-accent-color: #00e2ff !default; +$o-we-handles-accent-color-preview: $o-enterprise-color !default; +$o-we-handle-edge-size: 8px !default; +$o-we-handle-border-width: 2px !default; +$o-we-handle-border-width-preview: 6px !default; // Transitions $o-we-md-ease: cubic-bezier(0.19, 1, 0.22, 1) !default; // Editor -$o-we-dropzone-size: 40px !default; +$o-we-dropzone-size: 30px !default; // $grid-gutter-width (todo: allow to use the variable) $o-we-dropzone-border-width: 2px !default; $o-we-dropzone-border: $o-we-dropzone-border-width dashed $o-brand-odoo !default; -$o-we-handle-edge-size: 1px !default; -$o-we-handle-border-width: 1px !default; -$o-we-handles-offset-to-hide: 10000px !default; -$o-we-handles-accent-color: #00e2ff !default; -$o-we-handles-btn-size: 18px !default; - -$o-we-overlay-option-size: 25px !default; - $o-we-ace-color: #2F3129 !default; $o-we-switch-size: 2ex !default; -$o-we-switch-inactive-color: $o-we-color-paper !default; +$o-we-switch-inactive-color: $o-we-bg-color !default; // Checklist $o-checklist-margin-left: 20px; $o-checklist-checkmark-width: 2px; $o-checklist-before-size: 13px; +$o-we-toolbar-height: 32px; + // ============ 'Preview' component Mixins =============== // Create a "preview" work-area to edit/customize specific elements @@ -66,6 +74,26 @@ $o-checklist-before-size: 13px; box-shadow: 0 0 15px 2px #000; } +// Mixins for buttons with alpha bg-color +@mixin o-alpha-button-preview { + position: relative; + z-index: 1; + + // Place an overlay that inherit the button's bg-color. + &::before { + content: ""; + @include o-position-absolute(0, 0, 0, 0); + background-color: inherit; + } + + &::after { + content: ""; + @include o-position-absolute(0, 0, 0, 0); + z-index: -1; + background-image: url('/web_editor/static/src/img/transparent.png'); + background-size: 10px auto; + } +} // ============ Preservation mixins =============== // Used to preserve web_editor and website UI from themes customizations @@ -148,7 +176,7 @@ $o-checklist-before-size: 13px; .modal-content { border-radius: 0; - background-color: $o-we-color-paper; + background-color: $o-we-bg-color; .modal-header { border-bottom-color: #e9ecef; @@ -179,7 +207,7 @@ $o-checklist-before-size: 13px; &.active { &, &:hover, &:focus { color: $o-we-color-normal; - background-color: $o-we-color-paper; + background-color: $o-we-bg-color; border-color: #dee2e6 #dee2e6 #FFFFFF; } } @@ -199,11 +227,11 @@ $o-checklist-before-size: 13px; @include button-variant($o-brand-primary, $o-brand-primary); } &.btn-secondary { - @include button-variant($o-we-color-paper, $o-we-color-paper); + @include button-variant($o-we-bg-color, $o-we-bg-color); color: $o-brand-primary; } &.btn-link { - @include button-variant($o-we-color-paper, $o-we-color-paper); + @include button-variant($o-we-bg-color, $o-we-bg-color); color: $o-brand-primary; } &.btn-success { @@ -225,8 +253,8 @@ $o-checklist-before-size: 13px; .card { padding: 19px; margin-bottom: 20px; - background-color: $o-we-color-paper; - border: 1px solid darken($o-we-color-paper, 5%); + background-color: $o-we-bg-color; + border: 1px solid darken($o-we-bg-color, 5%); border-radius: 0; box-shadow: none; } diff --git a/addons/web_editor/static/src/scss/wysiwyg.scss b/addons/web_editor/static/src/scss/wysiwyg.scss index cd8fa7bcf44..175463da8f1 100644 --- a/addons/web_editor/static/src/scss/wysiwyg.scss +++ b/addons/web_editor/static/src/scss/wysiwyg.scss @@ -1,322 +1,171 @@ -// NOTE EDITOR +$o-we-overlay-zindex: ($zindex-fixed + $zindex-modal-backdrop) / 2 !default; +$o-we-zindex: $o-we-overlay-zindex + 1 !default; -body.editor_enable { - padding-top: $o-wysiwyg-toolbar-height !important; -} +// Use css variables to control the default style of the editor so that an +// external assets bundle can influence it without duplicating the css. +:root { + --o-we-toolbar-height: #{$o-we-toolbar-height}; -#wrapwrap { - height: calc(100vh - #{$o-wysiwyg-toolbar-height}); - border: 1px solid $o-wysiwyg-border-color; - border-left: 0; - border-top: 0; + --o-we-bg-color: #{$o-we-bg-color}; + --o-we-bg-color-dark: #{$o-we-bg-color-dark}; + --o-we-color: #{$o-we-color}; + --o-we-color-accent: #{$o-we-color}; - .btn { - -webkit-user-select: none; - } + --o-we-dropdown-bg-color: #{$o-we-dropdown-bg-color}; + --o-we-dropdown-color: #{$o-we-dropdown-color}; + + --o-we-snippets-bg-color: #{$o-we-bg-color}; + --o-we-snippets-color: #{$o-we-color}; } -// TOP BAR (EDIT) +// EDITOR TOP BAR AND POPOVER +body.editor_enable { + padding-top: var(--o-we-toolbar-height) !important; +} #web_editor-top-edit { - height: $o-wysiwyg-toolbar-height; - background-color: $o-wysiwyg-bg-color; - z-index: $o-wysiwyg-zindex - 2; + @include o-position-absolute(0, 0, auto, 0); position: fixed; - top: 0px; - right: 0px; - left: 0px; - white-space: nowrap; - border: 1px solid $o-wysiwyg-border-color; - border-bottom: 1px solid rgba(0, 0, 0, 0.125); - - transition: background-color 400ms $o-we-md-ease 0s; - font-family: $o-we-font-family; -} - -.note-editor.note-frame { - background-color: inherit; - - .note-editing-area .note-editable { - background-color: transparent !important; - color: inherit; - padding-left: 0; - padding-right: 0; - } - - &.fullscreen { - background-color: #fff; + z-index: $o-we-zindex; + height: var(--o-we-toolbar-height); + background-color: var(--o-we-bg-color); + + .note-popover .popover { + top: 0 !important; + left: 0 !important; + right: 0 !important; + border: none !important; } } -.note-editor:not(.note-frame) .note-editing-area { - overflow: visible; -} - -#web_editor-top-edit .note-popover .popover { - height: $o-wysiwyg-toolbar-height - 2; - top: 0 !important; - left: 0 !important; - padding: 0; +.wysiwyg_iframe, +.note-editor { + border: $o-we-border-width solid $o-we-border-color; margin: 0; - border: 0; - width: auto; - text-align: center; - box-shadow: none; - background-color: $o-wysiwyg-bg-color; - z-index: $o-wysiwyg-zindex; - - .popover-body { - height: $o-wysiwyg-toolbar-height - 2; - - > .btn-group { - height: $o-wysiwyg-toolbar-height - 2; - margin-top: 0; - } - - .btn { - height: $o-wysiwyg-toolbar-height - 2; - border-top: 0; - } - } + padding: 0; } +.note-popover .popover .popover-body, +.panel-heading.note-toolbar { + height: var(--o-we-toolbar-height); + padding-bottom: 0; + border-bottom: $o-we-border-width solid $o-we-border-color; + background-color: var(--o-we-bg-color); + font-family: $o-we-font-family; + white-space: nowrap; -#web_editor-toolbars .popover, -.note-popover.popover .popover-content, -.note-editor .note-toolbar { - font-family: $o-wysiwyg-font-family; - - h1, - h2, - h3, - h4, - h5, - h6, - .h1, - .h2, - .h3, - .h4, - .h5, - .h6 { - font-family: $o-wysiwyg-font-family; - } - - .btn { + // Main layout of buttons + .btn-group, .btn { + width: auto !important; + height: 100% !important; + margin-top: 0; + margin-bottom: 0; + background: transparent; border: none; border-radius: 0; - background-color: inherit; + } + .btn-secondary { color: inherit; - transition: all 0.3s ease 0s; + } + + // Active buttons and opened dropdowns + .btn { + padding: 0.5em 0.75em !important; + border-left: 1px solid transparent; + border-right: 1px solid transparent; + color: var(--o-we-color); + font-size: $o-we-font-size !important; - >.fa { - color: inherit; + &.active { + background-color: var(--o-we-bg-color-dark); + color: var(--o-we-color-accent); } &:hover { - border-top: 2px solid inherit; - color: inherit; - - >.fa { - color: inherit; - } + color: var(--o-we-color-accent); } - .o_image_alt { - @include o-text-overflow(inline-block); - max-width: 100px; + &:focus, &:active, &:focus:active { + @extend %we-active-button; } - - &.active { - background-color: inherit; - color: inherit; - box-shadow: inset 0 3px 10px rgba(0, 0, 0, 0.5); - - >.fa { - color: inherit; - } + } + .btn-group.show { + > .btn { + @extend %we-active-button; } - - &.o_disabled { - opacity: 0.5; - pointer-events: none; + &::after { + content: ''; + @include o-position-absolute(100%, $o-we-border-width, auto, $o-we-border-width); + height: $o-we-border-width; + background-color: var(--o-we-dropdown-bg-color); } } - - .note-btn-group { - margin: 0; + %we-active-button { + border-left-color: $o-we-border-color; + border-right-color: $o-we-border-color; + background: var(--o-we-dropdown-bg-color); + box-shadow: none !important; + outline: none !important; } - - >.btn-group { - margin-top: 0; - - &.show, - .btn-group.show { - .dropdown-toggle { - border-top: 2px solid white; - background-color: $o-wysiwyg-bg-color-dark; - box-shadow: none; - - >.fa, - >span { - color: inherit; - } - } - } - } - .dropdown-menu { - margin: 0; + margin-top: 0; + border: $o-we-border-width solid $o-we-border-color; + border-top-color: transparent; border-radius: 0; - background-color: $o-wysiwyg-bg-color; - color: inherit; - white-space: normal; - - .dropdown-item { - width: 100%; - color: inherit; - - >.fa { - color: inherit; - } - - &:hover { - background-color: $o-wysiwyg-bg-color-dark; - color: inherit; - - >.fa { - color: inherit; - } - } - } + background-color: var(--o-we-dropdown-bg-color); + color: var(--o-we-dropdown-color); + box-shadow: none; } - - .note-para .dropdown-menu { + .dropdown-menu.show { // To overcome .note-XXX .dropdown-menu rules min-width: 0; - white-space: nowrap; } + .dropdown-item { // To overcome summernote rules breaking this in iframes + display: block; + max-width: none; + overflow: visible; + color: var(--o-we-color); - .note-color { - - .note-back-color-preview, - .note-fore-color-preview { - .dropdown-toggle { - padding: 0px 7px; - height: 35px; - width: auto; - border-top: none; - border-bottom: 3px solid; - - &:after { - display: none; - } - } - } - - .dropdown-menu { - min-width: 180px; - - .note-custom-color { - cursor: pointer; - border-radius: 5px; - - &:hover { - background-color: gray('400'); - color: black - } - } - } - - .dropdown-toggle:after { - display: none; - } - - .note-palette:first-child { - width: 100%; - - .note-color-reset, - .note-custom-color { - width: 162px; - } - - .note-color-reset { - padding: 0 3px !important; - height: 32px; - } - - .note-holder, - .note-color-palette .note-color-row, - .note-custom-color-palette .note-color-row { - height: auto !important; - - &::after { - content: ""; - display: table; - clear: both; - } - - .note-color-btn { - float: left; - height: 20px; - width: 20px; - margin: 1px 1px 0 0; - border: 1px solid black; - cursor: pointer; - @include o-alpha-button-preview; - - &.o_small { - width: 16px; - height: 16px; - margin: 3px 3px 2px 2px; - border-radius: 50%; - } - - &.o_clear { - clear: both; - } + &:hover { + background-color: var(--o-we-bg-color) !important; + color: var(--o-we-color-accent) !important; - &.o_btn_transparent { - display: none; - } - } + > * { + color: inherit !important; } } } -} - -.note-popover.popover.note-hint-popover .popover-content { - height: auto; -} -.note-popover, -.note-editor { - .modal .modal-footer p { - display: none; + // Specific elements + .o_image_alt { + @include o-text-overflow(); + max-width: 150px; } -} - -.note-handle { - pointer-events: none; - - .note-control-selection { - z-index: 1; - overflow: hidden; - - .note-control-se { - border-top: 0; - border-left: 0; - background: transparent; - } + .note-color-palette div .note-color-btn { + border-color: var(--o-we-dropdown-bg-color); } } -.o_fake_editable:not([data-oe-model]):focus { - outline: none; +// ANIMATIONS +@keyframes fadeInDownSmall { + 0% { + opacity: 0; + transform: translate(0, -5px); + } + 100% { + opacity: 1; + transform: translate(0, 0); + } } -.note-editor.dragover .note-dropzone { - z-index: $o-wysiwyg-zindex + 1; +@keyframes inputHighlighter { + from { + background: $o-brand-primary; + } + to { + width: 0; + background: transparent; + } } // MODALS - body .modal { // SELECT MEDIA @@ -362,7 +211,7 @@ body .modal { @include o-position-absolute($top: 0, $right: 0); border-radius: 0 0 0 2px; &:hover { - color: $o-wysiwyg-color-danger; + color: $o-we-color-danger; } } @@ -384,13 +233,8 @@ body .modal { } } - .o_we_attachment_selected { - border-color: $o-brand-primary; - box-shadow: 0px 0px 2px 2px $o-brand-primary; - } - .font-icons-icons { - >span { + > span { text-align: center; font-size: 22px; margin: 5px; @@ -401,8 +245,8 @@ body .modal { &.o_selected { cursor: auto; - background-color: $o-wysiwyg-bg-color-dark; - box-shadow: 0px 0px 0px 1px $o-wysiwyg-color-success; + background-color: $o-we-bg-color-dark; + box-shadow: 0px 0px 0px 1px $o-we-color-success; } } } @@ -421,7 +265,7 @@ body .modal { position: relative; width: 100%; - >textarea { + > textarea { width: 100%; min-height: 95px; padding-bottom: 25px; @@ -460,7 +304,7 @@ body .modal { .cropper-point { background-color: white; - transition: all 400ms $o-wysiwyg-md-ease 0s; + transition: all 400ms $o-we-md-ease 0s; transform: scale(1.5, 1.5); border-radius: 10px; box-shadow: 0 0 1px 1px rgba(23, 23, 23, 0.5); @@ -482,7 +326,7 @@ body .modal { // LINK EDITOR DIALOG COLOR SELECTOR .o_link_dialog { - .o_link_dialog_color>.o_link_dialog_color_item { + .o_link_dialog_color > .o_link_dialog_color_item { position: relative; width: 30px; height: 30px; @@ -490,7 +334,7 @@ body .modal { border: 2px solid rgba(0, 0, 0, 0.25); border-radius: 50%; - input:checked+i::before { + input:checked + i::before { content: "\f00c"; @include o-position-absolute($top: 6px, $left: 5px); } @@ -498,7 +342,7 @@ body .modal { &.btn-link { padding: 3px 9px; - >span { + > span { text-decoration: underline; } } @@ -511,7 +355,7 @@ body .modal { .o_we_image_optimize_dialog { .o_we_title_label { - font-size: 1rem; + font-size: $o-we-font-size; } .o_we_preview_area { max-height: 400px; @@ -539,32 +383,3 @@ img.o_we_selected_image { img.o_we_custom_image.mx-auto { display: block; } - -// ENTER IN EDIT MODE - -.note-editor .css_editable_mode_display { - border: 1px dashed rgba(153, 153, 153, 0.5); - display: block !important; -} - -.note-editable { - - li, - p, - h1, - h2, - h3, - h4, - h5, - h6 { - &:empty:after { - content: "\00a0" - } - } -} - -.wysiwyg_iframe { - margin: 0; - padding: 0; - border: 0; -} diff --git a/addons/web_editor/static/src/scss/wysiwyg_iframe.scss b/addons/web_editor/static/src/scss/wysiwyg_iframe.scss index e1af34e41fa..e7554317994 100644 --- a/addons/web_editor/static/src/scss/wysiwyg_iframe.scss +++ b/addons/web_editor/static/src/scss/wysiwyg_iframe.scss @@ -16,21 +16,8 @@ iframe.wysiwyg_iframe.o_fullscreen { body.o_in_iframe { background-color: $o-view-background-color; - overflow: auto; - - .note-editable { - height: calc(100vh - #{$o-wysiwyg-toolbar-height} - 2px); - } .note-statusbar { display: none; } - - #web_editor-toolbars .popover .btn { - font-size: 1rem; - } - - .o_mail_theme_selector .btn { - font-size: 1rem; - } } diff --git a/addons/web_editor/static/src/scss/wysiwyg_snippets.scss b/addons/web_editor/static/src/scss/wysiwyg_snippets.scss index 6b576acecba..d3f10ebd526 100644 --- a/addons/web_editor/static/src/scss/wysiwyg_snippets.scss +++ b/addons/web_editor/static/src/scss/wysiwyg_snippets.scss @@ -2,86 +2,85 @@ /// This file contains all variables and mixins that are specific to the editor. /// -$o-wysiwyg-handles-accent-color: #00e2ff !default; -$o-wysiwyg-handle-edge-size: 1px !default; -$o-wysiwyg-dropzone-size: 40px !default; -$o-wysiwyg-sidebar-width: 210px !default; -$o-wysiwyg-sidebar-height: 36px !default; -$o-wysiwyg-overlay-option-size: 25px !default; -$o-wysiwyg-handles-btn-size: 18px !default; -$o-wysiwyg-handle-border-width: 1px !default; - -$o-wysiwyg-handles-offset-to-hide: 10000px !default; - // OVERRIDES FOR EDITOR WITH SNIPPETS body.editor_enable.editor_has_snippets { padding-left: $o-we-sidebar-width !important; #web_editor-top-edit .note-popover .popover { - margin: 0 0 0 $o-we-sidebar-width; + left: $o-we-sidebar-width !important; } -} - -// NOTE EDITOR -.note-editor.o_snippets_loaded { - transition: padding-left 400ms $o-wysiwyg-md-ease 0s; - padding-left: 210px !important; -} - -.note-editor.note-frame .note-editing-area .o_wrap_editable_snippets { - overflow: auto; - .note-editable { - overflow: visible; + .modal:not(.o_technical_modal) { + // set z-index so customize options visible on dialog. + z-index: $o-we-overlay-zindex - 1; + // just for fake backdrop effect + background-color: rgba(66, 66, 66, 0.4); } } -.o_in_iframe .note-editor.o_snippets_loaded .note-editable { - padding-top: 30px; -} - // SNIPPET PANEL #oe_snippets { @include o-w-preserve-btn; + @include o-position-absolute(0, auto, 0, -$o-we-sidebar-width); + position: fixed; + z-index: $o-we-zindex; display: flex; flex-flow: column nowrap; - position: fixed; - top: 0; - width: $o-wysiwyg-sidebar-width; - z-index: $o-wysiwyg-zindex - 1; + width: $o-we-sidebar-width; - font-family: Roboto, $font-family-sans-serif; - border-right: 1px solid #aaa; - transition: left 400ms $o-wysiwyg-md-ease 0s; - background-image: linear-gradient(45deg, $o-wysiwyg-bg-color, darken($o-wysiwyg-bg-color, 10%)); + border-right: $o-we-border-width solid $o-we-border-color; + background-color: var(--o-we-snippets-bg-color); + color: var(--o-we-snippets-color); box-shadow: 0px 10px 10px -10px black inset; - border: 1px solid $o-wysiwyg-border-color; + font-family: Roboto, $font-family-sans-serif; + + transition: left 400ms $o-we-md-ease 0s; + + &.o_loaded { + left: 0 !important; + } #snippets_menu { flex: 0 0 auto; - height: $o-wysiwyg-toolbar-height; - line-height: $o-wysiwyg-sidebar-height; - margin: 0; - padding: 0; - background: $o-wysiwyg-bg-color; - text-align: center; - font-weight: normal; - font-size: 17px; - color: $o-wysiwyg-color; - font-family: $o-wysiwyg-font-family; + display: flex; + height: var(--o-we-toolbar-height); + background: var(--o-we-bg-color); + color: var(--o-we-color); + font-family: $o-we-font-family; + + > button { + flex: 1 1 auto; + border: none; + background: transparent; + color: inherit; + @include o-text-overflow(); + + &.active { + background: var(--o-we-snippets-bg-color); + color: var(--o-we-snippets-color); + } + + &:focus, &:active, &:focus:active { + outline: none; + } + } } - #o_scroll { + > #o_scroll, > .o_we_customize_panel { + flex: 1 1 auto; height: 100%; overflow: auto; + } + + > #o_scroll { .o_panel_header { display: flex; align-items: center; justify-content: center; padding: 3%; - color: $o-wysiwyg-color; + color: inherit; margin-bottom: 3%; i { @@ -149,24 +148,130 @@ body.editor_enable.editor_has_snippets { } } - &:nth-child(2n+1) { + &:nth-child(2n + 1) { clear: left; } } } } - &.o_loaded { - left: 0 !important; + > .o_we_customize_panel { + + we-button, we-toggler { + cursor: pointer; + @include o-text-overflow(inline-block); + padding: 0.25rem 0.5rem; + margin: 0; + background-color: var(--o-we-bg-color-dark); + color: var(--o-we-color); + line-height: 1.5; + font-weight: 500; + white-space: nowrap; + } + + %we-icon-button { + position: relative; + + &::after { + position: absolute; + width: 1em; + right: 0.5em; + text-align: center; + font-family: FontAwesome; + } + } + + @mixin we-icon-button($icon, $color: null) { + @extend %we-icon-button; + padding-right: 0.5em + 1em + 0.5em; + + &::after { + content: $icon; + color: $color; + } + } + + we-customizeblock-options { + display: block; + color: var(--o-we-snippets-color); + margin-top: 0.5rem; + + > we-title { + display: flex; + align-items: center; + margin: 0.25rem 0; + padding: 0 0.5rem; + font-weight: bold; + color: inherit !important; + + > we-button-group { + flex: 0 0 auto; + display: block; + margin-left: auto; + text-align: center; + + > we-button { + margin-left: 0.25rem; + width: 1.5rem; + padding: 0.125rem; + } + } + } + } + + we-customizeblock-option { + position: relative; + display: block; + + > * { + display: block; + margin-top: 0.25rem; + } + + we-button, we-toggler { + display: block; + text-align: left; + border-radius: 0; + } + we-button { + &.active { + @include we-icon-button('\f00c', $o-we-color-success); + } + } + we-collapse { + position: relative; + display: block; + max-height: 0; + overflow: hidden; + transition: max-height 200ms ease 0s; + } + we-toggler { + @include we-icon-button('\f104'); + + &.active { + @include we-icon-button('\f107'); + + + we-collapse { + max-height: 500px; + } + } + } + + .dropdown-menu { + // FIXME temporary fix for m2o option for example + position: static !important; + } + } } } .oe_snippet { // No root because can be drag and drop (and the helper is in the body) position: relative; + z-index: $o-we-zindex; width: 100px; - background-color: $o-wysiwyg-bg-color; - z-index: $o-wysiwyg-zindex; + background-color: var(--o-we-snippets-bg-color); + color: var(--o-we-snippets-color); > :not(.oe_snippet_thumbnail) { display: none !important; @@ -178,7 +283,7 @@ body.editor_enable.editor_has_snippets { .oe_snippet_thumbnail_img { width: 100%; height: 0; - border: 1px solid $o-wysiwyg-border-color; + border: $o-we-border-width solid $o-we-border-color; padding-bottom: 75%; background-size: cover; background-position: center center; @@ -193,16 +298,14 @@ body.editor_enable.editor_has_snippets { .oe_snippet_thumbnail_title { display: block; - border: 1px solid $o-wysiwyg-bg-color; padding: 5px; font-size: 12px; font-weight: 300; text-shadow: none; - color: $o-wysiwyg-color; } &:hover .oe_snippet_thumbnail_title { - color: $o-wysiwyg-color-light; + color: var(--o-we-color-accent); } } } @@ -212,66 +315,59 @@ body.editor_enable.editor_has_snippets { .o_colorpicker_sections { font-size: 14px; - .o_colorpicker_section_tabs { - width: 225px; - height: 100%; - } - button { float: left; - height: 25px; - width: 25px; - margin: 1px 1px 0 2px; - border: 1px solid black; + width: percentage(1 / 8); + padding-top: percentage(1 / 8); + margin: 0; + border: none; + box-shadow: inset 0 0 0 1px var(--o-we-snippets-bg-color); } - div[data-group="transparent_grayscale"] button { - @include o-alpha-button-preview; + .o_colorpicker_section { - &.o_small { - width: 23px; - height: 23px; - margin: 2px; - border-radius: 50%; - } - - &:hover, - &.selected { - box-shadow: 0px 0px 2px 2px $o-wysiwyg-color; + > :first-child { + margin: 8px 0 4px; } + button { + position: relative; - &.selected:before { - content: "\f00c"; - font-family: "FontAwesome"; - padding-top: 1px; - color: theme-color('success'); - } + &.o_small { + border-radius: 50%; + } - &[data-event="foreColor"] { - background-color: $o-wysiwyg-bg-color; + &:hover, + &.selected { + box-shadow: inset 0 0 1px 1px var(--o-we-snippets-bg-color); + } - &:before { - background-color: rgba(white, 0.3); + &.selected::before { + @include o-position-absolute(0, 0, 0, 0); + content: "\f00c" !important; + display: flex; + justify-content: center; + align-items: center; + font-family: FontAwesome !important; + color: $o-we-color-success; } - } - &.o_btn_transparent::before { - background-color: transparent; + &.o_btn_transparent::before { + background-color: transparent; + } } - } - .note-color-palette>div { - clear: both; - } + &[data-name="transparent_grayscale"] button { + @include o-alpha-button-preview; - .note-color-row { - height: 28px; + &::before, &::after { + box-shadow: inherit; + } + } } } .note-palette-title { - padding: 10px 0px 10px 10px; - color: $o-wysiwyg-color-light; + padding: 10px 0 10px 10px; font-weight: bold; } @@ -282,14 +378,14 @@ body.editor_enable.editor_has_snippets { .note-color-reset { font-size: 20px !important; - color: desaturate(rgba($o-wysiwyg-color-danger, 0.6), 40%); + color: desaturate(rgba($o-we-color-danger, 0.6), 40%); margin: 1px 0 0; padding: 0; cursor: pointer; &:hover { background: transparent !important; - color: $o-wysiwyg-color-danger; + color: $o-we-color-danger; } } } @@ -298,50 +394,26 @@ body.editor_enable.editor_has_snippets { // DROPZONES @keyframes dropZoneInsert { to { - background-color: rgba($o-brand-odoo, 0.2); + background-color: rgba($o-brand-odoo, 0.3); } } -.note-editable .oe_drop_zone { - background-color: rgba($o-brand-odoo, 0.05); +.oe_drop_zone { + background-color: rgba($o-brand-odoo, 0.15); animation: dropZoneInsert 1s linear 0s infinite alternate; &.oe_insert { - z-index: $o-wysiwyg-zindex - 1; - - border: 2px dashed $o-wysiwyg-border-color; - border-top: none; - border-bottom: none; - position: relative; + z-index: $o-we-overlay-zindex; width: 100%; - height: $o-wysiwyg-dropzone-size; - margin: (-$o-wysiwyg-dropzone-size/2) 0; - - &:after { - content: ""; - display: block; - width: 100%; - height: 50%; - border-bottom: 2px dashed $o-wysiwyg-border-color; - box-sizing: content-box; - } + height: $o-we-dropzone-size; + margin: (-$o-we-dropzone-size/2) 0; + border: 2px dashed $o-we-border-color; &.oe_vertical { - border: 2px dashed $o-brand-odoo; - border-left: none; - border-right: none; - - width: $o-wysiwyg-dropzone-size; + width: $o-we-dropzone-size; float: left; - margin: 0 (-$o-wysiwyg-dropzone-size/2); - - &:after { - width: 50%; - height: 100%; - border-bottom: none; - border-right: 2px dashed $o-wysiwyg-border-color; - } + margin: 0 (-$o-we-dropzone-size/2); } } } @@ -349,13 +421,14 @@ body.editor_enable.editor_has_snippets { // MANIPULATORS #oe_manipulators { position: relative; - z-index: 1; + z-index: $o-we-overlay-zindex; // SNIPPET MANIPULATORS .oe_overlay { @include o-position-absolute; display: none; height: 0; + border-color: $o-we-handles-accent-color; background: transparent; text-align: center; transition: opacity 400ms linear 0s; @@ -363,301 +436,94 @@ body.editor_enable.editor_has_snippets { &.o_keypress { opacity: 0; } - &.oe_active { display: block; - } - - // OVERLAY OPTIONS - >.oe_overlay_options { - font-family: $o-wysiwyg-font-family; - @include o-position-absolute($bottom: 0, $left: 0); - width: 250px; // the parent oe_overlay has a width equal to the snippet width, the 250px is there to force the options to stay on one line without overriding bootstrap .btn-group - text-align: left; - z-index: $o-wysiwyg-zindex + 1; - - >.btn-group { - white-space: nowrap; - border: 0; - - .btn { - display: inline-block; - width: $o-wysiwyg-overlay-option-size; - height: $o-wysiwyg-overlay-option-size; - padding: 0; - border: 1px solid $o-wysiwyg-bg-color; - line-height: $o-wysiwyg-overlay-option-size; - font-size: 11px; - border-radius: 0; - font-weight: normal; - - transition: all 400ms ease 0s; - color: $o-wysiwyg-color; - background-color: $o-wysiwyg-bg-color; - - &:hover { - color: $o-wysiwyg-color-light; - background-color: lighten($o-wysiwyg-bg-color, 10%); - } - - &.oe_snippet_remove { - color: $o-wysiwyg-color; - background-color: $o-wysiwyg-color-danger; - border-color: darken($o-wysiwyg-color-danger, 20%); - - >.fa { - color: $o-wysiwyg-color; - } - - &:hover { - background-color: darken($o-wysiwyg-color-danger, 20%); - } - } - - &.oe_snippet_parent>i { - transform: scaleX(-1); - font-size: 12px; - } - - >.fa { - color: $o-wysiwyg-color; - line-height: inherit; - vertical-align: top; - } - } - - // CUSTOMIZE MENU BUTTON - >.oe_options { - .btn { - width: auto; - padding: 0 25px 0 5px; - background-color: $o-wysiwyg-bg-color; - text-transform: uppercase; - - &:hover, - &:active, - &:focus { - background-color: lighten($o-wysiwyg-bg-color, 5%); - border-color: lighten($o-wysiwyg-bg-color, 10%); - } - - &:before, - &:after { - content: ""; - width: 7px; - height: 2px; - background-color: $o-wysiwyg-color-light; - @include o-position-absolute(46%); - transition: all 0.3s ease 0s; - } - - &:before { - right: 9px; - transform: rotate(45deg); - } - - &:after { - right: 5px; - transform: rotate(-45deg); - } - } - - // Open menu - &.show .btn { - - &:active, - &:focus, - &:active:focus { - box-shadow: none; - } - - &:before, - &:after { - width: 9px; - } - - &:before { - right: 10px; - transform: translateX(5px) rotate(-45deg); - } - - &:after { - transform: rotate(45deg); - } - } - - // CUSTOMIZE MENU - .dropdown-menu { - margin: -1px 0 0 0; - padding: 0; - border: 1px solid $o-wysiwyg-bg-color; - border-radius: 0; - background-color: $o-wysiwyg-bg-color; - - .dropdown-item { - position: relative; - color: $o-wysiwyg-color-light; - font-weight: normal; - font-size: 12px; - padding: 5px 40px 5px 10px; - - &:hover { - background: transparent; - cursor: pointer; - } - - &.active { - &:before { - @include o-position-absolute($top: 5px, $right: 5px); - content: "\f00c"; - font-family: "FontAwesome"; - color: $o-brand-primary; - } - } - - >i { - width: 15px; - margin-right: 10px; - text-align: center; - } - } - - .dropdown-submenu { - position: relative; - - &::before { - @include o-position-absolute($top: 12px, $right: 10px); - @include o-caret-right(4px); - border-left-color: $o-wysiwyg-color-light; - } - - &:hover>.dropdown-menu { - display: block; - left: 100%; - top: 0; - - &.o_open_to_left { - left: auto; - right: 100%; - } - } - } - - .dropdown-divider { - display: block; - height: 1px; - border-top: 1px solid #666; - margin: 0; - } - - .dropdown-header { - color: $o-wysiwyg-color-light; - font-size: 11px; - margin-top: 2px; - text-transform: uppercase; - - &:hover { - background-color: initial; - } - - &.o_main_header { - padding-left: 10px; - } - - &.o_parent_editor_header { - position: relative; - color: $o-wysiwyg-color-light; - font-weight: 400; - font-size: 10px; - margin: 10px 0 0 10px; - text-transform: uppercase; - padding-bottom: .25em; - - &::before { - content: "\f148"; - @include o-position-absolute($top: 5px, $left: 4px); - font-family: FontAwesome; - transform: scaleX(-1); - } - - ~.dropdown-submenu .dropdown-item, - ~.dropdown-item { - font-size: 11px; - opacity: 0.7; - - &:hover { - opacity: 1; - } - } - - ~.dropdown-submenu::before { - @include o-position-absolute($top: 10px, $right: 10px); - } - } - } - } - } - } - } - - &.o_top_cover>.oe_overlay_options { - bottom: auto; - top: 1px; - right: 1px; + z-index: 1; } // HANDLES - >.o_handles { - @include o-position-absolute(-$o-wysiwyg-handles-offset-to-hide, 0, auto, 0); - z-index: $o-wysiwyg-zindex; + > .o_handles { + @include o-position-absolute(-$o-we-handles-offset-to-hide, 0, auto, 0); + border-color: inherit; - &:hover>.o_handle { - background-color: rgba($o-wysiwyg-handles-accent-color, 0.05); + &:hover > .o_handle { + background-color: rgba($o-we-handles-accent-color, 0.05); } - - >.o_handle { - + > .o_handle { position: relative; - border: 0 solid $o-wysiwyg-handles-accent-color; + border: 0 solid transparent; + border-color: inherit; transition: background 300ms ease 0s; - &:hover, - &.o_active { - background-color: rgba($o-wysiwyg-handles-accent-color, 0.2); + &.w { + @include o-position-absolute($o-we-handles-offset-to-hide, auto, -$o-we-handles-offset-to-hide, 0); + width: $o-we-handle-edge-size; + border-width: $o-we-handle-border-width; + border-right-width: 0; + cursor: e-resize; - &:before { - content: ''; - @include o-position-absolute(0, $left: 50%); - width: 1px; - height: 100%; - margin-left: -1px; - background-color: rgba($o-wysiwyg-handles-accent-color, 0.5); + &:after { + @include o-position-absolute($top: 50%, $left: 40%); + margin-top: -$o-we-handles-btn-size/2; + } + } + &.e { + @include o-position-absolute($o-we-handles-offset-to-hide, 0, -$o-we-handles-offset-to-hide, auto); + width: $o-we-handle-edge-size; + border-right-width: $o-we-handle-border-width; + cursor: w-resize; + + &:after { + @include o-position-absolute($top: 50%, $right: 40%); + margin-top: -$o-we-handles-btn-size/2; } + } + &.n { + @include o-position-absolute($o-we-handles-offset-to-hide, 0, auto, 0); + height: $o-we-handle-edge-size; + border-top-width: $o-we-handle-border-width; + cursor: ns-resize; - &.w:before { - @include o-position-absolute(50%, $left: 0); - width: 100%; - height: 1px; + &:after { + @include o-position-absolute($left: 50%, $top: 40%); + margin-left: -$o-we-handles-btn-size/2; } + } + &.s { + @include o-position-absolute(auto, 0, -$o-we-handles-offset-to-hide, 0); + height: $o-we-handle-edge-size; + border-bottom-width: $o-we-handle-border-width; + cursor: ns-resize; &:after { - border-color: darken($o-wysiwyg-handles-accent-color, 10%); - background-color: darken($o-wysiwyg-handles-accent-color, 20%); + @include o-position-absolute($left: 50%, $bottom: 40%); + margin-left: -$o-we-handles-btn-size/2; } } - &:after { + &::after { + z-index: 1; display: block; - width: $o-wysiwyg-handles-btn-size; - height: $o-wysiwyg-handles-btn-size; - border: solid 1px darken($o-wysiwyg-handles-accent-color, 10%); - line-height: $o-wysiwyg-handles-btn-size - 2; + width: $o-we-handles-btn-size; + height: $o-we-handles-btn-size; + border: solid 1px darken($o-we-handles-accent-color, 10%); + line-height: $o-we-handles-btn-size - 2; font-size: 14px; font-family: FontAwesome; - background-color: darken($o-wysiwyg-handles-accent-color, 20%); + background-color: darken($o-we-handles-accent-color, 20%); color: white; } + &:hover, + &.o_active { + background-color: rgba($o-we-handles-accent-color, 0.2); + + &::after { + border-color: darken($o-we-handles-accent-color, 10%); + background-color: darken($o-we-handles-accent-color, 20%); + } + } + &.w:after, &.e:after { content: "\f07e"; @@ -694,64 +560,103 @@ body.editor_enable.editor_has_snippets { } } - &.w { - @include o-position-absolute($o-wysiwyg-handles-offset-to-hide, auto, -$o-wysiwyg-handles-offset-to-hide, 0); - width: $o-wysiwyg-handle-edge-size; - border-width: $o-wysiwyg-handle-border-width; - border-right-width: 0; - cursor: e-resize; + &.readonly { + cursor: auto !important; &:after { - @include o-position-absolute($top: 50%, $left: 40%); - margin-top: -$o-wysiwyg-handles-btn-size/2; + display: none !important; + } + + &:hover { + opacity: 0.5; } } + } - &.e { - @include o-position-absolute($o-wysiwyg-handles-offset-to-hide, 0, -$o-wysiwyg-handles-offset-to-hide, auto); - width: $o-wysiwyg-handle-edge-size; - border-right-width: $o-wysiwyg-handle-border-width; - cursor: w-resize; + > .o_move_handle { + @include o-position-absolute($o-we-handles-offset-to-hide, $left: 50%); + transform: translate(-50%, -110%); + padding: 2px 4px; + border: 1px solid $o-we-color-text-normal; + border-radius: 20%; + color: $o-we-color-text-normal; + opacity: 0.5; + transition: opacity 400ms ease 0; + cursor: pointer; + cursor: move; - &:after { - @include o-position-absolute($top: 50%, $right: 40%); - margin-top: -$o-wysiwyg-handles-btn-size/2; - } + &::before, &::after { + content: '\f141'; + display: block; + font-family: FontAwesome; + font-size: 0.8em; + line-height: 0.35em; } - &.n { - @include o-position-absolute($o-wysiwyg-handles-offset-to-hide, 0, auto, 0); - border-top-width: $o-wysiwyg-handle-border-width; - cursor: ns-resize; + &:hover { + opacity: 1; + } + } + } - &:after { - @include o-position-absolute($left: 50%, $top: 40%); - margin-left: -$o-wysiwyg-handles-btn-size/2; + &.o_we_overlay_preview { + border-color: $o-we-handles-accent-color-preview; + + > .o_handles { + + > .o_handle { + border-style: dashed; + background-color: transparent !important; + + &::before { + content: ''; + @include o-position-absolute(0, 0, 0, 0); + z-index: -1; + display: block; + border-width: inherit; + border-style: solid; + border-color: white; + } + &::after { + display: none; } - } - &.s { - @include o-position-absolute(auto, 0, -$o-wysiwyg-handles-offset-to-hide, 0); - border-bottom-width: $o-wysiwyg-handle-border-width; - cursor: ns-resize; + &.w { + border-width: $o-we-handle-border-width-preview; + border-right-width: 0; - &:after { - @include o-position-absolute($left: 50%, $bottom: 40%); - margin-left: -$o-wysiwyg-handles-btn-size/2; + &::before { + top: -$o-we-handle-border-width-preview; + bottom: -$o-we-handle-border-width-preview; + left: -$o-we-handle-border-width-preview; + } } - } + &.e { + border-right-width: $o-we-handle-border-width-preview; - &.readonly { - cursor: auto !important; + &::before { + right: -$o-we-handle-border-width-preview; + } + } + &.n { + border-top-width: $o-we-handle-border-width-preview; - &:after { - display: none !important; + &::before { + top: -$o-we-handle-border-width-preview; + } } + &.s { + border-bottom-width: $o-we-handle-border-width-preview; - &:hover { - opacity: 0.5; + &::before { + bottom: -$o-we-handle-border-width-preview; + } } } + + > .o_move_handle { + display: none; + } } } } diff --git a/addons/web_editor/static/src/scss/wysiwyg_variables.scss b/addons/web_editor/static/src/scss/wysiwyg_variables.scss deleted file mode 100644 index 2beafe71681..00000000000 --- a/addons/web_editor/static/src/scss/wysiwyg_variables.scss +++ /dev/null @@ -1,42 +0,0 @@ -// Mixins for buttons with alpha bg-color -@mixin o-alpha-button-preview { - position: relative; - z-index: 1; - - // Place an overlay that inherit the button's bg-color. - &::before { - content: ""; - @include o-position-absolute(0, 0, 0, 0); - background-color: inherit; - } - - &::after { - content: ""; - @include o-position-absolute(0, 0, 0, 0); - z-index: -1; - background-image: url('/web_editor/static/src/img/transparent.png'); - background-size: 10px auto; - } -} - -$o-wysiwyg-zindex: 1000; - -$o-wysiwyg-md-ease: cubic-bezier(0.19, 1, 0.22, 1) !default; - -$o-wysiwyg-font-family: Roboto, -"Montserrat", -"Segoe UI", -"Helvetica Neue", -Helvetica, -Arial, -sans-serif; - -$o-wysiwyg-toolbar-height: 32px; - -$o-wysiwyg-color: #212629 !default; -$o-wysiwyg-color-light: #616669 !default; -$o-wysiwyg-color-danger: #912629 !default; -$o-wysiwyg-color-success: #219629 !default; -$o-wysiwyg-bg-color: #f7f7f7 !default; -$o-wysiwyg-bg-color-dark: #a7a7a7 !default; -$o-wysiwyg-border-color: #a9a9a9 !default; diff --git a/addons/web_editor/static/src/xml/snippets.xml b/addons/web_editor/static/src/xml/snippets.xml index 6e2ae656f2d..8e1fe723f0d 100644 --- a/addons/web_editor/static/src/xml/snippets.xml +++ b/addons/web_editor/static/src/xml/snippets.xml @@ -3,29 +3,38 @@ <t t-name="web_editor.snippet_overlay"> <div class="oe_overlay"> <div class="o_handles"> + <!-- Visible overlay borders + allow to resize when not readonly --> <div class="o_handle n readonly"/> <div class="o_handle e readonly"/> <div class="o_handle w readonly"/> <div class="o_handle s readonly"/> - </div> - <div class="oe_overlay_options" contentEditable="false"> - <div class="btn-group"> - <a role="button" href="#" class="btn btn-secondary oe_snippet_parent" title="Select Parent Container" aria-label="Select Parent Container"><i class="fa fa-lg fa-level-up"/></a> - <div class="dropdown oe_options d-none btn-group"> - <a role="button" href="#" data-toggle="dropdown" class="btn btn-primary" title="Customize">Customize</a> - <div class="dropdown-menu" role="menu"/> - </div> - <a role="button" href="#" class="btn btn-secondary oe_snippet_move" title="Drag to Move" aria-label="Drag to Move"><i class="fa fa-arrows ml4 mr4"></i></a> - <a role="button" href="#" class="btn btn-secondary oe_snippet_clone" title="Duplicate Container" aria-label="Duplicate Container"><i class="fa fa-files-o ml4 mr4"></i></a> - <a role="button" href="#" class="btn btn-secondary oe_snippet_remove" title="Remove Block" aria-label="Remove Block"><i class="fa fa-trash-o ml4 mr4"></i></a> - </div> + + <!-- Overlay button-like handle to drag and drop --> + <div class="o_move_handle"/> </div> </div> </t> + <t t-name="web_editor.customize_block_options_section"> + <we-customizeblock-options> + <we-title> + <span t-esc="name"/> + <we-button-group> + <we-button class="fa fa-files-o oe_snippet_clone" + title="Duplicate Container" + aria-label="Duplicate Container"/> + <we-button class="fa fa-trash-o oe_snippet_remove" + title="Remove Block" + aria-label="Remove Block"/> + </we-button-group> + </we-title> + </we-customizeblock-options> + </t> + <t t-name="web_editor.customize_block_option"> + <we-customizeblock-option/> + </t> <!-- options --> <div t-name="web_editor.snippet.option.colorpicker" class="colorpicker"> - <div class="note-palette-title">Background Color</div> <div class="btn-group palette-reset"> <div class="note-color-reset" data-event="foreColor" data-value="inherit" title="None" role="img" aria-label="No color"> <i class="fa fa-ban"/> |
