SettingPanel

SettingPanel for wenku8++

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

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