2026-02-07 16:30:08 +00:00
import { BlazeComponent } from 'meteor/peerlibrary:blaze-components' ;
import { Template } from 'meteor/templating' ;
const PopupBias = {
Before : Symbol ( "S" ) ,
Overlap : Symbol ( "M" ) ,
After : Symbol ( "A" ) ,
Fullscreen : Symbol ( "F" ) ,
includes ( e ) {
return Object . values ( this ) . includes ( e ) ;
}
}
// this class is a bit cumbersome and could probably be done simpler.
// it manages two things : initial placement and sizing given an opener element,
// and then movement and resizing. one difficulty was to be able, as a popup
// which can be resized from the "outside" (CSS4) and move from the inside (inner
// component), which also grows and shrinks frequently, to adapt.
// I tried many approach and failed to get the perfect fit; I feel that there is
// always something indeterminate at some point. so the only drawback is that
// if a popup contains another resizable component (e.g. card details), and if
// it has been resized (with CSS handle), it will lose its dimensions when dragging
// it next time.
class PopupDetachedComponent extends BlazeComponent {
onCreated ( ) {
// Set by parent/caller (usually PopupComponent)
( { nonPlaceholderOpener : this . nonPlaceholderOpener , closeDOMs : this . closeDOMs = [ ] , followDOM : this . followDOM } = this . data ( ) ) ;
if ( typeof ( this . closeDOMs ) === "string" ) {
// helper for passing arg in JADE template
this . closeDOMs = this . closeDOMs . split ( ';' ) ;
}
// The popup's own header, if it exists
this . closeDOMs . push ( "click .js-close-detached-popup" ) ;
}
// Main intent of this component is to have a modular popup with defaults:
// - sticks to its opener while being a child of body (thus in the same stacking context, no z-index issue)
// - is responsive on shrink while keeping position absolute
// - can grow back to initial position step by step
// - exposes various sizes as CSS variables so each rendered popup can use them to adapt defaults
// * issue is that it is done by hand, with heurisitic/simple algorithm from my thoughts, not sure it covers edge cases
// * however it works well so far and maybe more "fixed" element should be popups
onRendered ( ) {
// Remember initial ratio between initial dimensions and viewport
const viewportHeight = window . innerHeight ;
const viewportWidth = window . innerWidth ;
this . popup = this . firstNode ( ) ;
this . popupOpener = this . data ( ) . openerElement ;
const popupStyle = window . getComputedStyle ( this . firstNode ( ) ) ;
// margin may be in a relative unit, not computable in JS, but we get the actual pixels here
this . popupMargin = parseFloat ( popupStyle . getPropertyValue ( "--popup-margin" ) , 10 ) || Math . min ( window . innerWidth / 50 , window . innerHeight / 50 ) ;
this . dims ( this . computeMaxDims ( ) ) ;
this . initialPopupWidth = this . popupDims . width ;
this . initialPopupHeight = this . popupDims . height ;
this . initialHeightRatio = this . initialPopupHeight / viewportHeight ;
this . initialWidthRatio = this . initialPopupWidth / viewportWidth ;
this . dims ( this . computePopupDims ( ) ) ;
if ( this . followDOM ) {
this . innerElement = this . find ( this . followDOM ) ? ? document . querySelector ( this . followDOM ) ;
}
this . follow ( ) ;
this . toFront ( ) ;
// #FIXME the idea of keeping the initial ratio on resize is quite bad. remove that part.
// there is a reactive variable for window resize in Utils, but the interface is too slow
// with all reactive stuff, use events when possible and when not really bypassing logic
$ ( window ) . on ( 'resize' , ( ) => {
// #FIXME there is a bug when window grows; popup outer container
// will grow beyond the size of content and it's not easy to fix for me (and I feel tired of this popup)
this . dims ( this . computePopupDims ( ) ) ;
} ) ;
}
margin ( ) {
return this . popupMargin ;
}
ensureDimsLimit ( dims ) {
// boilerplate to make sure that popup visually fits
let { left , top , width , height } = dims ;
let overflowBottom = top + height + 2 * this . margin ( ) - window . innerHeight ;
let overflowRight = left + width + 2 * this . margin ( ) - window . innerWidth ;
if ( overflowRight > 0 ) {
width = Math . max ( 20 * this . margin ( ) , Math . min ( width - overflowRight , window . innerWidth - 2 * this . margin ( ) ) ) ;
}
if ( overflowBottom > 0 ) {
height = Math . max ( 10 * this . margin ( ) , Math . min ( height - overflowBottom , window . innerHeight - 2 * this . margin ( ) ) ) ;
}
left = Math . max ( left , this . margin ( ) ) ;
top = Math . max ( top , this . margin ( ) ) ;
return { left , top , width , height }
}
dims ( newDims ) {
if ( ! this . popupDims ) {
this . popupDims = { } ;
}
if ( newDims ) {
newDims = this . ensureDimsLimit ( newDims ) ;
for ( const e of Object . keys ( newDims ) ) {
let value = parseFloat ( newDims [ e ] ) ;
if ( ! isNaN ( value ) ) {
$ ( this . popup ) . css ( e , ` ${ value } px ` ) ;
this . popupDims [ e ] = value ;
}
}
}
return this . popupDims ;
}
isFullscreen ( ) {
return this . fullscreen ;
}
maximize ( ) {
this . fullscreen = true ;
this . dims ( this . computePopupDims ( ) ) ;
if ( this . innerElement ) {
$ ( this . innerElement ) . css ( 'width' , '' ) ;
$ ( this . innerElement ) . css ( 'height' , '' )
}
}
minimize ( ) {
this . fullscreen = false ;
this . dims ( this . computePopupDims ( ) ) ;
}
follow ( ) {
const adaptChild = new ResizeObserver ( ( _ ) => {
if ( this . fullscreen ) { return }
const width = this . innerElement ? . scrollWidth || this . popup . scrollWidth ;
const height = this . innerElement ? . scrollHeight || this . popup . scrollHeight ;
// we don't want to run this during something that we have caused, eg. dragging
if ( ! this . mouseDown ) {
// extra-"future-proof" stuff: if somebody adds a margin to the popup, it would trigger a loop
if ( Math . abs ( this . dims ( ) . width - width ) < 20 && Math . abs ( this . dims ( ) . height - height ) < 20 ) { return }
// if inner shrinks, follow
if ( width < this . dims ( ) . width || height < this . dims ( ) . height ) {
this . dims ( { width , height } ) ;
}
// otherwise it may be complicated to find a generic situation, but we have the
// classic positionning procedure which works, so use it and ignore positionning
else {
const newDims = this . computePopupDims ( ) ;
// a bit twisted/ad-hoc for card details, in the edge case where they are opened when collapsed then uncollapsed,
// not sure to understand why the sizing works differently that starting uncollapsed then doing the same sequence
this . dims ( this . ensureDimsLimit ( {
top : this . dims ( ) . top ,
left : this . dims ( ) . left ,
width : Math . max ( newDims . width , width ) ,
height : Math . max ( newDims . height , height )
} ) ) ;
}
}
else {
const { width , height } = this . popup . getBoundingClientRect ( ) ;
// only case when we bypass .dims(), to avoid loop
this . popupDims . width = width ;
this . popupDims . height = height ;
}
} ) ;
if ( this . innerElement ) {
adaptChild . observe ( this . innerElement ) ;
} else {
adaptChild . observe ( this . popup ) ;
}
}
currentZ ( z = undefined ) {
// relative, add a constant to be above root elements
if ( z !== undefined ) {
this . firstNode ( ) . style . zIndex = parseInt ( z ) + 10 ;
}
return parseInt ( this . firstNode ( ) . style . zIndex ) - 10 ;
}
// a bit complex...
toFront ( ) {
this . currentZ ( Math . max ( ... PopupComponent . stack . map ( p => BlazeComponent . getComponentForElement ( p . outerView . firstNode ( ) ) . currentZ ( ) ) ) || 0 + 1 ) ;
}
toBack ( ) {
this . currentZ ( Math . min ( ... PopupComponent . stack . map ( p => BlazeComponent . getComponentForElement ( p . outerView . firstNode ( ) ) . currentZ ( ) ) ) || 1 - 1 ) ;
}
events ( ) {
// needs to be done at this level; "parent" is not a parent in DOM
let closeEvents = { } ;
this . closeDOMs ? . forEach ( ( e ) => {
closeEvents [ e ] = ( _ ) => {
this . parentComponent ( ) . destroy ( ) ;
}
} )
const miscEvents = {
'click .js-confirm' ( ) {
this . data ( ) . afterConfirm ? . call ( this ) ;
} ,
// bad heuristic but only for best-effort UI
'pointerdown .pop-over' ( ) {
this . mouseDown = true ;
} ,
'pointerup .pop-over' ( ) {
this . mouseDown = false ;
}
} ;
const movePopup = ( event ) => {
event . preventDefault ( ) ;
$ ( event . target ) . addClass ( 'is-active' ) ;
const deltaHandleX = this . dims ( ) . left - event . clientX ;
const deltaHandleY = this . dims ( ) . top - event . clientY ;
const onPointerMove = ( e ) => {
this . dims ( this . ensureDimsLimit ( { left : e . clientX + deltaHandleX , top : e . clientY + deltaHandleY , width : this . dims ( ) . width , height : this . dims ( ) . height } ) ) ;
if ( this . popup . scrollY ) {
this . popup . scrollTo ( 0 , 0 ) ;
}
} ;
const onPointerUp = ( event ) => {
$ ( document ) . off ( 'pointermove' , onPointerMove ) ;
$ ( document ) . off ( 'pointerup' , onPointerUp ) ;
$ ( event . target ) . removeClass ( 'is-active' ) ;
} ;
if ( Utils . shouldIgnorePointer ( event ) ) {
onPointerUp ( event ) ;
return ;
}
$ ( document ) . on ( 'pointermove' , onPointerMove ) ;
$ ( document ) . on ( 'pointerup' , onPointerUp ) ;
} ;
// We do not manage dragging without our own header
const handleDOM = this . data ( ) . handleDOM ;
if ( this . data ( ) . showHeader ) {
const handleSelector = Utils . isMiniScreen ( ) ? '.js-popup-drag-handle' : '.header-title' ;
miscEvents [ ` pointerdown ${ handleSelector } ` ] = ( e ) => movePopup ( e ) ;
}
if ( handleDOM ) {
miscEvents [ ` pointerdown ${ handleDOM } ` ] = ( e ) => movePopup ( e ) ;
}
return super . events ( ) . concat ( closeEvents ) . concat ( miscEvents ) ;
}
computeMaxDims ( ) {
// Get size of inner content, even if it overflows
const content = this . find ( '.content' ) ;
let popupHeight = content . scrollHeight ;
let popupWidth = content . scrollWidth ;
if ( this . data ( ) . showHeader ) {
const headerRect = this . find ( '.header' ) ;
popupHeight += headerRect . scrollHeight ;
popupWidth = Math . max ( popupWidth , headerRect . scrollWidth )
}
return { width : Math . max ( popupWidth , $ ( this . popup ) . width ( ) ) , height : Math . max ( popupHeight , $ ( this . popup ) . height ( ) ) } ;
}
placeOnSingleDimension ( elementLength , openerPos , openerLength , maxLength , biases , n ) {
// avoid too much recursion if no solution
if ( ! n ) {
n = 0 ;
}
if ( n >= 5 ) {
// if we exhausted a bias, remove it
n = 0 ;
biases . pop ( ) ;
if ( biases . length === 0 ) {
return - 1 ;
}
} else {
n += 1 ;
}
if ( ! biases ? . length ) {
const cut = maxLength / 3 ;
if ( openerPos < cut ) {
// Corresponds to the default ordering: if element is close to the axe's start,
// try to put the popup after it; then to overlap; and give up otherwise.
biases = [ PopupBias . After , PopupBias . Overlap ]
}
else if ( openerPos > 2 * cut ) {
// Same idea if popup is close to the end
biases = [ PopupBias . Before , PopupBias . Overlap ]
}
else {
// If in the middle, try to overlap: choosing between start or end, even for
// default, is too arbitrary; a custom order can be passed in argument.
biases = [ PopupBias . Overlap ]
}
}
// Remove the first element and get it
const bias = biases . splice ( 0 , 1 ) [ 0 ] ;
let factor ;
const openerRef = openerPos + openerLength / 2 ;
if ( bias === PopupBias . Before ) {
factor = 1 ;
}
else if ( bias === PopupBias . Overlap ) {
factor = openerRef / maxLength ;
}
else {
factor = 0 ;
}
let candidatePos = openerRef - elementLength * factor ;
const deltaMax = candidatePos + elementLength - maxLength ;
if ( candidatePos < 0 || deltaMax > 0 ) {
if ( deltaMax <= 2 * this . margin ( ) ) {
// if this is just a matter of margin, try again
// useful for (literal) corner cases
biases = [ bias ] . concat ( biases ) ;
openerPos -= 5 ;
}
if ( biases . length === 0 ) {
// we could have returned candidate position even if the size is too large, so
// that the caller can choose, but it means more computations and edge cases...
// any negative means fullscreen overall as the caller will take the maximum between
// margin and candidate.
return - 1 ;
}
return this . placeOnSingleDimension ( elementLength , openerPos , openerLength , maxLength , biases , n ) ;
}
return candidatePos ;
}
computePopupDims ( ) {
if ( ! this . isRendered ? . ( ) ) {
return ;
}
// Coordinates of opener related to viewport
let { x : parentX , y : parentY } = this . nonPlaceholderOpener . getBoundingClientRect ( ) ;
let { height : parentHeight , width : parentWidth } = this . nonPlaceholderOpener . getBoundingClientRect ( ) ;
// Initial dimensions scaled to the viewport, if it has changed
let popupHeight = window . innerHeight * this . initialHeightRatio ;
let popupWidth = window . innerWidth * this . initialWidthRatio ;
if ( this . fullscreen || Utils . isMiniScreen ( ) && popupWidth >= 4 * window . innerWidth / 5 && popupHeight >= 4 * window . innerHeight / 5 ) {
// Go fullscreen!
popupWidth = window . innerWidth ;
// Avoid address bar, let a bit of margin to scroll
popupHeight = 4 * window . innerHeight / 5 ;
return ( {
width : window . innerWidth ,
height : window . innerHeight ,
left : 0 ,
top : 0 ,
2015-05-22 20:17:40 +02:00
} ) ;
2026-02-07 16:30:08 +00:00
} else {
// Current viewport dimensions
let maxHeight = window . innerHeight - this . margin ( ) * 2 ;
let maxWidth = window . innerWidth - this . margin ( ) * 2 ;
let biasX , biasY ;
if ( Utils . isMiniScreen ( ) ) {
// On mobile I found that being able to close a popup really close from where it has been clicked
// is comfortable; so given that the close button is top-right, we prefer the position of
// popup being right-bottom, when possible. We then try every position, rather than choosing
// relatively to the relative position of opener in viewport
biasX = [ PopupBias . Before , PopupBias . Overlap , PopupBias . After ] ;
biasY = [ PopupBias . After , PopupBias . Overlap , PopupBias . Before ] ;
}
const candidateX = this . placeOnSingleDimension ( popupWidth , parentX , parentWidth , maxWidth , biasX ) ;
const candidateY = this . placeOnSingleDimension ( popupHeight , parentY , parentHeight , maxHeight , biasY ) ;
// Reasonable defaults that can be overriden by CSS later: popups are tall, try to fit the reste
// of the screen starting from parent element, or full screen if element if not fitting
return ( {
width : popupWidth ,
height : popupHeight ,
left : candidateX ,
top : candidateY ,
} ) ;
}
}
}
class PopupComponent extends BlazeComponent {
static stack = [ ] ;
// good enough as long as few occurences of such cases
static multipleBlacklist = [ "cardDetails" ] ;
// to provide compatibility with Popup.open().
static open ( args ) {
const openerView = Blaze . getView ( args . openerElement ) ;
if ( ! openerView ) {
console . warn ( ` no parent found for popup ${ args . name } , attaching to body: this should not happen ` ) ;
}
// render ourselves; everything is automatically managed from that moment, we just added
// a level of indirection but this will not interfere with data
const popup = new PopupComponent ( ) ;
Blaze . renderWithData (
popup . renderComponent ( BlazeComponent . currentComponent ( ) ) ,
args ,
args . openerElement ,
null ,
openerView
) ;
return popup ;
}
static destroy ( ) {
PopupComponent . stack . at ( - 1 ) ? . destroy ( ) ;
}
static findParentPopup ( element ) {
return BlazeComponent . getComponentForElement ( $ ( element ) . closest ( '.pop-over' ) [ 0 ] ) ;
}
static toFront ( event ) {
const popup = PopupComponent . findParentPopup ( event . target )
popup ? . toFront ( ) ;
return popup ;
}
static toBack ( event ) {
const popup = PopupComponent . findParentPopup ( event . target ) ;
popup ? . toBack ( ) ;
return popup ;
}
static maximize ( event ) {
const popup = PopupComponent . findParentPopup ( event . target ) ;
popup ? . toFront ( ) ;
popup ? . maximize ( ) ;
return popup ;
}
static minimize ( event ) {
const popup = PopupComponent . findParentPopup ( event . target ) ;
popup ? . minimize ( ) ;
return popup ;
}
getOpenerElement ( view ) {
// Look for the first parent view whose first DOM element is not virtually us
const firstNode = $ ( view . firstNode ( ) ) ;
// The goal is to have the best chances to get the element whose size and pos
// are relevant; e.g. when clicking on a date on a minicard, we don't wan't
// the opener to be set to the minicard.
// In order to work in general, we need to take special situations into account,
// e.g. the placeholder is isolated, or does not have previous node, and so on.
// In general we prefer previous node, then next, then any displayed sibling,
// then the parent, and so on.
let candidates = [ ] ;
if ( ! firstNode . hasClass ( this . popupPlaceholderClass ( ) ) ) {
candidates . push ( firstNode ) ;
}
candidates = candidates . concat ( [ firstNode . prev ( ) , firstNode . next ( ) ] ) ;
const otherSiblings = Array . from ( firstNode . siblings ( ) ) . filter ( e => ! candidates . includes ( e ) ) ;
for ( const cand of candidates . concat ( otherSiblings ) ) {
const displayCSS = cand ? . css ( "display" ) ;
if ( displayCSS && displayCSS !== "none" ) {
return cand [ 0 ] ;
}
}
return this . getOpenerElement ( view . parentView ) ;
}
getParentData ( view ) { ;
let data ;
// ⚠️ node can be a text node
while ( view . firstNode ? . ( ) ? . classList ? . contains ( this . popupPlaceholderClass ( ) ) ) {
view = view . parentView ;
data = Blaze . getData ( view ) ;
}
// This is VERY IMPORTANT to get data like this and not with templateInstance.data,
// because this form is reactive. So all inner popups have reactive data, which is nice
return data ;
}
onCreated ( ) {
// #FIXME prevent secondary popups to open
// Special "magic number" case: never render, for any reason, the same card
// const maybeID = this.parentComponent?.()?.data?.()?._id;
// if (maybeID && PopupComponent.stack.find(e => e.parentComponent().data?.()?._id === maybeID)) {
// this.destroy();
// return;
// }
// do not render a template multiple times
const existing = PopupComponent . stack . find ( ( e ) => ( e . name == this . data ( ) . name ) ) ;
if ( existing && PopupComponent . multipleBlacklist . indexOf ( this . data ( ) . name ) ) {
// ⚠️ is there a default better than another? I feel that closing existing
// popup is not bad in general because having the same button for open and close
// is common
if ( PopupComponent . multipleBlacklist . includes ( existing . name ) ) {
existing . destroy ( ) ;
}
// but is could also be re-rendering, eg
// existing.render();
return ;
}
// All of this, except name, is optional. The rest is provided "just in case", for convenience (hopefully)
//
// - name is the name of a template to render inside the popup (to the detriment of its size) or the contrary
// - showHeader can be turned off if the inner content always have a header with buttons and so on
// - title is shown when header is shown
// - miscOptions is for compatibility
// - closeVar is an optional string representing a Session variable: if set, the popup reactively closes when the variable changes and set the variable to null on close
// - closeDOMs can be used alternatively; it is an array of "<event> <selector>" to listen that closes the popup.
// if header is shown, closing the popup is already managed. selector is relative to the inner template (same as its event map)
// - followDOM is an element whose dimension will serve as reference so that popup can react to inner changes; works only with inline styles (otherwise we probably would need IntersectionObserver-like stuff, async etc)
// - handleDOM is an element who can be clicked to move popup
// it is useful when the content can be redimensionned/moved by code or user; we still manage events, resizes etc
// but allow inner elements or handles to do it (and we adapt).
const data = this . data ( ) ;
this . popupArgs = {
name : data . name ,
showHeader : data . showHeader ? ? true ,
title : data . title ,
openerElement : data . openerElement ,
closeDOMs : data . closeDOMs ,
followDOM : data . followDOM ,
handleDOM : data . handleDOM ,
forceData : data . miscOptions ? . dataContextIfCurrentDataIsUndefined ,
afterConfirm : data . miscOptions ? . afterConfirm ,
}
this . name = this . data ( ) . name ;
this . innerTemplate = Template [ this . name ] ;
this . innerComponent = BlazeComponent . getComponent ( this . name ) ;
this . outerComponent = BlazeComponent . getComponent ( 'popupDetached' ) ;
if ( ! ( this . innerComponent || this . innerTemplate ) ) {
throw new Error ( ` template and/or component ${ this . name } not found ` ) ;
}
// If arg is not set, must be closed manually by calling destroy()
if ( this . popupArgs . closeVar ) {
this . closeInitialValue = Session . get ( this . data ( ) . closeVar ) ;
if ( ! this . closeInitialValue === undefined ) {
this . autorun ( ( ) => {
if ( Session . get ( this . data ( ) . closeVar ) !== this . closeInitialValue ) {
this . onDestroyed ( ) ;
}
} ) ;
}
}
}
popupPlaceholderClass ( ) {
return "popup-placeholder" ;
}
render ( ) {
const oldOuterView = this . outerView ;
// see below for comments
this . outerView = Blaze . renderWithData (
// data is passed through the parent relationship
// we need to render it again to keep events in sync with inner popup
this . outerComponent . renderComponent ( this . component ( ) ) ,
this . popupArgs ,
document . body ,
null ,
this . openerView
) ;
this . innerView = Blaze . renderWithData (
// the template to render: either the content is a BlazeComponent or a regular template
// if a BlazeComponent, render it as a template first
this . innerComponent ? . renderComponent ? . ( this . component ( ) ) || this . innerTemplate ,
// dataContext used for rendering: each time we go find data, because it is non-reactive
( ) => ( this . popupArgs . forceData || this . getParentData ( this . currentView ) ) ,
// DOM parent: ask to the detached popup, will be inserted at the last child
this . outerView . firstNode ( ) ? . getElementsByClassName ( 'content' ) ? . [ 0 ] || document . body ,
// "stop" DOM element; we don't use
null ,
// important: this is the Blaze.View object which will be set as `parentView` of
// the rendered view. we set it as the parent view, so that the detached popup
// can interact with its "parent" without being a child of it, and without
// manipulating DOM directly.
this . openerView
) ;
if ( oldOuterView ) {
Blaze . remove ( oldOuterView ) ;
}
}
onRendered ( ) {
if ( this . detached ) { return }
// Use plain Blaze stuff to be able to render all templates, but use components when available/relevant
this . currentView = Blaze . currentView || Blaze . getView ( this . component ( ) . firstNode ( ) ) ;
// Placement will be related to the opener (usually clicked element)
// But template data and view related to the opener are not the same:
// - view is probably outer, as is was already rendered on click
// - template data could be found with Template.parentData(n), but `n` can
// vary depending on context: using those methods feels more reliable for this use case
this . popupArgs . openerElement ? ? = this . getOpenerElement ( this . currentView ) ;
this . openerView = Blaze . getView ( this . popupArgs . openerElement ) ;
// With programmatic/click opening, we get the "real" opener; with dynamic
// templating we get the placeholder and need to go up to get a glimpse of
// the "real" opener size. It is quite imprecise in that case (maybe the
// interesting opener is a sibling, not an ancestor), but seems to do the job
// for now.
// Also it feels sane that inner content does not have a reference to
// a virtual placeholder.
const opener = this . popupArgs . openerElement ;
let sizedOpener = opener ;
if ( opener . classList ? . contains ? . ( this . popupPlaceholderClass ( ) ) ) {
sizedOpener = opener . parentNode ;
}
this . popupArgs . nonPlaceholderOpener = sizedOpener ;
PopupComponent . stack . push ( this ) ;
try {
this . render ( ) ;
// Render above other popups by default
} catch ( e ) {
// If something went wrong during rendering, do not create
// "zombie" popups
console . error ( ` cannot render popup ${ this . name } : ${ e } ` ) ;
this . destroy ( ) ;
}
}
destroy ( ) {
this . detached = true ;
if ( ! PopupComponent . stack . includes ( this ) ) {
// Avoid loop destroy
return ;
}
// Maybe overkill but may help to avoid leaking memory
// as programmatic rendering is less usual
for ( const view of [ this . innerView , this . currentView , this . outerView ] ) {
try {
Blaze . remove ( view ) ;
} catch {
console . warn ( ` A view failed to be removed: ${ view } ` )
}
}
this . innerComponent ? . removeComponent ? . ( ) ;
this . outerComponent ? . removeComponent ? . ( ) ;
this . removeComponent ( ) ;
// not necesserly removed in order, e.g. multiple cards
PopupComponent . stack = PopupComponent . stack . filter ( e => e !== this ) ;
}
closeWithPlaceholder ( parentElement ) {
// adapted from https://stackoverflow.com/questions/52834774/dom-event-when-element-is-removed
// strangely, when opener is removed because of a reactive change, this component
// do not get any lifecycle hook called, so we need to bridge the gap. Simply
// "close" popup when placeholder is off-DOM.
while ( parentElement . nodeType === Node . TEXT _NODE ) {
parentElement = parentElement . parentElement ;
}
const placeholder = parentElement . getElementsByClassName ( this . popupPlaceholderClass ( ) ) ;
if ( ! placeholder . length ) {
return ;
}
const observer = new MutationObserver ( ( ) => {
// DOM element being suppressed is reflected in array
if ( placeholder . length === 0 ) {
this . destroy ( ) ;
}
} ) ;
observer . observe ( parentElement , { childList : true } ) ;
}
}
PopupComponent . register ( "popup" ) ;
PopupDetachedComponent . register ( 'popupDetached' ) ;
export default PopupComponent ;