summaryrefslogtreecommitdiff
path: root/addons/web_editor/static/src/js/editor/snippets.options.js
diff options
context:
space:
mode:
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.js451
1 files changed, 410 insertions, 41 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 1060140c619..e752d07f66c 100644
--- a/addons/web_editor/static/src/js/editor/snippets.options.js
+++ b/addons/web_editor/static/src/js/editor/snippets.options.js
@@ -8,7 +8,10 @@ const time = require('web.time');
var Widget = require('web.Widget');
var ColorPaletteWidget = require('web_editor.ColorPalette').ColorPaletteWidget;
const weUtils = require('web_editor.utils');
-const {normalizeColor} = weUtils;
+const {
+ normalizeColor,
+ getBgImageURL,
+} = weUtils;
var weWidgets = require('wysiwyg.widgets');
const {
loadImage,
@@ -1588,6 +1591,87 @@ const RangeUserValueWidget = UserValueWidget.extend({
},
});
+const SelectPagerUserValueWidget = SelectUserValueWidget.extend({
+ className: (SelectUserValueWidget.prototype.className || '') + ' o_we_select_pager',
+ events: Object.assign({}, SelectUserValueWidget.prototype.events, {
+ 'click .o_we_pager_next, .o_we_pager_prev': '_onPageChange',
+ }),
+
+ /**
+ * @override
+ */
+ async start() {
+ const _super = this._super.bind(this);
+ this.pages = this.options.childNodes.filter(node => node.matches && node.matches('we-select-page'));
+ this.numPages = this.pages.length;
+
+ const prev = document.createElement('i');
+ prev.classList.add('o_we_pager_prev', 'fa', 'fa-chevron-left');
+
+ this.pageNum = document.createElement('span');
+ this.currentPage = 0;
+ this._updatePageNum();
+
+ const next = document.createElement('i');
+ next.classList.add('o_we_pager_next', 'fa', 'fa-chevron-right');
+
+ const pagerControls = document.createElement('div');
+ pagerControls.classList.add('o_we_pager_controls');
+ pagerControls.appendChild(prev);
+ pagerControls.appendChild(this.pageNum);
+ pagerControls.appendChild(next);
+
+ await _super(...arguments);
+ this.menuEl.classList.add('o_we_has_pager');
+ $(this.menuEl).prepend(pagerControls);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Updates the pager's page number display.
+ *
+ * @private
+ */
+ _updatePageNum() {
+ this.pages.forEach((page, i) => page.classList.toggle('active', i === this.currentPage));
+ this.pageNum.textContent = `${this.currentPage + 1}/${this.numPages}`;
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Goes to the previous/next page with wrap-around.
+ *
+ * @private
+ */
+ _onPageChange(ev) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ const delta = ev.target.matches('.o_we_pager_next') ? 1 : -1;
+ this.currentPage = (this.currentPage + this.numPages + delta) % this.numPages;
+ this._updatePageNum();
+ },
+ /**
+ * @override
+ */
+ _onClick(ev) {
+ const activeButton = this._getActiveSubWidget();
+ if (activeButton) {
+ const currentPage = this.pages.indexOf(activeButton.el.closest('we-select-page'));
+ if (currentPage !== -1) {
+ this.currentPage = currentPage;
+ this._updatePageNum();
+ }
+ }
+ return this._super(...arguments);
+ },
+});
+
const userValueWidgetsRegistry = {
'we-button': ButtonUserValueWidget,
'we-checkbox': CheckboxUserValueWidget,
@@ -1600,6 +1684,7 @@ const userValueWidgetsRegistry = {
'we-imagepicker': ImagepickerUserValueWidget,
'we-videopicker': VideopickerUserValueWidget,
'we-range': RangeUserValueWidget,
+ 'we-select-pager': SelectPagerUserValueWidget,
};
/**
@@ -3087,18 +3172,6 @@ registry.ImageOptimize = ImageHandlerOption.extend({
});
/**
- * 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({
@@ -3166,9 +3239,9 @@ registry.BackgroundOptimize = ImageHandlerOption.extend({
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);
+ const src = new URL(getBgImageURL(this.$target[0]), 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;
+ this.img.src = src.origin === window.location.origin && src.pathname;
return await this._super(...arguments);
},
@@ -3213,6 +3286,18 @@ registry.BackgroundToggler = SnippetOptionWidget.extend({
this._requestUserValueWidgets('bg_image_opt')[0].el.click();
}
},
+ /**
+ * Toggles background shape on or off.
+ *
+ * @see this.selectClass for parameters
+ */
+ toggleBgShape(previewMode, widgetValue, params) {
+ const [shapeWidget] = this._requestUserValueWidgets('bg_shape_opt');
+ const shapeOption = shapeWidget.getParent();
+ // TODO: open select after shape was selected?
+ // TODO: use setWidgetValue instead of calling shapeOption method directly when possible
+ return shapeOption._toggleShape();
+ },
//--------------------------------------------------------------------------
// Private
@@ -3222,10 +3307,17 @@ registry.BackgroundToggler = SnippetOptionWidget.extend({
* @override
*/
_computeWidgetState(methodName, params) {
- if (methodName === 'toggleBgImage') {
- const [bgImageWidget] = this._requestUserValueWidgets('bg_image_opt');
- const bgImageOpt = bgImageWidget.getParent();
- return !!bgImageOpt._computeWidgetState('background', bgImageWidget.getMethodsParams('background'));
+ switch (methodName) {
+ case 'toggleBgImage': {
+ const [bgImageWidget] = this._requestUserValueWidgets('bg_image_opt');
+ const bgImageOpt = bgImageWidget.getParent();
+ return !!bgImageOpt._computeWidgetState('background', bgImageWidget.getMethodsParams('background'));
+ }
+ case 'toggleBgShape': {
+ const [shapeWidget] = this._requestUserValueWidgets('bg_shape_opt');
+ const shapeOption = shapeWidget.getParent();
+ return !!shapeOption._computeWidgetState('shape', shapeWidget.getMethodsParams('shape'));
+ }
}
return this._super(...arguments);
},
@@ -3239,7 +3331,7 @@ registry.BackgroundImage = SnippetOptionWidget.extend({
* @override
*/
start: function () {
- this.__customImageSrc = this._getSrcFromCssValue();
+ this.__customImageSrc = getBgImageURL(this.$target[0]);
return this._super(...arguments);
},
@@ -3254,7 +3346,7 @@ registry.BackgroundImage = SnippetOptionWidget.extend({
*/
background: async function (previewMode, widgetValue, params) {
if (previewMode === true) {
- this.__customImageSrc = this._getSrcFromCssValue();
+ this.__customImageSrc = getBgImageURL(this.$target[0]);
} else if (previewMode === 'reset') {
widgetValue = this.__customImageSrc;
} else {
@@ -3285,7 +3377,7 @@ registry.BackgroundImage = SnippetOptionWidget.extend({
setTarget: function () {
this._super(...arguments);
// TODO should be automatic for all options as equal to the start method
- this.__customImageSrc = this._getSrcFromCssValue();
+ this.__customImageSrc = getBgImageURL(this.$target[0]);
},
//--------------------------------------------------------------------------
@@ -3293,23 +3385,309 @@ registry.BackgroundImage = SnippetOptionWidget.extend({
//--------------------------------------------------------------------------
/**
- * Returns the background image's src.
+ * @override
+ */
+ _computeWidgetState: function (methodName) {
+ if (methodName === 'background') {
+ return getBgImageURL(this.$target[0]);
+ }
+ return this._super(...arguments);
+ },
+});
+
+/**
+ * Handles background shapes.
+ */
+registry.BackgroundShape = SnippetOptionWidget.extend({
+ /**
+ * @override
+ */
+ updateUI() {
+ if (this.rerender) {
+ this.rerender = false;
+ return this._rerenderXML();
+ }
+ return this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Options
+ //--------------------------------------------------------------------------
+
+ /**
+ * Sets the current background shape.
*
- * @private
- * @returns {string}
+ * @see this.selectClass for params
+ */
+ shape(previewMode, widgetValue, params) {
+ this._handlePreviewState(previewMode, () => {
+ return {shape: widgetValue, colors: this._getDefaultColors(), flip: []};
+ });
+ },
+ /**
+ * Sets the current background shape's colors.
+ *
+ * @see this.selectClass for params
+ */
+ color(previewMode, widgetValue, params) {
+ this._handlePreviewState(previewMode, () => {
+ const {colorName} = params;
+ const {colors: previousColors} = this._getShapeData();
+ const newColor = widgetValue || this._getDefaultColors()[colorName];
+ const newColors = Object.assign(previousColors, {[colorName]: newColor});
+ return {colors: newColors};
+ });
+ },
+ /**
+ * Flips the shape on its x axis.
+ *
+ * @see this.selectClass for params
*/
- _getSrcFromCssValue: function () {
- return getSrcFromCssValue(this.$target.css('background-image'));
+ flipX(previewMode, widgetValue, params) {
+ this._flipShape(previewMode, 'x');
},
/**
+ * Flips the shape on its y axis.
+ *
+ * @see this.selectClass for params
+ */
+ flipY(previewMode, widgetValue, params) {
+ this._flipShape(previewMode, 'y');
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
* @override
*/
- _computeWidgetState: function (methodName) {
- if (methodName === 'background') {
- return this._getSrcFromCssValue();
+ _computeWidgetState(methodName, params) {
+ switch (methodName) {
+ case 'shape': {
+ return this._getShapeData().shape;
+ }
+ case 'color': {
+ const {shape, colors: customColors} = this._getShapeData();
+ const colors = Object.assign(this._getDefaultColors(), customColors);
+ const color = shape && colors[params.colorName];
+ return color ? normalizeColor(color) : '';
+ }
+ case 'flipX': {
+ return this.$target.find('> .o_we_shape.o_we_flip_x').length !== 0;
+ }
+ case 'flipY': {
+ return this.$target.find('> .o_we_shape.o_we_flip_y').length !== 0;
+ }
+ }
+ return this._super(...arguments);
+ },
+ /**
+ * @override
+ */
+ _renderCustomXML(uiFragment) {
+ Object.keys(this._getDefaultColors()).map(colorName => {
+ uiFragment.querySelector('[data-name="colors"]')
+ .prepend($(`<we-colorpicker data-color="true" data-color-name="${colorName}">`)[0]);
+ });
+
+ uiFragment.querySelectorAll('we-select-pager we-button[data-shape]').forEach(btn => {
+ const btnContent = document.createElement('div');
+ btnContent.classList.add('o_we_shape_btn_content', 'position-relative', 'border-dark');
+ const btnContentInnerDiv = document.createElement('div');
+ btnContentInnerDiv.classList.add('o_we_shape');
+ btnContent.appendChild(btnContentInnerDiv);
+
+ const {shape} = btn.dataset;
+ const shapeEl = btnContent.querySelector('.o_we_shape');
+ shapeEl.classList.add(`o_${shape.replace(/\//g, '_')}`);
+ btn.append(btnContent);
+ });
+ return uiFragment;
+ },
+ /**
+ * @override
+ */
+ async _computeWidgetVisibility(widgetName, params) {
+ if (widgetName === 'shape_none_opt') {
+ return false;
}
return this._super(...arguments);
},
+ /**
+ * Flips the shape on its x/y axis.
+ *
+ * @param {boolean} previewMode
+ * @param {'x'|'y'} axis the axis of the shape that should be flipped.
+ */
+ _flipShape(previewMode, axis) {
+ this._handlePreviewState(previewMode, () => {
+ const flip = new Set(this._getShapeData().flip);
+ if (flip.has(axis)) {
+ flip.delete(axis);
+ } else {
+ flip.add(axis);
+ }
+ return {flip: [...flip]};
+ });
+ },
+ /**
+ * Handles everything related to saving state before preview and restoring
+ * it after a preview or locking in the changes when not in preview.
+ *
+ * @param {boolean} previewMode
+ * @param {function} computeShapeData function to compute the new shape data.
+ */
+ _handlePreviewState(previewMode, computeShapeData) {
+ if (previewMode === 'reset') {
+ if (this.prevShape) {
+ this.$target[0].dataset.oeShapeData = this.prevShape;
+ } else {
+ delete this.$target[0].dataset.oeShapeData;
+ }
+ } else {
+ if (previewMode === true) {
+ this.prevShape = this.$target[0].dataset.oeShapeData;
+ }
+ const newShapeData = computeShapeData();
+ this._markShape(newShapeData);
+ if (previewMode === false) {
+ this.prevShape = this.$target[0].dataset.oeShapeData;
+ if (newShapeData.hasOwnProperty('shape')) {
+ // Need to rerender for correct number of colorpickers
+ this.rerender = true;
+ }
+ }
+ }
+
+ // Updates/removes the shape container as needed and gives it the
+ // correct background shape
+ const json = this.$target[0].dataset.oeShapeData;
+ const {shape, colors, flip = []} = json ? JSON.parse(json) : {};
+ const target = this.$target[0];
+ let shapeContainer = target.querySelector(':scope > .o_we_shape');
+ if (!shape) {
+ if (shapeContainer) {
+ shapeContainer.remove();
+ }
+ return;
+ }
+ if (!shapeContainer) {
+ shapeContainer = document.createElement('div');
+ target.prepend(shapeContainer);
+ if (shapeContainer.nextElementSibling.matches('.s_parallax_bg')) {
+ // Move parallax before shape so it shows on top.
+ target.prepend(shapeContainer.nextElementSibling);
+ }
+ target.style.position = 'relative';
+ }
+ shapeContainer.className = `o_we_shape o_${shape.replace(/\//g, '_')}`;
+ flip.forEach(dir => {
+ shapeContainer.classList.add(`o_we_flip_${dir}`);
+ });
+ // Custom colors, overwrite shape that is set by the class
+ $(shapeContainer).css('background-image', colors ? `url("${this._getShapeSrc()}")` : '');
+ },
+ /**
+ * Overwrites shape properties with the specified data.
+ *
+ * @private
+ * @param {Object} newData an object with the new data
+ */
+ _markShape(newData) {
+ const defaultColors = this._getDefaultColors();
+ const shapeData = Object.assign(this._getShapeData(), newData);
+ const areColorsDefault = Object.entries(shapeData.colors).every(([colorName, colorValue]) => {
+ return colorValue === defaultColors[colorName];
+ });
+ if (areColorsDefault) {
+ delete shapeData.colors;
+ }
+ if (!shapeData.shape) {
+ delete this.$target[0].dataset.oeShapeData;
+ } else {
+ this.$target[0].dataset.oeShapeData = JSON.stringify(shapeData);
+ }
+ },
+ /**
+ * Returns the src of the shape corresponding to the current parameters.
+ *
+ * @private
+ */
+ _getShapeSrc() {
+ const {shape, colors} = this._getShapeData();
+ if (!shape) {
+ return '';
+ }
+ const queryString = Object.entries(colors)
+ .map(([colorName, colorValue]) => {
+ const encodedCol = encodeURIComponent(normalizeColor(colorValue));
+ return `${colorName}=${encodedCol}`;
+ }).join('&');
+ return `/web_editor/shape/${shape}.svg?${queryString}`;
+ },
+ /**
+ * Retrieves current shape data from the target's dataset.
+ *
+ * @private
+ * @param {HTMLElement} [target=this.$target[0]] the target on which to read
+ * the shape data.
+ */
+ _getShapeData(target = this.$target[0]) {
+ const defaultData = {
+ shape: '',
+ colors: this._getDefaultColors(),
+ flip: [],
+ };
+ const json = target.dataset.oeShapeData;
+ return json ? Object.assign(defaultData, JSON.parse(json)) : defaultData;
+ },
+ /**
+ * Returns the default colors for the currently selected shape.
+ *
+ * @private
+ */
+ _getDefaultColors() {
+ const $shapeContainer = this.$target.find('> .o_we_shape')
+ .clone()
+ .addClass('d-none')
+ // Needs to be in document for bg-image class to take effect
+ .appendTo(document.body);
+ const shapeContainer = $shapeContainer[0];
+ $shapeContainer.css('background-image', '');
+ const shapeSrc = shapeContainer && getBgImageURL(shapeContainer);
+ $shapeContainer.remove();
+ if (!shapeSrc) {
+ return {};
+ }
+ const url = new URL(shapeSrc, window.location.origin);
+ return Object.fromEntries(url.searchParams.entries());
+ },
+ /**
+ * Toggles whether there is a shape or not, to be called from bg toggler.
+ *
+ * @private
+ */
+ _toggleShape() {
+ if (this._getShapeData().shape) {
+ return this._handlePreviewState(false, () => ({shape: ''}));
+ } else {
+ const target = this.$target[0];
+ const previousSibling = target.previousElementSibling;
+ const [shapeWidget] = this._requestUserValueWidgets('bg_shape_opt');
+ const possibleShapes = shapeWidget.getMethodsParams('shape').possibleValues;
+ let shapeToSelect;
+ if (previousSibling) {
+ const previousShape = this._getShapeData(previousSibling).shape;
+ shapeToSelect = possibleShapes.find((shape, i) => {
+ return possibleShapes[i - 1] === previousShape;
+ });
+ } else {
+ shapeToSelect = possibleShapes[1];
+ }
+ return this._handlePreviewState(false, () => ({shape: shapeToSelect}));
+ }
+ },
});
/**
@@ -3364,7 +3742,7 @@ registry.BackgroundPosition = SnippetOptionWidget.extend({
await new Promise(resolve => {
this.img = document.createElement('img');
this.img.addEventListener('load', () => resolve());
- this.img.src = this._getSrcFromCssValue();
+ this.img.src = getBgImageURL(this.$target[0]);
});
const position = this.$target.css('background-position').split(' ').map(v => parseInt(v));
@@ -3399,7 +3777,7 @@ registry.BackgroundPosition = SnippetOptionWidget.extend({
* @override
*/
_computeVisibility: function () {
- return this._super(...arguments) && (this.$target.css('background-image') !== 'none');
+ return this._super(...arguments) && !!getBgImageURL(this.$target[0]);
},
/**
* @override
@@ -3507,15 +3885,6 @@ registry.BackgroundPosition = SnippetOptionWidget.extend({
window.setTimeout(() => $(document).on('click.bgposition', this._onDocumentClicked.bind(this)), 0);
},
/**
- * Returns the background image's src.
- *
- * @private
- * @returns {string}
- */
- _getSrcFromCssValue: function () {
- return getSrcFromCssValue(this.$target.css('background-image'));
- },
- /**
* Returns the difference between the target's size and the background's
* rendered size. Background position values in % are a percentage of this.
*