From 18a056dff487680eb40b2f70dabe08cb9f1e8ec2 Mon Sep 17 00:00:00 2001 From: Zach Cohn Date: Thu, 16 Oct 2025 15:32:15 -0500 Subject: [PATCH 1/3] attach listeners to `data-modal` when they become visible --- src/components/modal.ts | 15 +++++++++++++-- src/elements.ts | 21 +++++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/components/modal.ts b/src/components/modal.ts index aff46df..eeeb828 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,16 @@ export class Modal extends CustomElementView { }); } + /** + * Attaches a click listener to an element with data-modal attribute, if there isn't one already. + */ + attachListener($button: any) { + if (elementsWithModalListeners.has($button._el)) return; + + $button.on('click', () => this.open()); + elementsWithModalListeners.add($button._el); + } + open(noAnimation = false) { if (this.isOpen) return; diff --git a/src/elements.ts b/src/elements.ts index 6c0bcbd..28d6374 100644 --- a/src/elements.ts +++ b/src/elements.ts @@ -197,11 +197,32 @@ export abstract class BaseView { if (show) { this.$placeholder.insertBefore(this); + this.reattachModalListenersInSubtree(); } else { this.detach(); } } + /** + * Re-attaches modal listeners to `this` element and its descendants with data-modal attributes when they become visible via :if. + * Ensures that `data-modal` still works on elements which are initially hidden. + */ + private reattachModalListenersInSubtree() { + 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 && typeof $modal.open === 'function') { + $modal.attachListener($el); + } + } + } + private makeDynamicAttribute(name: string, value: string, model: Observable) { if (name.startsWith('@')) { const event = name.slice(1); From 34efaf6436db8ca55847e112c3e9070b0b2f9d2a Mon Sep 17 00:00:00 2001 From: Zach Cohn Date: Wed, 22 Oct 2025 09:37:51 -0400 Subject: [PATCH 2/3] added functionality to remove modal listeners --- src/components/modal.ts | 10 ++++++++++ src/elements.ts | 21 +++++++++++++++------ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/components/modal.ts b/src/components/modal.ts index eeeb828..a964d2a 100755 --- a/src/components/modal.ts +++ b/src/components/modal.ts @@ -101,6 +101,16 @@ export class Modal extends CustomElementView { elementsWithModalListeners.add($button._el); } + /** + * Removes the click listener from an element + */ + removeListener($button: any) { + 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 28d6374..64c4b1c 100644 --- a/src/elements.ts +++ b/src/elements.ts @@ -199,15 +199,26 @@ export abstract class BaseView { 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)); + } + /** - * Re-attaches modal listeners to `this` element and its descendants with data-modal attributes when they become visible via :if. - * Ensures that `data-modal` still works on elements which are initially hidden. + * 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 reattachModalListenersInSubtree() { + private updateModalListenersInSubtree( + operation: (modal: Modal, element: ElementView) => void + ) { const modalElements: ElementView[] = this.hasAttr('data-modal') ? [this] : []; const childElementsWithModalAttrs = this.$$('[data-modal]'); modalElements.push(...childElementsWithModalAttrs); @@ -217,9 +228,7 @@ export abstract class BaseView { if (!modalId) continue; const $modal = $(`x-modal#${modalId}`) as Modal; - if ($modal && typeof $modal.open === 'function') { - $modal.attachListener($el); - } + if ($modal) operation($modal, $el); } } From 15201eb6d676aad66855d90d279e2decff9d159e Mon Sep 17 00:00:00 2001 From: Zach Cohn Date: Wed, 22 Oct 2025 09:41:26 -0400 Subject: [PATCH 3/3] fixed missing type --- src/components/modal.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/modal.ts b/src/components/modal.ts index a964d2a..3ebff51 100755 --- a/src/components/modal.ts +++ b/src/components/modal.ts @@ -94,7 +94,7 @@ export class Modal extends CustomElementView { /** * Attaches a click listener to an element with data-modal attribute, if there isn't one already. */ - attachListener($button: any) { + attachListener($button: ElementView) { if (elementsWithModalListeners.has($button._el)) return; $button.on('click', () => this.open()); @@ -104,7 +104,7 @@ export class Modal extends CustomElementView { /** * Removes the click listener from an element */ - removeListener($button: any) { + removeListener($button: ElementView) { if (!elementsWithModalListeners.has($button._el)) return; $button.off('click');