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; });