From 0e7640b5f22d2bea04bbe22d3189cff7e03af545 Mon Sep 17 00:00:00 2001 From: "Laurent Stukkens (LTU)" Date: Wed, 17 Jun 2020 14:48:40 +0000 Subject: [PATCH] [IMP] website, website_sale: add dynamic snippets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implement new snippets that allow the user to choose a filter and a template. Three snippets have been created: - Dynamic Snippet: Displays the data in a grid format - Dynamic Carousel: Displays the date in a carousel - Dynamic Products: Let the user pick a product category and displays the products in a carousel task-2276740 PR #53175 Signed-off-by: Jérémy Kersten (jke) --- addons/website/__manifest__.py | 2 + addons/website/controllers/main.py | 15 ++ addons/website/data/website_data.xml | 64 +++++ addons/website/models/__init__.py | 1 + addons/website/models/ir_actions.py | 2 + .../website/models/website_snippet_filter.py | 116 +++++++++ addons/website/security/ir.model.access.csv | 2 + .../src/snippets/s_dynamic_snippet/000.js | 245 ++++++++++++++++++ .../src/snippets/s_dynamic_snippet/000.scss | 16 ++ .../src/snippets/s_dynamic_snippet/000.xml | 20 ++ .../src/snippets/s_dynamic_snippet/options.js | 142 ++++++++++ .../s_dynamic_snippet_carousel/000.js | 46 ++++ .../s_dynamic_snippet_carousel/000.scss | 13 + .../s_dynamic_snippet_carousel/000.xml | 35 +++ .../s_dynamic_snippet_carousel/options.js | 28 ++ addons/website/views/assets.xml | 2 + .../views/snippets/s_dynamic_snippet.xml | 78 ++++++ .../snippets/s_dynamic_snippet_carousel.xml | 37 +++ addons/website/views/snippets/snippets.xml | 3 + .../website_blog/data/website_blog_data.xml | 1 + .../website_blog/data/website_blog_demo.xml | 3 + addons/website_sale/__manifest__.py | 3 +- addons/website_sale/data/data.xml | 49 ++++ .../s_dynamic_snippet_products/000.js | 41 +++ .../s_dynamic_snippet_products/000.xml | 8 + .../s_dynamic_snippet_products/options.js | 93 +++++++ .../snippets/s_dynamic_snippet_products.xml | 23 ++ .../views/{ => snippets}/snippets.xml | 5 +- addons/website_sale/views/templates.xml | 1 + 29 files changed, 1092 insertions(+), 2 deletions(-) create mode 100644 addons/website/models/website_snippet_filter.py 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 create mode 100644 addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.js create mode 100644 addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.scss create mode 100644 addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.xml create mode 100644 addons/website/static/src/snippets/s_dynamic_snippet_carousel/options.js create mode 100644 addons/website/views/snippets/s_dynamic_snippet.xml create mode 100644 addons/website/views/snippets/s_dynamic_snippet_carousel.xml create mode 100644 addons/website_sale/static/src/snippets/s_dynamic_snippet_products/000.js create mode 100644 addons/website_sale/static/src/snippets/s_dynamic_snippet_products/000.xml create mode 100644 addons/website_sale/static/src/snippets/s_dynamic_snippet_products/options.js create mode 100644 addons/website_sale/views/snippets/s_dynamic_snippet_products.xml rename addons/website_sale/views/{ => snippets}/snippets.xml (95%) diff --git a/addons/website/__manifest__.py b/addons/website/__manifest__.py index d121fbbc358..085a4aabe00 100644 --- a/addons/website/__manifest__.py +++ b/addons/website/__manifest__.py @@ -75,6 +75,8 @@ 'views/snippets/s_mega_menu_multi_menus.xml', 'views/snippets/s_mega_menu_menu_image_menu.xml', 'views/snippets/s_google_map.xml', + 'views/snippets/s_dynamic_snippet.xml', + 'views/snippets/s_dynamic_snippet_carousel.xml', 'views/website_views.xml', 'views/website_visitor_views.xml', 'views/res_config_settings_views.xml', diff --git a/addons/website/controllers/main.py b/addons/website/controllers/main.py index d5f242e02f9..82c26ca0344 100644 --- a/addons/website/controllers/main.py +++ b/addons/website/controllers/main.py @@ -269,6 +269,21 @@ class Website(Home): ] } + @http.route('/website/snippet/filters', type='json', auth='public', website=True) + def get_dynamic_filter(self, filter_id, template_key, limit=None, search_domain=None): + dynamic_filter = request.env['website.snippet.filter'].sudo().search( + [('id', '=', filter_id)] + request.website.website_domain() + ) + return dynamic_filter and dynamic_filter.render(template_key, limit, search_domain) or '' + + @http.route('/website/snippet/filter_templates', type='json', auth='public', website=True) + def get_dynamic_snippet_templates(self, filter_id=False): + # todo: if filter_id.model -> filter template + templates = request.env['ir.ui.view'].sudo().search_read( + [['key', 'ilike', '.dynamic_filter_template_'], ['type', '=', 'qweb']], ['key', 'name'] + ) + return templates + # ------------------------------------------------------ # Edit # ------------------------------------------------------ diff --git a/addons/website/data/website_data.xml b/addons/website/data/website_data.xml index b8462a17299..af1d1272ccb 100644 --- a/addons/website/data/website_data.xml +++ b/addons/website/data/website_data.xml @@ -698,6 +698,70 @@ url /website/static/src/img/snippets_demo/s_popup.jpg + + + + Countries + res.country + + + + + + + + name,code,image,phone_code + + + Countries + + + diff --git a/addons/website/models/__init__.py b/addons/website/models/__init__.py index b66ecb50a24..5df75fb65c0 100644 --- a/addons/website/models/__init__.py +++ b/addons/website/models/__init__.py @@ -23,3 +23,4 @@ from . import res_config_settings from . import res_lang from . import theme_models from . import website_visitor +from . import website_snippet_filter diff --git a/addons/website/models/ir_actions.py b/addons/website/models/ir_actions.py index e3fbf2a6761..24fbc937514 100644 --- a/addons/website/models/ir_actions.py +++ b/addons/website/models/ir_actions.py @@ -4,6 +4,7 @@ from werkzeug import urls from odoo import api, fields, models from odoo.http import request +from odoo.tools.json import scriptsafe as json_scriptsafe class ServerAction(models.Model): @@ -48,6 +49,7 @@ class ServerAction(models.Model): eval_context = super(ServerAction, self)._get_eval_context(action) if action.state == 'code': eval_context['request'] = request + eval_context['json'] = json_scriptsafe return eval_context @api.model diff --git a/addons/website/models/website_snippet_filter.py b/addons/website/models/website_snippet_filter.py new file mode 100644 index 00000000000..89fb3bd0817 --- /dev/null +++ b/addons/website/models/website_snippet_filter.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- + +from ast import literal_eval +from collections import OrderedDict +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError +from odoo.osv import expression + + +class WebsiteSnippetFilter(models.Model): + _name = 'website.snippet.filter' + _inherit = ['website.published.multi.mixin'] + _description = 'Website Snippet Filter' + _order = 'name ASC' + + name = fields.Char(required=True) + action_server_id = fields.Many2one('ir.actions.server', 'Server Action', ondelete='cascade') + field_names = fields.Char(help="A list of comma-separated field names", required=True) + filter_id = fields.Many2one('ir.filters', 'Filter', ondelete='cascade') + limit = fields.Integer(help='The limit is the maximum number of records retrieved', required=True) + website_id = fields.Many2one('website', string='Website', ondelete='cascade', required=True) + + @api.constrains('action_server_id', 'filter_id') + def _check_data_source_is_provided(self): + for record in self: + if bool(record.action_server_id) == bool(record.filter_id): + raise ValidationError(_("Either action_server_id or filter_id must be provided.")) + + @api.constrains('limit') + def _check_limit(self): + """Limit must be between 1 and 16.""" + for record in self: + if not 0 < record.limit <= 16: + raise ValidationError(_("The limit must be between 1 and 16.")) + + @api.constrains('field_names') + def _check_field_names(self): + for record in self: + for field_name in record.field_names.split(","): + if not field_name.strip(): + raise ValidationError(_("Empty field name in %r") % (record.field_names)) + + def render(self, template_key, limit, search_domain=[]): + """Renders the website dynamic snippet items""" + self.ensure_one() + assert '.dynamic_filter_template_' in template_key, _("You can only use template prefixed by dynamic_filter_template_ ") + + if self.env['website'].get_current_website() != self.website_id: + return '' + + records = self._prepare_values(limit, search_domain) + return self.env['ir.ui.view'].sudo().with_context(inherit_branding=False)._render_template(template_key, dict(records=records)) + + def _prepare_values(self, limit=None, search_domain=[]): + """Gets the data and returns it the right format for render.""" + self.ensure_one() + limit = limit and min(limit, self.limit) or self.limit + if self.filter_id: + filter_sudo = self.filter_id.sudo() + domain = filter_sudo._get_eval_domain() + if 'is_published' in self.env[filter_sudo.model_id]: + domain = expression.AND([domain, [('is_published', '=', True)]]) + if search_domain: + domain = expression.AND([domain, search_domain]), + + records = self.env[filter_sudo.model_id].search( + domain, + order=','.join(literal_eval(filter_sudo.sort)) or None, + limit=limit + ) + elif self.action_server_id: + records = self.action_server_id.with_context( + dynamic_filter=self, + limit=limit, + search_domain=search_domain, + ).sudo().run() + + return self._filter_records_to_dict_values(records) + + def _filter_records_to_dict_values(self, records): + """Extract the fields from the data source and put them into a dictionary of values + + [{ + 'fields': + OrderedDict([ + ('name', 'Afghanistan'), + ('code', 'AF'), + ]), + 'image_fields': + OrderedDict([ + ('image', '/web/image/res.country/3/image?unique=5d9b44e') + ]), + }, ... , ...] + + """ + + self.ensure_one() + values = [] + model = self.env[self.filter_id.model_id] if self.filter_id else self.env[self.action_server_id.model_id.model] + for record in records: + data = { + 'fields': OrderedDict({}), + 'image_fields': OrderedDict({}), + } + + for field_name in self.field_names.split(","): + field = model._fields.get(field_name) + data_type = 'image_fields' if field.type == 'binary' else 'fields' + if data_type == 'image_fields' and self.filter_id: + data[data_type][field_name] = self.env['website'].image_url(record, field_name) + else: + data[data_type][field_name] = record[field_name] + + data['fields']['call_to_action_url'] = 'website_url' in record and record['website_url'] + values.append(data) + return values diff --git a/addons/website/security/ir.model.access.csv b/addons/website/security/ir.model.access.csv index 163602f4d2d..e15f55b4c85 100644 --- a/addons/website/security/ir.model.access.csv +++ b/addons/website/security/ir.model.access.csv @@ -21,3 +21,5 @@ access_theme_ir_attachment,access_theme_ir_attachment,model_theme_ir_attachment, access_theme_website_menu,access_theme_website_menu,model_theme_website_menu,base.group_system,1,1,1,1 access_theme_website_page,access_theme_website_page,model_theme_website_page,base.group_system,1,1,1,1 access_website_robots,access.website.robots,model_website_robots,website.group_website_designer,1,1,1,0 +access_website_dynamic_filter_public,access_website_dynamic_filter,model_website_snippet_filter,,0,0,0,0 +access_website_dynamic_filter_system,access_website_dynamic_filter,model_website_snippet_filter,base.group_system,1,1,1,1 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 @@ + + + + + + + + +
+ + +
+ +
+
+
+
+
+
+
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 @@ + + + + + + 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; +}); diff --git a/addons/website/views/assets.xml b/addons/website/views/assets.xml index a2776696f85..b4c46e11a13 100644 --- a/addons/website/views/assets.xml +++ b/addons/website/views/assets.xml @@ -139,6 +139,8 @@