From aa8d2a8be2c8f713b4b39b2aa1ea2aaf1839396c Mon Sep 17 00:00:00 2001 From: Samuel Degueldre Date: Tue, 19 May 2020 09:21:49 +0000 Subject: [IMP] web_editor, *: move image optimization feature to left-panel *: website, mass_mailing In continuity with the nline crop, in an effort to make the image workflow as seamless as possible in the website editor, the image optimization feature has been moved out of the modal which could be opened through the media-dialog and into the left panel. This commit adds a quality slider and a width selector, as well as a preview of the image's weight to the left panel, and removes the image_optimize dialog. This commit also introduces the possibility to add a color filter on images. Part of https://github.com/odoo/odoo/pull/51517 task-2192755 closes odoo/odoo#51517 Signed-off-by: Quentin Smetz (qsm) --- addons/web_editor/static/src/js/editor/editor.js | 2 +- .../static/src/js/editor/image_processing.js | 133 ++++++ addons/web_editor/static/src/js/editor/rte.js | 4 + .../static/src/js/editor/rte.summernote.js | 56 ++- .../static/src/js/editor/snippets.editor.js | 3 + .../static/src/js/editor/snippets.options.js | 507 ++++++++++++++++++--- 6 files changed, 623 insertions(+), 82 deletions(-) create mode 100644 addons/web_editor/static/src/js/editor/image_processing.js (limited to 'addons/web_editor/static/src/js/editor') 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} 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 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(`${label}`); + }); + 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 -- cgit v1.2.3