Browse Source

feat: onSelector

Sv443 1 year ago
parent
commit
0a82a127f2
2 changed files with 92 additions and 16 deletions
  1. 62 10
      lib/onSelector.ts
  2. 30 6
      lib/types.ts

+ 62 - 10
lib/onSelector.ts

@@ -1,21 +1,73 @@
-import { SelectorExistsOpts } from "./types";
+import type { InitOnSelectorOpts, OnSelectorOpts } from "./types";
+
+const selectorMap = new Map<string, OnSelectorOpts[]>();
 
 /**
  * Calls the `listener` as soon as the `selector` exists in the DOM.  
  * Listeners are deleted when they are called once, unless `options.continuous` is set.  
  * Multiple listeners with the same selector may be registered.
- * @template TElem The type of element that this selector will return - FIXME: listener inferring doesn't work when this generic is given
+ * @param selector The selector to listen for
+ * @param options Used for switching to `querySelectorAll()` and for calling the listener continuously
+ * @template TElem The type of element that the listener will return as its argument (defaults to the generic HTMLElement)
  */
-export function onSelector<TElem = HTMLElement, TOpts extends SelectorExistsOpts = SelectorExistsOpts>(
-  options: TOpts,
-  listener: (element: TOpts["all"] extends true ? (TElem extends HTMLElement ? NodeListOf<TElem> : TElem) : TElem) => void,
+export function onSelector<TElem extends Element = HTMLElement>(
+  selector: string,
+  options: OnSelectorOpts<TElem>,
 ) {
-  // TODO:
-  void [options, listener];
+  let selectorMapItems: OnSelectorOpts[] = [];
+  if(selectorMap.has(selector))
+    selectorMapItems = selectorMap.get(selector)!;
+
+  // I don't feel like dealing with intersecting types, this should work just fine at runtime
+  // @ts-ignore
+  selectorMapItems.push(options);
+
+  selectorMap.set(selector, selectorMapItems);
+  checkSelectorExists(selector, selectorMapItems);
 }
 
-/** Removes all listeners registered in `onSelector()` with a matching selector property */
+/** Removes all listeners registered in `onSelector()` that have the given selector */
 export function removeOnSelector(selector: string) {
-  // TODO:
-  void [selector];
+  return selectorMap.delete(selector);
+}
+
+function checkSelectorExists<TElem extends Element = HTMLElement>(selector: string, options: OnSelectorOpts<TElem>[]) {
+  const deleteIndices: number[] = [];
+  options.forEach((option, i) => {
+    try {
+      const elements = option.all ? document.querySelectorAll<HTMLElement>(selector) : document.querySelector<HTMLElement>(selector);
+      if(elements) {
+        // I don't feel like dealing with intersecting types, this should work just fine at runtime
+        // @ts-ignore
+        option.listener(elements);
+        if(!option.continuous)
+          deleteIndices.push(i);
+      }
+    }
+    catch(err) {
+      void err;
+    }
+  });
+  if(deleteIndices.length > 0) {
+    // once again laziness strikes
+    // @ts-ignore
+    selectorMap.set(selector, options.filter((_, i) => !deleteIndices.includes(i)));
+  }
+}
+
+/**
+ * Initializes a MutationObserver that checks for all registered selectors whenever an element is added to or removed from the `<body>`
+ * @param opts For fine-tuning when the MutationObserver checks for the selectors
+ */
+export function initOnSelector(opts: InitOnSelectorOpts = {}) {
+  const observer = new MutationObserver(() => {
+    for(const [selector, options] of selectorMap.entries())
+      checkSelectorExists(selector, options);
+  });
+
+  observer.observe(document.body, {
+    ...opts,
+    // subtree: true, // this setting applies the options to the childList (which isn't necessary in this use case)
+    childList: true,
+  });
 }

+ 30 - 6
lib/types.ts

@@ -1,12 +1,36 @@
-export type SelectorExistsOpts = {
-  /** The selector to check for */
-  selector: string;
-  /** Whether to use `querySelectorAll()` instead */
-  all?: boolean;
-  /** Whether to call the listener continuously instead of once */
+//#SECTION selector exists
+
+export type InitOnSelectorOpts = {
+  /** Set to true if mutations to any element's attributes are to also trigger the onSelector check (warning: this might draw a lot of performance on larger sites) */
+  attributes?: boolean;
+  /** Set to true if mutations to any element's character data are to also trigger the onSelector check (warning: this might draw a lot of performance on larger sites) */
+  characterData?: boolean;
+}
+
+export type OnSelectorOpts<TElem extends Element = HTMLElement> = SelectorOptsOne<TElem> | SelectorOptsAll<TElem>;
+
+type SelectorOptsOne<TElem extends Element> = SelectorOptsBase & {
+  /** Whether to use `querySelectorAll()` instead - default is false */
+  all?: false;
+  /** Gets called whenever the selector was found in the DOM */
+  listener: (element: TElem) => void;
+};
+
+type SelectorOptsAll<TElem extends Element> = SelectorOptsBase & {
+  /** Whether to use `querySelectorAll()` instead - default is false */
+  all: true;
+  /** Gets called whenever the selector was found in the DOM */
+  listener: (elements: NodeListOf<TElem>) => void;
+};
+
+type SelectorOptsBase = {
+  /** Whether to call the listener continuously instead of once - default is false */
   continuous?: boolean;
 };
 
+//#SECTION fetch advanced
+
 export type FetchAdvancedOpts = RequestInit & Partial<{
+  /** Timeout in milliseconds after which the fetch call will be canceled with an AbortController signal */
   timeout: number;
 }>;