utils.js

GitHub userscript utilities

目前為 2020-12-07 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.gf.qytechs.cn/scripts/398877/877686/utilsjs.js

  1. /* GitHub userscript utilities v0.1.1-alpha
  2. * Copyright © 2020 Rob Garrison
  3. * License: MIT
  4. */
  5. /* exported
  6. * $ $$
  7. * addClass removeClass toggleClass
  8. * on off make
  9. * debounce
  10. * addMenu
  11. */
  12. "use strict";
  13.  
  14. const REGEX = {
  15. WHITESPACE: /\s+/,
  16. NAMESPACE: /[.:]/,
  17. COMMA: /\s*,\s*/
  18. };
  19.  
  20. /* DOM utilities */
  21. const $ = (selector, el) => (el || document).querySelector(selector);
  22. const $$ = (selector, el) => [...(el || document).querySelectorAll(selector)];
  23.  
  24. /**
  25. * Add class name(s) to one or more elements
  26. * @param {HTMLElements[]|Nodelist|HTMLElement|Node} elements
  27. * @param {string|array} classes - class name(s) to add; string can contain a
  28. * comma separated list
  29. */
  30. const addClass = (elements, classes) => {
  31. const classNames = _.getClasses(classes);
  32. const els = _.createElementArray(elements);
  33. let index = els.length;
  34. while (index--) {
  35. els[index]?.classList.add(...classNames);
  36. }
  37. };
  38.  
  39. /**
  40. * Remove class name(s) from one or more elements
  41. * @param {HTMLElements[]|NodeList|HTMLElement|Node} elements
  42. * @param {string|array} classes - class name(s) to add; string can contain a
  43. * comma separated list
  44. */
  45. const removeClass = (elements, classes) => {
  46. const classNames = _.getClasses(classes);
  47. const els = _.createElementArray(elements);
  48. let index = els.length;
  49. while (index--) {
  50. els[index]?.classList.remove(...classNames);
  51. }
  52. };
  53.  
  54. /**
  55. * Toggle class name of DOM element(s)
  56. * @param {HTMLElement|HTMLElement[]|NodeList} els
  57. * @param {string} name - class name to toggle (toggle only accepts one name)
  58. * @param {boolean} flag - force toggle; true = add class, false = remove class;
  59. * if undefined, the class will be toggled based on the element's class name
  60. */
  61. // flag = true, then add class
  62. const toggleClass = (elements, className, flag) => {
  63. const els = _.createElementArray(elements);
  64. let index = elms.length;
  65. while (index--) {
  66. els[index]?.classList.toggle(className, flag);
  67. }
  68. };
  69.  
  70. /**
  71. * Add/remove event listener
  72. * @param {HTMLElement|HTMLElement[]|NodeList} els
  73. * @param {string} name - event name(s) to bind, e.g. "mouseup mousedown"
  74. * @param {function} handler - event handler
  75. * @param {options} eventListener options
  76. */
  77. const on = (els, name = "", handler, options) => {
  78. _.eventListener("add", els, name, handler, options);
  79. };
  80. const off = (els, name = "", handler, options) => {
  81. _.eventListener("remove", els, name, handler, options);
  82. }
  83.  
  84. const _ = {};
  85. _.createElementArray = elements => {
  86. if (Array.isArray(elements)) {
  87. return elements;
  88. }
  89. return elements instanceof NodeList ? [...elements] : [elements];
  90. };
  91. _.eventListener = (type, els, name, handler, options) => {
  92. const events = name.split(REGEX.WHITESPACE);
  93. _.createElementArray(els).forEach(el => {
  94. events.forEach(ev => {
  95. el?.[`${type}EventListener`](ev, handler, options);
  96. });
  97. });
  98. };
  99. _.getClasses = classes => {
  100. if (Array.isArray(classes)) {
  101. return classes;
  102. }
  103. const names = classes.toString();
  104. return names.contains(",") ? names.split(REGEX.COMMA) : [names];
  105. };
  106.  
  107. /**
  108. * Helpers
  109. */
  110. const debounce = (fxn, time = 500) => {
  111. let timer;
  112. return function() {
  113. clearTimeout(timer);
  114. timer = setTimeout(() => {
  115. fxn.apply(this, arguments);
  116. }, time);
  117. }
  118. }
  119.  
  120. /**
  121. * @typedef Utils~makeOptions
  122. * @type {object}
  123. * @property {string} el - HTML element tag, e.g. "div" (default)
  124. * @property {string} appendTo - selector of target element to append menu
  125. * @property {string} className - CSS classes to add to the element
  126. * @property {object} attrs - HTML attributes (as key/value paries) to set
  127. * @property {object} text - string added to el using textContent
  128. * @property {string} html - html to be added using `innerHTML` (overrides `text`)
  129. * @property {array} children - array of elements to append to the created element
  130. */
  131. /**
  132. * Create a DOM element
  133. * @param {Utils~makeOptions}
  134. * @returns {HTMLElement} (may be already inserted in the DOM)
  135. * @example
  136. make({ el: 'ul', className: 'wrapper', appendTo: 'body' }, [
  137. make({ el: 'li', text: 'item #1' }),
  138. make({ el: 'li', text: 'item #2' })
  139. ]);
  140. */
  141. const make = (obj, children) => {
  142. const el = document.createElement(obj.el || "div");
  143. const xref = {
  144. className: "className",
  145. id: "id",
  146. text: "textContent",
  147. html: "innerHTML", // overrides text setting
  148. };
  149. Object.keys(xref).forEach(key => {
  150. if (obj[key]) {
  151. el[xref[key]] = obj[key];
  152. }
  153. })
  154. if (obj.attrs) {
  155. for (let key in obj.attrs) {
  156. if (obj.attrs.hasOwnProperty(key)) {
  157. el.setAttribute(key, obj.attrs[key]);
  158. }
  159. }
  160. }
  161. if (Array.isArray(children) && children.length) {
  162. children.forEach(child => el.appendChild(child));
  163. }
  164. if (obj.appendTo) {
  165. const wrap = typeof obj.appendTo === "string" ? $(el) : el;
  166. if (wrap) {
  167. wrap.appendChild(el);
  168. }
  169. }
  170. return el;
  171. }
  172.  
  173. /* Add GitHub menu
  174. * Example set up
  175. ghMenu.open(
  176. "Popup Title",
  177. [{
  178. name: "Title",
  179. type: "text",
  180. get: () => GM_getValue("title"),
  181. set: value => GM_setValue("title", value)
  182. }, {
  183. name: "Border width (px)",
  184. type: "number",
  185. get: () => GM_getValue("border-width"),
  186. set: value => GM_setValue("border-width", value)
  187. }, {
  188. name: "Is enabled?",
  189. type: "checkbox",
  190. get: () => GM_getValue("enabled"),
  191. set: value => GM_setValue("enabled", value)
  192. }, {
  193. name: "Background Color",
  194. type: "color",
  195. get: () => GM_getValue("bkg-color"),
  196. set: value => GM_setValue("bkg-color", value)
  197. }, {
  198. name: "Widget enabled",
  199. type: "checkbox",
  200. get: () => GM_getValue("widget-is-enabled"),
  201. set: value => GM_setValue("widget-is-enabled", value)
  202. }, {
  203. name: "Image choice",
  204. type: "select",
  205. get: () => GM_getValue("img-choice"),
  206. set: value => GM_setValue("img-choice", value),
  207. options: [
  208. { label: "Car", value: "/images/car.jpg" },
  209. { label: "Jet", value: "/images/jet.jpg" },
  210. { label: "Cat", value: "/images/cat.jpg" }
  211. ]
  212. }]
  213. );
  214. */
  215. const ghMenu = {
  216. init: () => {
  217. if (!$("#ghmenu-style")) {
  218. make({
  219. el: "style",
  220. id: "ghmenu-style",
  221. textContent: `
  222. #ghmenu, #ghmenu summary { cursor: default; }
  223. #ghmenu summary:before { cursor: pointer; }
  224. #ghmenu-inner input[type="color"] { border: 0; padding: 0 }
  225. #ghmenu-inner ::-webkit-color-swatch-wrapper { border: 0; padding: 0; }
  226. #ghmenu-inner ::-moz-color-swatch-wrapper { border: 0; padding: 0; }
  227. }
  228. `,
  229. appendTo: "body"
  230. });
  231. }
  232. },
  233.  
  234. open: (title, options) => {
  235. if (!$("#ghmenu")) {
  236. ghMenu._createMenu(title);
  237. ghMenu._options = options;
  238. }
  239. ghMenu._title = title;
  240. ghMenu._addContent(options);
  241. },
  242. close: event => {
  243. if (event) {
  244. event.preventDefault();
  245. }
  246. const menu = $("#ghmenu");
  247. if (menu) {
  248. menu.remove();
  249. }
  250. },
  251. append: options => {
  252. const menu = $("#ghmenu");
  253. if (menu) {
  254. ghMenu._appendContent(options);
  255. } else {
  256. ghMenu.open("", options);
  257. }
  258. },
  259. refresh: () => {
  260. ghMenu._addContent(ghMenu._options);
  261. },
  262.  
  263. _types: {
  264. _input: (type, eventType, opts) => {
  265. const elm = make({
  266. el: "input",
  267. id: `${opts.id}-input`,
  268. className: `ghmenu-${type} ${type === "checkbox"
  269. ? "m-2"
  270. : "form-control input-block width-full"
  271. }`,
  272. attrs: {
  273. type,
  274. value: opts.get()
  275. },
  276. });
  277. const handler = e => opts.set(type === "checkbox"
  278. ? e.target.checked
  279. : e.target.value
  280. );
  281. on(elm, eventType, handler);
  282. return elm;
  283. },
  284. text: opts => ghMenu._types._input("text", "input", opts),
  285. number: opts => ghMenu._types._input("number", "input", opts),
  286. checkbox: opts => ghMenu._types._input("checkbox", "change", opts),
  287. color: opts => ghMenu._types._input("color", "change", opts),
  288. radio: opts => {},
  289. select: opts => {
  290. const elm = make({
  291. el: "select",
  292. className: "width-full ghmenu-select",
  293. attrs: {
  294. value: opts.get()
  295. }
  296. }, opts.options.map(obj => (
  297. make({
  298. el: "option",
  299. text: obj.label,
  300. attrs: {
  301. value: obj.value
  302. }
  303. })
  304. )));
  305. on(elm, "change", e => opts.set(e.target.value));
  306. return elm;
  307. },
  308.  
  309. /* TO DO
  310. * - add multiple?
  311. * colors: ['#000', '#fff']
  312. * guideline: { width: '.2', color: '#a00', chars: 80 }
  313. * - link to more details/docs?
  314. */
  315. group: opts => {
  316. const group = opts.group;
  317. if (Array.isArray(group) && group.length) {
  318. const fragment = document.createDocumentFragment();
  319. fragment.appendChild(make({ el: "strong", text: opts.name }));
  320. group.forEach(entry => {
  321. const row = make({
  322. className: "Box-row d-flex flex-row pr-0"
  323. }, [
  324. ghMenu._createLabel(entry.id, entry.name),
  325. make({
  326. id,
  327. className: `ml-2 no-wrap${
  328. // align checkbox to right edge
  329. opt.type === "checkbox" ? " d-flex flex-justify-end" : ""
  330. }`,
  331. })
  332. ])
  333. })
  334. }
  335. },
  336. },
  337. _options: [],
  338. _createMenu: () => {
  339. // create menu
  340. make({
  341. el: "details",
  342. id: "ghmenu",
  343. className: "details-reset details-overlay details-overlay-dark lh-default text-gray-dark",
  344. attrs: {
  345. open: true
  346. },
  347. html: `
  348. <summary role="button" aria-label="Close dialog" />
  349. <details-dialog
  350. id="ghmenu-dialog"
  351. class="Box Box--overlay d-flex flex-column anim-fade-in fast container-xl"
  352. role="dialog"
  353. aria-modal="true"
  354. tab-index="-1"
  355. >
  356. <div class="readability-extra d-flex flex-auto flex-column overflow-hidden">
  357. <div class="Box-header">
  358. <h2 id="ghmenu-title" class="Box-title"></h2>
  359. </div>
  360. <div class="Box-body p-0 overflow-scroll">
  361. <div class="container-lg p-responsive advanced-search-form">
  362. <fieldset id="ghmenu-inner" class="pb-2 mb-2 min-width-0" />
  363. </div>
  364. </div>
  365. </div>
  366. <button id="ghmenu-close-menu" class="Box-btn-octicon m-0 btn-octicon position-absolute right-0 top-0" type="button" aria-label="Close dialog" data-close-dialog="">
  367. <svg class="octicon octicon-x" viewBox="0 0 12 16" version="1.1" width="12" height="16" aria-hidden="true">
  368. <path fill-rule="evenodd" d="M7.48 8l3.75 3.75-1.48 1.48L6 9.48l-3.75 3.75-1.48-1.48L4.52 8 .77 4.25l1.48-1.48L6 6.52l3.75-3.75 1.48 1.48L7.48 8z" />
  369. </svg>
  370. </button>
  371. </details-dialog>`,
  372. appendTo: "body"
  373. });
  374. on($("#ghmenu-close-menu"), "click", e => ghMenu.close(e), { once: true });
  375. on($("#ghmenu summary"), "click", e => {
  376. e.preventDefault();
  377. e.stopPropagation();
  378. const target = e.target;
  379. if (target && !target.closest("#ghmenu-dialog")) {
  380. ghMenu.close(e);
  381. }
  382. });
  383. },
  384. _addContent: options => {
  385. const menu = $("#ghmenu-inner");
  386. if (menu) {
  387. menu.innerHTML = "";
  388. ghMenu._appendContent(options);
  389. }
  390. },
  391. /* <dt><label for="{ID}-input">{NAME}</label></dt> */
  392. _createLabel: (id, text) => make({
  393. el: "dt",
  394. }, [
  395. make({
  396. el: "label",
  397. className: "flex-auto",
  398. text,
  399. attrs: {
  400. for: `${id}-input`
  401. }
  402. })
  403. ]),
  404. _appendContent: options => {
  405. const container = $("#ghmenu-inner");
  406. if (container) {
  407. // update title, if needed
  408. $("#ghmenu-title").textContent = ghMenu._title;
  409.  
  410. const fragment = document.createDocumentFragment();
  411. options.forEach((opt, indx) => {
  412. const id = `ghmenu-${opt.name.replace(/\s/g, "")}-${indx}`;
  413. const output = opt.type === "group"
  414. ? ghMenu._types.group({ ...opt, id })
  415. : make({
  416. el: "dl",
  417. className: "form-group flattened d-flex d-md-block flex-column border-bottom my-0 py-2",
  418. }, [
  419. ghMenu._createLabel(id, opt.name),
  420. make({
  421. el: "dd",
  422. id,
  423. className: opt.type === "checkbox"
  424. ? "d-flex flex-justify-end"
  425. : "",
  426. }, [
  427. ghMenu._types[opt.type || "text"]({ ...opt, id })
  428. ])
  429. ]);
  430. fragment.appendChild(output);
  431. });
  432. container.appendChild(fragment);
  433. }
  434. }
  435. };

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址