Skip to content
Snippets Groups Projects
rulesAssistantOptions.js 112 KiB
Newer Older
  • Learn to ignore specific revisions
  • brickode's avatar
    brickode committed
    /* eslint-disable camelcase */
    
    brickode's avatar
    brickode committed
    /* eslint-disable no-unused-vars */
    
    vas's avatar
    vas committed
    // rewrite of the rules assistant options page in javascript
    // uses an object-oriented widget pattern
    
    klorpa's avatar
    klorpa committed
    // wrapped in a closure so as not to pollute the global namespace
    
    vas's avatar
    vas committed
    // the widgets are generic enough to be reusable; if similar user interfaces are ported to JS, we could move the classes to the global scope
    
    vas's avatar
    vas committed
    
    
    brickode's avatar
    brickode committed
    window.rulesAssistantOptions = (function() {
    
    vas's avatar
    vas committed
    	"use strict";
    
    	const noDefaultSetting = {value: "!NDS!", text: "no default setting"};
    
    ezsh's avatar
    ezsh committed
    	let V;
    	/** @type {App.RA.Rule} */
    	let current_rule;
    
    vas's avatar
    vas committed
    
    
    vas's avatar
    vas committed
    	function rulesAssistantOptions(element) {
    
    vas's avatar
    vas committed
    		V = State.variables;
    		V.nextButton = "Back to Main";
    		V.nextLink = "Main";
    		V.returnTo = "Main";
    		V.showEncyclopedia = 1;
    		V.encyclopedia = "Personal Assistant";
    
    		if (V.currentRule !== null) {
    			const idx = V.defaultRules.findIndex(rule => rule.ID === V.currentRule);
    
    DCoded's avatar
    DCoded committed
    			if (idx === -1) {
    
    				current_rule = V.defaultRules[0];
    
    DCoded's avatar
    DCoded committed
    			} else {
    
    				current_rule = V.defaultRules[idx];
    
    DCoded's avatar
    DCoded committed
    			}
    
    brickode's avatar
    brickode committed
    		const root = new Root(element);
    
    vas's avatar
    vas committed
    	}
    
    
    	function returnP(e) { return e.keyCode === 13; }
    
    vas's avatar
    vas committed
    
    
    vas's avatar
    vas committed
    	function newRule(root) {
    
    		const rule = emptyDefaultRule();
    		V.defaultRules.push(rule);
    
    vas's avatar
    vas committed
    		V.currentRule = rule.ID;
    
    vas's avatar
    vas committed
    		reload(root);
    
    vas's avatar
    vas committed
    	}
    
    	function removeRule(root) {
    
    vas's avatar
    vas committed
    		const idx = V.defaultRules.findIndex(rule => rule.ID === current_rule.ID);
    
    vas's avatar
    vas committed
    		V.defaultRules.splice(idx, 1);
    
    		if (V.defaultRules.length > 0) {
    			const new_idx = idx < V.defaultRules.length ? idx : V.defaultRules.length - 1;
    			V.currentRule = V.defaultRules[new_idx].ID;
    
    DCoded's avatar
    DCoded committed
    		} else {
    			V.currentRule = null;
    		}
    
    vas's avatar
    vas committed
    		reload(root);
    
    vas's avatar
    vas committed
    	}
    
    
    vas's avatar
    vas committed
    	function lowerPriority(root) {
    
    DCoded's avatar
    DCoded committed
    		if (V.defaultRules.length === 1) { return; } // nothing to swap with
    
    vas's avatar
    vas committed
    		const idx = V.defaultRules.findIndex(rule => rule.ID === current_rule.ID);
    
    DCoded's avatar
    DCoded committed
    		if (idx === 0) { return; } // no lower rule
    
    brickode's avatar
    brickode committed
    		arraySwap(V.defaultRules, idx, idx - 1);
    
    vas's avatar
    vas committed
    		reload(root);
    
    vas's avatar
    vas committed
    	}
    
    	function higherPriority(root) {
    
    DCoded's avatar
    DCoded committed
    		if (V.defaultRules.length === 1) { return; } // nothing to swap with
    
    vas's avatar
    vas committed
    		const idx = V.defaultRules.findIndex(rule => rule.ID === current_rule.ID);
    
    DCoded's avatar
    DCoded committed
    		if (idx === V.defaultRules.length - 1) { return; } // no higher rule
    
    brickode's avatar
    brickode committed
    		arraySwap(V.defaultRules, idx, idx + 1);
    
    vas's avatar
    vas committed
    		reload(root);
    
    vas's avatar
    vas committed
    	}
    
    	function changeName(name, root) {
    
    DCoded's avatar
    DCoded committed
    		if (name === current_rule.name) { return; }
    
    vas's avatar
    vas committed
    		current_rule.name = name;
    
    vas's avatar
    vas committed
    		reload(root);
    
    vas's avatar
    vas committed
    	}
    
    	// reload the passage
    
    vas's avatar
    vas committed
    	function reload(root) {
    
    vas's avatar
    vas committed
    		const elem = root.element;
    
    vas's avatar
    vas committed
    		elem.innerHTML = "";
    
    vas's avatar
    vas committed
    		rulesAssistantOptions(elem);
    
    vas's avatar
    vas committed
    	}
    
    
    vas's avatar
    vas committed
    	const parse = {
    		integer(string) {
    			let n = parseInt(string, 10);
    
    brickode's avatar
    brickode committed
    			return isNaN(n) ? 0 : n;
    
    vas's avatar
    vas committed
    		},
    		boobs(string) {
    			return Math.clamp(parse.integer(string), 0, 48000);
    		},
    		butt(string) {
    
    x's avatar
    x committed
    			return Math.clamp(parse.integer(string), 0, 20);
    
    vas's avatar
    vas committed
    		},
    		lips(string) {
    			return Math.clamp(parse.integer(string), 0, 100);
    		},
    		dick(string) {
    
    x's avatar
    x committed
    			return Math.clamp(parse.integer(string), 0, 30);
    
    vas's avatar
    vas committed
    		},
    		balls(string) {
    
    x's avatar
    x committed
    			return Math.clamp(parse.integer(string), 0, 125);
    
    vas's avatar
    vas committed
    		},
    	};
    
    
    vas's avatar
    vas committed
    	// the Element class wraps around a DOM element and adds extra functionality
    	// this is safer than extending DOM objects directly
    	// it also turns DOM manipulation into an implementation detail
    	class Element {
    		constructor(...args) {
    
    vas's avatar
    vas committed
    			this.parent = null;
    			this.element = this.render(...args);
    			this.children = [];
    
    vas's avatar
    vas committed
    		}
    
    
    ezsh's avatar
    ezsh committed
    		/**
    		 * @param {Element} child
    		 */
    
    vas's avatar
    vas committed
    		appendChild(child) {
    
    vas's avatar
    vas committed
    			child.parent = this;
    			this.children.push(child);
    
    ezsh's avatar
    ezsh committed
    			child._appendContentTo(this.element);
    
    vas's avatar
    vas committed
    			this.element.appendChild(child.element);
    
    vas's avatar
    vas committed
    		}
    
    
    ezsh's avatar
    ezsh committed
    		/**
    		 * returns the first argument to simplify creation of basic container items
    		 * @returns {*}
    		 */
    
    vas's avatar
    vas committed
    		render(...args) {
    
    vas's avatar
    vas committed
    			return args[0];
    
    vas's avatar
    vas committed
    		}
    
    
    		remove() {
    			const idx = this.parent.children.findIndex(child => child === this);
    			this.parent.children.slice(idx, 1);
    			this.element.remove();
    		}
    
    ezsh's avatar
    ezsh committed
    
    		/**
    		 * @protected
    		 * @param {HTMLElement} container
    		 */
    		_appendContentTo(container) {
    			container.appendChild(this.element);
    		}
    
    vas's avatar
    vas committed
    	}
    
    klorpa's avatar
    klorpa committed
    
    
    vas's avatar
    vas committed
    	class Section extends Element {
    
    brickode's avatar
    brickode committed
    		constructor(header, hidden = false) {
    
    vas's avatar
    vas committed
    			super(header);
    			this.hidey = this.element.querySelector("div");
    
    DCoded's avatar
    DCoded committed
    			if (hidden) { this.toggle_hidey(); }
    
    vas's avatar
    vas committed
    		}
    
    klorpa's avatar
    klorpa committed
    
    
    vas's avatar
    vas committed
    		render(header) {
    			const section = document.createElement("section");
    			section.classList.add("rajs-section");
    			const h1 = document.createElement("h1");
    
    brickode's avatar
    brickode committed
    			h1.onclick = () => { this.toggle_hidey(); };
    
    vas's avatar
    vas committed
    			h1.innerHTML = header;
    			const hidey = document.createElement("div");
    			section.appendChild(h1);
    			section.appendChild(hidey);
    			return section;
    		}
    
    		appendChild(child) {
    			child.parent = this;
    			this.children.push(child);
    
    ezsh's avatar
    ezsh committed
    			child._appendContentTo(this.hidey);
    
    vas's avatar
    vas committed
    		}
    
    		toggle_hidey() {
    
    brickode's avatar
    brickode committed
    			switch (this.hidey.style.display) {
    
    vas's avatar
    vas committed
    				case "none":
    					this.hidey.style.display = "initial";
    					break;
    				default:
    					this.hidey.style.display = "none";
    					break;
    			}
    		}
    	}
    
    vas's avatar
    vas committed
    
    
    	class Tab extends Element {
    		/**
    		 *
    		 * @param {string} name
    		 * @param {string} label
    		 * @param {HTMLDivElement} tabButtonsContainer
    		 */
    		constructor(name, label, tabButtonsContainer) {
    			super(name);
    			tabButtonsContainer.appendChild(Tab.makeTabButton(name, label));
    		}
    		render(name) {
    			const tab = document.createElement("div");
    			tab.id = name;
    			tab.className = "tabcontent";
    
    			this.tabContent_ = document.createElement("div");
    
    ezsh's avatar
    ezsh committed
    			this.tabContent_.classList.add("content");
    			this.tabContent_.classList.add("ra-container");
    
    			tab.appendChild(this.tabContent_);
    
    			return tab;
    		}
    
    		appendChild(child) {
    			child.parent = this;
    			this.children.push(child);
    
    ezsh's avatar
    ezsh committed
    			child._appendContentTo(this.tabContent_);
    
    		}
    
    		static makeTabButton(name, text) {
    			const btn = document.createElement("button");
    			btn.className = "tablinks";
    			btn.id = `tab ${name}`;
    			btn.innerHTML = text;
    			btn.onclick = (event) => App.UI.tabbar.openTab(event, name);
    			return btn;
    		}
    	}
    
    
    ezsh's avatar
    ezsh committed
    	class ElementWithLabel extends Element {
    		/**
    		 * @param {string} label
    		 * @param {*} args
    		 */
    		constructor(label, ...args) {
    			super(...args);
    			this.labelElement_ = document.createElement("span");
    			this.labelElement_.className = "ra-label";
    			this.labelElement_.innerHTML = label;
    		}
    
    		/**
    		 * @protected
    		 * @param {HTMLElement} container
    		 */
    		_appendContentTo(container) {
    			container.appendChild(this.labelElement_);
    			super._appendContentTo(container);
    		}
    	}
    
    
    vas's avatar
    vas committed
    	// list of clickable elements
    	// has a short explanation (the prefix) and a value display
    	// value display can optionally be an editable text input field
    	// it can be "bound" to a variable by setting its "onchange" method
    
    ezsh's avatar
    ezsh committed
    	class EditorWithShortcuts extends ElementWithLabel {
    
    ezsh's avatar
    ezsh committed
    		/**
    		 *
    		 * @param {string} prefix
    
    		 * @param {Array} [data=[]]
    		 * @param {boolean} [allowNullValue=true]
    		 * @param {boolean} [editor=false]
    		 * @param {boolean} [capitalizeShortcuts]
    
    ezsh's avatar
    ezsh committed
    		 * @param {...any} args
    
    ezsh's avatar
    ezsh committed
    		 */
    
    		constructor(prefix, data = [], allowNullValue = true, editor = false, capitalizeShortcuts = false, ...args) {
    
    ezsh's avatar
    ezsh committed
    			super(prefix, editor, ...args);
    
    vas's avatar
    vas committed
    			this.selectedItem = null;
    
    ezsh's avatar
    ezsh committed
    			/** @protected */
    			this._allowNullValue = allowNullValue;
    
    			/** @private */
    			this._capitalizeShortcuts = capitalizeShortcuts;
    
    ezsh's avatar
    ezsh committed
    			if (allowNullValue) {
    
    				this.appendChild(new ListItem(capFirstChar(noDefaultSetting.text), null));
    
    ezsh's avatar
    ezsh committed
    			}
    
    ezsh's avatar
    ezsh committed
    			data.forEach(item => this.appendChild(this._createListItem(item)));
    
    vas's avatar
    vas committed
    		}
    
    vas's avatar
    vas committed
    
    
    		createEditor(...args) { return null; }
    
    ezsh's avatar
    ezsh committed
    		createValueElement() { return document.createElement("strong"); }
    
    		render(editor, ...args) {
    
    vas's avatar
    vas committed
    			const elem = document.createElement("div");
    
    ezsh's avatar
    ezsh committed
    			this.value = editor ? this.createEditor(...args) : this.createValueElement();
    			if (this.value !== null) {
    				elem.appendChild(this.value);
    			}
    
    vas's avatar
    vas committed
    			elem.classList.add("rajs-list");
    			return elem;
    
    vas's avatar
    vas committed
    		}
    
    vas's avatar
    vas committed
    
    
    vas's avatar
    vas committed
    		inputEdited() {
    
    DCoded's avatar
    DCoded committed
    			if (this.selectedItem) { this.selectedItem.deselect(); }
    
    vas's avatar
    vas committed
    			this.propagateChange();
    
    vas's avatar
    vas committed
    		}
    
    vas's avatar
    vas committed
    
    
    vas's avatar
    vas committed
    		selectItem(item) {
    
    DCoded's avatar
    DCoded committed
    			if (this.selectedItem) { this.selectedItem.deselect(); }
    
    vas's avatar
    vas committed
    			this.selectedItem = item;
    
    vas's avatar
    vas committed
    			this.setValue(item.data);
    
    vas's avatar
    vas committed
    			this.propagateChange();
    
    vas's avatar
    vas committed
    		}
    
    		setValue(what) {
    
    ezsh's avatar
    ezsh committed
    			const str = what === null ? "no default setting" : `${what}`;
    
    ezsh's avatar
    ezsh committed
    			if (this.value) {
    				if (this.value.tagName === "INPUT") {
    					this.value.value = str;
    				} else {
    					this.value.innerHTML = str;
    				}
    
    DCoded's avatar
    DCoded committed
    			}
    
    vas's avatar
    vas committed
    		}
    
    
    DCoded's avatar
    DCoded committed
    			return (this.value.tagName === "INPUT" ? this.parse(this.value.value) : this.selectedItem.data);
    
    vas's avatar
    vas committed
    
    
    klorpa's avatar
    klorpa committed
    		// customizable input field parser / sanity checker
    
    		parse(what) { return what; }
    
    vas's avatar
    vas committed
    
    		propagateChange() {
    
    DCoded's avatar
    DCoded committed
    			if (this.onchange instanceof Function) {
    
    vas's avatar
    vas committed
    				this.onchange(this.getData());
    
    DCoded's avatar
    DCoded committed
    			}
    
    vas's avatar
    vas committed
    		}
    
    
    		/**
    		 * @private
    
    ezsh's avatar
    ezsh committed
    		 * @param {string|string[]} item
    
    		 * @returns {ListItem}
    		 */
    
    ezsh's avatar
    ezsh committed
    		_createListItem(item) {
    			let display = '';
    			let data = null;
    			if (Array.isArray(item)) {
    				display = item[0];
    				data = item.length > 1 ? item[1] : display;
    
    ezsh's avatar
    ezsh committed
    				display = item;
    				data = item;
    
    ezsh's avatar
    ezsh committed
    			if (this._capitalizeShortcuts) {
    				display = capFirstChar(display);
    			}
    			return new ListItem(display, data);
    
    vas's avatar
    vas committed
    	}
    
    	// a clickable item of a list
    	class ListItem extends Element {
    
    vas's avatar
    vas committed
    		constructor(displayvalue, data) {
    
    vas's avatar
    vas committed
    			super(displayvalue);
    
    brickode's avatar
    brickode committed
    			this.data = data !== undefined ? data : displayvalue;
    
    vas's avatar
    vas committed
    			this.selected = false;
    
    vas's avatar
    vas committed
    		}
    
    vas's avatar
    vas committed
    
    
    vas's avatar
    vas committed
    		render(displayvalue) {
    
    vas's avatar
    vas committed
    			const elem = document.createElement("span");
    			elem.classList.add("rajs-listitem");
    			elem.innerHTML = displayvalue;
    
    brickode's avatar
    brickode committed
    			elem.onclick = () => { return this.select(); };
    
    vas's avatar
    vas committed
    			return elem;
    
    vas's avatar
    vas committed
    		}
    
    		select() {
    
    DCoded's avatar
    DCoded committed
    			if (this.selected) { return false; }
    
    vas's avatar
    vas committed
    			this.parent.selectItem(this);
    
    vas's avatar
    vas committed
    			this.element.classList.add("selected");
    
    vas's avatar
    vas committed
    			this.selected = true;
    			return true;
    
    vas's avatar
    vas committed
    		}
    
    		deselect() {
    
    vas's avatar
    vas committed
    			this.element.classList.remove("selected");
    
    vas's avatar
    vas committed
    			this.selected = false;
    
    vas's avatar
    vas committed
    		}
    	}
    
    
    ezsh's avatar
    ezsh committed
    	class ListSelector extends ElementWithLabel {
    
    		constructor(prefix, data = [], allowNullValue = true) {
    
    ezsh's avatar
    ezsh committed
    			super(prefix, data, allowNullValue);
    
    ezsh's avatar
    ezsh committed
    		render(data, allowNullValue) {
    
    			const elem = document.createElement("div");
    			this.value = document.createElement("select");
    			elem.appendChild(this.value);
    			elem.classList.add("rajs-list");
    
    ezsh's avatar
    ezsh committed
    			this.values_ = new Map();
    
    			// now add options
    			if (allowNullValue) {
    				let nullOpt = document.createElement("option");
    
    				nullOpt.value = noDefaultSetting.value;
    				nullOpt.text = capFirstChar(noDefaultSetting.text);
    
    				this.value.appendChild(nullOpt);
    
    ezsh's avatar
    ezsh committed
    				this.values_.set(nullOpt.value, null);
    
    			}
    			for (const dr of data) {
    
    				const dv = Array.isArray(dr) ? (dr.length > 1 ? [dr[1], dr[0]] : [dr[0], dr[0]]) : [dr, dr];
    
    				let opt = document.createElement("option");
    				opt.value = dv[0];
    
    				opt.text = capFirstChar(dv[1]);
    
    				this.value.appendChild(opt);
    
    ezsh's avatar
    ezsh committed
    				this.values_.set(opt.value, dv[0]);
    
    			}
    			this.value.onchange = () => {
    				this.inputEdited();
    			};
    			return elem;
    		}
    
    		getData() {
    
    ezsh's avatar
    ezsh committed
    			return this.values_.get(this.value.value);
    
    			this.value.value = what === null ? noDefaultSetting.value : what;
    
    		}
    
    		inputEdited() {
    			this.propagateChange();
    		}
    
    		propagateChange() {
    			if (this.onchange instanceof Function) {
    				this.onchange(this.getData());
    			}
    
    ezsh's avatar
    ezsh committed
    	class RadioSelector extends ElementWithLabel {
    
    ezsh's avatar
    ezsh committed
    		/**
    		 *
    		 * @param {string} prefix
    		 * @param {Array} [data=[]]
    		 * @param {boolean} [allowNullValue=true]
    		 */
    		constructor(prefix, data = [], allowNullValue = true) {
    
    ezsh's avatar
    ezsh committed
    			super(prefix, prefix, data, allowNullValue);
    
    ezsh's avatar
    ezsh committed
    		}
    
    		render(prefix, data, allowNullValue) {
    			this.name_ = prefix.replace(' ', '_');
    			const elem = document.createElement("div");
    			this.values_ = new Map();
    			this.radios_ = new Map();
    
    			let values = [];
    			if (allowNullValue) {
    				values.push([noDefaultSetting.value, noDefaultSetting.text]);
    				this.values_.set(noDefaultSetting.value, null);
    			}
    			for (const dr of data) {
    				const dv = Array.isArray(dr) ? (dr.length > 1 ? [dr[1], dr[0]] : [dr[0], dr[0]]) : [dr, dr];
    				values.push(dv);
    				this.values_.set(`${dv[0]}`, dv[0]);
    			}
    
    			for (const v of values) {
    				let inp = document.createElement("input");
    				inp.type = "radio";
    				inp.name = this.name_;
    
    Skriv's avatar
    Skriv committed
    				inp.id = `${prefix}_${v[0]}`;
    
    ezsh's avatar
    ezsh committed
    				inp.value = v[0];
    
    				let lbl = document.createElement("label");
    				lbl.htmlFor = inp.id;
    				lbl.className = "ra-radio-label";
    				lbl.innerHTML = capFirstChar(v[1]);
    				inp.onclick = () => { this.inputEdited(); };
    				this.radios_.set(v[0], inp);
    
    				elem.appendChild(inp);
    				elem.appendChild(lbl);
    			}
    			return elem;
    		}
    
    		getData() {
    			return this.values_.get($(`input[name='${this.name_}']:checked`).val());
    		}
    
    		setValue(what) {
    			this.radios_.get(what === null ? noDefaultSetting.value : what).checked = true;
    		}
    
    		inputEdited() {
    			this.propagateChange();
    		}
    
    		propagateChange() {
    			if (this.onchange instanceof Function) {
    				this.onchange(this.getData());
    			}
    		}
    	}
    
    
    ezsh's avatar
    ezsh committed
    	class List extends EditorWithShortcuts {
    
    		constructor(prefix, data = [], allowNullValue = true, textinput = false, capitalizeShortcuts = true) {
    			super(prefix, data, allowNullValue, textinput, capitalizeShortcuts);
    
    ezsh's avatar
    ezsh committed
    			this.values = new Map();
    
    			if (allowNullValue) {
    
    				this.values.set(noDefaultSetting.value, noDefaultSetting.text);
    
    ezsh's avatar
    ezsh committed
    			data.forEach(d => {
    				if (Array.isArray(d) && d.length > 1) {
    					this.values.set(d[1], d[0]);
    				} else {
    					this.values.set(d, d);
    				}
    			});
    
    			this.selectedValue = null;
    
    ezsh's avatar
    ezsh committed
    		}
    
    		createEditor() {
    			let res = document.createElement("input");
    			res.setAttribute("type", "text");
    			res.classList.add("rajs-value"); //
    			// call the variable binding when the input field is no longer being edited, and when the enter key is pressed
    
    			res.onblur = () => {
    				this.inputEdited();
    			};
    			res.onkeypress = (e) => {
    
    DCoded's avatar
    DCoded committed
    				if (returnP(e)) { this.inputEdited(); }
    
    ezsh's avatar
    ezsh committed
    			return res;
    		}
    
    			return this.selectedValue;
    		}
    
    
    ezsh's avatar
    ezsh committed
    		setValue(what) {
    
    			this.selectedValue = what;
    
    ezsh's avatar
    ezsh committed
    			if (this.values.has(what)) {
    				super.setValue(this.values.get(what));
    			} else {
    				super.setValue(what);
    			}
    		}
    
    ezsh's avatar
    ezsh committed
    	class StringEditor extends EditorWithShortcuts {
    		constructor(prefix, data = [], allowNullValue = true, capitalizeShortcuts = true) {
    			super(prefix, data, allowNullValue, true, capitalizeShortcuts);
    		}
    
    		createEditor() {
    			let res = document.createElement("input");
    			res.setAttribute("type", "text");
    			res.classList.add("rajs-value"); //
    			// call the variable binding when the input field is no longer being edited, and when the enter key is pressed
    			res.onblur = () => {
    				this.inputEdited();
    			};
    			res.onkeypress = (e) => {
    				if (returnP(e)) { this.inputEdited(); }
    			};
    			return res;
    		}
    
    		getData() {
    			return this.value.value;
    		}
    
    		setValue(what) {
    			this.value.value = what;
    		}
    	}
    
    
    ezsh's avatar
    ezsh committed
    	class BooleanSwitch extends ElementWithLabel {
    
    		/**
    		 * @param {string} prefix
    		 * @param {Array} values values for "false" and "true"
    		 */
    		constructor(prefix, values = [false, true]) {
    
    ezsh's avatar
    ezsh committed
    			super(prefix, prefix);
    
    
    			/** @private */
    			this.values_ = {
    				false: values[0],
    				true: values[1]
    			};
    		}
    
    		render(prefix) {
    			const elem = document.createElement("div");
    			let switchContainer = document.createElement("div");
    			switchContainer.className = "ra-onoffswitch";
    			this.checkBox_ = document.createElement("input");
    			this.checkBox_.type = "checkbox";
    			this.checkBox_.className = "ra-onoffswitch-checkbox";
    			this.checkBox_.id = `ra-option-${prefix}`;
    			let switchLabel = document.createElement("label");
    			switchLabel.className = "ra-onoffswitch-label";
    			switchLabel.htmlFor = this.checkBox_.id;
    			let innerSpan = document.createElement("span");
    			innerSpan.className = "ra-onoffswitch-inner";
    			let switchSpan = document.createElement("span");
    			switchSpan.className = "ra-onoffswitch-switch";
    			switchLabel.appendChild(innerSpan);
    			switchLabel.appendChild(switchSpan);
    			switchContainer.appendChild(this.checkBox_);
    			switchContainer.appendChild(switchLabel);
    			elem.appendChild(switchContainer);
    			elem.classList.add("rajs-list");
    
    
    ezsh's avatar
    ezsh committed
    			this.checkBox_.onchange = () => { this.inputEdited(); };
    
    			return elem;
    		}
    
    		getData() {
    			return this.values_[this.checkBox_.checked];
    		}
    
    		setValue(what) {
    			this.checkBox_.checked = this.values_[true] === what;
    		}
    
    		inputEdited() {
    			this.propagateChange();
    		}
    
    		propagateChange() {
    			if (this.onchange instanceof Function) {
    				this.onchange(this.getData());
    			}
    
    	class NumericTargetEditor extends EditorWithShortcuts {
    
    ezsh's avatar
    ezsh committed
    		/**
    		 * @param {string} prefix
    		 * @param {Array} [data=[]]
    		 * @param {boolean} [allowNullValue=true]
    		 * @param {number} [min=0]
    		 * @param {number} [max=100]
    		 * @param {boolean} [spinBox=false]
    		 */
    		constructor(prefix, data = [], allowNullValue = true, min = 0, max = 100, spinBox = false) {
    
    Skriv's avatar
    Skriv committed
    			super(prefix, data, allowNullValue, spinBox, true, min, max);
    
    ezsh's avatar
    ezsh committed
    		}
    
    		createEditor(min, max) {
    
    ezsh's avatar
    ezsh committed
    			function makeOp(op, ui) {
    				return {op: op, ui: ui};
    			}
    			this.opSelector = document.createElement("select");
    			for (const o of [makeOp('==', '='), makeOp('>=', "⩾"), makeOp('<=', '⩽'), makeOp('>', '>'), makeOp('<', '<')]) {
    				let opt = document.createElement("option");
    				opt.textContent = o.ui;
    				opt.value = o.op;
    				this.opSelector.appendChild(opt);
    			}
    			this.opSelector.classList.add("rajs-list");
    			this.opSelector.onchange = () => {
    
    				this.inputEdited();
    			};
    
    ezsh's avatar
    ezsh committed
    
    			this.numEditor = document.createElement("input");
    
    ezsh's avatar
    ezsh committed
    			this.numEditor.type = "number";
    			this.numEditor.min = min;
    			this.numEditor.max= max;
    
    ezsh's avatar
    ezsh committed
    			this.numEditor.classList.add("rajs-value"); //
    			this.numEditor.onblur = () => {
    				this.inputEdited();
    			};
    			this.numEditor.onkeypress = (e) => {
    
    				if (returnP(e)) { this.inputEdited(); }
    
    ezsh's avatar
    ezsh committed
    
    			const res = document.createElement("span");
    			res.appendChild(this.opSelector);
    			res.appendChild(this.numEditor);
    
    ezsh's avatar
    ezsh committed
    			return res;
    		}
    
    
    		parse(what) {
    
    ezsh's avatar
    ezsh committed
    			return what === "" ? null : parseInt(what);
    		}
    
    		setValue(what) {
    			if (typeof what === 'number') { // comes from a pre-set
    				this.numEditor.value = what.toString();
    			} else if (what === null) {
    				this.numEditor.value = null;
    				this.opSelector.value = '==';
    			} else if (typeof what === 'object') {
    				this.numEditor.value = what.val;
    				this.opSelector.value = what.cond;
    			}
    		}
    
    
    ezsh's avatar
    ezsh committed
    			const v = this.parse(this.numEditor.value);
    			return v === null ? null : App.RA.makeTarget(this.opSelector.value, v);
    
    	class NumericRangeEditor extends EditorWithShortcuts {
    		/**
    		 * @param {string} prefix
    		 * @param {Array} [data=[]]
    		 * @param {boolean} [allowNullValue=true]
    		 * @param {number} [min=0]
    		 * @param {number} [max=100]
    		 */
    		constructor(prefix, data = [], allowNullValue = true, min = 0, max = 100) {
    			super(prefix, data, allowNullValue, true, true,  min, max);
    		}
    
    		createEditor(min, max) {
    			this._min = min;
    			this._max = max;
    			let res = document.createElement("span");
    
    			function makeElem(lbl, container, editor) {
    				const spinBox = document.createElement("input");
    				spinBox.type = "number";
    				spinBox.min = min;
    				spinBox.max = max;
    
    				const label = document.createElement("span");
    				label.textContent = lbl;
    				label.className = "ra-inline-label";
    
    				const elem = document.createElement("span");
    				elem.appendChild(label);
    				elem.appendChild(spinBox);
    				container.appendChild(elem);
    
    				spinBox.onblur = () => {
    					editor.inputEdited();
    				};
    				spinBox.onkeypress = (e) => {
    					if (returnP(e)) { editor.inputEdited(); }
    				};
    
    				return spinBox;
    			}
    
    			this._minEditor = makeElem("Min", res, this);
    			this._maxEditor = makeElem("Max", res, this);
    
    			return res;
    		}
    
    		getData() {
    			function parse(what) {
    				return what === "" ? null : parseInt(what);
    			}
    
    			const vMin = parse(this._minEditor.value);
    			const vMax = parse(this._maxEditor.value);
    			return (vMin === null && vMax === null) ? null :
    				App.RA.makeRange(vMin !== null ? vMin : this._min, vMax !== null ? vMax : this._max);
    		}
    
    		setValue(what) {
    			if (what === null) {
    				this._minEditor.value = null;
    				this._maxEditor.value = null;
    
    			} else {
    				this._minEditor.value = what.min;
    				this._maxEditor.value = what.max;
    
    klorpa's avatar
    klorpa committed
    	// a way to organize lists with too many elements in subsections
    
    vas's avatar
    vas committed
    	// children are bound to the master list
    
    ezsh's avatar
    ezsh committed
    	class ListSubSection extends ElementWithLabel {
    
    vas's avatar
    vas committed
    		constructor(parent, label, pairs) {
    
    vas's avatar
    vas committed
    			super(label);
    
    vas's avatar
    vas committed
    			this.parent = parent;
    
    ezsh's avatar
    ezsh committed
    			this.labelElement_.className = "ra-sub-label";
    			pairs.forEach(item => this.appendChild(Array.isArray(item) ? new ListItem( ...item) : new ListItem(item)));
    
    vas's avatar
    vas committed
    		}
    
    klorpa's avatar
    klorpa committed
    
    
    ezsh's avatar
    ezsh committed
    		render() {
    
    Skriv's avatar
    Skriv committed
    			return document.createElement("div");
    
    vas's avatar
    vas committed
    		}
    
    vas's avatar
    vas committed
    
    
    vas's avatar
    vas committed
    		appendChild(child) {
    
    vas's avatar
    vas committed
    			super.appendChild(child);
    			child.parent = this.parent;
    			this.parent.children.push(child);
    
    vas's avatar
    vas committed
    		}
    	}
    
    
    vas's avatar
    vas committed
    	// similar to list, but is just a collection of buttons
    
    vas's avatar
    vas committed
    	class Options extends Element {
    
    brickode's avatar
    brickode committed
    		constructor(elements = []) {
    
    vas's avatar
    vas committed
    			super();
    
    			elements.forEach(element => { this.appendChild(element); });
    
    vas's avatar
    vas committed
    		}
    
    		render() {
    
    vas's avatar
    vas committed
    			const elem = document.createElement("div");
    			elem.classList.add("rajs-list");
    			return elem;
    
    vas's avatar
    vas committed
    		}
    	}
    
    
    ezsh's avatar
    ezsh committed
    	class OptionsWithLabel extends Options {
    		constructor(prefix, elements = []) {
    			super(elements);
    			this.labelElement_ = document.createElement("span");
    			this.labelElement_.className = "ra-label";
    			this.labelElement_.innerHTML = prefix;
    		}
    
    		/**
    		 * @protected
    		 * @param {HTMLElement} container
    		 */
    		_appendContentTo(container) {
    			container.appendChild(this.labelElement_);
    			super._appendContentTo(container);
    		}
    	}
    
    
    vas's avatar
    vas committed
    	// options equivalent of ListItem
    
    vas's avatar
    vas committed
    	class OptionsItem extends Element {
    		constructor(label, onclick) {
    
    vas's avatar
    vas committed
    			super(label);
    			this.label = label;
    			this.onclick = onclick;
    
    vas's avatar
    vas committed
    		}
    
    brickode's avatar
    brickode committed
    		render(label, onclick) {
    
    vas's avatar
    vas committed
    			const elem = document.createElement("span");
    			elem.classList.add("rajs-listitem");
    			elem.innerHTML = label;
    
    brickode's avatar
    brickode committed
    			elem.onclick = () => { return this.onclick(this); };
    
    vas's avatar
    vas committed
    			return elem;
    
    vas's avatar
    vas committed
    		}
    	}
    
    
    vas's avatar
    vas committed
    	class ButtonList extends Element {
    		render(label) {
    
    vas's avatar
    vas committed
    			const elem = document.createElement("div");
    			const labelel = document.createElement("span");
    
    vas's avatar
    vas committed
    			labelel.innerHTML = label += ": ";
    
    vas's avatar
    vas committed
    			elem.appendChild(labelel);
    			return elem;
    
    vas's avatar
    vas committed
    		}
    
    		getSelection() {
    			return (this.children
    				.filter(child => child.selected)
    				.map(child => child.setvalue)
    
    vas's avatar
    vas committed
    			);
    
    vas's avatar
    vas committed
    		}
    
    
    		onchange() { return; }
    
    vas's avatar
    vas committed
    	}
    
    	class ButtonItem extends Element {
    
    brickode's avatar
    brickode committed
    		constructor(label, setvalue, selected = false) {
    
    vas's avatar
    vas committed
    			super(label, selected);
    			this.selected = selected;
    			this.setvalue = setvalue ? setvalue : label;
    
    vas's avatar
    vas committed
    		}
    
    		render(label, selected) {
    
    vas's avatar
    vas committed
    			const container = document.createElement("div");
    			container.classList.add("rajs-listitem");
    
    vas's avatar
    vas committed
    
    
    vas's avatar
    vas committed
    			const labelel = document.createElement("span");
    			labelel.innerHTML = label;
    
    vas's avatar
    vas committed
    
    
    vas's avatar
    vas committed
    			const button = document.createElement("input");
    			button.setAttribute("type", "checkbox");
    			button.checked = selected;
    			button.onchange = () => this.onchange(button.checked);
    
    vas's avatar
    vas committed
    			labelel.onclick = () => button.click();
    
    vas's avatar
    vas committed
    
    
    vas's avatar
    vas committed
    			container.appendChild(labelel);
    			container.appendChild(button);
    
    vas's avatar
    vas committed
    
    
    vas's avatar
    vas committed
    			return container;
    
    vas's avatar
    vas committed
    		}
    
    		onchange(value) {
    
    vas's avatar
    vas committed
    			this.selected = value;
    
    vas's avatar
    vas committed
    			this.parent.onchange(this);
    
    vas's avatar
    vas committed
    		}
    	}
    
    
    vas's avatar
    vas committed
    	// rule import field
    
    vas's avatar
    vas committed
    	class NewRuleField extends Element {
    		constructor(root) {
    
    vas's avatar
    vas committed
    			super();
    			this.root = root;
    
    vas's avatar
    vas committed
    		}
    
    		render() {
    
    vas's avatar
    vas committed
    			let element = document.getElementById("importfield");
    
    brickode's avatar
    brickode committed
    			if (element !== null) {
    				return element;
    			}
    
    vas's avatar
    vas committed
    			const container = document.createElement("div");
    
    vas's avatar
    vas committed
    			container.id = "importfield";
    
    vas's avatar
    vas committed
    			const textarea = document.createElement("textarea");
    
    vas's avatar
    vas committed
    			textarea.placeholder = "Paste your rule here";
    			container.appendChild(textarea);
    			this.textarea = textarea;
    			const button = document.createElement("button");
    			button.name = "Load";
    
    vas's avatar
    vas committed
    			button.innerHTML = "Load";
    
    brickode's avatar
    brickode committed
    			button.onclick = () => { this.loadNewRule(); };
    
    vas's avatar
    vas committed
    			container.appendChild(button);
    			return container;
    
    vas's avatar
    vas committed
    		}
    
    		loadNewRule() {
    
    vas's avatar
    vas committed
    			const text = this.textarea.value;
    
    vas's avatar
    vas committed
    			try {
    
    vas's avatar
    vas committed
    				const rule = JSON.parse(text);
    
    Skriv's avatar
    Skriv committed
    				if (Array.isArray(rule)) {
    
    ezsh's avatar
    ezsh committed
    					rule.forEach(r => {
    
    						V.defaultRules.push(App.Entity.Utils.RARuleDatatypeCleanup(r));
    
    ezsh's avatar
    ezsh committed
    					});
    
    DCoded's avatar
    DCoded committed
    				} else {
    
    					V.defaultRules.push(App.Entity.Utils.RARuleDatatypeCleanup(rule));
    
    DCoded's avatar
    DCoded committed
    				}
    
    vas's avatar
    vas committed
    				reload(this.root);
    
    vas's avatar
    vas committed
    			} catch (e) {
    
    Skriv's avatar
    Skriv committed
    				alert(`Couldn't import that rule:\n${e.message}`);
    
    vas's avatar
    vas committed
    			}
    		}
    	}
    
    
    vas's avatar
    vas committed
    	// the base element, parent of all elements
    
    vas's avatar
    vas committed
    	class Root extends Element {
    
    vas's avatar
    vas committed
    		constructor(element) {
    			super(element);
    
    brickode's avatar
    brickode committed
    			if (V.defaultRules.length === 0) {
    
    vas's avatar
    vas committed
    				const paragraph = document.createElement("p");
    				paragraph.innerHTML = "<strong>No rules</strong>";
    				this.appendChild(new Element(paragraph));
    				this.appendChild(new NoRules(this));
    				return;
    
    vas's avatar
    vas committed
    			}
    
    vas's avatar
    vas committed
    			this.appendChild(new RuleSelector(this));
    			this.appendChild(new RuleOptions(this));
    
    vas's avatar
    vas committed
    			this.appendChild(new ConditionEditor(this));
    
    vas's avatar
    vas committed
    			this.appendChild(new EffectEditor(this));
    
    			App.UI.tabbar.handlePreSelectedTab("appearance", true);
    
    vas's avatar
    vas committed
    		}
    
    		render(element) {
    
    vas's avatar
    vas committed
    			const greeting = document.createElement("p");
    
    			greeting.innerHTML = `<em>${properTitle()}, I will review your slaves and make changes that will have a beneficial effect. Apologies, ${properTitle()}, but this function is... not fully complete. It may have some serious limitations. Please use the '${noDefaultSetting.text}' option to identify areas I should not address.</em>`;
    
    vas's avatar
    vas committed
    			element.appendChild(greeting);
    			return element;
    
    vas's avatar
    vas committed
    		}
    	}
    
    
    klorpa's avatar
    klorpa committed
    	// options displayed when there are no rules
    
    vas's avatar
    vas committed
    	class NoRules extends Options {
    		constructor(root) {
    
    vas's avatar
    vas committed
    			super();
    			this.root = root;
    
    brickode's avatar
    brickode committed
    			const newrule = new OptionsItem("Add a new rule", () => { newRule(this.root); });
    
    vas's avatar
    vas committed
    			this.appendChild(newrule);
    
    brickode's avatar
    brickode committed
    			const importrule = new OptionsItem("Import a rule", () => { this.root.appendChild(new NewRuleField(this.root)); });
    
    vas's avatar
    vas committed
    			this.appendChild(importrule);
    
    vas's avatar
    vas committed
    		}
    	}
    
    
    vas's avatar
    vas committed
    	// buttons for selecting the current rule
    	class RuleSelector extends List {
    		constructor(root) {
    
    ezsh's avatar
    ezsh committed
    			super("Current rule", V.defaultRules.map(i => [i.name, i]), false);
    
    vas's avatar
    vas committed
    			this.setValue(current_rule.name);
    
    Skriv's avatar
    Skriv committed
    			this.onchange = function(rule) {
    
    				V.currentRule = rule.ID;
    
    vas's avatar
    vas committed
    				reload(root);
    			};
    
    vas's avatar
    vas committed
    		}
    	}
    
    	// buttons for doing transformations on rules
    	class RuleOptions extends Options {
    		constructor(root) {
    
    vas's avatar
    vas committed
    			super();
    			this.appendChild(new OptionsItem("New Rule", () => newRule(root)));
    			this.appendChild(new OptionsItem("Remove Rule", () => removeRule(root)));
    			this.appendChild(new OptionsItem("Apply rules", () => this.appendChild(new ApplicationLog())));
    
    			this.appendChild(new OptionsItem("Lower Priority", () => lowerPriority(root)));
    
    vas's avatar
    vas committed
    			this.appendChild(new OptionsItem("Higher Priority", () => higherPriority(root)));
    			this.appendChild(new OptionsItem("Rename", () => this.appendChild(new RenameField(root))));
    
    vas's avatar
    vas committed
    			this.appendChild(new OptionsItem("Export this rule", () => this.appendChild(new ExportField(current_rule))));
    			this.appendChild(new OptionsItem("Export all rules", () => this.appendChild(new ExportField(...V.defaultRules))));
    
    vas's avatar
    vas committed
    			this.appendChild(new OptionsItem("Import rule(s)", () => this.appendChild(new NewRuleField(root))));
    
    vas's avatar
    vas committed
    		}
    	}
    
    	class ApplicationLog extends Element {
    		render() {