Răsfoiți Sursa

feat: add auto-enable and -disable options for SelectorObserver

Sv443 1 an în urmă
părinte
comite
6b03381a83
2 a modificat fișierele cu 35 adăugiri și 8 ștergeri
  1. 5 2
      README.md
  2. 30 6
      lib/SelectorObserver.ts

+ 5 - 2
README.md

@@ -148,9 +148,12 @@ If a selector string is passed instead, it will be used to find the element.
 If you want to observe the entire document, you can pass `document.body`  
 
 The `options` parameter is optional and will be passed to the MutationObserver that is used internally.  
-The default options are `{ childList: true, subtree: true }` - you may see the [MutationObserver.observe() documentation](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe#options) for more information and a list of options.  
-For example, if you want to trigger the listeners when certain attributes change, pass `{ attributes: true, attributeFilter: ["class", "data-my-attribute"] }`  
+The MutationObserver options present by default are `{ childList: true, subtree: true }` - you may see the [MutationObserver.observe() documentation](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/observe#options) for more information and a list of options.  
+For example, if you want to trigger the listeners when certain attributes change, pass `{ attributeFilter: ["class", "data-my-attribute"] }`  
+  
 Additionally, there are the following extra options:
+- `disableOnNoListeners` - whether to disable the SelectorObserver when there are no listeners left (defaults to false)
+- `enableOnAddListener` - whether to enable the SelectorObserver when a new listener is added (defaults to true)
 - `defaultDebounce` - if set to a number, this debounce will be applied to every listener that doesn't have a custom debounce set (defaults to 0)
   
 ⚠️ Make sure to call `enable()` to actually start observing. This will need to be done after the DOM has loaded (when using `@run-at document-end` or after `DOMContentLoaded` has fired) **and** as soon as the `baseElement` or `baseElementSelector` is available.

+ 30 - 6
lib/SelectorObserver.ts

@@ -22,9 +22,13 @@ type SelectorOptionsCommon = {
   debounce?: number;
 };
 
-export type SelectorObserverOptions = MutationObserverInit & {
+export type SelectorObserverOptions = {
   /** If set, applies this debounce in milliseconds to all listeners that don't have their own debounce set */
   defaultDebounce?: number;
+  /** Whether to disable the observer when no listeners are present - default is true */
+  disableOnNoListeners?: boolean;
+  /** Whether to ensure the observer is enabled when a new listener is added - default is true */
+  enableOnAddListener?: boolean;
 };
 
 /** Observes the children of the given element for changes */
@@ -32,7 +36,8 @@ export class SelectorObserver {
   private enabled = false;
   private baseElement: Element | string;
   private observer: MutationObserver;
-  private observerOptions: SelectorObserverOptions;
+  private observerOptions: MutationObserverInit;
+  private customOptions: SelectorObserverOptions;
   private listenerMap: Map<string, SelectorListenerOptions[]>;
 
   /**
@@ -40,7 +45,7 @@ export class SelectorObserver {
    * @param baseElementSelector The selector of the element to observe
    * @param options Fine-tune what triggers the MutationObserver's checking function - `subtree` and `childList` are set to true by default
    */
-  constructor(baseElementSelector: string, options?: SelectorObserverOptions)
+  constructor(baseElementSelector: string, options?: SelectorObserverOptions & MutationObserverInit)
   /**
    * Creates a new SelectorObserver that will observe the children of the given base element for changes (only creation and deletion of elements by default)
    * @param baseElement The element to observe
@@ -53,10 +58,24 @@ export class SelectorObserver {
     this.listenerMap = new Map<string, SelectorListenerOptions[]>();
     // if the arrow func isn't there, `this` will be undefined in the callback
     this.observer = new MutationObserver(() => this.checkAllSelectors());
+
+    const {
+      defaultDebounce,
+      disableOnNoListeners,
+      enableOnAddListener,
+      ...observerOptions
+    } = options;
+
     this.observerOptions = {
       childList: true,
       subtree: true,
-      ...options,
+      ...observerOptions,
+    };
+
+    this.customOptions = {
+      defaultDebounce: defaultDebounce ?? 0,
+      disableOnNoListeners: disableOnNoListeners ?? false,
+      enableOnAddListener: enableOnAddListener ?? true,
     };
   }
 
@@ -97,6 +116,8 @@ export class SelectorObserver {
       }
       if(this.listenerMap.get(selector)?.length === 0)
         this.listenerMap.delete(selector);
+      if(this.listenerMap.size === 0 && this.customOptions.disableOnNoListeners)
+        this.disable();
     }
   }
 
@@ -119,10 +140,10 @@ export class SelectorObserver {
    */
   public addListener<TElem extends Element = HTMLElement>(selector: string, options: SelectorListenerOptions<TElem>) {
     options = { all: false, continuous: false, debounce: 0, ...options };
-    if((options.debounce && options.debounce > 0) || (this.observerOptions.defaultDebounce && this.observerOptions.defaultDebounce > 0)) {
+    if((options.debounce && options.debounce > 0) || (this.customOptions.defaultDebounce && this.customOptions.defaultDebounce > 0)) {
       options.listener = this.debounce(
         options.listener as ((arg: NodeListOf<Element> | Element) => void),
-        (options.debounce || this.observerOptions.defaultDebounce)!,
+        (options.debounce || this.customOptions.defaultDebounce)!,
       );
     }
     if(this.listenerMap.has(selector))
@@ -130,6 +151,9 @@ export class SelectorObserver {
     else
       this.listenerMap.set(selector, [options as SelectorListenerOptions<Element>]);
 
+    if(this.enabled === false && this.customOptions.enableOnAddListener)
+      this.enable();
+
     this.checkSelector(selector, [options as SelectorListenerOptions<Element>]);
   }