SettingPanel

SettingPanel for wenku8++

当前为 2022-08-28 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/450209/1086710/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.2
  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. onclose: function() {},
  310. options: [], // SettingOption array
  311. saver: function() {
  312. this.get('options').forEach((o) => (o.save()));
  313. },
  314. reseter: function() {
  315. this.get('options').forEach((o) => (o.reset()));
  316. }
  317. },
  318. // AlertifyJS will invoke this each time a settings value gets updated.
  319. settingUpdated:function(key, oldValue, newValue){
  320. // Use this to respond to specific setting updates.
  321. ['onsave', 'onreset', 'onclose', '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. this.get('onclose')();
  348. },
  349. // triggered when a dialog option gets updated.
  350. // IMPORTANT: This will not be triggered for dialog custom settings updates ( use settingUpdated instead).
  351. onupdate: function(){
  352. }
  353. }
  354. }
  355. }, true);
  356.  
  357. exports = {
  358. SettingPanel: SettingPanel,
  359. SettingOption: SettingOption,
  360. isOption: isOption
  361. };
  362.  
  363. // A table-based setting panel using alertify-js
  364. // For wenku8++ only version
  365. // Use 'new' keyword
  366. // Usage:
  367. /*
  368. var panel = new SettingPanel({
  369. buttons: 0,
  370. header: '',
  371. className: '',
  372. id: '',
  373. name: '',
  374. tables: [
  375. {
  376. className: '',
  377. id: '',
  378. name: '',
  379. rows: [
  380. {
  381. className: '',
  382. id: '',
  383. name: '',
  384. blocks: [
  385. {
  386. isHeader: false,
  387. width: '',
  388. height: '',
  389. innerHTML / innerText: ''
  390. colSpan: 1,
  391. rowSpan: 1,
  392. className: '',
  393. id: '',
  394. name: '',
  395. options: [SettingOption, ...]
  396. children: [HTMLElement, ...]
  397. },
  398. ...
  399. ]
  400. },
  401. ...
  402. ]
  403. },
  404. ...
  405. ]
  406. });
  407. */
  408. function SettingPanel(details={}, storage) {
  409. const SP = this;
  410. SP.insertTable = insertTable;
  411. SP.appendTable = appendTable;
  412. SP.removeTable = removeTable;
  413. SP.remove = remove;
  414. SP.PanelTable = PanelTable;
  415. SP.PanelRow = PanelRow;
  416. SP.PanelBlock = PanelBlock;
  417.  
  418. // <div> element
  419. const elm = $CrE('div');
  420. copyProps(details, elm, ['id', 'name', 'className']);
  421. elm.classList.add('settingpanel-container');
  422.  
  423. // Configure object
  424. let css='', usercss='';
  425. SP.element = elm;
  426. SP.elements = {};
  427. SP.children = {};
  428. SP.tables = [];
  429. SP.length = 0;
  430. details.id !== undefined && (SP.elements[details.id] = elm);
  431. copyProps(details, SP, ['id', 'name']);
  432. Object.defineProperty(SP, 'css', {
  433. configurable: false,
  434. enumerable: true,
  435. get: function() {
  436. return css;
  437. },
  438. set: function(_css) {
  439. addStyle(_css, 'settingpanel-css');
  440. css = _css;
  441. }
  442. });
  443. Object.defineProperty(SP, 'usercss', {
  444. configurable: false,
  445. enumerable: true,
  446. get: function() {
  447. return usercss;
  448. },
  449. set: function(_usercss) {
  450. addStyle(_usercss, 'settingpanel-usercss');
  451. usercss = _usercss;
  452. }
  453. });
  454. 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;}`
  455.  
  456. // Make alerity box
  457. const box = SP.alertifyBox = alertify.setpanel({buttons: details.hasOwnProperty('buttons') ? details.buttons : 'basic'});
  458. clearChildNodes(box.elements.content);
  459. box.elements.content.appendChild(elm);
  460. box.elements.content.style.overflow = 'auto';
  461. box.setHeader(details.header);
  462. box.setting({
  463. maximizable: true,
  464. overflow: true
  465. });
  466. !box.isOpen() && box.show();
  467.  
  468. // Create tables
  469. if (details.tables) {
  470. for (const table of details.tables) {
  471. if (table instanceof PanelTable) {
  472. appendTable(table);
  473. } else {
  474. appendTable(new PanelTable(table));
  475. }
  476. }
  477. }
  478.  
  479. // Insert a Panel-Row
  480. // Returns Panel object
  481. function insertTable(table, index) {
  482. // Insert table
  483. !(table instanceof PanelTable) && (table = new PanelTable(table));
  484. index < SP.length ? elm.insertBefore(table.element, elm.children[index]) : elm.appendChild(table.element);
  485. insertItem(SP.tables, table, index);
  486. table.id !== undefined && (SP.children[table.id] = table);
  487. SP.length++;
  488.  
  489. // Set parent
  490. table.parent = SP;
  491.  
  492. // Inherit elements
  493. for (const [id, subelm] of Object.entries(table.elements)) {
  494. SP.elements[id] = subelm;
  495. }
  496.  
  497. // Inherit children
  498. for (const [id, child] of Object.entries(table.children)) {
  499. SP.children[id] = child;
  500. }
  501. return SP;
  502. }
  503.  
  504. // Append a Panel-Row
  505. // Returns Panel object
  506. function appendTable(table) {
  507. return insertTable(table, SP.length);
  508. }
  509.  
  510. // Remove a Panel-Row
  511. // Returns Panel object
  512. function removeTable(index) {
  513. const table = SP.tables[index];
  514. SP.element.removeChild(table.element);
  515. removeItem(SP.rows, index);
  516. return SP;
  517. }
  518.  
  519. // Remove itself from parentElement
  520. // Returns Panel object
  521. function remove() {
  522. SP.element.parentElement && SP.parentElement.removeChild(SP.element);
  523. return SP;
  524. }
  525.  
  526. // Panel-Table object
  527. // Use 'new' keyword
  528. function PanelTable(details={}) {
  529. const PT = this;
  530. PT.insertRow = insertRow;
  531. PT.appendRow = appendRow;
  532. PT.removeRow = removeRow;
  533. PT.remove = remove
  534.  
  535. // <table> element
  536. const elm = $CrE('table');
  537. copyProps(details, elm, ['id', 'name', 'className']);
  538. elm.classList.add('settingpanel-table');
  539.  
  540. // Configure
  541. PT.element = elm;
  542. PT.elements = {};
  543. PT.children = {};
  544. PT.rows = [];
  545. PT.length = 0;
  546. details.id !== undefined && (PT.elements[details.id] = elm);
  547. copyProps(details, PT, ['id', 'name']);
  548.  
  549. // Append rows
  550. if (details.rows) {
  551. for (const row of details.rows) {
  552. if (row instanceof PanelRow) {
  553. insertRow(row);
  554. } else {
  555. insertRow(new PanelRow(row));
  556. }
  557. }
  558. }
  559.  
  560. // Insert a Panel-Row
  561. // Returns Panel-Table object
  562. function insertRow(row, index) {
  563. // Insert row
  564. !(row instanceof PanelRow) && (row = new PanelRow(row));
  565. index < PT.length ? elm.insertBefore(row.element, elm.children[index]) : elm.appendChild(row.element);
  566. insertItem(PT.rows, row, index);
  567. row.id !== undefined && (PT.children[row.id] = row);
  568. PT.length++;
  569.  
  570. // Set parent
  571. row.parent = PT;
  572.  
  573. // Inherit elements
  574. for (const [id, subelm] of Object.entries(row.elements)) {
  575. PT.elements[id] = subelm;
  576. }
  577.  
  578. // Inherit children
  579. for (const [id, child] of Object.entries(row.children)) {
  580. PT.children[id] = child;
  581. }
  582. return PT;
  583. }
  584.  
  585. // Append a Panel-Row
  586. // Returns Panel-Table object
  587. function appendRow(row) {
  588. return insertRow(row, PT.length);
  589. }
  590.  
  591. // Remove a Panel-Row
  592. // Returns Panel-Table object
  593. function removeRow(index) {
  594. const row = PT.rows[index];
  595. PT.element.removeChild(row.element);
  596. removeItem(PT.rows, index);
  597. return PT;
  598. }
  599.  
  600. // Remove itself from parentElement
  601. // Returns Panel-Table object
  602. function remove() {
  603. PT.parent instanceof SettingPanel && PT.parent.removeTable(PT.tables.indexOf(PT));
  604. return PT;
  605. }
  606. }
  607.  
  608. // Panel-Row object
  609. // Use 'new' keyword
  610. function PanelRow(details={}) {
  611. const PR = this;
  612. PR.insertBlock = insertBlock;
  613. PR.appendBlock = appendBlock;
  614. PR.removeBlock = removeBlock;
  615. PR.remove = remove;
  616.  
  617. // <tr> element
  618. const elm = $CrE('tr');
  619. copyProps(details, elm, ['id', 'name', 'className']);
  620. elm.classList.add('settingpanel-row');
  621.  
  622. // Configure object
  623. PR.element = elm;
  624. PR.elements = {};
  625. PR.children = {};
  626. PR.blocks = [];
  627. PR.length = 0;
  628. details.id !== undefined && (PR.elements[details.id] = elm);
  629. copyProps(details, PR, ['id', 'name']);
  630.  
  631. // Append blocks
  632. if (details.blocks) {
  633. for (const block of details.blocks) {
  634. if (block instanceof PanelBlock) {
  635. appendBlock(block);
  636. } else {
  637. appendBlock(new PanelBlock(block));
  638. }
  639. }
  640. }
  641.  
  642. // Insert a Panel-Block
  643. // Returns Panel-Row object
  644. function insertBlock(block, index) {
  645. // Insert block
  646. !(block instanceof PanelBlock) && (block = new PanelBlock(block));
  647. index < PR.length ? elm.insertBefore(block.element, elm.children[index]) : elm.appendChild(block.element);
  648. insertItem(PR.blocks, block, index);
  649. block.id !== undefined && (PR.children[block.id] = block);
  650. PR.length++;
  651.  
  652. // Set parent
  653. block.parent = PR;
  654.  
  655. // Inherit elements
  656. for (const [id, subelm] of Object.entries(block.elements)) {
  657. PR.elements[id] = subelm;
  658. }
  659.  
  660. // Inherit children
  661. for (const [id, child] of Object.entries(block.children)) {
  662. PR.children[id] = child;
  663. }
  664. return PR;
  665. };
  666.  
  667. // Append a Panel-Block
  668. // Returns Panel-Row object
  669. function appendBlock(block) {
  670. return insertBlock(block, PR.length);
  671. }
  672.  
  673. // Remove a Panel-Block
  674. // Returns Panel-Row object
  675. function removeBlock(index) {
  676. const block = PR.blocks[index];
  677. PR.element.removeChild(block.element);
  678. removeItem(PR.blocks, index);
  679. return PR;
  680. }
  681.  
  682. // Remove itself from parent
  683. // Returns Panel-Row object
  684. function remove() {
  685. PR.parent instanceof PanelTable && PR.parent.removeRow(PR.parent.rows.indexOf(PR));
  686. return PR;
  687. }
  688. }
  689.  
  690. // Panel-Block object
  691. // Use 'new' keyword
  692. function PanelBlock(details={}) {
  693. const PB = this;
  694. PB.remove = remove;
  695.  
  696. // <td> element
  697. const elm = $CrE(details.isHeader ? 'th' : 'td');
  698. copyProps(details, elm, ['innerText', 'innerHTML', 'colSpan', 'rowSpan', 'id', 'name', 'className']);
  699. copyProps(details, elm.style, ['width', 'height']);
  700. elm.classList.add('settingpanel-block');
  701. details.isHeader && elm.classList.add('settingpanel-header');
  702.  
  703. // Configure object
  704. PB.element = elm;
  705. PB.elements = {};
  706. PB.children = {};
  707. details.id !== undefined && (PB.elements[details.id] = elm);
  708. copyProps(details, PB, ['id', 'name']);
  709.  
  710. // Append to parent if need
  711. details.parent instanceof PanelRow && (PB.parent = details.parent.appendBlock(PB));
  712.  
  713. // Append SettingOptions if exist
  714. if (details.options) {
  715. details.options.filter(storage ? () => (true) : isOption).map((o) => (isOption(o) ? o : new SettingOption(storage, o))).forEach(function(option) {
  716. SP.alertifyBox.get('options').push(option);
  717. elm.appendChild(option.element);
  718. });
  719. }
  720.  
  721. // Append child elements if exist
  722. if (details.children) {
  723. for (const child of details.children) {
  724. elm.appendChild(child);
  725. }
  726. }
  727.  
  728. // Remove itself from parent
  729. // Returns Panel-Block object
  730. function remove() {
  731. PB.parent instanceof PanelRow && PB.parent.removeBlock(PB.parent.blocks.indexOf(PB));
  732. return PB;
  733. }
  734. }
  735.  
  736. function $R(e) {return $(e) && $(e).parentElement.removeChild($(e));}
  737. function insertItem(arr, item, index) {
  738. for (let i = arr.length; i > index ; i--) {
  739. arr[i] = arr[i-1];
  740. }
  741. arr[index] = item;
  742. return arr;
  743. }
  744. function removeItem(arr, index) {
  745. for (let i = index; i < arr.length-1; i++) {
  746. arr[i] = arr[i+1];
  747. }
  748. delete arr[arr.length-1];
  749. return arr;
  750. }
  751. function MakeReadonlyObj(val) {
  752. return isObject(val) ? new Proxy(val, {
  753. get: function(target, property, receiver) {
  754. return MakeReadonlyObj(target[property]);
  755. },
  756. set: function(target, property, value, receiver) {},
  757. has: function(target, prop) {}
  758. }) : val;
  759.  
  760. function isObject(value) {
  761. return ['object', 'function'].includes(typeof value) && value !== null;
  762. }
  763. }
  764. }
  765.  
  766. // details = {path='config path', type='config type', data='option data'}
  767. function SettingOption(storage, details={}) {
  768. const SO = this;
  769. SO.save = save;
  770. SO.reset = reset;
  771.  
  772. // Initialize ConfigManager
  773. !storage && Err('SettingOption requires GM_storage functions');
  774. const CM = new ConfigManager(CONST.Manager_Config_Ruleset, storage);
  775. const CONFIG = CM.Config;
  776.  
  777. // Get args
  778. const options = ['path', 'type', 'checker', 'data', 'autoSave'];
  779. copyProps(details, SO, options);
  780.  
  781. // Create element
  782. const original_value = CM.getConfig(SO.path);
  783. // type => create element
  784. const valueElement = {
  785. 'string': (() => {const e = $CrE('input'); e.style.width = '90%'; return e;}) (),
  786. 'number': (() => {const e = $CrE('input'); e.type = 'number'; e.style.width = '90%'; return e;}) (),
  787. 'boolean': (() => {const e = $CrE('input'); e.type = 'checkbox'; return e;}) (),
  788. 'select': (() => {const e = $CrE('select'); SO.hasOwnProperty('data') && SO.data.forEach((d) => {const o = $CrE('option'); o.innerText = d; e.appendChild(o)}); return e;}) ()
  789. };
  790. // type => set element value
  791. const valueSetter = {
  792. 'string': (elm, val) => (elm.value = val),
  793. 'number': (elm, val) => (elm.value = val),
  794. 'boolean': (elm, val) => (elm.checked = val),
  795. 'select': (elm, val) => (Array.from(elm.children).find((opt) => (opt.value === val)).selected = true),
  796. };
  797. // type => get element value
  798. const valueGetter = {
  799. 'string': (elm) => (elm.value),
  800. 'number': (elm) => (elm.value),
  801. 'boolean': (elm) => (elm.checked ? (SO.hasOwnProperty('data') ? SO.data[0] : true) : (SO.hasOwnProperty('data') ? SO.data[1] : false)),
  802. 'select': (elm) => (elm.value),
  803. }
  804. !Object.keys(valueElement).includes(SO.type) && Err('Unsupported Panel-Option type');
  805. SO.element = valueElement[SO.type];
  806. valueSetter[SO.type](SO.element, original_value);
  807.  
  808. // Bind change-checker-saver
  809. SO.element.addEventListener('change', function(e) {
  810. if (SO.checker) {
  811. if (SO.checker(e, valueGetter[SO.type](SO.element))) {
  812. SO.autoSave && save();
  813. } else {
  814. // Reset value
  815. reset();
  816.  
  817. // Do some value-invalid reminding here
  818. }
  819. } else {
  820. SO.autoSave && save();
  821. }
  822. });
  823.  
  824. function save() {
  825. CM.setConfig(SO.path, valueGetter[SO.type](SO.element));
  826. }
  827.  
  828. function reset() {
  829. CM.setConfig(SO.path, original_value);
  830. valueSetter[SO.type](SO.element, original_value);
  831. }
  832. }
  833.  
  834. function isOption(obj) {
  835. return obj instanceof SettingOption;
  836. }
  837.  
  838. // Deep copy an object
  839. function deepClone(obj) {
  840. let newObj = Array.isArray(obj) ? [] : {};
  841. if (obj && typeof obj === "object") {
  842. for (let key in obj) {
  843. if (obj.hasOwnProperty(key)) {
  844. newObj[key] = (obj && typeof obj[key] === 'object') ? deepClone(obj[key]) : obj[key];
  845. }
  846. }
  847. }
  848. return newObj;
  849. }
  850. })();