|  | @@ -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;
 | 
	
		
			
				|  |  | +    };
 | 
	
		
			
				|  |  | +  }
 | 
	
		
			
				|  |  | +})();
 |