onSelector.ts 4.0 KB

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