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; + 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); - 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')); - - // 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); + }); + + 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(), + }); + + this.$overlayContent.offset(targetOffset); + + 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); + }, + /** + * Returns the src value from a css value related to a background image + * (e.g. "url('blabla')" => "blabla" / "none" => ""). + * + * @private + * @param {string} value + * @returns {string} + */ + _getSrcFromCssValue: function (value) { + if (value === undefined) { + value = this.$target.css('background-image'); + } + var srcValueWrapper = /url\(['"]*|['"]*\)|^none$/g; + return value && value.replace(srcValueWrapper, '') || ''; + }, + /** + * Returns the difference between the target's size and the background's + * rendered size. Background position values in % are a percentage of this. + * + * @private + */ + _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, + }; } - }); - $img.on('mouseup', function (e) { - self.$focus.addClass('o_with_transition'); - _update(e); - setTimeout(function () { - self.$focus.removeClass('o_with_transition'); - }, 200); - mousedown = false; - }); - - function _update(e) { - var posX = e.pageX - $(e.target).offset().left; - var posY = e.pageY - $(e.target).offset().top; - self.bgPos = { - x: clipValue(posX / $img.width() * 100).toFixed(2) + '%', - y: clipValue(posY / $img.height() * 100).toFixed(2) + '%', + // 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)), }; - self._updatePosInformation(); - self._saveChanges(); + } - function clipValue(value) { - return Math.max(0, Math.min(value, 100)); - } - } + 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 + //-------------------------------------------------------------------------- + /** - * Removes all option-related classes and style on the target element. + * Drags the overlay's background image, copied to target on "Apply". * * @private */ - _clean: function () { - this.$target.removeClass('o_bg_img_opt_contain o_bg_img_opt_custom o_bg_img_opt_repeat o_bg_img_opt_repeat_x o_bg_img_opt_repeat_y') - .css({ - 'background-size': '', - 'background-position': '', - }); - }, - /** - * Restores the target style before last edition made with the option. - * - * @private - */ - _discardChanges: function () { - this._clean(); - if (this.previousState) { - this.$target.addClass(this.previousState[0]).css({ - 'background-size': this.previousState[1], - 'background-position': this.previousState[2], - }); - } - }, - /** - * Updates the visual representation of the chosen background position. - * - * @private - */ - _updatePosInformation: function () { - this.modal.$('.o_bg_img_opt_ui_info .o_x').text(this.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); } }, }); diff --git a/addons/web_editor/static/src/scss/wysiwyg_snippets.scss b/addons/web_editor/static/src/scss/wysiwyg_snippets.scss index 5ddba517474..98c3d56b22e 100644 --- a/addons/web_editor/static/src/scss/wysiwyg_snippets.scss +++ b/addons/web_editor/static/src/scss/wysiwyg_snippets.scss @@ -836,6 +836,29 @@ body.editor_enable.editor_has_snippets { } } } + + // Background position overlay + &.o_we_background_position_overlay { + background-color: rgba(0,0,0,.7); + z-index: auto; + + .o_we_overlay_content { + cursor: grab; + + .o_we_grabbing { + cursor: grabbing; + } + } + + .o_overlay_background > * { + display: block !important; + top: 0 !important; + right: 0 !important; + bottom: 0 !important; + left: 0 !important; + transform: none !important; + } + } } .contact_menu { diff --git a/addons/web_editor/static/src/xml/editor.xml b/addons/web_editor/static/src/xml/editor.xml index ca653c5d7ea..b1d7128e2c1 100644 --- a/addons/web_editor/static/src/xml/editor.xml +++ b/addons/web_editor/static/src/xml/editor.xml @@ -13,102 +13,16 @@ - -
- - - - -
-
-
Assign a focal point that will always be visible
-
- - - - - -
X: Y:
+ + +
+
+
+
+ +
-
-
-
- -
-
- -
-
-
-
-
-
-
- Background size -

Sets the width and height of the background image in percent of the parent element.

-
- -
-
- -
-
%
-
-
-
-
-
- -
-
- -
-
%
-
-
-
-
-
-
- Background position -

Set the starting position of the background image.

-
- -
-
- -
-
%
-
-
-
-
-
- -
-
- -
-
%
-
-
-
-
-
-
- Background repeat -

Define if/how the background image will be repeated

- -
-
-
+
diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js index c1af7852fd2..bf7960cc13d 100644 --- a/addons/website/static/src/js/editor/snippets.options.js +++ b/addons/website/static/src/js/editor/snippets.options.js @@ -674,7 +674,7 @@ options.registry.parallax = options.Class.extend({ */ onFocus: function () { this.trigger_up('option_update', { - optionNames: ['background', 'background_position'], + optionNames: ['background', 'BackgroundPosition'], name: 'target', data: this.$target.find('> .s_parallax_bg'), }); diff --git a/addons/website/static/src/scss/website.scss b/addons/website/static/src/scss/website.scss index 850f180f903..6e29659612c 100644 --- a/addons/website/static/src/scss/website.scss +++ b/addons/website/static/src/scss/website.scss @@ -239,6 +239,14 @@ font[class*='bg-'] { .oe_img_bg { background-size: cover; background-repeat: no-repeat; + + &.o_bg_img_opt_repeat { + background-size: auto; + background-repeat: repeat; + } + + // Compatibility <= 13.0, TODO remove? + // ----------------------------------- &.o_bg_img_opt_contain { background-size: contain; background-position: center center; @@ -246,9 +254,6 @@ font[class*='bg-'] { &.o_bg_img_opt_custom { background-size: auto; } - &.o_bg_img_opt_repeat { - background-repeat: repeat; - } &.o_bg_img_opt_repeat_x { background-repeat: repeat-x; } diff --git a/addons/website/static/src/scss/website.wysiwyg.scss b/addons/website/static/src/scss/website.wysiwyg.scss index 3206e4b027a..4e6fa2bc0c9 100644 --- a/addons/website/static/src/scss/website.wysiwyg.scss +++ b/addons/website/static/src/scss/website.wysiwyg.scss @@ -159,161 +159,3 @@ html[lang] > body.editor_enable [data-oe-translation-state] { opacity: 0; } } - -// MODALS -body .modal { - - // BACKGROUND IMAGE OPTIONS - .o_bg_img_opt_modal .o_bg_img_opt { - margin: 15px 0; - - .help-control { - @include o-position-absolute(15px, 15px); - } - - .help { - margin: 15px 0; - - .simulator { - position: relative; - float: left; - margin-right: 15px; - width: 120px; - height: 100px; - - .bg { - @include o-position-absolute(0, 0, 0, 0); - border: 1px dotted #888787; - background-image: url(/web/image); - background-position: center center; - background-size: cover; - } - - .el { - @include o-position-absolute(10px, 0, 10px, 0); - border: 1px solid #222; - } - - &.contain_bg { - .bg { - @include o-position-absolute(0, 12px, 20px, 12px); - } - - .el { - @include o-position-absolute(0, 0, 20px, 0); - } - } - } - - +* { - clear: left; - } - } - - .o_bg_img_opt_cover_edition { - @include o-we-preview-box; - padding: 0 25px 25px 25px; - - h6 { - color: white; - font-weight: bold; - } - - .o_bg_img_opt_object { - position: relative; - background: white; - min-height: 10px; - @include o-we-preview-content; - - >img { - cursor: crosshair; - border-top: 1px solid #5A5A5A; - border-bottom: 1px solid #111; - } - - &:hover .o_focus_point:before { - opacity: 0.5; - } - } - - .o_bg_img_opt_ui_info { - animation: fadeInOut 2s ease forwards; - @include o-position-absolute($bottom: 1px, $right: 0); - display: block; - padding: 3px 13px; - background-color: rgba(255, 255, 255, 0.8); - color: #333; - text-align: center; - font-weight: bold; - pointer-events: none; - - span { - font-weight: normal; - } - - .o_x { - margin-right: 10px - } - } - - .grid { - @include o-position-absolute($top: 33.33%, $left: 0); - display: block; - width: 100%; - height: 1px; - background: fade-out(lighten($o-brand-primary, 30%), 0.5); - pointer-events: none; - - &.grid-2 { - top: 66.66% - } - - &.grid-3 { - top: 0; - left: 33.33%; - width: 1px; - height: 100% - } - - &.grid-4 { - top: 0; - left: 66.66%; - width: 1px; - height: 100% - } - } - - .o_focus_point { - @include o-position-absolute($top: 0, $left: 0); - width: 30px; - height: 30px; - border: 2px solid white; - border-radius: 100%; - margin-top: -15px; - margin-left: -15px; - box-shadow: 0 0 1px #333; - pointer-events: none; - - &.o_with_transition { - transition: all 0.2s ease 0s; - } - - &:before { - pointer-events: none; - content: ""; - display: block; - width: 100px; - height: 100px; - margin-top: -37px; - margin-left: -37px; - border: 1px solid #EAEAEA; - background: rgba(247, 76, 76, 0); - border-radius: 100em; - opacity: 1; - box-shadow: 0 0 0 100em rgba(0, 0, 0, 0.33); - transition: opacity 0.2s ease 0s; - } - } - } - } -} diff --git a/addons/website/views/snippets.xml b/addons/website/views/snippets.xml index 16b218b6366..103dd62cc4a 100644 --- a/addons/website/views/snippets.xml +++ b/addons/website/views/snippets.xml @@ -1226,11 +1226,20 @@ data-selector="section, .parallax, .carousel-item" data-exclude=".s_hr, .s_image_gallery, .o_gallery .carousel-item"/> - -
+
- Background Image Sizing + + + Cover + Repeat pattern + + + + +