SettingPanel

SettingPanel for wenku8++

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

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/450209/1085786/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.1.1
  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=1085356
  17. // @grant none
  18. // ==/UserScript==
  19.  
  20. (function __MAIN__() {
  21. 'use strict';
  22.  
  23. const alertify = require('alertify');
  24. const CONST = {
  25. Text: {},
  26. Manager_Config_Ruleset: {
  27. 'version-key': 'config-version',
  28. 'ignores': ["LOCAL-CDN"],
  29. 'defaultValues': {
  30. //'config-key': {},
  31. }
  32. }
  33. };
  34.  
  35. // initialize
  36. alertify.dialog('setpanel', function factory(){
  37. return {
  38. // The dialog startup function
  39. // This will be called each time the dialog is invoked
  40. // For example: alertify.myDialog( data );
  41. main:function(content, header, onsave, onunsave){
  42. this.resizeTo('80%', '80%');
  43. content && this.setContent(content);
  44. header && this.setHeader(header);
  45. content && this.set('onsave', onsave);
  46. header && this.set('onunsave', onunsave);
  47. return this;
  48. },
  49. // The dialog setup function
  50. // This should return the dialog setup object ( buttons, focus and options overrides ).
  51. setup:function(){
  52. return {
  53. /* buttons collection */
  54. buttons:[{
  55. /* button label */
  56. text: '不保存',
  57.  
  58. /*bind a keyboard key to the button */
  59. key: undefined,
  60.  
  61. /* indicate if closing the dialog should trigger this button action */
  62. invokeOnClose: true,
  63.  
  64. /* custom button class name */
  65. className: alertify.defaults.theme.cancel,
  66.  
  67. /* custom button attributes */
  68. attrs: {},
  69.  
  70. /* Defines the button scope, either primary (default) or auxiliary */
  71. scope:'auxiliary',
  72.  
  73. /* The will conatin the button DOMElement once buttons are created */
  74. element: undefined
  75. },{
  76. /* button label */
  77. text: '保存',
  78.  
  79. /*bind a keyboard key to the button */
  80. key: undefined,
  81.  
  82. /* indicate if closing the dialog should trigger this button action */
  83. invokeOnClose: false,
  84.  
  85. /* custom button class name */
  86. className: alertify.defaults.theme.ok,
  87.  
  88. /* custom button attributes */
  89. attrs: {},
  90.  
  91. /* Defines the button scope, either primary (default) or auxiliary */
  92. scope:'primary',
  93.  
  94. /* The will conatin the button DOMElement once buttons are created */
  95. element: undefined
  96. }],
  97.  
  98. /* default focus */
  99. focus:{
  100. /* the element to receive default focus, has differnt meaning based on value type:
  101. number: action button index.
  102. string: querySelector to select from dialog body contents.
  103. function: when invoked, should return the focus element.
  104. DOMElement: the focus element.
  105. object: an object that implements .focus() and .select() functions.
  106. */
  107. element: 0,
  108.  
  109. /* indicates if the element should be selected on focus or not*/
  110. select: true
  111.  
  112. },
  113. /* dialog options, these override the defaults */
  114. options: {
  115. title: 'Setting Panel',
  116. modal: true,
  117. basic: false,
  118. frameless: false,
  119. pinned: false,
  120. movable: true,
  121. moveBounded: false,
  122. resizable: true,
  123. autoReset: false,
  124. closable: true,
  125. closableByDimmer: true,
  126. maximizable: false,
  127. startMaximized: false,
  128. pinnable: false,
  129. transition: 'fade',
  130. padding: true,
  131. overflow: true,
  132. /*
  133. onshow:...,
  134. onclose:...,
  135. onfocus:...,
  136. onmove:...,
  137. onmoved:...,
  138. onresize:...,
  139. onresized:...,
  140. onmaximize:...,
  141. onmaximized:...,
  142. onrestore:...,
  143. onrestored:...
  144. */
  145. }
  146. };
  147. },
  148. // This will be called once the dialog DOM has been created, just before its added to the document.
  149. // Its invoked only once.
  150. build:function(){
  151.  
  152. // Do custom DOM manipulation here, accessible via this.elements
  153.  
  154. // this.elements.root ==> Root div
  155. // this.elements.dimmer ==> Modal dimmer div
  156. // this.elements.modal ==> Modal div (dialog wrapper)
  157. // this.elements.dialog ==> Dialog div
  158. // this.elements.reset ==> Array containing the tab reset anchor links
  159. // this.elements.reset[0] ==> First reset element (button).
  160. // this.elements.reset[1] ==> Second reset element (button).
  161. // this.elements.header ==> Dialog header div
  162. // this.elements.body ==> Dialog body div
  163. // this.elements.content ==> Dialog body content div
  164. // this.elements.footer ==> Dialog footer div
  165. // this.elements.resizeHandle ==> Dialog resize handle div
  166.  
  167. // Dialog commands (Pin/Maximize/Close)
  168. // this.elements.commands ==> Object containing dialog command buttons references
  169. // this.elements.commands.container ==> Root commands div
  170. // this.elements.commands.pin ==> Pin command button
  171. // this.elements.commands.maximize ==> Maximize command button
  172. // this.elements.commands.close ==> Close command button
  173.  
  174. // Dialog action buttons (Ok, cancel ... etc)
  175. // this.elements.buttons ==> Object containing dialog action buttons references
  176. // this.elements.buttons.primary ==> Primary buttons div
  177. // this.elements.buttons.auxiliary ==> Auxiliary buttons div
  178.  
  179. // Each created button will be saved with the button definition inside buttons collection
  180. // this.__internal.buttons[x].element
  181.  
  182. },
  183. // This will be called each time the dialog is shown
  184. prepare:function(){
  185. // Do stuff that should be done every time the dialog is shown.
  186. },
  187. // This will be called each time an action button is clicked.
  188. callback:function(closeEvent){
  189. //The closeEvent has the following properties
  190. //
  191. // index: The index of the button triggering the event.
  192. // button: The button definition object.
  193. // cancel: When set true, prevent the dialog from closing.
  194. switch (closeEvent.index) {
  195. case 0: {
  196. const onunsave = this.get('onunsave');
  197. typeof onunsave === 'function' && onunsave();
  198. //alertify.alert('Not saved');
  199. break;
  200. }
  201. case 1: {
  202. const onsave = this.get('onsave');
  203. typeof onsave === 'function' && onsave();
  204. //alertify.alert('Saved');
  205. }
  206. }
  207. },
  208. // To make use of AlertifyJS settings API, group your custom settings into a settings object.
  209. settings:{
  210. onsave: function() {},
  211. onunsave: function() {}
  212. },
  213. // AlertifyJS will invoke this each time a settings value gets updated.
  214. settingUpdated:function(key, oldValue, newValue){/*
  215. // Use this to respond to specific setting updates.
  216. switch(key){
  217. case 'myProp':
  218. // Do something when 'myProp' changes
  219. break;
  220. }
  221. */},
  222. // listen to internal dialog events.
  223. hooks:{
  224. // triggered when the dialog is shown, this is seperate from user defined onshow
  225. onshow: function(){
  226. this.resizeTo('80%', '80%');
  227. },
  228. // triggered when the dialog is closed, this is seperate from user defined onclose
  229. onclose: function(){
  230. },
  231. // triggered when a dialog option gets updated.
  232. // IMPORTANT: This will not be triggered for dialog custom settings updates ( use settingUpdated instead).
  233. onupdate: function(){
  234. }
  235. }
  236. }
  237. }, true);
  238. SettingPanel.SettingOption = SettingOption;
  239.  
  240. exports = {
  241. SettingPanel: SettingPanel,
  242. SettingOption: SettingOption,
  243. isOption: isOption
  244. };
  245.  
  246. // A table-based setting panel using alertify-js
  247. // For wenku8++ only version
  248. // Use 'new' keyword
  249. // Usage:
  250. /*
  251. var panel = new SettingPanel({
  252. header: '';
  253. className: '',
  254. id: '',
  255. name: '',
  256. tables: [
  257. {
  258. className: '',
  259. id: '',
  260. name: '',
  261. rows: [
  262. {
  263. className: '',
  264. id: '',
  265. name: '',
  266. blocks: [
  267. {
  268. isHeader: false,
  269. width: '',
  270. height: '',
  271. innerHTML / innerText: ''
  272. colSpan: 1,
  273. rowSpan: 1,
  274. className: '',
  275. id: '',
  276. name: '',
  277. options: [SettingOption, ...]
  278. children: [HTMLElement, ...]
  279. },
  280. ...
  281. ]
  282. },
  283. ...
  284. ]
  285. },
  286. ...
  287. ]
  288. });
  289. */
  290. function SettingPanel(details={}) {
  291. const SP = this;
  292. SP.insertTable = insertTable;
  293. SP.appendTable = appendTable;
  294. SP.removeTable = removeTable;
  295. SP.remove = remove;
  296. SP.PanelTable = PanelTable;
  297. SP.PanelRow = PanelRow;
  298. SP.PanelBlock = PanelBlock;
  299. SP.PanelOption = PanelOption;
  300. SP.PanelOption.isOption = isOption;
  301.  
  302. // <div> element
  303. const elm = $CrE('div');
  304. copyProps(details, elm, ['id', 'name', 'className']);
  305. elm.classList.add('settingpanel-container');
  306.  
  307. // Configure object
  308. let css='', usercss='';
  309. SP.element = elm;
  310. SP.elements = {};
  311. SP.children = {};
  312. SP.tables = [];
  313. SP.length = 0;
  314. details.id !== undefined && (SP.elements[details.id] = elm);
  315. copyProps(details, SP, ['id', 'name']);
  316. Object.defineProperty(SP, 'css', {
  317. configurable: false,
  318. enumerable: true,
  319. get: function() {
  320. return css;
  321. },
  322. set: function(_css) {
  323. addStyle(_css, 'settingpanel-css');
  324. css = _css;
  325. }
  326. });
  327. Object.defineProperty(SP, 'usercss', {
  328. configurable: false,
  329. enumerable: true,
  330. get: function() {
  331. return usercss;
  332. },
  333. set: function(_usercss) {
  334. addStyle(_usercss, 'settingpanel-usercss');
  335. usercss = _usercss;
  336. }
  337. });
  338. SP.css = '.settingpanel-table {border-spacing: 0px; border-collapse: collapse; width: 100%; margin: 2em 0;} .settingpanel-block {border: 1px solid; text-align: center; vertical-align: middle; padding: 3px; text-align: left;}'
  339.  
  340. // Create tables
  341. if (details.tables) {
  342. for (const table of details.tables) {
  343. if (table instanceof PanelTable) {
  344. appendTable(table);
  345. } else {
  346. appendTable(new PanelTable(table));
  347. }
  348. }
  349. }
  350.  
  351. // Make alerity box
  352. const box = SP.alertifyBox = alertify.setpanel ? alertify.setpanel('') : alertify.alert();
  353. clearChildNodes(box.elements.content);
  354. box.elements.content.appendChild(elm);
  355. box.elements.content.style.overflow = 'auto';
  356. box.setHeader(details.header);
  357. box.setting({
  358. maximizable: true,
  359. overflow: true
  360. });
  361. !box.isOpen() && box.show();
  362.  
  363. // Insert a Panel-Row
  364. // Returns Panel object
  365. function insertTable(table, index) {
  366. // Insert table
  367. !(table instanceof PanelTable) && (table = new PanelTable(table));
  368. index < SP.length ? elm.insertBefore(table.element, elm.children[index]) : elm.appendChild(table.element);
  369. insertItem(SP.tables, table, index);
  370. table.id !== undefined && (SP.children[table.id] = table);
  371. SP.length++;
  372.  
  373. // Set parent
  374. table.parent = SP;
  375.  
  376. // Inherit elements
  377. for (const [id, subelm] of Object.entries(table.elements)) {
  378. SP.elements[id] = subelm;
  379. }
  380.  
  381. // Inherit children
  382. for (const [id, child] of Object.entries(table.children)) {
  383. SP.children[id] = child;
  384. }
  385. return SP;
  386. }
  387.  
  388. // Append a Panel-Row
  389. // Returns Panel object
  390. function appendTable(table) {
  391. return insertTable(table, SP.length);
  392. }
  393.  
  394. // Remove a Panel-Row
  395. // Returns Panel object
  396. function removeTable(index) {
  397. const table = SP.tables[index];
  398. SP.element.removeChild(table.element);
  399. removeItem(SP.rows, index);
  400. return SP;
  401. }
  402.  
  403. // Remove itself from parentElement
  404. // Returns Panel object
  405. function remove() {
  406. SP.element.parentElement && SP.parentElement.removeChild(SP.element);
  407. return SP;
  408. }
  409.  
  410. // Panel-Table object
  411. // Use 'new' keyword
  412. function PanelTable(details={}) {
  413. const PT = this;
  414. PT.insertRow = insertRow;
  415. PT.appendRow = appendRow;
  416. PT.removeRow = removeRow;
  417. PT.remove = remove
  418.  
  419. // <table> element
  420. const elm = $CrE('table');
  421. copyProps(details, elm, ['id', 'name', 'className']);
  422. elm.classList.add('settingpanel-table');
  423.  
  424. // Configure
  425. PT.element = elm;
  426. PT.elements = {};
  427. PT.children = {};
  428. PT.rows = [];
  429. PT.length = 0;
  430. details.id !== undefined && (PT.elements[details.id] = elm);
  431. copyProps(details, PT, ['id', 'name']);
  432.  
  433. // Append rows
  434. if (details.rows) {
  435. for (const row of details.rows) {
  436. if (row instanceof PanelRow) {
  437. insertRow(row);
  438. } else {
  439. insertRow(new PanelRow(row));
  440. }
  441. }
  442. }
  443.  
  444. // Insert a Panel-Row
  445. // Returns Panel-Table object
  446. function insertRow(row, index) {
  447. // Insert row
  448. !(row instanceof PanelRow) && (row = new PanelRow(row));
  449. index < PT.length ? elm.insertBefore(row.element, elm.children[index]) : elm.appendChild(row.element);
  450. insertItem(PT.rows, row, index);
  451. row.id !== undefined && (PT.children[row.id] = row);
  452. PT.length++;
  453.  
  454. // Set parent
  455. row.parent = PT;
  456.  
  457. // Inherit elements
  458. for (const [id, subelm] of Object.entries(row.elements)) {
  459. PT.elements[id] = subelm;
  460. }
  461.  
  462. // Inherit children
  463. for (const [id, child] of Object.entries(row.children)) {
  464. PT.children[id] = child;
  465. }
  466. return PT;
  467. }
  468.  
  469. // Append a Panel-Row
  470. // Returns Panel-Table object
  471. function appendRow(row) {
  472. return insertRow(row, PT.length);
  473. }
  474.  
  475. // Remove a Panel-Row
  476. // Returns Panel-Table object
  477. function removeRow(index) {
  478. const row = PT.rows[index];
  479. PT.element.removeChild(row.element);
  480. removeItem(PT.rows, index);
  481. return PT;
  482. }
  483.  
  484. // Remove itself from parentElement
  485. // Returns Panel-Table object
  486. function remove() {
  487. PT.parent instanceof SettingPanel && PT.parent.removeTable(PT.tables.indexOf(PT));
  488. return PT;
  489. }
  490. }
  491.  
  492. // Panel-Row object
  493. // Use 'new' keyword
  494. function PanelRow(details={}) {
  495. const PR = this;
  496. PR.insertBlock = insertBlock;
  497. PR.appendBlock = appendBlock;
  498. PR.removeBlock = removeBlock;
  499. PR.remove = remove;
  500.  
  501. // <tr> element
  502. const elm = $CrE('tr');
  503. copyProps(details, elm, ['id', 'name', 'className']);
  504. elm.classList.add('settingpanel-row');
  505.  
  506. // Configure object
  507. PR.element = elm;
  508. PR.elements = {};
  509. PR.children = {};
  510. PR.blocks = [];
  511. PR.length = 0;
  512. details.id !== undefined && (PR.elements[details.id] = elm);
  513. copyProps(details, PR, ['id', 'name']);
  514.  
  515. // Append blocks
  516. if (details.blocks) {
  517. for (const block of details.blocks) {
  518. if (block instanceof PanelBlock) {
  519. appendBlock(block);
  520. } else {
  521. appendBlock(new PanelBlock(block));
  522. }
  523. }
  524. }
  525.  
  526. // Insert a Panel-Block
  527. // Returns Panel-Row object
  528. function insertBlock(block, index) {
  529. // Insert block
  530. !(block instanceof PanelBlock) && (block = new PanelBlock(block));
  531. index < PR.length ? elm.insertBefore(block.element, elm.children[index]) : elm.appendChild(block.element);
  532. insertItem(PR.blocks, block, index);
  533. block.id !== undefined && (PR.children[block.id] = block);
  534. PR.length++;
  535.  
  536. // Set parent
  537. block.parent = PR;
  538.  
  539. // Inherit elements
  540. for (const [id, subelm] of Object.entries(block.elements)) {
  541. PR.elements[id] = subelm;
  542. }
  543.  
  544. // Inherit children
  545. for (const [id, child] of Object.entries(block.children)) {
  546. PR.children[id] = child;
  547. }
  548. return PR;
  549. };
  550.  
  551. // Append a Panel-Block
  552. // Returns Panel-Row object
  553. function appendBlock(block) {
  554. return insertBlock(block, PR.length);
  555. }
  556.  
  557. // Remove a Panel-Block
  558. // Returns Panel-Row object
  559. function removeBlock(index) {
  560. const block = PR.blocks[index];
  561. PR.element.removeChild(block.element);
  562. removeItem(PR.blocks, index);
  563. return PR;
  564. }
  565.  
  566. // Remove itself from parent
  567. // Returns Panel-Row object
  568. function remove() {
  569. PR.parent instanceof PanelTable && PR.parent.removeRow(PR.parent.rows.indexOf(PR));
  570. return PR;
  571. }
  572. }
  573.  
  574. // Panel-Block object
  575. // Use 'new' keyword
  576. function PanelBlock(details={}) {
  577. const PB = this;
  578. PB.remove = remove;
  579.  
  580. // <td> element
  581. const elm = $CrE(details.isHeader ? 'th' : 'td');
  582. copyProps(details, elm, ['innerText', 'innerHTML', 'colSpan', 'rowSpan', 'id', 'name', 'className']);
  583. copyProps(details, elm.style, ['width', 'height']);
  584. elm.classList.add('settingpanel-block');
  585.  
  586. // Configure object
  587. PB.element = elm;
  588. PB.elements = {};
  589. PB.children = {};
  590. details.id !== undefined && (PB.elements[details.id] = elm);
  591. copyProps(details, PB, ['id', 'name']);
  592.  
  593. // Append to parent if need
  594. details.parent instanceof PanelRow && (PB.parent = details.parent.appendBlock(PB));
  595.  
  596. // Append SettingOptions if exist
  597. if (details.options) {
  598. details.options.map((o) => (isOption(o) ? o : new SettingOption(o))).forEach(function(option) {
  599. elm.appendChild(option.element);
  600. });
  601. }
  602.  
  603. // Append child elements if exist
  604. if (details.children) {
  605. for (const child of details.children) {
  606. elm.appendChild(child);
  607. }
  608. }
  609.  
  610. // Remove itself from parent
  611. // Returns Panel-Block object
  612. function remove() {
  613. PB.parent instanceof PanelRow && PB.parent.removeBlock(PB.parent.blocks.indexOf(PB));
  614. return PB;
  615. }
  616. }
  617.  
  618. function $R(e) {return $(e) && $(e).parentElement.removeChild($(e));}
  619. function insertItem(arr, item, index) {
  620. for (let i = arr.length; i > index ; i--) {
  621. arr[i] = arr[i-1];
  622. }
  623. arr[index] = item;
  624. return arr;
  625. }
  626. function removeItem(arr, index) {
  627. for (let i = index; i < arr.length-1; i++) {
  628. arr[i] = arr[i+1];
  629. }
  630. delete arr[arr.length-1];
  631. return arr;
  632. }
  633. function MakeReadonlyObj(val) {
  634. return isObject(val) ? new Proxy(val, {
  635. get: function(target, property, receiver) {
  636. return MakeReadonlyObj(target[property]);
  637. },
  638. set: function(target, property, value, receiver) {},
  639. has: function(target, prop) {}
  640. }) : val;
  641.  
  642. function isObject(value) {
  643. return ['object', 'function'].includes(typeof value) && value !== null;
  644. }
  645. }
  646. }
  647.  
  648. // details = {path='config path', type='config type', data='option data'}
  649. function SettingOption(storage, details={}) {
  650. const SO = this;
  651. SO.save = save;
  652.  
  653. // Initialize ConfigManager
  654. !storage && Err('SettingOption requires GM_storage functions');
  655. const CM = new ConfigManager(CONST.Manager_Config_Ruleset, storage);
  656. const CONFIG = CM.Config;
  657.  
  658. // Get args
  659. const options = ['path', 'type', 'checker', 'data'];
  660. copyProps(details, SO, options);
  661.  
  662. // Create element
  663. const original_value = CM.getConfig(SO.path);
  664. const valueElement = {
  665. 'string': $CrE('input'),
  666. 'number': (() => {const e = $CrE('input'); e.type = 'number'; return e;}) (),
  667. 'boolean': (() => {const e = $CrE('input'); e.type = 'checkbox'; return e;}) (),
  668. 'select': $CrE('select')
  669. };
  670. const valueSetter = {
  671. 'string': (elm, val) => (elm.value = val),
  672. 'number': (elm, val) => (elm.value = val),
  673. 'boolean': (elm, val) => (elm.checked = val),
  674. 'select': (elm, val) => (Array.from(elm.children).find((opt) => (opt.value === val)).selected = true),
  675. };
  676. !Object.keys(valueElement).includes(SO.type) && Err('Unsupported Panel-Option type');
  677. SO.element = valueElement[SO.type];
  678. valueSetter[SO.type](SO.element, original_value);
  679.  
  680. // Bind change-checker-saver
  681. ![false, null].includes(SO.checker) && SO.element.addEventListener('change', function(e) {
  682. if (!SO.checker || SO.checker(e, SO.element.value)) {
  683. // Allows checker to modify saved value
  684. CM.setConfig(SO.path, SO.element.value);
  685. } else {
  686. valueSetter[SO.type](SO.element, original_value);
  687. }
  688. });
  689.  
  690. function save() {
  691. CM.setConfig(SO.path, SO.element.value);
  692. }
  693. }
  694.  
  695. function isOption(obj) {
  696. return obj instanceof PanelOption;
  697. }
  698. })();