Greasy Fork is available in English.

Basic Functions (For userscripts)

Useful functions for myself

Fra 24.01.2025. Se den seneste versjonen.

Dette scriptet burde ikke installeres direkte. Det er et bibliotek for andre script å inkludere med det nye metadirektivet // @require https://update.greatest.deepsurf.us/scripts/456034/1526010/Basic%20Functions%20%28For%20userscripts%29.js

  1. /* eslint-disable no-multi-spaces */
  2. /* eslint-disable no-return-assign */
  3.  
  4. // ==UserScript==
  5. // @name Basic Functions (For userscripts)
  6. // @name:zh-CN 常用函数(用户脚本)
  7. // @name:en Basic Functions (For userscripts)
  8. // @namespace PY-DNG Userscripts
  9. // @version 0.10.1
  10. // @description Useful functions for myself
  11. // @description:zh-CN 自用函数
  12. // @description:en Useful functions for myself
  13. // @author PY-DNG
  14. // @license GPL-3.0-or-later
  15. // ==/UserScript==
  16.  
  17. // Note: version 0.8.2.1 is modified just the license and it's not uploaded to GF yet 23-11-26 15:03
  18. // Note: version 0.8.3.1 is added just the description of parseArgs and has not uploaded to GF yet 24-02-03 18:55
  19.  
  20. let [
  21. // Console & Debug
  22. LogLevel, DoLog, Err, Assert,
  23.  
  24. // DOM
  25. $, $All, $CrE, $AEL, $$CrE, addStyle, detectDom, destroyEvent,
  26.  
  27. // Data
  28. copyProp, copyProps, parseArgs, escJsStr, replaceText,
  29.  
  30. // Environment & Browser
  31. getUrlArgv, dl_browser, dl_GM,
  32.  
  33. // Logic & Task
  34. AsyncManager, queueTask, testChecker, registerChecker, loadFuncs
  35. ] = (function() {
  36. /**
  37. * level defination for DoLog function, bigger ones has higher possibility to be printed in console
  38. * @property {Number} None - 0
  39. * @property {Number} Error - 1
  40. * @property {Number} Success - 2
  41. * @property {Number} Warning - 3
  42. * @property {Number} Info - 4
  43. */
  44. /**
  45. * Logger with level and logger function specification
  46. * @param {Number} [level=LogLevel.Info] - level specified in LogLevel object
  47. * @param {String} content - log content
  48. * @param {String} [logger=log] - which log function to use (in window.console[logger])
  49. */
  50. const [LogLevel, DoLog] = (function() {
  51. const LogLevel = {
  52. None: 0,
  53. Error: 1,
  54. Success: 2,
  55. Warning: 3,
  56. Info: 4,
  57. };
  58.  
  59. return [LogLevel, DoLog];
  60. function DoLog() {
  61. // Get window
  62. const win = (typeof(unsafeWindow) === 'object' && unsafeWindow !== null) ? unsafeWindow : window;
  63.  
  64. const LogLevelMap = {};
  65. LogLevelMap[LogLevel.None] = {
  66. prefix: '',
  67. color: 'color:#ffffff'
  68. }
  69. LogLevelMap[LogLevel.Error] = {
  70. prefix: '[Error]',
  71. color: 'color:#ff0000'
  72. }
  73. LogLevelMap[LogLevel.Success] = {
  74. prefix: '[Success]',
  75. color: 'color:#00aa00'
  76. }
  77. LogLevelMap[LogLevel.Warning] = {
  78. prefix: '[Warning]',
  79. color: 'color:#ffa500'
  80. }
  81. LogLevelMap[LogLevel.Info] = {
  82. prefix: '[Info]',
  83. color: 'color:#888888'
  84. }
  85. LogLevelMap[LogLevel.Elements] = {
  86. prefix: '[Elements]',
  87. color: 'color:#000000'
  88. }
  89.  
  90. // Current log level
  91. DoLog.logLevel = (win.isPY_DNG && win.userscriptDebugging) ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error
  92.  
  93. // Log counter
  94. DoLog.logCount === undefined && (DoLog.logCount = 0);
  95.  
  96. // Get args
  97. let [level, logContent, logger] = parseArgs([...arguments], [
  98. [2],
  99. [1,2],
  100. [1,2,3]
  101. ], [LogLevel.Info, 'DoLog initialized.', 'log']);
  102.  
  103. let msg = '%c' + LogLevelMap[level].prefix + (typeof GM_info === 'object' ? `[${GM_info.script.name}]` : '') + (LogLevelMap[level].prefix ? ' ' : '');
  104. let subst = LogLevelMap[level].color;
  105.  
  106. switch (typeof(logContent)) {
  107. case 'string':
  108. msg += '%s';
  109. break;
  110. case 'number':
  111. msg += '%d';
  112. break;
  113. default:
  114. msg += '%o';
  115. break;
  116. }
  117.  
  118. // Log when log level permits
  119. if (level <= DoLog.logLevel) {
  120. // Log to console when log level permits
  121. if (level <= DoLog.logLevel) {
  122. if (++DoLog.logCount > 512) {
  123. console.clear();
  124. DoLog.logCount = 0;
  125. }
  126. console[logger](msg, subst, logContent);
  127. }
  128. }
  129. }
  130. }) ();
  131.  
  132. // type: [Error, TypeError]
  133. /**
  134. * @typedef {Number} ErrorType
  135. *
  136. */
  137. /**
  138. * Throw an error
  139. * @param {String} msg - the error message
  140. * @param {ErrorType} [type=0] - error type, which also means the Error constructor
  141. */
  142. function Err(msg, type=0) {
  143. throw new [Error, TypeError][type]((typeof GM_info === 'object' ? `[${GM_info.script.name}]` : '') + msg);
  144. }
  145.  
  146. function Assert(val, errmsg, errtype) {
  147. val || Err(errmsg, errtype);
  148. }
  149.  
  150. // Basic functions
  151. // querySelector
  152. function $() {
  153. switch(arguments.length) {
  154. case 2:
  155. return arguments[0].querySelector(arguments[1]);
  156. break;
  157. default:
  158. return document.querySelector(arguments[0]);
  159. }
  160. }
  161. // querySelectorAll
  162. function $All() {
  163. switch(arguments.length) {
  164. case 2:
  165. return arguments[0].querySelectorAll(arguments[1]);
  166. break;
  167. default:
  168. return document.querySelectorAll(arguments[0]);
  169. }
  170. }
  171. // createElement
  172. function $CrE() {
  173. switch(arguments.length) {
  174. case 2:
  175. return arguments[0].createElement(arguments[1]);
  176. break;
  177. default:
  178. return document.createElement(arguments[0]);
  179. }
  180. }
  181. // addEventListener
  182. function $AEL(...args) {
  183. const target = args.shift();
  184. return target.addEventListener.apply(target, args);
  185. }
  186. function $$CrE() {
  187. const [tagName, props, attrs, classes, styles, listeners] = parseArgs([...arguments], [
  188. function(args, defaultValues) {
  189. const arg = args[0];
  190. return {
  191. 'string': () => [arg, ...defaultValues.filter((arg, i) => i > 0)],
  192. 'object': () => ['tagName', 'props', 'attrs', 'classes', 'styles', 'listeners'].map((prop, i) => arg.hasOwnProperty(prop) ? arg[prop] : defaultValues[i])
  193. }[typeof arg]();
  194. },
  195. [1,2],
  196. [1,2,3],
  197. [1,2,3,4],
  198. [1,2,3,4,5]
  199. ], ['div', {}, {}, [], {}, []]);
  200. const elm = $CrE(tagName);
  201. for (const [name, val] of Object.entries(props)) {
  202. elm[name] = val;
  203. }
  204. for (const [name, val] of Object.entries(attrs)) {
  205. elm.setAttribute(name, val);
  206. }
  207. for (const cls of Array.isArray(classes) ? classes : [classes]) {
  208. elm.classList.add(cls);
  209. }
  210. for (const [name, val] of Object.entries(styles)) {
  211. elm.style[name] = val;
  212. }
  213. for (const listener of listeners) {
  214. $AEL(...[elm, ...listener]);
  215. }
  216. return elm;
  217. }
  218.  
  219. // Append a style text to document(<head>) with a <style> element
  220. // arguments: css | css, id | parentElement, css, id
  221. // remove old one when id duplicates with another element in document
  222. function addStyle() {
  223. // Get arguments
  224. const [parentElement, css, id] = parseArgs([...arguments], [
  225. [2],
  226. [2,3],
  227. [1,2,3]
  228. ], [document.head, '', null]);
  229.  
  230. // Make <style>
  231. const style = $CrE("style");
  232. style.textContent = css;
  233. id !== null && (style.id = id);
  234. id !== null && $(`#${id}`) && $(`#${id}`).remove();
  235.  
  236. // Append to parentElement
  237. parentElement.appendChild(style);
  238. return style;
  239. }
  240.  
  241. // Get callback when specific dom/element loaded
  242. // detectDom({[root], selector, callback}) | detectDom(selector, callback) | detectDom(root, selector, callback) | detectDom(root, selector, callback, attributes)
  243. // Supports both callback for multiple detection, and promise for one-time detection.
  244. // By default promise mode is preferred, meaning `callback` argument should be provided explicitly when using callback
  245. // mode (by adding `callback` property in details object, or provide all 4 arguments where callback should be the last)
  246. // This behavior is different from versions that equals to or older than 0.8.4.2, so be careful when using it.
  247. function detectDom() {
  248. let [selectors, root, attributes, callback] = parseArgs([...arguments], [
  249. function(args, defaultValues) {
  250. const arg = args[0];
  251. return {
  252. 'string': () => [arg, ...defaultValues.filter((arg, i) => i > 0)],
  253. 'object': () => ['selector', 'root', 'attributes', 'callback'].map((prop, i) => arg.hasOwnProperty(prop) ? arg[prop] : defaultValues[i])
  254. }[typeof arg]();
  255. },
  256. [2,1],
  257. [2,1,3],
  258. [2,1,3,4],
  259. ], [[''], document, false, null]);
  260. !Array.isArray(selectors) && (selectors = [selectors]);
  261.  
  262. if (select(root, selectors)) {
  263. for (const elm of selectAll(root, selectors)) {
  264. if (callback) {
  265. setTimeout(callback.bind(null, elm));
  266. } else {
  267. return Promise.resolve(elm);
  268. }
  269. }
  270. }
  271.  
  272. const observer = new MutationObserver(mCallback);
  273. observer.observe(root, {
  274. childList: true,
  275. subtree: true,
  276. attributes,
  277. });
  278.  
  279. let isPromise = !callback;
  280. return callback ? observer : new Promise((resolve, reject) => callback = resolve);
  281.  
  282. function mCallback(mutationList, observer) {
  283. const addedNodes = mutationList.reduce((an, mutation) => {
  284. switch (mutation.type) {
  285. case 'childList':
  286. an.push(...mutation.addedNodes);
  287. break;
  288. case 'attributes':
  289. an.push(mutation.target);
  290. break;
  291. }
  292. return an;
  293. }, []);
  294. const addedSelectorNodes = addedNodes.reduce((nodes, anode) => {
  295. if (anode.matches && match(anode, selectors)) {
  296. nodes.add(anode);
  297. }
  298. const childMatches = anode.querySelectorAll ? selectAll(anode, selectors) : [];
  299. for (const cm of childMatches) {
  300. nodes.add(cm);
  301. }
  302. return nodes;
  303. }, new Set());
  304. for (const node of addedSelectorNodes) {
  305. callback(node);
  306. isPromise && observer.disconnect();
  307. }
  308. }
  309.  
  310. function selectAll(elm, selectors) {
  311. !Array.isArray(selectors) && (selectors = [selectors]);
  312. return selectors.map(selector => [...$All(elm, selector)]).reduce((all, arr) => {
  313. all.push(...arr);
  314. return all;
  315. }, []);
  316. }
  317.  
  318. function select(elm, selectors) {
  319. const all = selectAll(elm, selectors);
  320. return all.length ? all[0] : null;
  321. }
  322.  
  323. function match(elm, selectors) {
  324. return !!elm.matches && selectors.some(selector => elm.matches(selector));
  325. }
  326. }
  327.  
  328. // Just stopPropagation and preventDefault
  329. function destroyEvent(e) {
  330. if (!e) {return false;};
  331. if (!e instanceof Event) {return false;};
  332. e.stopPropagation();
  333. e.preventDefault();
  334. }
  335.  
  336. // Object1[prop] ==> Object2[prop]
  337. function copyProp(obj1, obj2, prop) {obj1[prop] !== undefined && (obj2[prop] = obj1[prop]);}
  338. function copyProps(obj1, obj2, props) {(props || Object.keys(obj1)).forEach((prop) => (copyProp(obj1, obj2, prop)));}
  339.  
  340. // Argument parser with sorting and defaultValue support
  341. function parseArgs(args, rules, defaultValues=[]) {
  342. // args and rules should be array, but not just iterable (string is also iterable)
  343. if (!Array.isArray(args) || !Array.isArray(rules)) {
  344. throw new TypeError('parseArgs: args and rules should be array')
  345. }
  346.  
  347. // fill rules[0]
  348. (!Array.isArray(rules[0]) || rules[0].length === 1) && rules.splice(0, 0, []);
  349.  
  350. // max arguments length
  351. const count = rules.length - 1;
  352.  
  353. // args.length must <= count
  354. if (args.length > count) {
  355. throw new TypeError(`parseArgs: args has more elements(${args.length}) longer than ruless'(${count})`);
  356. }
  357.  
  358. // rules[i].length should be === i if rules[i] is an array, otherwise it should be a function
  359. for (let i = 1; i <= count; i++) {
  360. const rule = rules[i];
  361. if (Array.isArray(rule)) {
  362. if (rule.length !== i) {
  363. throw new TypeError(`parseArgs: rules[${i}](${rule}) should have ${i} numbers, but given ${rules[i].length}`);
  364. }
  365. if (!rule.every((num) => (typeof num === 'number' && num <= count))) {
  366. throw new TypeError(`parseArgs: rules[${i}](${rule}) should contain numbers smaller than count(${count}) only`);
  367. }
  368. } else if (typeof rule !== 'function') {
  369. throw new TypeError(`parseArgs: rules[${i}](${rule}) should be an array or a function.`)
  370. }
  371. }
  372.  
  373. // Parse
  374. const rule = rules[args.length];
  375. let parsed;
  376. if (Array.isArray(rule)) {
  377. parsed = [...defaultValues];
  378. for (let i = 0; i < rule.length; i++) {
  379. parsed[rule[i]-1] = args[i];
  380. }
  381. } else {
  382. parsed = rule(args, defaultValues);
  383. }
  384. return parsed;
  385. }
  386.  
  387. // escape str into javascript written format
  388. function escJsStr(str, quote='"') {
  389. str = str.replaceAll('\\', '\\\\').replaceAll(quote, '\\' + quote).replaceAll('\t', '\\t');
  390. str = quote === '`' ? str.replaceAll(/(\$\{[^\}]*\})/g, '\\$1') : str.replaceAll('\r', '\\r').replaceAll('\n', '\\n');
  391. return quote + str + quote;
  392. }
  393.  
  394. // Replace model text with no mismatching of replacing replaced text
  395. // e.g. replaceText('aaaabbbbccccdddd', {'a': 'b', 'b': 'c', 'c': 'd', 'd': 'e'}) === 'bbbbccccddddeeee'
  396. // replaceText('abcdAABBAA', {'BB': 'AA', 'AAAAAA': 'This is a trap!'}) === 'abcdAAAAAA'
  397. // replaceText('abcd{AAAA}BB}', {'{AAAA}': '{BB', '{BBBB}': 'This is a trap!'}) === 'abcd{BBBB}'
  398. // replaceText('abcd', {}) === 'abcd'
  399. /* Note:
  400. replaceText will replace in sort of replacer's iterating sort
  401. e.g. currently replaceText('abcdAABBAA', {'BBAA': 'TEXT', 'AABB': 'TEXT'}) === 'abcdAATEXT'
  402. but remember: (As MDN Web Doc said,) Although the keys of an ordinary Object are ordered now, this was
  403. not always the case, and the order is complex. As a result, it's best not to rely on property order.
  404. So, don't expect replaceText will treat replacer key-values in any specific sort. Use replaceText to
  405. replace irrelevance replacer keys only.
  406. */
  407. function replaceText(text, replacer) {
  408. if (Object.entries(replacer).length === 0) {return text;}
  409. const [models, targets] = Object.entries(replacer);
  410. const len = models.length;
  411. let text_arr = [{text: text, replacable: true}];
  412. for (const [model, target] of Object.entries(replacer)) {
  413. text_arr = replace(text_arr, model, target);
  414. }
  415. return text_arr.map((text_obj) => (text_obj.text)).join('');
  416.  
  417. function replace(text_arr, model, target) {
  418. const result_arr = [];
  419. for (const text_obj of text_arr) {
  420. if (text_obj.replacable) {
  421. const splited = text_obj.text.split(model);
  422. for (const part of splited) {
  423. result_arr.push({text: part, replacable: true});
  424. result_arr.push({text: target, replacable: false});
  425. }
  426. result_arr.pop();
  427. } else {
  428. result_arr.push(text_obj);
  429. }
  430. }
  431. return result_arr;
  432. }
  433. }
  434.  
  435. // Get a url argument from location.href
  436. // also recieve a function to deal the matched string
  437. // returns defaultValue if name not found
  438. // Args: {name, url=location.href, defaultValue=null, dealFunc=((a)=>{return a;})} or (name) or (url, name) or (url, name, defaultValue) or (url, name, defaultValue, dealFunc)
  439. function getUrlArgv(details) {
  440. const [name, url, defaultValue, dealFunc] = parseArgs([...arguments], [
  441. function(args, defaultValues) {
  442. const arg = args[0];
  443. return {
  444. 'string': () => [arg, ...defaultValues.filter((arg, i) => i > 0)],
  445. 'object': () => ['name', 'url', 'defaultValue', 'dealFunc'].map((prop, i) => arg.hasOwnProperty(prop) ? arg[prop] : defaultValues[i])
  446. }[typeof arg]();
  447. },
  448. [2,1],
  449. [2,1,3],
  450. [2,1,3,4]
  451. ], [null, location.href, null, a => a]);
  452.  
  453. if (name === null) { return null; }
  454.  
  455. const search = new URL(url).search;
  456. const objSearch = new URLSearchParams(search);
  457. const raw = objSearch.has(name) ? objSearch.get(name) : defaultValue;
  458. const argv = dealFunc(raw);
  459.  
  460. return argv;
  461. }
  462.  
  463. // Save dataURL to file
  464. function dl_browser(dataURL, filename) {
  465. const a = document.createElement('a');
  466. a.href = dataURL;
  467. a.download = filename;
  468. a.click();
  469. }
  470.  
  471. // File download function
  472. // details looks like the detail of GM_xmlhttpRequest
  473. // onload function will be called after file saved to disk
  474. function dl_GM(details) {
  475. if (!details.url || !details.name) {return false;};
  476.  
  477. // Configure request object
  478. const requestObj = {
  479. url: details.url,
  480. responseType: 'blob',
  481. onload: function(e) {
  482. // Save file
  483. dl_browser(URL.createObjectURL(e.response), details.name);
  484.  
  485. // onload callback
  486. details.onload ? details.onload(e) : function() {};
  487. }
  488. }
  489. if (details.onloadstart ) {requestObj.onloadstart = details.onloadstart;};
  490. if (details.onprogress ) {requestObj.onprogress = details.onprogress;};
  491. if (details.onerror ) {requestObj.onerror = details.onerror;};
  492. if (details.onabort ) {requestObj.onabort = details.onabort;};
  493. if (details.onreadystatechange) {requestObj.onreadystatechange = details.onreadystatechange;};
  494. if (details.ontimeout ) {requestObj.ontimeout = details.ontimeout;};
  495.  
  496. // Send request
  497. GM_xmlhttpRequest(requestObj);
  498. }
  499.  
  500. function AsyncManager() {
  501. const AM = this;
  502.  
  503. // Ongoing tasks count
  504. this.taskCount = 0;
  505.  
  506. // Whether generate finish events
  507. let finishEvent = false;
  508. Object.defineProperty(this, 'finishEvent', {
  509. configurable: true,
  510. enumerable: true,
  511. get: () => (finishEvent),
  512. set: (b) => {
  513. finishEvent = b;
  514. b && AM.taskCount === 0 && AM.onfinish && AM.onfinish();
  515. }
  516. });
  517.  
  518. // Add one task
  519. this.add = () => (++AM.taskCount);
  520.  
  521. // Finish one task
  522. this.finish = () => ((--AM.taskCount === 0 && AM.finishEvent && AM.onfinish && AM.onfinish(), AM.taskCount));
  523. }
  524.  
  525. function queueTask(task, queueId='default') {
  526. init();
  527.  
  528. return new Promise((resolve, reject) => {
  529. queueTask.hasOwnProperty(queueId) || (queueTask[queueId] = { tasks: [], ongoing: 0 });
  530. queueTask[queueId].tasks.push({task, resolve, reject});
  531. checkTask(queueId);
  532. });
  533.  
  534. function init() {
  535. if (!queueTask[queueId]?.initialized) {
  536. queueTask[queueId] = {
  537. // defaults
  538. tasks: [],
  539. ongoing: 0,
  540. max: 3,
  541. sleep: 500,
  542.  
  543. // user's pre-sets
  544. ...(queueTask[queueId] || {}),
  545.  
  546. // initialized flag
  547. initialized: true
  548. }
  549. };
  550. }
  551.  
  552. function checkTask() {
  553. const queue = queueTask[queueId];
  554. setTimeout(() => {
  555. if (queue.ongoing < queue.max && queue.tasks.length) {
  556. const task = queue.tasks.shift();
  557. queue.ongoing++;
  558. setTimeout(
  559. () => task.task().then(v => {
  560. queue.ongoing--;
  561. task.resolve(v);
  562. checkTask(queueId);
  563. }).catch(e => {
  564. queue.ongoing--;
  565. task.reject(e);
  566. checkTask(queueId);
  567. }),
  568. queue.sleep
  569. );
  570. }
  571. });
  572. }
  573. }
  574.  
  575. const [FunctionLoader, loadFuncs, require, isLoaded] = (function() {
  576. /**
  577. * 一般用作函数对象oFunc的加载条件,检测当前环境是否适合/需要该oFunc加载
  578. * @typedef {Object} checker_func
  579. * @property {string} type - checker's identifier
  580. * @property {function} func - actual internal judgement implementation
  581. */
  582. /**
  583. * 一般用作函数对象oFunc的加载条件,检测当前环境是否适合/需要该oFunc加载
  584. * @typedef {Object} checker
  585. * @property {string} type - checker's identifier
  586. * @property {*} value - param that goes into checker function
  587. */
  588. /**
  589. * 被加载函数对象的func函数
  590. * @callback oFuncBody
  591. * @param {oFunc} oFunc
  592. * @returns {*|Promise<*>}
  593. */
  594. /**
  595. * 被加载执行的函数对象
  596. * @typedef {Object} oFunc
  597. * @property {string} id - 每次load(每个FuncPool实例)内唯一的标识符
  598. * @property {checker[]|checker} [checkers] - oFunc执行的条件
  599. * @property {string[]|string} [detectDom] - 如果提供,开始checker检查前会首先等待其中所有css选择器对应的元素在document中出现
  600. * @property {string[]|string} [dependencies] - 如果提供,应为其他函数对象的id或者id列表;开始checker检查前会首先等待其中所有指定的函数对象加载完毕
  601. * @property {boolean} [readonly] - 指定该函数的返回值是否应该被Proxy保护为不可修改对象
  602. * @property {oFuncBody} func - 实际实现了功能的函数
  603. * @property {boolean} [STOP] - [调试用] 指定不执行此函数对象
  604. */
  605.  
  606. const registered_checkers = {
  607. switch: value => value,
  608. url: value => location.href === value,
  609. path: value => location.pathname === value,
  610. regurl: value => !!location.href.match(value),
  611. regpath: value => !!location.pathname.match(value),
  612. starturl: value => location.href.startsWith(value),
  613. startpath: value => location.pathname.startsWith(value),
  614. func: value => value()
  615. };
  616.  
  617. class FuncPool extends EventTarget {
  618. static #STILL_LOADING = Symbol('oFunc still loading');
  619. static FunctionNotFound = Symbol('Function not found');
  620. static FunctionNotLoaded = Symbol('Function not loaded');
  621.  
  622. /** @typedef {symbol|*} return_value */
  623. /** @type {Map<oFunc, return_value>} */
  624. #oFuncs = new Map();
  625.  
  626. /**
  627. * 创建新函数池,并加载提供的函数对象
  628. * @param {oFunc[]|oFunc} [oFuncs] - 可选,需要加载的函数对象或其数组,不提供时默认为空数组
  629. * @returns {FuncPool}
  630. */
  631. constructor(oFuncs=[]) {
  632. super();
  633. this.load(oFuncs);
  634. }
  635.  
  636. /**
  637. * 加载提供的一个或多个函数对象,并将其加入到函数池中
  638. * @param {oFunc[]|oFunc} [oFuncs] - 可选,需要加载的函数对象或其数组,不提供时默认为空数组
  639. * @returns {undefined}
  640. */
  641. load(oFuncs=[]) {
  642. oFuncs = Array.isArray(oFuncs) ? oFuncs : [oFuncs];
  643. for (const oFunc of oFuncs) {
  644. this.#load(oFunc);
  645. }
  646. }
  647.  
  648. /**
  649. * 加载一个函数对象,并将其加入到函数池中
  650. * 当id重复时,直接报错RedeclarationError
  651. * 异步函数,当彻底load完毕/checkers确定不加载时resolve
  652. * 当加载完毕时,广播load事件;如果全部加载完毕,还广播all_load事件
  653. * @param {oFunc} oFunc
  654. * @returns {Promise<undefined>}
  655. */
  656. async #load(oFunc) {
  657. const that = this;
  658.  
  659. // 已经在函数池中的函数对象,不重复load
  660. if (this.#oFuncs.has(oFunc)) {
  661. return;
  662. }
  663.  
  664. // 检查有无重复id
  665. for (const o of this.#oFuncs.keys()) {
  666. if (o.id === oFunc.id) {
  667. throw new RedeclarationError(`Attempts to load oFunc with id already in use: ${oFunc.id}`);
  668. }
  669. }
  670.  
  671. // 设置当前返回值为STILL_LOADING
  672. this.#oFuncs.set(oFunc, FuncPool.#STILL_LOADING);
  673.  
  674. // 加载依赖
  675. const dependencies = Array.isArray(oFunc.dependencies) ? oFunc.dependencies : ( oFunc.dependencies ? [oFunc.dependencies] : [] );
  676. const promise_deps = Promise.all(dependencies.map(id => new Promise((resolve, reject) => {
  677. $AEL(that, 'load', e => e.detail.oFunc.id === id && resolve());
  678. })));
  679.  
  680. // 检测detectDOM中css选择器指定的元素出现
  681. const selectors = Array.isArray(oFunc.detectDom) ? oFunc.detectDom : ( oFunc.detectDom ? [oFunc.detectDom] : [] );
  682. const promise_css = Promise.all(selectors.map(selector => detectDom(selector)));
  683.  
  684. // 等待上述两项完成
  685. await Promise.all([promise_deps, promise_css]);
  686.  
  687. // 检测checkers加载条件
  688. const checkers = Array.isArray(oFunc.checkers) ? oFunc.checkers : ( oFunc.checkers ? [oFunc.checkers] : [] );
  689. if (!testCheckers(checkers)) {
  690. return;
  691. }
  692.  
  693. // 执行函数对象
  694. const raw_return_value = oFunc.func(oFunc);
  695. const return_value = await Promise.resolve(raw_return_value);
  696.  
  697. // 设置返回值
  698. this.#oFuncs.set(oFunc, return_value);
  699.  
  700. // 广播事件
  701. this.dispatchEvent(new CustomEvent('load', {
  702. detail: {
  703. oFunc, id: oFunc.id, return_value
  704. }
  705. }));
  706. Array.from(this.#oFuncs.values()).every(v => v !== FuncPool.#STILL_LOADING) &&
  707. this.dispatchEvent(new CustomEvent('all_load', {}));
  708. }
  709.  
  710. /**
  711. * 获取指定函数对象的返回值
  712. * 如果指定的函数对象不存在,返回FunctionNotFound
  713. * 如果指定的函数对象存在但尚未加载,返回FunctionNotLoaded
  714. * 如果函数对象指定了readonly为真值,则返回前用Proxy包装返回值,使其不可修改
  715. * @param {string} id - 函数对象的id
  716. * @returns {*}
  717. */
  718. require(id) {
  719. for (const [oFunc, return_value] of this.#oFuncs.entries()) {
  720. if (oFunc.id === id) {
  721. if (return_value === FuncPool.#STILL_LOADING) {
  722. return FuncPool.FunctionNotLoaded;
  723. } else {
  724. return oFunc.readonly ? FuncPool.#MakeReadonlyObj(return_value) : return_value;
  725. }
  726. }
  727. }
  728. return FuncPool.FunctionNotFound;
  729. }
  730.  
  731. isLoaded(id) {
  732. for (const [oFunc, return_value] of this.#oFuncs.entries()) {
  733. if (oFunc.id === id) {
  734. if (return_value === FuncPool.#STILL_LOADING) {
  735. return false;
  736. } else {
  737. return true;
  738. }
  739. }
  740. return false;
  741. }
  742. }
  743.  
  744. /**
  745. * 以Proxy包装value,使其属性只读
  746. * 如果传入的不是obj,则直接返回value
  747. * @param {Object} val
  748. * @returns {Proxy}
  749. */
  750. static #MakeReadonlyObj(val) {
  751. return isObject(val) ? new Proxy(val, {
  752. get: function(target, property, receiver) {
  753. return FuncPool.#MakeReadonlyObj(target[property]);
  754. },
  755. set: function(target, property, value, receiver) {},
  756. has: function(target, prop) {},
  757. setPrototypeOf(target, newProto) {
  758. return false;
  759. },
  760. defineProperty(target, property, descriptor) {
  761. return true;
  762. },
  763. deleteProperty(target, property) {
  764. return false;
  765. },
  766. preventExtensions(target) {
  767. return false;
  768. }
  769. }) : val;
  770.  
  771. function isObject(value) {
  772. return ['object', 'function'].includes(typeof value) && value !== null;
  773. }
  774. }
  775. }
  776. class RedeclarationError extends TypeError {}
  777. class CircularDependencyError extends ReferenceError {}
  778.  
  779.  
  780. // 预置的函数池
  781. const default_pool = new FuncPool();
  782.  
  783. /**
  784. * 在预置的函数池中加载函数对象或其数组
  785. * @param {oFunc[]|oFunc} oFuncs - 需要执行的函数对象
  786. * @returns {FuncPool}
  787. */
  788. function loadFuncs(oFuncs) {
  789. default_pool.load(oFuncs);
  790. return default_pool;
  791. }
  792.  
  793. /**
  794. * 在预置的函数池中获取函数对象的返回值
  795. * @param {string} id - 函数对象的字符串id
  796. * @returns {*}
  797. */
  798. function require(id) {
  799. return default_pool.require(id);
  800. }
  801.  
  802. /**
  803. * 在预置的函数池中检查指定函数对象是否已经加载完毕(有返回值可用)
  804. * @param {string} id - 函数对象的字符串id
  805. * @returns {boolean}
  806. */
  807. function isLoaded(id) {
  808. return default_pool.isLoaded(id);
  809. }
  810.  
  811. /**
  812. * 测试给定checker是否检测通过
  813. * 给定多个checker时,checkers之间是 或 关系,有一个checker通过即算作整体通过
  814. * 注意此函数设计和旧版testChecker的设计不同,旧版中一个checker可以有多个值,还可通过checker.all指定多值之间的关系为 与 还是 或
  815. * @param {checker[]|checker} [checkers] - 需要检测的checkers
  816. * @returns {boolean}
  817. */
  818. function testCheckers(checkers=[]) {
  819. checkers = Array.isArray(checkers) ? checkers : [checkers];
  820. return checkers.length === 0 || checkers.some(checker => !!registered_checkers[checker.type]?.(checker.value));
  821. }
  822.  
  823. /**
  824. * 注册新checker
  825. * 如果给定type已经被其他checker占用,则会报错RedeclarationError
  826. * @param {string} type - checker类名
  827. * @param {function} func - checker implementation
  828. * @returns {undefined}
  829. */
  830. function registerChecker(type, func) {
  831. if (registered_checkers.hasOwnProperty(type)) {
  832. throw RedeclarationError(`Attempts to register checker with type already in use: ${type}`);
  833. }
  834. registered_checkers[type] = func;
  835. }
  836.  
  837. return [{
  838. FuncPool,
  839. testCheckers,
  840. registerChecker,
  841. get checkers() {
  842. return Object.assign({}, registered_checkers);
  843. },
  844. Error: {
  845. RedeclarationError,
  846. CircularDependencyError
  847. }
  848. }, loadFuncs, require, isLoaded];
  849. }) ();
  850.  
  851. return [
  852. // Console & Debug
  853. LogLevel, DoLog, Err, Assert,
  854.  
  855. // DOM
  856. $, $All, $CrE, $AEL, $$CrE, addStyle, detectDom, destroyEvent,
  857.  
  858. // Data
  859. copyProp, copyProps, parseArgs, escJsStr, replaceText,
  860.  
  861. // Environment & Browser
  862. getUrlArgv, dl_browser, dl_GM,
  863.  
  864. // Logic & Task
  865. AsyncManager, queueTask, FunctionLoader, loadFuncs, require
  866. ];
  867. })();