[IMP] website, *: give flexibility to dynamic snippet templates

*: website_blog, website_event

Commit [1] introduced new templates. This commit's goal is to build on
that template system to provide more flexibility for designers.
It adds new data-attributes to set on the first node of a template.

- data-row-per-slide: Allows for the template to tell a dynamic snippet
carousel how many rows of cards it should display.

- data-arrow-position: Allows for the template to tell a dynamic snippet
carousel where the arrows should be positioned.
(for now just bottom, or side)

- data-extra-classes: give the template the possibility to add extra
classes to the dynamic snippet instead of just the card.

This commit also removes options to select the amount of elements
displayed on each row. This is now only changeable through templates, or
by directly editing the snippet's DOM through debug tools. This was done
to reduce the amount of options visible, so that the user just chooses
a template and it is ready to go.

[1]: https://github.com/odoo/odoo/commit/49bd79b0bdca76415780bdfa199195371c2692e5

task-2677203

Part-of: odoo/odoo#80128
This commit is contained in:
Arthur Detroux (ard)
2022-10-04 14:56:50 +02:00
parent 5afd3607a1
commit d3307aaa08
13 changed files with 110 additions and 91 deletions
+3
View File
@@ -398,6 +398,9 @@ class Website(Home):
t['numOfEl'] = attribs.get('data-number-of-elements')
t['numOfElSm'] = attribs.get('data-number-of-elements-sm')
t['numOfElFetch'] = attribs.get('data-number-of-elements-fetch')
t['rowPerSlide'] = attribs.get('data-row-per-slide')
t['arrowPosition'] = attribs.get('data-arrow-position')
t['extraClasses'] = attribs.get('data-extra-classes')
return templates
@http.route('/website/get_current_currency', type='json', auth="public", website=True)
@@ -5,6 +5,8 @@ const core = require('web.core');
const config = require('web.config');
const publicWidget = require('web.public.widget');
const {Markup} = require('web.utils');
const DEFAULT_NUMBER_OF_ELEMENTS = 4;
const DEFAULT_NUMBER_OF_ELEMENTS_SM = 1;
const DynamicSnippet = publicWidget.Widget.extend({
selector: '.s_dynamic_snippet',
@@ -134,28 +136,31 @@ const DynamicSnippet = publicWidget.Widget.extend({
* @private
*/
_prepareContent: function () {
if (this.$target[0].dataset.numberOfElements && this.$target[0].dataset.numberOfElementsSmallDevices) {
this.renderedContent = core.qweb.render(
this.template_key,
this._getQWebRenderOptions());
} else {
this.renderedContent = '';
}
this.renderedContent = core.qweb.render(
this.template_key,
this._getQWebRenderOptions()
);
},
/**
* Method to be overridden in child components in order to prepare QWeb
* options.
* @private
*/
_getQWebRenderOptions: function () {
_getQWebRenderOptions: function () {
const dataset = this.$target[0].dataset;
const numberOfRecords = parseInt(dataset.numberOfRecords);
let numberOfElements;
if (config.device.isMobile) {
numberOfElements = parseInt(dataset.numberOfElementsSmallDevices) || DEFAULT_NUMBER_OF_ELEMENTS_SM;
} else {
numberOfElements = parseInt(dataset.numberOfElements) || DEFAULT_NUMBER_OF_ELEMENTS;
}
const chunkSize = numberOfRecords < numberOfElements ? numberOfRecords : numberOfElements;
return {
chunkSize: parseInt(
config.device.isMobile
? this.$target[0].dataset.numberOfElementsSmallDevices
: this.$target[0].dataset.numberOfElements
),
chunkSize: chunkSize,
data: this.data,
uniqueId: this.uniqueId
uniqueId: this.uniqueId,
extraClasses: dataset.extraClasses || '',
};
},
/**
@@ -6,7 +6,7 @@
<t t-set="rowIndexGenerator" t-value="Array.from(Array(Math.ceil(data.length/chunkSize)).keys())"/>
<t t-set="colIndexGenerator" t-value="Array.from(Array(chunkSize).keys())"/>
<t t-foreach="rowIndexGenerator" t-as="rowIndex">
<div class="row my-4">
<div t-attf-class="row my-4 #{extraClasses}">
<t t-foreach="colIndexGenerator" t-as="colIndex">
<t t-if="(rowIndex * chunkSize + colIndex) &lt; data.length">
<div t-attf-class="#{colClass}">
@@ -47,18 +47,6 @@ const dynamicSnippetOptions = options.Class.extend({
if (params.attributeName === 'templateKey' && previewMode === false) {
this._templateUpdated(widgetValue, params.activeValue);
}
if (params.attributeName === 'numberOfRecords' && previewMode === false) {
const dataSet = this.$target.get(0).dataset;
const numberOfElements = parseInt(dataSet.numberOfElements);
const numberOfRecords = parseInt(dataSet.numberOfRecords);
const numberOfElementsSmallDevices = parseInt(dataSet.numberOfElementsSmallDevices);
if (numberOfElements > numberOfRecords) {
dataSet.numberOfElements = numberOfRecords;
}
if (numberOfElementsSmallDevices > numberOfRecords) {
dataSet.numberOfElementsSmallDevices = numberOfRecords;
}
}
},
//--------------------------------------------------------------------------
@@ -79,16 +67,6 @@ const dynamicSnippetOptions = options.Class.extend({
await this._super(...arguments);
},
/**
* @override
*/
async updateUIVisibility() {
await this._super(...arguments);
const template = this._getCurrentTemplate();
const groupingMessage = this.el.querySelector('.o_grouping_message');
groupingMessage.classList.toggle('d-none', template && !!template.numOfEl && !!template.numOfElSm);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
@@ -112,16 +90,6 @@ const dynamicSnippetOptions = options.Class.extend({
return Object.keys(this.dynamicFilters).length !== 1;
}
if (widgetName === 'number_of_elements_opt') {
const template = this._getCurrentTemplate();
return template && !template.numOfEl;
}
if (widgetName === 'number_of_elements_small_devices_opt') {
const template = this._getCurrentTemplate();
return template && !template.numOfElSm;
}
if (widgetName === 'number_of_records_opt') {
const template = this._getCurrentTemplate();
return template && !template.numOfElFetch;
@@ -249,8 +217,6 @@ const dynamicSnippetOptions = options.Class.extend({
// numberOfElements or numberOfElementsSmallDevices) might throw an
// exception by not finding the attribute on the element.
this.options.wysiwyg.odooEditor.observerUnactive();
this._setOptionValue('numberOfElements', 4);
this._setOptionValue('numberOfElementsSmallDevices', 1);
const filterKeys = this.$el.find("we-select[data-attribute-name='filterId'] we-selection-items we-button");
if (filterKeys.length > 0) {
this._setOptionValue('numberOfRecords', this.dynamicFilters[Object.keys(this.dynamicFilters)[0]].limit);
@@ -285,13 +251,22 @@ const dynamicSnippetOptions = options.Class.extend({
const template = this.dynamicFilterTemplates[newTemplate];
if (template.numOfEl) {
this.$target[0].dataset.numberOfElements = template.numOfEl;
} else {
delete this.$target[0].dataset.numberOfElements;
}
if (template.numOfElSm) {
this.$target[0].dataset.numberOfElementsSmallDevices = template.numOfElSm;
} else {
delete this.$target[0].dataset.numberOfElementsSmallDevices;
}
if (template.numOfElFetch) {
this.$target[0].dataset.numberOfRecords = template.numOfElFetch;
}
if (template.extraClasses) {
this.$target[0].dataset.extraClasses = template.extraClasses;
} else {
delete this.$target[0].dataset.extraClasses;
}
},
/**
* Sets the option value.
@@ -3,6 +3,7 @@ odoo.define('website.s_dynamic_snippet_carousel', function (require) {
const publicWidget = require('web.public.widget');
const DynamicSnippet = require('website.s_dynamic_snippet');
const config = require('web.config');
const DynamicSnippetCarousel = DynamicSnippet.extend({
selector: '.s_dynamic_snippet_carousel',
@@ -26,6 +27,8 @@ const DynamicSnippetCarousel = DynamicSnippet.extend({
this._super.apply(this, arguments),
{
interval: parseInt(this.$target[0].dataset.carouselInterval),
rowPerSlide: parseInt(config.device.isMobile ? 1 : this.$target[0].dataset.rowPerSlide || 1),
arrowPosition: this.$target[0].dataset.arrowPosition || '',
},
);
},
@@ -18,4 +18,9 @@
text-overflow: ellipsis;
overflow: hidden;
}
.s_dynamic_snippet_arrow_bottom {
> .carousel-control-prev, .carousel-control-next {
position: relative;
}
}
}
@@ -1,36 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="website.s_dynamic_snippet.carousel.arrows">
<a t-attf-href="##{uniqueId}" class="carousel-control-prev" data-bs-slide="prev" role="button" aria-label="Previous" title="Previous">
<span class="fa fa-chevron-circle-left fa-2x"/>
<span class="visually-hidden">Previous</span>
</a>
<a t-attf-href="##{uniqueId}" class="carousel-control-next" data-bs-slide="next" role="button" aria-label="Next" title="Next">
<span class="fa fa-chevron-circle-right fa-2x"/>
<span class="visually-hidden">Next</span>
</a>
</t>
<t t-name="website.s_dynamic_snippet.carousel">
<div t-att-id="uniqueId" class="carousel slide" t-att-data-bs-interval="interval">
<!-- Content -->
<t t-set="colClass" t-value="'d-flex flex-grow-0 flex-shrink-0 col-' + Math.trunc(12 / chunkSize).toString()"/>
<t t-set="slideIndexGenerator" t-value="Array.from(Array(Math.ceil(data.length/chunkSize)).keys())"/>
<t t-set="itemIndexGenerator" t-value="Array.from(Array(chunkSize).keys())"/>
<t t-set="rowSize" t-value="chunkSize" />
<t t-set="slideSize" t-value="rowSize * rowPerSlide" />
<t t-set="colClass" t-value="'d-flex flex-grow-0 flex-shrink-0 col-' + Math.trunc(12 / rowSize).toString()"/>
<t t-set="slideIndexGenerator" t-value="Array.from(Array(Math.ceil(data.length/slideSize)).keys())"/>
<t t-set="rowIndexGenerator" t-value="Array.from(Array(rowPerSlide).keys())"/>
<t t-set="colIndexGenerator" t-value="Array.from(Array(rowSize).keys())"/>
<div class="carousel-inner row w-100 mx-auto" role="listbox">
<t t-foreach="slideIndexGenerator" t-as="slideIndex">
<div t-attf-class="carousel-item #{slideIndex_first ? 'active' : ''}">
<div class="row">
<t t-foreach="itemIndexGenerator" t-as="itemIndex">
<t t-if="(slideIndex * chunkSize + itemIndex) &lt; data.length">
<div t-attf-class="#{colClass}">
<t t-out="data[slideIndex * chunkSize + itemIndex]"/>
</div>
</t>
<t t-set="slideOffset" t-value="slideIndex * slideSize"/>
<div t-attf-class="carousel-item #{extraClasses} #{slideIndex_first ? 'active' : ''}">
<t t-foreach="rowIndexGenerator" t-as="rowIndex">
<t t-set="rowOffset" t-value="slideOffset + (rowIndex * rowSize)"/>
<t t-if="rowOffset &lt; data.length">
<div class="row">
<t t-foreach="colIndexGenerator" t-as="colIndex">
<t t-set="itemIndex" t-value="rowOffset + colIndex"/>
<t t-if="itemIndex &lt; data.length">
<div t-attf-class="#{colClass}">
<t t-out="data[itemIndex]"/>
</div>
</t>
</t>
</div>
</t>
</div>
</t>
</div>
</t>
</div>
<!-- Controls -->
<t t-if='slideIndexGenerator.length > 1'>
<a t-attf-href="##{uniqueId}" class="carousel-control-prev" data-bs-slide="prev" role="button" aria-label="Previous" title="Previous">
<span class="fa fa-chevron-circle-left fa-2x"/>
<span class="visually-hidden">Previous</span>
</a>
<a t-attf-href="##{uniqueId}" class="carousel-control-next" data-bs-slide="next" role="button" aria-label="Next" title="Next">
<span class="fa fa-chevron-circle-right fa-2x"/>
<span class="visually-hidden">Next</span>
</a>
<t t-if="arrowPosition === 'bottom'">
<div class="s_dynamic_snippet_arrow_bottom pt-2 d-flex justify-content-center">
<t t-call="website.s_dynamic_snippet.carousel.arrows">
<t t-set="uniqueId" t-value="uniqueId" />
</t>
</div>
</t>
<t t-else="">
<t t-call="website.s_dynamic_snippet.carousel.arrows">
<t t-set="uniqueId" t-value="uniqueId" />
</t>
</t>
</t>
</div>
</t>
@@ -18,7 +18,28 @@ const dynamicSnippetCarouselOptions = s_dynamic_snippet_options.extend({
_setOptionsDefaultValues: function () {
this._super.apply(this, arguments);
this._setOptionValue('carouselInterval', '5000');
}
},
/**
* Take the new template selection into account
*
* @param {number} newTemplate id of the newly selected template
* @param {number} oldTemplate id of the previously selected template
* @override
*/
_templateUpdated(newTemplate, oldTemplate) {
this._super(...arguments);
const template = this.dynamicFilterTemplates[newTemplate];
if (template.rowPerSlide) {
this.$target[0].dataset.rowPerSlide = template.rowPerSlide;
} else {
delete this.$target[0].dataset.rowPerSlide;
}
if (template.arrowPosition) {
this.$target[0].dataset.arrowPosition = template.arrowPosition;
} else {
delete this.$target[0].dataset.arrowPosition;
}
},
});
@@ -26,19 +26,6 @@
<we-select string="Fetched elements" data-name="number_of_records_opt" data-attribute-name="numberOfRecords" data-no-preview="true">
<we-button t-foreach="range(1, 17)" t-as="value" t-att-data-select-data-attribute="value" t-esc="value"/>
</we-select>
<we-title class="mt-2 o_grouping_message"><t t-esc="grouping_message"/></we-title>
<we-select string="Desktop" class="o_we_sublevel_1" data-name="number_of_elements_opt" data-attribute-name="numberOfElements" data-no-preview="true">
<we-button data-select-data-attribute="1">1</we-button>
<we-button data-select-data-attribute="2">2</we-button>
<we-button data-select-data-attribute="3">3</we-button>
<we-button data-select-data-attribute="4">4</we-button>
<we-button data-select-data-attribute="6">6</we-button>
</we-select>
<we-select string="Mobile" class="o_we_sublevel_1" data-name="number_of_elements_small_devices_opt" data-attribute-name="numberOfElementsSmallDevices" data-no-preview="true">
<we-button data-select-data-attribute="1">1</we-button>
<we-button data-select-data-attribute="2">2</we-button>
<we-button data-select-data-attribute="3">3</we-button>
</we-select>
<t t-out="0"/>
</div>
</template>
@@ -47,7 +34,6 @@
<t t-call="website.s_dynamic_snippet_options_template">
<t t-set="snippet_name" t-value="'dynamic_snippet'"/>
<t t-set="snippet_selector" t-value="'.s_dynamic_snippet'"/>
<t t-set="grouping_message">Items per row</t>
</t>
</xpath>
</template>
@@ -9,7 +9,6 @@
<t t-call="website.s_dynamic_snippet_options_template">
<t t-set="snippet_name" t-value="snippet_name"/>
<t t-set="snippet_selector" t-value="snippet_selector"/>
<t t-set="grouping_message">Items per slide</t>
<we-input string="Slider Speed"
data-select-data-attribute="1s" data-name="speed_opt" data-attribute-name="carouselInterval" data-no-preview="true"
data-unit="s" data-save-unit="ms" data-step="0.1"/>
@@ -78,7 +78,6 @@ const dynamicSnippetBlogPostsOptions = dynamicSnippetOptions.extend({
* @private
*/
_setOptionsDefaultValues: function () {
this._setOptionValue('numberOfElements', 3);
this._setOptionValue('filterByBlogId', -1);
this._super.apply(this, arguments);
},
@@ -11,7 +11,7 @@
<!-- Load-time templates (rendered in JS on page load) -->
<!-- List layout -->
<template id="dynamic_filter_template_blog_post_list" name="List layout">
<div t-foreach="records" t-as="data" class="d-flex mt-3 s_blog_posts_post">
<div t-foreach="records" t-as="data" class="d-flex mt-3 s_blog_posts_post" data-number-of-elements="2">
<t t-set="record" t-value="data['_record']"/>
<a class="s_blog_posts_post_cover flex-grow-0 flex-shrink-0 align-self-baseline position-relative" t-att-href="data['call_to_action_url']" t-att-title="'Read' + data['name']">
<t t-call="website.record_cover">
@@ -34,7 +34,7 @@
</template>
<!-- Big picture layout -->
<template id="dynamic_filter_template_blog_post_big_picture" name="Big picture layout">
<figure t-foreach="records" t-as="data" class="my-3 w-100 s_blog_posts_post">
<figure t-foreach="records" t-as="data" class="my-3 w-100 s_blog_posts_post" data-number-of-elements="3">
<t t-set="record" t-value="data['_record']"/>
<a class="s_blog_posts_post_cover position-relative d-flex flex-column shadow-sm overflow-hidden rounded text-decoration-none" t-att-href="data['call_to_action_url']">
<t t-call="website.record_cover">
@@ -55,7 +55,7 @@
</template>
<!-- Horizontal layout -->
<template id="dynamic_filter_template_blog_post_horizontal" name="Horizontal layout">
<figure t-foreach="records" t-as="data" class="post s_blog_posts_post w-100">
<figure t-foreach="records" t-as="data" class="post s_blog_posts_post w-100" data-number-of-elements="3">
<t t-set="record" t-value="data['_record']"/>
<span t-if="is_sample" class="h5 float-end bg-primary text-uppercase rounded-circle px-2 py-2">Sample</span>
<figcaption>
@@ -76,7 +76,7 @@
</template>
<!-- Card layout -->
<template id="dynamic_filter_template_blog_post_card" name="Card layout">
<div t-foreach="records" t-as="data" class="s_blog_posts_post pb32 w-100">
<div t-foreach="records" t-as="data" class="s_blog_posts_post pb32 w-100" data-number-of-elements="3">
<t t-set="record" t-value="data['_record']"/>
<div class="card">
<a class="s_blog_posts_post_cover" t-att-href="data['call_to_action_url']">
@@ -74,7 +74,6 @@
<t t-call="website_event.s_dynamic_snippet_options_template">
<t t-set="snippet_name" t-value="'event_upcoming_snippet'"/>
<t t-set="snippet_selector" t-value="'.s_event_upcoming_snippet'"/>
<t t-set="grouping_message">Items per row</t>
</t>
</xpath>
</template>