dom util

dom manipulation util

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greatest.deepsurf.us/scripts/499616/1565714/dom%20util.js

  1. const HtmlSanitizer = {
  2. tempElement: document.createElement("div"),
  3. sanitize: function (/** @type {string} */ htmlString) {
  4. this.tempElement.innerText = htmlString;
  5. return this.tempElement.innerHTML;
  6. },
  7. };
  8.  
  9. const trustedHTMLPolicy = window.trustedTypes.createPolicy('mmHtmlPolicy', {
  10. createHTML: (input) => input // Add sanitization logic here if necessary
  11. });
  12.  
  13. class HtmlString extends String {
  14. /**@type {HTMLElement|null} */
  15. element = null;
  16.  
  17. /**@param {string} value */
  18. constructor(value) {
  19. super(value);
  20. }
  21.  
  22. /**@returns {HTMLElement} */
  23. asElement() {
  24. if (this.element !== null) {
  25. return this.element;
  26. }
  27.  
  28. const temp = document.createElement("div");
  29. temp.innerHTML = trustedHTMLPolicy.createHTML(this.valueOf());
  30. if (temp.childElementCount > 1) {
  31. throw new Error("html template does not accept more than 1 element");
  32. }
  33.  
  34. this.element = /**@type {HTMLElement} */ (temp.firstElementChild);
  35. return /**@type {HTMLElement} */ (this.element);
  36. }
  37. }
  38.  
  39. /**
  40. * @param {string} selector
  41. * @param {HTMLElement|Document} rootElement
  42. * @returns {HTMLElement|null}
  43. */
  44. function $findElm(selector, rootElement = document) {
  45. return /**@type {HTMLElement|null} */ (rootElement.querySelector(selector));
  46. }
  47.  
  48. /**
  49. * @param {string} selector
  50. * @param {HTMLElement|Document} rootElement
  51. * @returns {HTMLElement}
  52. */
  53. function $findElmStrictly(selector, rootElement = document) {
  54. const element = /**@type {HTMLElement|null} */ (rootElement.querySelector(selector));
  55. if (element === null) {
  56. throw new Error(`Element with selector '${selector}' not found`);
  57. }
  58.  
  59. return element;
  60. }
  61.  
  62. /**
  63. * @param {string} selector
  64. * @returns {NodeListOf<HTMLElement>}
  65. */
  66. function $findAll(selector) {
  67. return /**@type {NodeListOf<HTMLElement>} */ (document.querySelectorAll(selector));
  68. }
  69.  
  70. /**@typedef {string|HtmlString|number|boolean} TInterpolatedValue */
  71.  
  72. /**
  73. * safe html interpolation
  74. * @param {TemplateStringsArray} literalValues
  75. * @param {TInterpolatedValue[]|TInterpolatedValue[][]} interpolatedValues
  76. * @returns {HtmlString}
  77. */
  78. function html(literalValues, ...interpolatedValues) {
  79. let result = "";
  80.  
  81. interpolatedValues.forEach((currentInterpolatedVal, idx) => {
  82. let literalVal = literalValues[idx];
  83. let interpolatedVal = "";
  84. if (Array.isArray(currentInterpolatedVal)) {
  85. interpolatedVal = currentInterpolatedVal.join("\n");
  86. } else if (typeof currentInterpolatedVal !== "boolean") {
  87. interpolatedVal = currentInterpolatedVal.toString();
  88. }
  89.  
  90. const isSanitize = !literalVal.endsWith("$");
  91. if (isSanitize) {
  92. result += literalVal;
  93. result += HtmlSanitizer.sanitize(interpolatedVal);
  94. } else {
  95. literalVal = literalVal.slice(0, -1);
  96.  
  97. result += literalVal;
  98. result += interpolatedVal;
  99. }
  100. });
  101.  
  102. result += literalValues.slice(-1);
  103. return new HtmlString(result);
  104. }
  105.  
  106. /**
  107. * wait for element to be added to the DOM
  108. * @param {string} selector
  109. * @param {number} timeout
  110. * @param {(element: HTMLElement) => void} callback
  111. * @returns {CallableFunction | null} callback to stop observing
  112. */
  113. function waitForElement(selector, timeout, callback) {
  114. let matchingElement = /**@type {HTMLElement|null} */ (document.querySelector(selector));
  115. if (matchingElement) {
  116. callback(matchingElement);
  117. return null;
  118. }
  119.  
  120. const observer = new MutationObserver((mutations) => {
  121. for (let mutation of mutations) {
  122. if (!mutation.addedNodes) continue;
  123.  
  124. for (let node of mutation.addedNodes) {
  125. if (node.matches && node.matches(selector)) {
  126. callback(node);
  127. observer.disconnect();
  128. clearTimeout(timeoutId);
  129. return;
  130. }
  131. if (node.querySelector) {
  132. matchingElement = /**@type {HTMLElement|null} */ (node.querySelector(selector));
  133. if (matchingElement !== null) {
  134. callback(matchingElement);
  135. observer.disconnect();
  136. clearTimeout(timeoutId);
  137. return;
  138. }
  139. }
  140. }
  141. }
  142. });
  143.  
  144. observer.observe(document.documentElement, {
  145. childList: true,
  146. subtree: true,
  147. attributes: false,
  148. characterData: false,
  149. });
  150.  
  151. const timeoutId = setTimeout(() => {
  152. observer.disconnect();
  153. console.log(`Timeout reached: Element "${selector}" not found`);
  154. }, timeout);
  155.  
  156. return () => {
  157. observer.disconnect();
  158. };
  159. }
  160.  
  161. /**
  162. * Smoothly scrolls the page to the given element with an optional offset above it.
  163. *
  164. * @param {HTMLElement} el - The target element to scroll to.
  165. * @param {number} [offset=20] - The number of pixels to offset above the element.
  166. * @returns {void}
  167. */
  168. function scrollToElementWithOffset(el, offset = 20) {
  169. const rect = el.getBoundingClientRect();
  170. const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
  171. const targetY = rect.top + scrollTop - offset;
  172.  
  173. window.scrollTo({
  174. top: targetY,
  175. behavior: 'smooth'
  176. });
  177. }
  178.  
  179. /**
  180. * Appends a <style> tag with class 'mm-styles' and the given CSS rules to the document head.
  181. *
  182. * @param {string} cssText - The CSS rules to insert into the style tag.
  183. * @returns {HTMLStyleElement} The created style element.
  184. */
  185. function addStyles(cssText) {
  186. const styleEl = document.createElement('style');
  187. styleEl.className = 'mm-styles';
  188. styleEl.textContent = cssText;
  189. document.head.appendChild(styleEl);
  190. return styleEl;
  191. }