diff options
Diffstat (limited to 'addons/web_editor/static/src/js/editor/snippets.options.js')
| -rw-r--r-- | addons/web_editor/static/src/js/editor/snippets.options.js | 507 |
1 files changed, 455 insertions, 52 deletions
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 |
