From dfd98f49e7f2ab5c94dcae3af2d14750d301fa0a Mon Sep 17 00:00:00 2001 From: Samuel Degueldre Date: Thu, 24 Oct 2019 13:15:55 +0000 Subject: [IMP] web_editor: rework background-sizing option Previously, background size and position for snippets was configured in a modal, the user could choose a "focus point", which is rather unintuitive. They could also choose to use the "contain" background mode, with or without repeat, or a third option, where they could customize everything, from the background size in both directions to whether to repeat the background in only one or in both directions. This commit streamlines the background-sizing options, only the "cover" and "repeat pattern" options have been kept, as they cover almost every use case. Their configuration has been moved into the options menu for consistency with the rest of the editor, and the background's position is now chosen by clicking on the corresponding menu option, and dragging the background to the desired place. Part of https://github.com/odoo/odoo/pull/38959 maintask-2066614 task-2088304 --- .../static/src/js/editor/snippets.options.js | 380 ++++++++++++--------- 1 file changed, 228 insertions(+), 152 deletions(-) (limited to 'addons/web_editor/static/src/js') 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 1237c041bf1..49867305fa7 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -3,7 +3,6 @@ odoo.define('web_editor.snippets.options', function (require) { var core = require('web.core'); var ColorPaletteWidget = require('web_editor.ColorPalette').ColorPaletteWidget; -var Dialog = require('web.Dialog'); var Widget = require('web.Widget'); var weWidgets = require('wysiwyg.widgets'); @@ -1520,9 +1519,9 @@ registry.background = SnippetOption.extend({ }); /** - * Handles the edition of snippet's background image position. + * Handles the edition of snippets' background image position. */ -registry['background_position'] = SnippetOption.extend({ +registry.BackgroundPosition = SnippetOption.extend({ xmlDependencies: ['/web_editor/static/src/xml/editor.xml'], /** @@ -1530,16 +1529,36 @@ registry['background_position'] = SnippetOption.extend({ */ start: function () { this._super.apply(this, arguments); - var self = this; - this.$target.on('snippet-option-change', function () { - self.onFocus(); + + this._initOverlay(); + this.img = document.createElement('img'); + this.img.src = this._getSrcFromCssValue(); + + this.$target.on('snippet-option-change', () => { + // Hides option if the bg image is removed in favor of a bg color + this._updateUI(); + // this.img is used to compute dragging speed + this.img.src = this._getSrcFromCssValue(); }); + + // Resize overlay content on window resize because background images + // change size, and on carousel slide because they sometimes take up + // more space and move elements around them. + $(window).on('resize.bgposition', () => this._dimensionOverlay()); + }, + /** + * @override + */ + destroy: function () { + this._toggleBgOverlay(false); + $(window).off('.bgposition'); + this._super.apply(this, arguments); }, /** * @override */ onFocus: function () { - this.$el.toggleClass('d-none', this.$target.css('background-image') === 'none'); + this._updateUI(); }, //-------------------------------------------------------------------------- @@ -1547,77 +1566,31 @@ registry['background_position'] = SnippetOption.extend({ //-------------------------------------------------------------------------- /** - * Opens a Dialog to edit the snippet's backgroung image position. + * Sets the background type (cover/repeat pattern). * - * @see this.selectClass for parameters + * @see this.selectClass for params */ - backgroundPosition: function (previewMode, value, $opt) { - var self = this; - - this.previousState = [this.$target.attr('class'), this.$target.css('background-size'), this.$target.css('background-position')]; - - 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"), - $content: $(qweb.render('web_editor.dialog.background_position')), - buttons: [ - {text: _t("Ok"), classes: 'btn-primary', close: true, click: _.bind(this._saveChanges, this)}, - {text: _t("Discard"), close: true, click: _.bind(this._discardChanges, this)}, - ], - }).open(); - - this.modal.opened().then(function () { - // Fetch data form $target - var value = ((self.$target.hasClass('o_bg_img_opt_contain')) ? 'contain' : ((self.$target.hasClass('o_bg_img_opt_custom')) ? 'custom' : 'cover')); - self.modal.$('> label > input[value=' + value + ']').prop('checked', true); - - if (self.$target.hasClass('o_bg_img_opt_repeat')) { - self.modal.$('#o_bg_img_opt_contain_repeat').prop('checked', true); - self.modal.$('#o_bg_img_opt_custom_repeat').val('o_bg_img_opt_repeat'); - } else if (self.$target.hasClass('o_bg_img_opt_repeat_x')) { - self.modal.$('#o_bg_img_opt_custom_repeat').val('o_bg_img_opt_repeat_x'); - } else if (self.$target.hasClass('o_bg_img_opt_repeat_y')) { - self.modal.$('#o_bg_img_opt_custom_repeat').val('o_bg_img_opt_repeat_y'); - } - - if (self.bgPos.length > 1) { - self.bgPos = { - x: self.bgPos[0], - y: self.bgPos[1], - }; - 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.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._updatePosInformation(); - - var imgURL = /\(['"]?([^'"]+)['"]?\)/g.exec(self.$target.css('background-image')); - imgURL = (imgURL && imgURL[1]) || ''; - var $img = $('', {class: 'img img-fluid', src: imgURL}); - $img.on('load', function () { - self._bindImageEvents($img); - }); - $img.prependTo(self.modal.$('.o_bg_img_opt_object')); + backgroundType: function (previewMode, value, $opt) { + this.$target.toggleClass('o_bg_img_opt_repeat', value === 'repeat-pattern'); + this.$target.css('background-position', ''); + this.$target.css('background-size', ''); + }, + /** + * Saves current background position and enables overlay. + * + * @see this.selectClass for params + */ + backgroundPositionOverlay: function (previewMode, value, $opt) { + const position = this.$target.css('background-position').split(' ').map(v => parseInt(v)); + // Convert % values to pixels (because mouse movement is in pixels) + const delta = this._getBackgroundDelta(); + this.originalPosition = { + left: position[0] / 100 * delta.x || 0, + top: position[1] / 100 * delta.y || 0, + }; + this.currentPosition = _.clone(this.originalPosition); - // Bind events - self.modal.$el.on('change', '> label > input', function (e) { - self.modal.$('> .o_bg_img_opt').addClass('o_hidden') - .filter('[data-value=' + e.target.value + ']') - .removeClass('o_hidden'); - }); - self.modal.$el.on('change', 'input, select', function (e) { - self._saveChanges(); - }); - self.modal.$('> label > input:checked').trigger('change'); - }); + this._toggleBgOverlay(true); }, //-------------------------------------------------------------------------- @@ -1625,116 +1598,219 @@ registry['background_position'] = SnippetOption.extend({ //-------------------------------------------------------------------------- /** - * Bind events on the given image so that the users can adapt the focus - * point. + * Initializes the overlay, binds events to the buttons, inserts it in + * the DOM. * * @private - * @param {jQuery} $img */ - _bindImageEvents: function ($img) { - var self = this; + _initOverlay: function () { + this.$backgroundOverlay = $(qweb.render('web_editor.background_position_overlay')); + this.$overlayContent = this.$backgroundOverlay.find('.o_we_overlay_content'); + this.$overlayBackground = this.$overlayContent.find('.o_overlay_background'); - var mousedown = false; - $img.on('mousedown', function (e) { - mousedown = true; + this.$backgroundOverlay.on('click', '.o_btn_apply', () => { + this.$target.css('background-position', this.$bgDragger.css('background-position')); + this._toggleBgOverlay(false); }); - $img.on('mousemove', function (e) { - if (mousedown) { - _update(e); - } + this.$backgroundOverlay.on('click', '.o_btn_discard', () => { + this._toggleBgOverlay(false); }); - $img.on('mouseup', function (e) { - self.$focus.addClass('o_with_transition'); - _update(e); - setTimeout(function () { - self.$focus.removeClass('o_with_transition'); - }, 200); - mousedown = false; + + this.$backgroundOverlay.insertAfter(this.$overlay); + }, + /** + * Sets the overlay in the right place so that the draggable background + * renders over the target, and size the background item like the target. + * + * @private + */ + _dimensionOverlay: function () { + if (!this.$backgroundOverlay.is('.oe_active')) { + return; + } + // TODO: change #wrapwrap after web_editor rework. + const $wrapwrap = $('#wrapwrap'); + const targetOffset = this.$target.offset(); + + this.$backgroundOverlay.css({ + width: $wrapwrap.innerWidth(), + height: $wrapwrap.innerHeight(), }); - function _update(e) { - var posX = e.pageX - $(e.target).offset().left; - var posY = e.pageY - $(e.target).offset().top; - self.bgPos = { - x: clipValue(posX / $img.width() * 100).toFixed(2) + '%', - y: clipValue(posY / $img.height() * 100).toFixed(2) + '%', - }; - self._updatePosInformation(); - self._saveChanges(); + this.$overlayContent.offset(targetOffset); - function clipValue(value) { - return Math.max(0, Math.min(value, 100)); - } + this.$bgDragger.css({ + width: `${this.$target.innerWidth()}px`, + height: `${this.$target.innerHeight()}px`, + }); + }, + /** + * Toggles the overlay's display and renders a background clone inside of it. + * + * @private + * @param {boolean} activate toggle the overlay on (true) or off (false) + */ + _toggleBgOverlay: function (activate) { + if (this.$backgroundOverlay.is('.oe_active') === activate) { + return; } + + if (!activate) { + this.$backgroundOverlay.removeClass('oe_active'); + this.trigger_up('unblock_preview_overlays'); + this.trigger_up('activate_snippet', {$snippet: this.$target}); + + $(document).off('click.bgposition'); + return; + } + + this.trigger_up('hide_overlay'); + this.trigger_up('activate_snippet', { + $snippet: this.$target, + previewMode: true, + }); + this.trigger_up('block_preview_overlays'); + + // Create empty clone of $target with same display size, make it draggable and give it a tooltip. + this.$bgDragger = this.$target.clone().empty(); + this.$bgDragger.on('mousedown', this._onDragBackgroundStart.bind(this)); + this.$bgDragger.tooltip({ + title: 'Click and drag the background to adjust its position!', + trigger: 'manual', + container: this.$backgroundOverlay + }); + + // Replace content of overlayBackground, activate the overlay and give it the right dimensions. + this.$overlayBackground.empty().append(this.$bgDragger); + this.$backgroundOverlay.addClass('oe_active'); + this._dimensionOverlay(); + this.$bgDragger.tooltip('show'); + + // Needs to be deferred or the click event that activated the overlay deactivates it as well. + // This is caused by the click event which we are currently handling bubbling up to the document. + window.setTimeout(() => $(document).on('click.bgposition', this._onDocumentClicked.bind(this)), 0); + }, + /** + * Disables background position if no background image, disables size inputs + * in cover mode, and activates the proper select option. + * + * @override + */ + _setActive: function () { + this.$el.toggleClass('d-none', this.$target.css('background-image') === 'none'); + this.$el.find('we-input').toggleClass('d-none', this.$target.css('background-repeat') !== 'repeat'); + this.$el.find('[data-background-type]').removeClass('active') + .filter(`[data-background-type=${this.$target.css('background-repeat') === 'repeat' ? 'repeat-pattern' : 'cover'}]`).addClass('active'); + + this._super.apply(this, arguments); }, /** - * Removes all option-related classes and style on the target element. + * Returns the src value from a css value related to a background image + * (e.g. "url('blabla')" => "blabla" / "none" => ""). * * @private + * @param {string} value + * @returns {string} */ - _clean: function () { - this.$target.removeClass('o_bg_img_opt_contain o_bg_img_opt_custom o_bg_img_opt_repeat o_bg_img_opt_repeat_x o_bg_img_opt_repeat_y') - .css({ - 'background-size': '', - 'background-position': '', - }); + _getSrcFromCssValue: function (value) { + if (value === undefined) { + value = this.$target.css('background-image'); + } + var srcValueWrapper = /url\(['"]*|['"]*\)|^none$/g; + return value && value.replace(srcValueWrapper, '') || ''; }, /** - * Restores the target style before last edition made with the option. + * Returns the difference between the target's size and the background's + * rendered size. Background position values in % are a percentage of this. * * @private */ - _discardChanges: function () { - this._clean(); - if (this.previousState) { - this.$target.addClass(this.previousState[0]).css({ - 'background-size': this.previousState[1], - 'background-position': this.previousState[2], - }); + _getBackgroundDelta: function () { + const bgSize = this.$target.css('background-size'); + if (bgSize !== 'cover') { + let [width, height] = bgSize.split(' '); + if (width === 'auto' && (height === 'auto' || !height)) { + return { + x: this.$target.outerWidth() - this.img.naturalWidth, + y: this.$target.outerHeight() - this.img.naturalHeight, + }; + } + // At least one of width or height is not auto, so we can use it to calculate the other if it's not set + [width, height] = [parseInt(width), parseInt(height)]; + return { + x: this.$target.outerWidth() - (width || (height * this.img.naturalWidth / this.img.naturalHeight)), + y: this.$target.outerHeight() - (height || (width * this.img.naturalHeight / this.img.naturalWidth)), + }; } + + const renderRatio = Math.max( + this.$target.outerWidth() / this.img.naturalWidth, + this.$target.outerHeight() / this.img.naturalHeight + ); + + return { + x: this.$target.outerWidth() - Math.round(renderRatio * this.img.naturalWidth), + y: this.$target.outerHeight() - Math.round(renderRatio * this.img.naturalHeight), + }; }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + /** - * Updates the visual representation of the chosen background position. + * Drags the overlay's background image, copied to target on "Apply". * * @private */ - _updatePosInformation: function () { - 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.bgPos.x, - top: this.bgPos.y, + _onDragBackgroundStart: function (ev) { + ev.preventDefault(); + this.$bgDragger.addClass('o_we_grabbing'); + const $document = $(this.ownerDocument); + $document.on('mousemove.bgposition', this._onDragBackgroundMove.bind(this)); + $document.one('mouseup', () => { + this.$bgDragger.removeClass('o_we_grabbing'); + $document.off('mousemove.bgposition'); }); }, /** - * Updates the target element to match the chosen options. + * Drags the overlay's background image, copied to target on "Apply". * * @private */ - _saveChanges: function () { - this._clean(); + _onDragBackgroundMove: function (ev) { + ev.preventDefault(); - var bgImgSize = this.modal.$('> :not(label):not(.o_hidden)').data('value') || 'cover'; - switch (bgImgSize) { - case 'cover': - this.$target.css('background-position', this.bgPos.x + ' ' + this.bgPos.y); - break; - case 'contain': - this.$target.addClass('o_bg_img_opt_contain'); - this.$target.toggleClass('o_bg_img_opt_repeat', this.modal.$('#o_bg_img_opt_contain_repeat').prop('checked')); - break; - case 'custom': - this.$target.addClass('o_bg_img_opt_custom'); - var sizeX = this.modal.$('#o_bg_img_opt_custom_size_x').val(); - var sizeY = this.modal.$('#o_bg_img_opt_custom_size_y').val(); - var posX = this.modal.$('#o_bg_img_opt_custom_pos_x').val(); - var posY = this.modal.$('#o_bg_img_opt_custom_pos_y').val(); - this.$target.addClass(this.modal.$('#o_bg_img_opt_custom_repeat').val()) - .css({ - 'background-size': (sizeX ? sizeX + '%' : 'auto') + ' ' + (sizeY ? sizeY + '%' : 'auto'), - 'background-position': (posX ? posX + '%' : 'auto') + ' ' + (posY ? posY + '%' : 'auto'), - }); - break; + const delta = this._getBackgroundDelta(); + this.currentPosition.left = clamp(this.currentPosition.left + ev.originalEvent.movementX, [0, delta.x]); + this.currentPosition.top = clamp(this.currentPosition.top + ev.originalEvent.movementY, [0, delta.y]); + + const percentPosition = { + left: this.currentPosition.left / delta.x * 100, + top: this.currentPosition.top / delta.y * 100, + }; + // In cover mode, one delta will be 0 and dividing by it will yield Infinity. + // Defaulting to originalPosition in that case (can't be dragged) + percentPosition.left = isFinite(percentPosition.left) ? percentPosition.left : this.originalPosition.left; + percentPosition.top = isFinite(percentPosition.top) ? percentPosition.top : this.originalPosition.top; + + this.$bgDragger.css('background-position', `${percentPosition.left}% ${percentPosition.top}%`); + + function clamp(val, bounds) { + // We sort the bounds because when one dimension of the rendered background is + // larger than the container, delta is negative, and we want to use it as lower bound + bounds = bounds.sort(); + return Math.max(bounds[0], Math.min(val, bounds[1])); + } + }, + /** + * Deactivates the overlay if the user clicks outside of it. + * + * @private + */ + _onDocumentClicked: function (ev) { + if (!ev.target.closest('.o_we_background_position_overlay')) { + this._toggleBgOverlay(false); } }, }); -- cgit v1.2.3