summaryrefslogtreecommitdiff
path: root/addons/web_editor/static/src/js/editor
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web_editor/static/src/js/editor')
-rw-r--r--addons/web_editor/static/src/js/editor/editor.js2
-rw-r--r--addons/web_editor/static/src/js/editor/image_processing.js133
-rw-r--r--addons/web_editor/static/src/js/editor/rte.js4
-rw-r--r--addons/web_editor/static/src/js/editor/rte.summernote.js56
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.editor.js3
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.options.js507
6 files changed, 623 insertions, 82 deletions
diff --git a/addons/web_editor/static/src/js/editor/editor.js b/addons/web_editor/static/src/js/editor/editor.js
index 5cd8113e7f9..1d6f34aa4cf 100644
--- a/addons/web_editor/static/src/js/editor/editor.js
+++ b/addons/web_editor/static/src/js/editor/editor.js
@@ -171,7 +171,7 @@ var EditorMenuBar = Widget.extend({
if (this.snippetsMenu) {
await this.snippetsMenu.cleanForSave();
}
- await this.getParent().saveCroppedImages(this.rte.editable());
+ await this.getParent().saveModifiedImages(this.rte.editable());
await this.rte.save();
if (reload !== false) {
diff --git a/addons/web_editor/static/src/js/editor/image_processing.js b/addons/web_editor/static/src/js/editor/image_processing.js
new file mode 100644
index 00000000000..e775dfde421
--- /dev/null
+++ b/addons/web_editor/static/src/js/editor/image_processing.js
@@ -0,0 +1,133 @@
+odoo.define('web_editor.image_processing', function (require) {
+'use strict';
+
+// Fields returned by cropperjs 'getData' method, also need to be passed when
+// initializing the cropper to reuse the previous crop.
+const cropperDataFields = ['x', 'y', 'width', 'height', 'rotate', 'scaleX', 'scaleY'];
+const modifierFields = [
+ 'filter',
+ 'quality',
+ 'mimetype',
+ 'originalId',
+ 'originalSrc',
+ 'resizeWidth',
+ 'aspectRatio',
+];
+/**
+ * Applies data-attributes modifications to an img tag and returns a dataURL
+ * containing the result. This function does not modify the original image.
+ *
+ * @param {HTMLImageElement} img the image to which modifications are applied
+ * @returns {string} dataURL of the image with the applied modifications
+ */
+async function applyModifications(img) {
+ const data = Object.assign({
+ filter: '#0000',
+ quality: '95',
+ }, img.dataset);
+ let {width, height, resizeWidth, quality, filter, mimetype, originalSrc} = data;
+ [width, height, resizeWidth] = [width, height, resizeWidth].map(s => parseFloat(s));
+ quality = parseInt(quality);
+
+ // Crop
+ const container = document.createElement('div');
+ const original = await loadImage(originalSrc);
+ container.appendChild(original);
+ await activateCropper(original, 0, data);
+ const croppedImg = $(original).cropper('getCroppedCanvas', {width, height});
+ $(original).cropper('destroy');
+
+ // Width
+ const result = document.createElement('canvas');
+ result.width = resizeWidth || croppedImg.width;
+ result.height = croppedImg.height * result.width / croppedImg.width;
+ const ctx = result.getContext('2d');
+ ctx.drawImage(croppedImg, 0, 0, croppedImg.width, croppedImg.height, 0, 0, result.width, result.height);
+
+ // Color filter
+ ctx.fillStyle = filter || '#0000';
+ ctx.fillRect(0, 0, result.width, result.height);
+
+ // Quality
+ return result.toDataURL(mimetype, quality / 100);
+}
+
+/**
+ * Loads an src into an HTMLImageElement.
+ *
+ * @param {String} src URL of the image to load
+ * @param {HTMLImageElement} [img] img element in which to load the image
+ * @returns {Promise<HTMLImageElement>} Promise that resolves to the loaded img
+ */
+function loadImage(src, img = new Image()) {
+ return new Promise((resolve, reject) => {
+ img.addEventListener('load', () => resolve(img), {once: true});
+ img.addEventListener('error', reject, {once: true});
+ img.src = src;
+ });
+}
+
+// Because cropperjs acquires images through XHRs on the image src and we don't
+// want to load big images over the network many times when adjusting quality
+// and filter, we create a local cache of the images using object URLs.
+const imageCache = new Map();
+/**
+ * Activates the cropper on a given image.
+ *
+ * @param {jQuery} $image the image on which to activate the cropper
+ * @param {Number} aspectRatio the aspectRatio of the crop box
+ * @param {DOMStringMap} dataset dataset containing the cropperDataFields
+ */
+async function activateCropper(image, aspectRatio, dataset) {
+ const src = image.getAttribute('src');
+ if (!imageCache.has(src)) {
+ const res = await fetch(src);
+ imageCache.set(src, URL.createObjectURL(await res.blob()));
+ }
+ image.src = imageCache.get(src);
+ $(image).cropper({
+ viewMode: 2,
+ dragMode: 'move',
+ autoCropArea: 1.0,
+ aspectRatio: aspectRatio,
+ data: _.mapObject(_.pick(dataset, ...cropperDataFields), value => parseFloat(value)),
+ // Can't use 0 because it's falsy and cropperjs will then use its defaults (200x100)
+ minContainerWidth: 1,
+ minContainerHeight: 1,
+ });
+ return new Promise(resolve => image.addEventListener('ready', resolve, {once: true}));
+}
+/**
+ * Marks an <img> with its attachment data (originalId, originalSrc, mimetype)
+ *
+ * @param {HTMLImageElement} img the image whose attachment data should be found
+ * @param {Function} rpc a function that can be used to make the RPC. Typically
+ * this would be passed as 'this._rpc.bind(this)' from widgets.
+ */
+async function loadImageInfo(img, rpc) {
+ // If there is a marked originalSrc, the data is already loaded.
+ if (img.dataset.originalSrc) {
+ return;
+ }
+
+ const {original} = await rpc({
+ route: '/web_editor/get_image_info',
+ params: {src: img.getAttribute('src').split(/[?#]/)[0]},
+ });
+ // Check that url is local.
+ if (original && new URL(original.image_src, window.location.origin).origin === window.location.origin) {
+ img.dataset.originalId = original.id;
+ img.dataset.originalSrc = original.image_src;
+ img.dataset.mimetype = original.mimetype;
+ }
+}
+
+return {
+ applyModifications,
+ cropperDataFields,
+ activateCropper,
+ loadImageInfo,
+ loadImage,
+ removeOnImageChangeAttrs: [...cropperDataFields, ...modifierFields, 'aspectRatio'],
+};
+});
diff --git a/addons/web_editor/static/src/js/editor/rte.js b/addons/web_editor/static/src/js/editor/rte.js
index bf5c0db613d..71217563ca7 100644
--- a/addons/web_editor/static/src/js/editor/rte.js
+++ b/addons/web_editor/static/src/js/editor/rte.js
@@ -458,6 +458,10 @@ var RTEWidget = Widget.extend({
if (initialActiveElement && initialActiveElement !== document.activeElement) {
initialActiveElement.focus();
+ // Range inputs don't support selection
+ if (initialActiveElement.matches('input[type=range]')) {
+ return;
+ }
try {
initialActiveElement.selectionStart = initialSelectionStart;
initialActiveElement.selectionEnd = initialSelectionEnd;
diff --git a/addons/web_editor/static/src/js/editor/rte.summernote.js b/addons/web_editor/static/src/js/editor/rte.summernote.js
index 92d8cefe0bc..229acc0616c 100644
--- a/addons/web_editor/static/src/js/editor/rte.summernote.js
+++ b/addons/web_editor/static/src/js/editor/rte.summernote.js
@@ -12,7 +12,6 @@ const topBus = window.top.odoo.__DEBUG__.services['web.core'].bus;
const {ColorpickerWidget} = require('web.Colorpicker');
var ColorPaletteWidget = require('web_editor.ColorPalette').ColorPaletteWidget;
var mixins = require('web.mixins');
-const session = require('web.session');
var fonts = require('wysiwyg.fonts');
var rte = require('web_editor.rte');
var ServicesMixin = require('web.ServicesMixin');
@@ -1092,32 +1091,36 @@ var SummernoteManager = Class.extend(mixins.EventDispatcherMixin, ServicesMixin,
},
/**
- * Create/Update cropped attachments.
+ * Create modified image attachments.
*
* @param {jQuery} $editable
* @returns {Promise}
*/
- saveCroppedImages: function ($editable) {
- var defs = _.map($editable.find('.o_cropped_img_to_save'), async croppedImg => {
- croppedImg.classList.remove('o_cropped_img_to_save');
- // Cropping an image always creates a copy of the original, even if
- // it was cropped previously, as the other cropped image may be used
- // elsewhere if the snippet was duplicated or was saved as a custom one.
- const croppedAttachmentSrc = await this._rpc({
- route: '/web_editor/crop_attachment',
- params: {
- res_model: croppedImg.dataset.resModel,
- res_id: parseInt(croppedImg.dataset.resId),
- name: croppedImg.dataset.originalName + '.crop',
- data: croppedImg.getAttribute('src').split(',')[1],
- mimetype: croppedImg.dataset.mimetype,
- original_id: parseInt(croppedImg.dataset.originalId),
- },
- });
- croppedImg.setAttribute('src', croppedAttachmentSrc);
- weWidgets.ImageCropWidget.prototype.removeOnSaveAttributes.forEach(attr => {
- delete croppedImg.dataset[attr];
+ saveModifiedImages: function ($editable) {
+ const defs = _.map($editable, async editableEl => {
+ const {oeModel: resModel, oeId: resId} = editableEl.dataset;
+ const proms = [...editableEl.querySelectorAll('.o_modified_image_to_save')].map(async el => {
+ const isBackground = !el.matches('img');
+ el.classList.remove('o_modified_image_to_save');
+ // Modifying an image always creates a copy of the original, even if
+ // it was modified previously, as the other modified image may be used
+ // elsewhere if the snippet was duplicated or was saved as a custom one.
+ const newAttachmentSrc = await this._rpc({
+ route: `/web_editor/modify_image/${el.dataset.originalId}`,
+ params: {
+ res_model: resModel,
+ res_id: parseInt(resId),
+ data: (isBackground ? el.dataset.bgSrc : el.getAttribute('src')).split(',')[1],
+ },
+ });
+ if (isBackground) {
+ $(el).css('background-image', `url('${newAttachmentSrc}')`);
+ delete el.dataset.bgSrc;
+ } else {
+ el.setAttribute('src', newAttachmentSrc);
+ }
});
+ return Promise.all(proms);
});
return Promise.all(defs);
},
@@ -1160,13 +1163,8 @@ var SummernoteManager = Class.extend(mixins.EventDispatcherMixin, ServicesMixin,
return;
}
data.__alreadyDone = true;
- new weWidgets.ImageCropWidget(this,
- _.extend({
- res_model: data.$editable.data('oe-model'),
- res_id: data.$editable.data('oe-id'),
- }, data.options || {}),
- data.media
- ).appendTo(data.$editable);
+ new weWidgets.ImageCropWidget(this, data.media)
+ .appendTo(data.$editable);
},
/**
* Called when a demand to open a link dialog is received on the bus.
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 06263b23182..20c73e39e99 100644
--- a/addons/web_editor/static/src/js/editor/snippets.editor.js
+++ b/addons/web_editor/static/src/js/editor/snippets.editor.js
@@ -219,6 +219,9 @@ var SnippetEditor = Widget.extend({
if (this.$target.data('name') !== undefined) {
return this.$target.data('name');
}
+ if (this.$target.is('img')) {
+ return _t("Image");
+ }
if (this.$target.parent('.row').length) {
return _t("Column");
}
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 1c603eff10c..ae40fd76a63 100644
--- a/addons/web_editor/static/src/js/editor/snippets.options.js
+++ b/addons/web_editor/static/src/js/editor/snippets.options.js
@@ -9,6 +9,12 @@ var Widget = require('web.Widget');
var ColorPaletteWidget = require('web_editor.ColorPalette').ColorPaletteWidget;
const weUtils = require('web_editor.utils');
var weWidgets = require('wysiwyg.widgets');
+const {
+ loadImage,
+ loadImageInfo,
+ applyModifications,
+ removeOnImageChangeAttrs,
+} = require('web_editor.image_processing');
var qweb = core.qweb;
var _t = core._t;
@@ -1424,6 +1430,49 @@ const DatetimePickerUserValueWidget = InputUserValueWidget.extend({
},
});
+const RangeUserValueWidget = UserValueWidget.extend({
+ tagName: 'we-range',
+ events: {
+ 'change input': '_onInputChange',
+ },
+
+ /**
+ * @override
+ */
+ async start() {
+ await this._super(...arguments);
+ this.input = document.createElement('input');
+ this.input.type = "range";
+ this.input.className = "custom-range";
+ this.containerEl.appendChild(this.input);
+ },
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ setValue(value, methodName) {
+ this.input.value = value;
+ return this._super(...arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ _onInputChange(ev) {
+ this._value = ev.target.value;
+ this._onUserValueChange(ev);
+ },
+});
+
+
const userValueWidgetsRegistry = {
'we-button': ButtonUserValueWidget,
'we-checkbox': CheckboxUserValueWidget,
@@ -1433,6 +1482,7 @@ const userValueWidgetsRegistry = {
'we-colorpicker': ColorpickerUserValueWidget,
'we-datetimepicker': DatetimePickerUserValueWidget,
'we-imagepicker': ImagepickerUserValueWidget,
+ 'we-range': RangeUserValueWidget,
};
/**
@@ -2662,6 +2712,385 @@ registry['sizing_y'] = registry.sizing.extend({
},
});
+/*
+ * Abstract option to be extended by the ImageOptimize and BackgroundOptimize
+ * options that handles all the common parts.
+ */
+const ImageHandlerOption = SnippetOptionWidget.extend({
+
+ /**
+ * @override
+ */
+ async willStart() {
+ const _super = this._super.bind(this);
+ await this._loadImageInfo();
+ // Make sure image is loaded because we need its naturalWidth to render XML
+ const img = this._getImg();
+ await new Promise((resolve, reject) => {
+ if (img.complete) {
+ return resolve();
+ }
+ img.addEventListener('load', resolve, {once: true});
+ img.addEventListener('error', resolve, {once: true});
+ });
+ return _super(...arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Options
+ //--------------------------------------------------------------------------
+
+ /**
+ * @see this.selectClass for parameters
+ */
+ selectWidth(previewMode, widgetValue, params) {
+ this._getImg().dataset.resizeWidth = widgetValue;
+ return this._applyOptions();
+ },
+ /**
+ * @see this.selectClass for parameters
+ */
+ setFilter(previewMode, widgetValue, params) {
+ this._getImg().dataset.filter = this._normalizeColor(widgetValue);
+ return this._applyOptions();
+ },
+ /**
+ * @see this.selectClass for parameters
+ */
+ setQuality(previewMode, widgetValue, params) {
+ this._getImg().dataset.quality = widgetValue;
+ return this._applyOptions();
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ _computeVisibility() {
+ const src = this._getImg().getAttribute('src');
+ return src && src !== '/';
+ },
+ /**
+ * @override
+ */
+ _computeWidgetState(methodName, params) {
+ const img = this._getImg();
+ switch (methodName) {
+ case 'selectWidth':
+ return img.naturalWidth;
+ case 'setFilter':
+ return img.dataset.filter;
+ case 'setQuality':
+ return img.dataset.quality || 95;
+ }
+ return this._super(...arguments);
+ },
+ /**
+ * @override
+ */
+ async _renderCustomXML(uiFragment) {
+ if (!this.originalSrc) {
+ return [...uiFragment.childNodes].forEach(node => {
+ if (node.matches('.o_we_external_warning')) {
+ node.classList.remove('d-none');
+ } else {
+ node.remove();
+ }
+ });
+ }
+ const img = this._getImg();
+ const $select = $(uiFragment).find('we-select[data-name=width_select_opt]');
+ (await this._computeAvailableWidths()).forEach(([value, label]) => {
+ $select.append(`<we-button data-select-width="${value}">${label}</we-button>`);
+ });
+ const qualityRange = uiFragment.querySelector('we-range');
+ if (img.dataset.mimetype !== 'image/jpeg') {
+ qualityRange.remove();
+ }
+ },
+ /**
+ * Returns a list of valid widths for a given image.
+ *
+ * @private
+ */
+ async _computeAvailableWidths() {
+ const img = this._getImg();
+ const original = await loadImage(this.originalSrc);
+ const maxWidth = img.dataset.width ? img.naturalWidth : original.naturalWidth;
+ const optimizedWidth = Math.min(maxWidth, this._computeMaxDisplayWidth());
+ this.optimizedWidth = optimizedWidth;
+ const widths = {
+ 128: '128px',
+ 256: '256px',
+ 512: '512px',
+ 1024: '1024px',
+ 1920: '1920px',
+ };
+ widths[img.naturalWidth] = _.str.sprintf(_t("%spx"), img.naturalWidth);
+ widths[optimizedWidth] = _.str.sprintf(_t("%dpx (Suggested)"), optimizedWidth);
+ widths[maxWidth] = _.str.sprintf(_t("%dpx (Original)"), maxWidth);
+ return Object.entries(widths)
+ .filter(([width]) => width <= maxWidth)
+ .sort(([v1], [v2]) => v1 - v2);
+ },
+ /**
+ * Applies all selected options on the original image.
+ *
+ * @private
+ */
+ async _applyOptions() {
+ const img = this._getImg();
+ if (!['image/jpeg', 'image/png'].includes(img.dataset.mimetype)) {
+ this.originalId = null;
+ return;
+ }
+ const dataURL = await applyModifications(img);
+ const weight = dataURL.split(',')[1].length / 4 * 3;
+ this.$el.find('.o_we_image_weight').text(`${(weight / 1024).toFixed(1)}kb`);
+ img.classList.add('o_modified_image_to_save');
+ return loadImage(dataURL, img);
+ },
+ /**
+ * Loads the image's attachment info.
+ *
+ * @private
+ */
+ async _loadImageInfo() {
+ const img = this._getImg();
+ await loadImageInfo(img, this._rpc.bind(this));
+ if (!img.dataset.originalId) {
+ this.originalId = null;
+ this.originalSrc = null;
+ return;
+ }
+ this.originalId = img.dataset.originalId;
+ this.originalSrc = img.dataset.originalSrc;
+ },
+ /**
+ * Returns the image that is currently being modified.
+ *
+ * @private
+ * @abstract
+ * @returns {HTMLImageElement} the image to use for modifications
+ */
+ _getImg() {},
+ /**
+ * Computes the image's maximum display width.
+ *
+ * @private
+ * @abstract
+ * @returns {Int} the maximum width at which the image can be displayed
+ */
+ _computeMaxDisplayWidth() {},
+
+ //--------------------------------------------------------------------------
+ // Util
+ //--------------------------------------------------------------------------
+
+ /**
+ * Normalize a color into a css value usable by the canvas rendering context.
+ *
+ * @private
+ * @param {string} color the color to normalize into a css value
+ */
+ _normalizeColor(color) {
+ if (!ColorpickerWidget.isCSSColor(color)) {
+ const style = window.getComputedStyle(document.documentElement);
+ color = style.getPropertyValue('--' + color).trim();
+ color = ColorpickerWidget.normalizeCSSColor(color);
+ }
+ return color;
+ },
+});
+
+/**
+ * Controls image width and quality.
+ */
+registry.ImageOptimize = ImageHandlerOption.extend({
+ /**
+ * @override
+ */
+ start() {
+ this.$target.on('image_changed.ImageOptimization', this._onImageChanged.bind(this));
+ this.$target.on('image_cropped.ImageOptimization', this._onImageCropped.bind(this));
+ return this._super(...arguments);
+ },
+ /**
+ * @override
+ */
+ destroy() {
+ this.$target.off('.ImageOptimization');
+ return this._super(...arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ _computeMaxDisplayWidth() {
+ // TODO: read widths from computed style in case container widths are not default
+ const displayWidth = this._getImg().clientWidth;
+ // If the image is in a column, it might get bigger on smaller screens.
+ // We use col-lg for this in snippets, so they get bigger on the md breakpoint
+ if (this.$target.closest('[class*="col-lg"]').length) {
+ // container and o_container_small have maximum inner width of 690px on the md breakpoint
+ if (this.$target.closest('.container, .o_container_small').length) {
+ return Math.min(1920, Math.max(displayWidth, 690));
+ }
+ // A container-fluid's max inner width is 962px on the md breakpoint
+ return Math.min(1920, Math.max(displayWidth, 962));
+ }
+ // If it's not in a col-lg, it's probably not going to change size depending on breakpoints
+ return displayWidth;
+ },
+ /**
+ * @override
+ */
+ _getImg() {
+ return this.$target[0];
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Reloads image data and auto-optimizes the new image.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ async _onImageChanged(ev) {
+ this.trigger_up('snippet_edition_request', {exec: async () => {
+ await this._loadImageInfo();
+ await this._rerenderXML();
+ this._getImg().dataset.resizeWidth = this.optimizedWidth;
+ await this._applyOptions();
+ await this.updateUI();
+ }});
+ },
+ /**
+ * Available widths will change, need to rerender the width select.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ async _onImageCropped(ev) {
+ await this._rerenderXML();
+ },
+});
+
+/**
+ * Returns the src value from a css value related to a background image
+ * (e.g. "url('blabla')" => "blabla" / "none" => "").
+ *
+ * @param {string} value
+ * @returns {string}
+ */
+const getSrcFromCssValue = value => {
+ var srcValueWrapper = /url\(['"]*|['"]*\)|^none$/g;
+ return value && value.replace(srcValueWrapper, '') || '';
+};
+
+/**
+ * Controls background image width and quality.
+ */
+registry.BackgroundOptimize = ImageHandlerOption.extend({
+ /**
+ * @override
+ */
+ start() {
+ this.$target.on('background_changed.BackgroundOptimize', this._onBackgroundChanged.bind(this));
+ return this._super(...arguments);
+ },
+ /**
+ * @override
+ */
+ destroy() {
+ this.$target.off('.BackgroundOptimize');
+ return this._super(...arguments);
+ },
+ /**
+ * Marks the target for creation of an attachment and copies data attributes
+ * to the target so that they can be restored on this.img in later editions.
+ *
+ * @override
+ */
+ async cleanForSave() {
+ const img = this._getImg();
+ if (img.matches('.o_modified_image_to_save')) {
+ this.$target.addClass('o_modified_image_to_save');
+ Object.entries(img.dataset).forEach(([key, value]) => {
+ this.$target[0].dataset[key] = value;
+ });
+ this.$target[0].dataset.bgSrc = img.getAttribute('src');
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ _getImg() {
+ return this.img;
+ },
+ /**
+ * @override
+ */
+ _computeMaxDisplayWidth() {
+ return 1920;
+ },
+ /**
+ * @override
+ */
+ async _applyOptions() {
+ await this._super(...arguments);
+ this.$target.css('background-image', `url('${this._getImg().getAttribute('src')}')`);
+ },
+ /**
+ * Initializes this.img to an image with the background image url as src.
+ *
+ * @override
+ */
+ async _loadImageInfo() {
+ this.img = new Image();
+ Object.entries(this.$target[0].dataset).forEach(([key, value]) => {
+ this.img.dataset[key] = value;
+ });
+ const src = new URL(getSrcFromCssValue(this.$target.css('background-image')), window.location.origin);
+ // Make URL relative because that is how image urls are stored in the database.
+ this.img.src = src.origin === window.location.origin ? src.pathname : src;
+ return await this._super(...arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Reloads image data when the background is changed.
+ *
+ * @private
+ */
+ async _onBackgroundChanged(ev, previewMode) {
+ if (!previewMode) {
+ this.trigger_up('snippet_edition_request', {exec: async () => {
+ await this._loadImageInfo();
+ await this._rerenderXML();
+ }});
+ }
+ },
+});
+
/**
* Handles the edition of snippet's background image.
*/
@@ -2687,12 +3116,14 @@ registry.background = SnippetOptionWidget.extend({
* @see this.selectClass for parameters
*/
background: async function (previewMode, widgetValue, params) {
- if (previewMode === 'reset') {
- return this._setCustomBackground(this.__customImageSrc, previewMode);
- }
- if (!previewMode) {
+ if (previewMode === true) {
+ this.__customImageSrc = this._getSrcFromCssValue();
+ } else if (previewMode === 'reset') {
+ widgetValue = this.__customImageSrc;
+ } else {
this.__customImageSrc = widgetValue;
}
+
if (widgetValue) {
this.$target.css('background-image', `url('${widgetValue}')`);
this.$target.addClass('oe_img_bg');
@@ -2700,6 +3131,20 @@ registry.background = SnippetOptionWidget.extend({
this.$target.css('background-image', '');
this.$target.removeClass('oe_img_bg');
}
+
+ if (previewMode === 'reset') {
+ return new Promise(resolve => {
+ // Will update the UI of the correct widgets for all options
+ // related to the same $target/editor
+ this.trigger_up('snippet_option_update', {
+ previewMode: 'reset',
+ onSuccess: () => resolve(),
+ });
+ });
+ } else {
+ removeOnImageChangeAttrs.forEach(attr => delete this.$target[0].dataset[attr]);
+ this.$target.trigger('background_changed', [previewMode]);
+ }
},
//--------------------------------------------------------------------------
@@ -2732,38 +3177,13 @@ registry.background = SnippetOptionWidget.extend({
//--------------------------------------------------------------------------
/**
- * Returns the src value from a css value related to a background image
- * (e.g. "url('blabla')" => "blabla" / "none" => "").
+ * Returns the background image's src.
*
* @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, '') || '';
- },
- /**
- * Sets the given value as custom background image.
- *
- * @private
- * @param {string} value
- * @returns {Promise}
- */
- _setCustomBackground: async function (value, previewMode) {
- this.__customImageSrc = value;
- this.background(false, this.__customImageSrc, {});
- await new Promise(resolve => {
- // Will update the UI of the correct widgets for all options
- // related to the same $target/editor
- this.trigger_up('snippet_option_update', {
- previewMode: previewMode,
- onSuccess: () => resolve(),
- });
- });
+ _getSrcFromCssValue: function () {
+ return getSrcFromCssValue(this.$target.css('background-image'));
},
/**
* @override
@@ -2801,17 +3221,6 @@ registry.background = SnippetOptionWidget.extend({
await this.background(previewMode, '', {});
return true;
},
- /**
- * Called on media dialog save (when choosing a snippet's background) ->
- * sets the resulting media as the snippet's background somehow.
- *
- * @private
- * @param {Object} data
- * @returns {Promise}
- */
- _onSaveMediaDialog: async function (data) {
- await this._setCustomBackground($(data).attr('src'));
- },
});
/**
@@ -3009,19 +3418,13 @@ registry.BackgroundPosition = SnippetOptionWidget.extend({
window.setTimeout(() => $(document).on('click.bgposition', this._onDocumentClicked.bind(this)), 0);
},
/**
- * Returns the src value from a css value related to a background image
- * (e.g. "url('blabla')" => "blabla" / "none" => "").
+ * Returns the background image's src.
*
* @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, '') || '';
+ _getSrcFromCssValue: function () {
+ return getSrcFromCssValue(this.$target.css('background-image'));
},
/**
* Returns the difference between the target's size and the background's