settings: hide groups if all children are hidden

only really affects the "Email" group when messages|enabled is false.
This commit is contained in:
Harvey Tindall
2025-11-27 16:36:26 +00:00
parent 85eea23d98
commit 96ec12f2bd
4 changed files with 200 additions and 91 deletions
+1
View File
@@ -790,6 +790,7 @@ sections:
- ["smtp", "SMTP"] - ["smtp", "SMTP"]
- ["mailgun", "Mailgun"] - ["mailgun", "Mailgun"]
value: smtp value: smtp
depends_true: messages|enabled
description: Method of sending email to use. description: Method of sending email to use.
- setting: address - setting: address
name: Sent from (address) name: Sent from (address)
-7
View File
@@ -221,15 +221,8 @@ sup.\~critical, .text-critical {
padding-bottom: 0.1rem; padding-bottom: 0.1rem;
} }
.settings-section-button {
width: 100%;
height: 2.5rem;
}
.settings-section-button:hover, .settings-section-button:focus { .settings-section-button:hover, .settings-section-button:focus {
box-sizing: border-box; box-sizing: border-box;
width: 100%;
height: 2.5rem;
background-color: var(--color-neutral-normal-fill); background-color: var(--color-neutral-normal-fill);
filter: brightness(var(--settings-section-button-filter)) !important; filter: brightness(var(--settings-section-button-filter)) !important;
} }
+198 -83
View File
@@ -1,6 +1,7 @@
import { _get, _post, _delete, _download, _upload, toggleLoader, addLoader, removeLoader, insertText, toClipboard, toDateString } from "../modules/common.js"; import { _get, _post, _delete, _download, _upload, toggleLoader, addLoader, removeLoader, insertText, toClipboard, toDateString } from "../modules/common.js";
import { Marked } from "@ts-stack/markdown"; import { Marked } from "@ts-stack/markdown";
import { stripMarkdown } from "../modules/stripmd.js"; import { stripMarkdown } from "../modules/stripmd.js";
import { PDT } from "src/data/timezoneNames";
declare var window: GlobalWindow; declare var window: GlobalWindow;
@@ -176,7 +177,7 @@ class DOMSetting {
<div class="flex flex-row gap-2 items-baseline"> <div class="flex flex-row gap-2 items-baseline">
<span class="setting-label"></span> <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 align-baseline"></i> <i class="icon ri-information-line align-[-0.05rem]"></i>
<span class="content sm"></span> <span class="content sm"></span>
</div> </div>
<span class="setting-required unfocused"></span> <span class="setting-required unfocused"></span>
@@ -191,10 +192,10 @@ class DOMSetting {
// "input" variable should supply the HTML of an element with class "setting-input" // "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 [sect, 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: settingsChangedEvent) => { document.addEventListener(`settings-${sect}-${dependant}`, (event: settingsChangedEvent) => {
if (toBool(event.detail) !== state) { if (toBool(event.detail) !== state) {
this.hide(); this.hide();
} else { } else {
@@ -478,9 +479,29 @@ interface Group {
members: Member[]; members: Member[];
} }
class groupButton { abstract class groupableItem {
private _el: HTMLElement; protected _el: HTMLElement;
private _button: HTMLElement; asElement = () => { return this._el; }
remove = () => { this._el.remove(); };
inGroup = (): string|null => { return this._el.parentElement.getAttribute("data-group"); }
get hidden(): boolean { return this._el.classList.contains("unfocused"); }
set hidden(v: boolean) {
if (v) {
this._el.classList.add("unfocused");
if (this.inGroup()) {
document.dispatchEvent(new CustomEvent(`settings-group-${this.inGroup()}-child-hidden`));
}
} else {
this._el.classList.remove("unfocused");
if (this.inGroup()) {
document.dispatchEvent(new CustomEvent(`settings-group-${this.inGroup()}-child-visible`));
}
}
}
}
class groupButton extends groupableItem {
button: HTMLElement;
private _dropdown: HTMLElement; private _dropdown: HTMLElement;
private _icon: HTMLElement; private _icon: HTMLElement;
private _check: HTMLInputElement; private _check: HTMLInputElement;
@@ -489,6 +510,11 @@ class groupButton {
private _parentSidebar: HTMLElement; private _parentSidebar: HTMLElement;
private static readonly _margin = "ml-6"; private static readonly _margin = "ml-6";
private _indentClasses = ["h-11", "h-10", "h-9"];
private _indentClass = () => {
const classes = [["h-10"], ["h-9"]];
return classes[Math.min(this.indent, classes.length-1)];
};
asElement = () => { return this._el; }; asElement = () => { return this._el; };
@@ -503,8 +529,12 @@ class groupButton {
append(item: HTMLElement|groupButton) { append(item: HTMLElement|groupButton) {
if (item instanceof groupButton) { if (item instanceof groupButton) {
item.button.classList.remove(...this._indentClasses);
item.button.classList.add(...this._indentClass());
this._dropdown.appendChild(item.asElement()); this._dropdown.appendChild(item.asElement());
} else { } else {
item.classList.remove(...this._indentClasses);
item.classList.add(...this._indentClass());
this._dropdown.appendChild(item); this._dropdown.appendChild(item);
} }
} }
@@ -512,14 +542,18 @@ class groupButton {
get name(): string { return this._group.name; } get name(): string { return this._group.name; }
set name(v: string) { set name(v: string) {
this._group.name = v; this._group.name = v;
this._button.querySelector(".group-button-name").textContent = v; this.button.querySelector(".group-button-name").textContent = v;
} }
get group(): string { return this._group.group; } get group(): string { return this._group.group; }
set group(v: string) { set group(v: string) {
document.removeEventListener(`settings-group-${this.group}-child-visible`, this._childVisible);
document.removeEventListener(`settings-group-${this.group}-child-hidden`, this._childHidden);
this._group.group = v; this._group.group = v;
document.addEventListener(`settings-group-${this.group}-child-visible`, this._childVisible);
document.addEventListener(`settings-group-${this.group}-child-hidden`, this._childHidden);
this._el.setAttribute("data-group", v); this._el.setAttribute("data-group", v);
this._button.setAttribute("data-group", v); this.button.setAttribute("data-group", v);
this._check.setAttribute("data-group", v); this._check.setAttribute("data-group", v);
this._dropdown.setAttribute("data-group", v); this._dropdown.setAttribute("data-group", v);
} }
@@ -532,17 +566,14 @@ class groupButton {
this._dropdown.classList.remove(groupButton._margin); this._dropdown.classList.remove(groupButton._margin);
this._indent = v; this._indent = v;
this._dropdown.classList.add(groupButton._margin); this._dropdown.classList.add(groupButton._margin);
} for (let child of this._dropdown.children) {
child.classList.remove(...this._indentClasses);
get hidden(): boolean { return this._el.classList.contains("unfocused"); } child.classList.add(...this._indentClass());
set hidden(v: boolean) { };
if (v) this._el.classList.add("unfocused");
else this._el.classList.remove("unfocused");
} }
get open(): boolean { return this._check.checked; } get open(): boolean { return this._check.checked; }
set open(v: boolean) { set open(v: boolean) {
console.trace("set", v);
this.openCloseWithAnimation(v); this.openCloseWithAnimation(v);
} }
@@ -608,20 +639,35 @@ class groupButton {
} }
} }
private _childVisible = () => {
this.hidden = false;
}
private _childHidden = () => {
for (let el of this._dropdown.children) {
if (!(el.classList.contains("unfocused"))) {
return;
}
}
// All children are hidden, so hide ourself
this.hidden = true;
}
// Takes sidebar as we need to disable scrolling on it when animation starts. // Takes sidebar as we need to disable scrolling on it when animation starts.
constructor(parentSidebar: HTMLElement) { constructor(parentSidebar: HTMLElement) {
super();
this._parentSidebar = parentSidebar; this._parentSidebar = parentSidebar;
this._el = document.createElement("div"); this._el = document.createElement("div");
this._el.classList.add("flex", "flex-col", "gap-2"); this._el.classList.add("flex", "flex-col", "gap-2");
this._button = document.createElement("span") as HTMLSpanElement; this.button = document.createElement("span") as HTMLSpanElement;
this._el.appendChild(this._button); this._el.appendChild(this.button);
this._button.classList.add("button", "~neutral", "@low", "settings-section-button", "justify-between"); this.button.classList.add("button", "~neutral", "@low", "settings-section-button", "h-11", "justify-between");
this._button.innerHTML = ` this.button.innerHTML = `
<span class="group-button-name"></span> <span class="group-button-name"></span>
<label> <label class="button border-none shadow-none">
<i class="icon ri-arrow-down-s-line text-xl"></i> <i class="icon ri-arrow-down-s-line"></i>
<input class="unfocused" type="checkbox"> <input class="unfocused" type="checkbox">
</label> </label>
`; `;
@@ -632,15 +678,16 @@ class groupButton {
this._dropdown.style.opacity = "0"; this._dropdown.style.opacity = "0";
this._dropdown.classList.add("settings-dropdown", "unfocused", "flex", "flex-col", "gap-2", "transition-all"); this._dropdown.classList.add("settings-dropdown", "unfocused", "flex", "flex-col", "gap-2", "transition-all");
this._icon = this._button.querySelector("i.icon"); this._icon = this.button.querySelector("i.icon");
this._check = this._button.querySelector("input[type=checkbox]") as HTMLInputElement; this._check = this.button.querySelector("input[type=checkbox]") as HTMLInputElement;
this._button.onclick = (event: Event) => { this.button.onclick = (event: Event) => {
if (event.target != this._icon && event.target != this._check) this.open = !this.open; if (event.target != this._icon && event.target != this._check) this.open = !this.open;
}; };
this._check.onclick = () => { this._check.onclick = () => {
this.open = this.open; this.open = this.open;
} }
this.openCloseWithoutAnimation(false); this.openCloseWithoutAnimation(false);
} }
}; };
@@ -741,6 +788,103 @@ class sectionPanel {
type Member = { group: string } | { section: string }; type Member = { group: string } | { section: string };
class sectionButton extends groupableItem {
section: string;
private _name: HTMLElement;
private _subButton: HTMLElement;
private _meta: Meta;
update = (section: string, sm: Meta) => {
this.section = section;
this._meta = sm;
this.name = sm.name;
this.advanced = sm.advanced;
this._registerDependencies();
};
get subButton(): HTMLElement { return this._subButton.children[0] as HTMLElement; }
set subButton(v: HTMLElement) { this._subButton.replaceChildren(v); }
get name(): string { return this._meta.name; }
set name(v: string) {
this._meta.name = v;
this._name.textContent = v;
};
get depends_true(): string { return this._meta.depends_true; }
set depends_true(v: string) {
this._meta.depends_true = v;
this._registerDependencies();
}
get depends_false(): string { return this._meta.depends_false; }
set depends_false(v: string) {
this._meta.depends_false = v;
this._registerDependencies();
}
get selected(): boolean { return this._el.classList.contains("selected"); }
set selected(v: boolean) {
if (v) this._el.classList.add("selected");
else this._el.classList.remove("selected");
}
select = () => {
document.dispatchEvent(new CustomEvent("settings-show-panel", { detail: this.section }));
}
private _registerDependencies() {
// Doesn't re-register dependencies, but that isn't important in this application
if (!(this._meta.depends_true || this._meta.depends_false)) return;
let [sect, dependant] = splitDependant(this.section, this._meta.depends_true || this._meta.depends_false);
let state = !(Boolean(this._meta.depends_false));
document.addEventListener(`settings-${sect}-${dependant}`, (event: settingsChangedEvent) => {
const hide = (toBool(event.detail) !== state);
this.hidden = hide;
document.dispatchEvent(new CustomEvent(`settings-${name}`, { detail: !hide }));
});
document.addEventListener(`settings-${sect}`, (event: settingsChangedEvent) => {
if (toBool(event.detail) !== state) {
this.hidden = true;
document.dispatchEvent(new CustomEvent(`settings-${name}`, { detail: false }));
}
});
}
private _advancedListener = (event: settingsChangedEvent) => {
if (!toBool(event.detail)) {
this._el.classList.add("unfocused");
} else {
this._el.classList.remove("unfocused");
}
document.dispatchEvent(new CustomEvent("settings-re-search"));
}
get advanced(): boolean { return this._meta.advanced }
set advanced(v: boolean) {
this._meta.advanced = v;
if (v) document.addEventListener("settings-advancedState", this._advancedListener);
else document.removeEventListener("settings-advancedState", this._advancedListener);
}
constructor(section?: string, sectionMeta?: Meta) {
super();
this._el = document.createElement("span") as HTMLSpanElement;
this._el.classList.add("button", "~neutral", "@low", "settings-section-button", "h-11", "justify-between");
this._el.innerHTML = `
<span class="settings-section-button-name"></span>
<div class="settings-section-button-sub-button"></div>
`;
this._name = this._el.getElementsByClassName("settings-section-button-name")[0] as HTMLElement;
this._subButton = this._el.getElementsByClassName("settings-section-button-sub-button")[0] as HTMLElement;
this._el.onclick = this.select;
if (sectionMeta) this.update(section, sectionMeta);
}
}
interface Settings { interface Settings {
groups: Group[]; groups: Group[];
sections: Section[]; sections: Section[];
@@ -758,7 +902,7 @@ export class settingsList {
private _sidebar = document.getElementById("settings-sidebar-items") as HTMLDivElement; private _sidebar = document.getElementById("settings-sidebar-items") as HTMLDivElement;
private _visibleSection: string; private _visibleSection: string;
private _sections: { [name: string]: sectionPanel }; private _sections: { [name: string]: sectionPanel };
private _buttons: { [name: string]: HTMLSpanElement }; private _buttons: { [name: string]: sectionButton };
private _groups: { [name: string]: Group }; private _groups: { [name: string]: Group };
private _groupButtons: { [name: string]: groupButton }; private _groupButtons: { [name: string]: groupButton };
@@ -800,7 +944,7 @@ export class settingsList {
const subsection = this._buttons[member.section]; const subsection = this._buttons[member.section];
// Remove from page // Remove from page
subsection.remove(); subsection.remove();
container.append(subsection); container.append(subsection.asElement());
} }
} }
@@ -820,43 +964,10 @@ export class settingsList {
const section = new sectionPanel(s, name); const section = new sectionPanel(s, name);
this._sections[name] = section; this._sections[name] = section;
this._panel.appendChild(this._sections[name].asElement()); this._panel.appendChild(this._sections[name].asElement());
const button = document.createElement("span") as HTMLSpanElement; const button = new sectionButton(name, s.meta);
button.classList.add("button", "~neutral", "@low", "settings-section-button", "justify-between"); if (subButton) button.subButton = subButton;
button.textContent = s.meta.name;
if (subButton) { button.appendChild(subButton); }
button.onclick = () => { this._showPanel(name); };
if (s.meta.depends_true || s.meta.depends_false) {
let dependant = splitDependant(name, s.meta.depends_true || s.meta.depends_false);
let state = true;
if (s.meta.depends_false) { state = false; }
document.addEventListener(`settings-${dependant[0]}-${dependant[1]}`, (event: settingsChangedEvent) => {
if (toBool(event.detail) !== state) {
button.classList.add("unfocused");
document.dispatchEvent(new CustomEvent(`settings-${name}`, { detail: false }));
} else {
button.classList.remove("unfocused");
document.dispatchEvent(new CustomEvent(`settings-${name}`, { detail: true }));
}
});
document.addEventListener(`settings-${dependant[0]}`, (event: settingsChangedEvent) => {
if (toBool(event.detail) !== state) {
button.classList.add("unfocused");
document.dispatchEvent(new CustomEvent(`settings-${name}`, { detail: false }));
}
});
}
if (s.meta.advanced) {
document.addEventListener("settings-advancedState", (event: settingsChangedEvent) => {
if (!toBool(event.detail)) {
button.classList.add("unfocused");
} else {
button.classList.remove("unfocused");
}
this._searchbox.oninput(null);
});
}
this._buttons[name] = button; this._buttons[name] = button;
this._sidebar.appendChild(this._buttons[name]); this._sidebar.appendChild(button.asElement());
} }
private _traverseMemberList = (list: Member[], func: (sect: string) => void) => { private _traverseMemberList = (list: Member[], func: (sect: string) => void) => {
@@ -882,7 +993,7 @@ export class settingsList {
this._groupButtons[member.group].openCloseWithoutAnimation(false); this._groupButtons[member.group].openCloseWithoutAnimation(false);
} else if ("section" in member) { } else if ("section" in member) {
if (member.section in this._buttons) { if (member.section in this._buttons) {
this._sidebar.appendChild(this._buttons[member.section]); this._sidebar.appendChild(this._buttons[member.section].asElement());
} else { } else {
console.warn("Settings section specified in order but missing:", member.section); console.warn("Settings section specified in order but missing:", member.section);
} }
@@ -891,15 +1002,11 @@ export class settingsList {
} }
private _showPanel = (name: string) => { private _showPanel = (name: string) => {
// console.log("showing", name);
for (let n in this._sections) { for (let n in this._sections) {
this._sections[n].visible = n == name;
this._buttons[name].selected = n == name;
if (n == name) { if (n == name) {
this._sections[name].visible = true;
this._visibleSection = name; this._visibleSection = name;
this._buttons[name].classList.add("selected");
} else {
this._sections[n].visible = false;
this._buttons[n].classList.remove("selected");
} }
} }
} }
@@ -949,7 +1056,7 @@ export class settingsList {
setBackupSort = (ascending: boolean) => { setBackupSort = (ascending: boolean) => {
this._backupSortAscending = ascending; this._backupSortAscending = ascending;
this._backupSortDirection.innerHTML = `${window.lang.strings("sortDirection")} <i class="ri-arrow-${ascending ? "up" : "down"}-s-line ml-2"></i>`; this._backupSortDirection.innerHTML = `${window.lang.strings("sortDirection")} <i class="${ascending ? "ri-arrow-up-s-line" : "ri-arrow-down-s-line"} ml-2"></i>`;
this._getBackups(); this._getBackups();
}; };
@@ -1029,6 +1136,10 @@ export class settingsList {
this._backup(); this._backup();
}; };
document.addEventListener("settings-show-panel", (event: CustomEvent) => {
this._showPanel(event.detail as string);
});
document.getElementById("settings-backups").onclick = () => { document.getElementById("settings-backups").onclick = () => {
this.setBackupSort(this._backupSortAscending); this.setBackupSort(this._backupSortAscending);
window.modals.backups.show(); window.modals.backups.show();
@@ -1068,6 +1179,11 @@ export class settingsList {
this._searchbox.oninput = () => { this._searchbox.oninput = () => {
this.search(this._searchbox.value); this.search(this._searchbox.value);
}; };
document.addEventListener("settings-re-search", () => {
this._searchbox.oninput(null);
});
for (let b of this._clearSearchboxButtons) { for (let b of this._clearSearchboxButtons) {
b.onclick = () => { b.onclick = () => {
this._searchbox.value = ""; this._searchbox.value = "";
@@ -1080,7 +1196,7 @@ export class settingsList {
const rr = document.createElement("span"); const rr = document.createElement("span");
rr.classList.add("tooltip", "below"); rr.classList.add("tooltip", "below");
rr.innerHTML = ` rr.innerHTML = `
<span class="badge ~info dark:~d_warning"><i class="icon ri-refresh-line align-baseline h-full"></i></span> <span class="badge ~info dark:~d_warning align-[0.08rem]"><i class="icon ri-refresh-line h-full"></i></span>
<span class="content sm">${window.lang.strings("restartRequired")}</span> <span class="content sm">${window.lang.strings("restartRequired")}</span>
`; `;
@@ -1090,7 +1206,7 @@ export class settingsList {
const r = document.createElement("span"); const r = document.createElement("span");
r.classList.add("tooltip", "below"); r.classList.add("tooltip", "below");
r.innerHTML = ` r.innerHTML = `
<span class="badge ~critical"><i class="icon ri-asterisk align-baseline h-full"></i></span> <span class="badge ~critical align-[0.08rem]"><i class="icon ri-asterisk h-full"></i></span>
<span class="content sm">${window.lang.strings("required")}</span> <span class="content sm">${window.lang.strings("required")}</span>
`; `;
@@ -1151,9 +1267,9 @@ export class settingsList {
} else { } else {
if (section.section == "messages" || section.section == "user_page") { if (section.section == "messages" || section.section == "user_page") {
const editButton = document.createElement("div"); const editButton = document.createElement("div");
editButton.classList.add("tooltip", "left"); editButton.classList.add("tooltip", "left", "h-full");
editButton.innerHTML = ` editButton.innerHTML = `
<span class="button ~neutral @low"> <span class="button ~neutral @low h-full">
<i class="icon ri-edit-line"></i> <i class="icon ri-edit-line"></i>
</span> </span>
<span class="content sm"> <span class="content sm">
@@ -1189,9 +1305,9 @@ export class settingsList {
this.addSection(section.section, section, icon); this.addSection(section.section, section, icon);
} else if (section.section == "matrix" && !window.matrixEnabled) { } else if (section.section == "matrix" && !window.matrixEnabled) {
const addButton = document.createElement("div"); const addButton = document.createElement("div");
addButton.classList.add("tooltip", "left"); addButton.classList.add("tooltip", "left", "h-full");
addButton.innerHTML = ` addButton.innerHTML = `
<span class="button ~neutral @low">+</span> <span class="button ~neutral h-full"><i class="icon ri-links-line"></i></span>
<span class="content sm"> <span class="content sm">
${window.lang.strings("linkMatrix")} ${window.lang.strings("linkMatrix")}
</span> </span>
@@ -1263,8 +1379,8 @@ export class settingsList {
// hide button, unhide if matched // hide button, unhide if matched
const button = this._buttons[section.section]; const button = this._buttons[section.section];
button.classList.add("unfocused"); button.hidden = true;
const parentGroup = button.parentElement.getAttribute("data-group"); const parentGroup = button.inGroup();
let parentGroupButton: groupButton = null; let parentGroupButton: groupButton = null;
let matchedGroup = false; let matchedGroup = false;
if (parentGroup) { if (parentGroup) {
@@ -1273,14 +1389,13 @@ export class settingsList {
} }
const show = () => { const show = () => {
button.classList.remove("unfocused"); button.hidden = false;
if (parentGroupButton) { if (parentGroupButton) {
if (query != "") parentGroupButton.openCloseWithoutAnimation(true); if (query != "") parentGroupButton.openCloseWithoutAnimation(true);
parentGroupButton.hidden = false;
} }
} }
const hide = () => { const hide = () => {
button.classList.add("unfocused"); button.hidden = true;
} }
let matchedSection = matchedGroup || let matchedSection = matchedGroup ||
@@ -1369,13 +1484,13 @@ export class settingsList {
} }
if (firstVisibleSection && (query != "" || this._visibleSection == "")) { if (firstVisibleSection && (query != "" || this._visibleSection == "")) {
this._buttons[firstVisibleSection].onclick(null); this._buttons[firstVisibleSection].select();
this._noResultsPanel.classList.add("unfocused"); this._noResultsPanel.classList.add("unfocused");
} else if (query != "") { } else if (query != "") {
this._noResultsPanel.classList.remove("unfocused"); this._noResultsPanel.classList.remove("unfocused");
if (this._visibleSection) { if (this._visibleSection) {
this._sections[this._visibleSection].visible = false; this._sections[this._visibleSection].visible = false;
this._buttons[this._visibleSection].classList.remove("selected"); this._buttons[this._visibleSection].selected = false;
this._visibleSection = ""; this._visibleSection = "";
} }
} }
+1 -1
View File
@@ -2,7 +2,7 @@
"compilerOptions": { "compilerOptions": {
"outDir": "../js", "outDir": "../js",
"target": "es2017", "target": "es2017",
"lib": ["dom", "es2017"], "lib": ["dom", "es2017", "dom.iterable"],
"typeRoots": ["./typings", "../node_modules/@types"], "typeRoots": ["./typings", "../node_modules/@types"],
"module": "esnext", "module": "esnext",
"moduleResolution": "bundler", "moduleResolution": "bundler",