summaryrefslogtreecommitdiff
path: root/addons/web_editor/static/src/js
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web_editor/static/src/js')
-rw-r--r--addons/web_editor/static/src/js/common/utils.js13
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.editor.js7
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.options.js121
-rw-r--r--addons/web_editor/static/src/js/wysiwyg/widgets/media.js195
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);
+ },
});