1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-22 09:00:10 +00:00

settings: de-dupe settings

all DOM elements now based off DOMSetting, which encompasses most
functionality. Extending classes (i forgot the terminology) pretty much just pass a
custom "input" element, "hider" element (the one to unfocus). DOMList
and DOMSelect remain slightly more complicated, but are much cleaner
now. Some CSS stuff has been adjusted too.
This commit is contained in:
Harvey Tindall 2024-08-23 21:12:16 +01:00
parent 32161139b2
commit a7aa3fd53e
Signed by: hrfee
GPG Key ID: BBC65952848FB1A2
4 changed files with 211 additions and 436 deletions

View File

@ -127,6 +127,8 @@ func (app *appContext) loadConfig() error {
app.MustSetValue("user_expiry", "adjustment_email_html", "jfa-go:"+"expiry-adjusted.html") app.MustSetValue("user_expiry", "adjustment_email_html", "jfa-go:"+"expiry-adjusted.html")
app.MustSetValue("user_expiry", "adjustment_email_text", "jfa-go:"+"expiry-adjusted.txt") app.MustSetValue("user_expiry", "adjustment_email_text", "jfa-go:"+"expiry-adjusted.txt")
app.MustSetValue("email", "collect", "true")
app.MustSetValue("matrix", "topic", "Jellyfin notifications") app.MustSetValue("matrix", "topic", "Jellyfin notifications")
app.MustSetValue("matrix", "show_on_reg", "true") app.MustSetValue("matrix", "show_on_reg", "true")

View File

@ -896,11 +896,20 @@
"value": false, "value": false,
"description": "Send emails as plain text instead of HTML." "description": "Send emails as plain text instead of HTML."
}, },
"collect": {
"name": "Collect on sign-up",
"required": false,
"requires_restart": false,
"depends_true": "method",
"type": "bool",
"value": true,
"description": "Ask for an email address on the sign-up form."
},
"required": { "required": {
"name": "Require on sign-up", "name": "Require on sign-up",
"required": false, "required": false,
"requires_restart": false, "requires_restart": false,
"depends_true": "method", "depends_true": "collect",
"type": "bool", "type": "bool",
"value": false, "value": false,
"description": "Require an email address on sign-up." "description": "Require an email address on sign-up."
@ -909,6 +918,7 @@
"name": "Require unique address", "name": "Require unique address",
"required": false, "required": false,
"requires_restart": true, "requires_restart": true,
"depends_true": "method",
"type": "bool", "type": "bool",
"value": false, "value": false,
"description": "Disables using the same address on multiple accounts." "description": "Disables using the same address on multiple accounts."

View File

@ -312,25 +312,27 @@
<form class="relative mx-auto my-[10%] w-4/5 lg:w-2/3 content card" id="form-editor" href=""> <form class="relative mx-auto my-[10%] w-4/5 lg:w-2/3 content card" id="form-editor" href="">
<span class="heading"><span id="header-editor"></span> <span class="modal-close">&times;</span></span> <span class="heading"><span id="header-editor"></span> <span class="modal-close">&times;</span></span>
<div class="row"> <div class="row">
<div class="col card ~neutral @low"> <div class="col card ~neutral @low flex flex-col gap-2 justify-between">
<aside class="aside sm ~urge dark:~d_info mb-2 @low" id="aside-editor"></aside> <div class="flex flex-col gap-2">
<span class="label supra" for="editor-variables" id="label-editor-variables">{{ .strings.variables }}</span> <aside class="aside sm ~urge dark:~d_info @low" id="aside-editor"></aside>
<div id="editor-variables" class="mt-4"></div> <label class="label supra" for="editor-variables" id="label-editor-variables">{{ .strings.variables }}</label>
<div id="editor-variables" class="flex flex-row gap-2 flex-wrap"></div>
<span class="label supra" for="editor-conditionals" id="label-editor-conditionals">{{ .strings.conditionals }}</span> <span class="label supra" for="editor-conditionals" id="label-editor-conditionals">{{ .strings.conditionals }}</span>
<div id="editor-conditionals"></div> <div id="editor-conditionals"></div>
<label class="label supra" for="textarea-editor">{{ .strings.message }}</label> <label class="label supra" for="textarea-editor">{{ .strings.message }}</label>
<textarea id="textarea-editor" class="textarea full-width flex-auto ~neutral @low mt-4 font-mono"></textarea> <textarea id="textarea-editor" class="textarea full-width flex-auto ~neutral @low font-mono"></textarea>
<p class="support mt-4 mb-2">{{ .strings.markdownSupported }}</p> </div>
<div class="flex-row"> <div class="flex flex-col gap-2">
<label class="full-width ml-2"> <p class="support">{{ .strings.markdownSupported }}</p>
<label class="w-full">
<input type="submit" class="unfocused"> <input type="submit" class="unfocused">
<span class="button ~urge @low full-width center supra submit">{{ .strings.submit }}</span> <span class="button ~urge @low w-full supra submit">{{ .strings.submit }}</span>
</label> </label>
</div> </div>
</div> </div>
<div class="col card ~neutral @low"> <div class="col card ~neutral @low flex flex-col gap-2">
<span class="subheading supra">{{ .strings.preview }}</span> <span class="subheading supra">{{ .strings.preview }}</span>
<div class="mt-8" id="editor-preview"></div> <div id="editor-preview"></div>
</div> </div>
</div> </div>
</form> </form>

View File

@ -2,6 +2,12 @@ import { _get, _post, _delete, _download, _upload, toggleLoader, addLoader, remo
import { Marked } from "@ts-stack/markdown"; import { Marked } from "@ts-stack/markdown";
import { stripMarkdown } from "../modules/stripmd.js"; import { stripMarkdown } from "../modules/stripmd.js";
const toBool = (s: string): boolean => {
let b = Boolean(s);
if (s == "false") b = false;
return b;
}
interface BackupDTO { interface BackupDTO {
size: string; size: string;
name: string; name: string;
@ -9,8 +15,8 @@ interface BackupDTO {
date: number; date: number;
} }
interface settingsBoolEvent extends Event { interface settingsChangedEvent extends Event {
detail: boolean; detail: string;
} }
interface Meta { interface Meta {
@ -52,7 +58,8 @@ const splitDependant = (section: string, dep: string): string[] => {
return parts return parts
}; };
class DOMInput { class DOMSetting {
protected _hideEl: HTMLElement;
protected _input: HTMLInputElement; protected _input: HTMLInputElement;
protected _container: HTMLDivElement; protected _container: HTMLDivElement;
protected _tooltip: HTMLDivElement; protected _tooltip: HTMLDivElement;
@ -63,19 +70,19 @@ class DOMInput {
protected _name: string; protected _name: string;
hide = () => { hide = () => {
this._input.parentElement.classList.add("unfocused"); this._hideEl.classList.add("unfocused");
const event = new CustomEvent(`settings-${this._section}-${this._name}`, { "detail": false }) const event = new CustomEvent(`settings-${this._section}-${this._name}`, { "detail": false })
document.dispatchEvent(event); document.dispatchEvent(event);
}; };
show = () => { show = () => {
this._input.parentElement.classList.remove("unfocused"); this._hideEl.classList.remove("unfocused");
const event = new CustomEvent(`settings-${this._section}-${this._name}`, { "detail": this.valueAsString() }) const event = new CustomEvent(`settings-${this._section}-${this._name}`, { "detail": this.valueAsString() })
document.dispatchEvent(event); document.dispatchEvent(event);
}; };
private _advancedListener = (event: settingsBoolEvent) => { private _advancedListener = (event: settingsChangedEvent) => {
if (!Boolean(event.detail)) { if (!toBool(event.detail)) {
this.hide(); this.hide();
} else { } else {
this.show(); this.show();
@ -109,9 +116,11 @@ class DOMInput {
get required(): boolean { return this._required.classList.contains("badge"); } get required(): boolean { return this._required.classList.contains("badge"); }
set required(state: boolean) { set required(state: boolean) {
if (state) { if (state) {
this._required.classList.remove("unfocused");
this._required.classList.add("badge", "~critical"); this._required.classList.add("badge", "~critical");
this._required.textContent = "*"; this._required.textContent = "*";
} else { } else {
this._required.classList.add("unfocused");
this._required.classList.remove("badge", "~critical"); this._required.classList.remove("badge", "~critical");
this._required.textContent = ""; this._required.textContent = "";
} }
@ -120,9 +129,11 @@ class DOMInput {
get requires_restart(): boolean { return this._restart.classList.contains("badge"); } get requires_restart(): boolean { return this._restart.classList.contains("badge"); }
set requires_restart(state: boolean) { set requires_restart(state: boolean) {
if (state) { if (state) {
this._restart.classList.remove("unfocused");
this._restart.classList.add("badge", "~info", "dark:~d_warning"); this._restart.classList.add("badge", "~info", "dark:~d_warning");
this._restart.textContent = "R"; this._restart.textContent = "R";
} else { } else {
this._restart.classList.add("unfocused");
this._restart.classList.remove("badge", "~info", "dark:~d_warning"); this._restart.classList.remove("badge", "~info", "dark:~d_warning");
this._restart.textContent = ""; this._restart.textContent = "";
} }
@ -138,35 +149,38 @@ class DOMInput {
if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); } if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); }
}; };
constructor(inputType: string, setting: Setting, section: string, name: string, customInput?: string) { constructor(input: string, setting: Setting, section: string, name: string, inputOnTop: boolean = false) {
this._section = section; this._section = section;
this._name = name; this._name = name;
this._container = document.createElement("div"); this._container = document.createElement("div");
this._container.classList.add("setting"); this._container.classList.add("setting");
this._container.setAttribute("data-name", name) this._container.setAttribute("data-name", name);
const defaultInput = `
<input type="${inputType}" class="input setting-input ~neutral @low mt-2 mb-2">
`;
this._container.innerHTML = ` this._container.innerHTML = `
<label class="label"> <label class="label flex flex-col gap-2">
<span class="setting-label"></span> <span class="setting-required"></span> <span class="setting-restart"></span> ${inputOnTop ? input : ""}
<div class="flex flex-row gap-2 items-baseline">
<span class="setting-label"></span>
<div class="setting-tooltip tooltip right unfocused"> <div class="setting-tooltip tooltip right unfocused">
<i class="icon ri-information-line"></i> <i class="icon ri-information-line align-baseline"></i>
<span class="content sm"></span> <span class="content sm"></span>
</div> </div>
${customInput ? customInput : defaultInput} <span class="setting-required unfocused"></span>
<span class="setting-restart unfocused"></span>
</div>
${inputOnTop ? "" : input}
</label> </label>
`; `;
this._tooltip = this._container.querySelector("div.setting-tooltip") as HTMLDivElement; this._tooltip = this._container.querySelector("div.setting-tooltip") as HTMLDivElement;
this._required = this._container.querySelector("span.setting-required") as HTMLSpanElement; this._required = this._container.querySelector("span.setting-required") as HTMLSpanElement;
this._restart = this._container.querySelector("span.setting-restart") as HTMLSpanElement; this._restart = this._container.querySelector("span.setting-restart") as HTMLSpanElement;
// "input" variable should supply the HTML of an element with class "setting-input"
this._input = this._container.querySelector(".setting-input") as HTMLInputElement; this._input = this._container.querySelector(".setting-input") as HTMLInputElement;
if (setting.depends_false || setting.depends_true) { if (setting.depends_false || setting.depends_true) {
let dependant = splitDependant(section, setting.depends_true || setting.depends_false); let dependant = splitDependant(section, setting.depends_true || setting.depends_false);
let state = true; let state = true;
if (setting.depends_false) { state = false; } if (setting.depends_false) { state = false; }
document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsBoolEvent) => { document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsChangedEvent) => {
if (Boolean(event.detail) !== state) { if (toBool(event.detail) !== state) {
this.hide(); this.hide();
} else { } else {
this.show(); this.show();
@ -174,13 +188,14 @@ class DOMInput {
}); });
} }
this._input.onchange = this.onValueChange; this._input.onchange = this.onValueChange;
this.update(setting); document.addEventListener(`settings-loaded`, this.onValueChange);
this._hideEl = this._container;
} }
get value(): any { return this._input.value; } get value(): any { return this._input.value; }
set value(v: any) { this._input.value = v; } set value(v: any) { this._input.value = v; }
update = (s: Setting) => { update(s: Setting) {
this.name = s.name; this.name = s.name;
this.description = s.description; this.description = s.description;
this.required = s.required; this.required = s.required;
@ -192,6 +207,17 @@ class DOMInput {
asElement = (): HTMLDivElement => { return this._container; } asElement = (): HTMLDivElement => { return this._container; }
} }
class DOMInput extends DOMSetting {
constructor(inputType: string, setting: Setting, section: string, name: string) {
super(
`<input type="${inputType}" class="input setting-input ~neutral @low">`,
setting, section, name,
);
// this._hideEl = this._input.parentElement;
this.update(setting);
}
}
interface SText extends Setting { interface SText extends Setting {
value: string; value: string;
} }
@ -202,85 +228,6 @@ class DOMText extends DOMInput implements SText {
set value(v: string) { this._input.value = v; } set value(v: string) { this._input.value = v; }
} }
interface SList extends Setting {
value: string[];
}
class DOMList extends DOMInput implements SList {
protected _inputs: HTMLDivElement;
type: string = "list";
valueAsString = (): string => { return this.value.join("|"); };
get value(): string[] {
let values = [];
const inputs = this._input.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
for (let i in inputs) {
if (inputs[i].value) values.push(inputs[i].value);
}
return values;
}
set value(v: string[]) {
this._input.textContent = ``;
for (let val of v) {
let input = this.inputRow(val);
this._input.appendChild(input);
}
const addDummy = () => {
const dummyRow = this.inputRow();
const input = dummyRow.querySelector("input") as HTMLInputElement;
input.placeholder = window.lang.strings("add");
const onDummyChange = () => {
if (!(input.value)) return;
addDummy();
input.removeEventListener("change", onDummyChange);
input.placeholder = ``;
}
input.addEventListener("change", onDummyChange);
this._input.appendChild(dummyRow);
};
addDummy();
}
private inputRow(v: string = ""): HTMLDivElement {
let container = document.createElement("div") as HTMLDivElement;
container.classList.add("flex", "flex-row", "justify-between");
container.innerHTML = `
<input type="text" class="input ~neutral @low">
<button class="button ~neutral @low center -ml-10 rounded-s-none aria-label="${window.lang.strings("delete")}" title="${window.lang.strings("delete")}">
<i class="ri-close-line"></i>
</button>
`;
const input = container.querySelector("input") as HTMLInputElement;
input.value = v;
input.onchange = this.onValueChange;
const removeRow = container.querySelector("button") as HTMLButtonElement;
removeRow.onclick = () => {
if (!(container.nextElementSibling)) return;
container.remove();
this.onValueChange();
}
return container;
}
update = (s: Setting) => {
this.name = s.name;
this.description = s.description;
this.required = s.required;
this.requires_restart = s.requires_restart;
this.value = s.value as string[];
this.advanced = s.advanced;
}
asElement = (): HTMLDivElement => { return this._container; }
constructor(setting: Setting, section: string, name: string) {
super("list", setting, section, name,
`<div class="setting-input flex flex-col gap-2 mt-2 mb-2"></div>`
);
}
}
interface SPassword extends Setting { interface SPassword extends Setting {
value: string; value: string;
} }
@ -311,235 +258,107 @@ class DOMNumber extends DOMInput implements SNumber {
set value(v: number) { this._input.value = ""+v; } set value(v: number) { this._input.value = ""+v; }
} }
interface SList extends Setting {
value: string[];
}
class DOMList extends DOMSetting implements SList {
protected _inputs: HTMLDivElement;
type: string = "list";
valueAsString = (): string => { return this.value.join("|"); };
get value(): string[] {
let values = [];
const inputs = this._input.querySelectorAll("input") as NodeListOf<HTMLInputElement>;
for (let i in inputs) {
if (inputs[i].value) values.push(inputs[i].value);
}
return values;
}
set value(v: string[]) {
this._input.textContent = ``;
for (let val of v) {
let input = this.inputRow(val);
this._input.appendChild(input);
}
const addDummy = () => {
const dummyRow = this.inputRow();
const input = dummyRow.querySelector("input") as HTMLInputElement;
input.placeholder = window.lang.strings("add");
const onDummyChange = () => {
if (!(input.value)) return;
addDummy();
input.removeEventListener("change", onDummyChange);
input.removeEventListener("keyup", onDummyChange);
input.placeholder = ``;
}
input.addEventListener("change", onDummyChange);
input.addEventListener("keyup", onDummyChange);
this._input.appendChild(dummyRow);
};
addDummy();
}
private inputRow(v: string = ""): HTMLDivElement {
let container = document.createElement("div") as HTMLDivElement;
container.classList.add("flex", "flex-row", "justify-between");
container.innerHTML = `
<input type="text" class="input ~neutral @low">
<button class="button ~neutral @low center -ml-10 rounded-s-none aria-label="${window.lang.strings("delete")}" title="${window.lang.strings("delete")}">
<i class="ri-close-line"></i>
</button>
`;
const input = container.querySelector("input") as HTMLInputElement;
input.value = v;
input.onchange = this.onValueChange;
const removeRow = container.querySelector("button") as HTMLButtonElement;
removeRow.onclick = () => {
if (!(container.nextElementSibling)) return;
container.remove();
this.onValueChange();
}
return container;
}
constructor(setting: Setting, section: string, name: string) {
super(
`<div class="setting-input flex flex-col gap-2"></div>`,
setting, section, name,
);
// this._hideEl = this._input.parentElement;
this.update(setting);
}
}
interface SBool extends Setting { interface SBool extends Setting {
value: boolean; value: boolean;
} }
class DOMBool implements SBool { class DOMBool extends DOMSetting implements SBool {
protected _input: HTMLInputElement;
private _container: HTMLDivElement;
private _tooltip: HTMLDivElement;
private _required: HTMLSpanElement;
private _restart: HTMLSpanElement;
type: string = "bool"; type: string = "bool";
private _advanced: boolean;
protected _section: string;
protected _name: string;
hide = () => {
this._input.parentElement.classList.add("unfocused");
const event = new CustomEvent(`settings-${this._section}-${this._name}`, { "detail": false })
document.dispatchEvent(event);
};
show = () => {
this._input.parentElement.classList.remove("unfocused");
const event = new CustomEvent(`settings-${this._section}-${this._name}`, { "detail": this.valueAsString() })
document.dispatchEvent(event);
};
private _advancedListener = (event: settingsBoolEvent) => {
if (!Boolean(event.detail)) {
this.hide();
} else {
this.show();
}
}
get advanced(): boolean { return this._advanced; }
set advanced(advanced: boolean) {
this._advanced = advanced;
if (advanced) {
document.addEventListener("settings-advancedState", this._advancedListener);
} else {
document.removeEventListener("settings-advancedState", this._advancedListener);
}
}
get name(): string { return this._container.querySelector("span.setting-label").textContent; }
set name(n: string) { this._container.querySelector("span.setting-label").textContent = n; }
get description(): string { return this._tooltip.querySelector("span.content").textContent; }
set description(d: string) {
const content = this._tooltip.querySelector("span.content") as HTMLSpanElement;
content.textContent = d;
if (d == "") {
this._tooltip.classList.add("unfocused");
} else {
this._tooltip.classList.remove("unfocused");
}
}
get required(): boolean { return this._required.classList.contains("badge"); }
set required(state: boolean) {
if (state) {
this._required.classList.add("badge", "~critical");
this._required.textContent = "*";
} else {
this._required.classList.remove("badge", "~critical");
this._required.textContent = "";
}
}
get requires_restart(): boolean { return this._restart.classList.contains("badge"); }
set requires_restart(state: boolean) {
if (state) {
this._restart.classList.add("badge", "~info", "dark:~d_warning");
this._restart.textContent = "R";
} else {
this._restart.classList.remove("badge", "~info", "dark:~d_warning");
this._restart.textContent = "";
}
}
valueAsString = (): string => { return ""+this.value; };
get value(): boolean { return this._input.checked; } get value(): boolean { return this._input.checked; }
set value(state: boolean) { this._input.checked = state; } set value(state: boolean) { this._input.checked = state; }
constructor(setting: SBool, section: string, name: string) { constructor(setting: SBool, section: string, name: string) {
this._section = section; super(
this._name = name; `<input type="checkbox" class="setting-input">`,
this._container = document.createElement("div"); setting, section, name, true,
this._container.classList.add("setting"); );
this._container.setAttribute("data-name", name) const label = this._container.getElementsByTagName("LABEL")[0];
this._container.innerHTML = ` label.classList.remove("flex-col");
<label class="switch mb-2"> label.classList.add("flex-row");
<input type="checkbox"> // this._hideEl = this._input.parentElement;
<span class="setting-label"></span> <span class="setting-required"></span> <span class="setting-restart"></span>
<div class="setting-tooltip tooltip right unfocused">
<i class="icon ri-information-line"></i>
<span class="content sm"></span>
</div>
</label>
`;
this._tooltip = this._container.querySelector("div.setting-tooltip") as HTMLDivElement;
this._required = this._container.querySelector("span.setting-required") as HTMLSpanElement;
this._restart = this._container.querySelector("span.setting-restart") as HTMLSpanElement;
this._input = this._container.querySelector("input[type=checkbox]") as HTMLInputElement;
const onValueChange = () => {
const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.valueAsString() })
const setEvent = new CustomEvent(`settings-set-${section}-${name}`, { "detail": this.valueAsString() })
document.dispatchEvent(event);
document.dispatchEvent(setEvent);
};
this._input.onchange = () => {
onValueChange();
if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); }
};
document.addEventListener(`settings-loaded`, onValueChange);
if (setting.depends_false || setting.depends_true) {
let dependant = splitDependant(section, setting.depends_true || setting.depends_false);
let state = true;
if (setting.depends_false) { state = false; }
console.log(`I, ${section}-${name}, am adding a listener for ${dependant[0]}-${dependant[1]}`);
document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsBoolEvent) => {
console.log(`I, ${section}-${name}, was triggered by a listener for ${dependant[0]}-${dependant[1]}`);
if (Boolean(event.detail) !== state) {
this.hide();
} else {
this.show();
}
});
}
this.update(setting); this.update(setting);
} }
update = (s: SBool) => {
this.name = s.name;
this.description = s.description;
this.required = s.required;
this.requires_restart = s.requires_restart;
this.value = s.value;
this.advanced = s.advanced;
}
asElement = (): HTMLDivElement => { return this._container; }
} }
interface SSelect extends Setting { interface SSelect extends Setting {
options: string[][]; options: string[][];
value: string; value: string;
} }
class DOMSelect implements SSelect { class DOMSelect extends DOMSetting implements SSelect {
protected _select: HTMLSelectElement;
private _container: HTMLDivElement;
private _tooltip: HTMLDivElement;
private _required: HTMLSpanElement;
private _restart: HTMLSpanElement;
private _options: string[][];
type: string = "bool"; type: string = "bool";
private _advanced: boolean; private _options: string[][];
protected _section: string;
protected _name: string;
hide = () => {
this._container.classList.add("unfocused");
const event = new CustomEvent(`settings-${this._section}-${this._name}`, { "detail": false })
document.dispatchEvent(event);
};
show = () => {
this._container.classList.remove("unfocused");
const event = new CustomEvent(`settings-${this._section}-${this._name}`, { "detail": this.valueAsString() })
document.dispatchEvent(event);
};
private _advancedListener = (event: settingsBoolEvent) => {
if (!Boolean(event.detail)) {
this.hide();
} else {
this.show();
}
}
get advanced(): boolean { return this._advanced; }
set advanced(advanced: boolean) {
this._advanced = advanced;
if (advanced) {
document.addEventListener("settings-advancedState", this._advancedListener);
} else {
document.removeEventListener("settings-advancedState", this._advancedListener);
}
}
get name(): string { return this._container.querySelector("span.setting-label").textContent; }
set name(n: string) { this._container.querySelector("span.setting-label").textContent = n; }
get description(): string { return this._tooltip.querySelector("span.content").textContent; }
set description(d: string) {
const content = this._tooltip.querySelector("span.content") as HTMLSpanElement;
content.textContent = d;
if (d == "") {
this._tooltip.classList.add("unfocused");
} else {
this._tooltip.classList.remove("unfocused");
}
}
get required(): boolean { return this._required.classList.contains("badge"); }
set required(state: boolean) {
if (state) {
this._required.classList.add("badge", "~critical");
this._required.textContent = "*";
} else {
this._required.classList.remove("badge", "~critical");
this._required.textContent = "";
}
}
get requires_restart(): boolean { return this._restart.classList.contains("badge"); }
set requires_restart(state: boolean) {
if (state) {
this._restart.classList.add("badge", "~info", "dark:~d_warning");
this._restart.textContent = "R";
} else {
this._restart.classList.remove("badge", "~info", "dark:~d_warning");
this._restart.textContent = "";
}
}
valueAsString = (): string => { return ""+this.value; };
get value(): string { return this._select.value; }
set value(v: string) { this._select.value = v; }
get options(): string[][] { return this._options; } get options(): string[][] { return this._options; }
set options(opt: string[][]) { set options(opt: string[][]) {
@ -548,98 +367,47 @@ class DOMSelect implements SSelect {
for (let option of this._options) { for (let option of this._options) {
innerHTML += `<option value="${option[0]}">${option[1]}</option>`; innerHTML += `<option value="${option[0]}">${option[1]}</option>`;
} }
this._select.innerHTML = innerHTML; this._input.innerHTML = innerHTML;
} }
update(s: SSelect) {
this.options = s.options;
super.update(s);
};
constructor(setting: SSelect, section: string, name: string) { constructor(setting: SSelect, section: string, name: string) {
this._section = section; super(
this._name = name; `<div class="select ~neutral @low">
this._options = []; <select class="setting-select setting-input"></select>
this._container = document.createElement("div"); </div>`,
this._container.classList.add("setting"); setting, section, name,
this._container.setAttribute("data-name", name)
this._container.innerHTML = `
<label class="label">
<span class="setting-label"></span> <span class="setting-required"></span> <span class="setting-restart"></span>
<div class="setting-tooltip tooltip right unfocused">
<i class="icon ri-information-line"></i>
<span class="content sm"></span>
</div>
<div class="select ~neutral @low mt-2 mb-2">
<select class="settings-select"></select>
</div>
</label>
`;
this._tooltip = this._container.querySelector("div.setting-tooltip") as HTMLDivElement;
this._required = this._container.querySelector("span.setting-required") as HTMLSpanElement;
this._restart = this._container.querySelector("span.setting-restart") as HTMLSpanElement;
this._select = this._container.querySelector("select.settings-select") as HTMLSelectElement;
if (setting.depends_false || setting.depends_true) {
let dependant = splitDependant(section, setting.depends_true || setting.depends_false);
let state = true;
if (setting.depends_false) { state = false; }
document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsBoolEvent) => {
if (Boolean(event.detail) !== state) {
this.hide();
} else {
this.show();
}
});
}
const onValueChange = () => {
const event = new CustomEvent(`settings-${section}-${name}`, { "detail": this.valueAsString() });
const setEvent = new CustomEvent(`settings-${section}-${name}`, { "detail": this.valueAsString() });
document.dispatchEvent(event);
document.dispatchEvent(setEvent);
if (this.requires_restart) { document.dispatchEvent(new CustomEvent("settings-requires-restart")); }
};
this._select.onchange = onValueChange;
document.addEventListener(`settings-loaded`, onValueChange);
const message = document.getElementById("settings-message") as HTMLElement;
message.innerHTML = window.lang.var("strings",
"settingsRequiredOrRestartMessage",
`<span class="badge ~critical">*</span>`,
`<span class="badge ~info dark:~d_warning">R</span>`
); );
this._options = [];
// this._hideEl = this._container;
this.update(setting); this.update(setting);
} }
update = (s: SSelect) => {
this.name = s.name;
this.description = s.description;
this.required = s.required;
this.requires_restart = s.requires_restart;
this.options = s.options;
this.value = s.value;
}
asElement = (): HTMLDivElement => { return this._container; }
} }
interface SNote extends Setting { interface SNote extends Setting {
value: string; value: string;
style?: string; style?: string;
} }
class DOMNote implements SNote { class DOMNote extends DOMSetting implements SNote {
private _container: HTMLDivElement; private _nameEl: HTMLElement;
private _aside: HTMLElement;
private _name: HTMLElement;
private _description: HTMLElement; private _description: HTMLElement;
type: string = "note"; type: string = "note";
private _style: string; private _style: string;
// We're a note, no one depends on us so we don't need to broadcast a state change.
hide = () => { hide = () => {
this._container.classList.add("unfocused"); this._container.classList.add("unfocused");
// We're a note, no one depends on us so we don't need to broadcast a state change.
}; };
show = () => { show = () => {
this._container.classList.remove("unfocused"); this._container.classList.remove("unfocused");
// We're a note, no one depends on us so we don't need to broadcast a state change.
}; };
get name(): string { return this._name.textContent; } get name(): string { return this._nameEl.textContent; }
set name(n: string) { this._name.textContent = n; } set name(n: string) { this._nameEl.textContent = n; }
get description(): string { return this._description.textContent; } get description(): string { return this._description.textContent; }
set description(d: string) { set description(d: string) {
@ -649,51 +417,37 @@ class DOMNote implements SNote {
valueAsString = (): string => { return ""; }; valueAsString = (): string => { return ""; };
get value(): string { return ""; } get value(): string { return ""; }
set value(v: string) { return; } set value(_: string) { return; }
get required(): boolean { return false; } get required(): boolean { return false; }
set required(v: boolean) { return; } set required(_: boolean) { return; }
get requires_restart(): boolean { return false; } get requires_restart(): boolean { return false; }
set requires_restart(v: boolean) { return; } set requires_restart(_: boolean) { return; }
get style(): string { return this._style; } get style(): string { return this._style; }
set style(s: string) { set style(s: string) {
this._aside.classList.remove("~" + this._style); this._input.classList.remove("~" + this._style);
this._style = s; this._style = s;
this._aside.classList.add("~" + this._style); this._input.classList.add("~" + this._style);
} }
constructor(setting: SNote, section: string) { constructor(setting: SNote, section: string) {
this._container = document.createElement("div"); super(
this._container.classList.add("setting"); `
this._container.innerHTML = ` <aside class="aside setting-input">
<aside class="aside my-2">
<span class="font-bold setting-name"></span> <span class="font-bold setting-name"></span>
<span class="content setting-description"> <span class="content setting-description">
</aside> </aside>
`; `, setting, section, "",
this._name = this._container.querySelector(".setting-name"); );
// this._hideEl = this._container;
this._nameEl = this._container.querySelector(".setting-name");
this._description = this._container.querySelector(".setting-description"); this._description = this._container.querySelector(".setting-description");
this._aside = this._container.querySelector("aside");
if (setting.depends_false || setting.depends_true) {
let dependant = splitDependant(section, setting.depends_true || setting.depends_false);
let state = true;
if (setting.depends_false) { state = false; }
document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsBoolEvent) => {
if (Boolean(event.detail) !== state) {
this.hide();
} else {
this.show();
}
});
}
this.update(setting); this.update(setting);
} }
update = (s: SNote) => { update(s: SNote) {
this.name = s.name; this.name = s.name;
this.description = s.description; this.description = s.description;
this.style = ("style" in s && s.style) ? s.style : "info"; this.style = ("style" in s && s.style) ? s.style : "info";
@ -718,7 +472,7 @@ class sectionPanel {
this._sectionName = sectionName; this._sectionName = sectionName;
this._settings = {}; this._settings = {};
this._section = document.createElement("div") as HTMLDivElement; this._section = document.createElement("div") as HTMLDivElement;
this._section.classList.add("settings-section", "unfocused"); this._section.classList.add("settings-section", "unfocused", "flex", "flex-col", "gap-2");
this._section.setAttribute("data-section", sectionName); this._section.setAttribute("data-section", sectionName);
let innerHTML = ` let innerHTML = `
<div class="flex flex-row justify-between"> <div class="flex flex-row justify-between">
@ -730,7 +484,7 @@ class sectionPanel {
innerHTML += ` innerHTML += `
</div> </div>
<p class="support lg my-2 settings-section-description">${s.meta.description}</p> <p class="support lg settings-section-description">${s.meta.description}</p>
`; `;
this._section.innerHTML = innerHTML; this._section.innerHTML = innerHTML;
@ -840,9 +594,8 @@ export class settingsList {
let dependant = splitDependant(name, s.meta.depends_true || s.meta.depends_false); let dependant = splitDependant(name, s.meta.depends_true || s.meta.depends_false);
let state = true; let state = true;
if (s.meta.depends_false) { state = false; } if (s.meta.depends_false) { state = false; }
document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsBoolEvent) => { document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsChangedEvent) => {
if (toBool(event.detail) !== state) {
if (Boolean(event.detail) !== state) {
button.classList.add("unfocused"); button.classList.add("unfocused");
document.dispatchEvent(new CustomEvent(`settings-${name}`, { detail: false })); document.dispatchEvent(new CustomEvent(`settings-${name}`, { detail: false }));
} else { } else {
@ -850,16 +603,16 @@ export class settingsList {
document.dispatchEvent(new CustomEvent(`settings-${name}`, { detail: true })); document.dispatchEvent(new CustomEvent(`settings-${name}`, { detail: true }));
} }
}); });
document.addEventListener(`settings-${dependant[0]}`, (event: settingsBoolEvent) => { document.addEventListener(`settings-${dependant[0]}`, (event: settingsChangedEvent) => {
if (Boolean(event.detail) !== state) { if (toBool(event.detail) !== state) {
button.classList.add("unfocused"); button.classList.add("unfocused");
document.dispatchEvent(new CustomEvent(`settings-${name}`, { detail: false })); document.dispatchEvent(new CustomEvent(`settings-${name}`, { detail: false }));
} }
}); });
} }
if (s.meta.advanced) { if (s.meta.advanced) {
document.addEventListener("settings-advancedState", (event: settingsBoolEvent) => { document.addEventListener("settings-advancedState", (event: settingsChangedEvent) => {
if (!Boolean(event.detail)) { if (!toBool(event.detail)) {
button.classList.add("unfocused"); button.classList.add("unfocused");
} else { } else {
button.classList.remove("unfocused"); button.classList.remove("unfocused");
@ -1051,6 +804,14 @@ export class settingsList {
this._searchbox.oninput(null); this._searchbox.oninput(null);
}; };
}; };
// What possessed me to put this in the DOMSelect constructor originally? like what????????
const message = document.getElementById("settings-message") as HTMLElement;
message.innerHTML = window.lang.var("strings",
"settingsRequiredOrRestartMessage",
`<span class="badge ~critical">*</span>`,
`<span class="badge ~info dark:~d_warning">R</span>`
);
} }
private _addMatrix = () => { private _addMatrix = () => {
@ -1327,7 +1088,7 @@ class MessageEditor {
let innerHTML = ''; let innerHTML = '';
for (let i = 0; i < this._templ.variables.length; i++) { for (let i = 0; i < this._templ.variables.length; i++) {
let ci = i % colors.length; let ci = i % colors.length;
innerHTML += '<span class="button ~' + colors[ci] +' @low mb-4" style="margin-left: 0.25rem; margin-right: 0.25rem;"></span>' innerHTML += '<span class="button ~' + colors[ci] +' @low"></span>'
} }
if (this._templ.variables.length == 0) { if (this._templ.variables.length == 0) {
this._variablesLabel.classList.add("unfocused"); this._variablesLabel.classList.add("unfocused");