mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-22 09:00:10 +00:00
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.
This commit is contained in:
parent
418f3c4566
commit
ae808c5109
@ -962,7 +962,7 @@ func (app *appContext) ModifyLabels(gc *gin.Context) {
|
|||||||
emailStore = oldEmail
|
emailStore = oldEmail
|
||||||
}
|
}
|
||||||
emailStore.Label = label
|
emailStore.Label = label
|
||||||
app.debug.Println(lm.UserLabelAdjusted, id, label)
|
app.debug.Printf(lm.UserLabelAdjusted, id, label)
|
||||||
app.storage.SetEmailsKey(id, emailStore)
|
app.storage.SetEmailsKey(id, emailStore)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@ import { Marked } from "@ts-stack/markdown";
|
|||||||
import { stripMarkdown } from "../modules/stripmd.js";
|
import { stripMarkdown } from "../modules/stripmd.js";
|
||||||
import { DiscordUser, newDiscordSearch } from "../modules/discord.js";
|
import { DiscordUser, newDiscordSearch } from "../modules/discord.js";
|
||||||
import { Search, SearchConfiguration, QueryType, SearchableItem } from "../modules/search.js";
|
import { Search, SearchConfiguration, QueryType, SearchableItem } from "../modules/search.js";
|
||||||
|
import { HiddenInputField } from "./ui.js";
|
||||||
|
|
||||||
const dateParser = require("any-date-parser");
|
const dateParser = require("any-date-parser");
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
@ -48,9 +50,9 @@ class user implements User, SearchableItem {
|
|||||||
private _admin: HTMLSpanElement;
|
private _admin: HTMLSpanElement;
|
||||||
private _disabled: HTMLSpanElement;
|
private _disabled: HTMLSpanElement;
|
||||||
private _email: HTMLInputElement;
|
private _email: HTMLInputElement;
|
||||||
|
private _emailEditor: HiddenInputField;
|
||||||
private _notifyEmail: boolean;
|
private _notifyEmail: boolean;
|
||||||
private _emailAddress: string;
|
private _emailAddress: string;
|
||||||
private _emailEditButton: HTMLElement;
|
|
||||||
private _telegram: HTMLTableDataCellElement;
|
private _telegram: HTMLTableDataCellElement;
|
||||||
private _telegramUsername: string;
|
private _telegramUsername: string;
|
||||||
private _notifyTelegram: boolean;
|
private _notifyTelegram: boolean;
|
||||||
@ -67,8 +69,8 @@ class user implements User, SearchableItem {
|
|||||||
private _lastActiveUnix: number;
|
private _lastActiveUnix: number;
|
||||||
private _notifyDropdown: HTMLDivElement;
|
private _notifyDropdown: HTMLDivElement;
|
||||||
private _label: HTMLInputElement;
|
private _label: HTMLInputElement;
|
||||||
|
private _labelEditor: HiddenInputField;
|
||||||
private _userLabel: string;
|
private _userLabel: string;
|
||||||
private _labelEditButton: HTMLElement;
|
|
||||||
private _accounts_admin: HTMLInputElement
|
private _accounts_admin: HTMLInputElement
|
||||||
private _selected: boolean;
|
private _selected: boolean;
|
||||||
private _referralsEnabled: boolean;
|
private _referralsEnabled: boolean;
|
||||||
@ -110,10 +112,10 @@ class user implements User, SearchableItem {
|
|||||||
get admin(): boolean { return this._admin.classList.contains("chip"); }
|
get admin(): boolean { return this._admin.classList.contains("chip"); }
|
||||||
set admin(state: boolean) {
|
set admin(state: boolean) {
|
||||||
if (state) {
|
if (state) {
|
||||||
this._admin.classList.add("chip", "~info", "ml-4");
|
this._admin.classList.add("chip", "~info");
|
||||||
this._admin.textContent = window.lang.strings("admin");
|
this._admin.textContent = window.lang.strings("admin");
|
||||||
} else {
|
} else {
|
||||||
this._admin.classList.remove("chip", "~info", "ml-4");
|
this._admin.classList.remove("chip", "~info");
|
||||||
this._admin.textContent = "";
|
this._admin.textContent = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -133,10 +135,10 @@ class user implements User, SearchableItem {
|
|||||||
get disabled(): boolean { return this._disabled.classList.contains("chip"); }
|
get disabled(): boolean { return this._disabled.classList.contains("chip"); }
|
||||||
set disabled(state: boolean) {
|
set disabled(state: boolean) {
|
||||||
if (state) {
|
if (state) {
|
||||||
this._disabled.classList.add("chip", "~warning", "ml-4");
|
this._disabled.classList.add("chip", "~warning");
|
||||||
this._disabled.textContent = window.lang.strings("disabled");
|
this._disabled.textContent = window.lang.strings("disabled");
|
||||||
} else {
|
} else {
|
||||||
this._disabled.classList.remove("chip", "~warning", "ml-4");
|
this._disabled.classList.remove("chip", "~warning");
|
||||||
this._disabled.textContent = "";
|
this._disabled.textContent = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -144,12 +146,7 @@ class user implements User, SearchableItem {
|
|||||||
get email(): string { return this._emailAddress; }
|
get email(): string { return this._emailAddress; }
|
||||||
set email(value: string) {
|
set email(value: string) {
|
||||||
this._emailAddress = value;
|
this._emailAddress = value;
|
||||||
const input = this._email.querySelector("input");
|
this._emailEditor.value = value;
|
||||||
if (input) {
|
|
||||||
input.value = value;
|
|
||||||
} else {
|
|
||||||
this._email.textContent = value;
|
|
||||||
}
|
|
||||||
const lastNotifyMethod = this.lastNotifyMethod() == "email";
|
const lastNotifyMethod = this.lastNotifyMethod() == "email";
|
||||||
if (!value) {
|
if (!value) {
|
||||||
this._notifyDropdown.querySelector(".accounts-area-email").classList.add("unfocused");
|
this._notifyDropdown.querySelector(".accounts-area-email").classList.add("unfocused");
|
||||||
@ -468,14 +465,7 @@ class user implements User, SearchableItem {
|
|||||||
get label(): string { return this._userLabel; }
|
get label(): string { return this._userLabel; }
|
||||||
set label(l: string) {
|
set label(l: string) {
|
||||||
this._userLabel = l ? l : "";
|
this._userLabel = l ? l : "";
|
||||||
this._label.innerHTML = l ? l : "";
|
this._labelEditor.value = 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");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
matchesSearch = (query: string): boolean => {
|
matchesSearch = (query: string): boolean => {
|
||||||
@ -498,7 +488,12 @@ class user implements User, SearchableItem {
|
|||||||
this._row = document.createElement("tr") as HTMLTableRowElement;
|
this._row = document.createElement("tr") as HTMLTableRowElement;
|
||||||
let innerHTML = `
|
let innerHTML = `
|
||||||
<td><input type="checkbox" class="accounts-select-user" value=""></td>
|
<td><input type="checkbox" class="accounts-select-user" value=""></td>
|
||||||
<td><div class="table-inline"><span class="accounts-username py-2 mr-2"></span><span class="accounts-label-container ml-2"></span> <i class="icon ri-edit-line accounts-label-edit"></i> <span class="accounts-admin"></span> <span class="accounts-disabled"></span></span></div></td>
|
<td><div class="flex flex-row gap-2 items-center">
|
||||||
|
<span class="accounts-username"></span>
|
||||||
|
<span class="accounts-label-container" title="${window.lang.strings("label")}"></span>
|
||||||
|
<span class="accounts-admin"></span>
|
||||||
|
<span class="accounts-disabled"></span></span>
|
||||||
|
</div></td>
|
||||||
`;
|
`;
|
||||||
if (window.jellyfinLogin) {
|
if (window.jellyfinLogin) {
|
||||||
innerHTML += `
|
innerHTML += `
|
||||||
@ -506,7 +501,9 @@ class user implements User, SearchableItem {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
innerHTML += `
|
innerHTML += `
|
||||||
<td><div class="table-inline"><i class="icon ri-edit-line accounts-email-edit"></i><span class="accounts-email-container ml-2"></span></div></td>
|
<td><div class="flex flex-row gap-2 items-center">
|
||||||
|
<span class="accounts-email-container" title="${window.lang.strings("emailAddress")}"></span>
|
||||||
|
</div></td>
|
||||||
`;
|
`;
|
||||||
if (window.telegramEnabled) {
|
if (window.telegramEnabled) {
|
||||||
innerHTML += `
|
innerHTML += `
|
||||||
@ -534,21 +531,32 @@ class user implements User, SearchableItem {
|
|||||||
`;
|
`;
|
||||||
this._row.innerHTML = innerHTML;
|
this._row.innerHTML = innerHTML;
|
||||||
const emailEditor = `<input type="email" class="input ~neutral @low stealth-input">`;
|
const emailEditor = `<input type="email" class="input ~neutral @low stealth-input">`;
|
||||||
const labelEditor = `<input type="text" class="field ~neutral @low stealth-input">`;
|
|
||||||
this._check = this._row.querySelector("input[type=checkbox].accounts-select-user") as HTMLInputElement;
|
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._accounts_admin = this._row.querySelector("input[type=checkbox].accounts-access-jfa") as HTMLInputElement;
|
||||||
this._username = this._row.querySelector(".accounts-username") as HTMLSpanElement;
|
this._username = this._row.querySelector(".accounts-username") as HTMLSpanElement;
|
||||||
this._admin = this._row.querySelector(".accounts-admin") as HTMLSpanElement;
|
this._admin = this._row.querySelector(".accounts-admin") as HTMLSpanElement;
|
||||||
this._disabled = this._row.querySelector(".accounts-disabled") as HTMLSpanElement;
|
this._disabled = this._row.querySelector(".accounts-disabled") as HTMLSpanElement;
|
||||||
this._email = this._row.querySelector(".accounts-email-container") as HTMLInputElement;
|
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: `<span class="hidden-input-content"></span>`,
|
||||||
|
buttonOnLeft: true,
|
||||||
|
clickAwayShouldSave: false,
|
||||||
|
});
|
||||||
this._telegram = this._row.querySelector(".accounts-telegram") as HTMLTableDataCellElement;
|
this._telegram = this._row.querySelector(".accounts-telegram") as HTMLTableDataCellElement;
|
||||||
this._discord = this._row.querySelector(".accounts-discord") as HTMLTableDataCellElement;
|
this._discord = this._row.querySelector(".accounts-discord") as HTMLTableDataCellElement;
|
||||||
this._matrix = this._row.querySelector(".accounts-matrix") as HTMLTableDataCellElement;
|
this._matrix = this._row.querySelector(".accounts-matrix") as HTMLTableDataCellElement;
|
||||||
this._expiry = this._row.querySelector(".accounts-expiry") as HTMLTableDataCellElement;
|
this._expiry = this._row.querySelector(".accounts-expiry") as HTMLTableDataCellElement;
|
||||||
this._lastActive = this._row.querySelector(".accounts-last-active") as HTMLTableDataCellElement;
|
this._lastActive = this._row.querySelector(".accounts-last-active") as HTMLTableDataCellElement;
|
||||||
this._label = this._row.querySelector(".accounts-label-container") as HTMLInputElement;
|
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: `<span class="chip ~gray hidden-input-content"></span>`,
|
||||||
|
clickAwayShouldSave: false,
|
||||||
|
});
|
||||||
|
|
||||||
this._check.onchange = () => { this.selected = this._check.checked; }
|
this._check.onchange = () => { this.selected = this._check.checked; }
|
||||||
|
|
||||||
if (window.jellyfinLogin) {
|
if (window.jellyfinLogin) {
|
||||||
@ -573,66 +581,6 @@ class user implements User, SearchableItem {
|
|||||||
|
|
||||||
this._notifyDropdown = this._constructDropdown();
|
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);
|
this.update(user);
|
||||||
|
|
||||||
document.addEventListener("timefmt-change", () => {
|
document.addEventListener("timefmt-change", () => {
|
||||||
@ -642,14 +590,12 @@ class user implements User, SearchableItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _updateLabel = () => {
|
private _updateLabel = () => {
|
||||||
let oldLabel = this.label;
|
|
||||||
this.label = this._label.querySelector("input").value;
|
|
||||||
let send = {};
|
let send = {};
|
||||||
send[this.id] = this.label;
|
send[this.id] = this._labelEditor.value;
|
||||||
_post("/users/labels", send, (req: XMLHttpRequest) => {
|
_post("/users/labels", send, (req: XMLHttpRequest) => {
|
||||||
if (req.readyState == 4) {
|
if (req.readyState == 4) {
|
||||||
if (req.status != 204) {
|
if (req.status != 204) {
|
||||||
this.label = oldLabel;
|
this.label = this._labelEditor.previous;
|
||||||
window.notifications.customError("labelChanged", window.lang.notif("errorUnknown"));
|
window.notifications.customError("labelChanged", window.lang.notif("errorUnknown"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -657,16 +603,14 @@ class user implements User, SearchableItem {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private _updateEmail = () => {
|
private _updateEmail = () => {
|
||||||
let oldEmail = this.email;
|
|
||||||
this.email = this._email.querySelector("input").value;
|
|
||||||
let send = {};
|
let send = {};
|
||||||
send[this.id] = this.email;
|
send[this.id] = this._emailEditor.value;
|
||||||
_post("/users/emails", send, (req: XMLHttpRequest) => {
|
_post("/users/emails", send, (req: XMLHttpRequest) => {
|
||||||
if (req.readyState == 4) {
|
if (req.readyState == 4) {
|
||||||
if (req.status == 200) {
|
if (req.status == 200) {
|
||||||
window.notifications.customSuccess("emailChanged", window.lang.var("notifications", "changedEmailAddress", `"${this.name}"`));
|
window.notifications.customSuccess("emailChanged", window.lang.var("notifications", "changedEmailAddress", `"${this.name}"`));
|
||||||
} else {
|
} else {
|
||||||
this.email = oldEmail;
|
this.email = this._emailEditor.previous;
|
||||||
window.notifications.customError("emailChanged", window.lang.var("notifications", "errorChangedEmailAddress", `"${this.name}"`));
|
window.notifications.customError("emailChanged", window.lang.var("notifications", "errorChangedEmailAddress", `"${this.name}"`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
91
ts/modules/ui.ts
Normal file
91
ts/modules/ui.ts
Normal file
@ -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 = `<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-center">
|
||||||
|
${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.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); }
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user