wekan/client/lib/popup.js
Maxime Quandalle 2dbea30842 Renaissance
_,,ad8888888888bba,_
                  ,ad88888I888888888888888ba,
                ,88888888I88888888888888888888a,
              ,d888888888I8888888888888888888888b,
             d88888PP"""" ""YY88888888888888888888b,
           ,d88"'__,,--------,,,,.;ZZZY8888888888888,
          ,8IIl'"                ;;l"ZZZIII8888888888,
         ,I88l;'                  ;lZZZZZ888III8888888,
       ,II88Zl;.                  ;llZZZZZ888888I888888,
      ,II888Zl;.                .;;;;;lllZZZ888888I8888b
     ,II8888Z;;                 `;;;;;''llZZ8888888I8888,
     II88888Z;'                        .;lZZZ8888888I888b
     II88888Z; _,aaa,      .,aaaaa,__.l;llZZZ88888888I888
     II88888IZZZZZZZZZ,  .ZZZZZZZZZZZZZZ;llZZ88888888I888,
     II88888IZZ<'(@@>Z|  |ZZZ<'(@@>ZZZZ;;llZZ888888888I88I
    ,II88888;   `""" ;|  |ZZ; `"""     ;;llZ8888888888I888
    II888888l            `;;          .;llZZ8888888888I888,
   ,II888888Z;           ;;;        .;;llZZZ8888888888I888I
   III888888Zl;    ..,   `;;       ,;;lllZZZ88888888888I888
   II88888888Z;;...;(_    _)      ,;;;llZZZZ88888888888I888,
   II88888888Zl;;;;;' `--'Z;.   .,;;;;llZZZZ88888888888I888b
   ]I888888888Z;;;;'   ";llllll;..;;;lllZZZZ88888888888I8888,
   II888888888Zl.;;"Y88bd888P";;,..;lllZZZZZ88888888888I8888I
   II8888888888Zl;.; `"PPP";;;,..;lllZZZZZZZ88888888888I88888
   II888888888888Zl;;. `;;;l;;;;lllZZZZZZZZW88888888888I88888
   `II8888888888888Zl;.    ,;;lllZZZZZZZZWMZ88888888888I88888
    II8888888888888888ZbaalllZZZZZZZZZWWMZZZ8888888888I888888,
    `II88888888888888888b"WWZZZZZWWWMMZZZZZZI888888888I888888b
     `II88888888888888888;ZZMMMMMMZZZZZZZZllI888888888I8888888
      `II8888888888888888 `;lZZZZZZZZZZZlllll888888888I8888888,
       II8888888888888888, `;lllZZZZllllll;;.Y88888888I8888888b,
      ,II8888888888888888b   .;;lllllll;;;.;..88888888I88888888b,
      II888888888888888PZI;.  .`;;;.;;;..; ...88888888I8888888888,
      II888888888888PZ;;';;.   ;. .;.  .;. .. Y8888888I88888888888b,
     ,II888888888PZ;;'                        `8888888I8888888888888b,
     II888888888'                              888888I8888888888888888
    ,II888888888                              ,888888I8888888888888888
   ,d88888888888                              d888888I8888888888ZZZZZZ
,ad888888888888I                              8888888I8888ZZZZZZZZZZZZ
888888888888888'                              888888IZZZZZZZZZZZZZZZZZ
8888888888P'8P'                               Y888ZZZZZZZZZZZZZZZZZZZZ
888888888,  "                                 ,ZZZZZZZZZZZZZZZZZZZZZZZ
8888888888,                                ,ZZZZZZZZZZZZZZZZZZZZZZZZZZ
888888888888a,      _                    ,ZZZZZZZZZZZZZZZZZZZZ88888888
888888888888888ba,_d'                  ,ZZZZZZZZZZZZZZZZZ8888888888888
8888888888888888888888bbbaaa,,,______,ZZZZZZZZZZZZZZZ88888888888888888
88888888888888888888888888888888888ZZZZZZZZZZZZZZZ88888888888888888888
8888888888888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888
888888888888888888888888888888888ZZZZZZZZZZZZZZ88888888888888888888888
8888888888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888888
88888888888888888888888888888ZZZZZZZZZZZZZZ888888888888888888888888888
8888888888888888888888888888ZZZZZZZZZZZZZZ88888888888888888 Normand  8
88888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888 Veilleux 8
8888888888888888888888888ZZZZZZZZZZZZZZ8888888888888888888888888888888
2015-05-12 19:33:50 +02:00

200 lines
7.4 KiB
JavaScript

