ItemSelector

An gui for users to select items from given standardized json

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greatest.deepsurf.us/scripts/458132/1138364/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.4
  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-background.itemselector-hide {display: 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-corner {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. // cleanings
  231. [...list.children].forEach(c => c.remove());
  232. [...bglist.children].forEach(c => c.remove());
  233.  
  234. // make new <ul>
  235. const ul = makeListItem(json);
  236. ul.classList.add('itemselector-item-root');
  237. list.appendChild(ul);
  238.  
  239. // configure with options
  240. options.hasOwnProperty('title') && (title.innerText = options.title);
  241.  
  242. // display container
  243. updateElementSelect();
  244. container.classList.add('itemselector-show');
  245.  
  246. return IS;
  247.  
  248. function makeListItem(json_item, path=[]) {
  249. const item = pathItem(path);
  250. const hasChild = Array.isArray(item.children);
  251.  
  252. // create new div
  253. const div = item.elements.div = $CrE('div');
  254. const self_container = item.elements.self_container = $CrE('div');
  255. const child_container = item.elements.child_container = $CrE('div');
  256. const background = item.elements.background = $CrE('div');
  257. div.classList.add('itemselector-item');
  258. self_container.classList.add('itemselector-item-self');
  259. child_container.classList.add('itemselector-item-child');
  260. background.classList.add('itemselector-item-background');
  261. hasChild && div.classList.add('itemselector-item-parent');
  262. $AEL(background, 'mouseenter', e => background.classList.add('itemselector-item-hover'));
  263. $AEL(background, 'mouseleave', e => background.classList.remove('itemselector-item-hover'));
  264. $AEL(self_container, 'mouseenter', e => background.classList.add('itemselector-item-hover'));
  265. $AEL(self_container, 'mouseleave', e => background.classList.remove('itemselector-item-hover'));
  266. bglist.appendChild(background);
  267. div.appendChild(self_container);
  268. div.appendChild(child_container);
  269.  
  270. // triangle toggle for folder items
  271. const toggle = item.elements.toggle = $CrE('a');
  272. toggle.classList.add('itemselector-toggle');
  273. hasChild && toggle.classList.add('itemselector-show');
  274. $AEL(toggle, 'click', e => {
  275. destroyEvent(e);
  276. const collapsed = [...div.classList].includes('itemselector-item-collapsed');
  277. div.classList[collapsed ? 'remove' : 'add']('itemselector-item-collapsed');
  278. toggleBackground(item);
  279.  
  280. function toggleBackground(item) {
  281. if (Array.isArray(item.children)) {
  282. for (const child of item.children) {
  283. child.elements.background.classList[collapsed ? 'remove' : 'add']('itemselector-hide');
  284. toggleBackground(child);
  285. }
  286. }
  287. }
  288. });
  289. self_container.appendChild(toggle);
  290.  
  291. // checkbox for selecting
  292. const checkbox = item.elements.checkbox = $CrE('input');
  293. checkbox.type = 'checkbox';
  294. checkbox.classList.add('itemselector-checker');
  295. $AEL(checkbox, 'change', checkbox_onChange);
  296. self_container.appendChild(checkbox);
  297.  
  298. // check checkbox when self_container or background block onclick
  299. const clickTargets = [self_container, background]
  300. clickTargets.forEach(elm => $AEL(elm, 'click', function(e) {
  301. if (clickTargets.includes(e.target)) {
  302. checkbox.checked = !checkbox.checked;
  303. checkbox_onChange();
  304. }
  305. }));
  306.  
  307. // item text
  308. const text = item.elements.text = $CrE('span');
  309. text.classList.add('itemselector-text');
  310. text.innerText = json_item.text;
  311. self_container.appendChild(text);
  312.  
  313. // make child items
  314. if (hasChild) {
  315. item.elements.children = [];
  316. for (let i = 0; i < json_item.children.length; i++) {
  317. const childItem = makeListItem(json_item.children[i], [...path, i]);
  318. item.elements.children.push(childItem);
  319. child_container.appendChild(childItem);
  320. }
  321. }
  322.  
  323. return div;
  324.  
  325. function checkbox_onChange(e) {
  326. // set select status
  327. item.selected = checkbox.checked;
  328.  
  329. // update element
  330. updateElementSelect();
  331. }
  332. }
  333. }
  334.  
  335. function close() {
  336. if (!DATA.showing) {
  337. DoLog(LogLevel.Error, 'show invoked while DATA.showing === false');
  338. return false;
  339. }
  340. DATA.showing = false;
  341. DATA.options = null;
  342.  
  343. elements.container.classList.remove('itemselector-show');
  344. }
  345.  
  346. function setTheme(theme='light') {
  347. const THEMES = ['light', 'dark'];
  348. const root = elements.container;
  349. if (THEMES.includes(theme)) {
  350. THEMES.filter(t => t !== theme).forEach(t => root.classList.remove(t));
  351. root.classList.add(theme);
  352. return true;
  353. } else {
  354. return false;
  355. }
  356. }
  357.  
  358. function updateElementSelect() {
  359. //const data = DATA.data;
  360. update(DATA.data);
  361.  
  362. function update(item) {
  363. // item elements
  364. const elements = item.elements;
  365. const checkbox = elements.checkbox;
  366.  
  367. // props
  368. checkbox.checked = item.selected;
  369. checkbox.indeterminate = item.childSelected && !item.selected;
  370.  
  371. // update children
  372. if (Array.isArray(item.children)) {
  373. for (const child of item.children) {
  374. update(child);
  375. }
  376. }
  377. }
  378. }
  379.  
  380. function getSelectedItems() {
  381. const json = structuredClone(DATA.json);
  382. const data = DATA.data;
  383. const MARK = Symbol('cut-mark');
  384.  
  385. mark(json, data);
  386. return cut(json);
  387.  
  388. function mark(json_item, data_item) {
  389. if (!data_item.selected && !data_item.childSelected) {
  390. json_item[MARK] = true;
  391. } else if (Array.isArray(data_item.children)) {
  392. for (let i = 0; i < data_item.children.length; i++) {
  393. mark(json_item.children[i], data_item.children[i]);
  394. }
  395. }
  396. }
  397.  
  398. function cut(json_item) {
  399. if (json_item[MARK]) {
  400. return null;
  401. } else {
  402. const children = json_item.children;
  403. if (Array.isArray(children)) {
  404. for (const cutchild of children.filter(child => child[MARK])) {
  405. children.splice(children.indexOf(cutchild), 1);
  406. }
  407. children.forEach((child, i) => {
  408. children[i] = cut(child);
  409. });
  410. }
  411. return json_item;
  412. }
  413. }
  414. }
  415.  
  416. function pathItem(path) {
  417. return pathObj(DATA.data, path);
  418. }
  419.  
  420. function pathObj(obj, path) {
  421. let target = obj;
  422. const _path = [...path];
  423. while (_path.length) {
  424. target = target.children[_path.shift()];
  425. }
  426. return target;
  427. }
  428.  
  429. function makeData(json) {
  430. return proxyItemData(makeItemData(json));
  431.  
  432. function proxyItemData(data) {
  433. return typeof data === 'object' && data !== null ? new Proxy(data, {
  434. get: function(target, property, receiver) {
  435. const value = target[property];
  436. const noproxy = typeof value === 'object' && value !== null && value['__NOPROXY__'] === true;
  437. return noproxy ? value : proxyItemData(value);
  438. },
  439. set: function(target, property, value, receiver) {
  440. switch (property) {
  441. case 'selected':
  442. // set item and its children's selected status by rule
  443. select(target, value, !value);
  444. break;
  445. default:
  446. // setting other props are not allowed
  447. break;
  448. }
  449. return true;
  450.  
  451. function select(item, selected) {
  452. // write item
  453. item.selected = selected;
  454.  
  455. // write children selected
  456. select_children(item)
  457.  
  458. // write parent selected
  459. select_parent(item);
  460.  
  461. // calculate children childSelected
  462. childSelected_children(item);
  463.  
  464. // calculate parent childSelected
  465. childSelected_parent(item);
  466.  
  467. function select_children(item) {
  468. if (Array.isArray(item.children)) {
  469. for (const child of item.children) {
  470. if (child.selected !== selected) {
  471. child.selected = selected;
  472. select_children(child, selected);
  473. }
  474. }
  475. }
  476. }
  477.  
  478. function select_parent(item) {
  479. if (item.parent) {
  480. const parent = item.parent;
  481. const selected = parent.children.every(child => child.selected);
  482. if (parent.selected !== selected) {
  483. parent.selected = selected;
  484. select_parent(parent);
  485. }
  486. }
  487. }
  488.  
  489. function childSelected_children(item) {
  490. if (Array.isArray(item.children)) {
  491. for (const child of item.children) {
  492. childSelected_children(child);
  493. }
  494. item.childSelected = item.children.some(child => child.selected || child.childSelected);
  495. } else {
  496. item.childSelected = false;
  497. }
  498. }
  499.  
  500. function childSelected_parent(item) {
  501. if (item.parent) {
  502. const parent = item.parent;
  503. const childSelected = parent.children.some(child => child.selected || child.childSelected);
  504. if (parent.childSelected !== childSelected) {
  505. parent.childSelected = childSelected;
  506. childSelected_parent(parent);
  507. }
  508. }
  509. }
  510. }
  511. }
  512. }) : data;
  513. }
  514.  
  515. function makeItemData(json, parent=null) {
  516. const hasChild = Array.isArray(json.children);
  517. const item = {};
  518. item.elements = {__NOPROXY__:true};
  519. item.selected = true;
  520. item.childSelected = hasChild && json.children.length > 0;
  521. item.parent = parent !== null && typeof parent === 'object' ? parent : null;
  522. if (hasChild) {
  523. item.children = json.children.map(child => makeItemData(child, item));
  524. }
  525. return item;
  526. }
  527. }
  528.  
  529. function defineGetter(obj, prop, getter) {
  530. Object.defineProperty(obj, prop, {
  531. get: getter,
  532. set: v => true,
  533. configurable: false,
  534. enumerable: true,
  535. });
  536. }
  537. }
  538.  
  539. // Basic functions
  540. // querySelector
  541. function $() {
  542. switch(arguments.length) {
  543. case 2:
  544. return arguments[0].querySelector(arguments[1]);
  545. break;
  546. default:
  547. return document.querySelector(arguments[0]);
  548. }
  549. }
  550. // querySelectorAll
  551. function $All() {
  552. switch(arguments.length) {
  553. case 2:
  554. return arguments[0].querySelectorAll(arguments[1]);
  555. break;
  556. default:
  557. return document.querySelectorAll(arguments[0]);
  558. }
  559. }
  560. // createElement
  561. function $CrE() {
  562. switch(arguments.length) {
  563. case 2:
  564. return arguments[0].createElement(arguments[1]);
  565. break;
  566. default:
  567. return document.createElement(arguments[0]);
  568. }
  569. }
  570. // addEventListener
  571. function $AEL(...args) {
  572. const target = args.shift();
  573. return target.addEventListener.apply(target, args);
  574. }
  575.  
  576. // Just stopPropagation and preventDefault
  577. function destroyEvent(e) {
  578. if (!e) {return false;};
  579. if (!e instanceof Event) {return false;};
  580. e.stopPropagation();
  581. e.preventDefault();
  582. }
  583.  
  584. function parseArgs(args, rules, defaultValues=[]) {
  585. // args and rules should be array, but not just iterable (string is also iterable)
  586. if (!Array.isArray(args) || !Array.isArray(rules)) {
  587. throw new TypeError('parseArgs: args and rules should be array')
  588. }
  589.  
  590. // fill rules[0]
  591. (!Array.isArray(rules[0]) || rules[0].length === 1) && rules.splice(0, 0, []);
  592.  
  593. // max arguments length
  594. const count = rules.length - 1;
  595.  
  596. // args.length must <= count
  597. if (args.length > count) {
  598. throw new TypeError(`parseArgs: args has more elements(${args.length}) longer than ruless'(${count})`);
  599. }
  600.  
  601. // rules[i].length should be === i if rules[i] is an array, otherwise it should be a function
  602. for (let i = 1; i <= count; i++) {
  603. const rule = rules[i];
  604. if (Array.isArray(rule)) {
  605. if (rule.length !== i) {
  606. throw new TypeError(`parseArgs: rules[${i}](${rule}) should have ${i} numbers, but given ${rules[i].length}`);
  607. }
  608. if (!rule.every((num) => (typeof num === 'number' && num <= count))) {
  609. throw new TypeError(`parseArgs: rules[${i}](${rule}) should contain numbers smaller than count(${count}) only`);
  610. }
  611. } else if (typeof rule !== 'function') {
  612. throw new TypeError(`parseArgs: rules[${i}](${rule}) should be an array or a function.`)
  613. }
  614. }
  615.  
  616. // Parse
  617. const rule = rules[args.length];
  618. let parsed;
  619. if (Array.isArray(rule)) {
  620. parsed = [...defaultValues];
  621. for (let i = 0; i < rule.length; i++) {
  622. parsed[rule[i]-1] = args[i];
  623. }
  624. } else {
  625. parsed = rule(args, defaultValues);
  626. }
  627. return parsed;
  628. }
  629.  
  630. function MakeReadonlyObj(val) {
  631. return isObject(val) ? new Proxy(val, {
  632. get: function(target, property, receiver) {
  633. return MakeReadonlyObj(target[property]);
  634. },
  635. set: function(target, property, value, receiver) {
  636. return true;
  637. }
  638. }) : val;
  639.  
  640. function isObject(value) {
  641. return ['object', 'function'].includes(typeof value) && value !== null;
  642. }
  643. }
  644.  
  645. // Returns a random string
  646. function randstr(length=16, nums=true, cases=true) {
  647. const all = 'abcdefghijklmnopqrstuvwxyz' + (nums ? '0123456789' : '') + (cases ? 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' : '');
  648. return Array(length).fill(0).reduce(pre => (pre += all.charAt(randint(0, all.length-1))), '');
  649. }
  650.  
  651. function randint(min, max) {
  652. return Math.floor(Math.random() * (max - min + 1)) + min;
  653. }
  654. }) ();