Skip to content
Permalink
Browse files

[IMP] web_editor, website: add new 'Countdown' snippet

This commit introduces a new snippet: Countdown.

The countdown snippet is composed of 4 circle countdown, one for every time
unit: seconds, minutes, hours and days.

On countdown ends, 3 possible actions:
1. Nothing
2. Show message: show a message once the countdown ends. The message will be
   displayed bellow the countdown (stopped on 0).
   The message can be edited through the website builder.
3. Redirect: redirect the user to the chosen URL. If the user is on the page at
   the exact moment the countdown reach 0, the user will be redirected
   automatically. If the user lands on the page after the countdown has reached
   0, there will be no redirection and the link will be shown bellow the
   countdown (stopped on 0).

The countdown layout can be customized in multiple ways:
1. By choosing to hide some time units. For instance, seconds can be hidden or
   only days shown.
2. By changing its design (plain background, thin circle etc).
3. By changing its size between small/medium/large.

task-2093081
  • Loading branch information...
rdeodoo committed Oct 28, 2019
1 parent 99ee754 commit 99697649486482dd99b9435f7dc60fe590e24a64
@@ -347,6 +347,9 @@ var SnippetEditor = Widget.extend({
uiEl.querySelectorAll('we-input').forEach(inputEl => {
options.Class.prototype.buildInputElement(inputEl);
});
uiEl.querySelectorAll('we-datetimepicker').forEach(datetimepickerEl => {
options.Class.prototype.buildDatetimepickerElement(datetimepickerEl);
});

return $optionSection;
},
@@ -4,6 +4,7 @@ odoo.define('web_editor.snippets.options', function (require) {
var core = require('web.core');
var ColorPaletteWidget = require('web_editor.ColorPalette').ColorPaletteWidget;
var Dialog = require('web.Dialog');
var time = require('web.time');
var Widget = require('web.Widget');
var weWidgets = require('wysiwyg.widgets');

@@ -340,7 +341,7 @@ var SnippetOption = Widget.extend({
if (methodNames.includes('setStyle')) {
methodNames = ['setStyle'];
}
let isInput = $el.is('we-input');
let isInput = $el.is('we-input, we-datetimepicker');

methodNames.forEach(methodName => {
if (!this[methodName]) {
@@ -532,7 +533,7 @@ var SnippetOption = Widget.extend({
* @param {Event} ev
*/
_onOptionPreview: function (ev) {
var $opt = $(ev.target).closest('we-button, we-input');
var $opt = $(ev.target).closest('we-button, we-input, we-datetimepicker');
if (!$opt.length) {
return;
}
@@ -552,7 +553,7 @@ var SnippetOption = Widget.extend({
* @param {Event} ev
*/
_onOptionSelection: function (ev) {
var $opt = $(ev.target).closest('we-button, we-input');
var $opt = $(ev.target).closest('we-button, we-input, we-datetimepicker');
if (ev.isDefaultPrevented() || !$opt.length || !$opt.is(':hasData')) {
return;
}
@@ -650,6 +651,56 @@ var SnippetOption = Widget.extend({
inputWrapperEl.appendChild(unitEl);
return inputWrapperEl;
},
/**
* Build the correct DOM for a we-datetimepicker element.
*
* @static
* @param {HTMLElement} datetimepickerEl
*/
buildDatetimepickerElement: function (datetimepickerEl) {
var titleEl = SnippetOption.prototype.stringToTitle(datetimepickerEl);

var datetimePickerId = _.uniqueId('datetimepicker');

var weInput = document.createElement('we-input');
datetimepickerEl.classList.forEach(className => weInput.classList.add(className));
datetimepickerEl.setAttribute('class', '');
for (const key in datetimepickerEl.dataset) {
weInput.dataset[key] = datetimepickerEl.dataset[key];
delete datetimepickerEl.dataset[key];
}
weInput.dataset['unit'] = null;

var inputEl = document.createElement('input');
inputEl.setAttribute('type', 'text');
inputEl.setAttribute('class', 'datetimepicker-input mx-0 text-left');
inputEl.setAttribute('id', datetimePickerId);
inputEl.setAttribute('data-target', '#' + datetimePickerId);
inputEl.setAttribute('data-toggle', 'datetimepicker');

weInput.appendChild(inputEl);

datetimepickerEl.appendChild(titleEl);
datetimepickerEl.appendChild(weInput);

var datepickersOptions = {
minDate: moment({y: 1900}),
maxDate: moment().add(200, 'y'),
calendarWeeks: true,
defaultDate: moment().format(),
icons: {
close: 'fa fa-check primary',
},
locale: moment.locale(),
format: time.getLangDatetimeFormat(),
sideBySide: true,
buttons: {
showClose: true,
},
widgetParent: 'body',
};
$(inputEl).datetimepicker(datepickersOptions);
},
/**
* Build the correct DOM for a we-select element.
*
Binary file not shown.
@@ -13,6 +13,7 @@ var publicWidget = require('web.public.widget');
var utils = require('web.utils');

var qweb = core.qweb;
var _t = core._t;

// Initialize fallbacks for the use of requestAnimationFrame,
// cancelAnimationFrame and performance.now()
@@ -1147,6 +1148,278 @@ 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.hereBeforeTimerEnds = false;
this.endAction = this.$el[0].dataset.endAction;
this.endTime = parseInt(this.$el[0].dataset.endTime);
this.size = this.$el[0].dataset.size;
this.display = this.$el[0].dataset.display;

this.layout = this.$el[0].dataset.layout;
this.shape = this.$el[0].dataset.shape;
this.shapeBackground = this.$el[0].dataset.shapeBackground;
this.progressBarStyle = this.$el[0].dataset.progressBarStyle;
this.progressBarWeight = this.$el[0].dataset.progressBarWeight;

this.diff = {
'days': {
'canvas': this.$('.s_countdown_days')[0],
'total': 15, // TODO RATIO DAYS ? Based on how many days ?
'visible': this._isUnitVisible('d'),
'label': _t('Days'),
},
'hours': {
'canvas': this.$('.s_countdown_hours')[0],
'total': 24,
'visible': this._isUnitVisible('h'),
'label': _t('Hours'),
},
'minutes': {
'canvas': this.$('.s_countdown_minutes')[0],
'total': 60,
'visible': this._isUnitVisible('m'),
'label': _t('Minutes'),
},
'seconds': {
'canvas': this.$('.s_countdown_seconds')[0],
'total': 60,
'visible': this._isUnitVisible('s'),
'label': _t('Seconds'),
},
};

this._render();

// Needed if edit mode entered after countdown end
this.$('.s_countdown_end_redirect_message').addClass('d-none');

// Show end message event if not finished so it can be edited.
this.$('.s_countdown_end_message').toggleClass('d-none', !(this.editableMode && this.endAction === 'message'));

this.setInterval = setInterval(this._render.bind(this), 1000);
return this._super(...arguments);
},
/**
* @override
*/
destroy: function () {
clearInterval(this.setInterval);
this._super(...arguments);
},

//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------

/**
* Draws the whole countdown, including one countdown for each time unit.
*
* @private
*/
_render: function () {
this._updateTimeDiff();
_.each(this.diff, (val, index) => {
var canvas = val['canvas'];
var ctx = canvas.getContext("2d");
ctx.canvas.width = this.size;
ctx.canvas.height = this.size;
this._clearCanvas(ctx);

$(canvas).toggleClass('d-none', !val['visible']);
if (!val['visible']) {
return;
}

if (this.shapeBackground !== 'none') {
this._drawBgCircle(ctx, this.shapeBackground === 'plain');
}
this._drawText(canvas, val['nb'], val['label']);
if (this.progressBarStyle === 'surrounded') {
this._drawOuterCircleBg(ctx, this.progressBarWeight === 'thin');
}
if (this.progressBarStyle !== 'none') {
this._drawOuterCicle(ctx, val['nb'], val['total'], this.progressBarWeight === 'thin');
}
});
if (this.isFinished) {
clearInterval(this.setInterval);
if (!this.editableMode) {
this._handleEndCountdown();
}
}
},
/**
* Updates the remaining units into the `diff` object.
*
* @private
*/
_updateTimeDiff: function () {
var currentTimestamp = Date.now() / 1000;
var delta = this.endTime - currentTimestamp;

this.isFinished = delta < 0;
if (this.isFinished) {
this.diff['days']['nb'] = 0;
this.diff['hours']['nb'] = 0;
this.diff['minutes']['nb'] = 0;
this.diff['seconds']['nb'] = 0;
return;
}

this.hereBeforeTimerEnds = true;

if (this._isUnitVisible('d')) {
var days = Math.floor(delta / 86400);
delta -= days * 86400;
this.diff['days']['nb'] = days;
}
if (this._isUnitVisible('h')) {
var hours = Math.floor(delta / 3600) % 24;
delta -= hours * 3600;
this.diff['hours']['nb'] = hours;
}
if (this._isUnitVisible('m')) {
var minutes = Math.floor(delta / 60) % 60;
delta -= minutes * 60;
this.diff['minutes']['nb'] = minutes;
}
if (this._isUnitVisible('s')) {
var seconds = parseInt(delta);
this.diff['seconds']['nb'] = seconds;
}
},
/**
* Handles the action that should be executed once the countdown ends.
*
* @private
*/
_handleEndCountdown: function () {
if (this.endAction === 'redirect') {
var redirectUrl = this.$el[0].dataset.redirectUrl;
if (this.hereBeforeTimerEnds) {
// Wait a bit, if the landing page has the same publish date
setTimeout(function () {
window.location = redirectUrl;
}, 500);
} else {
// Show (non editable) msg when user lands on already finished countdown
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');
}
},
/**
* 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);
},

//--------------------------------------------------------------------------
// 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
*/
_drawText: function (canvas, textNb, textUnit) {
var ctx = canvas.getContext("2d");
var nbSize = this.size / 3;
ctx.font = `${nbSize}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = this.layout === 'dark' ? 'white' : 'black';
ctx.fillText(textNb, canvas.width / 2, canvas.height / 2);

var unitSize = this.size / 7;
ctx.font = `${unitSize}px Arial`;
ctx.fillText(textUnit, canvas.width / 2, canvas.height / 2 + nbSize / 2);
},
/**
* Draws a plain circle into the canvas.
*
* @private
* @param {RenderingContext} ctx - Context of the canvas
* @param {boolean} full - if true, the circle will be drawn up to the outer circle
*/
_drawBgCircle: function (ctx, full = false) {
ctx.strokeStyle = '#acb5ce78';
var rayon = this.size / 2;
if (this.progressBarWeight === 'thin') {
rayon -= full ? this.size / 15 : this.size / 29;
} else {
rayon -= full ? 0 : this.size / 10;
}
ctx.beginPath();
ctx.arc(this.size / 2, this.size / 2, rayon, 0, Math.PI * 2);
ctx.fillStyle = '#acb5ce78';
ctx.fill();
ctx.stroke();
},
/**
* Draws an arc around the plain circle.
*
* @private
* @param {RenderingContext} ctx - Context of the canvas
* @param {string} nbUnit - how many unit should fill the arc
* @param {string} totalUnit - number of unit to do a complete circle
* @param {boolean} thinLine - if true, the arc size will be thin
*/
_drawOuterCicle: function (ctx, nbUnit, totalUnit, thinLine) {
ctx.strokeStyle = '#9e8df5';
ctx.lineWidth = thinLine ? this.size / 35 : this.size / 10;
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();
},
/**
* Draws a full arc around the plain circle.
*
* @private
* @param {RenderingContext} ctx - Context of the canvas
*/
_drawOuterCircleBg: function (ctx, thinLine) {
ctx.strokeStyle = '#848996';
ctx.lineWidth = thinLine ? this.size / 35 : this.size / 10;
ctx.beginPath();
ctx.arc(this.size / 2, this.size / 2, this.size / 2 - this.size / 20, 0, Math.PI * 2);
ctx.stroke();
},
});

return {
Widget: publicWidget.Widget,
Animation: Animation,

0 comments on commit 9969764

Please sign in to comment.
You can’t perform that action at this time.