SettingPanel

SettingPanel for wenku8++

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

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