// 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
// of a window resize.
var windowResizeDep = new Tracker.Dependency();
$(window).on('resize', function() { windowResizeDep.changed(); });
Popup = {
/// This function returns a callback that can be used in an event map:
///
/// Template.tplName.events({
/// 'click .elementClass': Popup.open("popupName")
/// });
///
/// The popup inherit the data context of its parent.
open: function(name) {
var self = this;
var popupName = name + 'Popup';
return function(evt) {
// If a popup is already openened, clicking again on the opener element
// should close it -- and interupt the current `open` function.
if (self.isOpen() &&
self._getTopStack().openerElement === evt.currentTarget) {
return self.close();
}
// We determine the `openerElement` (the DOM element that is being clicked
// and the one we take in reference to position the popup) from the event
// 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
// position than its parent.
var openerElement;
if (self._hasPopupParent()) {
openerElement = self._getTopStack().openerElement;
} else {
self._stack = [];
openerElement = evt.currentTarget;
}
// We modify the event to prevent the popup being closed when the event
// bubble up to the document element.
evt.originalEvent.clickInPopup = true;
evt.preventDefault();
// We push our popup data to the stack. The top of the stack is always
// used as the data source for our current popup.
self._stack.push({
__isPopup: true,
popupName: popupName,
hasPopupParent: self._hasPopupParent(),
title: self._getTitle(popupName),
openerElement: openerElement,
offset: self._getOffset(openerElement),
dataContext: this.currentData && this.currentData() || this
});
// 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
// just return the top element on the stack and depends on our internal
// dependency that is being invalidated every time the top element of the
// stack has changed and we want to update the popup.
//
// Otherwise if there is already a popup open we just need to invalidate
// our internal dependency, and since we just changed the top element of
// our internal stack, the popup will be updated with the new data.
if (! self.isOpen()) {
self.current = Blaze.renderWithData(self.template, function() {
self._dep.depend();
return self._stack[self._stack.length - 1];
}, document.body);
} else {
self._dep.changed();
}
};
},
/// This function returns a callback that can be used in an event map:
///
/// Template.tplName.events({
/// 'click .elementClass': Popup.afterConfirm("popupName", function() {
/// // What to do after the user has confirmed the action
/// })
/// });
afterConfirm: function(name, action) {
var self = this;
return function(evt, tpl) {
var context = this;
context.__afterConfirmAction = action;
self.open(name).call(context, evt, tpl);
};
},
/// The public reactive state of the popup.
isOpen: function() {
this._dep.changed();
return !! this.current;
},
/// 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
/// 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
/// steps back is greater than the popup stack size, the popup will be closed.
back: function(n) {
n = n || 1;
var self = this;
if (self._stack.length > n) {
_.times(n, function() { self._stack.pop(); });
self._dep.changed();
} else {
self.close();
}
},
/// Close the current opened popup.
close: function() {
if (this.isOpen()) {
Blaze.remove(this.current);
this.current = null;
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
_getTopStack: function() {
return this._stack[this._stack.length - 1];
},
// We use the blaze API to determine if the current popup has been opened from
// a parent popup. The number we give to the `Template.parentData` has been
// determined experimentally and is susceptible to change if you modify the
// `Popup.template`
_hasPopupParent: function() {
var tryParentData = Template.parentData(3);
return !! (tryParentData && tryParentData.__isPopup);
},
// We automatically calculate the popup offset from the reference element
// position and dimensions. We also reactively use the window dimensions to
// ensure that the popup is always visible on the screen.
_getOffset: function(element) {
var $element = $(element);
return function() {
windowResizeDep.depend();
var offset = $element.offset();
var popupWidth = 300 + 15;
return {
left: Math.min(offset.left, $(window).width() - popupWidth),
top: offset.top + $element.outerHeight()
};
};
},
// We get the title from the translation files. Instead of returning the
// result, we return a function that compute the result and since `TAPi18n.__`
// is a reactive data source, the title will be changed reactively.
_getTitle: function(popupName) {
return function() {
var translationKey = popupName + '-title';
// 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 output equals the key input we deduce that no translation
// was available and returns `false`. There is a (small) risk a false
// positives.
var title = TAPi18n.__(translationKey);
return title !== translationKey ? title : false;
};
}
};
// We automatically close a potential opened popup on any left click on the
// document. To avoid closing it unexpectedly we modify the bubbled event in
// case the click event happen in the popup or in a button that open a popup.
$(document).on('click', function(evt) {
if (evt.which === 1 && ! (evt.originalEvent &&
evt.originalEvent.clickInPopup)) {
Popup.close();
}
});