Skip to content
Snippets Groups Projects
rulesAssistantOptions.js 119 KiB
Newer Older
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

/**
 * @param {HTMLDivElement} div container for the RA UI
 */
App.RA.options = (function() {
vas's avatar
vas committed
	"use strict";
	const noDefaultSetting = {value: "!NDS!", text: "no default setting"};
	/** @type {FC.RA.Rule} */
vas's avatar
vas committed

	function rulesAssistantOptions(div) {
vas's avatar
vas committed
		V.nextButton = "Back to Main";
		V.nextLink = "Main";
		V.returnTo = "Main";
		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
			}
		root = new Root(div);
vas's avatar
vas committed
	}

	function returnP(e) { return e.keyCode === 13; }
vas's avatar
vas committed

		const rule = emptyDefaultRule();
		V.defaultRules.push(rule);
vas's avatar
vas committed
		V.currentRule = rule.ID;
vas's avatar
vas committed
	}

vas's avatar
vas committed
		const idx = V.defaultRules.findIndex(rule => rule.ID === current_rule.ID);
i107760's avatar
i107760 committed
		if (V.rulesToApplyOnce[V.defaultRules[idx].ID] !== "undefined") {
			delete V.rulesToApplyOnce[V.defaultRules[idx].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
	}

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
	}

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
	}

	function rename(container) {
lowercasedonkey's avatar
lowercasedonkey committed
		let rename = false;
		return () => {
			if (!rename) {
				container.appendChild(new RenameField());
				rename = true;
			}
		};
	}

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 the passage
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;
lowercasedonkey's avatar
lowercasedonkey committed
			tab.className = "tab-content";

			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");
lowercasedonkey's avatar
lowercasedonkey committed
			btn.className = "tab-links";
			btn.id = `tab ${name}`;
			btn.innerHTML = text;
			btn.onclick = (event) => App.UI.tabBar.openTab(event, name);
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);
		}
	}

	let _blockCallback=Symbol("Block Callback");
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.setValue(item.data);
vas's avatar
vas committed
			this.propagateChange();
vas's avatar
vas committed
		}
		trySetValue(what) {
			if(what == null && this._allowNullValue) {
				this.setValue(what);
				return;
			}
			const selected = this.children.filter(listItem => _.isEqual(listItem.data, what));
			if(selected != null && selected.length === 1) {
			} else if(this._allowNullValue) {
vas's avatar
vas committed
		setValue(what) {
			if(what == null && !this._allowNullValue) { what = ""; }
			this.realValue = what;
			if(this[_blockCallback]) { return; }
			try {
				this[_blockCallback] = true;
				this.setTextValue(what);
				this.updateSelected();
			} finally {
				this[_blockCallback] = false;
			}
		}
		setTextValue(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
		}
		dataEqual(left, right) {
			return _.isEqual(left, right);
		}
		updateSelected() {
			const dataValue = this.getData();
			let selected;
			if(dataValue == null) {
				selected = this.children.filter(listItem => listItem.data == null);
			} else {
				selected = this.children.filter(listItem => this.dataEqual(listItem.data, dataValue));
			if (selected.length > 1) { throw Error(`Multiple shortcuts matched ${JSON.stringify(dataValue)}`); }
			if (selected.length === 1) {
				const listItem = selected[0];
				listItem.select(false);
				if(this.selectedItem != null
				&& !_.isEqual(this.selectedItem, listItem)) {
					this.selectedItem.deselect();
				}
				this.selectedItem = listItem;
			}
		}

		/**
		 * @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");
lowercasedonkey's avatar
lowercasedonkey committed
			elem.classList.add("rajs-list-item");
vas's avatar
vas committed
			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
		}

DCoded's avatar
DCoded committed
			if (this.selected) { return false; }
vas's avatar
vas committed
			this.selected = true;
			this.element.classList.add("selected");
			if(notify) { this.parent.selectItem(this); }
vas's avatar
vas committed
			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
	/**
	 * Displays the <select> element with multiple choices
	 */
	class MultiListSelector extends ListSelector {
		constructor(prefix, data = []) {
			super(prefix, data, false);
		}

		render(data, allowNullValue) {
			const res = super.render(data, allowNullValue);
			this.value.multiple = true;
			return res;
		}

		getData() {
			const res = [];
			for (const opt of this.value.selectedOptions) {
				res.push(this.values_.get(opt.value));
			}
			return res;
		}

		setValue(what) {
			what = what || [];
			if (!Array.isArray(what)) {
				what = [what];
			}
ezsh's avatar
ezsh committed
			const vs = new Set(what);
			for (const opt of this.value.options) {
				opt.selected = vs.has(this.values_.get(opt.value));
			}
		}
	}

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) {
			const option = this.radios_.get(what === null ? noDefaultSetting.value : what);
			if (option) {
				option.checked = true;
			}
ezsh's avatar
ezsh committed
		}

		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;
		}

			this.selectedValue = what;
ezsh's avatar
ezsh committed
			if (this.values.has(what)) {
				super.setTextValue(this.values.get(what));
ezsh's avatar
ezsh committed
			} else {
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");
ezsh's avatar
ezsh committed
			// 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(); }
			};
			$(res).click(()=>res.setAttribute("placeholder", ""));
ezsh's avatar
ezsh committed
			return res;
		}
		setValue(what) {
			super.setValue(what);
			this.value.setAttribute("placeholder", what == null
klorpa's avatar
klorpa committed
												 ? `(${capFirstChar(noDefaultSetting.text)})`
												 : '');
ezsh's avatar
ezsh committed
			return this.value.value;
		}

