summaryrefslogtreecommitdiff
path: root/addons/website/static/src
diff options
context:
space:
mode:
Diffstat (limited to 'addons/website/static/src')
-rw-r--r--addons/website/static/src/snippets/s_dynamic_snippet/000.js245
-rw-r--r--addons/website/static/src/snippets/s_dynamic_snippet/000.scss16
-rw-r--r--addons/website/static/src/snippets/s_dynamic_snippet/000.xml20
-rw-r--r--addons/website/static/src/snippets/s_dynamic_snippet/options.js142
-rw-r--r--addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.js46
-rw-r--r--addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.scss13
-rw-r--r--addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.xml35
-rw-r--r--addons/website/static/src/snippets/s_dynamic_snippet_carousel/options.js28
8 files changed, 545 insertions, 0 deletions
diff --git a/addons/website/static/src/snippets/s_dynamic_snippet/000.js b/addons/website/static/src/snippets/s_dynamic_snippet/000.js
new file mode 100644
index 00000000000..bb77874be01
--- /dev/null
+++ b/addons/website/static/src/snippets/s_dynamic_snippet/000.js
@@ -0,0 +1,245 @@
+odoo.define('website.s_dynamic_snippet', function (require) {
+'use strict';
+
+const core = require('web.core');
+const config = require('web.config');
+const publicWidget = require('web.public.widget');
+
+const DynamicSnippet = publicWidget.Widget.extend({
+ selector: '.s_dynamic_snippet',
+ xmlDependencies: ['/website/static/src/snippets/s_dynamic_snippet/000.xml'],
+ disabledInEditableMode: false,
+
+ /**
+ *
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ /**
+ * The dynamic filter data source data formatted with the chosen template.
+ * Can be accessed when overriding the _render_content() function in order to generate
+ * a new renderedContent from the original data.
+ *
+ * @type {*|jQuery.fn.init|jQuery|HTMLElement}
+ */
+ this.$data = $();
+ this.renderedContent = '';
+ this.isDesplayedAsMobile = config.device.isMobile;
+ this.uniqueId = _.uniqueId('s_dynamic_snippet_');
+ this.template_key = 'website.s_dynamic_snippet.grid';
+ },
+ /**
+ *
+ * @override
+ */
+ willStart: function () {
+ return this._super.apply(this, arguments).then(
+ () => Promise.all([
+ this._fetchData(),
+ this._manageWarningMessageVisibility()
+ ])
+ );
+ },
+ /**
+ *
+ * @override
+ */
+ start: function () {
+ return this._super.apply(this, arguments)
+ .then(() => {
+ this._setupCallToAction(true);
+ this._setupSizeChangedManagement(true);
+ this._render();
+ this._toggleVisibility(true);
+ });
+ },
+ /**
+ *
+ * @override
+ */
+ destroy: function () {
+ this._toggleVisibility(false);
+ this._setupSizeChangedManagement(false);
+ this._setupCallToAction(false);
+ this._clearContent();
+ this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ *
+ * @private
+ */
+ _clearContent: function() {
+ const $dynamicSnippetTemplate = this.$el.find('.dynamic_snippet_template');
+ if ($dynamicSnippetTemplate) {
+ $dynamicSnippetTemplate.html('');
+ }
+ },
+ /**
+ * Method to be overridden in child components if additional configuration elements
+ * are required in order to fetch data.
+ * @private
+ */
+ _isConfigComplete: function() {
+ return this.$el.get(0).dataset.filterId !== undefined && this.$el.get(0).dataset.templateKey !== undefined;
+ },
+ /**
+ * Method to be overridden in child components in order to provide a search
+ * domain if needed.
+ * @private
+ */
+ _getSearchDomain: function () {
+ return [];
+ },
+ /**
+ * Fetches the data.
+ * @private
+ */
+ _fetchData: function () {
+ if (this._isConfigComplete()) {
+ return this._rpc(
+ {
+ 'route': '/website/snippet/filters',
+ 'params': {
+ 'filter_id': parseInt(this.$el.get(0).dataset.filterId),
+ 'template_key': this.$el.get(0).dataset.templateKey,
+ 'limit': parseInt(this.$el.get(0).dataset.numberOfRecords),
+ 'search_domain': this._getSearchDomain()
+ },
+ })
+ .then(
+ (data) => {
+ this.$data = $(data).filter((index, node) => node.nodeType === 1);
+ }
+ );
+ } else {
+ return new Promise((resolve) => {
+ this.$data = $();
+ resolve();
+ });
+ }
+ },
+ /**
+ *
+ * @private
+ */
+ _manageWarningMessageVisibility: async function () {
+ this.$el.find('.missing_option_warning').toggleClass(
+ 'd-none',
+ this._isConfigComplete()
+ );
+ },
+ /**
+ * Method to be overridden in child components in order to prepare content
+ * before rendering.
+ * @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 = '';
+ }
+ },
+ /**
+ * Method to be overridden in child components in order to prepare QWeb
+ * options.
+ * @private
+ */
+ _getQWebRenderOptions: function () {
+ return {
+ chunkSize: parseInt(
+ config.device.isMobile
+ ? this.$target[0].dataset.numberOfElementsSmallDevices
+ : this.$target[0].dataset.numberOfElements
+ ),
+ data: this.$data.map((index, data) => data.outerHTML),
+ uniqueId: this.uniqueId
+ };
+ },
+ /**
+ *
+ * @private
+ */
+ _render: function () {
+ if (this.$data.length > 0) {
+ this._prepareContent();
+ } else {
+ this.renderedContent = '';
+ }
+ this._renderContent();
+ },
+ /**
+ *
+ * @private
+ */
+ _renderContent: function () {
+ this.$el.find('.dynamic_snippet_template').html(this.renderedContent);
+ },
+ /**
+ *
+ * @param {Boolean} enable
+ * @private
+ */
+ _setupCallToAction: function (enable) {
+ if (enable === true) {
+ this.$el.on('click.s_dynamic_snippet', '*[data-url]', function (ev) {
+ if (this.editableMode !== true) {
+ window.location = $(ev.currentTarget).attr('data-url');
+ }
+ }.bind(this));
+ } else {
+ this.$el.off('.s_dynamic_snippet');
+ }
+ },
+ /**
+ *
+ * @param {Boolean} enable
+ * @private
+ */
+ _setupSizeChangedManagement: function (enable) {
+ if (enable === true) {
+ config.device.bus.on('size_changed', this, this._onSizeChanged);
+ } else {
+ config.device.bus.off('size_changed', this, this._onSizeChanged);
+ }
+ },
+ /**
+ *
+ * @param visible
+ * @private
+ */
+ _toggleVisibility: function (visible) {
+ this.$el.toggleClass('d-none', !visible);
+ },
+
+ //------------------------------------- -------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Called when the size has reached a new bootstrap breakpoint.
+ *
+ * @private
+ * @param {number} size as Integer @see web.config.device.SIZES
+ */
+ _onSizeChanged: function (size) {
+ if (this.isDesplayedAsMobile !== config.device.isMobile) {
+ this.isDesplayedAsMobile = config.device.isMobile;
+ this._render();
+ }
+ },
+});
+
+publicWidget.registry.dynamic_snippet = DynamicSnippet;
+
+return DynamicSnippet;
+
+});
diff --git a/addons/website/static/src/snippets/s_dynamic_snippet/000.scss b/addons/website/static/src/snippets/s_dynamic_snippet/000.scss
new file mode 100644
index 00000000000..f463eecbe4b
--- /dev/null
+++ b/addons/website/static/src/snippets/s_dynamic_snippet/000.scss
@@ -0,0 +1,16 @@
+body:not(.editor_enable) {
+ .s_dynamic {
+ *[data-url] {
+ cursor: pointer;
+ }
+ }
+}
+
+.s_dynamic {
+ .card-img-top {
+ height: 12rem;
+ }
+ img {
+ object-fit: scale-down;
+ }
+}
diff --git a/addons/website/static/src/snippets/s_dynamic_snippet/000.xml b/addons/website/static/src/snippets/s_dynamic_snippet/000.xml
new file mode 100644
index 00000000000..105078e087f
--- /dev/null
+++ b/addons/website/static/src/snippets/s_dynamic_snippet/000.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+ <t t-name="website.s_dynamic_snippet.grid">
+ <!-- Content -->
+ <t t-set="colClass" t-value="'col-' + (12 / chunkSize).toString()"/>
+ <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">
+ <t t-foreach="colIndexGenerator" t-as="colIndex">
+ <t t-if="(rowIndex * chunkSize + colIndex) &lt; data.length">
+ <div t-attf-class="#{colClass}">
+ <t t-raw="data[rowIndex * chunkSize + colIndex]"/>
+ </div>
+ </t>
+ </t>
+ </div>
+ </t>
+ </t>
+</templates>
diff --git a/addons/website/static/src/snippets/s_dynamic_snippet/options.js b/addons/website/static/src/snippets/s_dynamic_snippet/options.js
new file mode 100644
index 00000000000..4c713703eb9
--- /dev/null
+++ b/addons/website/static/src/snippets/s_dynamic_snippet/options.js
@@ -0,0 +1,142 @@
+odoo.define('website.s_dynamic_snippet_options', function (require) {
+'use strict';
+
+const options = require('web_editor.snippets.options');
+
+const dynamicSnippetOptions = options.Class.extend({
+
+ /**
+ *
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ this.dynamicFilters = {};
+ this.dynamicFilterTemplates = {};
+ },
+ /**
+ *
+ * @override
+ */
+ onBuilt: function () {
+ this._setOptionsDefaultValues();
+ },
+
+ //--------------------------------------------------------------------------
+ // Options
+ //--------------------------------------------------------------------------
+
+ /**
+ *
+ * @see this.selectClass for parameters
+ */
+ selectDataAttribute: function (previewMode, widgetValue, params) {
+ this._super.apply(this, arguments);
+ if (params.attributeName === 'filterId' && previewMode === false) {
+ this.$target.get(0).dataset.numberOfRecords = this.dynamicFilters[parseInt(widgetValue)].limit;
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Fetches dynamic filters.
+ * @private
+ * @returns {Promise}
+ */
+ _fetchDynamicFilters: function () {
+ return this._rpc({
+ model: 'website.snippet.filter',
+ method: 'search_read',
+ kwargs: {
+ domain: [],
+ fields: ['id', 'name', 'limit'],
+ }
+ });
+ },
+ /**
+ * Fetch dynamic filters templates.
+ * @private
+ * @returns {Promise}
+ */
+ _fetchDynamicFilterTemplates: function () {
+ return this._rpc({route: '/website/snippet/filter_templates'});
+ },
+ /**
+ *
+ * @override
+ * @private
+ */
+ _renderCustomXML: function (uiFragment) {
+ return Promise.all([this._renderDynamicFiltersSelector(uiFragment), this._renderDynamicFilterTemplatesSelector(uiFragment)]);
+ },
+ /**
+ * Renders the dynamic filter option selector content into the provided uiFragment.
+ * @param {HTMLElement} uiFragment
+ * @private
+ */
+ _renderDynamicFiltersSelector: async function (uiFragment) {
+ const dynamicFilters = await this._fetchDynamicFilters();
+ for (let index in dynamicFilters) {
+ this.dynamicFilters[dynamicFilters[index].id] = dynamicFilters[index];
+ }
+ const filtersSelectorEl = uiFragment.querySelector('[data-name="filter_opt"]');
+ return this._renderSelectUserValueWidgetButtons(filtersSelectorEl, this.dynamicFilters);
+ },
+ /**
+ * Renders we-buttons into a SelectUserValueWidget element according to provided data.
+ * @param {HTMLElement} selectUserValueWidgetElement the SelectUserValueWidget buttons
+ * have to be created into.
+ * @param {JSON} data
+ * @private
+ */
+ _renderSelectUserValueWidgetButtons: async function (selectUserValueWidgetElement, data) {
+ for (let id in data) {
+ const button = document.createElement('we-button');
+ button.dataset.selectDataAttribute = id;
+ button.innerHTML = data[id].name;
+ selectUserValueWidgetElement.appendChild(button);
+ }
+ },
+ /**
+ * Renders the template option selector content into the provided uiFragment.
+ * @param {HTMLElement} uiFragment
+ * @private
+ */
+ _renderDynamicFilterTemplatesSelector: async function (uiFragment) {
+ const dynamicFilterTemplates = await this._fetchDynamicFilterTemplates();
+ for (let index in dynamicFilterTemplates) {
+ this.dynamicFilterTemplates[dynamicFilterTemplates[index].key] = dynamicFilterTemplates[index];
+ }
+ const templatesSelectorEl = uiFragment.querySelector('[data-name="template_opt"]');
+ return this._renderSelectUserValueWidgetButtons(templatesSelectorEl, this.dynamicFilterTemplates);
+ },
+ /**
+ * Sets default options values.
+ * Method to be overridden in child components in order to set additional
+ * options default values.
+ * @private
+ */
+ _setOptionsDefaultValues: function () {
+ this._setOptionValue('numberOfElements', 4);
+ this._setOptionValue('numberOfElementsSmallDevices', 1);
+ },
+ /**
+ * Sets the option value.
+ * @param optionName
+ * @param value
+ * @private
+ */
+ _setOptionValue: function (optionName, value) {
+ if (this.$target.get(0).dataset[optionName] === undefined) {
+ this.$target.get(0).dataset[optionName] = value;
+ }
+ },
+});
+
+options.registry.dynamic_snippet = dynamicSnippetOptions;
+
+return dynamicSnippetOptions;
+});
diff --git a/addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.js b/addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.js
new file mode 100644
index 00000000000..cf43100a8c9
--- /dev/null
+++ b/addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.js
@@ -0,0 +1,46 @@
+odoo.define('website.s_dynamic_snippet_carousel', function (require) {
+'use strict';
+
+const config = require('web.config');
+const core = require('web.core');
+const publicWidget = require('web.public.widget');
+const DynamicSnippet = require('website.s_dynamic_snippet');
+
+const DynamicSnippetCarousel = DynamicSnippet.extend({
+ selector: '.s_dynamic_snippet_carousel',
+ xmlDependencies: (DynamicSnippet.prototype.xmlDependencies || []).concat(
+ ['/website/static/src/snippets/s_dynamic_snippet_carousel/000.xml']
+ ),
+
+ /**
+ *
+ * @override
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ this.template_key = 'website.s_dynamic_snippet.carousel';
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Method to be overridden in child components in order to prepare QWeb
+ * options
+ * @private
+ */
+ _getQWebRenderParams: function () {
+ return Object.assign(
+ this._super.apply(this, arguments),
+ {
+ interval : parseInt(this.$target[0].dataset.carouselInterval),
+ },
+ );
+ },
+
+});
+publicWidget.registry.dynamic_snippet_carousel = DynamicSnippetCarousel;
+
+return DynamicSnippetCarousel;
+});
diff --git a/addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.scss b/addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.scss
new file mode 100644
index 00000000000..dee896d1468
--- /dev/null
+++ b/addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.scss
@@ -0,0 +1,13 @@
+.s_dynamic {
+ .carousel-control-prev, .carousel-control-next {
+ position: absolute;
+ text-shadow: 0px 0px 3px #343A40;
+ width: 4rem;
+ }
+ .carousel-control-prev{
+ left: -4rem
+ }
+ .carousel-control-next{
+ right: -4rem
+ }
+}
diff --git a/addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.xml b/addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.xml
new file mode 100644
index 00000000000..ca6cbf4d419
--- /dev/null
+++ b/addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<templates xml:space="preserve">
+ <t t-name="website.s_dynamic_snippet.carousel">
+ <div t-att-id="uniqueId" class="carousel slide" t-att-data-interval="interval">
+ <!-- Content -->
+ <div class="carousel-inner row w-100 mx-auto" role="listbox">
+ <t t-set="colClass" t-value="'col-' + (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-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-raw="data[slideIndex * chunkSize + itemIndex]"/>
+ </div>
+ </t>
+ </t>
+ </div>
+ </div>
+ </t>
+ </div>
+ <!-- Controls -->
+ <a t-attf-href="##{uniqueId}" class="carousel-control-prev" data-slide="prev" role="button" aria-label="Previous" title="Previous">
+ <span class="fa fa-chevron-left fa-2x text-white"/>
+ <span class="sr-only">Previous</span>
+ </a>
+ <a t-attf-href="##{uniqueId}" class="carousel-control-next" data-slide="next" role="button" aria-label="Next" title="Next">
+ <span class="fa fa-chevron-right fa-2x text-white"/>
+ <span class="sr-only">Next</span>
+ </a>
+ </div>
+ </t>
+</templates>
diff --git a/addons/website/static/src/snippets/s_dynamic_snippet_carousel/options.js b/addons/website/static/src/snippets/s_dynamic_snippet_carousel/options.js
new file mode 100644
index 00000000000..cb9a3cf566a
--- /dev/null
+++ b/addons/website/static/src/snippets/s_dynamic_snippet_carousel/options.js
@@ -0,0 +1,28 @@
+odoo.define('website.s_dynamic_snippet_carousel_options', function (require) {
+'use strict';
+
+const options = require('web_editor.snippets.options');
+const s_dynamic_snippet_options = require('website.s_dynamic_snippet_options');
+
+const dynamicSnippetCarouselOptions = s_dynamic_snippet_options.extend({
+
+ //--------------------------------------------------------------------------
+ // Options
+ //--------------------------------------------------------------------------
+
+ /**
+ *
+ * @override
+ * @private
+ */
+ _setOptionsDefaultValues: function () {
+ this._super.apply(this, arguments);
+ this._setOptionValue('carouselInterval', '5000');
+ }
+
+});
+
+options.registry.dynamic_snippet_carousel = dynamicSnippetCarouselOptions;
+
+return dynamicSnippetCarouselOptions;
+});