SettingPanel

SettingPanel for wenku8++

目前为 2022-08-28 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/450209/1086714/SettingPanel.js

  1. /* eslint-disable no-multi-spaces */
  2. /* eslint-disable no-implicit-globals */
  3. /* eslint-disable userscripts/no-invalid-headers */
  4. /* eslint-disable userscripts/no-invalid-grant */
  5.  
  6. // ==UserScript==
  7. // @name SettingPanel
  8. // @displayname SettingPanel
  9. // @namespace Wenku8++
  10. // @version 0.3.3
  11. // @description SettingPanel for wenku8++
  12. // @author PY-DNG
  13. // @license GPL-v3
  14. // @regurl NONE
  15. // @require https://greasyfork.org/scripts/449412-basic-functions/code/Basic%20Functions.js?version=1085783
  16. // @require https://greasyfork.org/scripts/449583-configmanager/code/ConfigManager.js?version=1085836
  17. // @grant none
  18. // ==/UserScript==
  19.  
  20. /*
  21. 计划任务:
  22. [-] 表格线换成蓝色的
  23. [ ] 允许使用不同的alertify对话框
  24. [ ] 点击按钮的时候要有GUI反馈
  25. [ ] 未保存内容,关闭窗口前要有提示
  26. [ ] 提供注册SettingOptions组件的接口
  27. */
  28.  
  29. (function __MAIN__() {
  30. 'use strict';
  31.  
  32. const ASSETS = require('assets');
  33. const alertify = require('alertify');
  34. const CONST = {
  35. Text: {},
  36. Manager_Config_Ruleset: {
  37. 'version-key': 'config-version',
  38. 'ignores': ["LOCAL-CDN"],
  39. 'defaultValues': {
  40. //'config-key': {},
  41. }
  42. }
  43. };
  44.  
  45. // initialize
  46. alertify.dialog('setpanel', function factory(){
  47. return {
  48. // The dialog startup function
  49. // This will be called each time the dialog is invoked
  50. // For example: alertify.myDialog( data );
  51. main:function(){
  52. // Split arguments
  53. let content, header, buttons, onsave, onreset, onclose;
  54. switch (arguments.length) {
  55. case 1:
  56. switch (typeof arguments[0]) {
  57. case 'string':
  58. content = arguments[0];
  59. break;
  60. case 'object':
  61. arguments[0].hasOwnProperty('content') && (content = arguments[0].content);
  62. arguments[0].hasOwnProperty('header') && (header = arguments[0].header);
  63. arguments[0].hasOwnProperty('onsave') && (content = arguments[0].onsave);
  64. arguments[0].hasOwnProperty('onreset') && (content = arguments[0].onreset);
  65. arguments[0].hasOwnProperty('buttons') && (content = arguments[0].buttons);
  66. break;
  67. default:
  68. Err('Arguments invalid', 1);
  69. }
  70. break;
  71. case 2:
  72. content = arguments[0];
  73. header = arguments[1];
  74. break;
  75. case 3:
  76. content = arguments[0];
  77. header = arguments[1];
  78. buttons = buttons[2];
  79. break;
  80. }
  81.  
  82. // Prepare dialog
  83. this.resizeTo('80%', '80%');
  84. content && this.setContent(content);
  85. header && this.setHeader(header);
  86. onsave && this.set('onsave', onsave);
  87. onreset && this.set('onreset', onreset);
  88. onclose && this.set('onclose', onclose);
  89.  
  90. // Choose & show selected button groups
  91. const btnGroups = {
  92. // Close button only
  93. basic: [[1, 0]],
  94.  
  95. // Save & reset button
  96. saver: [[0, 0], [1, 1]]
  97. };
  98. const group = btnGroups[buttons || 'basic'];
  99. const divs = ['auxiliary', 'primary'];
  100. divs.forEach((div) => {
  101. Array.from(this.elements.buttons[div].children).forEach((btn) => {
  102. btn.style.display = 'none';
  103. });
  104. });
  105. group.forEach((button) => {
  106. this.elements.buttons[divs[button[0]]].children[button[1]].style.display = '';
  107. });
  108.  
  109. return this;
  110. },
  111. // The dialog setup function
  112. // This should return the dialog setup object ( buttons, focus and options overrides ).
  113. setup:function(){
  114. return {
  115. /* buttons collection */
  116. buttons:[{
  117. /* button label */
  118. text: '恢复到修改前',
  119.  
  120. /*bind a keyboard key to the button */
  121. key: undefined,
  122.  
  123. /* indicate if closing the dialog should trigger this button action */
  124. invokeOnClose: false,
  125.  
  126. /* custom button class name */
  127. className: alertify.defaults.theme.cancel,
  128.  
  129. /* custom button attributes */
  130. attrs: {},
  131.  
  132. /* Defines the button scope, either primary (default) or auxiliary */
  133. scope:'auxiliary',
  134.  
  135. /* The will conatin the button DOMElement once buttons are created */
  136. element: undefined
  137. },{
  138. /* button label */
  139. text: '关闭',
  140.  
  141. /*bind a keyboard key to the button */
  142. key: undefined,
  143.  
  144. /* indicate if closing the dialog should trigger this button action */
  145. invokeOnClose: false,
  146.  
  147. /* custom button class name */
  148. className: alertify.defaults.theme.ok,
  149.  
  150. /* custom button attributes */
  151. attrs: {},
  152.  
  153. /* Defines the button scope, either primary (default) or auxiliary */
  154. scope:'primary',
  155.  
  156. /* The will conatin the button DOMElement once buttons are created */
  157. element: undefined
  158. },{
  159. /* button label */
  160. text: '保存',
  161.  
  162. /*bind a keyboard key to the button */
  163. key: undefined,
  164.  
  165. /* indicate if closing the dialog should trigger this button action */
  166. invokeOnClose: false,
  167.  
  168. /* custom button class name */
  169. className: alertify.defaults.theme.ok,
  170.  
  171. /* custom button attributes */
  172. attrs: {},
  173.  
  174. /* Defines the button scope, either primary (default) or auxiliary */
  175. scope:'primary',
  176.  
  177. /* The will conatin the button DOMElement once buttons are created */
  178. element: undefined
  179. }],
  180.  
  181. /* default focus */
  182. focus:{
  183. /* the element to receive default focus, has differnt meaning based on value type:
  184. number: action button index.
  185. string: querySelector to select from dialog body contents.
  186. function: when invoked, should return the focus element.
  187. DOMElement: the focus element.
  188. object: an object that implements .focus() and .select() functions.
  189. */
  190. element: 0,
  191.  
  192. /* indicates if the element should be selected on focus or not*/
  193. select: true
  194.  
  195. },
  196. /* dialog options, these override the defaults */
  197. options: {
  198. title: 'Setting Panel',
  199. modal: true,
  200. basic: false,
  201. frameless: false,
  202. pinned: false,
  203. movable: true,
  204. moveBounded: false,
  205. resizable: true,
  206. autoReset: false,
  207. closable: true,
  208. closableByDimmer: true,
  209. maximizable: false,
  210. startMaximized: false,
  211. pinnable: false,
  212. transition: 'fade',
  213. padding: true,
  214. overflow: true,
  215. /*
  216. onshow:...,
  217. onclose:...,
  218. onfocus:...,
  219. onmove:...,
  220. onmoved:...,
  221. onresize:...,
  222. onresized:...,
  223. onmaximize:...,
  224. onmaximized:...,
  225. onrestore:...,
  226. onrestored:...
  227. */
  228. }
  229. };
  230. },
  231. // This will be called once the dialog DOM has been created, just before its added to the document.
  232. // Its invoked only once.
  233. build:function(){
  234.  
  235. // Do custom DOM manipulation here, accessible via this.elements
  236.  
  237. // this.elements.root ==> Root div
  238. // this.elements.dimmer ==> Modal dimmer div
  239. // this.elements.modal ==> Modal div (dialog wrapper)
  240. // this.elements.dialog ==> Dialog div
  241. // this.elements.reset ==> Array containing the tab reset anchor links
  242. // this.elements.reset[0] ==> First reset element (button).
  243. // this.elements.reset[1] ==> Second reset element (button).
  244. // this.elements.header ==> Dialog header div
  245. // this.elements.body ==> Dialog body div
  246. // this.elements.content ==> Dialog body content div
  247. // this.elements.footer ==> Dialog footer div
  248. // this.elements.resizeHandle ==> Dialog resize handle div
  249.  
  250. // Dialog commands (Pin/Maximize/Close)
  251. // this.elements.commands ==> Object containing dialog command buttons references
  252. // this.elements.commands.container ==> Root commands div
  253. // this.elements.commands.pin ==> Pin command button
  254. // this.elements.commands.maximize ==> Maximize command button
  255. // this.elements.commands.close ==> Close command button
  256.  
  257. // Dialog action buttons (Ok, cancel ... etc)
  258. // this.elements.buttons ==> Object containing dialog action buttons references
  259. // this.elements.buttons.primary ==> Primary buttons div
  260. // this.elements.buttons.auxiliary ==> Auxiliary buttons div
  261.  
  262. // Each created button will be saved with the button definition inside buttons collection
  263. // this.__internal.buttons[x].element
  264.  
  265. },
  266. // This will be called each time the dialog is shown
  267. prepare:function(){
  268. // Do stuff that should be done every time the dialog is shown.
  269. },
  270. // This will be called each time an action button is clicked.
  271. callback:function(closeEvent){
  272. //The closeEvent has the following properties
  273. //
  274. // index: The index of the button triggering the event.
  275. // button: The button definition object.
  276. // cancel: When set true, prevent the dialog from closing.
  277. const myEvent = deepClone(closeEvent);
  278. switch (closeEvent.index) {
  279. case 0: {
  280. // Rests button
  281. closeEvent.cancel = true;
  282. myEvent.save = false;
  283. myEvent.reset = true;
  284. const onreset = this.get('onreset');
  285. typeof onreset === 'function' && onreset(myEvent);
  286. break;
  287. }
  288. case 1: {
  289. // Close button
  290. // Do something here if need
  291. break;
  292. }
  293. case 2: {
  294. // Save button
  295. closeEvent.cancel = true;
  296. myEvent.save = true;
  297. myEvent.reset = false;
  298. const onsave = this.get('onsave');
  299. typeof onsave === 'function' && onsave(myEvent);
  300. }
  301. }
  302. this.get(myEvent.save ? 'saver' : 'reseter').call(this);
  303. closeEvent.cancel = myEvent.cancel;
  304. },
  305. // To make use of AlertifyJS settings API, group your custom settings into a settings object.
  306. settings:{
  307. onsave: function() {},
  308. onreset: function() {},
  309. options: [], // SettingOption array
  310. saver: function() {
  311. this.get('options').forEach((o) => (o.save()));
  312. },
  313. reseter: function() {
  314. this.get('options').forEach((o) => (o.reset()));
  315. }
  316. },
  317. // AlertifyJS will invoke this each time a settings value gets updated.
  318. settingUpdated:function(key, oldValue, newValue){
  319. // Use this to respond to specific setting updates.
  320. const _this = this;
  321. ['onsave', 'onreset', 'saver', 'reseter'].includes(key) && check('function');
  322. ['options'].includes(key) && check(Array);
  323.  
  324. function rollback() {
  325. _this.set(key, oldValue);
  326. }
  327.  
  328. function check(type) {
  329. valid(oldValue, type) && !valid(newValue, type) && rollback();
  330. }
  331.  
  332. function valid(value, type) {
  333. return ({
  334. 'string': () => (typeof value === type),
  335. 'function': () => (value instanceof type)
  336. })[typeof type]();
  337. }
  338. },
  339. // listen to internal dialog events.
  340. hooks:{
  341. // triggered when the dialog is shown, this is seperate from user defined onshow
  342. onshow: function() {
  343. this.resizeTo('80%', '80%');
  344. },
  345. // triggered when the dialog is closed, this is seperate from user defined onclose
  346. onclose: function() {
  347. const onclose = this.get('onclose');
  348. typeof onclose === 'function' && onclose();
  349. },
  350. // triggered when a dialog option gets updated.
  351. // IMPORTANT: This will not be triggered for dialog custom settings updates ( use settingUpdated instead).
  352. onupdate: function() {
  353. }
  354. }
  355. }
  356. }, true);
  357.  
  358. exports = {
  359. SettingPanel: SettingPanel,
  360. SettingOption: SettingOption,
  361. isOption: isOption
  362. };
  363.  
  364. // A table-based setting panel using alertify-js
  365. // For wenku8++ only version
  366. // Use 'new' keyword
  367. // Usage:
  368. /*
  369. var panel = new SettingPanel({
  370. buttons: 0,
  371. header: '',
  372. className: '',
  373. id: '',
  374. name: '',
  375. tables: [
  376. {
  377. className: '',
  378. id: '',
  379. name: '',
  380. rows: [
  381. {
  382. className: '',
  383. id: '',
  384. name: '',
  385. blocks: [
  386. {
  387. isHeader: false,
  388. width: '',
  389. height: '',
  390. innerHTML / innerText: ''
  391. colSpan: 1,
  392. rowSpan: 1,
  393. className: '',
  394. id: '',
  395. name: '',
  396. options: [SettingOption, ...]
  397. children: [HTMLElement, ...]
  398. },
  399. ...
  400. ]
  401. },
  402. ...
  403. ]
  404. },
  405. ...
  406. ]
  407. });
  408. */
  409. function SettingPanel(details={}, storage) {
  410. const SP = this;
  411. SP.insertTable = insertTable;
  412. SP.appendTable = appendTable;
  413. SP.removeTable = removeTable;
  414. SP.remove = remove;
  415. SP.PanelTable = PanelTable;
  416. SP.PanelRow = PanelRow;
  417. SP.PanelBlock = PanelBlock;
  418.  
  419. // <div> element
  420. const elm = $CrE('div');
  421. copyProps(details, elm, ['id', 'name', 'className']);
  422. elm.classList.add('settingpanel-container');
  423.  
  424. // Configure object
  425. let css='', usercss='';
  426. SP.element = elm;
  427. SP.elements = {};
  428. SP.children = {};
  429. SP.tables = [];
  430. SP.length = 0;
  431. details.id !== undefined && (SP.elements[details.id] = elm);
  432. copyProps(details, SP, ['id', 'name']);
  433. Object.defineProperty(SP, 'css', {
  434. configurable: false,
  435. enumerable: true,
  436. get: function() {
  437. return css;
  438. },
  439. set: function(_css) {
  440. addStyle(_css, 'settingpanel-css');
  441. css = _css;
  442. }
  443. });
  444. Object.defineProperty(SP, 'usercss', {
  445. configurable: false,
  446. enumerable: true,
  447. get: function() {
  448. return usercss;
  449. },
  450. set: function(_usercss) {
  451. addStyle(_usercss, 'settingpanel-usercss');
  452. usercss = _usercss;
  453. }
  454. });
  455. SP.css = `.settingpanel-table {border-spacing: 0px; border-collapse: collapse; width: 100%; margin: 2em 0;} .settingpanel-block {border: 1px solid ${ASSETS.Color.Text}; text-align: center; vertical-align: middle; padding: 3px; text-align: left;} .settingpanel-header {font-weight: bold;}`
  456.  
  457. // Make alerity box
  458. const box = SP.alertifyBox = alertify.setpanel({buttons: details.hasOwnProperty('buttons') ? details.buttons : 'basic'});
  459. clearChildNodes(box.elements.content);
  460. box.elements.content.appendChild(elm);
  461. box.elements.content.style.overflow = 'auto';
  462. box.setHeader(details.header);
  463. box.setting({
  464. maximizable: true,
  465. overflow: true
  466. });
  467. !box.isOpen() && box.show();
  468.  
  469. // Create tables
  470. if (details.tables) {
  471. for (const table of details.tables) {
  472. if (table instanceof PanelTable) {
  473. appendTable(table);
  474. } else {
  475. appendTable(new PanelTable(table));
  476. }
  477. }
  478. }
  479.  
  480. // Insert a Panel-Row
  481. // Returns Panel object
  482. function insertTable(table, index) {
  483. // Insert table
  484. !(table instanceof PanelTable) && (table = new PanelTable(table));
  485. index < SP.length ? elm.insertBefore(table.element, elm.children[index]) : elm.appendChild(table.element);
  486. insertItem(SP.tables, table, index);
  487. table.id !== undefined && (SP.children[table.id] = table);
  488. SP.length++;
  489.  
  490. // Set parent
  491. table.parent = SP;
  492.  
  493. // Inherit elements
  494. for (const [id, subelm] of Object.entries(table.elements)) {
  495. SP.elements[id] = subelm;
  496. }
  497.  
  498. // Inherit children
  499. for (const [id, child] of Object.entries(table.children)) {
  500. SP.children[id] = child;
  501. }
  502. return SP;
  503. }
  504.  
  505. // Append a Panel-Row
  506. // Returns Panel object
  507. function appendTable(table) {
  508. return insertTable(table, SP.length);
  509. }
  510.  
  511. // Remove a Panel-Row
  512. // Returns Panel object
  513. function removeTable(index) {
  514. const table = SP.tables[index];
  515. SP.element.removeChild(table.element);
  516. removeItem(SP.rows, index);
  517. return SP;
  518. }
  519.  
  520. // Remove itself from parentElement
  521. // Returns Panel object
  522. function remove() {
  523. SP.element.parentElement && SP.parentElement.removeChild(SP.element);
  524. return SP;
  525. }
  526.  
  527. // Panel-Table object
  528. // Use 'new' keyword
  529. function PanelTable(details={}) {
  530. const PT = this;
  531. PT.insertRow = insertRow;
  532. PT.appendRow = appendRow;
  533. PT.removeRow = removeRow;
  534. PT.remove = remove
  535.  
  536. // <table> element
  537. const elm = $CrE('table');
  538. copyProps(details, elm, ['id', 'name', 'className']);
  539. elm.classList.add('settingpanel-table');
  540.  
  541. // Configure
  542. PT.element = elm;
  543. PT.elements = {};
  544. PT.children = {};
  545. PT.rows = [];
  546. PT.length = 0;
  547. details.id !== undefined && (PT.elements[details.id] = elm);
  548. copyProps(details, PT, ['id', 'name']);
  549.  
  550. // Append rows
  551. if (details.rows) {
  552. for (const row of details.rows) {
  553. if (row instanceof PanelRow) {
  554. insertRow(row);
  555. } else {
  556. insertRow(new PanelRow(row));
  557. }
  558. }
  559. }
  560.  
  561. // Insert a Panel-Row
  562. // Returns Panel-Table object
  563. function insertRow(row, index) {
  564. // Insert row
  565. !(row instanceof PanelRow) && (row = new PanelRow(row));
  566. index < PT.length ? elm.insertBefore(row.element, elm.children[index]) : elm.appendChild(row.element);
  567. insertItem(PT.rows, row, index);
  568. row.id !== undefined && (PT.children[row.id] = row);
  569. PT.length++;
  570.  
  571. // Set parent
  572. row.parent = PT;
  573.  
  574. // Inherit elements
  575. for (const [id, subelm] of Object.entries(row.elements)) {
  576. PT.elements[id] = subelm;
  577. }
  578.  
  579. // Inherit children
  580. for (const [id, child] of Object.entries(row.children)) {
  581. PT.children[id] = child;
  582. }
  583. return PT;
  584. }
  585.  
  586. // Append a Panel-Row
  587. // Returns Panel-Table object
  588. function appendRow(row) {
  589. return insertRow(row, PT.length);
  590. }
  591.  
  592. // Remove a Panel-Row
  593. // Returns Panel-Table object
  594. function removeRow(index) {
  595. const row = PT.rows[index];
  596. PT.element.removeChild(row.element);
  597. removeItem(PT.rows, index);
  598. return PT;
  599. }
  600.  
  601. // Remove itself from parentElement
  602. // Returns Panel-Table object
  603. function remove() {
  604. PT.parent instanceof SettingPanel && PT.parent.removeTable(PT.tables.indexOf(PT));
  605. return PT;
  606. }
  607. }
  608.  
  609. // Panel-Row object
  610. // Use 'new' keyword
  611. function PanelRow(details={}) {
  612. const PR = this;
  613. PR.insertBlock = insertBlock;
  614. PR.appendBlock = appendBlock;
  615. PR.removeBlock = removeBlock;
  616. PR.remove = remove;
  617.  
  618. // <tr> element
  619. const elm = $CrE('tr');
  620. copyProps(details, elm, ['id', 'name', 'className']);
  621. elm.classList.add('settingpanel-row');
  622.  
  623. // Configure object
  624. PR.element = elm;
  625. PR.elements = {};
  626. PR.children = {};
  627. PR.blocks = [];
  628. PR.length = 0;
  629. details.id !== undefined && (PR.elements[details.id] = elm);
  630. copyProps(details, PR, ['id', 'name']);
  631.  
  632. // Append blocks
  633. if (details.blocks) {
  634. for (const block of details.blocks) {
  635. if (block instanceof PanelBlock) {
  636. appendBlock(block);
  637. } else {
  638. appendBlock(new PanelBlock(block));
  639. }
  640. }
  641. }
  642.  
  643. // Insert a Panel-Block
  644. // Returns Panel-Row object
  645. function insertBlock(block, index) {
  646. // Insert block
  647. !(block instanceof PanelBlock) && (block = new PanelBlock(block));
  648. index < PR.length ? elm.insertBefore(block.element, elm.children[index]) : elm.appendChild(block.element);
  649. insertItem(PR.blocks, block, index);
  650. block.id !== undefined && (PR.children[block.id] = block);
  651. PR.length++;
  652.  
  653. // Set parent
  654. block.parent = PR;
  655.  
  656. // Inherit elements
  657. for (const [id, subelm] of Object.entries(block.elements)) {
  658. PR.elements[id] = subelm;
  659. }
  660.  
  661. // Inherit children
  662. for (const [id, child] of Object.entries(block.children)) {
  663. PR.children[id] = child;
  664. }
  665. return PR;
  666. };
  667.  
  668. // Append a Panel-Block
  669. // Returns Panel-Row object
  670. function appendBlock(block) {
  671. return insertBlock(block, PR.length);
  672. }
  673.  
  674. // Remove a Panel-Block
  675. // Returns Panel-Row object
  676. function removeBlock(index) {
  677. const block = PR.blocks[index];
  678. PR.element.removeChild(block.element);
  679. removeItem(PR.blocks, index);
  680. return PR;
  681. }
  682.  
  683. // Remove itself from parent
  684. // Returns Panel-Row object
  685. function remove() {
  686. PR.parent instanceof PanelTable && PR.parent.removeRow(PR.parent.rows.indexOf(PR));
  687. return PR;
  688. }
  689. }
  690.  
  691. // Panel-Block object
  692. // Use 'new' keyword
  693. function PanelBlock(details={}) {
  694. const PB = this;
  695. PB.remove = remove;
  696.  
  697. // <td> element
  698. const elm = $CrE(details.isHeader ? 'th' : 'td');
  699. copyProps(details, elm, ['innerText', 'innerHTML', 'colSpan', 'rowSpan', 'id', 'name', 'className']);
  700. copyProps(details, elm.style, ['width', 'height']);
  701. elm.classList.add('settingpanel-block');
  702. details.isHeader && elm.classList.add('settingpanel-header');
  703.  
  704. // Configure object
  705. PB.element = elm;
  706. PB.elements = {};
  707. PB.children = {};
  708. details.id !== undefined && (PB.elements[details.id] = elm);
  709. copyProps(details, PB, ['id', 'name']);
  710.  
  711. // Append to parent if need
  712. details.parent instanceof PanelRow && (PB.parent = details.parent.appendBlock(PB));
  713.  
  714. // Append SettingOptions if exist
  715. if (details.options) {
  716. details.options.filter(storage ? () => (true) : isOption).map((o) => (isOption(o) ? o : new SettingOption(storage, o))).forEach(function(option) {
  717. SP.alertifyBox.get('options').push(option);
  718. elm.appendChild(option.element);
  719. });
  720. }
  721.  
  722. // Append child elements if exist
  723. if (details.children) {
  724. for (const child of details.children) {
  725. elm.appendChild(child);
  726. }
  727. }
  728.  
  729. // Remove itself from parent
  730. // Returns Panel-Block object
  731. function remove() {
  732. PB.parent instanceof PanelRow && PB.parent.removeBlock(PB.parent.blocks.indexOf(PB));
  733. return PB;
  734. }
  735. }
  736.  
  737. function $R(e) {return $(e) && $(e).parentElement.removeChild($(e));}
  738. function insertItem(arr, item, index) {
  739. for (let i = arr.length; i > index ; i--) {
  740. arr[i] = arr[i-1];
  741. }
  742. arr[index] = item;
  743. return arr;
  744. }
  745. function removeItem(arr, index) {
  746. for (let i = index; i < arr.length-1; i++) {
  747. arr[i] = arr[i+1];
  748. }
  749. delete arr[arr.length-1];
  750. return arr;
  751. }
  752. function MakeReadonlyObj(val) {
  753. return isObject(val) ? new Proxy(val, {
  754. get: function(target, property, receiver) {
  755. return MakeReadonlyObj(target[property]);
  756. },
  757. set: function(target, property, value, receiver) {},
  758. has: function(target, prop) {}
  759. }) : val;
  760.  
  761. function isObject(value) {
  762. return ['object', 'function'].includes(typeof value) && value !== null;
  763. }
  764. }
  765. }
  766.  
  767. // details = {path='config path', type='config type', data='option data'}
  768. function SettingOption(storage, details={}) {
  769. const SO = this;
  770. SO.save = save;
  771. SO.reset = reset;
  772.  
  773. // Initialize ConfigManager
  774. !storage && Err('SettingOption requires GM_storage functions');
  775. const CM = new ConfigManager(CONST.Manager_Config_Ruleset, storage);
  776. const CONFIG = CM.Config;
  777.  
  778. // Get args
  779. const options = ['path', 'type', 'checker', 'data', 'autoSave'];
  780. copyProps(details, SO, options);
  781.  
  782. // Create element
  783. const original_value = CM.getConfig(SO.path);
  784. // type => create element
  785. const valueElement = {
  786. 'string': (() => {const e = $CrE('input'); e.style.width = '90%'; return e;}) (),
  787. 'number': (() => {const e = $CrE('input'); e.type = 'number'; e.style.width = '90%'; return e;}) (),
  788. 'boolean': (() => {const e = $CrE('input'); e.type = 'checkbox'; return e;}) (),
  789. 'select': (() => {const e = $CrE('select'); SO.hasOwnProperty('data') && SO.data.forEach((d) => {const o = $CrE('option'); o.innerText = d; e.appendChild(o)}); return e;}) ()
  790. };
  791. // type => set element value
  792. const valueSetter = {
  793. 'string': (elm, val) => (elm.value = val),
  794. 'number': (elm, val) => (elm.value = val),
  795. 'boolean': (elm, val) => (elm.checked = val),
  796. 'select': (elm, val) => (Array.from(elm.children).find((opt) => (opt.value === val)).selected = true),
  797. };
  798. // type => get element value
  799. const valueGetter = {
  800. 'string': (elm) => (elm.value),
  801. 'number': (elm) => (elm.value),
  802. 'boolean': (elm) => (elm.checked ? (SO.hasOwnProperty('data') ? SO.data[0] : true) : (SO.hasOwnProperty('data') ? SO.data[1] : false)),
  803. 'select': (elm) => (elm.value),
  804. }
  805. !Object.keys(valueElement).includes(SO.type) && Err('Unsupported Panel-Option type');
  806. SO.element = valueElement[SO.type];
  807. valueSetter[SO.type](SO.element, original_value);
  808.  
  809. // Bind change-checker-saver
  810. SO.element.addEventListener('change', function(e) {
  811. if (SO.checker) {
  812. if (SO.checker(e, valueGetter[SO.type](SO.element))) {
  813. SO.autoSave && save();
  814. } else {
  815. // Reset value
  816. reset();
  817.  
  818. // Do some value-invalid reminding here
  819. }
  820. } else {
  821. SO.autoSave && save();
  822. }
  823. });
  824.  
  825. function save() {
  826. CM.setConfig(SO.path, valueGetter[SO.type](SO.element));
  827. }
  828.  
  829. function reset() {
  830. CM.setConfig(SO.path, original_value);
  831. valueSetter[SO.type](SO.element, original_value);
  832. }
  833. }
  834.  
  835. function isOption(obj) {
  836. return obj instanceof SettingOption;
  837. }
  838.  
  839. // Deep copy an object
  840. function deepClone(obj) {
  841. let newObj = Array.isArray(obj) ? [] : {};
  842. if (obj && typeof obj === "object") {
  843. for (let key in obj) {
  844. if (obj.hasOwnProperty(key)) {
  845. newObj[key] = (obj && typeof obj[key] === 'object') ? deepClone(obj[key]) : obj[key];
  846. }
  847. }
  848. }
  849. return newObj;
  850. }
  851. })();