SettingPanel

SettingPanel for wenku8++

Dit script moet niet direct worden geïnstalleerd - het is een bibliotheek voor andere scripts om op te nemen met de meta-richtlijn // @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;},
		}
	}

	// 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(){
				// Split arguments
				let 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 dialog
				this.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 groups
				const btnGroups = {
					// Close button only
					basic: [[1, 0]],

					// Save & reset button
					saver: [[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 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.
				const myEvent = deepClone(closeEvent);
				switch (closeEvent.index) {
					case 0: {
						// Rests button
						closeEvent.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 need
						break;
					}
					case 2: {
						// Save button
						closeEvent.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 array
				saver: 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 onshow
				onshow: function() {
					this.resizeTo('80%', '80%');
				},
				// triggered when the dialog is closed, this is seperate from user defined onclose
				onclose: 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> 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 ${ASSETS.Color.Text}; text-align: center; vertical-align: middle; padding: 3px; text-align: left;} .settingpanel-header {font-weight: bold;}`

		// Make alerity box
		const 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 tables
		if (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 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');
			details.isHeader && elm.classList.add('settingpanel-header');

			// 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.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 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;
		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 args
		const options = ['path', 'type', 'checker', 'data', 'autoSave'];
		copyProps(details, SO, options);

		// Get first available type if multiple types provided
		Array.isArray(SO.type) && (SO.type = SO.type.find((t) => (optionAvailable(t))));
		!optionAvailable(SO.type) && Err('Unsupported Panel-Option type: ' + details.type);

		// Create element
		const 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-saver
		SO.element.addEventListener('change', function(e) {
			if (SO.checker) {
				if (SO.checker(e, SOE.get())) {
					SO.autoSave && save();
				} else {
					// Reset value
					reset();

					// 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 available
	function optionAvailable(type) {
		return Object.keys(SettingOptionElements).includes(type);
	}

	// Register SettingOption element
	function 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 object
	function 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;
	}
})();