onSelector.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. export type OnSelectorOpts<TElem extends Element = HTMLElement> = SelectorOptsOne<TElem> | SelectorOptsAll<TElem>;
  2. type SelectorOptsOne<TElem extends Element> = SelectorOptsBase & {
  3. /** Whether to use `querySelectorAll()` instead - default is false */
  4. all?: false;
  5. /** Gets called whenever the selector was found in the DOM */
  6. listener: (element: TElem) => void;
  7. };
  8. type SelectorOptsAll<TElem extends Element> = SelectorOptsBase & {
  9. /** Whether to use `querySelectorAll()` instead - default is false */
  10. all: true;
  11. /** Gets called whenever the selector was found in the DOM */
  12. listener: (elements: NodeListOf<TElem>) => void;
  13. };
  14. type SelectorOptsBase = {
  15. /** Whether to call the listener continuously instead of once - default is false */
  16. continuous?: boolean;
  17. };
  18. const selectorMap = new Map<string, OnSelectorOpts[]>();
  19. /**
  20. * Calls the `listener` as soon as the `selector` exists in the DOM.
  21. * Listeners are deleted when they are called once, unless `options.continuous` is set.
  22. * Multiple listeners with the same selector may be registered.
  23. * @param selector The selector to listen for
  24. * @param options Used for switching to `querySelectorAll()` and for calling the listener continuously
  25. * @template TElem The type of element that the listener will return as its argument (defaults to the generic HTMLElement)
  26. */
  27. export function onSelector<TElem extends Element = HTMLElement>(
  28. selector: string,
  29. options: OnSelectorOpts<TElem>,
  30. ) {
  31. let selectorMapItems: OnSelectorOpts[] = [];
  32. if(selectorMap.has(selector))
  33. selectorMapItems = selectorMap.get(selector)!;
  34. // I don't feel like dealing with intersecting types, this should work just fine at runtime
  35. // @ts-ignore
  36. selectorMapItems.push(options);
  37. selectorMap.set(selector, selectorMapItems);
  38. checkSelectorExists(selector, selectorMapItems);
  39. }
  40. /**
  41. * Removes all listeners registered in `onSelector()` that have the given selector
  42. * @returns Returns true when all listeners with the associated selector were found and removed, false otherwise
  43. */
  44. export function removeOnSelector(selector: string) {
  45. return selectorMap.delete(selector);
  46. }
  47. function checkSelectorExists<TElem extends Element = HTMLElement>(selector: string, options: OnSelectorOpts<TElem>[]) {
  48. const deleteIndices: number[] = [];
  49. options.forEach((option, i) => {
  50. try {
  51. const elements = option.all ? document.querySelectorAll<HTMLElement>(selector) : document.querySelector<HTMLElement>(selector);
  52. if((elements !== null && elements instanceof NodeList && elements.length > 0) || elements !== null) {
  53. // I don't feel like dealing with intersecting types, this should work just fine at runtime
  54. // @ts-ignore
  55. option.listener(elements);
  56. if(!option.continuous)
  57. deleteIndices.push(i);
  58. }
  59. }
  60. catch(err) {
  61. console.error(`Couldn't call listener for selector '${selector}'`, err);
  62. }
  63. });
  64. if(deleteIndices.length > 0) {
  65. const newOptsArray = options.filter((_, i) => !deleteIndices.includes(i));
  66. if(newOptsArray.length === 0)
  67. selectorMap.delete(selector);
  68. else {
  69. // once again laziness strikes
  70. // @ts-ignore
  71. selectorMap.set(selector, newOptsArray);
  72. }
  73. }
  74. }
  75. /**
  76. * Initializes a MutationObserver that checks for all registered selectors whenever an element is added to or removed from the `<body>`
  77. * @param options For fine-tuning what triggers the MutationObserver's checking function - `subtree` and `childList` are set to true by default
  78. */
  79. export function initOnSelector(options: MutationObserverInit = {}) {
  80. const observer = new MutationObserver(() => {
  81. for(const [selector, options] of selectorMap.entries())
  82. checkSelectorExists(selector, options);
  83. });
  84. observer.observe(document.body, {
  85. subtree: true,
  86. childList: true,
  87. ...options,
  88. });
  89. }
  90. /** Returns all currently registered selectors, as a map of selector strings to their associated options */
  91. export function getSelectorMap() {
  92. return selectorMap;
  93. }