From 3751379f1e9a4c215fb6eb898b4ccc67659b9ace Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Tue, 10 May 2022 21:51:50 +0700 Subject: initial commit 2 --- .../static/src/snippets/s_dynamic_snippet/000.js | 244 +++++++++++++++++++++ .../static/src/snippets/s_dynamic_snippet/000.scss | 11 + .../static/src/snippets/s_dynamic_snippet/000.xml | 20 ++ .../src/snippets/s_dynamic_snippet/options.js | 136 ++++++++++++ 4 files changed, 411 insertions(+) create mode 100644 addons/website/static/src/snippets/s_dynamic_snippet/000.js create mode 100644 addons/website/static/src/snippets/s_dynamic_snippet/000.scss create mode 100644 addons/website/static/src/snippets/s_dynamic_snippet/000.xml create mode 100644 addons/website/static/src/snippets/s_dynamic_snippet/options.js (limited to 'addons/website/static/src/snippets/s_dynamic_snippet') 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 00000000..d6a3e0ff --- /dev/null +++ b/addons/website/static/src/snippets/s_dynamic_snippet/000.js @@ -0,0 +1,244 @@ +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'], + read_events: { + 'click [data-url]': '_onCallToAction', + }, + 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._setupSizeChangedManagement(true); + this._render(); + this._toggleVisibility(true); + }); + }, + /** + * + * @override + */ + destroy: function () { + this._toggleVisibility(false); + this._setupSizeChangedManagement(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; + } + ); + } else { + return new Promise((resolve) => { + this.data = []; + resolve(); + }); + } + }, + /** + * + * @private + */ + _mustMessageWarningBeHidden: function() { + return this._isConfigComplete() || !this.editableMode; + }, + /** + * + * @private + */ + _manageWarningMessageVisibility: async function () { + this.$el.find('.missing_option_warning').toggleClass( + 'd-none', + this._mustMessageWarningBeHidden() + ); + }, + /** + * 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, + uniqueId: this.uniqueId + }; + }, + /** + * + * @private + */ + _render: function () { + if (this.data.length) { + this._prepareContent(); + } else { + this.renderedContent = ''; + } + this._renderContent(); + }, + /** + * + * @private + */ + _renderContent: function () { + this.$el.find('.dynamic_snippet_template').html(this.renderedContent); + }, + /** + * + * @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 + //-------------------------------------------------------------------------- + + /** + * Navigates to the call to action url. + * @private + */ + _onCallToAction: function (ev) { + window.location = $(ev.currentTarget).attr('data-url'); + }, + /** + * 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 00000000..536aba85 --- /dev/null +++ b/addons/website/static/src/snippets/s_dynamic_snippet/000.scss @@ -0,0 +1,11 @@ +.s_dynamic { + [data-url] { + cursor: pointer; + } + .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 00000000..105078e0 --- /dev/null +++ b/addons/website/static/src/snippets/s_dynamic_snippet/000.xml @@ -0,0 +1,20 @@ + + + + + + + + +
+ + +
+ +
+
+
+
+
+
+
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 00000000..2cbdcb1c --- /dev/null +++ b/addons/website/static/src/snippets/s_dynamic_snippet/options.js @@ -0,0 +1,136 @@ +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({route: '/website/snippet/options_filters'}); + }, + /** + * Fetch dynamic filters templates. + * @private + * @returns {Promise} + */ + _fetchDynamicFilterTemplates: function () { + return this._rpc({route: '/website/snippet/filter_templates'}); + }, + /** + * + * @override + * @private + */ + _renderCustomXML: async function (uiFragment) { + await this._renderDynamicFiltersSelector(uiFragment); + await 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; +}); -- cgit v1.2.3