SettingPanel

SettingPanel for wenku8++

目前為 2022-08-28 提交的版本,檢視 最新版本

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