常用函数(用户脚本)

自用函数

目前为 2025-01-24 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/456034/1526016/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.2
  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. const FunctionLoader = {
  838. FuncPool,
  839. testCheckers,
  840. registerChecker,
  841. get checkers() {
  842. return Object.assign({}, registered_checkers);
  843. },
  844. Error: {
  845. RedeclarationError,
  846. CircularDependencyError
  847. }
  848. };
  849. return [FunctionLoader, loadFuncs, require, isLoaded];
  850. }) ();
  851.  
  852. return [
  853. // Console & Debug
  854. LogLevel, DoLog, Err, Assert,
  855.  
  856. // DOM
  857. $, $All, $CrE, $AEL, $$CrE, addStyle, detectDom, destroyEvent,
  858.  
  859. // Data
  860. copyProp, copyProps, parseArgs, escJsStr, replaceText,
  861.  
  862. // Environment & Browser
  863. getUrlArgv, dl_browser, dl_GM,
  864.  
  865. // Logic & Task
  866. AsyncManager, queueTask, FunctionLoader, loadFuncs, require, isLoaded
  867. ];
  868. })();