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
const noDefaultSetting = {value: "!NDS!", text: "no default setting"};

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

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.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();

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;
tab.className = "tabcontent";
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.className = "tablinks";
btn.id = `tab ${name}`;
btn.innerHTML = text;
btn.onclick = (event) => App.UI.tabbar.openTab(event, name);
return btn;
}
}
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)
{
this.selectItem(selected[0]);
}
else if(this._allowNullValue) {
this.setValue(null);
}
}

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

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

MouseOfLight
committed
}

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

MouseOfLight
committed
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;
}
}
_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 {
const elem = document.createElement("span");
elem.classList.add("rajs-listitem");
elem.innerHTML = displayvalue;

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());
}
/**
*
* @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_;
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
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());
}
}
}
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
? `(${capFirstChar(noDefaultSetting.text)})`
: '');
}

MouseOfLight
committed
getTextData() {

MouseOfLight
committed
setTextValue(what) {
*/
constructor(prefix, values = [false, true]) {
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
/** @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);

MouseOfLight
committed
dataEqual(left, right) {
debugger;
if (left == null && right == null) { return true; }
if (left == null || right == null) { return false; }
//ignore the operator
if(_.isObject(left )) { left = left .val; }
if(_.isObject(right)) { right = right.val; }
return 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);
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
}
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("span");
elem.classList.add("rajs-listitem");
elem.innerHTML = label;
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 container = document.createElement("div");
container.classList.add("rajs-listitem");
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);

MouseOfLight
committed
constructor() {
textarea.placeholder = "Paste your rule here";
container.appendChild(textarea);
this.textarea = textarea;
const button = document.createElement("button");
button.name = "Load";