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
/**
* @param {HTMLDivElement} div container for the RA UI
*/
App.RA.options = (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);
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]) {
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
711
712
713
/** @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);
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);
}

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);
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) {
super(prefix, data, allowNullValue, true, true, min, max);
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
}
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;
// Basically just a copy of NumericTargetEditor modified to handle strings as well
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
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,
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);
}
// 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)));