diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/website/static/src/snippets/s_countdown | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/website/static/src/snippets/s_countdown')
3 files changed, 586 insertions, 0 deletions
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: $('<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: $('<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: $('<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: $('<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 = $('<span/>').attr({ + class: 's_countdown_text_wrapper d-none', + }); + this.$textWrapper.text(_t("Countdown ends in")); + this.$textWrapper.append($('<span/>').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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<templates id="template" xml:space="preserve"> + <t t-name="website.s_countdown.end_redirect_message"> + <p class="text-center s_countdown_end_redirect_message">Time's up! You can now visit <a class="s_countdown_end_redirect_url" t-attf-href="#{redirectUrl}">this page</a>.</p> + </t> + <t t-name="website.s_countdown.end_message"> + <div class="s_countdown_end_message d-none"> + <div class="text-center alert alert-info css_non_editable_mode_hidden o_not_editable" t-ignore="True" role="status"> + The following message will become visible <strong>only</strong> once the countdown ends. + </div> + <div class="oe_structure"> + <section class="s_picture bg-200 pt48 pb24" data-snippet="s_picture"> + <div class="container"> + <h2 style="text-align: center;">Happy Odoo Anniversary!</h2> + <p style="text-align: center;">As promised, we will offer 4 free tickets to our next summit.<br/>Visit our Facebook page to know if you are one of the lucky winners.</p> + <p><br/></p> + <div class="row s_nb_column_fixed"> + <div class="col-lg-12 pb24"> + <figure class="figure"> + <img src="/web/image/website.library_image_18" class="figure-img img-thumbnail mx-auto padding-large" style="width: 50%;" alt="Countdown is over - Firework"/> + </figure> + </div> + </div> + </div> + </section> + </div> + </div> + </t> +</templates> 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'); + }, +}); +}); |
