From ae808c5109b0d45bff5ed7bb4ac754d2a41b90ff Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Wed, 28 Aug 2024 20:22:25 +0100 Subject: [PATCH] accounts: standardise "text with edit button" component The sort of thing used for the user's label and their email address is now implemented by ui.ts/HiddenInputField, and used by the two. --- api-users.go | 2 +- ts/modules/accounts.ts | 130 ++++++++++++----------------------------- ts/modules/ui.ts | 91 +++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 94 deletions(-) create mode 100644 ts/modules/ui.ts diff --git a/api-users.go b/api-users.go index 43f7f67..ca92e49 100644 --- a/api-users.go +++ b/api-users.go @@ -962,7 +962,7 @@ func (app *appContext) ModifyLabels(gc *gin.Context) { emailStore = oldEmail } emailStore.Label = label - app.debug.Println(lm.UserLabelAdjusted, id, label) + app.debug.Printf(lm.UserLabelAdjusted, id, label) app.storage.SetEmailsKey(id, emailStore) } } diff --git a/ts/modules/accounts.ts b/ts/modules/accounts.ts index dc39c22..8820284 100644 --- a/ts/modules/accounts.ts +++ b/ts/modules/accounts.ts @@ -4,6 +4,8 @@ import { Marked } from "@ts-stack/markdown"; import { stripMarkdown } from "../modules/stripmd.js"; import { DiscordUser, newDiscordSearch } from "../modules/discord.js"; import { Search, SearchConfiguration, QueryType, SearchableItem } from "../modules/search.js"; +import { HiddenInputField } from "./ui.js"; + const dateParser = require("any-date-parser"); interface User { @@ -48,9 +50,9 @@ class user implements User, SearchableItem { private _admin: HTMLSpanElement; private _disabled: HTMLSpanElement; private _email: HTMLInputElement; + private _emailEditor: HiddenInputField; private _notifyEmail: boolean; private _emailAddress: string; - private _emailEditButton: HTMLElement; private _telegram: HTMLTableDataCellElement; private _telegramUsername: string; private _notifyTelegram: boolean; @@ -67,8 +69,8 @@ class user implements User, SearchableItem { private _lastActiveUnix: number; private _notifyDropdown: HTMLDivElement; private _label: HTMLInputElement; + private _labelEditor: HiddenInputField; private _userLabel: string; - private _labelEditButton: HTMLElement; private _accounts_admin: HTMLInputElement private _selected: boolean; private _referralsEnabled: boolean; @@ -110,10 +112,10 @@ class user implements User, SearchableItem { get admin(): boolean { return this._admin.classList.contains("chip"); } set admin(state: boolean) { if (state) { - this._admin.classList.add("chip", "~info", "ml-4"); + this._admin.classList.add("chip", "~info"); this._admin.textContent = window.lang.strings("admin"); } else { - this._admin.classList.remove("chip", "~info", "ml-4"); + this._admin.classList.remove("chip", "~info"); this._admin.textContent = ""; } } @@ -133,10 +135,10 @@ class user implements User, SearchableItem { get disabled(): boolean { return this._disabled.classList.contains("chip"); } set disabled(state: boolean) { if (state) { - this._disabled.classList.add("chip", "~warning", "ml-4"); + this._disabled.classList.add("chip", "~warning"); this._disabled.textContent = window.lang.strings("disabled"); } else { - this._disabled.classList.remove("chip", "~warning", "ml-4"); + this._disabled.classList.remove("chip", "~warning"); this._disabled.textContent = ""; } } @@ -144,12 +146,7 @@ class user implements User, SearchableItem { get email(): string { return this._emailAddress; } set email(value: string) { this._emailAddress = value; - const input = this._email.querySelector("input"); - if (input) { - input.value = value; - } else { - this._email.textContent = value; - } + this._emailEditor.value = value; const lastNotifyMethod = this.lastNotifyMethod() == "email"; if (!value) { this._notifyDropdown.querySelector(".accounts-area-email").classList.add("unfocused"); @@ -468,14 +465,7 @@ class user implements User, SearchableItem { get label(): string { return this._userLabel; } set label(l: string) { this._userLabel = l ? l : ""; - this._label.innerHTML = l ? l : ""; - this._labelEditButton.classList.add("ri-edit-line"); - this._labelEditButton.classList.remove("ri-check-line"); - if (!l) { - this._label.classList.remove("chip", "~gray"); - } else { - this._label.classList.add("chip", "~gray", "mr-2"); - } + this._labelEditor.value = l ? l : ""; } matchesSearch = (query: string): boolean => { @@ -498,7 +488,12 @@ class user implements User, SearchableItem { this._row = document.createElement("tr") as HTMLTableRowElement; let innerHTML = ` -
+
+ + + + +
`; if (window.jellyfinLogin) { innerHTML += ` @@ -506,7 +501,9 @@ class user implements User, SearchableItem { `; } innerHTML += ` -
+
+ +
`; if (window.telegramEnabled) { innerHTML += ` @@ -534,21 +531,32 @@ class user implements User, SearchableItem { `; this._row.innerHTML = innerHTML; const emailEditor = ``; - const labelEditor = ``; this._check = this._row.querySelector("input[type=checkbox].accounts-select-user") as HTMLInputElement; this._accounts_admin = this._row.querySelector("input[type=checkbox].accounts-access-jfa") as HTMLInputElement; this._username = this._row.querySelector(".accounts-username") as HTMLSpanElement; this._admin = this._row.querySelector(".accounts-admin") as HTMLSpanElement; this._disabled = this._row.querySelector(".accounts-disabled") as HTMLSpanElement; this._email = this._row.querySelector(".accounts-email-container") as HTMLInputElement; - this._emailEditButton = this._row.querySelector(".accounts-email-edit") as HTMLElement; + this._emailEditor = new HiddenInputField({ + container: this._email, + onSet: this._updateEmail, + customContainerHTML: ``, + buttonOnLeft: true, + clickAwayShouldSave: false, + }); this._telegram = this._row.querySelector(".accounts-telegram") as HTMLTableDataCellElement; this._discord = this._row.querySelector(".accounts-discord") as HTMLTableDataCellElement; this._matrix = this._row.querySelector(".accounts-matrix") as HTMLTableDataCellElement; this._expiry = this._row.querySelector(".accounts-expiry") as HTMLTableDataCellElement; this._lastActive = this._row.querySelector(".accounts-last-active") as HTMLTableDataCellElement; this._label = this._row.querySelector(".accounts-label-container") as HTMLInputElement; - this._labelEditButton = this._row.querySelector(".accounts-label-edit") as HTMLElement; + this._labelEditor = new HiddenInputField({ + container: this._label, + onSet: this._updateLabel, + customContainerHTML: ``, + clickAwayShouldSave: false, + }); + this._check.onchange = () => { this.selected = this._check.checked; } if (window.jellyfinLogin) { @@ -573,66 +581,6 @@ class user implements User, SearchableItem { this._notifyDropdown = this._constructDropdown(); - const toggleEmailInput = () => { - if (this._emailEditButton.classList.contains("ri-edit-line")) { - this._email.innerHTML = emailEditor; - this._email.querySelector("input").value = this._emailAddress; - this._email.classList.remove("ml-2"); - } else { - this._email.textContent = this._emailAddress; - this._email.classList.add("ml-2"); - } - this._emailEditButton.classList.toggle("ri-check-line"); - this._emailEditButton.classList.toggle("ri-edit-line"); - }; - const emailClickListener = (event: Event) => { - if (!(event.target instanceof HTMLElement && (this._email.contains(event.target) || this._emailEditButton.contains(event.target)))) { - toggleEmailInput(); - this.email = this.email; - document.removeEventListener("click", emailClickListener); - } - }; - this._emailEditButton.onclick = () => { - if (this._emailEditButton.classList.contains("ri-edit-line")) { - document.addEventListener('click', emailClickListener); - } else { - this._updateEmail(); - document.removeEventListener('click', emailClickListener); - } - toggleEmailInput(); - }; - - const toggleLabelInput = () => { - if (this._labelEditButton.classList.contains("ri-edit-line")) { - this._label.innerHTML = labelEditor; - const input = this._label.querySelector("input"); - input.value = this._userLabel; - input.placeholder = window.lang.strings("label"); - this._label.classList.remove("ml-2"); - this._labelEditButton.classList.add("ri-check-line"); - this._labelEditButton.classList.remove("ri-edit-line"); - } else { - this._updateLabel(); - this._email.classList.add("ml-2"); - } - }; - - const labelClickListener = (event: Event) => { - if (!(event.target instanceof HTMLElement && (this._label.contains(event.target) || this._labelEditButton.contains(event.target)))) { - toggleLabelInput(); - document.removeEventListener("click", labelClickListener); - } - }; - - this._labelEditButton.onclick = () => { - if (this._labelEditButton.classList.contains("ri-edit-line")) { - document.addEventListener('click', labelClickListener); - } else { - document.removeEventListener('click', labelClickListener); - } - toggleLabelInput(); - }; - this.update(user); document.addEventListener("timefmt-change", () => { @@ -642,14 +590,12 @@ class user implements User, SearchableItem { } private _updateLabel = () => { - let oldLabel = this.label; - this.label = this._label.querySelector("input").value; let send = {}; - send[this.id] = this.label; + send[this.id] = this._labelEditor.value; _post("/users/labels", send, (req: XMLHttpRequest) => { if (req.readyState == 4) { if (req.status != 204) { - this.label = oldLabel; + this.label = this._labelEditor.previous; window.notifications.customError("labelChanged", window.lang.notif("errorUnknown")); } } @@ -657,16 +603,14 @@ class user implements User, SearchableItem { }; private _updateEmail = () => { - let oldEmail = this.email; - this.email = this._email.querySelector("input").value; let send = {}; - send[this.id] = this.email; + send[this.id] = this._emailEditor.value; _post("/users/emails", send, (req: XMLHttpRequest) => { if (req.readyState == 4) { if (req.status == 200) { window.notifications.customSuccess("emailChanged", window.lang.var("notifications", "changedEmailAddress", `"${this.name}"`)); } else { - this.email = oldEmail; + this.email = this._emailEditor.previous; window.notifications.customError("emailChanged", window.lang.var("notifications", "errorChangedEmailAddress", `"${this.name}"`)); } } diff --git a/ts/modules/ui.ts b/ts/modules/ui.ts new file mode 100644 index 0000000..575aac4 --- /dev/null +++ b/ts/modules/ui.ts @@ -0,0 +1,91 @@ +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 = ``; + } + if (!(this._c.input)) { + this._c.input = ``; + } + this._c.container.innerHTML = ` +
+ ${this._c.buttonOnLeft ? "" : this._c.input} + ${this._c.buttonOnLeft ? "" : this._c.customContainerHTML} + + ${this._c.buttonOnLeft ? this._c.input : ""} + ${this._c.buttonOnLeft ? this._c.customContainerHTML : ""} +
+ `; + + 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.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._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); } +}