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 = [];
child.parent = this;
this.children.push(child);
this.element.appendChild(child.element);
/**
* 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();
}
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);
this.hidey.appendChild(child.element);
}
toggle_hidey() {
case "none":
this.hidey.style.display = "initial";
break;
default:
this.hidey.style.display = "none";
break;
}
}
}
// 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
* @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)));
const elem = document.createElement("div");
const label = document.createElement("span");
label.innerHTML = prefix;
this.value = editor ? this.createEditor(...args) : document.createElement("strong");
return (this.value.tagName === "INPUT" ? this.parse(this.value.value) : this.selectedItem.data);
/**
* @private
* @param {string} display
* @param {any} data
* @returns {ListItem}
*/
_createListItem(display, data) {
if (this._capitalizeShortcuts) {
return new ListItem(capFirstChar(display), data);
} else {
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;
class ListSelector extends Element {
constructor(prefix, data = [], allowNullValue = true) {
super(`${prefix}: `, data, allowNullValue);
}
render(prefix, data, allowNullValue) {
const elem = document.createElement("div");
const label = document.createElement("span");
label.innerHTML = prefix;
this.value = document.createElement("select");
elem.appendChild(label);
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() {
return this.value.value === noDefaultSetting.value ? null : this.value.value;
}
setValue(what) {
this.value.value = what === null ? noDefaultSetting.value : what;
}
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);
}
}
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
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;
}
}
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
class BooleanSwitch extends Element {
/**
* @param {string} prefix
* @param {Array} values values for "false" and "true"
*/
constructor(prefix, values = [false, true]) {
super(prefix);
/** @private */
this.values_ = {
false: values[0],
true: values[1]
};
}
render(prefix) {
const elem = document.createElement("div");
const label = document.createElement("span");
label.innerHTML = `${prefix}: `;
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);
elem.appendChild(label);
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 NumberRange 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);
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 = document.createElement("input");
this.numEditor.setAttribute("type", "number");
this.numEditor.setAttribute("min", min);
this.numEditor.setAttribute("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);
// a way to organize lists with too many elements in subsections
// children are bound to the master list
class ListSubSection extends Element {
const elem = document.createElement("div");
const lelem = document.createElement("em");
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 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));
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))));
const elem = document.querySelector("#application-log") || document.createElement("div");
elem.id = "application-log";
}
}
class RenameField extends Element {
constructor(root) {
this.element.onblur = () => changeName(this.element.value, root);
this.element.onkeypress = (e) => { if (returnP(e)) { changeName(this.element.value, root); } };
elem.setAttribute("value", current_rule.name);
return elem;
render(...args) {
let element = document.getElementById("exportfield");
if (element === null) {
this.appendChild(new ConditionFunction());
this.appendChild(new AssignmentInclusion());
this.appendChild(new SpecialInclusion());
const items = [
["Never", false],
["Always", true],
["Custom", "custom"],
["Devotion", "devotion"],
["Trust", "trust"],
["Health", "health"],
["Weight", "weight"],
["Age", "actualAge"],
["Body Age", "physicalAge"],
["Visible Age", "visualAge"],
["Muscles", "muscles"],
["Lactation", "lactation"],
["Pregnancy", "preg"],
["Pregnancy Multiples", "pregType"],
["Belly Implant", "bellyImplant"],
["Belly Size", "belly"],
["Education", "intelligenceImplant"],
["Intelligence", "intelligence"],
["Fetish", "fetish"],
];
this.fnlist = new List("Activation function", items);
this.fnlist.setValue(current_rule.condition.function === "between" ? current_rule.condition.data.attribute : current_rule.condition.function);
this.show_custom_editor(CustomEditor, current_rule.condition.data);
this.show_custom_editor(RangeEditor, current_rule.condition.function, current_rule.condition.data);
case "belongs":
this.show_custom_editor(ItemEditor, current_rule.condition.function, current_rule.condition.data);
break;
betweenP(attribute) {
return [
"devotion",
"trust",
"health",
"energy",
"weight",
"actualAge",
"physicalAge",
"visualAge",
"muscles",
"lactation",
"preg",
"pregType",
"bellyImplant",
"belly",
"intelligenceImplant",
"intelligence",
].includes(attribute);
}
belongsP(attribute) {
return [
"fetish",
this.custom_editor = new what(...args);
this.appendChild(this.custom_editor);
}
hide_custom_editor() {
if (this.custom_editor) {
this.custom_editor.remove();
this.custom_editor = null;
}
}
const elem = document.createElement("div");
return elem;
if (value === true || value === false) {
current_rule.condition.function = value;
current_rule.condition.data = {};
this.hide_custom_editor();
} else if (value === "custom") {
current_rule.condition.function = "custom";
current_rule.condition.data = "";
this.show_custom_editor(CustomEditor, current_rule.condition.data);
} else if (this.betweenP(value)) {
current_rule.condition.function = "between";
current_rule.condition.data = {attribute: value, value: [null, null]};
this.show_custom_editor(RangeEditor, current_rule.condition.function, current_rule.condition.data);
} else if (this.belongsP(value)) {
current_rule.condition.function = "belongs";
this.show_custom_editor(ItemEditor, current_rule.condition.function, current_rule.condition.data);
}
}
}
class CustomEditor extends Element {
constructor(data) {
if (data.length === 0) { data = "(slave) => slave.slaveName === 'Fancy Name'"; }
const elem = document.createElement("div");
const textarea = document.createElement("textarea");
textarea.innerHTML = data;
elem.appendChild(textarea);
const explanation = document.createElement("div");
explanation.innerHTML = "Insert a valid <a target='_blank' class='link-external' href='https://www.w3schools.com/js/js_comparisons.asp'>JavaScript comparison and/or logical operation</a>.";
elem.appendChild(explanation);
const minlabel = document.createElement("label");
minlabel.innerHTML = "Lower bound: ";
elem.appendChild(minlabel);
const min = document.createElement("input");
min.setAttribute("type", "text");
elem.appendChild(document.createElement("br"));
const maxlabel = document.createElement("label");
maxlabel.innerHTML = "Upper bound: ";
elem.appendChild(maxlabel);
const max = document.createElement("input");
max.setAttribute("type", "text");
const infobar = document.createElement("div");
infobar.innerHTML = this.info(data.attribute);
elem.appendChild(infobar);