SettingPanel

SettingPanel for wenku8++

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

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/450209/1085840/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.1.2
// @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
// @protect
// ==/UserScript==

(function __MAIN__() {
    'use strict';

	const alertify = require('alertify');
	const CONST = {
		Text: {},
		Manager_Config_Ruleset: {
			'version-key': 'config-version',
			'ignores': ["LOCAL-CDN"],
			'defaultValues': {
				//'config-key': {},
			}
		}
	};

	// initialize
	alertify.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(content, header, onsave, onunsave){
				this.resizeTo('80%', '80%');
				content && this.setContent(content);
				header && this.setHeader(header);
				content && this.set('onsave', onsave);
				header && this.set('onunsave', onunsave);
				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: true,

						/* 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
					}],

					/* 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 shown
			prepare: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.
				switch (closeEvent.index) {
					case 0: {
						const onunsave = this.get('onunsave');
						typeof onunsave === 'function' && onunsave();
						//alertify.alert('Not saved');
						break;
					}
					case 1: {
						const onsave = this.get('onsave');
						typeof onsave === 'function' && onsave();
						//alertify.alert('Saved');
					}
				}
			},
			// To make use of AlertifyJS settings API, group your custom settings into a settings object.
			settings:{
				onsave: function() {},
				onunsave: function() {}
			},
			// AlertifyJS will invoke this each time a settings value gets updated.
			settingUpdated:function(key, oldValue, newValue){/*
				// Use this to respond to specific setting updates.
				switch(key){
					case 'myProp':
						// Do something when 'myProp' changes
						break;
				}
			*/},
			// listen to internal dialog events.
			hooks:{
				// triggered when the dialog is shown, this is seperate from user defined onshow
				onshow: function(){
					this.resizeTo('80%', '80%');
				},
				// triggered when the dialog is closed, this is seperate from user defined onclose
				onclose: function(){
				},
				// triggered when a dialog option gets updated.
				// IMPORTANT: This will not be triggered for dialog custom settings updates ( use settingUpdated instead).
				onupdate: function(){
				}
			}
		}
	}, true);
	SettingPanel.SettingOption = SettingOption;

	exports = {
		SettingPanel: SettingPanel,
		SettingOption: SettingOption,
		isOption: isOption
	};

	// A table-based setting panel using alertify-js
	// For wenku8++ only version
	// Use 'new' keyword
	// Usage:
	/*
		var panel = new SettingPanel({
			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={}) {
		const SP = this;
		SP.insertTable = insertTable;
		SP.appendTable = appendTable;
		SP.removeTable = removeTable;
		SP.remove = remove;
		SP.PanelTable = PanelTable;
		SP.PanelRow = PanelRow;
		SP.PanelBlock = PanelBlock;
		SP.PanelOption = PanelOption;
		SP.PanelOption.isOption = isOption;

		// <div> element
		const elm = $CrE('div');
		copyProps(details, elm, ['id', 'name', 'className']);
		elm.classList.add('settingpanel-container');

		// Configure object
		let 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; text-align: center; vertical-align: middle; padding: 3px; text-align: left;}'

		// Create tables
		if (details.tables) {
			for (const table of details.tables) {
				if (table instanceof PanelTable) {
					appendTable(table);
				} else {
					appendTable(new PanelTable(table));
				}
			}
		}

		// Make alerity box
		const box = SP.alertifyBox = alertify.setpanel ? alertify.setpanel('') : alertify.alert();
		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();

		// Insert a Panel-Row
		// Returns Panel object
		function 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 parent
			table.parent = SP;

			// Inherit elements
			for (const [id, subelm] of Object.entries(table.elements)) {
				SP.elements[id] = subelm;
			}

			// Inherit children
			for (const [id, child] of Object.entries(table.children)) {
				SP.children[id] = child;
			}
			return SP;
		}

		// Append a Panel-Row
		// Returns Panel object
		function appendTable(table) {
			return insertTable(table, SP.length);
		}

		// Remove a Panel-Row
		// Returns Panel object
		function removeTable(index) {
			const table = SP.tables[index];
			SP.element.removeChild(table.element);
			removeItem(SP.rows, index);
			return SP;
		}

		// Remove itself from parentElement
		// Returns Panel object
		function remove() {
			SP.element.parentElement && SP.parentElement.removeChild(SP.element);
			return SP;
		}

		// Panel-Table object
		// Use 'new' keyword
		function PanelTable(details={}) {
			const PT = this;
			PT.insertRow = insertRow;
			PT.appendRow = appendRow;
			PT.removeRow = removeRow;
			PT.remove = remove

			// <table> element
			const elm = $CrE('table');
			copyProps(details, elm, ['id', 'name', 'className']);
			elm.classList.add('settingpanel-table');

			// Configure
			PT.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 rows
			if (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 object
			function 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 parent
				row.parent = PT;

				// Inherit elements
				for (const [id, subelm] of Object.entries(row.elements)) {
					PT.elements[id] = subelm;
				}

				// Inherit children
				for (const [id, child] of Object.entries(row.children)) {
					PT.children[id] = child;
				}
				return PT;
			}

			// Append a Panel-Row
			// Returns Panel-Table object
			function appendRow(row) {
				return insertRow(row, PT.length);
			}

			// Remove a Panel-Row
			// Returns Panel-Table object
			function 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 object
			function remove() {
				PT.parent instanceof SettingPanel && PT.parent.removeTable(PT.tables.indexOf(PT));
				return PT;
			}
		}

		// Panel-Row object
		// Use 'new' keyword
		function PanelRow(details={}) {
			const PR = this;
			PR.insertBlock = insertBlock;
			PR.appendBlock = appendBlock;
			PR.removeBlock = removeBlock;
			PR.remove = remove;

			// <tr> element
			const elm = $CrE('tr');
			copyProps(details, elm, ['id', 'name', 'className']);
			elm.classList.add('settingpanel-row');

			// Configure object
			PR.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 blocks
			if (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 object
			function 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 parent
				block.parent = PR;

				// Inherit elements
				for (const [id, subelm] of Object.entries(block.elements)) {
					PR.elements[id] = subelm;
				}

				// Inherit children
				for (const [id, child] of Object.entries(block.children)) {
					PR.children[id] = child;
				}
				return PR;
			};

			// Append a Panel-Block
			// Returns Panel-Row object
			function appendBlock(block) {
				return insertBlock(block, PR.length);
			}

			// Remove a Panel-Block
			// Returns Panel-Row object
			function 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 object
			function remove() {
				PR.parent instanceof PanelTable && PR.parent.removeRow(PR.parent.rows.indexOf(PR));
				return PR;
			}
		}

		// Panel-Block object
		// Use 'new' keyword
		function PanelBlock(details={}) {
			const PB = this;
			PB.remove = remove;

			// <td> element
			const 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');

			// Configure object
			PB.element = elm;
			PB.elements = {};
			PB.children = {};
			details.id !== undefined && (PB.elements[details.id] = elm);
			copyProps(details, PB, ['id', 'name']);

			// Append to parent if need
			details.parent instanceof PanelRow && (PB.parent = details.parent.appendBlock(PB));

			// Append SettingOptions if exist
			if (details.options) {
				details.options.map((o) => (isOption(o) ? o : new SettingOption(o))).forEach(function(option) {
					elm.appendChild(option.element);
				});
			}

			// Append child elements if exist
			if (details.children) {
				for (const child of details.children) {
					elm.appendChild(child);
				}
			}

			// Remove itself from parent
			// Returns Panel-Block object
			function 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;

		// Initialize ConfigManager
		!storage && Err('SettingOption requires GM_storage functions');
		const CM = new ConfigManager(CONST.Manager_Config_Ruleset, storage);
		const CONFIG = CM.Config;

		// Get args
		const options = ['path', 'type', 'checker', 'data'];
		copyProps(details, SO, options);

		// Create element
		const original_value = CM.getConfig(SO.path);
		const valueElement = {
			'string': $CrE('input'),
			'number': (() => {const e = $CrE('input'); e.type = 'number'; return e;}) (),
			'boolean': (() => {const e = $CrE('input'); e.type = 'checkbox'; return e;}) (),
			'select': $CrE('select')
		};
		const valueSetter = {
			'string': (elm, val) => (elm.value = val),
			'number': (elm, val) => (elm.value = val),
			'boolean': (elm, val) => (elm.checked = val),
			'select': (elm, val) => (Array.from(elm.children).find((opt) => (opt.value === val)).selected = true),
		};
		!Object.keys(valueElement).includes(SO.type) && Err('Unsupported Panel-Option type');
		SO.element = valueElement[SO.type];
		valueSetter[SO.type](SO.element, original_value);

		// Bind change-checker-saver
		![false, null].includes(SO.checker) && SO.element.addEventListener('change', function(e) {
			if (!SO.checker || SO.checker(e, SO.element.value)) {
				// Allows checker to modify saved value
				CM.setConfig(SO.path, SO.element.value);
			} else {
				valueSetter[SO.type](SO.element, original_value);
			}
		});

		function save() {
			CM.setConfig(SO.path, SO.element.value);
		}
	}

	function isOption(obj) {
		return obj instanceof PanelOption;
	}
})();