diff --git a/src/components/modal.ts b/src/components/modal.ts index aff46df..3ebff51 100755 --- a/src/components/modal.ts +++ b/src/components/modal.ts @@ -15,6 +15,7 @@ let $openModal: Modal|undefined = undefined; let lastFocusElement: HTMLElement|undefined = undefined; const TITLE_ID = 'boost-modal-title'; +const elementsWithModalListeners = new WeakSet(); function tryClose() { if ($openModal && $openModal.canClose) $openModal.close(); @@ -51,12 +52,12 @@ export class Modal extends CustomElementView { this.$video = this.$('video') as MediaView|undefined; const $buttons = $$(`[data-modal=${this.id}]`); - for (const $b of $buttons) $b.on('click', () => this.open()); + for (const $b of $buttons) this.attachListener($b); // Look for new modals to open, after browser navigation. Router.on('afterChange', ({$viewport}) => { const $buttons = $viewport.$$(`[data-modal=${this.id}]`); - for (const $b of $buttons) $b.on('click', () => this.open()); + for (const $b of $buttons) this.attachListener($b); }); // Open modals that are shown on pageload @@ -90,6 +91,26 @@ export class Modal extends CustomElementView { }); } + /** + * Attaches a click listener to an element with data-modal attribute, if there isn't one already. + */ + attachListener($button: ElementView) { + if (elementsWithModalListeners.has($button._el)) return; + + $button.on('click', () => this.open()); + elementsWithModalListeners.add($button._el); + } + + /** + * Removes the click listener from an element + */ + removeListener($button: ElementView) { + if (!elementsWithModalListeners.has($button._el)) return; + + $button.off('click'); + elementsWithModalListeners.delete($button._el); + } + open(noAnimation = false) { if (this.isOpen) return; diff --git a/src/elements.ts b/src/elements.ts index 6c0bcbd..64c4b1c 100644 --- a/src/elements.ts +++ b/src/elements.ts @@ -197,11 +197,41 @@ export abstract class BaseView { if (show) { this.$placeholder.insertBefore(this); + this.reattachModalListenersInSubtree(); } else { + this.removeModalListenersInSubtree(); this.detach(); } } + private reattachModalListenersInSubtree() { + this.updateModalListenersInSubtree((modal, el) => modal.attachListener?.(el)); + } + + private removeModalListenersInSubtree() { + this.updateModalListenersInSubtree((modal, el) => modal.removeListener?.(el)); + } + + /** + * Updates modal listeners for `this` element and its descendants with data-modal attributes. + * Used when elements are shown/hidden via :if to attach/remove listeners + */ + private updateModalListenersInSubtree( + operation: (modal: Modal, element: ElementView) => void + ) { + const modalElements: ElementView[] = this.hasAttr('data-modal') ? [this] : []; + const childElementsWithModalAttrs = this.$$('[data-modal]'); + modalElements.push(...childElementsWithModalAttrs); + + for (const $el of modalElements) { + const modalId = $el.attr('data-modal'); + if (!modalId) continue; + + const $modal = $(`x-modal#${modalId}`) as Modal; + if ($modal) operation($modal, $el); + } + } + private makeDynamicAttribute(name: string, value: string, model: Observable) { if (name.startsWith('@')) { const event = name.slice(1);