ES6ify our Popup library

This is an experiment about the implications of ES6 transpilation in
our code base.

We should also define a new ES6 style guide and a jsHint
configuration, for instance semi-colons are automatically inserted at
the end of lines, so we may remove them. We also need to figure which
ES6 features we want to use, currently I have followed Meteor-core
guidance which is reasonable.
This commit is contained in:
Maxime Quandalle 2015-08-26 16:28:21 +02:00
parent 60712e1ac4
commit 46a5e08aa7

View file

@ -1,34 +1,55 @@
// A simple tracker dependency that we invalidate every time the window is // A simple tracker dependency that we invalidate every time the window is
// resized. This is used to reactively re-calculate the popup position in case // resized. This is used to reactively re-calculate the popup position in case
// of a window resize. // of a window resize. This is the equivalent of a "Signal" in some other
var windowResizeDep = new Tracker.Dependency(); // programming environments.
$(window).on('resize', function() { windowResizeDep.changed(); }); let windowResizeDep = new Tracker.Dependency()
$(window).on('resize', () => windowResizeDep.changed())
window.Popup = new class {
constructor() {
// The template we use to render popups
this.template = Template.popup
// We only want to display one popup at a time and we keep the view object
// in this `Popup._current` variable. If there is no popup currently opened
// the value is `null`.
this._current = null
// It's possible to open a sub-popup B from a popup A. In that case we keep
// the data of popup A so we can return back to it. Every time we open a new
// popup the stack grows, every time we go back the stack decrease, and if
// we close the popup the stack is reseted to the empty stack [].
this._stack = []
// We invalidate this internal dependency every time the top of the stack
// has changed and we want to re-render a popup with the new top-stack data.
this._dep = new Tracker.Dependency()
}
Popup = {
/// This function returns a callback that can be used in an event map: /// This function returns a callback that can be used in an event map:
/// ///
/// Template.tplName.events({ /// Template.tplName.events({
/// 'click .elementClass': Popup.open("popupName") /// 'click .elementClass': Popup.open("popupName")
/// }); /// })
/// ///
/// The popup inherit the data context of its parent. /// The popup inherit the data context of its parent.
open: function(name) { open(name) {
var self = this; let self = this
var popupName = name + 'Popup'; const popupName = `${name}Popup`
var clickFromPopup = function(evt) { function clickFromPopup(evt) {
return $(evt.target).closest('.js-pop-over').length !== 0; return $(evt.target).closest('.js-pop-over').length !== 0
}; }
return function(evt) { return function(evt) {
// If a popup is already openened, clicking again on the opener element // If a popup is already opened, clicking again on the opener element
// should close it -- and interupt the current `open` function. // should close it -- and interrupt the current `open` function.
if (self.isOpen()) { if (self.isOpen()) {
var previousOpenerElement = self._getTopStack().openerElement; let previousOpenerElement = self._getTopStack().openerElement
if (previousOpenerElement === evt.currentTarget) { if (previousOpenerElement === evt.currentTarget) {
return self.close(); return self.close()
} else { } else {
$(previousOpenerElement).removeClass('is-active'); $(previousOpenerElement).removeClass('is-active')
} }
} }
@ -37,28 +58,28 @@ Popup = {
// if the popup has no parent, or from the parent `openerElement` if it // if the popup has no parent, or from the parent `openerElement` if it
// has one. This allows us to position a sub-popup exactly at the same // has one. This allows us to position a sub-popup exactly at the same
// position than its parent. // position than its parent.
var openerElement; let openerElement
if (clickFromPopup(evt)) { if (clickFromPopup(evt)) {
openerElement = self._getTopStack().openerElement; openerElement = self._getTopStack().openerElement
} else { } else {
self._stack = []; self._stack = []
openerElement = evt.currentTarget; openerElement = evt.currentTarget
} }
$(openerElement).addClass('is-active'); $(openerElement).addClass('is-active')
evt.preventDefault(); evt.preventDefault()
// We push our popup data to the stack. The top of the stack is always // We push our popup data to the stack. The top of the stack is always
// used as the data source for our current popup. // used as the data source for our current popup.
self._stack.push({ self._stack.push({
popupName: popupName, popupName,
openerElement,
hasPopupParent: clickFromPopup(evt), hasPopupParent: clickFromPopup(evt),
title: self._getTitle(popupName), title: self._getTitle(popupName),
openerElement: openerElement,
depth: self._stack.length, depth: self._stack.length,
offset: self._getOffset(openerElement), offset: self._getOffset(openerElement),
dataContext: this.currentData && this.currentData() || this dataContext: this.currentData && this.currentData() || this,
}); })
// If there are no popup currently opened we use the Blaze API to render // If there are no popup currently opened we use the Blaze API to render
// one into the DOM. We use a reactive function as the data parameter that // one into the DOM. We use a reactive function as the data parameter that
@ -70,18 +91,16 @@ Popup = {
// our internal dependency, and since we just changed the top element of // our internal dependency, and since we just changed the top element of
// our internal stack, the popup will be updated with the new data. // our internal stack, the popup will be updated with the new data.
if (! self.isOpen()) { if (! self.isOpen()) {
self.current = Blaze.renderWithData(self.template, function() { self.current = Blaze.renderWithData(self.template, () => {
self._dep.depend(); self._dep.depend()
return _.extend(self._stack[self._stack.length - 1], { return _.extend(self._getTopStack(), { stack: self._stack })
stack: self._stack }, document.body)
});
}, document.body);
} else { } else {
self._dep.changed(); self._dep.changed()
} }
}; }
}, }
/// This function returns a callback that can be used in an event map: /// This function returns a callback that can be used in an event map:
/// ///
@ -89,119 +108,100 @@ Popup = {
/// 'click .elementClass': Popup.afterConfirm("popupName", function() { /// 'click .elementClass': Popup.afterConfirm("popupName", function() {
/// // What to do after the user has confirmed the action /// // What to do after the user has confirmed the action
/// }) /// })
/// }); /// })
afterConfirm: function(name, action) { afterConfirm(name, action) {
var self = this; let self = this
return function(evt, tpl) { return function(evt, tpl) {
var context = this.currentData && this.currentData() || this; let context = this.currentData && this.currentData() || this
context.__afterConfirmAction = action; context.__afterConfirmAction = action
self.open(name).call(context, evt, tpl); self.open(name).call(context, evt, tpl)
}; }
}, }
/// The public reactive state of the popup. /// The public reactive state of the popup.
isOpen: function() { isOpen() {
this._dep.changed(); this._dep.changed()
return !! this.current; return !! this.current
}, }
/// In case the popup was opened from a parent popup we can get back to it /// In case the popup was opened from a parent popup we can get back to it
/// with this `Popup.back()` function. You can go back several steps at once /// with this `Popup.back()` function. You can go back several steps at once
/// by providing a number to this function, e.g. `Popup.back(2)`. In this case /// by providing a number to this function, e.g. `Popup.back(2)`. In this case
/// intermediate popup won't even be rendered on the DOM. If the number of /// intermediate popup won't even be rendered on the DOM. If the number of
/// steps back is greater than the popup stack size, the popup will be closed. /// steps back is greater than the popup stack size, the popup will be closed.
back: function(n) { back(n = 1) {
n = n || 1; if (this._stack.length > n) {
var self = this; _.times(n, () => this._stack.pop())
if (self._stack.length > n) { this._dep.changed()
_.times(n, function() { self._stack.pop(); });
self._dep.changed();
} else { } else {
self.close(); this.close()
} }
}, }
/// Close the current opened popup. /// Close the current opened popup.
close: function() { close() {
if (this.isOpen()) { if (this.isOpen()) {
Blaze.remove(this.current); Blaze.remove(this.current)
this.current = null; this.current = null
var openerElement = this._getTopStack().openerElement; let openerElement = this._getTopStack().openerElement
$(openerElement).removeClass('is-active'); $(openerElement).removeClass('is-active')
this._stack = []; this._stack = []
} }
}, }
// The template we use for every popup
template: Template.popup,
// We only want to display one popup at a time and we keep the view object in
// this `Popup._current` variable. If there is no popup currently opened the
// value is `null`.
_current: null,
// It's possible to open a sub-popup B from a popup A. In that case we keep
// the data of popup A so we can return back to it. Every time we open a new
// popup the stack grows, every time we go back the stack decrease, and if we
// close the popup the stack is reseted to the empty stack [].
_stack: [],
// We invalidate this internal dependency every time the top of the stack has
// changed and we want to render a popup with the new top-stack data.
_dep: new Tracker.Dependency(),
// An utility fonction that returns the top element of the internal stack // An utility fonction that returns the top element of the internal stack
_getTopStack: function() { _getTopStack() {
return this._stack[this._stack.length - 1]; return this._stack[this._stack.length - 1]
}, }
// We automatically calculate the popup offset from the reference element // We automatically calculate the popup offset from the reference element
// position and dimensions. We also reactively use the window dimensions to // position and dimensions. We also reactively use the window dimensions to
// ensure that the popup is always visible on the screen. // ensure that the popup is always visible on the screen.
_getOffset: function(element) { _getOffset(element) {
var $element = $(element); let $element = $(element)
return function() { return () => {
windowResizeDep.depend(); windowResizeDep.depend()
var offset = $element.offset(); const offset = $element.offset()
var popupWidth = 300 + 15; const popupWidth = 300 + 15
return { return {
left: Math.min(offset.left, $(window).width() - popupWidth), left: Math.min(offset.left, $(window).width() - popupWidth),
top: offset.top + $element.outerHeight() top: offset.top + $element.outerHeight(),
}; }
}; }
}, }
// We get the title from the translation files. Instead of returning the // We get the title from the translation files. Instead of returning the
// result, we return a function that compute the result and since `TAPi18n.__` // result, we return a function that compute the result and since `TAPi18n.__`
// is a reactive data source, the title will be changed reactively. // is a reactive data source, the title will be changed reactively.
_getTitle: function(popupName) { _getTitle(popupName) {
return function() { return () => {
var translationKey = popupName + '-title'; const translationKey = `${popupName}-title`
// XXX There is no public API to check if there is an available // XXX There is no public API to check if there is an available
// translation for a given key. So we try to translate the key and if the // translation for a given key. So we try to translate the key and if the
// translation output equals the key input we deduce that no translation // translation output equals the key input we deduce that no translation
// was available and returns `false`. There is a (small) risk a false // was available and returns `false`. There is a (small) risk a false
// positives. // positives.
var title = TAPi18n.__(translationKey); const title = TAPi18n.__(translationKey)
return title !== translationKey ? title : false; return title !== translationKey ? title : false
}; }
} }
}; }
// We close a potential opened popup on any left click on the document, or go // We close a potential opened popup on any left click on the document, or go
// one step back by pressing escape. // one step back by pressing escape.
var escapeActions = ['back', 'close']; const escapeActions = ['back', 'close']
_.each(escapeActions, function(actionName) { _.each(escapeActions, (actionName) => {
EscapeActions.register('popup-' + actionName, EscapeActions.register(`popup-${actionName}`,
_.bind(Popup[actionName], Popup), () => Popup[actionName](),
_.bind(Popup.isOpen, Popup), { () => Popup.isOpen(),
{
noClickEscapeOn: '.js-pop-over', noClickEscapeOn: '.js-pop-over',
enabledOnClick: actionName === 'close' enabledOnClick: actionName === 'close',
} }
); )
}); })