Greasy Fork is available in English.
SettingPanel for wenku8++
Ce script ne doit pas être installé directement. C'est une librairie destinée à être incluse dans d'autres scripts avec la méta-directive // @require https://update.greasyfork.org/scripts/450209/1091923/SettingPanel.js
/* eslint-disable no-multi-spaces *//* eslint-disable no-implicit-globals *//* eslint-disable userscripts/no-invalid-headers *//* eslint-disable userscripts/no-invalid-grant */// ==UserScript==// @name SettingPanel// @displayname SettingPanel// @namespace Wenku8++// @version 0.3.9// @description SettingPanel for wenku8++// @author PY-DNG// @license GPL-v3// @regurl NONE// @require https://greasyfork.org/scripts/449412-basic-functions/code/Basic%20Functions.js?version=1085783// @require https://greasyfork.org/scripts/449583-configmanager/code/ConfigManager.js?version=1085836// @grant none// ==/UserScript==/*计划任务:[x] 表格线换成蓝色的[x] 允许使用不同的alertify对话框[x] 点击按钮的时候要有GUI反馈[ ] 未保存内容,关闭窗口前要有提示[ ] 提供注册SettingOptions组件的接口*/(function __MAIN__() {'use strict';const ASSETS = require('assets');const alertify = require('alertify');const CONST = {Text: {Saved: '已保存',Reset: '已恢复到修改前'},Manager_Config_Ruleset: {'version-key': 'config-version','ignores': ["LOCAL-CDN"],'defaultValues': {//'config-key': {},}}};const SettingOptionElements = {'string': {createElement: function() {const e = $CrE('input'); e.style.width = '90%'; return e;},setValue: function(val) {this.element.value = val;},getValue: function() {return this.element.value;},},'number': {createElement: function() {const e = $CrE('input'); e.type = 'number'; e.style.width = '90%'; return e;},setValue: function (val) {this.element.value = val;},getValue: function() {return this.element.value;},},'boolean': {createElement: function() {const e = $CrE('input'); e.type = 'checkbox'; return e;},setValue: function(val) {this.element.checked = val;},getValue: function(data) {return this.element.checked ? (this.hasOwnProperty('data') ? this.data.checked : true) : (data ? this.data.unchecked : false);},},'select': {createElement: (() => {const e = $CrE('select'); this.hasOwnProperty('data') && this.data.forEach((d) => {const o = $CrE('option'); o.innerText = d; e.appendChild(o)}); return e;}),setValue: (val) => (Array.from(this.element.children).find((opt) => (opt.value === val)).selected = true),getValue: function() {return this.element.value;},}}// initializealertify.dialog('setpanel', function factory(){return {// The dialog startup function// This will be called each time the dialog is invoked// For example: alertify.myDialog( data );main:function(){// Split argumentslet content, header, buttons, onsave, onreset, onclose;switch (arguments.length) {case 1:switch (typeof arguments[0]) {case 'string':content = arguments[0];break;case 'object':arguments[0].hasOwnProperty('content') && (content = arguments[0].content);arguments[0].hasOwnProperty('header') && (header = arguments[0].header);arguments[0].hasOwnProperty('buttons') && (buttons = arguments[0].buttons);arguments[0].hasOwnProperty('onsave') && (onsave = arguments[0].onsave);arguments[0].hasOwnProperty('onreset') && (onreset = arguments[0].onreset);arguments[0].hasOwnProperty('onclose') && (buttons = arguments[0].onclose);break;default:Err('Arguments invalid', 1);}break;case 2:content = arguments[0];header = arguments[1];break;case 3:content = arguments[0];header = arguments[1];buttons = buttons[2];break;}// Prepare dialogthis.resizeTo('80%', '80%');content && this.setContent(content);header && this.setHeader(header);onsave && this.set('onsave', onsave);onreset && this.set('onreset', onreset);onclose && this.set('onclose', onclose);// Choose & show selected button groupsconst btnGroups = {// Close button onlybasic: [[1, 0]],// Save & reset buttonsaver: [[0, 0], [1, 1]]};const group = btnGroups[buttons || 'basic'];const divs = ['auxiliary', 'primary'];divs.forEach((div) => {Array.from(this.elements.buttons[div].children).forEach((btn) => {btn.style.display = 'none';});});group.forEach((button) => {this.elements.buttons[divs[button[0]]].children[button[1]].style.display = '';});return this;},// The dialog setup function// This should return the dialog setup object ( buttons, focus and options overrides ).setup:function(){return {/* buttons collection */buttons:[{/* button label */text: '恢复到修改前',/*bind a keyboard key to the button */key: undefined,/* indicate if closing the dialog should trigger this button action */invokeOnClose: false,/* custom button class name */className: alertify.defaults.theme.cancel,/* custom button attributes */attrs: {},/* Defines the button scope, either primary (default) or auxiliary */scope:'auxiliary',/* The will conatin the button DOMElement once buttons are created */element: undefined},{/* button label */text: '关闭',/*bind a keyboard key to the button */key: undefined,/* indicate if closing the dialog should trigger this button action */invokeOnClose: false,/* custom button class name */className: alertify.defaults.theme.ok,/* custom button attributes */attrs: {},/* Defines the button scope, either primary (default) or auxiliary */scope:'primary',/* The will conatin the button DOMElement once buttons are created */element: undefined},{/* button label */text: '保存',/*bind a keyboard key to the button */key: undefined,/* indicate if closing the dialog should trigger this button action */invokeOnClose: false,/* custom button class name */className: alertify.defaults.theme.ok,/* custom button attributes */attrs: {},/* Defines the button scope, either primary (default) or auxiliary */scope:'primary',/* The will conatin the button DOMElement once buttons are created */element: undefined}],/* default focus */focus:{/* the element to receive default focus, has differnt meaning based on value type:number: action button index.string: querySelector to select from dialog body contents.function: when invoked, should return the focus element.DOMElement: the focus element.object: an object that implements .focus() and .select() functions.*/element: 0,/* indicates if the element should be selected on focus or not*/select: true},/* dialog options, these override the defaults */options: {title: 'Setting Panel',modal: true,basic: false,frameless: false,pinned: false,movable: true,moveBounded: false,resizable: true,autoReset: false,closable: true,closableByDimmer: true,maximizable: false,startMaximized: false,pinnable: false,transition: 'fade',padding: true,overflow: true,/*onshow:...,onclose:...,onfocus:...,onmove:...,onmoved:...,onresize:...,onresized:...,onmaximize:...,onmaximized:...,onrestore:...,onrestored:...*/}};},// This will be called once the dialog DOM has been created, just before its added to the document.// Its invoked only once.build:function(){// Do custom DOM manipulation here, accessible via this.elements// this.elements.root ==> Root div// this.elements.dimmer ==> Modal dimmer div// this.elements.modal ==> Modal div (dialog wrapper)// this.elements.dialog ==> Dialog div// this.elements.reset ==> Array containing the tab reset anchor links// this.elements.reset[0] ==> First reset element (button).// this.elements.reset[1] ==> Second reset element (button).// this.elements.header ==> Dialog header div// this.elements.body ==> Dialog body div// this.elements.content ==> Dialog body content div// this.elements.footer ==> Dialog footer div// this.elements.resizeHandle ==> Dialog resize handle div// Dialog commands (Pin/Maximize/Close)// this.elements.commands ==> Object containing dialog command buttons references// this.elements.commands.container ==> Root commands div// this.elements.commands.pin ==> Pin command button// this.elements.commands.maximize ==> Maximize command button// this.elements.commands.close ==> Close command button// Dialog action buttons (Ok, cancel ... etc)// this.elements.buttons ==> Object containing dialog action buttons references// this.elements.buttons.primary ==> Primary buttons div// this.elements.buttons.auxiliary ==> Auxiliary buttons div// Each created button will be saved with the button definition inside buttons collection// this.__internal.buttons[x].element},// This will be called each time the dialog is shownprepare:function(){// Do stuff that should be done every time the dialog is shown.},// This will be called each time an action button is clicked.callback:function(closeEvent){//The closeEvent has the following properties//// index: The index of the button triggering the event.// button: The button definition object.// cancel: When set true, prevent the dialog from closing.const myEvent = deepClone(closeEvent);switch (closeEvent.index) {case 0: {// Rests buttoncloseEvent.cancel = myEvent.cancel = true;myEvent.save = false;myEvent.reset = true;const onreset = this.get('onreset');typeof onreset === 'function' && onreset(myEvent);break;}case 1: {// Close button// Do something here if needbreak;}case 2: {// Save buttoncloseEvent.cancel = myEvent.cancel = true;myEvent.save = true;myEvent.reset = false;const onsave = this.get('onsave');typeof onsave === 'function' && onsave(myEvent);}}this.get(myEvent.save ? 'saver' : 'reseter').call(this);closeEvent.cancel = myEvent.cancel;},// To make use of AlertifyJS settings API, group your custom settings into a settings object.settings:{onsave: function() {},onreset: function() {},options: [], // SettingOption arraysaver: function() {this.get('options').forEach((o) => (o.save()));},reseter: function() {this.get('options').forEach((o) => (o.reset()));}},// AlertifyJS will invoke this each time a settings value gets updated.settingUpdated:function(key, oldValue, newValue){// Use this to respond to specific setting updates.const _this = this;['onsave', 'onreset', 'saver', 'reseter'].includes(key) && check('function');['options'].includes(key) && check(Array);function rollback() {_this.set(key, oldValue);}function check(type) {valid(oldValue, type) && !valid(newValue, type) && rollback();}function valid(value, type) {return ({'string': () => (typeof value === type),'function': () => (value instanceof type)})[typeof type]();}},// listen to internal dialog events.hooks:{// triggered when the dialog is shown, this is seperate from user defined onshowonshow: function() {this.resizeTo('80%', '80%');},// triggered when the dialog is closed, this is seperate from user defined oncloseonclose: function() {const onclose = this.get('onclose');typeof onclose === 'function' && onclose();},// triggered when a dialog option gets updated.// IMPORTANT: This will not be triggered for dialog custom settings updates ( use settingUpdated instead).onupdate: function() {}}}}, true);exports = {SettingPanel: SettingPanel,SettingOption: SettingOption,optionAvailable: optionAvailable,isOption: isOption,registerElement: registerElement,};// A table-based setting panel using alertify-js// For wenku8++ only version// Use 'new' keyword// Usage:/*var panel = new SettingPanel({buttons: 0,header: '',className: '',id: '',name: '',tables: [{className: '',id: '',name: '',rows: [{className: '',id: '',name: '',blocks: [{isHeader: false,width: '',height: '',innerHTML / innerText: ''colSpan: 1,rowSpan: 1,className: '',id: '',name: '',options: [SettingOption, ...]children: [HTMLElement, ...]},...]},...]},...]});*/function SettingPanel(details={}, storage) {const SP = this;SP.insertTable = insertTable;SP.appendTable = appendTable;SP.removeTable = removeTable;SP.remove = remove;SP.PanelTable = PanelTable;SP.PanelRow = PanelRow;SP.PanelBlock = PanelBlock;// <div> elementconst elm = $CrE('div');copyProps(details, elm, ['id', 'name', 'className']);elm.classList.add('settingpanel-container');// Configure objectlet css='', usercss='';SP.element = elm;SP.elements = {};SP.children = {};SP.tables = [];SP.length = 0;details.id !== undefined && (SP.elements[details.id] = elm);copyProps(details, SP, ['id', 'name']);Object.defineProperty(SP, 'css', {configurable: false,enumerable: true,get: function() {return css;},set: function(_css) {addStyle(_css, 'settingpanel-css');css = _css;}});Object.defineProperty(SP, 'usercss', {configurable: false,enumerable: true,get: function() {return usercss;},set: function(_usercss) {addStyle(_usercss, 'settingpanel-usercss');usercss = _usercss;}});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;}`// Make alerity boxconst box = SP.alertifyBox = alertify.setpanel({onsave: function() {alertify.notify(CONST.Text.Saved);},onreset: function() {alertify.notify(CONST.Text.Reset);},buttons: details.hasOwnProperty('buttons') ? details.buttons : 'basic'});clearChildNodes(box.elements.content);box.elements.content.appendChild(elm);box.elements.content.style.overflow = 'auto';box.setHeader(details.header);box.setting({maximizable: true,overflow: true});!box.isOpen() && box.show();// Create tablesif (details.tables) {for (const table of details.tables) {if (table instanceof PanelTable) {appendTable(table);} else {appendTable(new PanelTable(table));}}}// Insert a Panel-Row// Returns Panel objectfunction insertTable(table, index) {// Insert table!(table instanceof PanelTable) && (table = new PanelTable(table));index < SP.length ? elm.insertBefore(table.element, elm.children[index]) : elm.appendChild(table.element);insertItem(SP.tables, table, index);table.id !== undefined && (SP.children[table.id] = table);SP.length++;// Set parenttable.parent = SP;// Inherit elementsfor (const [id, subelm] of Object.entries(table.elements)) {SP.elements[id] = subelm;}// Inherit childrenfor (const [id, child] of Object.entries(table.children)) {SP.children[id] = child;}return SP;}// Append a Panel-Row// Returns Panel objectfunction appendTable(table) {return insertTable(table, SP.length);}// Remove a Panel-Row// Returns Panel objectfunction removeTable(index) {const table = SP.tables[index];SP.element.removeChild(table.element);removeItem(SP.rows, index);return SP;}// Remove itself from parentElement// Returns Panel objectfunction remove() {SP.element.parentElement && SP.parentElement.removeChild(SP.element);return SP;}// Panel-Table object// Use 'new' keywordfunction PanelTable(details={}) {const PT = this;PT.insertRow = insertRow;PT.appendRow = appendRow;PT.removeRow = removeRow;PT.remove = remove// <table> elementconst elm = $CrE('table');copyProps(details, elm, ['id', 'name', 'className']);elm.classList.add('settingpanel-table');// ConfigurePT.element = elm;PT.elements = {};PT.children = {};PT.rows = [];PT.length = 0;details.id !== undefined && (PT.elements[details.id] = elm);copyProps(details, PT, ['id', 'name']);// Append rowsif (details.rows) {for (const row of details.rows) {if (row instanceof PanelRow) {insertRow(row);} else {insertRow(new PanelRow(row));}}}// Insert a Panel-Row// Returns Panel-Table objectfunction insertRow(row, index) {// Insert row!(row instanceof PanelRow) && (row = new PanelRow(row));index < PT.length ? elm.insertBefore(row.element, elm.children[index]) : elm.appendChild(row.element);insertItem(PT.rows, row, index);row.id !== undefined && (PT.children[row.id] = row);PT.length++;// Set parentrow.parent = PT;// Inherit elementsfor (const [id, subelm] of Object.entries(row.elements)) {PT.elements[id] = subelm;}// Inherit childrenfor (const [id, child] of Object.entries(row.children)) {PT.children[id] = child;}return PT;}// Append a Panel-Row// Returns Panel-Table objectfunction appendRow(row) {return insertRow(row, PT.length);}// Remove a Panel-Row// Returns Panel-Table objectfunction removeRow(index) {const row = PT.rows[index];PT.element.removeChild(row.element);removeItem(PT.rows, index);return PT;}// Remove itself from parentElement// Returns Panel-Table objectfunction remove() {PT.parent instanceof SettingPanel && PT.parent.removeTable(PT.tables.indexOf(PT));return PT;}}// Panel-Row object// Use 'new' keywordfunction PanelRow(details={}) {const PR = this;PR.insertBlock = insertBlock;PR.appendBlock = appendBlock;PR.removeBlock = removeBlock;PR.remove = remove;// <tr> elementconst elm = $CrE('tr');copyProps(details, elm, ['id', 'name', 'className']);elm.classList.add('settingpanel-row');// Configure objectPR.element = elm;PR.elements = {};PR.children = {};PR.blocks = [];PR.length = 0;details.id !== undefined && (PR.elements[details.id] = elm);copyProps(details, PR, ['id', 'name']);// Append blocksif (details.blocks) {for (const block of details.blocks) {if (block instanceof PanelBlock) {appendBlock(block);} else {appendBlock(new PanelBlock(block));}}}// Insert a Panel-Block// Returns Panel-Row objectfunction insertBlock(block, index) {// Insert block!(block instanceof PanelBlock) && (block = new PanelBlock(block));index < PR.length ? elm.insertBefore(block.element, elm.children[index]) : elm.appendChild(block.element);insertItem(PR.blocks, block, index);block.id !== undefined && (PR.children[block.id] = block);PR.length++;// Set parentblock.parent = PR;// Inherit elementsfor (const [id, subelm] of Object.entries(block.elements)) {PR.elements[id] = subelm;}// Inherit childrenfor (const [id, child] of Object.entries(block.children)) {PR.children[id] = child;}return PR;};// Append a Panel-Block// Returns Panel-Row objectfunction appendBlock(block) {return insertBlock(block, PR.length);}// Remove a Panel-Block// Returns Panel-Row objectfunction removeBlock(index) {const block = PR.blocks[index];PR.element.removeChild(block.element);removeItem(PR.blocks, index);return PR;}// Remove itself from parent// Returns Panel-Row objectfunction remove() {PR.parent instanceof PanelTable && PR.parent.removeRow(PR.parent.rows.indexOf(PR));return PR;}}// Panel-Block object// Use 'new' keywordfunction PanelBlock(details={}) {const PB = this;PB.remove = remove;// <td> elementconst elm = $CrE(details.isHeader ? 'th' : 'td');copyProps(details, elm, ['innerText', 'innerHTML', 'colSpan', 'rowSpan', 'id', 'name', 'className']);copyProps(details, elm.style, ['width', 'height']);elm.classList.add('settingpanel-block');details.isHeader && elm.classList.add('settingpanel-header');// Configure objectPB.element = elm;PB.elements = {};PB.children = {};details.id !== undefined && (PB.elements[details.id] = elm);copyProps(details, PB, ['id', 'name']);// Append to parent if needdetails.parent instanceof PanelRow && (PB.parent = details.parent.appendBlock(PB));// Append SettingOptions if existif (details.options) {details.options.filter(storage ? () => (true) : isOption).map((o) => (isOption(o) ? o : new SettingOption(storage, o))).forEach(function(option) {SP.alertifyBox.get('options').push(option);elm.appendChild(option.element);});}// Append child elements if existif (details.children) {for (const child of details.children) {elm.appendChild(child);}}// Remove itself from parent// Returns Panel-Block objectfunction remove() {PB.parent instanceof PanelRow && PB.parent.removeBlock(PB.parent.blocks.indexOf(PB));return PB;}}function $R(e) {return $(e) && $(e).parentElement.removeChild($(e));}function insertItem(arr, item, index) {for (let i = arr.length; i > index ; i--) {arr[i] = arr[i-1];}arr[index] = item;return arr;}function removeItem(arr, index) {for (let i = index; i < arr.length-1; i++) {arr[i] = arr[i+1];}delete arr[arr.length-1];return arr;}function MakeReadonlyObj(val) {return isObject(val) ? new Proxy(val, {get: function(target, property, receiver) {return MakeReadonlyObj(target[property]);},set: function(target, property, value, receiver) {},has: function(target, prop) {}}) : val;function isObject(value) {return ['object', 'function'].includes(typeof value) && value !== null;}}}// details = {path='config path', type='config type', data='option data'}function SettingOption(storage, details={}) {const SO = this;SO.save = save;SO.reset = reset;// Initialize ConfigManager!storage && Err('SettingOption requires GM_storage functions');const CM = new ConfigManager(CONST.Manager_Config_Ruleset, storage);const CONFIG = CM.Config;// Get argsconst options = ['path', 'type', 'checker', 'data', 'autoSave'];copyProps(details, SO, options);// Get first available type if multiple types providedArray.isArray(SO.type) && (SO.type = SO.type.find((t) => (optionAvailable(t))));!optionAvailable(SO.type) && Err('Unsupported Panel-Option type: ' + details.type);// Create elementconst original_value = CM.getConfig(SO.path);const SOE = {create: SettingOptionElements[SO.type].createElement.bind(SO),get: SettingOptionElements[SO.type].getValue.bind(SO),set: SettingOptionElements[SO.type].setValue.bind(SO),}SO.element = SOE.create();SOE.set(original_value);// Bind change-checker-saverSO.element.addEventListener('change', function(e) {if (SO.checker) {if (SO.checker(e, SOE.get())) {SO.autoSave && save();} else {// Reset valuereset();// Do some value-invalid reminding here}} else {SO.autoSave && save();}});function save() {CM.setConfig(SO.path, SOE.get());}function reset(save=false) {SOE.set(original_value);save && CM.setConfig(SO.path, original_value);}}// Check if an settingoption type availablefunction optionAvailable(type) {return Object.keys(SettingOptionElements).includes(type);}// Register SettingOption elementfunction registerElement(name, obj) {const formatOkay = typeof obj.createElement === 'function' && typeof obj.setValue === 'function' && typeof obj.getValue === 'function';const noConflict = !SettingOptionElements.hasOwnProperty(name);const okay = formatOkay && noConflict;okay && (SettingOptionElements[name] = obj);return okay;}function isOption(obj) {return obj instanceof SettingOption;}// Deep copy an objectfunction deepClone(obj) {let newObj = Array.isArray(obj) ? [] : {};if (obj && typeof obj === "object") {for (let key in obj) {if (obj.hasOwnProperty(key)) {newObj[key] = (obj && typeof obj[key] === 'object') ? deepClone(obj[key]) : obj[key];}}}return newObj;}})();