diff options
Diffstat (limited to 'addons/web_editor/static/src/js')
4 files changed, 320 insertions, 16 deletions
diff --git a/addons/web_editor/static/src/js/common/utils.js b/addons/web_editor/static/src/js/common/utils.js index c66bb75a47c..6e92bd6955b 100644 --- a/addons/web_editor/static/src/js/common/utils.js +++ b/addons/web_editor/static/src/js/common/utils.js @@ -32,6 +32,18 @@ const CSS_UNITS_CONVERSION = { 'rem-px': () => _computePxByRem(), 'px-rem': () => _computePxByRem(true), }; +/** + * Colors of the default palette, used for substitution in shapes/illustrations. + * key: number of the color in the palette (ie, o-color-<1-5>) + * value: color hex code + */ +const DEFAULT_PALETTE = { + '1': '#3AADAA', + '2': '#7C6576', + '3': '#F6F6F6', + '4': '#FFFFFF', + '5': '#383E45', +}; /** * Computes the number of "px" needed to make a "rem" unit. Subsequent calls @@ -218,6 +230,7 @@ function _getBgImageURL(el) { return { CSS_SHORTHANDS: CSS_SHORTHANDS, CSS_UNITS_CONVERSION: CSS_UNITS_CONVERSION, + DEFAULT_PALETTE: DEFAULT_PALETTE, computePxByRem: _computePxByRem, convertValueToUnit: _convertValueToUnit, convertNumericToUnit: _convertNumericToUnit, 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 4254afe270f..37eda9910d1 100644 --- a/addons/web_editor/static/src/js/editor/snippets.editor.js +++ b/addons/web_editor/static/src/js/editor/snippets.editor.js @@ -10,6 +10,7 @@ var options = require('web_editor.snippets.options'); var Wysiwyg = require('web_editor.wysiwyg'); const {ColorPaletteWidget} = require('web_editor.ColorPalette'); const SmoothScrollOnDrag = require('web/static/src/js/core/smooth_scroll_on_drag.js'); +const {getCSSVariableValue} = require('web_editor.utils'); var _t = core._t; @@ -1853,6 +1854,12 @@ var SnippetsMenu = Widget.extend({ } $toInsert = $baseBody.clone(); + // Color-customize dynamic SVGs in dropped snippets with current theme colors. + [...$toInsert.find('img[src^="/web_editor/shape/"]')].forEach(dynamicSvg => { + const colorCustomizedURL = new URL(dynamicSvg.getAttribute('src'), window.location.origin); + colorCustomizedURL.searchParams.set('c1', getCSSVariableValue('o-color-1')); + dynamicSvg.src = colorCustomizedURL.pathname + colorCustomizedURL.search; + }); if (!$selectorSiblings.length && !$selectorChildren.length) { console.warn($snippet.find('.oe_snippet_thumbnail_title').text() + " have not insert action: data-drop-near or data-drop-in"); 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 8e236bb93d1..1a0e8d34935 100644 --- a/addons/web_editor/static/src/js/editor/snippets.options.js +++ b/addons/web_editor/static/src/js/editor/snippets.options.js @@ -3578,6 +3578,30 @@ registry.BackgroundImage = SnippetOptionWidget.extend({ this.$target.trigger('background_changed', [previewMode]); } }, + /** + * Changes the main color of dynamic SVGs. + * + * @see this.selectClass for parameters + */ + async dynamicColor(previewMode, widgetValue, params) { + const currentSrc = getBgImageURL(this.$target[0]); + switch (previewMode) { + case true: + this.previousSrc = currentSrc; + break; + case 'reset': + this.$target.css('background-image', `url('${this.previousSrc}')`); + return; + } + const newURL = new URL(currentSrc, window.location.origin); + newURL.searchParams.set('c1', normalizeColor(widgetValue)); + const src = newURL.pathname + newURL.search; + await loadImage(src); + this.$target.css('background-image', `url('${src}')`); + if (!previewMode) { + this.previousSrc = src; + } + }, //-------------------------------------------------------------------------- // Public @@ -3608,8 +3632,21 @@ registry.BackgroundImage = SnippetOptionWidget.extend({ * @override */ _computeWidgetState: function (methodName) { - if (methodName === 'background') { - return getBgImageURL(this.$target[0]); + switch (methodName) { + case 'background': + return getBgImageURL(this.$target[0]); + case 'dynamicColor': + return new URL(getBgImageURL(this.$target[0]), window.location.origin).searchParams.get('c1'); + } + return this._super(...arguments); + }, + /** + * @override + */ + _computeWidgetVisibility(widgetName, params) { + if (widgetName === 'dynamic_color_opt') { + const src = new URL(getBgImageURL(this.$target[0]), window.location.origin); + return src.origin === window.location.origin && src.pathname.startsWith('/web_editor/shape/'); } return this._super(...arguments); }, @@ -4505,6 +4542,86 @@ registry.SnippetSave = SnippetOptionWidget.extend({ }, }); +/** + * Handles the dynamic colors for dynamic SVGs. + */ +registry.DynamicSvg = SnippetOptionWidget.extend({ + /** + * @override + */ + start() { + this.$target.on('image_changed.DynamicSvg', this._onImageChanged.bind(this)); + return this._super(...arguments); + }, + /** + * @override + */ + destroy() { + this.$target.off('.DynamicSvg'); + return this._super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Sets the dynamic SVG's dynamic color. + * + * @see this.selectClass for params + */ + async color(previewMode, widgetValue, params) { + const target = this.$target[0]; + switch (previewMode) { + case true: + this.previousSrc = target.getAttribute('src'); + break; + case 'reset': + target.src = this.previousSrc; + return; + } + const newURL = new URL(target.src, window.location.origin); + newURL.searchParams.set('c1', normalizeColor(widgetValue)); + const src = newURL.pathname + newURL.search; + await loadImage(src); + target.src = src; + if (!previewMode) { + this.previousSrc = src; + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeWidgetState(methodName, params) { + switch (methodName) { + case 'color': + return new URL(this.$target[0].src, window.location.origin).searchParams.get('c1'); + } + return this._super(...arguments); + }, + /** + * @override + */ + _computeVisibility(methodName, params) { + return this.$target.is("img[src^='/web_editor/shape/']"); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @override + */ + _onImageChanged(methodName, params) { + return this.updateUI(); + }, +}); return { SnippetOptionWidget: SnippetOptionWidget, diff --git a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js index d405d647ce7..3134f55b373 100644 --- a/addons/web_editor/static/src/js/wysiwyg/widgets/media.js +++ b/addons/web_editor/static/src/js/wysiwyg/widgets/media.js @@ -10,6 +10,7 @@ var utils = require('web.utils'); var Widget = require('web.Widget'); var session = require('web.session'); const {removeOnImageChangeAttrs} = require('web_editor.image_processing'); +const {getCSSVariableValue, DEFAULT_PALETTE} = require('web_editor.utils'); var QWeb = core.qweb; var _t = core._t; @@ -106,6 +107,7 @@ var SearchableMediaWidget = MediaWidget.extend({ * @private */ _onSearchInput: function (ev) { + this.attachments = []; this.search($(ev.currentTarget).val() || '').then(() => this._renderThumbnails()); this.hasSearched = true; }, @@ -128,6 +130,7 @@ var FileWidget = SearchableMediaWidget.extend({ IMAGE_MIMETYPES: ['image/gif', 'image/jpe', 'image/jpeg', 'image/jpg', 'image/gif', 'image/png', 'image/svg+xml'], NUMBER_OF_ATTACHMENTS_TO_DISPLAY: 30, + MAX_DB_ATTACHMENTS: 5, /** * @constructor @@ -144,6 +147,8 @@ var FileWidget = SearchableMediaWidget.extend({ this.attachments = []; this.selectedAttachments = []; + this.libraryMedia = []; + this.selectedMedia = []; this._onUploadURLButtonClick = dom.makeAsyncHandler(this._onUploadURLButtonClick); }, @@ -204,7 +209,6 @@ var FileWidget = SearchableMediaWidget.extend({ * @override */ search: function (needle) { - this.attachments = []; this.needle = needle; return this.fetchAttachments(this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY, 0); }, @@ -231,6 +235,12 @@ var FileWidget = SearchableMediaWidget.extend({ Array.prototype.splice.apply(this.attachments, [offset, attachments.length].concat(attachments)); }); }, + /** + * Computes whether there is content to display in the template. + */ + hasContent() { + return this.attachments.length; + }, //-------------------------------------------------------------------------- // Private @@ -292,7 +302,7 @@ var FileWidget = SearchableMediaWidget.extend({ domain.push(['name', 'ilike', needle]); } domain.push('!', ['name', '=like', '%.crop']); - domain.push('|', ['type', '=', 'binary'], ['url', '!=', false]); + domain.push('|', ['type', '=', 'binary'], '!', ['url', '=like', '/%/static/%']); return domain; }, /** @@ -357,6 +367,7 @@ var FileWidget = SearchableMediaWidget.extend({ this._highlightSelected(); // adapt load more + this.$('.o_we_load_more').toggleClass('d-none', !this.hasContent()); var noLoadMoreButton = this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY >= this.attachments.length; var noMoreImgToLoad = this.numberOfAttachmentsToDisplay >= this.attachments.length; this.$('.o_load_done_msg').toggleClass('d-none', noLoadMoreButton || !noMoreImgToLoad); @@ -367,11 +378,36 @@ var FileWidget = SearchableMediaWidget.extend({ * @returns {Promise} */ _save: async function () { + // Create all media-library attachments. + const toSave = Object.fromEntries(this.selectedMedia.map(media => [ + media.id, { + query: media.query || '', + is_dynamic_svg: !!media.isDynamicSVG, + } + ])); + let mediaAttachments = []; + if (Object.keys(toSave).length !== 0) { + mediaAttachments = await this._rpc({ + route: '/web_editor/save_library_media', + params: { + media: toSave, + }, + }); + } + const selected = this.selectedAttachments.concat(mediaAttachments).map(attachment => { + // Color-customize dynamic SVGs with the primary theme color + if (attachment.image_src.startsWith('/web_editor/shape/')) { + const colorCustomizedURL = new URL(attachment.image_src, window.location.origin); + colorCustomizedURL.searchParams.set('c1', getCSSVariableValue('o-color-1')); + attachment.image_src = colorCustomizedURL.pathname + colorCustomizedURL.search; + } + return attachment; + }); if (this.options.multiImages) { - return this.selectedAttachments; + return selected; } - var img = this.selectedAttachments[0]; + const img = selected[0]; if (!img || !img.id) { return this.media; } @@ -435,22 +471,30 @@ var FileWidget = SearchableMediaWidget.extend({ * and to close the media dialog * @private */ - _selectAttachement: function (attachment, save) { + _selectAttachement: function (attachment, save, {type = 'attachment'} = {}) { + const possibleProps = { + 'attachment': 'selectedAttachments', + 'media': 'selectedMedia' + }; + const prop = possibleProps[type]; if (this.options.multiImages) { // if the clicked attachment is already selected then unselect it // unless it was a save request (then keep the current selection) - var index = this.selectedAttachments.indexOf(attachment); + const index = this[prop].indexOf(attachment); if (index !== -1) { if (!save) { - this.selectedAttachments.splice(index, 1); + this[prop].splice(index, 1); } } else { // if the clicked attachment is not selected, add it to selected - this.selectedAttachments.push(attachment); + this[prop].push(attachment); } } else { + Object.values(possibleProps).forEach(prop => { + this[prop] = []; + }); // select the clicked attachment - this.selectedAttachments = [attachment]; + this[prop] = [attachment]; } this._highlightSelected(); if (save) { @@ -481,9 +525,15 @@ var FileWidget = SearchableMediaWidget.extend({ * @private */ _onAttachmentClick: function (ev) { - var $attachment = $(ev.currentTarget); - var attachment = _.find(this.attachments, {id: $attachment.data('id')}); - this._selectAttachement(attachment, !this.options.multiImages); + const attachment = ev.currentTarget; + const {id: attachmentID, mediaId} = attachment.dataset; + if (attachmentID) { + const attachment = this.attachments.find(attachment => attachment.id === parseInt(attachmentID)); + this._selectAttachement(attachment, !this.options.multiImages); + } else if (mediaId) { + const media = this.libraryMedia.find(media => media.id === parseInt(mediaId)); + this._selectAttachement(media, !this.options.multiImages, {type: 'media'}); + } }, /** * Handles change of the file input: create attachments with the new files @@ -639,6 +689,7 @@ var FileWidget = SearchableMediaWidget.extend({ * @override */ _onSearchInput: function () { + this.attachments = []; this.numberOfAttachmentsToDisplay = this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY; this._super.apply(this, arguments); }, @@ -652,6 +703,7 @@ var ImageWidget = FileWidget.extend({ existingAttachmentsTemplate: 'wysiwyg.widgets.image.existing.attachments', events: Object.assign({}, FileWidget.prototype.events, { 'change input.o_we_show_optimized': '_onShowOptimizedChange', + 'change .o_we_search_select': '_onSearchSelect', }), MIN_ROW_HEIGHT: 128, @@ -659,6 +711,7 @@ var ImageWidget = FileWidget.extend({ * @constructor */ init: function (parent, media, options) { + this.searchService = 'all'; options = _.extend({ accept: 'image/*', mimetypeDomain: [['mimetype', 'in', this.IMAGE_MIMETYPES]], @@ -681,6 +734,60 @@ var ImageWidget = FileWidget.extend({ this.el.removeEventListener('load', this._onAttachmentImageLoad, true); return this._super(...arguments); }, + /** + * @override + */ + async fetchAttachments(number, offset) { + if (this.needle && this.searchService !== 'database') { + number = this.MAX_DB_ATTACHMENTS; + offset = 0; + } + const result = await this._super(number, offset); + // Color-substitution for dynamic SVG attachment + const primaryColor = getCSSVariableValue('o-color-1'); + this.attachments.forEach(attachment => { + if (attachment.image_src.startsWith('/')) { + const newURL = new URL(attachment.image_src, window.location.origin); + // Set the main color of dynamic SVGs to o-color-1 + if (attachment.image_src.startsWith('/web_editor/shape/')) { + newURL.searchParams.set('c1', primaryColor); + } else { + // Set height so that db images load faster + newURL.searchParams.set('height', 2 * this.MIN_ROW_HEIGHT); + } + attachment.thumbnail_src = newURL.pathname + newURL.search; + } + }); + if (this.needle) { + try { + const response = await this._rpc({ + route: '/web_editor/media_library_search', + params: { + 'query': this.needle, + 'offset': this.libraryMedia.length, + }, + }); + const newMedia = response.media; + this.nbMediaResults = response.results; + this.libraryMedia.push(...newMedia); + } catch (e) { + // Either API endpoint doesn't exist or is misconfigured. + console.error(`Couldn't reach API endpoint.`); + } + } + return result; + }, + /** + * @override + */ + hasContent() { + if (this.searchService === 'all') { + return this._super(...arguments) || this.libraryMedia.length; + } else if (this.searchService === 'media-library') { + return !!this.libraryMedia.length; + } + return this._super(...arguments); + }, //-------------------------------------------------------------------------- // Private @@ -708,7 +815,7 @@ var ImageWidget = FileWidget.extend({ this.$('.o_existing_attachment_cell').addClass('d-none'); // Replace images that had been previously loaded if any to prevent scroll resetting to top alreadyLoaded.each((index, el) => { - const toReplace = this.$(`.o_existing_attachment_cell[data-id="${el.dataset.id}"]`); + const toReplace = this.$(`.o_existing_attachment_cell[data-id="${el.dataset.id}"], .o_existing_attachment_cell[data-media-id="${el.dataset.mediaId}"]`); if (toReplace.length) { toReplace.replaceWith(el); } @@ -720,13 +827,23 @@ var ImageWidget = FileWidget.extend({ flexGrow: placeholderWidth, flexBasis: placeholderWidth, }); + if (this.needle && ['media-library', 'all'].includes(this.searchService)) { + const noMoreImgToLoad = this.libraryMedia.length === this.nbMediaResults; + const noLoadMoreButton = noMoreImgToLoad && this.libraryMedia.length <= 15; + this.$('.o_load_done_msg').toggleClass('d-none', noLoadMoreButton || !noMoreImgToLoad); + this.$('.o_load_more').toggleClass('d-none', noMoreImgToLoad); + } }, /** * @override */ _renderExisting: function (attachments) { + if (this.needle && this.searchService !== 'database') { + attachments = attachments.slice(0, this.MAX_DB_ATTACHMENTS); + } return QWeb.render(this.existingAttachmentsTemplate, { attachments: attachments, + libraryMedia: this.libraryMedia, widget: this, }); }, @@ -738,6 +855,16 @@ var ImageWidget = FileWidget.extend({ _toggleOptimized: function (value) { this.$('.o_we_attachment_optimized').each((i, cell) => cell.style.setProperty('display', value ? null : 'none', 'important')); }, + /** + * @override + */ + _highlightSelected: function () { + this._super(...arguments); + this.selectedMedia.forEach(media => { + this.$(`.o_existing_attachment_cell[data-media-id=${media.id}]`) + .addClass("o_we_attachment_selected"); + }); + }, //-------------------------------------------------------------------------- // Handlers @@ -746,12 +873,37 @@ var ImageWidget = FileWidget.extend({ /** * @override */ - _onAttachmentImageLoad: function (ev) { + _onAttachmentImageLoad: async function (ev) { const img = ev.target; const cell = img.closest('.o_existing_attachment_cell'); if (!cell) { return; } + if (cell.dataset.mediaId && !img.src.startsWith('blob')) { + const mediaUrl = img.src; + try { + const response = await fetch(mediaUrl); + if (response.headers.get('content-type') === 'image/svg+xml') { + const svg = await response.text(); + const colorRegex = new RegExp(DEFAULT_PALETTE['1'], 'gi'); + if (colorRegex.test(svg)) { + const fileName = mediaUrl.split('/').pop(); + const file = new File([svg.replace(colorRegex, getCSSVariableValue('o-color-1'))], fileName, { + type: "image/svg+xml", + }); + img.src = URL.createObjectURL(file); + const media = this.libraryMedia.find(media => media.id === parseInt(cell.dataset.mediaId)); + if (media) { + media.isDynamicSVG = true; + } + // We changed the src: wait for the next load event to do the styling + return; + } + } + } catch (e) { + console.error('CORS is misconfigured on the API server, image will be treated as non-dynamic.'); + } + } const aspectRatio = img.naturalWidth / img.naturalHeight; const width = aspectRatio * this.MIN_ROW_HEIGHT; cell.style.flexGrow = width; @@ -766,6 +918,21 @@ var ImageWidget = FileWidget.extend({ _onShowOptimizedChange: function (ev) { this._toggleOptimized(ev.target.checked); }, + /** + * @override + */ + _onSearchSelect: function (ev) { + const {value} = ev.target; + this.searchService = value; + this.$('.o_we_search').trigger('input'); + }, + /** + * @private + */ + _onSearchInput: function (ev) { + this.libraryMedia = []; + this._super(...arguments); + }, }); |
