ItemSelector

An gui for users to select items from given standardized json

目前為 2023-01-14 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/458132/1138353/ItemSelector.js

  1. /* eslint-disable no-multi-spaces */
  2. /* eslint-disable dot-notation */
  3.  
  4. // ==UserScript==
  5. // @name ItemSelector
  6. // @namespace ItemSelector
  7. // @version 0.3.2
  8. // @description An gui for users to select items from given standardized json
  9. // @author PY-DNG
  10. // @license GPL-v3
  11. // ==/UserScript==
  12.  
  13. /* global structuredClone */
  14. let ItemSelector = (function() {
  15. // function DoLog() {}
  16. // Arguments: level=LogLevel.Info, logContent, trace=false
  17. const [LogLevel, DoLog] = (function() {
  18. const LogLevel = {
  19. None: 0,
  20. Error: 1,
  21. Success: 2,
  22. Warning: 3,
  23. Info: 4,
  24. };
  25.  
  26. return [LogLevel, DoLog];
  27. function DoLog() {
  28. // Get window
  29. const win = (typeof(unsafeWindow) === 'object' && unsafeWindow !== null) ? unsafeWindow : window;
  30.  
  31. const LogLevelMap = {};
  32. LogLevelMap[LogLevel.None] = {
  33. prefix: '',
  34. color: 'color:#ffffff'
  35. }
  36. LogLevelMap[LogLevel.Error] = {
  37. prefix: '[Error]',
  38. color: 'color:#ff0000'
  39. }
  40. LogLevelMap[LogLevel.Success] = {
  41. prefix: '[Success]',
  42. color: 'color:#00aa00'
  43. }
  44. LogLevelMap[LogLevel.Warning] = {
  45. prefix: '[Warning]',
  46. color: 'color:#ffa500'
  47. }
  48. LogLevelMap[LogLevel.Info] = {
  49. prefix: '[Info]',
  50. color: 'color:#888888'
  51. }
  52. LogLevelMap[LogLevel.Elements] = {
  53. prefix: '[Elements]',
  54. color: 'color:#000000'
  55. }
  56.  
  57. // Current log level
  58. DoLog.logLevel = (win.isPY_DNG && win.userscriptDebugging) ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error
  59.  
  60. // Log counter
  61. DoLog.logCount === undefined && (DoLog.logCount = 0);
  62.  
  63. // Get args
  64. let [level, logContent, trace] = parseArgs([...arguments], [
  65. [2],
  66. [1,2],
  67. [1,2,3]
  68. ], [LogLevel.Info, 'DoLog initialized.', false]);
  69.  
  70. // Log when log level permits
  71. if (level <= DoLog.logLevel) {
  72. let msg = '%c' + LogLevelMap[level].prefix + (typeof GM_info === 'object' ? `[${GM_info.script.name}]` : '') + (LogLevelMap[level].prefix ? ' ' : '');
  73. let subst = LogLevelMap[level].color;
  74.  
  75. switch (typeof(logContent)) {
  76. case 'string':
  77. msg += '%s';
  78. break;
  79. case 'number':
  80. msg += '%d';
  81. break;
  82. default:
  83. msg += '%o';
  84. break;
  85. }
  86.  
  87. if (++DoLog.logCount > 512) {
  88. console.clear();
  89. DoLog.logCount = 0;
  90. }
  91. console[trace ? 'trace' : 'log'](msg, subst, logContent);
  92. }
  93. }
  94. }) ();
  95.  
  96. return ItemSelector;
  97.  
  98. function ItemSelector(useWrapper=true) {
  99. const IS = this;
  100. const DATA = {
  101. showing: false, json: null, data: null, options: null
  102. };
  103. const elements = IS.elements = {};
  104. defineGetter(IS, 'showing', () => DATA.showing);
  105. defineGetter(IS, 'json', () => MakeReadonlyObj(DATA.json));
  106. defineGetter(IS, 'data', () => MakeReadonlyObj(DATA.data));
  107. defineGetter(IS, 'options', () => MakeReadonlyObj(DATA.options));
  108. IS.show = show;
  109. IS.close = close;
  110. IS.setTheme = setTheme;
  111. IS.getSelectedItems = getSelectedItems;
  112. init();
  113.  
  114. function init() {
  115. const wrapperDoc = elements.wrapperDoc = useWrapper ? (function() {
  116. const wrapper = elements.wrapper = $CrE(randstr(4, false, false) + '-' + randstr(4, false, false));
  117. const shadow = wrapper.attachShadow({mode: 'closed'});
  118. wrapper.style.display = 'block';
  119. wrapper.style.zIndex = 99999999;
  120. document.body.appendChild(wrapper);
  121. return shadow;
  122. }) () : document;
  123. const wrapper = elements.wrapper = useWrapper ? wrapperDoc : wrapperDoc.body;
  124. const container = elements.container = $CrE('div');
  125. const header = elements.header = $CrE('div');
  126. const body = elements.body = $CrE('div');
  127. const footer = elements.footer = $CrE('div');
  128. container.classList.add('itemselector-container');
  129. header.classList.add('itemselector-header');
  130. body.classList.add('itemselector-body');
  131. footer.classList.add('itemselector-footer');
  132. container.appendChild(header);
  133. container.appendChild(body);
  134. container.appendChild(footer);
  135. wrapper.appendChild(container);
  136.  
  137. const title = elements.title = $CrE('span');
  138. title.classList.add('itemselector-title');
  139. header.appendChild(title);
  140.  
  141. const bglist = elements.bglist = $CrE('div');
  142. bglist.classList.add('itemselector-bglist');
  143. body.appendChild(bglist);
  144.  
  145. const list = elements.list = $CrE('pre');
  146. list.classList.add('itemselector-list');
  147. body.appendChild(list);
  148.  
  149. const btnOK = $CrE('button');
  150. const btnCancel = $CrE('button');
  151. const btnClose = $CrE('button');
  152. btnOK.innerText = 'OK';
  153. btnCancel.innerText = 'Cancel';
  154. btnClose.innerText = 'x';
  155. btnOK.className = 'itemselector-button itemselector-button-ok';
  156. btnCancel.className = 'itemselector-button itemselector-button-cancel';
  157. btnClose.className = 'itemselector-button itemselector-button-close';
  158. $AEL(btnOK, 'click', ok_onClick);
  159. $AEL(btnCancel, 'click', cancel_onClick);
  160. $AEL(btnClose, 'click', close_onClick);
  161. header.appendChild(btnClose);
  162. footer.appendChild(btnCancel);
  163. footer.appendChild(btnOK);
  164. elements.button = {btnOK, btnCancel, btnClose};
  165.  
  166. const cssParent = useWrapper ? wrapper : document.head;
  167. const css = '.itemselector-container {display: none;position: fixed;position: fixed;width: 60vw;height: 60vh;left: 20vw;top: 20vh;border-radius: 1em;padding: 2em;user-select: none;font-family: -apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Helvetica Neue,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol!important;}.itemselector-container.itemselector-show {display: block;}.itemselector-header {position: absolute;width: calc(100% - 4em);padding-bottom: 0.3em;}.itemselector-title {position: relative;font-size: 1.3em;}.itemselector-body {position: absolute;top: calc(2em + 20px * 1.3 + 20px * 0.3 + 1px + 0.3em);bottom: calc(2em + 20px + 20px + calc(60vw - 4em) * 2 / 100 + 0.3em);overflow: auto;width: calc(100% - 4em);z-index: -2;}.itemselector-bglist {position: absolute;left: 0;width: 100%;z-index: -1;}.itemselector-footer {position: absolute;bottom: 2em;width: calc(100% - 4em);}.itemselector-button {font-size: 20px;width: 48%;margin: 1%;border: none;border-radius: 3px;padding: 0.5em;font-weight: 500;}.itemselector-button.itemselector-button-close {position: relative;float: right;margin: 0;padding: 0;width: 1.3em;height: 1.3em;text-align: center;font-size: 20px;}.itemselector-list {margin: 0;pointer-events: none;}.itemselector-item {margin: 0;margin-left: 1em;}.itemselector-item-root {margin-left: 0;}.itemselector-item-background {width: 100%;height: 49px;}.itemselector-item-background:first-child {border-top: none;}.itemselector-item-self {font-size: 14px;line-height: 34px;padding: 8px;background-color: rgba(0,0,0,0);pointer-events: auto;}.itemselector-toggle {position: relative;visibility: hidden;}.itemselector-toggle.itemselector-show {visibility: visible;}.itemselector-toggle:before {content: "\\25BC";width: 1em;display: inline-block;position: relative;}.itemselector-item-collapsed>.itemselector-item-self>.itemselector-toggle:before {content: "\\25B6";}.itemselector-item-collapsed>.itemselector-item-child>.itemselector-item {display: none;}.itemselector-text {pointer-events: none;margin-left: 0.5em;}.itemselector-container.light {--itemselector-color: #000;--itemselector-bgcolor-1: #dddddd;--itemselector-bgcolor-0: #e2e2e2;--itemselector-bgcolor-2: #cdcdcd;--itemselector-bgcolor-3: #bdbdbd;--itemselector-btnclose-bgcolor: #00bcd4;--itemselector-spliter-color: rgba(0,0,0,0.28);}.itemselector-container.dark {--itemselector-color: #fff;--itemselector-bgcolor-0: #1d1d1d;--itemselector-bgcolor-1: #222222;--itemselector-bgcolor-2: #323232;--itemselector-bgcolor-3: #424242;--itemselector-btnclose-bgcolor: #00bcd4;--itemselector-spliter-color: rgba(255,255,255,0.28);}.itemselector-container {box-shadow: 0 3px 15px rgb(0 0 0 / 20%), 0 6px 6px rgb(0 0 0 / 14%), 0 9px 3px -6px rgb(0 0 0 / 12%);color: var(--itemselector-color);background-color: var(--itemselector-bgcolor-0);}.itemselector-header {border-bottom: 1px solid var(--itemselector-spliter-color);}.itemselector-body {scrollbar-color: var(--itemselector-bgcolor-2) var(--itemselector-bgcolor-1);}.itemselector-body:hover {scrollbar-color: var(--itemselector-bgcolor-3) var(--itemselector-bgcolor-1);}.itemselector-body::-webkit-scrollbar {background-color: var(--itemselector-bgcolor-1);}.itemselector-body::-webkit-scrollbar-thumb, .itemselector-body::-webkit-scrollbar-button {background-color: var(--itemselector-bgcolor-2);}.itemselector-body::-webkit-scrollbar-thumb:hover, .itemselector-body::-webkit-scrollbar-button:hover {background-color: var(--itemselector-bgcolor-3);}.itemselector-item-background {transition-duration: 0.3s;border-top: 1px solid var(--itemselector-spliter-color);}.itemselector-item-background.itemselector-item-hover {background-color: var(--itemselector-bgcolor-2);}.itemselector-button {background-color: var(--itemselector-btnclose-bgcolor);color: var(--itemselector-color);}.itemselector-button.itemselector-button-close {background-color: var(--itemselector-bgcolor-2);}.itemselector-button.itemselector-button-close:hover {background-color: var(--itemselector-bgcolor-3);}';
  168. const style = $CrE('style');
  169. style.innerHTML = css;
  170. cssParent.appendChild(style);
  171.  
  172. function ok_onClick(e) {
  173. if (!DATA.showing) {
  174. DoLog(LogLevel.Warning, 'ok_onClick invoked when dialog is not showing');
  175. return false;
  176. }
  177. if (!DATA.options) {
  178. DoLog(LogLevel.Warning, 'DATA.options missing while ok_onClick invoked');
  179. return false;
  180. }
  181. typeof DATA.options.onok === 'function' && DATA.options.onok.call(this, e, getSelectedItems());
  182. close();
  183. }
  184.  
  185. function cancel_onClick(e) {
  186. if (!DATA.showing) {
  187. DoLog(LogLevel.Warning, 'cancel_onClick invoked when dialog is not showing');
  188. return false;
  189. }
  190. if (!DATA.options) {
  191. DoLog(LogLevel.Warning, 'DATA.options missing while cancel_onClick invoked');
  192. return false;
  193. }
  194. typeof DATA.options.oncancel === 'function' && DATA.options.oncancel.call(this, e, getSelectedItems());
  195. close();
  196. }
  197.  
  198. function close_onClick(e) {
  199. if (!DATA.showing) {
  200. DoLog(LogLevel.Warning, 'close_onClick invoked when dialog is not showing');
  201. return false;
  202. }
  203. if (!DATA.options) {
  204. DoLog(LogLevel.Warning, 'DATA.options missing while close_onClick invoked');
  205. return false;
  206. }
  207. typeof DATA.options.onclose === 'function' && DATA.options.onclose.call(this, e, getSelectedItems());
  208. close();
  209. }
  210. }
  211.  
  212. function show(json, options={title: ''}) {
  213. // Status check & update
  214. if (!json) {
  215. DoLog(LogLevel.Error, 'json missing');
  216. return false;
  217. }
  218. if (DATA.showing) {
  219. DoLog(LogLevel.Error, 'show invoked while DATA.showing === true');
  220. return false;
  221. }
  222. DATA.showing = true;
  223. DATA.options = options;
  224. DATA.json = structuredClone(json);
  225. DATA.data = makeData(json);
  226.  
  227. // elements
  228. const {container, header, title, body, footer, bglist, list} = elements;
  229.  
  230. // make new <ul>
  231. const ul = makeListItem(json);
  232. ul.classList.add('itemselector-item-root');
  233. [...list.children].forEach(c => c.remove());
  234. [...bglist.children].forEach(c => c.remove());
  235. list.appendChild(ul);
  236.  
  237. // configure with options
  238. options.hasOwnProperty('title') && (title.innerText = options.title);
  239.  
  240. // display container
  241. updateElementSelect();
  242. container.classList.add('itemselector-show');
  243.  
  244. return IS;
  245.  
  246. function makeListItem(json_item, path=[]) {
  247. const item = pathItem(path);
  248. const hasChild = Array.isArray(item.children);
  249.  
  250. // create new div
  251. const div = item.elements.div = $CrE('div');
  252. const self_container = item.elements.self_container = $CrE('div');
  253. const child_container = item.elements.child_container = $CrE('div');
  254. const background = item.elements.background = $CrE('div');
  255. div.classList.add('itemselector-item');
  256. self_container.classList.add('itemselector-item-self');
  257. child_container.classList.add('itemselector-item-child');
  258. background.classList.add('itemselector-item-background');
  259. hasChild && div.classList.add('itemselector-item-parent');
  260. $AEL(background, 'mouseenter', e => background.classList.add('itemselector-item-hover'));
  261. $AEL(background, 'mouseleave', e => background.classList.remove('itemselector-item-hover'));
  262. $AEL(self_container, 'mouseenter', e => background.classList.add('itemselector-item-hover'));
  263. $AEL(self_container, 'mouseleave', e => background.classList.remove('itemselector-item-hover'));
  264. bglist.appendChild(background);
  265. div.appendChild(self_container);
  266. div.appendChild(child_container);
  267.  
  268. // triangle toggle for folder items
  269. const toggle = item.elements.toggle = $CrE('a');
  270. toggle.classList.add('itemselector-toggle');
  271. hasChild && toggle.classList.add('itemselector-show');
  272. $AEL(toggle, 'click', e => {
  273. destroyEvent(e);
  274. const collapsed = [...div.classList].includes('itemselector-item-collapsed');
  275. div.classList[collapsed ? 'remove' : 'add']('itemselector-item-collapsed');
  276. toggleBackground(item);
  277.  
  278. function toggleBackground(item) {
  279. if (Array.isArray(item.children)) {
  280. for (const child of item.children) {
  281. child.elements.background.classList[collapsed ? 'remove' : 'add']('itemselector-hide');
  282. toggleBackground(child);
  283. }
  284. }
  285. }
  286. });
  287. self_container.appendChild(toggle);
  288.  
  289. // checkbox for selecting
  290. const checkbox = item.elements.checkbox = $CrE('input');
  291. checkbox.type = 'checkbox';
  292. checkbox.classList.add('itemselector-checker');
  293. $AEL(checkbox, 'change', checkbox_onChange);
  294. self_container.appendChild(checkbox);
  295.  
  296. // check checkbox when self_container or background block onclick
  297. const clickTargets = [self_container, background]
  298. clickTargets.forEach(elm => $AEL(elm, 'click', function(e) {
  299. if (clickTargets.includes(e.target)) {
  300. checkbox.checked = !checkbox.checked;
  301. checkbox_onChange();
  302. }
  303. }));
  304.  
  305. // item text
  306. const text = item.elements.text = $CrE('span');
  307. text.classList.add('itemselector-text');
  308. text.innerText = json_item.text;
  309. self_container.appendChild(text);
  310.  
  311. // make child items
  312. if (hasChild) {
  313. item.elements.children = [];
  314. for (let i = 0; i < json_item.children.length; i++) {
  315. const childItem = makeListItem(json_item.children[i], [...path, i]);
  316. item.elements.children.push(childItem);
  317. child_container.appendChild(childItem);
  318. }
  319. }
  320.  
  321. return div;
  322.  
  323. function checkbox_onChange(e) {
  324. // set select status
  325. item.selected = checkbox.checked;
  326.  
  327. // update element
  328. updateElementSelect();
  329. }
  330. }
  331. }
  332.  
  333. function close() {
  334. if (!DATA.showing) {
  335. DoLog(LogLevel.Error, 'show invoked while DATA.showing === false');
  336. return false;
  337. }
  338. DATA.showing = false;
  339. DATA.options = null;
  340.  
  341. elements.container.classList.remove('itemselector-show');
  342. }
  343.  
  344. function setTheme(theme='light') {
  345. const THEMES = ['light', 'dark'];
  346. const root = elements.container;
  347. if (THEMES.includes(theme)) {
  348. THEMES.filter(t => t !== theme).forEach(t => root.classList.remove(t));
  349. root.classList.add(theme);
  350. return true;
  351. } else {
  352. return false;
  353. }
  354. }
  355.  
  356. function updateElementSelect() {
  357. //const data = DATA.data;
  358. update(DATA.data);
  359.  
  360. function update(item) {
  361. // item elements
  362. const elements = item.elements;
  363. const checkbox = elements.checkbox;
  364.  
  365. // props
  366. checkbox.checked = item.selected;
  367. checkbox.indeterminate = item.childSelected && !item.selected;
  368.  
  369. // update children
  370. if (Array.isArray(item.children)) {
  371. for (const child of item.children) {
  372. update(child);
  373. }
  374. }
  375. }
  376. }
  377.  
  378. function getSelectedItems() {
  379. const json = structuredClone(DATA.json);
  380. const data = DATA.data;
  381. const MARK = Symbol('cut-mark');
  382.  
  383. mark(json, data);
  384. return cut(json);
  385.  
  386. function mark(json_item, data_item) {
  387. if (!data_item.selected && !data_item.childSelected) {
  388. json_item[MARK] = true;
  389. } else if (Array.isArray(data_item.children)) {
  390. for (let i = 0; i < data_item.children.length; i++) {
  391. mark(json_item.children[i], data_item.children[i]);
  392. }
  393. }
  394. }
  395.  
  396. function cut(json_item) {
  397. if (json_item[MARK]) {
  398. return null;
  399. } else {
  400. const children = json_item.children;
  401. if (Array.isArray(children)) {
  402. for (const cutchild of children.filter(child => child[MARK])) {
  403. children.splice(children.indexOf(cutchild), 1);
  404. }
  405. children.forEach((child, i) => {
  406. children[i] = cut(child);
  407. });
  408. }
  409. return json_item;
  410. }
  411. }
  412. }
  413.  
  414. function pathItem(path) {
  415. return pathObj(DATA.data, path);
  416. }
  417.  
  418. function pathObj(obj, path) {
  419. let target = obj;
  420. const _path = [...path];
  421. while (_path.length) {
  422. target = target.children[_path.shift()];
  423. }
  424. return target;
  425. }
  426.  
  427. function makeData(json) {
  428. return proxyItemData(makeItemData(json));
  429.  
  430. function proxyItemData(data) {
  431. return typeof data === 'object' && data !== null ? new Proxy(data, {
  432. get: function(target, property, receiver) {
  433. const value = target[property];
  434. const noproxy = typeof value === 'object' && value !== null && value['__NOPROXY__'] === true;
  435. return noproxy ? value : proxyItemData(value);
  436. },
  437. set: function(target, property, value, receiver) {
  438. switch (property) {
  439. case 'selected':
  440. // set item and its children's selected status by rule
  441. select(target, value, !value);
  442. break;
  443. default:
  444. // setting other props are not allowed
  445. break;
  446. }
  447. return true;
  448.  
  449. function select(item, selected) {
  450. // write item
  451. item.selected = selected;
  452.  
  453. // write children selected
  454. select_children(item)
  455.  
  456. // write parent selected
  457. select_parent(item);
  458.  
  459. // calculate children childSelected
  460. childSelected_children(item);
  461.  
  462. // calculate parent childSelected
  463. childSelected_parent(item);
  464.  
  465. function select_children(item) {
  466. if (Array.isArray(item.children)) {
  467. for (const child of item.children) {
  468. if (child.selected !== selected) {
  469. child.selected = selected;
  470. select_children(child, selected);
  471. }
  472. }
  473. }
  474. }
  475.  
  476. function select_parent(item) {
  477. if (item.parent) {
  478. const parent = item.parent;
  479. const selected = parent.children.every(child => child.selected);
  480. if (parent.selected !== selected) {
  481. parent.selected = selected;
  482. select_parent(parent);
  483. }
  484. }
  485. }
  486.  
  487. function childSelected_children(item) {
  488. if (Array.isArray(item.children)) {
  489. for (const child of item.children) {
  490. childSelected_children(child);
  491. }
  492. item.childSelected = item.children.some(child => child.selected || child.childSelected);
  493. } else {
  494. item.childSelected = false;
  495. }
  496. }
  497.  
  498. function childSelected_parent(item) {
  499. if (item.parent) {
  500. const parent = item.parent;
  501. const childSelected = parent.children.some(child => child.selected || child.childSelected);
  502. if (parent.childSelected !== childSelected) {
  503. parent.childSelected = childSelected;
  504. childSelected_parent(parent);
  505. }
  506. }
  507. }
  508. }
  509. }
  510. }) : data;
  511. }
  512.  
  513. function makeItemData(json, parent=null) {
  514. const hasChild = Array.isArray(json.children);
  515. const item = {};
  516. item.elements = {__NOPROXY__:true};
  517. item.selected = true;
  518. item.childSelected = hasChild && json.children.length > 0;
  519. item.parent = parent !== null && typeof parent === 'object' ? parent : null;
  520. if (hasChild) {
  521. item.children = json.children.map(child => makeItemData(child, item));
  522. }
  523. return item;
  524. }
  525. }
  526.  
  527. function defineGetter(obj, prop, getter) {
  528. Object.defineProperty(obj, prop, {
  529. get: getter,
  530. set: v => true,
  531. configurable: false,
  532. enumerable: true,
  533. });
  534. }
  535. }
  536.  
  537. // Basic functions
  538. // querySelector
  539. function $() {
  540. switch(arguments.length) {
  541. case 2:
  542. return arguments[0].querySelector(arguments[1]);
  543. break;
  544. default:
  545. return document.querySelector(arguments[0]);
  546. }
  547. }
  548. // querySelectorAll
  549. function $All() {
  550. switch(arguments.length) {
  551. case 2:
  552. return arguments[0].querySelectorAll(arguments[1]);
  553. break;
  554. default:
  555. return document.querySelectorAll(arguments[0]);
  556. }
  557. }
  558. // createElement
  559. function $CrE() {
  560. switch(arguments.length) {
  561. case 2:
  562. return arguments[0].createElement(arguments[1]);
  563. break;
  564. default:
  565. return document.createElement(arguments[0]);
  566. }
  567. }
  568. // addEventListener
  569. function $AEL(...args) {
  570. const target = args.shift();
  571. return target.addEventListener.apply(target, args);
  572. }
  573.  
  574. // Just stopPropagation and preventDefault
  575. function destroyEvent(e) {
  576. if (!e) {return false;};
  577. if (!e instanceof Event) {return false;};
  578. e.stopPropagation();
  579. e.preventDefault();
  580. }
  581.  
  582. function parseArgs(args, rules, defaultValues=[]) {
  583. // args and rules should be array, but not just iterable (string is also iterable)
  584. if (!Array.isArray(args) || !Array.isArray(rules)) {
  585. throw new TypeError('parseArgs: args and rules should be array')
  586. }
  587.  
  588. // fill rules[0]
  589. (!Array.isArray(rules[0]) || rules[0].length === 1) && rules.splice(0, 0, []);
  590.  
  591. // max arguments length
  592. const count = rules.length - 1;
  593.  
  594. // args.length must <= count
  595. if (args.length > count) {
  596. throw new TypeError(`parseArgs: args has more elements(${args.length}) longer than ruless'(${count})`);
  597. }
  598.  
  599. // rules[i].length should be === i if rules[i] is an array, otherwise it should be a function
  600. for (let i = 1; i <= count; i++) {
  601. const rule = rules[i];
  602. if (Array.isArray(rule)) {
  603. if (rule.length !== i) {
  604. throw new TypeError(`parseArgs: rules[${i}](${rule}) should have ${i} numbers, but given ${rules[i].length}`);
  605. }
  606. if (!rule.every((num) => (typeof num === 'number' && num <= count))) {
  607. throw new TypeError(`parseArgs: rules[${i}](${rule}) should contain numbers smaller than count(${count}) only`);
  608. }
  609. } else if (typeof rule !== 'function') {
  610. throw new TypeError(`parseArgs: rules[${i}](${rule}) should be an array or a function.`)
  611. }
  612. }
  613.  
  614. // Parse
  615. const rule = rules[args.length];
  616. let parsed;
  617. if (Array.isArray(rule)) {
  618. parsed = [...defaultValues];
  619. for (let i = 0; i < rule.length; i++) {
  620. parsed[rule[i]-1] = args[i];
  621. }
  622. } else {
  623. parsed = rule(args, defaultValues);
  624. }
  625. return parsed;
  626. }
  627.  
  628. function MakeReadonlyObj(val) {
  629. return isObject(val) ? new Proxy(val, {
  630. get: function(target, property, receiver) {
  631. return MakeReadonlyObj(target[property]);
  632. },
  633. set: function(target, property, value, receiver) {
  634. return true;
  635. }
  636. }) : val;
  637.  
  638. function isObject(value) {
  639. return ['object', 'function'].includes(typeof value) && value !== null;
  640. }
  641. }
  642.  
  643. // Returns a random string
  644. function randstr(length=16, nums=true, cases=true) {
  645. const all = 'abcdefghijklmnopqrstuvwxyz' + (nums ? '0123456789' : '') + (cases ? 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' : '');
  646. return Array(length).fill(0).reduce(pre => (pre += all.charAt(randint(0, all.length-1))), '');
  647. }
  648.  
  649. function randint(min, max) {
  650. return Math.floor(Math.random() * (max - min + 1)) + min;
  651. }
  652. }) ();