summaryrefslogtreecommitdiff
path: root/addons/website/static/src/snippets/s_countdown
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/website/static/src/snippets/s_countdown
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/website/static/src/snippets/s_countdown')
-rw-r--r--addons/website/static/src/snippets/s_countdown/000.js422
-rw-r--r--addons/website/static/src/snippets/s_countdown/000.xml29
-rw-r--r--addons/website/static/src/snippets/s_countdown/options.js135
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');
+ },
+});
+});