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 21d1289 commit 9e971c926d2450d9f377009b0682f14899b35c5d
@@ -334,6 +334,9 @@ var SnippetEditor = Widget.extend({
uiEl.querySelectorAll('we-input').forEach(inputEl => {
options.Class.prototype.buildInputElement(inputEl);
});
uiEl.querySelectorAll('we-datetimepicker').forEach(inputEl => {
options.Class.prototype.buildDatetimepickerElement(inputEl);
});

return $optionSection;
},
@@ -5,6 +5,7 @@ var core = require('web.core');
var ColorPaletteWidget = require('web_editor.ColorPalette').ColorPaletteWidget;
var Dialog = require('web.Dialog');
var rte = require('web_editor.rte');
var time = require('web.time');
var Widget = require('web.Widget');
var weWidgets = require('wysiwyg.widgets');

@@ -341,7 +342,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]) {
@@ -533,7 +534,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;
}
@@ -553,7 +554,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;
}
@@ -648,6 +649,58 @@ var SnippetOption = Widget.extend({
inputWrapperEl.appendChild(inputEl);
inputWrapperEl.appendChild(unitEl);
},
/**
* Build the correct DOM for a we-datetimepicker element.
*
* @static
* @param {HTMLElement} inputWrapperEl
*/
buildDatetimepickerElement: function (inputWrapperEl) {
var titleEl = SnippetOption.prototype.stringToTitle(inputWrapperEl);

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

var weInput = document.createElement('we-input');
// TODO transfer data attribute from we-datetimepicker to we-input
weInput.setAttribute('data-end-time', '');

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

var faIcon = document.createElement('i');
faIcon.setAttribute('class', 'fa fa-calendar');
faIcon.setAttribute('data-target', '#' + datetimePickerId);
faIcon.setAttribute('data-toggle', 'datetimepicker');

weInput.appendChild(inputEl);
weInput.appendChild(faIcon);

inputWrapperEl.appendChild(titleEl);
inputWrapperEl.appendChild(weInput);

var datepickersOptions = {
minDate: moment({y: 1900}),
maxDate: moment().add(200, 'y'),
calendarWeeks: true,
defaultDate: moment().format(),
icons: {
time: 'fa fa-clock-o',
date: 'fa fa-calendar',
next: 'fa fa-chevron-right',
previous: 'fa fa-chevron-left',
up: 'fa fa-chevron-up',
down: 'fa fa-chevron-down',
},
locale: moment.locale(),
format: time.getLangDatetimeFormat(),
widgetParent: 'body',
};
$(inputEl).datetimepicker(datepickersOptions);
},
/**
* Build the correct DOM for a we-select element.
*
Binary file not shown.
@@ -1155,6 +1155,280 @@ registry.anchorSlide = publicWidget.Widget.extend({
},
});


registry.countdown = publicWidget.Widget.extend({
selector: '.s_countdown',
disabledInEditableMode: false,

/**
* @override
*/
start: function () {
var $el = this.$el;
this.endAction = $el.attr('data-end-action');
this.endTime = parseInt($el.attr('data-end-time'));
this.size = $el.attr('data-size');
this.layout = $el.attr('data-layout');
this.dataDisplay = this.$el.attr('data-display');

this._initTimeDiff();
this._drawCountdown();

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

// Show (non editable) msg when user lands on already finished countdown
if (this.isFinished && this.endAction === 'redirect' && !this.editableMode) {
this.$el.find('.s_countdown_end_redirect_message').removeClass('d-none');
return this._super.apply(this, arguments);
}

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

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

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

/**
* Draws the whole countdown, including one countdown for each time unit.
*
* @private
*/
_drawCountdown: 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.layout === 'dark') {
this._drawBgCircle(ctx, true);
}
if (this.layout === 'thin-filled') {
this._drawBgCircle(ctx);
}
this._drawText(canvas, val['nb'], index);
if (this.layout === 'light') {
this._drawOuterCircleBg(ctx);
}
this._drawOuterCicle(ctx, val['nb'], val['total'], this.layout === 'thin' || this.layout === 'thin-filled');
});
},
/**
* Initializes the `diff` object. It will contains for every time unit the
* related canvas, its total step and its visibility.
*
* @private
*/
_initTimeDiff: function () {
this.diff = {
'days': {
'canvas': this.$el.find('.s_countdown_days canvas')[0],
'total': 50, // TODO RATIO DAYS ? Based on how many days ?
'visible': this._isUnitVisible('d'),
},
'hours': {
'canvas': this.$el.find('.s_countdown_hours canvas')[0],
'total': 24,
'visible': this._isUnitVisible('h'),
},
'minutes': {
'canvas': this.$el.find('.s_countdown_minutes canvas')[0],
'total': 60,
'visible': this._isUnitVisible('m'),
},
'seconds': {
'canvas': this.$el.find('.s_countdown_seconds canvas')[0],
'total': 60,
'visible': this._isUnitVisible('s'),
},
};
},
/**
* Manages the countdown by drawing it and handling the end action.
* Call every seconds by the setInterval.
*
* @private
*/
_manageCountdown: function () {
this._drawCountdown();
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;
}

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') {
window.location = this.$target.find('.s_countdown_end_redirect_url').attr('href');
} else if (this.endAction === 'message') {
this.$el.find('.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.dataDisplay.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 / 4;
ctx.font = `${nbSize}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = this.$el.attr('data-layout') === 'dark' ? 'white' : 'black';
ctx.fillText(textNb, canvas.width / 2, canvas.height / 2);

var unitSize = this.size / 10;
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 = 'white';
var minus = full ? 0 : this.size / 10;
if (this.layout === 'thin-filled') {
// TODO: chose the styles we want and clean this mess
minus = 5;
}
ctx.beginPath();
ctx.arc(this.size / 2, this.size / 2, this.size / 2 - minus, 0, Math.PI * 2);
ctx.fillStyle = '#777d8e';
if (this.layout === 'thin-filled') {
// TODO: this should be color pickers
ctx.fillStyle = '#acb5ce';
}
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 ? 5 : 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) {
ctx.strokeStyle = '#31343c';
ctx.lineWidth = 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 9e971c9

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