diff --git a/src/devTools/editor/tabs/trigger/TriggerConfiguration.tsx b/src/devTools/editor/tabs/trigger/TriggerConfiguration.tsx index c5da40e053..571ef5c471 100644 --- a/src/devTools/editor/tabs/trigger/TriggerConfiguration.tsx +++ b/src/devTools/editor/tabs/trigger/TriggerConfiguration.tsx @@ -32,6 +32,7 @@ function supportsSelector(trigger: Trigger) { } function supportsTargetMode(trigger: Trigger) { + // XXX: why doesn't `appear` support target mode? return supportsSelector(trigger) && trigger !== "appear"; } @@ -50,6 +51,9 @@ const TriggerConfiguration: React.FC<{ if (!supportsSelector(nextTrigger)) { setFieldValue("extensionPoint.definition.rootSelector", null); setFieldValue("extensionPoint.definition.attachMode", null); + } + + if (!supportsTargetMode(nextTrigger)) { setFieldValue("extensionPoint.definition.targetMode", null); } @@ -75,6 +79,7 @@ const TriggerConfiguration: React.FC<{ > + diff --git a/src/devTools/editor/toolbar/ReloadToolbar.tsx b/src/devTools/editor/toolbar/ReloadToolbar.tsx index fad2a65618..88aa8c5905 100644 --- a/src/devTools/editor/toolbar/ReloadToolbar.tsx +++ b/src/devTools/editor/toolbar/ReloadToolbar.tsx @@ -36,7 +36,7 @@ function isPanelElement(element: FormState | null): boolean { * @param element */ function isAutomaticTrigger(element: FormState): boolean { - const automatic = ["load", "appear", "interval"]; + const automatic = ["load", "appear", "initialize", "interval"]; return ( element?.type === "trigger" && automatic.includes(element?.extensionPoint.definition.trigger) diff --git a/src/extensionPoints/triggerExtension.ts b/src/extensionPoints/triggerExtension.ts index ee6bc49e80..06069027ad 100644 --- a/src/extensionPoints/triggerExtension.ts +++ b/src/extensionPoints/triggerExtension.ts @@ -75,8 +75,10 @@ export type Trigger = | "interval" // `appear` is triggered when an element enters the user's viewport | "appear" - | "click" + // `initialize` is triggered when an element is added to the DOM + | "initialize" | "blur" + | "click" | "dblclick" | "mouseover" | "change"; @@ -143,24 +145,6 @@ export abstract class TriggerExtensionPoint extends ExtensionPoint void) | null; - - /** - * Cancel the initialization observer in "watch" attachMode. - * @private - */ - private cancelWatchNewElements: (() => void) | null; - - /** - * Observer to watch for new elements to appear, or undefined if the trigger is not an `appear` trigger - * @private - */ - private appearObserver: IntersectionObserver | undefined; - /** * Installed DOM event listeners, e.g., `click` * @private @@ -176,10 +160,10 @@ export abstract class TriggerExtensionPoint extends ExtensionPoint; /** - * Controller to abort/cancel the currently running interval loop + * Controller to drop all listeners and timers * @private */ - private intervalController: AbortController | null; + private abortController = new AbortController(); protected constructor( id: string, @@ -188,8 +172,6 @@ export abstract class TriggerExtensionPoint extends ExtensionPoint void): void { + this.abortController.signal.addEventListener("abort", callback); + } + removeExtensions(): void { // NOP: the removeExtensions method doesn't need to unregister anything from the page because the // observers/handlers are installed for the extensionPoint itself, not the extensions. I.e., there's a single @@ -212,14 +206,7 @@ export abstract class TriggerExtensionPoint extends ExtensionPoint 0) { this.logger.debug("Attaching interval trigger"); - // Cast setInterval return value to number. For some reason Typescript is using the Node types for setInterval - const controller = new AbortController(); - const intervalEffect = async () => { const $root = await this.getRoot(); await Promise.allSettled( @@ -402,12 +378,10 @@ export abstract class TriggerExtensionPoint extends ExtensionPoint + ): void { + this.cancelObservers(); + + // The caller will have already waited for the element. So $element will contain at least one element + if (this.attachMode === "once") { + for (const element of $element.get()) { + void this.runTrigger(element); + } + + return; + } + + const observer = initialize( + this.triggerSelector, + (index, element) => { + void this.runTrigger(element as HTMLElement).then((errors) => { + if (errors.length > 0) { + console.error("An error occurred while running a trigger", { + errors, + }); + notifyError("An error occurred while running a trigger"); + } + }); + }, + // `target` is a required option + { target: document } + ); - this.appearObserver?.disconnect(); + this.addCancelHandler(() => { + observer.disconnect(); + }); + } + + private attachAppearTrigger($element: JQuery): void { + this.cancelObservers(); - this.appearObserver = new IntersectionObserver( + // https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API + const appearObserver = new IntersectionObserver( (entries) => { for (const entry of entries.filter((x) => x.isIntersecting)) { void this.runTrigger(entry.target as HTMLElement).then((errors) => { @@ -444,7 +452,7 @@ export abstract class TriggerExtensionPoint extends ExtensionPoint { console.debug("initialize: %s", selector); - this.appearObserver.observe(element); + appearObserver.observe(element); }, + // `target` is a required option { target: document } ); - - this.cancelWatchNewElements = mutationObserver.disconnect.bind( - mutationObserver - ); + this.addCancelHandler(() => { + mutationObserver.disconnect(); + }); } + + this.addCancelHandler(() => { + appearObserver.disconnect(); + }); } private attachDOMTrigger( @@ -498,8 +510,7 @@ export abstract class TriggerExtensionPoint extends ExtensionPoint { + mutationObserver.disconnect(); + }); } } @@ -524,13 +536,6 @@ export abstract class TriggerExtensionPoint extends ExtensionPoint { this.cancelObservers(); @@ -550,6 +555,11 @@ export abstract class TriggerExtensionPoint extends ExtensionPoint