From 3751379f1e9a4c215fb6eb898b4ccc67659b9ace Mon Sep 17 00:00:00 2001 From: stephanchrst Date: Tue, 10 May 2022 21:51:50 +0700 Subject: initial commit 2 --- .../website/static/src/snippets/s_alert/000.scss | 30 ++ .../website/static/src/snippets/s_badge/000.scss | 10 + .../static/src/snippets/s_badge/000_variables.scss | 4 + .../static/src/snippets/s_blockquote/000.scss | 73 ++++ .../static/src/snippets/s_blockquote/options.js | 46 ++ addons/website/static/src/snippets/s_btn/000.scss | 6 + addons/website/static/src/snippets/s_card/000.scss | 12 + addons/website/static/src/snippets/s_chart/000.js | 142 ++++++ .../website/static/src/snippets/s_chart/options.js | 477 ++++++++++++++++++++ .../static/src/snippets/s_color_blocks_2/000.scss | 36 ++ .../static/src/snippets/s_company_team/000.scss | 8 + .../static/src/snippets/s_comparisons/000.scss | 21 + .../website/static/src/snippets/s_countdown/000.js | 422 ++++++++++++++++++ .../static/src/snippets/s_countdown/000.xml | 29 ++ .../static/src/snippets/s_countdown/options.js | 135 ++++++ .../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 ++++++ .../src/snippets/s_dynamic_snippet_carousel/000.js | 46 ++ .../snippets/s_dynamic_snippet_carousel/000.scss | 11 + .../snippets/s_dynamic_snippet_carousel/000.xml | 35 ++ .../snippets/s_dynamic_snippet_carousel/options.js | 28 ++ .../static/src/snippets/s_facebook_page/000.js | 56 +++ .../static/src/snippets/s_facebook_page/options.js | 157 +++++++ .../static/src/snippets/s_faq_collapse/000.scss | 35 ++ .../static/src/snippets/s_features_grid/000.scss | 13 + .../static/src/snippets/s_google_map/000.js | 96 ++++ .../static/src/snippets/s_google_map/000.scss | 42 ++ .../snippets/s_google_map/img/thumbs/map-blue.jpg | Bin 0 -> 2617 bytes .../snippets/s_google_map/img/thumbs/map-bw.jpg | Bin 0 -> 4105 bytes .../s_google_map/img/thumbs/map-caramello.jpg | Bin 0 -> 2688 bytes .../s_google_map/img/thumbs/map-cobalt.jpg | Bin 0 -> 2966 bytes .../s_google_map/img/thumbs/map-cupertino.jpg | Bin 0 -> 3443 bytes .../s_google_map/img/thumbs/map-default.jpg | Bin 0 -> 3440 bytes .../snippets/s_google_map/img/thumbs/map-flat.jpg | Bin 0 -> 3109 bytes .../s_google_map/img/thumbs/map-lightMono.jpg | Bin 0 -> 4149 bytes .../snippets/s_google_map/img/thumbs/map-lilla.jpg | Bin 0 -> 2642 bytes .../snippets/s_google_map/img/thumbs/map-retro.jpg | Bin 0 -> 4055 bytes .../static/src/snippets/s_google_map/options.js | 56 +++ addons/website/static/src/snippets/s_hr/000.scss | 11 + .../static/src/snippets/s_image_gallery/000.js | 180 ++++++++ .../static/src/snippets/s_image_gallery/000.scss | 155 +++++++ .../static/src/snippets/s_image_gallery/000.xml | 68 +++ .../static/src/snippets/s_image_gallery/001.scss | 281 ++++++++++++ .../static/src/snippets/s_image_gallery/options.js | 481 +++++++++++++++++++++ .../static/src/snippets/s_masonry_block/000.scss | 93 ++++ .../snippets/s_masonry_block/000_variables.scss | 4 + .../static/src/snippets/s_masonry_block/001.scss | 5 + .../static/src/snippets/s_media_list/000.scss | 27 ++ .../static/src/snippets/s_media_list/001.scss | 11 + .../static/src/snippets/s_media_list/options.js | 50 +++ addons/website/static/src/snippets/s_popup/000.js | 119 +++++ .../website/static/src/snippets/s_popup/000.scss | 99 +++++ .../website/static/src/snippets/s_popup/001.scss | 72 +++ .../website/static/src/snippets/s_popup/options.js | 114 +++++ .../static/src/snippets/s_process_steps/000.scss | 52 +++ .../static/src/snippets/s_product_catalog/001.scss | 29 ++ .../src/snippets/s_product_catalog/options.js | 66 +++ .../static/src/snippets/s_product_list/000.scss | 45 ++ .../src/snippets/s_product_list/000_variables.scss | 1 + .../static/src/snippets/s_progress_bar/options.js | 80 ++++ .../static/src/snippets/s_quotes_carousel/000.scss | 22 + .../static/src/snippets/s_quotes_carousel/001.scss | 8 + .../website/static/src/snippets/s_rating/000.scss | 55 +++ .../website/static/src/snippets/s_rating/001.scss | 15 + .../static/src/snippets/s_rating/options.js | 151 +++++++ .../static/src/snippets/s_references/000.scss | 4 + addons/website/static/src/snippets/s_share/000.js | 49 +++ .../website/static/src/snippets/s_share/000.scss | 65 +++ .../static/src/snippets/s_showcase/000.scss | 77 ++++ .../static/src/snippets/s_showcase/001.scss | 46 ++ .../static/src/snippets/s_showcase/options.js | 19 + .../static/src/snippets/s_table_of_content/000.js | 77 ++++ .../src/snippets/s_table_of_content/000.scss | 65 +++ .../src/snippets/s_table_of_content/options.js | 122 ++++++ addons/website/static/src/snippets/s_tabs/001.scss | 25 ++ .../website/static/src/snippets/s_tabs/options.js | 167 +++++++ .../static/src/snippets/s_text_highlight/000.scss | 7 + .../static/src/snippets/s_three_columns/000.scss | 5 + .../static/src/snippets/s_timeline/000.scss | 70 +++ .../static/src/snippets/s_timeline/options.js | 32 ++ .../website/static/src/snippets/s_title/000.scss | 36 ++ 83 files changed, 5597 insertions(+) create mode 100644 addons/website/static/src/snippets/s_alert/000.scss create mode 100644 addons/website/static/src/snippets/s_badge/000.scss create mode 100644 addons/website/static/src/snippets/s_badge/000_variables.scss create mode 100644 addons/website/static/src/snippets/s_blockquote/000.scss create mode 100644 addons/website/static/src/snippets/s_blockquote/options.js create mode 100644 addons/website/static/src/snippets/s_btn/000.scss create mode 100644 addons/website/static/src/snippets/s_card/000.scss create mode 100644 addons/website/static/src/snippets/s_chart/000.js create mode 100644 addons/website/static/src/snippets/s_chart/options.js create mode 100644 addons/website/static/src/snippets/s_color_blocks_2/000.scss create mode 100644 addons/website/static/src/snippets/s_company_team/000.scss create mode 100644 addons/website/static/src/snippets/s_comparisons/000.scss create mode 100644 addons/website/static/src/snippets/s_countdown/000.js create mode 100644 addons/website/static/src/snippets/s_countdown/000.xml create mode 100644 addons/website/static/src/snippets/s_countdown/options.js 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/static/src/snippets/s_facebook_page/000.js create mode 100644 addons/website/static/src/snippets/s_facebook_page/options.js create mode 100644 addons/website/static/src/snippets/s_faq_collapse/000.scss create mode 100644 addons/website/static/src/snippets/s_features_grid/000.scss create mode 100644 addons/website/static/src/snippets/s_google_map/000.js create mode 100644 addons/website/static/src/snippets/s_google_map/000.scss create mode 100644 addons/website/static/src/snippets/s_google_map/img/thumbs/map-blue.jpg create mode 100644 addons/website/static/src/snippets/s_google_map/img/thumbs/map-bw.jpg create mode 100644 addons/website/static/src/snippets/s_google_map/img/thumbs/map-caramello.jpg create mode 100644 addons/website/static/src/snippets/s_google_map/img/thumbs/map-cobalt.jpg create mode 100644 addons/website/static/src/snippets/s_google_map/img/thumbs/map-cupertino.jpg create mode 100644 addons/website/static/src/snippets/s_google_map/img/thumbs/map-default.jpg create mode 100644 addons/website/static/src/snippets/s_google_map/img/thumbs/map-flat.jpg create mode 100644 addons/website/static/src/snippets/s_google_map/img/thumbs/map-lightMono.jpg create mode 100644 addons/website/static/src/snippets/s_google_map/img/thumbs/map-lilla.jpg create mode 100644 addons/website/static/src/snippets/s_google_map/img/thumbs/map-retro.jpg create mode 100644 addons/website/static/src/snippets/s_google_map/options.js create mode 100644 addons/website/static/src/snippets/s_hr/000.scss create mode 100644 addons/website/static/src/snippets/s_image_gallery/000.js create mode 100644 addons/website/static/src/snippets/s_image_gallery/000.scss create mode 100644 addons/website/static/src/snippets/s_image_gallery/000.xml create mode 100644 addons/website/static/src/snippets/s_image_gallery/001.scss create mode 100644 addons/website/static/src/snippets/s_image_gallery/options.js create mode 100644 addons/website/static/src/snippets/s_masonry_block/000.scss create mode 100644 addons/website/static/src/snippets/s_masonry_block/000_variables.scss create mode 100644 addons/website/static/src/snippets/s_masonry_block/001.scss create mode 100644 addons/website/static/src/snippets/s_media_list/000.scss create mode 100644 addons/website/static/src/snippets/s_media_list/001.scss create mode 100644 addons/website/static/src/snippets/s_media_list/options.js create mode 100644 addons/website/static/src/snippets/s_popup/000.js create mode 100644 addons/website/static/src/snippets/s_popup/000.scss create mode 100644 addons/website/static/src/snippets/s_popup/001.scss create mode 100644 addons/website/static/src/snippets/s_popup/options.js create mode 100644 addons/website/static/src/snippets/s_process_steps/000.scss create mode 100644 addons/website/static/src/snippets/s_product_catalog/001.scss create mode 100644 addons/website/static/src/snippets/s_product_catalog/options.js create mode 100644 addons/website/static/src/snippets/s_product_list/000.scss create mode 100644 addons/website/static/src/snippets/s_product_list/000_variables.scss create mode 100644 addons/website/static/src/snippets/s_progress_bar/options.js create mode 100644 addons/website/static/src/snippets/s_quotes_carousel/000.scss create mode 100644 addons/website/static/src/snippets/s_quotes_carousel/001.scss create mode 100644 addons/website/static/src/snippets/s_rating/000.scss create mode 100644 addons/website/static/src/snippets/s_rating/001.scss create mode 100644 addons/website/static/src/snippets/s_rating/options.js create mode 100644 addons/website/static/src/snippets/s_references/000.scss create mode 100644 addons/website/static/src/snippets/s_share/000.js create mode 100644 addons/website/static/src/snippets/s_share/000.scss create mode 100644 addons/website/static/src/snippets/s_showcase/000.scss create mode 100644 addons/website/static/src/snippets/s_showcase/001.scss create mode 100644 addons/website/static/src/snippets/s_showcase/options.js create mode 100644 addons/website/static/src/snippets/s_table_of_content/000.js create mode 100644 addons/website/static/src/snippets/s_table_of_content/000.scss create mode 100644 addons/website/static/src/snippets/s_table_of_content/options.js create mode 100644 addons/website/static/src/snippets/s_tabs/001.scss create mode 100644 addons/website/static/src/snippets/s_tabs/options.js create mode 100644 addons/website/static/src/snippets/s_text_highlight/000.scss create mode 100644 addons/website/static/src/snippets/s_three_columns/000.scss create mode 100644 addons/website/static/src/snippets/s_timeline/000.scss create mode 100644 addons/website/static/src/snippets/s_timeline/options.js create mode 100644 addons/website/static/src/snippets/s_title/000.scss (limited to 'addons/website/static/src/snippets') diff --git a/addons/website/static/src/snippets/s_alert/000.scss b/addons/website/static/src/snippets/s_alert/000.scss new file mode 100644 index 00000000..a44d1d2d --- /dev/null +++ b/addons/website/static/src/snippets/s_alert/000.scss @@ -0,0 +1,30 @@ + +.s_alert { + margin: $grid-gutter-width/2 0; + border: $alert-border-width solid; + border-radius: $alert-border-radius; + p, ul, ol { + &:last-child { + margin-bottom: 0; + } + } + &_sm { + padding: $grid-gutter-width/3; + font-size: $font-size-sm; + } + &_md { + padding: $grid-gutter-width/2; + font-size: $font-size-base; + } + &_lg { + padding: $grid-gutter-width; + font-size: $font-size-lg; + } + &_icon { + float: left; + margin-right: 10px; + } + &_content { + overflow: hidden; + } +} diff --git a/addons/website/static/src/snippets/s_badge/000.scss b/addons/website/static/src/snippets/s_badge/000.scss new file mode 100644 index 00000000..acdb811e --- /dev/null +++ b/addons/website/static/src/snippets/s_badge/000.scss @@ -0,0 +1,10 @@ + +.s_badge { + padding: $s-badge-padding; + margin: $s-badge-margin; + border-radius: if($s-badge-border-radius != null, $s-badge-border-radius, $badge-border-radius); + font-size: $font-size-sm; + .fa { + margin: $s-badge-i-margin; + } +} diff --git a/addons/website/static/src/snippets/s_badge/000_variables.scss b/addons/website/static/src/snippets/s_badge/000_variables.scss new file mode 100644 index 00000000..b6bc1929 --- /dev/null +++ b/addons/website/static/src/snippets/s_badge/000_variables.scss @@ -0,0 +1,4 @@ +$s-badge-border-radius: null; +$s-badge-padding: .5rem; +$s-badge-margin: .5rem .5rem .5rem 0; +$s-badge-i-margin: 0 .3rem 0 0; diff --git a/addons/website/static/src/snippets/s_blockquote/000.scss b/addons/website/static/src/snippets/s_blockquote/000.scss new file mode 100644 index 00000000..a41d1f27 --- /dev/null +++ b/addons/website/static/src/snippets/s_blockquote/000.scss @@ -0,0 +1,73 @@ +.s_blockquote { + // Reset + border: 0; + padding: 0; + .s_blockquote_icon { + font-size: $font-size-base; + } + .s_blockquote_author { + opacity: .75; + } + // Classic + &.s_blockquote_classic { + .s_blockquote_icon { + float: left; + border-top-right-radius: 0 !important; + border-bottom-right-radius: 0 !important; + &.float-right { + border-top-left-radius: 0 !important; + border-bottom-left-radius: 0 !important; + } + } + .s_blockquote_content { + overflow: hidden; + padding: $spacer * 1.5; + .blockquote-footer { + &::before { + content: ''; + } + .s_blockquote_avatar { + max-height: $spacer * 2.5; + } + } + } + } + // Cover + &.s_blockquote_cover { + text-align: center; + .s_blockquote_icon { + position: relative; + z-index: 1; + float: none; + margin-bottom: -$spacer * 1.5; + } + p:last-of-type { + margin-bottom: $spacer * .5; + } + .s_blockquote_content, .s_blockquote_filter { // s_blockquote_filter is there for compatibility + padding: $spacer * 3 $spacer * 2 $spacer * 2; + } + // Compatibility + .s_blockquote_filter { + margin: $spacer * -3 $spacer * -2 $spacer * -2; + } + .quote_char { + margin: $spacer * 2 0 $spacer 0; + & ~ .blockquote-footer { + padding-bottom: $spacer * 2; + } + } + } + // Minimalist + &.s_blockquote_minimalist { + border-left: 5px solid; + border-color: o-color('secondary'); + .s_blockquote_content { + padding: $spacer; + @include border-right-radius($border-radius); + p:last-of-type { + margin-bottom: 0; + } + } + } +} diff --git a/addons/website/static/src/snippets/s_blockquote/options.js b/addons/website/static/src/snippets/s_blockquote/options.js new file mode 100644 index 00000000..46748f15 --- /dev/null +++ b/addons/website/static/src/snippets/s_blockquote/options.js @@ -0,0 +1,46 @@ +odoo.define('website.s_blockquote_options', function (require) { +'use strict'; + +const options = require('web_editor.snippets.options'); + +options.registry.Blockquote = options.Class.extend({ + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Change blockquote design. + * + * @see this.selectClass for parameters + */ + display: function (previewMode, widgetValue, params) { + + // Classic + this.$target.find('.s_blockquote_avatar').toggleClass('d-none', widgetValue !== 'classic'); + + // Cover + const $blockquote = this.$target.find('.s_blockquote_content'); + if (widgetValue === 'cover') { + $blockquote.css({"background-image": "url('/web/image/website.s_blockquote_cover_default_image')"}); + $blockquote.css({"background-position": "50% 50%"}); + $blockquote.addClass('oe_img_bg'); + if (!$blockquote.find('.o_we_bg_filter').length) { + const bgFilterEl = document.createElement('div'); + bgFilterEl.classList.add('o_we_bg_filter', 'bg-white-50'); + $blockquote.prepend(bgFilterEl); + } + } else { + $blockquote.css({"background-image": ""}); + $blockquote.css({"background-position": ""}); + $blockquote.removeClass('oe_img_bg'); + $blockquote.find('.o_we_bg_filter').remove(); + $blockquote.find('.s_blockquote_filter').contents().unwrap(); // Compatibility + } + + // Minimalist + this.$target.find('.s_blockquote_icon').toggleClass('d-none', widgetValue === 'minimalist'); + this.$target.find('footer').toggleClass('d-none', widgetValue === 'minimalist'); + }, +}); +}); diff --git a/addons/website/static/src/snippets/s_btn/000.scss b/addons/website/static/src/snippets/s_btn/000.scss new file mode 100644 index 00000000..4e5a3215 --- /dev/null +++ b/addons/website/static/src/snippets/s_btn/000.scss @@ -0,0 +1,6 @@ + +.s_btn { + .btn + .btn { + margin-left: .75rem; + } +} diff --git a/addons/website/static/src/snippets/s_card/000.scss b/addons/website/static/src/snippets/s_card/000.scss new file mode 100644 index 00000000..436d9a70 --- /dev/null +++ b/addons/website/static/src/snippets/s_card/000.scss @@ -0,0 +1,12 @@ + +.s_card { + margin: $grid-gutter-width/2 0; + .card-body { + // color: initial; + p, ul, ol { + &:last-child { + margin-bottom: 0; + } + } + } +} diff --git a/addons/website/static/src/snippets/s_chart/000.js b/addons/website/static/src/snippets/s_chart/000.js new file mode 100644 index 00000000..2c67bd03 --- /dev/null +++ b/addons/website/static/src/snippets/s_chart/000.js @@ -0,0 +1,142 @@ +odoo.define('website.s_chart', function (require) { +'use strict'; + +const publicWidget = require('web.public.widget'); +const weUtils = require('web_editor.utils'); + +const ChartWidget = publicWidget.Widget.extend({ + selector: '.s_chart', + disabledInEditableMode: false, + jsLibs: [ + '/web/static/lib/Chart/Chart.js', + ], + + /** + * @override + * @param {Object} parent + * @param {Object} options The default value of the chartbar. + */ + init: function (parent, options) { + this._super.apply(this, arguments); + this.style = window.getComputedStyle(document.documentElement); + }, + /** + * @override + */ + start: function () { + // Convert Theme colors to css color + const data = JSON.parse(this.el.dataset.data); + data.datasets.forEach(el => { + if (Array.isArray(el.backgroundColor)) { + el.backgroundColor = el.backgroundColor.map(el => this._convertToCssColor(el)); + el.borderColor = el.borderColor.map(el => this._convertToCssColor(el)); + } else { + el.backgroundColor = this._convertToCssColor(el.backgroundColor); + el.borderColor = this._convertToCssColor(el.borderColor); + } + el.borderWidth = this.el.dataset.borderWidth; + }); + + // Make chart data + const chartData = { + type: this.el.dataset.type, + data: data, + options: { + legend: { + display: this.el.dataset.legendPosition !== 'none', + position: this.el.dataset.legendPosition, + }, + tooltips: { + enabled: this.el.dataset.tooltipDisplay === 'true', + }, + title: { + display: !!this.el.dataset.title, + text: this.el.dataset.title, + }, + }, + }; + + // Add type specific options + if (this.el.dataset.type === 'radar') { + chartData.options.scale = { + ticks: { + beginAtZero: true, + } + }; + } else if (['pie', 'doughnut'].includes(this.el.dataset.type)) { + chartData.options.tooltips.callbacks = { + label: (tooltipItem, data) => { + const label = data.datasets[tooltipItem.datasetIndex].label; + const secondLabel = data.labels[tooltipItem.index]; + let final = label; + if (label) { + if (secondLabel) { + final = label + ' - ' + secondLabel; + } + } else if (secondLabel) { + final = secondLabel; + } + return final + ':' + data.datasets[tooltipItem.datasetIndex].data[tooltipItem.index]; + }, + }; + } else { + chartData.options.scales = { + xAxes: [{ + stacked: this.el.dataset.stacked === 'true', + ticks: { + beginAtZero: true + }, + }], + yAxes: [{ + stacked: this.el.dataset.stacked === 'true', + ticks: { + beginAtZero: true + }, + }], + }; + } + + // Disable animation in edit mode + if (this.editableMode) { + chartData.options.animation = { + duration: 0, + }; + } + + const canvas = this.el.querySelector('canvas'); + this.chart = new window.Chart(canvas, chartData); + return this._super.apply(this, arguments); + }, + /** + * @override + * Discard all library changes to reset the state of the Html. + */ + destroy: function () { + if (this.chart) { // The widget can be destroyed before start has completed + this.chart.destroy(); + this.el.querySelectorAll('.chartjs-size-monitor').forEach(el => el.remove()); + } + this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + * @param {string} color A css color or theme color string + * @returns {string} Css color + */ + _convertToCssColor: function (color) { + if (!color) { + return 'transparent'; + } + return weUtils.getCSSVariableValue(color, this.style) || color; + }, +}); + +publicWidget.registry.chart = ChartWidget; + +return ChartWidget; +}); diff --git a/addons/website/static/src/snippets/s_chart/options.js b/addons/website/static/src/snippets/s_chart/options.js new file mode 100644 index 00000000..fe59cedf --- /dev/null +++ b/addons/website/static/src/snippets/s_chart/options.js @@ -0,0 +1,477 @@ +odoo.define('website.s_chart_options', function (require) { +'use strict'; + +var core = require('web.core'); +const {ColorpickerWidget} = require('web.Colorpicker'); +var options = require('web_editor.snippets.options'); +const weUtils = require('web_editor.utils'); + +var _t = core._t; + +options.registry.InnerChart = options.Class.extend({ + custom_events: _.extend({}, options.Class.prototype.custom_events, { + 'get_custom_colors': '_onGetCustomColors', + }), + events: _.extend({}, options.Class.prototype.events, { + 'click we-button.add_column': '_onAddColumnClick', + 'click we-button.add_row': '_onAddRowClick', + 'click we-button.o_we_matrix_remove_col': '_onRemoveColumnClick', + 'click we-button.o_we_matrix_remove_row': '_onRemoveRowClick', + 'blur we-matrix input': '_onMatrixInputFocusOut', + 'focus we-matrix input': '_onMatrixInputFocus', + }), + + /** + * @override + */ + init: function () { + this._super.apply(this, arguments); + this.themeArray = ['o-color-1', 'o-color-2', 'o-color-3', 'o-color-4', 'o-color-5']; + this.style = window.getComputedStyle(document.documentElement); + }, + /** + * @override + */ + start: function () { + this.backSelectEl = this.el.querySelector('[data-name="chart_bg_color_opt"]'); + this.borderSelectEl = this.el.querySelector('[data-name="chart_border_color_opt"]'); + + // Build matrix content + this.tableEl = this.el.querySelector('we-matrix table'); + const data = JSON.parse(this.$target[0].dataset.data); + data.labels.forEach(el => { + this._addRow(el); + }); + data.datasets.forEach((el, i) => { + if (this._isPieChart()) { + // Add header colors in case the user changes the type of graph + const headerBackgroundColor = this.themeArray[i] || this._randomColor(); + const headerBorderColor = this.themeArray[i] || this._randomColor(); + this._addColumn(el.label, el.data, headerBackgroundColor, headerBorderColor, el.backgroundColor, el.borderColor); + } else { + this._addColumn(el.label, el.data, el.backgroundColor, el.borderColor); + } + }); + this._displayRemoveColButton(); + this._displayRemoveRowButton(); + this._setDefaultSelectedInput(); + return this._super(...arguments); + }, + /** + * @override + */ + updateUI: async function () { + // Selected input might not be in dom anymore if col/row removed + // Done before _super because _computeWidgetState of colorChange + if (!this.lastEditableSelectedInput.closest('table') || this.colorPaletteSelectedInput && !this.colorPaletteSelectedInput.closest('table')) { + this._setDefaultSelectedInput(); + } + + await this._super(...arguments); + + // prevent the columns from becoming too small. + this.tableEl.classList.toggle('o_we_matrix_five_col', this.tableEl.querySelectorAll('tr:first-child th').length > 5); + + this.backSelectEl.querySelector('we-title').textContent = this._isPieChart() ? _t("Data Color") : _t("Dataset Color"); + this.borderSelectEl.querySelector('we-title').textContent = this._isPieChart() ? _t("Data Border") : _t("Dataset Border"); + + // Dataset/Cell color + this.tableEl.querySelectorAll('input').forEach(el => el.style.border = ''); + const selector = this._isPieChart() ? 'td input' : 'tr:first-child input'; + this.tableEl.querySelectorAll(selector).forEach(el => { + const color = el.dataset.backgroundColor || el.dataset.borderColor; + if (color) { + el.style.border = '2px solid'; + el.style.borderColor = ColorpickerWidget.isCSSColor(color) ? color : weUtils.getCSSVariableValue(color, this.style); + } + }); + }, + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Set the color on the selected input. + */ + colorChange: async function (previewMode, widgetValue, params) { + if (widgetValue) { + this.colorPaletteSelectedInput.dataset[params.attributeName] = widgetValue; + } else { + delete this.colorPaletteSelectedInput.dataset[params.attributeName]; + } + await this._reloadGraph(); + // To focus back the input that is edited we have to wait for the color + // picker to be fully reloaded. + await new Promise(resolve => setTimeout(() => { + this.lastEditableSelectedInput.focus(); + resolve(); + })); + }, + /** + * @override + */ + selectDataAttribute: async function (previewMode, widgetValue, params) { + await this._super(...arguments); + // Data might change if going from or to a pieChart. + if (params.attributeName === 'type') { + this._setDefaultSelectedInput(); + await this._reloadGraph(); + } + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeWidgetState: function (methodName, params) { + if (methodName === 'colorChange') { + return this.colorPaletteSelectedInput && this.colorPaletteSelectedInput.dataset[params.attributeName] || ''; + } + return this._super(...arguments); + }, + /** + * @override + */ + _computeWidgetVisibility: function (widgetName, params) { + switch (widgetName) { + case 'stacked_chart_opt': { + return this._getColumnCount() > 1; + } + case 'chart_bg_color_opt': + case 'chart_border_color_opt': { + return !!this.colorPaletteSelectedInput; + } + } + return this._super(...arguments); + }, + /** + * Sets and reloads the data on the canvas if it has changed. + * Used in matrix related method. + * + * @private + */ + _reloadGraph: async function () { + const jsonValue = this._matrixToChartData(); + if (this.$target[0].dataset.data !== jsonValue) { + this.$target[0].dataset.data = jsonValue; + await this._refreshPublicWidgets(); + } + }, + /** + * Return a stringifyed chart.js data object from the matrix + * Pie charts have one color per data while other charts have one color per dataset. + * + * @private + */ + _matrixToChartData: function () { + const data = { + labels: [], + datasets: [], + }; + this.tableEl.querySelectorAll('tr:first-child input').forEach(el => { + data.datasets.push({ + label: el.value || '', + data: [], + backgroundColor: this._isPieChart() ? [] : el.dataset.backgroundColor || '', + borderColor: this._isPieChart() ? [] : el.dataset.borderColor || '', + }); + }); + this.tableEl.querySelectorAll('tr:not(:first-child):not(:last-child)').forEach((el) => { + const title = el.querySelector('th input').value || ''; + data.labels.push(title); + el.querySelectorAll('td input').forEach((el, i) => { + data.datasets[i].data.push(el.value || 0); + if (this._isPieChart()) { + data.datasets[i].backgroundColor.push(el.dataset.backgroundColor || ''); + data.datasets[i].borderColor.push(el.dataset.borderColor || ''); + } + }); + }); + return JSON.stringify(data); + }, + /** + * Return a td containing a we-button with minus icon + * + * @param {...string} classes Classes to add to the we-button + * @returns {HTMLElement} + */ + _makeDeleteButton: function (...classes) { + const rmbuttonEl = options.buildElement('we-button', null, { + classes: ['o_we_text_danger', 'o_we_link', 'fa', 'fa-fw', 'fa-minus', ...classes], + }); + const newEl = document.createElement('td'); + newEl.appendChild(rmbuttonEl); + return newEl; + }, + /** + * Add a column to the matrix + * The th (dataset label) of a column hold the colors for the entire dataset if the graph is not a pie chart + * If the graph is a pie chart the color of the td (data) are used. + * + * @private + * @param {String} title The title of the column + * @param {Array} values The values of the column input + * @param {String} heardeBackgroundColor The background color of the dataset + * @param {String} headerBorderColor The border color of the dataset + * @param {string[]} cellBackgroundColors The background colors of the datas inputs, random color if missing + * @param {string[]} cellBorderColors The border color of the datas inputs, no color if missing + */ + _addColumn: function (title, values, heardeBackgroundColor, headerBorderColor, cellBackgroundColors = [], cellBorderColors = []) { + const firstRow = this.tableEl.querySelector('tr:first-child'); + const headerInput = this._makeCell('th', title, heardeBackgroundColor, headerBorderColor); + firstRow.insertBefore(headerInput, firstRow.lastElementChild); + + this.tableEl.querySelectorAll('tr:not(:first-child):not(:last-child)').forEach((el, i) => { + const newCell = this._makeCell('td', values ? values[i] : null, cellBackgroundColors[i] || this._randomColor(), cellBorderColors[i - 1]); + el.insertBefore(newCell, el.lastElementChild); + }); + + const lastRow = this.tableEl.querySelector('tr:last-child'); + const removeButton = this._makeDeleteButton('o_we_matrix_remove_col'); + lastRow.appendChild(removeButton); + }, + /** + * Add a row to the matrix + * The background color of the datas are random + * + * @private + * @param {String} tilte The title of the row + */ + _addRow: function (tilte) { + const trEl = document.createElement('tr'); + trEl.appendChild(this._makeCell('th', tilte)); + this.tableEl.querySelectorAll('tr:first-child input').forEach(() => { + trEl.appendChild(this._makeCell('td', null, this._randomColor())); + }); + trEl.appendChild(this._makeDeleteButton('o_we_matrix_remove_row')); + const tbody = this.tableEl.querySelector('tbody'); + tbody.insertBefore(trEl, tbody.lastElementChild); + }, + /** + * @private + * @param {string} tag tag of the HTML Element (td/th) + * @param {string} value The current value of the cell input + * @param {string} backgroundColor The background Color of the data on the graph + * @param {string} borderColor The border Color of the the data on the graph + * @returns {HTMLElement} + */ + _makeCell: function (tag, value, backgroundColor, borderColor) { + const newEl = document.createElement(tag); + const contentEl = document.createElement('input'); + contentEl.type = 'text'; + contentEl.value = value || ''; + if (backgroundColor) { + contentEl.dataset.backgroundColor = backgroundColor; + } + if (borderColor) { + contentEl.dataset.borderColor = borderColor; + } + newEl.appendChild(contentEl); + return newEl; + }, + /** + * Display the remove button coresponding to the colIndex + * + * @private + * @param {Int} colIndex Can be undefined, if so the last remove button of the column will be shown + */ + _displayRemoveColButton: function (colIndex) { + if (this._getColumnCount() > 1) { + this._displayRemoveButton(colIndex, 'o_we_matrix_remove_col'); + } + }, + /** + * Display the remove button coresponding to the rowIndex + * + * @private + * @param {Int} rowIndex Can be undefined, if so the last remove button of the row will be shown + */ + _displayRemoveRowButton: function (rowIndex) { + //Nbr of row minus header and button + const rowCount = this.tableEl.rows.length - 2; + if (rowCount > 1) { + this._displayRemoveButton(rowIndex, 'o_we_matrix_remove_row'); + } + }, + /** + * @private + * @param {Int} tdIndex Can be undefined, if so the last remove button will be shown + * @param {String} btnClass Either o_we_matrix_remove_col or o_we_matrix_remove_row + */ + _displayRemoveButton: function (tdIndex, btnClass) { + const removeBtn = this.tableEl.querySelectorAll(`td we-button.${btnClass}`); + removeBtn.forEach(el => el.style.display = ''); //hide all + const index = tdIndex < removeBtn.length ? tdIndex : removeBtn.length - 1; + removeBtn[index].style.display = 'inline-block'; + }, + /** + * @private + * @return {boolean} + */ + _isPieChart: function () { + return ['pie', 'doughnut'].includes(this.$target[0].dataset.type); + }, + /** + * Return the number of column minus header and button + * @private + * @return {integer} + */ + _getColumnCount: function () { + return this.tableEl.rows[0].cells.length - 2; + }, + /** + * Select the first data input + * + * @private + */ + _setDefaultSelectedInput: function () { + this.lastEditableSelectedInput = this.tableEl.querySelector('td input'); + if (this._isPieChart()) { + this.colorPaletteSelectedInput = this.lastEditableSelectedInput; + } else { + this.colorPaletteSelectedInput = this.tableEl.querySelector('th input'); + } + }, + /** + * Return a random hexadecimal color. + * + * @private + * @return {string} + */ + _randomColor: function () { + return '#' + ('00000' + (Math.random() * (1 << 24) | 0).toString(16)).slice(-6).toUpperCase(); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Used by colorPalette to retrieve the custom colors used on the chart + * Make an array with all the custom colors used on the chart + * and apply it to the onSuccess method provided by the trigger_up. + * + * @private + */ + _onGetCustomColors: function (ev) { + const data = JSON.parse(this.$target[0].dataset.data || ''); + let customColors = []; + data.datasets.forEach(el => { + if (this._isPieChart()) { + customColors = customColors.concat(el.backgroundColor).concat(el.borderColor); + } else { + customColors.push(el.backgroundColor); + customColors.push(el.borderColor); + } + }); + customColors = customColors.filter((el, i, array) => { + return !weUtils.getCSSVariableValue(el, this.style) && array.indexOf(el) === i && el !== ''; // unique non class not transparent + }); + ev.data.onSuccess(customColors); + }, + /** + * Add a row at the end of the matrix and display it's remove button + * Choose the color of the column from the theme array or a random color if they are already used + * + * @private + */ + _onAddColumnClick: function () { + const usedColor = Array.from(this.tableEl.querySelectorAll('tr:first-child input')).map(el => el.dataset.backgroundColor); + const color = this.themeArray.filter(el => !usedColor.includes(el))[0] || this._randomColor(); + this._addColumn(null, null, color, color); + this._reloadGraph().then(() => { + this._displayRemoveColButton(); + this.updateUI(); + }); + }, + /** + * Add a column at the end of the matrix and display it's remove button + * + * @private + */ + _onAddRowClick: function () { + this._addRow(); + this._reloadGraph().then(() => { + this._displayRemoveRowButton(); + this.updateUI(); + }); + }, + /** + * Remove the column and show the remove button of the next column or the last if no next. + * + * @private + * @param {Event} ev + */ + _onRemoveColumnClick: function (ev) { + const cell = ev.currentTarget.parentElement; + const cellIndex = cell.cellIndex; + this.tableEl.querySelectorAll('tr').forEach((el) => { + el.children[cellIndex].remove(); + }); + this._displayRemoveColButton(cellIndex - 1); + this._reloadGraph().then(() => { + this.updateUI(); + }); + }, + /** + * Remove the row and show the remove button of the next row or the last if no next. + * + * @private + * @param {Event} ev + */ + _onRemoveRowClick: function (ev) { + const row = ev.currentTarget.parentElement.parentElement; + const rowIndex = row.rowIndex; + row.remove(); + this._displayRemoveRowButton(rowIndex - 1); + this._reloadGraph().then(() => { + this.updateUI(); + }); + }, + /** + * @private + * @param {Event} ev + */ + _onMatrixInputFocusOut: function (ev) { + // Sometimes, an input is focusout for internal reason (like an undo + // recording) then focused again manually in the same JS stack + // execution. In that case, the blur should not trigger an option + // selection as the user did not leave the input. We thus defer the blur + // handling to then check that the target is indeed still blurred before + // executing the actual option selection. + setTimeout(() => { + if (ev.currentTarget === document.activeElement) { + return; + } + this._reloadGraph(); + }); + }, + /** + * Set the selected cell/header and display the related remove button + * + * @private + * @param {Event} ev + */ + _onMatrixInputFocus: function (ev) { + this.lastEditableSelectedInput = ev.target; + const col = ev.target.parentElement.cellIndex; + const row = ev.target.parentElement.parentElement.rowIndex; + if (this._isPieChart()) { + this.colorPaletteSelectedInput = ev.target.parentNode.tagName === 'TD' ? ev.target : null; + } else { + this.colorPaletteSelectedInput = this.tableEl.querySelector(`tr:first-child th:nth-of-type(${col + 1}) input`); + } + if (col > 0) { + this._displayRemoveColButton(col - 1); + } + if (row > 0) { + this._displayRemoveRowButton(row - 1); + } + this.updateUI(); + }, +}); +}); diff --git a/addons/website/static/src/snippets/s_color_blocks_2/000.scss b/addons/website/static/src/snippets/s_color_blocks_2/000.scss new file mode 100644 index 00000000..555cab58 --- /dev/null +++ b/addons/website/static/src/snippets/s_color_blocks_2/000.scss @@ -0,0 +1,36 @@ +.s_color_blocks_2 { + // Needed to be able to stretch the inner container so that + // the snippet works with the 50% and 100% height + &.o_half_screen_height, &.o_full_screen_height { + > :first-child { // container + &, > .row { + min-height: inherit; + } + } + } + .row { + display: flex; + flex-flow: row wrap; + + // Fix for safari browser as it 'supports' flex but not with the right + // behavior + &::before, &::after { + width: 0; + } + } + [class*="col-lg-"] { + padding: 8% 5%; + padding-top: 8vw; // A flex item cannot have % padding top and bottom (even if it works on chrome) + padding-bottom: 8vw; // Solution is vw units but we keep 8% as a fallback + } + @include media-breakpoint-down(md) { + [class*="col-lg-"] { + flex: 1 1 100%; + } + } + + img { + max-width: 100%; + height: auto; + } +} diff --git a/addons/website/static/src/snippets/s_company_team/000.scss b/addons/website/static/src/snippets/s_company_team/000.scss new file mode 100644 index 00000000..7947b831 --- /dev/null +++ b/addons/website/static/src/snippets/s_company_team/000.scss @@ -0,0 +1,8 @@ + +.s_company_team { + @include media-breakpoint-down(md) { + img { + max-width: 50%; + } + } +} diff --git a/addons/website/static/src/snippets/s_comparisons/000.scss b/addons/website/static/src/snippets/s_comparisons/000.scss new file mode 100644 index 00000000..cf1e9218 --- /dev/null +++ b/addons/website/static/src/snippets/s_comparisons/000.scss @@ -0,0 +1,21 @@ + +.s_comparisons { + .card-body { + .card-title { + margin: 0; + } + .s_comparisons_currency, + .s_comparisons_price, + .s_comparisons_decimal { + display: inline-block; + vertical-align: middle; + } + .s_comparisons_currency, + .s_comparisons_decimal { + font-size: 80%; + } + .s_comparisons_price { + font-size: 200%; + } + } +} diff --git a/addons/website/static/src/snippets/s_countdown/000.js b/addons/website/static/src/snippets/s_countdown/000.js new file mode 100644 index 00000000..fcdac7b7 --- /dev/null +++ b/addons/website/static/src/snippets/s_countdown/000.js @@ -0,0 +1,422 @@ +odoo.define('website.s_countdown', function (require) { +'use strict'; + +const {ColorpickerWidget} = require('web.Colorpicker'); +const core = require('web.core'); +const publicWidget = require('web.public.widget'); +const weUtils = require('web_editor.utils'); + +const qweb = core.qweb; +const _t = core._t; + +const CountdownWidget = publicWidget.Widget.extend({ + selector: '.s_countdown', + xmlDependencies: ['/website/static/src/snippets/s_countdown/000.xml'], + disabledInEditableMode: false, + defaultColor: 'rgba(0, 0, 0, 255)', + + /** + * @override + */ + start: function () { + this.$wrapper = this.$('.s_countdown_canvas_wrapper'); + this.hereBeforeTimerEnds = false; + this.endAction = this.el.dataset.endAction; + this.endTime = parseInt(this.el.dataset.endTime); + this.size = parseInt(this.el.dataset.size); + this.display = this.el.dataset.display; + + this.layout = this.el.dataset.layout; + this.layoutBackground = this.el.dataset.layoutBackground; + this.progressBarStyle = this.el.dataset.progressBarStyle; + this.progressBarWeight = this.el.dataset.progressBarWeight; + + this.textColor = this._ensureCssColor(this.el.dataset.textColor); + this.layoutBackgroundColor = this._ensureCssColor(this.el.dataset.layoutBackgroundColor); + this.progressBarColor = this._ensureCssColor(this.el.dataset.progressBarColor); + + this.onlyOneUnit = this.display === 'd'; + this.width = parseInt(this.size); + if (this.layout === 'boxes') { + this.width /= 1.75; + } + this._initTimeDiff(); + + this._render(); + + this.setInterval = setInterval(this._render.bind(this), 1000); + return this._super(...arguments); + }, + /** + * @override + */ + destroy: function () { + this.$('.s_countdown_end_redirect_message').remove(); + this.$('canvas').remove(); + this.$('.s_countdown_end_message').addClass('d-none'); + this.$('.s_countdown_text_wrapper').remove(); + this.$('.s_countdown_canvas_wrapper').removeClass('d-none'); + + clearInterval(this.setInterval); + this._super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * Ensures the color is an actual css color. In case of a color variable, + * the color will be mapped to hexa. + * + * @private + * @param {string} color + * @returns {string} + */ + _ensureCssColor: function (color) { + if (ColorpickerWidget.isCSSColor(color)) { + return color; + } + return weUtils.getCSSVariableValue(color) || this.defaultColor; + }, + /** + * Gets the time difference in seconds between now and countdown due date. + * + * @private + */ + _getDelta: function () { + const currentTimestamp = Date.now() / 1000; + return this.endTime - currentTimestamp; + }, + /** + * Handles the action that should be executed once the countdown ends. + * + * @private + */ + _handleEndCountdownAction: function () { + if (this.endAction === 'redirect') { + const redirectUrl = this.el.dataset.redirectUrl || '/'; + if (this.hereBeforeTimerEnds) { + // Wait a bit, if the landing page has the same publish date + setTimeout(() => window.location = redirectUrl, 500); + } else { + // Show (non editable) msg when user lands on already finished countdown + if (!this.$('.s_countdown_end_redirect_message').length) { + const $container = this.$('> .container, > .container-fluid, > .o_container_small'); + $container.append( + $(qweb.render('website.s_countdown.end_redirect_message', { + redirectUrl: redirectUrl, + })) + ); + } + } + } else if (this.endAction === 'message') { + this.$('.s_countdown_end_message').removeClass('d-none'); + } + }, + /** + * Initializes the `diff` object. It will contains every visible time unit + * which will each contain its related canvas, total step, label.. + * + * @private + */ + _initTimeDiff: function () { + const delta = this._getDelta(); + this.diff = []; + if (this._isUnitVisible('d') && !(this.onlyOneUnit && delta < 86400)) { + this.diff.push({ + canvas: $('', {class: 'o_temp_auto_element'}).appendTo(this.$wrapper)[0], + // There is no logical number of unit (total) on which day units + // can be compared against, so we use an arbitrary number. + total: 15, + label: _t("Days"), + nbSeconds: 86400, + }); + } + if (this._isUnitVisible('h') || (this.onlyOneUnit && delta < 86400 && delta > 3600)) { + this.diff.push({ + canvas: $('', {class: 'o_temp_auto_element'}).appendTo(this.$wrapper)[0], + total: 24, + label: _t("Hours"), + nbSeconds: 3600, + }); + } + if (this._isUnitVisible('m') || (this.onlyOneUnit && delta < 3600 && delta > 60)) { + this.diff.push({ + canvas: $('', {class: 'o_temp_auto_element'}).appendTo(this.$wrapper)[0], + total: 60, + label: _t("Minutes"), + nbSeconds: 60, + }); + } + if (this._isUnitVisible('s') || (this.onlyOneUnit && delta < 60)) { + this.diff.push({ + canvas: $('', {class: 'o_temp_auto_element'}).appendTo(this.$wrapper)[0], + total: 60, + label: _t("Seconds"), + nbSeconds: 1, + }); + } + }, + /** + * Returns weither or not the countdown should be displayed for the given + * unit (days, sec..). + * + * @private + * @param {string} unit - either 'd', 'm', 'h', or 's' + * @returns {boolean} + */ + _isUnitVisible: function (unit) { + return this.display.includes(unit); + }, + /** + * Draws the whole countdown, including one countdown for each time unit. + * + * @private + */ + _render: function () { + // If only one unit mode, restart widget on unit change to populate diff + if (this.onlyOneUnit && this._getDelta() < this.diff[0].nbSeconds) { + this.$('canvas').remove(); + this._initTimeDiff(); + } + this._updateTimeDiff(); + + const hideCountdown = this.isFinished && !this.editableMode && this.$el.hasClass('hide-countdown'); + if (this.layout === 'text') { + this.$('canvas').addClass('d-none'); + if (!this.$textWrapper) { + this.$textWrapper = $('').attr({ + class: 's_countdown_text_wrapper d-none', + }); + this.$textWrapper.text(_t("Countdown ends in")); + this.$textWrapper.append($('').attr({ + class: 's_countdown_text ml-1', + })); + this.$textWrapper.appendTo(this.$wrapper); + } + + this.$textWrapper.toggleClass('d-none', hideCountdown); + + const countdownText = this.diff.map(e => e.nb + ' ' + e.label).join(', '); + this.$('.s_countdown_text').text(countdownText.toLowerCase()); + } else { + for (const val of this.diff) { + const canvas = val.canvas; + const ctx = canvas.getContext("2d"); + ctx.canvas.width = this.width; + ctx.canvas.height = this.size; + this._clearCanvas(ctx); + + $(canvas).toggleClass('d-none', hideCountdown); + if (hideCountdown) { + continue; + } + + // Draw canvas elements + if (this.layoutBackground !== 'none') { + this._drawBgShape(ctx, this.layoutBackground === 'plain'); + } + this._drawText(canvas, val.nb, val.label, this.layoutBackground === 'plain'); + if (this.progressBarStyle === 'surrounded') { + this._drawProgressBarBg(ctx, this.progressBarWeight === 'thin'); + } + if (this.progressBarStyle !== 'none') { + this._drawProgressBar(ctx, val.nb, val.total, this.progressBarWeight === 'thin'); + } + $(canvas).toggleClass('mx-2', this.layout === 'boxes'); + } + } + + if (this.isFinished) { + clearInterval(this.setInterval); + if (!this.editableMode) { + this._handleEndCountdownAction(); + } + } + }, + /** + * Updates the remaining units into the `diff` object. + * + * @private + */ + _updateTimeDiff: function () { + let delta = this._getDelta(); + this.isFinished = delta < 0; + if (this.isFinished) { + for (const unitData of this.diff) { + unitData.nb = 0; + } + return; + } + + this.hereBeforeTimerEnds = true; + for (const unitData of this.diff) { + unitData.nb = Math.floor(delta / unitData.nbSeconds); + delta -= unitData.nb * unitData.nbSeconds; + } + }, + + //-------------------------------------------------------------------------- + // Canvas drawing methods + //-------------------------------------------------------------------------- + + /** + * Erases the canvas. + * + * @private + * @param {RenderingContext} ctx - Context of the canvas + */ + _clearCanvas: function (ctx) { + ctx.clearRect(0, 0, this.size, this.size); + }, + /** + * Draws a text into the canvas. + * + * @private + * @param {HTMLCanvasElement} canvas + * @param {string} textNb - text to display in the center of the canvas, in big + * @param {string} textUnit - text to display bellow `textNb` in small + * @param {boolean} full - if true, the shape will be drawn up to the progressbar + */ + _drawText: function (canvas, textNb, textUnit, full = false) { + const ctx = canvas.getContext("2d"); + const nbSize = this.size / 4; + ctx.font = `${nbSize}px Arial`; + ctx.fillStyle = this.textColor; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(textNb, canvas.width / 2, canvas.height / 2); + + const unitSize = this.size / 12; + ctx.font = `${unitSize}px Arial`; + ctx.fillText(textUnit, canvas.width / 2, canvas.height / 2 + nbSize / 1.5, this.width); + + if (this.layout === 'boxes' && this.layoutBackground !== 'none' && this.progressBarStyle === 'none') { + let barWidth = this.size / (this.progressBarWeight === 'thin' ? 31 : 10); + if (full) { + barWidth = 0; + } + ctx.beginPath(); + ctx.moveTo(barWidth, this.size / 2); + ctx.lineTo(this.width - barWidth, this.size / 2); + ctx.stroke(); + } + }, + /** + * Draws a plain shape into the canvas. + * + * @private + * @param {RenderingContext} ctx - Context of the canvas + * @param {boolean} full - if true, the shape will be drawn up to the progressbar + */ + _drawBgShape: function (ctx, full = false) { + ctx.fillStyle = this.layoutBackgroundColor; + ctx.beginPath(); + if (this.layout === 'circle') { + let rayon = this.size / 2; + if (this.progressBarWeight === 'thin') { + rayon -= full ? this.size / 29 : this.size / 15; + } else { + rayon -= full ? 0 : this.size / 10; + } + ctx.arc(this.size / 2, this.size / 2, rayon, 0, Math.PI * 2); + ctx.fill(); + } else if (this.layout === 'boxes') { + let barWidth = this.size / (this.progressBarWeight === 'thin' ? 31 : 10); + if (full) { + barWidth = 0; + } + + ctx.fillStyle = this.layoutBackgroundColor; + ctx.rect(barWidth, barWidth, this.width - barWidth * 2, this.size - barWidth * 2); + ctx.fill(); + + const gradient = ctx.createLinearGradient(0, this.width, 0, 0); + gradient.addColorStop(0, '#ffffff24'); + gradient.addColorStop(1, this.layoutBackgroundColor); + ctx.fillStyle = gradient; + ctx.rect(barWidth, barWidth, this.width - barWidth * 2, this.size - barWidth * 2); + ctx.fill(); + $(ctx.canvas).css({'border-radius': '8px'}); + } + }, + /** + * Draws a progress bar around the countdown shape. + * + * @private + * @param {RenderingContext} ctx - Context of the canvas + * @param {string} nbUnit - how many unit should fill progress bar + * @param {string} totalUnit - number of unit to do a complete progress bar + * @param {boolean} thinLine - if true, the progress bar will be thiner + */ + _drawProgressBar: function (ctx, nbUnit, totalUnit, thinLine) { + ctx.strokeStyle = this.progressBarColor; + ctx.lineWidth = thinLine ? this.size / 35 : this.size / 10; + if (this.layout === 'circle') { + ctx.beginPath(); + ctx.arc(this.size / 2, this.size / 2, this.size / 2 - this.size / 20, Math.PI / -2, (Math.PI * 2) * (nbUnit / totalUnit) + (Math.PI / -2)); + ctx.stroke(); + } else if (this.layout === 'boxes') { + ctx.lineWidth *= 2; + let pc = nbUnit / totalUnit * 100; + + // Lines: Top(x1,y1,x2,y2) Right(x1,y1,x2,y2) Bottom(x1,y1,x2,y2) Left(x1,y1,x2,y2) + const linesCoordFuncs = [ + (linePc) => [0 + ctx.lineWidth / 2, 0, (this.width - ctx.lineWidth / 2) * linePc / 25 + ctx.lineWidth / 2, 0], + (linePc) => [this.width, 0 + ctx.lineWidth / 2, this.width, (this.size - ctx.lineWidth / 2) * linePc / 25 + ctx.lineWidth / 2], + (linePc) => [this.width - ((this.width - ctx.lineWidth / 2) * linePc / 25) - ctx.lineWidth / 2, this.size, this.width - ctx.lineWidth / 2, this.size], + (linePc) => [0, this.size - ((this.size - ctx.lineWidth / 2) * linePc / 25) - ctx.lineWidth / 2, 0, this.size - ctx.lineWidth / 2], + ]; + while (pc > 0 && linesCoordFuncs.length) { + const linePc = Math.min(pc, 25); + const lineCoord = (linesCoordFuncs.shift())(linePc); + ctx.beginPath(); + ctx.moveTo(lineCoord[0], lineCoord[1]); + ctx.lineTo(lineCoord[2], lineCoord[3]); + ctx.stroke(); + pc -= linePc; + } + } + }, + /** + * Draws a full lighter background progressbar around the shape. + * + * @private + * @param {RenderingContext} ctx - Context of the canvas + * @param {boolean} thinLine - if true, the progress bar will be thiner + */ + _drawProgressBarBg: function (ctx, thinLine) { + ctx.strokeStyle = this.progressBarColor; + ctx.globalAlpha = 0.2; + ctx.lineWidth = thinLine ? this.size / 35 : this.size / 10; + if (this.layout === 'circle') { + ctx.beginPath(); + ctx.arc(this.size / 2, this.size / 2, this.size / 2 - this.size / 20, 0, Math.PI * 2); + ctx.stroke(); + } else if (this.layout === 'boxes') { + ctx.lineWidth *= 2; + + // Lines: Top(x1,y1,x2,y2) Right(x1,y1,x2,y2) Bottom(x1,y1,x2,y2) Left(x1,y1,x2,y2) + const points = [ + [0 + ctx.lineWidth / 2, 0, this.width, 0], + [this.width, 0 + ctx.lineWidth / 2, this.width, this.size], + [0, this.size, this.width - ctx.lineWidth / 2, this.size], + [0, 0, 0, this.size - ctx.lineWidth / 2], + ]; + while (points.length) { + const point = points.shift(); + ctx.beginPath(); + ctx.moveTo(point[0], point[1]); + ctx.lineTo(point[2], point[3]); + ctx.stroke(); + } + } + ctx.globalAlpha = 1; + }, +}); + +publicWidget.registry.countdown = CountdownWidget; + +return CountdownWidget; +}); diff --git a/addons/website/static/src/snippets/s_countdown/000.xml b/addons/website/static/src/snippets/s_countdown/000.xml new file mode 100644 index 00000000..07905447 --- /dev/null +++ b/addons/website/static/src/snippets/s_countdown/000.xml @@ -0,0 +1,29 @@ + + + +

Time's up! You can now visit this page.

+
+ +
+
+ The following message will become visible only once the countdown ends. +
+
+
+
+

Happy Odoo Anniversary!

+

As promised, we will offer 4 free tickets to our next summit.
Visit our Facebook page to know if you are one of the lucky winners.

+


+
+
+
+ Countdown is over - Firework +
+
+
+
+
+
+
+
+
diff --git a/addons/website/static/src/snippets/s_countdown/options.js b/addons/website/static/src/snippets/s_countdown/options.js new file mode 100644 index 00000000..ee99e0a8 --- /dev/null +++ b/addons/website/static/src/snippets/s_countdown/options.js @@ -0,0 +1,135 @@ +odoo.define('website.s_countdown_options', function (require) { +'use strict'; + +const core = require('web.core'); +const options = require('web_editor.snippets.options'); +const CountdownWidget = require('website.s_countdown'); + +const qweb = core.qweb; + +options.registry.countdown = options.Class.extend({ + events: _.extend({}, options.Class.prototype.events || {}, { + 'click .toggle-edit-message': '_onToggleEndMessageClick', + }), + + //-------------------------------------------------------------------------- + // Options + //-------------------------------------------------------------------------- + + /** + * Changes the countdown action at zero. + * + * @see this.selectClass for parameters + */ + endAction: function (previewMode, widgetValue, params) { + this.$target[0].dataset.endAction = widgetValue; + if (widgetValue === 'message') { + if (!this.$target.find('.s_countdown_end_message').length) { + const message = this.endMessage || qweb.render('website.s_countdown.end_message'); + this.$target.append(message); + } + } else { + const $message = this.$target.find('.s_countdown_end_message').detach(); + if ($message.length) { + this.endMessage = $message[0].outerHTML; + } + } + }, + /** + * Changes the countdown style. + * + * @see this.selectClass for parameters + */ + layout: function (previewMode, widgetValue, params) { + switch (widgetValue) { + case 'circle': + this.$target[0].dataset.progressBarStyle = 'disappear'; + this.$target[0].dataset.progressBarWeight = 'thin'; + this.$target[0].dataset.layoutBackground = 'none'; + break; + case 'boxes': + this.$target[0].dataset.progressBarStyle = 'none'; + this.$target[0].dataset.layoutBackground = 'plain'; + break; + case 'clean': + this.$target[0].dataset.progressBarStyle = 'none'; + this.$target[0].dataset.layoutBackground = 'none'; + break; + case 'text': + this.$target[0].dataset.progressBarStyle = 'none'; + this.$target[0].dataset.layoutBackground = 'none'; + break; + } + this.$target[0].dataset.layout = widgetValue; + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + updateUIVisibility: async function () { + await this._super(...arguments); + const dataset = this.$target[0].dataset; + + // End Action UI + this.$el.find('.toggle-edit-message') + .toggleClass('d-none', dataset.endAction !== 'message'); + + // End Message UI + this.updateUIEndMessage(); + }, + /** + * @see this.updateUI + */ + updateUIEndMessage: function () { + this.$target.find('.s_countdown_canvas_wrapper') + .toggleClass("d-none", this.showEndMessage === true && this.$target.hasClass("hide-countdown")); + this.$target.find('.s_countdown_end_message') + .toggleClass("d-none", !this.showEndMessage); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _computeWidgetState: function (methodName, params) { + switch (methodName) { + case 'endAction': + case 'layout': + return this.$target[0].dataset[methodName]; + + case 'selectDataAttribute': { + if (params.colorNames) { + // In this case, it is a colorpicker controlling a data + // value on the countdown: the default value is determined + // by the countdown public widget. + params.attributeDefaultValue = CountdownWidget.prototype.defaultColor; + } + break; + } + } + return this._super(...arguments); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onToggleEndMessageClick: function () { + this.showEndMessage = !this.showEndMessage; + this.$el.find(".toggle-edit-message") + .toggleClass('text-primary', this.showEndMessage); + this.updateUIEndMessage(); + this.trigger_up('cover_update'); + }, +}); +}); 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; +}); 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 00000000..cf43100a --- /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 00000000..3eceb172 --- /dev/null +++ b/addons/website/static/src/snippets/s_dynamic_snippet_carousel/000.scss @@ -0,0 +1,11 @@ +.s_dynamic { + .carousel-control-prev, .carousel-control-next { + position: absolute; + width: 4rem; + + > span.fa { + color: gray('700'); + background: radial-gradient($white 50%, transparent 50%); + } + } +} 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 00000000..1efb8f22 --- /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 00000000..cb9a3cf5 --- /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/static/src/snippets/s_facebook_page/000.js b/addons/website/static/src/snippets/s_facebook_page/000.js new file mode 100644 index 00000000..0a88f1b3 --- /dev/null +++ b/addons/website/static/src/snippets/s_facebook_page/000.js @@ -0,0 +1,56 @@ +odoo.define('website.s_facebook_page', function (require) { +'use strict'; + +var publicWidget = require('web.public.widget'); +var utils = require('web.utils'); + +const FacebookPageWidget = publicWidget.Widget.extend({ + selector: '.o_facebook_page', + disabledInEditableMode: false, + + /** + * @override + */ + start: function () { + var def = this._super.apply(this, arguments); + + var params = _.pick(this.$el.data(), 'href', 'height', 'tabs', 'small_header', 'hide_cover', 'show_facepile'); + if (!params.href) { + return def; + } + params.width = utils.confine(Math.floor(this.$el.width()), 180, 500); + + var src = $.param.querystring('https://www.facebook.com/plugins/page.php', params); + this.$iframe = $('