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