Files
jfa-go/ts/modules/ui.ts
Harvey Tindall 3308739619 admin: tab system improvement, search: ?search qp
tab content classes (e.g. settingsList, activityList)
can implement "AsTab", "Navigatable" and or "PageEventBindable",
the first giving them a tab name, a subpath and a reloader function,
the second an "isURL" and "navigate" function for loading resources,
the last giving them bind/unbindPageEvent methods. These are looped
through in ts/admin.ts still crudely, maybe tabs.ts could accept "AsTab"
implementers directly.

"Search" class now has a ?search query param which just encodes the
search box content, set when you perform a server search (hit enter or
press the button). ?user queries from the accounts or activity tab will
be converted to this form on loading.
2025-12-24 12:56:02 +00:00

212 lines
7.3 KiB
TypeScript

declare var window: GlobalWindow;
export interface HiddenInputConf {
container: HTMLElement;
onSet: () => void;
buttonOnLeft?: boolean;
customContainerHTML?: string;
input?: string;
clickAwayShouldSave?: boolean;
}
export class HiddenInputField {
public static editClass = "ri-edit-line";
public static saveClass = "ri-check-line";
private _c: HiddenInputConf;
private _input: HTMLInputElement;
private _content: HTMLElement;
private _toggle: HTMLElement;
previous: string;
constructor(c: HiddenInputConf) {
this._c = c;
if (!this._c.customContainerHTML) {
this._c.customContainerHTML = `<span class="hidden-input-content"></span>`;
}
if (!this._c.input) {
this._c.input = `<input type="text" class="field ~neutral @low max-w-24 hidden-input-input">`;
}
this._c.container.innerHTML = `
<div class="flex flex-row gap-2 items-baseline">
${this._c.buttonOnLeft ? "" : this._c.input}
${this._c.buttonOnLeft ? "" : this._c.customContainerHTML}
<i class="hidden-input-toggle"></i>
${this._c.buttonOnLeft ? this._c.input : ""}
${this._c.buttonOnLeft ? this._c.customContainerHTML : ""}
</div>
`;
this._input = this._c.container.querySelector(".hidden-input-input") as HTMLInputElement;
this._input.classList.add("py-0.5", "px-1", "hidden");
this._toggle = this._c.container.querySelector(".hidden-input-toggle");
this._content = this._c.container.querySelector(".hidden-input-content");
this._toggle.onclick = () => {
this.editing = !this.editing;
};
this._input.addEventListener("keypress", (e: KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
this._toggle.click();
}
});
this.setEditing(false, true);
}
// FIXME: not working
outerClickListener = ((event: Event) => {
if (
!(
event.target instanceof HTMLElement &&
(this._input.contains(event.target) || this._toggle.contains(event.target))
)
) {
this.toggle(!this._c.clickAwayShouldSave);
}
}).bind(this);
get editing(): boolean {
return this._toggle.classList.contains(HiddenInputField.saveClass);
}
set editing(e: boolean) {
this.setEditing(e);
}
setEditing(e: boolean, noEvent: boolean = false, noSave: boolean = false) {
if (e) {
document.addEventListener("click", this.outerClickListener);
this.previous = this.value;
this._input.value = this.value;
this._toggle.classList.add(HiddenInputField.saveClass);
this._toggle.classList.remove(HiddenInputField.editClass);
this._input.classList.remove("hidden");
this._input.focus();
this._content.classList.add("hidden");
} else {
document.removeEventListener("click", this.outerClickListener);
this.value = noSave ? this.previous : this._input.value;
this._toggle.classList.add(HiddenInputField.editClass);
this._toggle.classList.remove(HiddenInputField.saveClass);
// done by set value()
// this._content.classList.remove("hidden");
this._input.classList.add("hidden");
if (this.value != this.previous && !noEvent && !noSave) this._c.onSet();
}
}
get value(): string {
return this._content.textContent;
}
set value(v: string) {
this._content.textContent = v;
this._input.value = v;
if (!v) this._content.classList.add("hidden");
else this._content.classList.remove("hidden");
}
toggle(noSave: boolean = false) {
this.setEditing(!this.editing, false, noSave);
}
}
export interface RadioBasedTab {
name: string;
id?: string;
// If passed, will be put inside the button instead of the name.
buttonHTML?: string;
// You must at least pass a content element or an onShow function.
content?: HTMLElement;
onShow?: () => void;
onHide?: () => void;
}
interface RadioBasedTabItem {
tab: RadioBasedTab;
input: HTMLInputElement;
button: HTMLElement;
}
export class RadioBasedTabSelector {
private _id: string;
private _container: HTMLElement;
private _tabs: RadioBasedTabItem[];
private _selected: string;
constructor(container: HTMLElement, id: string, ...tabs: RadioBasedTab[]) {
this._container = container;
this._container.classList.add("flex", "flex-row", "gap-2");
this._tabs = [];
this._id = id;
let i = 0;
const frag = document.createDocumentFragment();
for (let tab of tabs) {
if (!tab.id) tab.id = tab.name;
const label = document.createElement("label");
label.classList.add("grow");
label.innerHTML = `
<input type="radio" name="${this._id}" value="${tab.name}" class="unfocused" ${i == 0 ? "checked" : ""}>
<span class="button ~neutral ${i == 0 ? "@high" : "@low"} radio-tab-button supra w-full text-center">${tab.buttonHTML || tab.name}</span>
`;
let ft: RadioBasedTabItem = {
tab: tab,
input: label.getElementsByTagName("input")[0] as HTMLInputElement,
button: label.getElementsByClassName("radio-tab-button")[0] as HTMLElement,
};
ft.input.onclick = () => {
ft.input.checked = true;
this.checkSource();
};
frag.appendChild(label);
this._tabs.push(ft);
i++;
}
this._container.replaceChildren(frag);
this.selected = 0;
}
checkSource = () => {
for (let tab of this._tabs) {
if (tab.input.checked) {
this._selected = tab.tab.id;
tab.tab.content?.classList.remove("unfocused");
tab.button.classList.add("@high");
tab.button.classList.remove("@low");
if (tab.tab.onShow) tab.tab.onShow();
} else {
tab.tab.content?.classList.add("unfocused");
tab.button.classList.add("@low");
tab.button.classList.remove("@high");
if (tab.tab.onHide) tab.tab.onHide();
}
}
};
get selected(): string {
return this._selected;
}
set selected(id: string | number) {
if (typeof id !== "string") {
id = this._tabs[id as number].tab.id;
}
for (let tab of this._tabs) {
if (tab.tab.id == id) {
this._selected = tab.tab.id;
tab.input.checked = true;
tab.tab.content?.classList.remove("unfocused");
tab.button.classList.add("@high");
tab.button.classList.remove("@low");
if (tab.tab.onShow) tab.tab.onShow();
} else {
tab.input.checked = false;
tab.tab.content?.classList.add("unfocused");
tab.button.classList.add("@low");
tab.button.classList.remove("@high");
if (tab.tab.onHide) tab.tab.onHide();
}
}
}
}