diff options
Diffstat (limited to 'addons/web_editor/static/src/js/editor')
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 |
