Newer
Older
// rewrite of the rules assistant options page in javascript
// uses an object-oriented widget pattern
// 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
globalThis.rulesAssistantOptions = (function() {
const noDefaultSetting = {value: "!NDS!", text: "no default setting"};
/** @type {FC.RA.Rule} */

MouseOfLight
committed
let current_rule, root;
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);

MouseOfLight
committed
root = new Root(element);
function returnP(e) { return e.keyCode === 13; }

MouseOfLight
committed
function newRule() {
const rule = emptyDefaultRule();
V.defaultRules.push(rule);

MouseOfLight
committed
reload();

MouseOfLight
committed
function removeRule() {
const idx = V.defaultRules.findIndex(rule => rule.ID === current_rule.ID);
if (V.rulesToApplyOnce[V.defaultRules[idx].ID] !== "undefined") {
delete V.rulesToApplyOnce[V.defaultRules[idx].ID];
}
if (V.defaultRules.length > 0) {
const new_idx = idx < V.defaultRules.length ? idx : V.defaultRules.length - 1;
V.currentRule = V.defaultRules[new_idx].ID;

MouseOfLight
committed
reload();

MouseOfLight
committed
function lowerPriority() {
const idx = V.defaultRules.findIndex(rule => rule.ID === current_rule.ID);

MouseOfLight
committed
reload();

MouseOfLight
committed
function higherPriority() {
const idx = V.defaultRules.findIndex(rule => rule.ID === current_rule.ID);

MouseOfLight
committed
reload();
function rename(container) {
return () => {
if (!rename) {
container.appendChild(new RenameField());
rename = true;
}
};
}

MouseOfLight
committed
function changeName(name) {

MouseOfLight
committed
reload();

MouseOfLight
committed
function reload() {
const parse = {
integer(string) {
let n = parseInt(string, 10);
},
boobs(string) {
return Math.clamp(parse.integer(string), 0, 48000);
},
butt(string) {
},
lips(string) {
return Math.clamp(parse.integer(string), 0, 100);
},
dick(string) {
// 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) {
this.parent = null;
this.element = this.render(...args);
this.children = [];
/**
* returns the first argument to simplify creation of basic container items
* @returns {*}
*/
remove() {
const idx = this.parent.children.findIndex(child => child === this);
this.parent.children.slice(idx, 1);
this.element.remove();
}
/**
* @protected
* @param {HTMLElement} container
*/
_appendContentTo(container) {
container.appendChild(this.element);
}
super(header);
this.hidey = this.element.querySelector("div");
render(header) {
const section = document.createElement("section");
section.classList.add("rajs-section");
const h1 = document.createElement("h1");
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);
case "none":
this.hidey.style.display = "initial";
break;
default:
this.hidey.style.display = "none";
break;
}
}
}
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;
this.tabContent_ = document.createElement("div");
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);
}
static makeTabButton(name, text) {
const btn = document.createElement("button");
btn.id = `tab ${name}`;
btn.innerHTML = text;
btn.onclick = (event) => App.UI.tabBar.openTab(event, name);
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);
}
}

MouseOfLight
committed
let _blockCallback=Symbol("Block Callback");
// 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
class EditorWithShortcuts extends ElementWithLabel {
* @param {Array} [data=[]]
* @param {boolean} [allowNullValue=true]
* @param {boolean} [editor=false]
* @param {boolean} [capitalizeShortcuts]
constructor(prefix, data = [], allowNullValue = true, editor = false, capitalizeShortcuts = false, ...args) {

MouseOfLight
committed
this[_blockCallback] = false;
/** @private */
this._capitalizeShortcuts = capitalizeShortcuts;
this.appendChild(new ListItem(capFirstChar(noDefaultSetting.text), null));
data.forEach(item => this.appendChild(this._createListItem(item)));
createValueElement() { return document.createElement("strong"); }
render(editor, ...args) {
this.value = editor ? this.createEditor(...args) : this.createValueElement();
if (this.value !== null) {
elem.appendChild(this.value);
}

MouseOfLight
committed
this.setValue(this.getTextData());

MouseOfLight
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) {

MouseOfLight
committed
this.selectItem(selected[0]);

MouseOfLight
committed
this.setValue(null);
}
}

MouseOfLight
committed
if(what == null && !this._allowNullValue) { what = ""; }
this.realValue = what;

MouseOfLight
committed
try {
this[_blockCallback] = true;
this.setTextValue(what);
this.updateSelected();
} finally {
this[_blockCallback] = false;
}
}
setTextValue(what) {
if (this.value) {
if (this.value.tagName === "INPUT") {
this.value.value = str;
} else {
this.value.innerHTML = str;
}

MouseOfLight
committed
return this.realValue;
}
getTextData() {
return (this.value.tagName === "INPUT" ? this.parse(this.value.value) : this.selectedItem.data);

MouseOfLight
committed
dataEqual(left, right) {
return _.isEqual(left, right);
}

MouseOfLight
committed
updateSelected() {
const dataValue = this.getData();
let selected;
if(dataValue == null) {
selected = this.children.filter(listItem => listItem.data == null);

MouseOfLight
committed
selected = this.children.filter(listItem => this.dataEqual(listItem.data, dataValue));

MouseOfLight
committed
}
wkwk
committed
if (selected.length > 1) { throw Error(`Multiple shortcuts matched ${JSON.stringify(dataValue)}`); }

MouseOfLight
committed
const listItem = selected[0];
listItem.select(false);
if(this.selectedItem != null
&& !_.isEqual(this.selectedItem, listItem)) {
this.selectedItem.deselect();
}
this.selectedItem = listItem;
}
}
_createListItem(item) {
let display = '';
let data = null;
if (Array.isArray(item)) {
display = item[0];
data = item.length > 1 ? item[1] : display;
if (this._capitalizeShortcuts) {
display = capFirstChar(display);
}
return new ListItem(display, data);
}
// a clickable item of a list
class ListItem extends Element {

MouseOfLight
committed
select(notify = true) {

MouseOfLight
committed
this.element.classList.add("selected");
if(notify) { this.parent.selectItem(this); }
constructor(prefix, data = [], allowNullValue = true) {
const elem = document.createElement("div");
this.value = document.createElement("select");
elem.appendChild(this.value);
elem.classList.add("rajs-list");
// now add options
if (allowNullValue) {
let nullOpt = document.createElement("option");
nullOpt.value = noDefaultSetting.value;
nullOpt.text = capFirstChar(noDefaultSetting.text);
this.value.appendChild(nullOpt);
}
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);
}
this.value.onchange = () => {
this.inputEdited();
};
return elem;
}
getData() {
}
setValue(what) {
this.value.value = what === null ? noDefaultSetting.value : what;
}
inputEdited() {
this.propagateChange();
}
propagateChange() {
if (this.onchange instanceof Function) {
this.onchange(this.getData());
}
/**
* 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];
}
const vs = new Set(what);
for (const opt of this.value.options) {
opt.selected = vs.has(this.values_.get(opt.value));
}
}
}
/**
*
* @param {string} prefix
* @param {Array} [data=[]]
* @param {boolean} [allowNullValue=true]
*/
constructor(prefix, data = [], allowNullValue = true) {
}
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_;
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;
}
}
inputEdited() {
this.propagateChange();
}
propagateChange() {
if (this.onchange instanceof Function) {
this.onchange(this.getData());
}
}
}
constructor(prefix, data = [], allowNullValue = true, textinput = false, capitalizeShortcuts = true) {
super(prefix, data, allowNullValue, textinput, capitalizeShortcuts);
this.values.set(noDefaultSetting.value, noDefaultSetting.text);
data.forEach(d => {
if (Array.isArray(d) && d.length > 1) {
this.values.set(d[1], d[0]);
} else {
this.values.set(d, d);
}
});
}
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) => {

MouseOfLight
committed
getTextData() {

MouseOfLight
committed
setTextValue(what) {

MouseOfLight
committed
super.setTextValue(this.values.get(what));

MouseOfLight
committed
super.setTextValue(what);
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");

MouseOfLight
committed
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(); }
};

MouseOfLight
committed
$(res).click(()=>res.setAttribute("placeholder", ""));

MouseOfLight
committed
setValue(what) {
super.setValue(what);
this.value.setAttribute("placeholder", what == null

MouseOfLight
committed
}

MouseOfLight
committed
getTextData() {

MouseOfLight
committed
setTextValue(what) {
*/
constructor(prefix, values = [false, true]) {
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
/** @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");
this.checkBox_.onchange = () => { this.inputEdited(); };
return elem;
}
getData() {
return this.values_[this.checkBox_.checked];
}
setValue(what) {
}
inputEdited() {
this.propagateChange();
}
propagateChange() {
if (this.onchange instanceof Function) {
this.onchange(this.getData());
}
class NumericTargetEditor 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) {
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.numEditor.type = "number";
this.numEditor.min = min;
this.numEditor.max= max;
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);

MouseOfLight
committed
setTextValue(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;
}
}

MouseOfLight
committed
getTextData() {
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);
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
}
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);
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();
}
});

MouseOfLight
committed
getTextData() {
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);
}

MouseOfLight
committed
setTextValue(what) {
if (what === null) {
this._minEditor.value = null;
this._maxEditor.value = null;
} else {
this._minEditor.value = what.min;
this._maxEditor.value = what.max;
// a way to organize lists with too many elements in subsections
this.labelElement_.className = "ra-sub-label";
pairs.forEach(item => this.appendChild(Array.isArray(item) ? new ListItem( ...item) : new ListItem(item)));
super.appendChild(child);
child.parent = this.parent;
this.parent.children.push(child);
elements.forEach(element => { this.appendChild(element); });
const elem = document.createElement("div");
elem.classList.add("rajs-list");
return elem;
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);
}
}
class OptionsItem extends Element {
constructor(label, onclick) {
const elem = document.createElement("div");
const labelel = document.createElement("span");
}
getSelection() {
return (this.children
.filter(child => child.selected)
.map(child => child.setvalue)
getAllValues() {
return this.children.map(child => child.setvalue);
}
super(label, selected);
this.selected = selected;
this.setvalue = setvalue ? setvalue : label;
const labelel = document.createElement("span");
labelel.innerHTML = label;
const button = document.createElement("input");
button.setAttribute("type", "checkbox");
button.checked = selected;
button.onchange = () => this.onchange(button.checked);