ezsh's avatar
ezsh committed
			this.value.value = what;
		}
	}

ezsh's avatar
ezsh committed
	class BooleanSwitch extends ElementWithLabel {
		/**
		 * @param {string} prefix
Blank's avatar
Blank committed
		 * @param {Array} 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) {
wkwk's avatar
wkwk committed
			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 (_.isNumber(what)) { // shortcut list data is just numbers, turn them into targets
				what = App.RA.makeTarget(this.opSelector.value, what);
			}
			super.setValue(what);
		}

ezsh's avatar
ezsh committed
			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);

		dataEqual(left, right) {
			// when comparing a plain number to a target, assume equal conditions
			const xor = (a, b) => (a) ? !(b) : !!(b);
			if (xor(_.isNumber(left), _.isNumber(right))) {
				left = _.isNil(left.val) ? left : left.val;
				right = _.isNil(right.val) ? right : right.val;
			}
			return _.isEqual(left, right);
		}
	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) {
ezsh's avatar
ezsh committed
			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);

ezsh's avatar
ezsh committed
			this._minEditor.addEventListener("input", event => {
				const v = parseInt(this._minEditor.value);
				if (!Number.isNaN(v)) {
					this._maxEditor.min = Math.max(this._min, v).toString();
				}
			});

			this._maxEditor.addEventListener("input", event => {
				const v = parseInt(this._maxEditor.value);
				if (!Number.isNaN(v)) {
					this._minEditor.max = Math.min(this._max, v).toString();
				}
			});

			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);
		}

			if (what === null) {
				this._minEditor.value = null;
				this._maxEditor.value = null;
			} else {
				this._minEditor.value = what.min;
				this._maxEditor.value = what.max;
	//  Basically just a copy of NumericTargetEditor modified to handle strings as well
	class ExpressiveNumericTargetEditor extends EditorWithShortcuts {
		/**
		 * @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) {
			super(prefix, data, allowNullValue, spinBox, true, min, max);
		}

		createEditor(min, max) {
			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();
			};

			this.numEditor = document.createElement("input");
			this.numEditor.type = "text";
			this.numEditor.classList.add("rajs-value");
			this.numEditor.onblur = () => {
				this.inputEdited();
			};
			this.numEditor.onkeypress = (e) => {
				if (returnP(e)) { this.inputEdited(); }
			};

			const res = document.createElement("span");
			res.appendChild(this.opSelector);
			res.appendChild(this.numEditor);
			return res;
		}

		setValue(what) {
			if (_.isNumber(what)) { // shortcut list data is just numbers, turn them into targets
				what = App.RA.makeTarget(this.opSelector.value, what);
			}
			super.setValue(what);
		}

		setTextValue(what) {
			if (typeof what === 'number') {
				this.numEditor.value = what.toString();
			} else if (typeof what === 'string') {
				this.numEditor.value = what;
			} else if (what === null) {
				this.numEditor.value = null;
				this.opSelector.value = '==';
			} else if (typeof what === 'object') {
				this.opSelector.value = what.cond;
				this.numEditor.value = what.val;
			}
		}

		getTextData() {
			const n = this.numEditor.value !== "" ? Number(this.numEditor.value) : Number.NaN;  // Attempt to convert numEditor.value to number,
HPotato's avatar
HPotato committed
			const v = isNaN(n) ? this.numEditor.value : Math.floor(n);                         // return numEditor.value as number if !NaN (should result in realValue being of number)
			return v === null || v === "" ? null : { cond: this.opSelector.value, val: v };

		dataEqual(left, right) {
			// when comparing a plain number to a target, assume equal conditions
			const xor = (a, b) => (a) ? !(b) : !!(b);
			if (xor(_.isNumber(left), _.isNumber(right))) {
				left = _.isNil(left.val) ? left : left.val;
				right = _.isNil(right.val) ? right : right.val;
			}
			return _.isEqual(left, right);
		}
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