onSelector.ts 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  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. /** Removes all listeners registered in `onSelector()` that have the given selector */
  25. export function removeOnSelector(selector: string) {
  26. return selectorMap.delete(selector);
  27. }
  28. function checkSelectorExists<TElem extends Element = HTMLElement>(selector: string, options: OnSelectorOpts<TElem>[]) {
  29. const deleteIndices: number[] = [];
  30. options.forEach((option, i) => {
  31. try {
  32. const elements = option.all ? document.querySelectorAll<HTMLElement>(selector) : document.querySelector<HTMLElement>(selector);
  33. if(elements) {
  34. // I don't feel like dealing with intersecting types, this should work just fine at runtime
  35. // @ts-ignore
  36. option.listener(elements);
  37. if(!option.continuous)
  38. deleteIndices.push(i);
  39. }
  40. }
  41. catch(err) {
  42. void err;
  43. }
  44. });
  45. if(deleteIndices.length > 0) {
  46. // once again laziness strikes
  47. // @ts-ignore
  48. selectorMap.set(selector, options.filter((_, i) => !deleteIndices.includes(i)));
  49. }
  50. }
  51. /**
  52. * Initializes a MutationObserver that checks for all registered selectors whenever an element is added to or removed from the `<body>`
  53. * @param opts For fine-tuning when the MutationObserver checks for the selectors
  54. */
  55. export function initOnSelector(opts: InitOnSelectorOpts = {}) {
  56. const observer = new MutationObserver(() => {
  57. for(const [selector, options] of selectorMap.entries())
  58. checkSelectorExists(selector, options);
  59. });
  60. observer.observe(document.body, {
  61. ...opts,
  62. // subtree: true, // this setting applies the options to the childList (which isn't necessary in this use case)
  63. childList: true,
  64. });
  65. }