utils.js

GitHub userscript utilities

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/398877/1333419/utilsjs.js

  1. /* GitHub userscript utilities v0.3.0
  2. * Copyright © 2023 Rob Garrison
  3. * License: MIT
  4. */
  5. /* exported
  6. * $ $$
  7. * addClass removeClass toggleClass
  8. * removeEls removeSelection
  9. * on off make
  10. * debounce
  11. * iterateGenerator
  12. */
  13. "use strict";
  14.  
  15. const REGEX = {
  16. WHITESPACE: /\s+/,
  17. NAMESPACE: /[.:]/,
  18. COMMA: /\s*,\s*/
  19. };
  20.  
  21. /* DOM utilities */
  22. /**
  23. * Find & return a single DOM node
  24. * @param {String} selector - CSS selector string
  25. * @param {HTMLElement} el - DOM node to start the query (defaults to document)
  26. * @returns {HTMLElement|null}
  27. */
  28. const $ = (selector, el) => (el || document).querySelector(selector);
  29.  
  30. /**
  31. * Find & return multiple DOM nodes
  32. * @param {String} selector - CSS selector string
  33. * @param {HTMLElement} el - DOM node to start the query (defaults to document)
  34. * @returns {HTMLElement[]}
  35. */
  36. const $$ = (selector, el) => [...(el || document).querySelectorAll(selector)];
  37.  
  38. /**
  39. * Common functions
  40. */
  41. const _ = {};
  42. /**
  43. * Return an array of elements
  44. * @param {HTMLElement|HTMLElement[]|NodeList} elements
  45. * @returns {HTMLElement[]}
  46. */
  47. _.createElementArray = elements => {
  48. if (Array.isArray(elements)) {
  49. return elements;
  50. }
  51. return elements instanceof NodeList ? [...elements] : [elements];
  52. };
  53. /**
  54. * Common event listener code
  55. * @param {String} type - "add" or "remove" event listener
  56. * @param {HTMLElement[]} els - DOM node array that need listeners
  57. * @param {String} name - Event name, e.g. "click", "mouseover", etc
  58. * @param {Function} handler - Event callback
  59. * @param {Object} options - Event listener options or useCapture - see
  60. * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#parameters
  61. */
  62. _.eventListener = (type, els, name, handler, options) => {
  63. const events = name.split(REGEX.WHITESPACE);
  64. _.createElementArray(els).forEach(el => {
  65. events.forEach(ev => {
  66. el?.[`${type}EventListener`](ev, handler, options);
  67. });
  68. });
  69. };
  70. /**
  71. * Create an array of classes/event types from a space or comma separated string
  72. * @param {String} classes - space or comma separated list of classes or events
  73. * @returns {String[]}
  74. */
  75. _.getClasses = classes => {
  76. if (Array.isArray(classes)) {
  77. return classes;
  78. }
  79. const names = classes.toString();
  80. return names.includes(",") ? names.split(REGEX.COMMA) : [names];
  81. };
  82.  
  83. /**
  84. * Add class name(s) to one or more elements
  85. * @param {HTMLElements[]|Nodelist|HTMLElement|Node} elements
  86. * @param {string|array} classes - class name(s) to add; string can contain a
  87. * comma separated list
  88. */
  89. const addClass = (elements, classes) => {
  90. const classNames = _.getClasses(classes);
  91. const els = _.createElementArray(elements);
  92. let index = els.length;
  93. while (index--) {
  94. els[index]?.classList.add(...classNames);
  95. }
  96. };
  97.  
  98. /**
  99. * Remove class name(s) from one or more elements
  100. * @param {HTMLElements[]|NodeList|HTMLElement|Node} elements
  101. * @param {string|array} classes - class name(s) to add; string can contain a
  102. * comma separated list
  103. */
  104. const removeClass = (elements, classes) => {
  105. const classNames = _.getClasses(classes);
  106. const els = _.createElementArray(elements);
  107. let index = els.length;
  108. while (index--) {
  109. els[index]?.classList.remove(...classNames);
  110. }
  111. };
  112.  
  113. /**
  114. * Toggle class name of DOM element(s)
  115. * @param {HTMLElement|HTMLElement[]|NodeList} els
  116. * @param {string} name - class name to toggle (toggle only accepts one name)
  117. * @param {boolean} flag - force toggle; true = add class, false = remove class;
  118. * if undefined, the class will be toggled based on the element's class name
  119. */
  120. // flag = true, then add class
  121. const toggleClass = (elements, className, flag) => {
  122. const els = _.createElementArray(elements);
  123. let index = elms.length;
  124. while (index--) {
  125. els[index]?.classList.toggle(className, flag);
  126. }
  127. };
  128.  
  129. /**
  130. * Remove DOM nodes
  131. * @param {String} selector - CSS selector string
  132. * @param {HTMLElement|undefined} el - parent DOM node (defaults to document)
  133. */
  134. const removeEls = (selector, el) => {
  135. let els = $$(selector, el);
  136. let index = els.length;
  137. while (index--) {
  138. els[index].parentNode.removeChild(els[index]);
  139. }
  140. };
  141.  
  142. /**
  143. * Remove text selection
  144. */
  145. const removeSelection = () => {
  146. // remove text selection - https://stackoverflow.com/a/3171348/145346
  147. const sel = window.getSelection
  148. ? window.getSelection()
  149. : document.selection;
  150. if (sel) {
  151. if (sel.removeAllRanges) {
  152. sel.removeAllRanges();
  153. } else if (sel.empty) {
  154. sel.empty();
  155. }
  156. }
  157. };
  158.  
  159. /**
  160. * Add/remove event listener
  161. * @param {HTMLElement|HTMLElement[]|NodeList} els
  162. * @param {string} name - event name(s) to bind, e.g. "mouseup mousedown"; also
  163. * accpets a comma separated string, e.g. "mouseup, mousedown"
  164. * @param {function} handler - event handler
  165. * @param {options} eventListener options
  166. */
  167. const on = (els, name = "", handler, options) => {
  168. _.eventListener("add", els, name, handler, options);
  169. };
  170. const off = (els, name = "", handler, options) => {
  171. _.eventListener("remove", els, name, handler, options);
  172. }
  173.  
  174. /**
  175. * **** Helpers ****
  176. */
  177. /**
  178. * Debounce
  179. * @param {Function} fxn - callback executed after debounce
  180. * @param {Number} time - time (in ms) to delay
  181. * @returns {Function} debounced function
  182. */
  183. const debounce = (fxn, time = 500) => {
  184. let timer;
  185. return function() {
  186. clearTimeout(timer);
  187. timer = setTimeout(() => {
  188. fxn.apply(this, arguments);
  189. }, time);
  190. }
  191. }
  192.  
  193. /**
  194. * Iterate through function asynchronously until done
  195. * @param {function} generator
  196. * @param {number} maxPerCycle
  197. */
  198. const iterateGenerator = (generator, maxPerCycle = 40) => {
  199. let status;
  200. // loop with delay to allow user interaction
  201. const loop = () => {
  202. for (let i = 0; i < maxPerCycle; i++) {
  203. status = generator.next();
  204. }
  205. if (!status.done) {
  206. requestAnimationFrame(loop);
  207. }
  208. };
  209. loop();
  210. }
  211.  
  212. /**
  213. * @typedef Utils~makeEvents
  214. * @type {object[]}
  215. * @property {string} el - event listener target
  216. * @property {string} type - event type
  217. * @property {func} callback - event callback
  218. */
  219. /**
  220. * @typedef Utils~makeOptions
  221. * @type {object}
  222. * @property {string} el - HTML element tag, e.g. "div" (default)
  223. * @property {string} appendTo - selector of target element to append menu
  224. * @property {string} className - CSS classes to add to the element
  225. * @property {object} attrs - HTML attributes (as key/value paries) to set
  226. * @property {object} text - string added to el using textContent
  227. * @property {string} html - html to be added using `innerHTML` (overrides `text`)
  228. * @property {array} children - array of elements to append to the created element
  229. * @property {Utils~makeEvent} events - events to attach listeners
  230. */
  231. /**
  232. * Create a DOM element
  233. * @param {Utils~makeOptions}
  234. * @returns {HTMLElement} (may be already inserted in the DOM)
  235. * @example
  236. make({ el: 'ul', className: 'wrapper', appendTo: 'body' }, [
  237. make({ el: 'li', text: 'item #1' }),
  238. make({ el: 'li', text: 'item #2' })
  239. ]);
  240. */
  241. const make = (obj = {}, children) => {
  242. const el = document.createElement(obj.el || "div");
  243. const { appendTo, attrs, events } = obj;
  244. const xref = {
  245. className: "className",
  246. id: "id",
  247. text: "textContent",
  248. html: "innerHTML", // overrides text setting
  249. type: "type" // button type
  250. };
  251. Object.keys(xref).forEach(key => {
  252. if (obj[key]) {
  253. el[xref[key]] = obj[key];
  254. }
  255. });
  256. if (attrs) {
  257. Object.keys(attrs).forEach(key => {
  258. el.setAttribute(key, attrs[key]);
  259. });
  260. }
  261. if (Array.isArray(children) && children.length) {
  262. children.forEach(child => el.appendChild(child));
  263. }
  264. if (events?.length) {
  265. for (let event of events) {
  266. on(event?.el || el, event.type, event.callback);
  267. }
  268. }
  269. if (appendTo) {
  270. const wrap = typeof appendTo === "string"
  271. ? $(appendTo)
  272. : appendTo;
  273. if (wrap) {
  274. wrap.appendChild(el);
  275. }
  276. }
  277. return el;
  278. }