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"};
V = State.variables;
V.nextButton = "Back to Main";
V.nextLink = "Main";
V.returnTo = "Main";
V.showEncyclopedia = 1;
V.encyclopedia = "Personal Assistant";
if (V.currentRule !== null) {
const idx = V.defaultRules.findIndex(rule => rule.ID === V.currentRule);
function returnP(e) { return e.keyCode === 13; }
const rule = emptyDefaultRule();
V.defaultRules.push(rule);
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;
const idx = V.defaultRules.findIndex(rule => rule.ID === current_rule.ID);
const idx = V.defaultRules.findIndex(rule => rule.ID === current_rule.ID);
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);
}
}
// 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) {
/** @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);
}
if (this.value) {
if (this.value.tagName === "INPUT") {
this.value.value = str;
} else {
this.value.innerHTML = str;
}
return (this.value.tagName === "INPUT" ? this.parse(this.value.value) : this.selectedItem.data);
_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;
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_;
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
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) => {
if (this.values.has(what)) {
super.setValue(this.values.get(what));
} else {
super.setValue(what);
}
}
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
class StringEditor extends EditorWithShortcuts {
constructor(prefix, data = [], allowNullValue = true, capitalizeShortcuts = true) {
super(prefix, data, allowNullValue, true, capitalizeShortcuts);
}
createEditor() {
let res = document.createElement("input");
res.setAttribute("type", "text");
res.classList.add("rajs-value"); //
// call the variable binding when the input field is no longer being edited, and when the enter key is pressed
res.onblur = () => {
this.inputEdited();
};
res.onkeypress = (e) => {
if (returnP(e)) { this.inputEdited(); }
};
return res;
}
getData() {
return this.value.value;
}
setValue(what) {
this.value.value = what;
}
}
/**
* @param {string} prefix
* @param {Array} values values for "false" and "true"
*/
constructor(prefix, values = [false, true]) {
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
/** @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) {
this.checkBox_.checked = this.values_[true] === 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);
return what === "" ? null : parseInt(what);
}
setValue(what) {
if (typeof what === 'number') { // comes from a pre-set
this.numEditor.value = what.toString();
} else if (what === null) {
this.numEditor.value = null;
this.opSelector.value = '==';
} else if (typeof what === 'object') {
this.numEditor.value = what.val;
this.opSelector.value = what.cond;
}
}
const v = this.parse(this.numEditor.value);
return v === null ? null : App.RA.makeTarget(this.opSelector.value, v);
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
class NumericRangeEditor extends EditorWithShortcuts {
/**
* @param {string} prefix
* @param {Array} [data=[]]
* @param {boolean} [allowNullValue=true]
* @param {number} [min=0]
* @param {number} [max=100]
*/
constructor(prefix, data = [], allowNullValue = true, min = 0, max = 100) {
super(prefix, data, allowNullValue, true, true, min, max);
}
createEditor(min, max) {
this._min = min;
this._max = max;
let res = document.createElement("span");
function makeElem(lbl, container, editor) {
const spinBox = document.createElement("input");
spinBox.type = "number";
spinBox.min = min;
spinBox.max = max;
const label = document.createElement("span");
label.textContent = lbl;
label.className = "ra-inline-label";
const elem = document.createElement("span");
elem.appendChild(label);
elem.appendChild(spinBox);
container.appendChild(elem);
spinBox.onblur = () => {
editor.inputEdited();
};
spinBox.onkeypress = (e) => {
if (returnP(e)) { editor.inputEdited(); }
};
return spinBox;
}
this._minEditor = makeElem("Min", res, this);
this._maxEditor = makeElem("Max", res, this);
return res;
}
getData() {
function parse(what) {
return what === "" ? null : parseInt(what);
}
const vMin = parse(this._minEditor.value);
const vMax = parse(this._maxEditor.value);
return (vMin === null && vMax === null) ? null :
App.RA.makeRange(vMin !== null ? vMin : this._min, vMax !== null ? vMax : this._max);
}
setValue(what) {
if (what === null) {
this._minEditor.value = null;
this._maxEditor.value = null;
} else {
this._minEditor.value = what.min;
this._maxEditor.value = what.max;
// 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)
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);
textarea.placeholder = "Paste your rule here";
container.appendChild(textarea);
this.textarea = textarea;
const button = document.createElement("button");
button.name = "Load";
V.defaultRules.push(App.Entity.Utils.RARuleDatatypeCleanup(r));
V.defaultRules.push(App.Entity.Utils.RARuleDatatypeCleanup(rule));
const paragraph = document.createElement("p");
paragraph.innerHTML = "<strong>No rules</strong>";
this.appendChild(new Element(paragraph));
this.appendChild(new NoRules(this));
return;
this.appendChild(new RuleSelector(this));
this.appendChild(new RuleOptions(this));
App.UI.tabbar.handlePreSelectedTab("appearance", true);
greeting.innerHTML = `<em>${properTitle()}, I will review your slaves and make changes that will have a beneficial effect. Apologies, ${properTitle()}, but this function is... not fully complete. It may have some serious limitations. Please use the '${noDefaultSetting.text}' option to identify areas I should not address.</em>`;
const newrule = new OptionsItem("Add a new rule", () => { newRule(this.root); });
const importrule = new OptionsItem("Import a rule", () => { this.root.appendChild(new NewRuleField(this.root)); });
// buttons for selecting the current rule
class RuleSelector extends List {
constructor(root) {
super("Current rule", V.defaultRules.map(i => [i.name, i]), false);
}
}
// buttons for doing transformations on rules
class RuleOptions extends Options {
constructor(root) {
super();
this.appendChild(new OptionsItem("New Rule", () => newRule(root)));
this.appendChild(new OptionsItem("Remove Rule", () => removeRule(root)));
this.appendChild(new OptionsItem("Apply rules", () => this.appendChild(new ApplicationLog())));
this.appendChild(new OptionsItem("Lower Priority", () => lowerPriority(root)));
this.appendChild(new OptionsItem("Higher Priority", () => higherPriority(root)));
this.appendChild(new OptionsItem("Rename", () => this.appendChild(new RenameField(root))));
this.appendChild(new OptionsItem("Export this rule", () => this.appendChild(new ExportField(current_rule))));
this.appendChild(new OptionsItem("Export all rules", () => this.appendChild(new ExportField(...V.defaultRules))));
this.appendChild(new OptionsItem("Import rule(s)", () => this.appendChild(new NewRuleField(root))));