diff --git a/addons/website/__manifest__.py b/addons/website/__manifest__.py index a9e01a1c51b..1f260fdf816 100644 --- a/addons/website/__manifest__.py +++ b/addons/website/__manifest__.py @@ -45,6 +45,7 @@ 'views/snippets/s_hr.xml', 'views/snippets/s_facebook_page.xml', 'views/snippets/s_image_gallery.xml', + 'views/snippets/s_countdown.xml', 'views/website_views.xml', 'views/website_visitor_views.xml', 'views/res_config_settings_views.xml', diff --git a/temp_js b/addons/website/static/src/js/content/snippets.animation.js similarity index 70% rename from temp_js rename to addons/website/static/src/js/content/snippets.animation.js index 8dcd9809c31..b9ca31ee248 100644 --- a/temp_js +++ b/addons/website/static/src/js/content/snippets.animation.js @@ -919,412 +919,6 @@ registry.anchorSlide = publicWidget.Widget.extend({ }, }); -registry.countdown = publicWidget.Widget.extend({ - selector: '.s_countdown', - xmlDependencies: ['/website/static/src/xml/website.s_countdown.xml'], - disabledInEditableMode: false, - - /** - * @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 (ColorpickerDialog.isCSSColor(color)) { - return color; - } - const style = window.getComputedStyle(document.documentElement); - return style.getPropertyValue('--' + color).trim(); - }, - /** - * 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) { - this.$target.find('.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: $('').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: $('').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: $('').appendTo(this.$wrapper)[0], - total: 60, - label: _t("Minutes"), - nbSeconds: 60, - }); - } - if (this._isUnitVisible('s') || (this.onlyOneUnit && delta < 60)) { - this.diff.push({ - canvas: $('').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) { - return; - } - - // 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; - }, -}); - registry.chart = publicWidget.Widget.extend({ selector: '.s_chart', disabledInEditableMode: false, diff --git a/temp_options b/addons/website/static/src/js/editor/snippets.options.js similarity index 93% rename from temp_options rename to addons/website/static/src/js/editor/snippets.options.js index 47ba28bd791..6c22382a0f2 100644 --- a/temp_options +++ b/addons/website/static/src/js/editor/snippets.options.js @@ -844,122 +844,6 @@ options.registry.collapse = options.Class.extend({ }, }); -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.find('.container').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]; - } - 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'); - }, -}); - options.registry.topMenuTransparency = options.Class.extend({ //-------------------------------------------------------------------------- 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 00000000000..3abd6441aa9 --- /dev/null +++ b/addons/website/static/src/snippets/s_countdown/000.js @@ -0,0 +1,420 @@ +odoo.define('website.s_countdown', function (require) { +'use strict'; + +const ColorpickerDialog = require('web.ColorpickerDialog'); +const core = require('web.core'); +const publicWidget = require('web.public.widget'); + +const qweb = core.qweb; +const _t = core._t; + +const CountdownWidget = publicWidget.Widget.extend({ + selector: '.s_countdown', + xmlDependencies: ['/website/static/src/xml/website.s_countdown.xml'], + disabledInEditableMode: false, + + /** + * @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 (ColorpickerDialog.isCSSColor(color)) { + return color; + } + const style = window.getComputedStyle(document.documentElement); + return style.getPropertyValue('--' + color).trim(); + }, + /** + * 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) { + this.$target.find('.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: $('').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: $('').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: $('').appendTo(this.$wrapper)[0], + total: 60, + label: _t("Minutes"), + nbSeconds: 60, + }); + } + if (this._isUnitVisible('s') || (this.onlyOneUnit && delta < 60)) { + this.diff.push({ + canvas: $('').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) { + return; + } + + // 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/options.js b/addons/website/static/src/snippets/s_countdown/options.js new file mode 100644 index 00000000000..c591c155b1b --- /dev/null +++ b/addons/website/static/src/snippets/s_countdown/options.js @@ -0,0 +1,124 @@ +odoo.define('website.s_countdown_options', function (require) { +'use strict'; + +const core = require('web.core'); +const options = require('web_editor.snippets.options'); + +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.find('.container').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]; + } + 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/views/assets.xml b/addons/website/views/assets.xml index 625fe454b0d..39137714e4c 100644 --- a/addons/website/views/assets.xml +++ b/addons/website/views/assets.xml @@ -110,6 +110,7 @@