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