onSelector.ts 3.2 KB

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