onSelector.ts 3.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  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. console.info("##-- opts", options, "\n##-- deleteIndices", deleteIndices, "\n##-- selectorMap", selectorMap.size, selectorMap);
  49. if(deleteIndices.length > 0) {
  50. const newOptsArray = options.filter((_, i) => !deleteIndices.includes(i));
  51. if(newOptsArray.length === 0)
  52. selectorMap.delete(selector);
  53. else {
  54. // once again laziness strikes
  55. // @ts-ignore
  56. selectorMap.set(selector, newOptsArray);
  57. }
  58. }
  59. }
  60. /**
  61. * Initializes a MutationObserver that checks for all registered selectors whenever an element is added to or removed from the `<body>`
  62. * @param opts For fine-tuning when the MutationObserver checks for the selectors
  63. */
  64. export function initOnSelector(opts: InitOnSelectorOpts = {}) {
  65. const observer = new MutationObserver(() => {
  66. for(const [selector, options] of selectorMap.entries())
  67. checkSelectorExists(selector, options);
  68. });
  69. observer.observe(document.body, {
  70. ...opts,
  71. // subtree: true, // this setting applies the options to the childList (which isn't necessary in this use case)
  72. childList: true,
  73. });
  74. }
  75. /** Returns all currently registered selectors, as a map of selector strings to their associated options */
  76. export function getSelectorMap() {
  77. return selectorMap;
  78. }