Monkey DOM

Useful library for dealing with the DOM.

Този скрипт не може да бъде инсталиран директно. Това е библиотека за други скриптове и може да бъде използвана с мета-директива // @require https://update.greasyfork.org/scripts/405802/823982/Monkey%20DOM.js

  1. // ==UserScript==
  2. // @name DOM
  3. // @namespace https://rafaelgssa.gitlab.io/monkey-scripts
  4. // @version 4.1.6
  5. // @author rafaelgssa
  6. // @description Useful library for dealing with the DOM.
  7. // @match *://*/*
  8. // @require https://greasyfork.org/scripts/405813-monkey-utils/code/Monkey%20Utils.js
  9. // ==/UserScript==
  10.  
  11. /* global Utils */
  12.  
  13. /**
  14. * @typedef {(element?: Element) => void} ElementCallback
  15. *
  16. * @typedef {InsertPosition | 'atouter' | 'atinner'} ExtendedInsertPosition
  17. *
  18. * @typedef {ElementArrayConstructor<ElementArrayBase, 8>} ElementArray Any higher than 8 is too deep and does not work.
  19. *
  20. * **The definition for ElementArrayConstructor is in DOM.d.ts, as it is too complex for JSDoc:**
  21. * declare type ElementArrayConstructor<
  22. * T extends [any, any] | ElementArrayChildrenBase | null,
  23. * N extends number
  24. * > = T extends [infer A, infer B]
  25. * ? {
  26. * done: [A, B, ElementArrayChildrenBase | null];
  27. * recurse: [
  28. * A,
  29. * B,
  30. * (
  31. * | ElementArrayConstructor<ElementArrayBase, ElementArrayDepth[N]>[]
  32. * | ElementArrayChildrenBase
  33. * | null
  34. * )
  35. * ];
  36. * }[N extends 0 ? 'done' : 'recurse']
  37. * : T extends ElementArrayChildrenBase | null
  38. * ? T
  39. * : never;
  40. *
  41. * @typedef {[never, 0, 1, 2, 3, 4, 5, 6, 7]} ElementArrayDepth
  42. *
  43. * @typedef {{ [K in ElementTag]: [K, ElementAttributes<K> | null] }[ElementTag] | ElementArrayChildrenBase | null} ElementArrayBase
  44. *
  45. * @typedef {keyof HTMLElementTagNameMap} ElementTag
  46. *
  47. * @typedef {Object} ExtendedElementBase
  48. * @property {Record<string, string>} attrs
  49. * @property {NodeCallback} ref
  50. *
  51. * @typedef {ElementArray[] | ElementArrayChildrenBase} ElementArrayChildren
  52. *
  53. * @typedef {Node | string} ElementArrayChildrenBase
  54. *
  55. * @typedef {Object} MutationTypes
  56. * @property {boolean} [attributes]
  57. * @property {boolean} [childList]
  58. * @property {boolean} [subtree]
  59. *
  60. * @typedef {(node: Node) => void} NodeCallback
  61. */
  62.  
  63. /**
  64. * @template {ElementTag} T
  65. * @typedef {{
  66. * [K in keyof ExtendedElement<T>]?: {
  67. * [L in keyof ExtendedElement<T>[K]]?: ExtendedElement<T>[K][L] | null;
  68. * } | null;
  69. * }} ElementAttributes
  70. */
  71.  
  72. /**
  73. * @template {ElementTag} T
  74. * @typedef {HTMLElementTagNameMap[T] & ExtendedElementBase} ExtendedElement
  75. */
  76.  
  77. // eslint-disable-next-line
  78. const DOM = (() => {
  79. const _parser = new DOMParser();
  80.  
  81. /**
  82. * Waits for an element that is dynamically added to the DOM.
  83. * @param {string} selectors The selectors to query for the element.
  84. * @param {number} [timeout] How long to wait for the element in seconds. Defaults to 60 (1 minute).
  85. * @param {number} [frequency] How often to keep checking for the element in seconds. Defaults to 1.
  86. * @returns {Promise<Element | undefined>} The element, if found.
  87. */
  88. const dynamicQuerySelector = (selectors, timeout = 60, frequency = 1) => {
  89. return new Promise((resolve) => _checkElementExists(selectors, resolve, timeout, frequency));
  90. };
  91.  
  92. /**
  93. * @param {string} selectors
  94. * @param {ElementCallback} callback
  95. * @param {number} [timeout]
  96. * @param {number} [frequency]
  97. */
  98. const _checkElementExists = (selectors, callback, timeout = 60, frequency = 1) => {
  99. const element = document.querySelector(selectors);
  100. if (element) {
  101. callback(element);
  102. } else if (timeout > 0) {
  103. window.setTimeout(
  104. _checkElementExists,
  105. frequency * 1000,
  106. selectors,
  107. callback,
  108. timeout - frequency,
  109. frequency
  110. );
  111. } else {
  112. callback();
  113. }
  114. };
  115.  
  116. /**
  117. * Inserts elements in reference to another element based on element arrays that are visually similar to JSX.
  118. * @param {Element} referenceEl The element to use as reference.
  119. * @param {ExtendedInsertPosition} position Where to insert the elements.
  120. * @param {ElementArray[]} arrays The arrays to use.
  121. * @returns {(HTMLElement | undefined)[]} The inserted elements from the root level, if successful.
  122. *
  123. * @example
  124. * // `pElement` will contain the P element.
  125. * // `elements` will be an array containing the DIV and SPAN elements, in this order, if successful.
  126. * let pElement;
  127. * const elements = DOM.insertElement(document.body, 'beforeend', [
  128. * ['div', { className: 'hello', onclick: () => {} }, [
  129. * 'Hello, ', // This is added as a text node.
  130. * ['p', { ref: (ref) => pElement = ref }, 'John'],
  131. * '!' // This is added as a text node.
  132. * ]],
  133. * ['span', null, 'How are you?']
  134. * ]);
  135. *
  136. * @example
  137. * // Using array destructuring.
  138. * // `divElement` will contain the DIV element and `spanElement` will contain the SPAN element, if successful.
  139. * let pElement;
  140. * const [divElement, spanElement] = DOM.insertElement(document.body, 'beforeend', [
  141. * ['div', { className: 'hello', onclick: () => {} }, [
  142. * 'Hello, ', // This is added as a text node.
  143. * ['p', { ref: (ref) => pElement = ref }, 'John'],
  144. * '!' // This is added as a text node.
  145. * ]],
  146. * ['span', null, 'How are you?']
  147. * ]);
  148. */
  149. const insertElements = (referenceEl, position, arrays) => {
  150. const docFragment = _buildFragment(arrays);
  151. if (!docFragment) {
  152. return [];
  153. }
  154. const elements = /** @type {HTMLElement[]} */ (Array.from(docFragment.children));
  155. const referenceElParent = referenceEl.parentElement;
  156. switch (position) {
  157. case 'beforebegin':
  158. if (referenceElParent) {
  159. referenceElParent.insertBefore(docFragment, referenceEl);
  160. }
  161. break;
  162. case 'afterbegin':
  163. referenceEl.insertBefore(docFragment, referenceEl.firstElementChild);
  164. break;
  165. case 'beforeend':
  166. referenceEl.appendChild(docFragment);
  167. break;
  168. case 'afterend':
  169. if (referenceElParent) {
  170. referenceElParent.insertBefore(docFragment, referenceEl.nextElementSibling);
  171. }
  172. break;
  173. case 'atouter':
  174. if (referenceElParent) {
  175. referenceElParent.insertBefore(docFragment, referenceEl.nextElementSibling);
  176. referenceEl.remove();
  177. }
  178. break;
  179. case 'atinner':
  180. referenceEl.innerHTML = '';
  181. referenceEl.appendChild(docFragment);
  182. break;
  183. // no default
  184. }
  185. if (docFragment.children.length > 0) {
  186. return [];
  187. }
  188. return elements;
  189. };
  190.  
  191. /**
  192. * Builds a document fragment from element arrays.
  193. * @param {ElementArray[]} arrays The arrays to use.
  194. * @returns {DocumentFragment | undefined} The built document fragment, if successful.
  195. */
  196. const _buildFragment = (arrays) => {
  197. if (!Array.isArray(arrays)) {
  198. return;
  199. }
  200. const docFragment = document.createDocumentFragment();
  201. // @ts-ignore
  202. const filteredArrays = arrays.filter(Utils.isSet);
  203. for (const array of filteredArrays) {
  204. const element = _buildElement(array);
  205. if (element) {
  206. docFragment.appendChild(element);
  207. }
  208. }
  209. return docFragment;
  210. };
  211.  
  212. /**
  213. * Builds an element from an element array.
  214. * @param {ElementArray} array The array to use.
  215. * @returns {Node | undefined} The built element, if successful.
  216. */
  217. const _buildElement = (array) => {
  218. if (!array) {
  219. return;
  220. }
  221. if (array instanceof Node) {
  222. return array;
  223. }
  224. if (typeof array === 'string') {
  225. return document.createTextNode(array);
  226. }
  227. const [tag, attributes, children] = array;
  228. const element = document.createElement(tag);
  229. if (attributes) {
  230. _setElementAttributes(element, attributes);
  231. }
  232. if (children) {
  233. _appendElementChildren(element, children);
  234. }
  235. return element;
  236. };
  237.  
  238. /**
  239. * Sets attributes for an element.
  240. * @template {ElementTag} T
  241. * @param {HTMLElement} element
  242. * @param {ElementAttributes<T>} attributes
  243. */
  244. const _setElementAttributes = (element, attributes) => {
  245. const filteredAttributes = Object.entries(attributes).filter(([, value]) => Utils.isSet(value));
  246. for (const [key, value] of filteredAttributes) {
  247. if (key === 'attrs' && typeof value === 'object') {
  248. _setCustomElementAttributes(element, value);
  249. } else if (key === 'ref' && typeof value === 'function') {
  250. value(element);
  251. } else if (key.startsWith('on') && typeof value === 'function') {
  252. const eventType = key.slice(2);
  253. element.addEventListener(eventType, value);
  254. } else if (typeof value === 'object') {
  255. _setElementProperties(element, key, value);
  256. } else {
  257. // @ts-ignore
  258. element[key] = value;
  259. }
  260. }
  261. };
  262.  
  263. /**
  264. * Sets custom attributes for an element.
  265. * @template {ElementTag} T
  266. * @param {HTMLElement} element
  267. * @param {ElementAttributes<T>} attributes
  268. */
  269. const _setCustomElementAttributes = (element, attributes) => {
  270. const filteredAttributes = Object.entries(attributes).filter(([, value]) => Utils.isSet(value));
  271. for (const [key, value] of filteredAttributes) {
  272. element.setAttribute(key, value);
  273. }
  274. };
  275.  
  276. /**
  277. * Sets properties for the attribute of an element.
  278. * @param {HTMLElement} element
  279. * @param {string} attribute
  280. * @param {Object} properties
  281. */
  282. const _setElementProperties = (element, attribute, properties) => {
  283. const filteredProperties = Object.entries(properties).filter(([, value]) => Utils.isSet(value));
  284. for (const [key, value] of filteredProperties) {
  285. // @ts-ignore
  286. element[attribute][key] = value;
  287. }
  288. };
  289.  
  290. /**
  291. * Appends children to an element from an element array.
  292. * @param {HTMLElement} element
  293. * @param {ElementArrayChildren} children
  294. */
  295. const _appendElementChildren = (element, children) => {
  296. const docFragment = _buildFragment(Array.isArray(children) ? children : [children]);
  297. if (docFragment) {
  298. element.appendChild(docFragment);
  299. }
  300. };
  301.  
  302. /**
  303. * Observes a node for mutations.
  304. * @param {Node} node The node to observe.
  305. * @param {MutationTypes | null} types The types of mutations to observe. Defaults to the child list of the node and all its descendants.
  306. * @param {NodeCallback} callback The callback to call with each updated / added node.
  307. * @returns {MutationObserver} The observer.
  308. */
  309. const observeNode = (node, types, callback) => {
  310. const observer = new MutationObserver((mutations) =>
  311. _processNodeMutations(mutations, callback)
  312. );
  313. observer.observe(
  314. node,
  315. types || {
  316. childList: true,
  317. subtree: true,
  318. }
  319. );
  320. return observer;
  321. };
  322.  
  323. /**
  324. * @param {MutationRecord[]} mutations
  325. * @param {NodeCallback} callback
  326. */
  327. const _processNodeMutations = (mutations, callback) => {
  328. for (const mutation of mutations) {
  329. if (mutation.type === 'attributes') {
  330. callback(mutation.target);
  331. } else {
  332. mutation.addedNodes.forEach(callback);
  333. }
  334. }
  335. };
  336.  
  337. /**
  338. * Parses an HTML string into a DOM.
  339. * @param {string} html The HTML string to parse.
  340. * @returns {Document} The parsed DOM.
  341. */
  342. const parse = (html) => {
  343. return _parser.parseFromString(html, 'text/html');
  344. };
  345.  
  346. return {
  347. dynamicQuerySelector,
  348. insertElements,
  349. observeNode,
  350. parse,
  351. };
  352. })();