diff options
Diffstat (limited to 'client/lib')
-rw-r--r-- | client/lib/popup.js | 204 |
1 files changed, 204 insertions, 0 deletions
diff --git a/client/lib/popup.js b/client/lib/popup.js index 8095fbd2..8a55c2df 100644 --- a/client/lib/popup.js +++ b/client/lib/popup.js @@ -206,3 +206,207 @@ escapeActions.forEach(actionName => { }, ); }); + +// Prevent @member mentions on Add Comment input field +// from closing card, part 5. +// This duplicate below of above popup function is needed, because at +// wekan/components/main/editor.js at bottom is popping up visible +// @member mention, and it seems to trigger closing also card popup, +// so in below closing popup is disabled. +window.PopupNoClose = 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(); + } + + /// 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(name) { + const self = this; + const popupName = `${name}Popup`; + function clickFromPopup(evt) { + return $(evt.target).closest('.js-pop-over').length !== 0; + } + return function(evt) { + // If a popup is already opened, clicking again on the opener element + // should close it -- and interrupt the current `open` function. + /* + if (self.isOpen()) { + const previousOpenerElement = self._getTopStack().openerElement; + if (previousOpenerElement === evt.currentTarget) { + self.close(); + return; + } else { + $(previousOpenerElement).removeClass('is-active'); + } + } + */ + // 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. + let openerElement; + if (clickFromPopup(evt)) { + openerElement = self._getTopStack().openerElement; + } else { + self._stack = []; + openerElement = evt.currentTarget; + } + $(openerElement).addClass('is-active'); + 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({ + popupName, + openerElement, + hasPopupParent: clickFromPopup(evt), + title: self._getTitle(popupName), + depth: self._stack.length, + offset: self._getOffset(openerElement), + dataContext: (this && 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 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, + () => { + self._dep.depend(); + return { ...self._getTopStack(), stack: self._stack }; + }, + 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(name, action) { + const self = this; + + return function(evt, tpl) { + const context = (this.currentData && this.currentData()) || this; + context.__afterConfirmAction = action; + self.open(name).call(context, evt, tpl); + }; + } + + /// The public reactive state of the popup. + isOpen() { + this._dep.changed(); + return Boolean(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(n = 1) { + if (this._stack.length > n) { + _.times(n, () => this._stack.pop()); + this._dep.changed(); + } + // else { + // this.close(); + //} + } + + /// Close the current opened popup. + /* + close() { + if (this.isOpen()) { + Blaze.remove(this.current); + this.current = null; + + const openerElement = this._getTopStack().openerElement; + $(openerElement).removeClass('is-active'); + + this._stack = []; + } + } + */ + + getOpenerComponent() { + const { openerElement } = Template.parentData(4); + return BlazeComponent.getComponentForElement(openerElement); + } + + // An utility fonction that returns the top element of the internal stack + _getTopStack() { + return this._stack[this._stack.length - 1]; + } + + // 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(element) { + const $element = $(element); + return () => { + Utils.windowResizeDep.depend(); + + if (Utils.isMiniScreen()) return { left: 0, top: 0 }; + + const offset = $element.offset(); + const 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(popupName) { + return () => { + const 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. + const title = TAPi18n.__(translationKey); + // when popup showed as full of small screen, we need a default header to clearly see [X] button + const defaultTitle = Utils.isMiniScreen() ? '' : false; + return title !== translationKey ? title : defaultTitle; + }; + } +})(); |