123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- // 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
- // return the the complete along with its top element 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 _.extend(self._stack[self._stack.length - 1], {
- stack: self._stack,
- containerTranslation: (self._stack.length - 1) * -300
- });
- }, 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();
- }
- });
- // Press escape to close the popup.
- var bindPopup = function(f) { return _.bind(f, Popup); };
- EscapeActions.register('popup',
- bindPopup(Popup.isOpen),
- bindPopup(Popup.close)
- );
|