mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Add profile modal controller skeleton
This commit is contained in:
1 parent
6dd970d55b
commit
e07d4c4cab
1 file changed
+503
@@ -0,0 +1,503 @@
|
||||
const noop = () => {};
|
||||
|
||||
export class ProfileModalController {
|
||||
constructor(options = {}) {
|
||||
const {
|
||||
modalContainer = null,
|
||||
removeTrackingScripts = noop,
|
||||
createWatchHistoryRenderer = null,
|
||||
setGlobalModalState = noop,
|
||||
showError = noop,
|
||||
showSuccess = noop,
|
||||
showStatus = noop,
|
||||
callbacks = {},
|
||||
} = options;
|
||||
|
||||
this.modalContainer = modalContainer;
|
||||
this.removeTrackingScripts = removeTrackingScripts;
|
||||
this.createWatchHistoryRenderer = createWatchHistoryRenderer;
|
||||
this.setGlobalModalState = setGlobalModalState;
|
||||
this.showError = showError;
|
||||
this.showSuccess = showSuccess;
|
||||
this.showStatus = showStatus;
|
||||
|
||||
this.callbacks = {
|
||||
onClose: callbacks.onClose || noop,
|
||||
onLogout: callbacks.onLogout || noop,
|
||||
onChannelLink: callbacks.onChannelLink || noop,
|
||||
onAddAccount: callbacks.onAddAccount || noop,
|
||||
onSelectPane: callbacks.onSelectPane || noop,
|
||||
onPaneShown: callbacks.onPaneShown || noop,
|
||||
onAddRelay: callbacks.onAddRelay || noop,
|
||||
onRestoreRelays: callbacks.onRestoreRelays || noop,
|
||||
onAddBlocked: callbacks.onAddBlocked || noop,
|
||||
onWalletSave: callbacks.onWalletSave || noop,
|
||||
onWalletTest: callbacks.onWalletTest || noop,
|
||||
onWalletDisconnect: callbacks.onWalletDisconnect || noop,
|
||||
onAdminAddModerator: callbacks.onAdminAddModerator || noop,
|
||||
onAdminAddWhitelist: callbacks.onAdminAddWhitelist || noop,
|
||||
onAdminAddBlacklist: callbacks.onAdminAddBlacklist || noop,
|
||||
onAdminRemoveModerator: callbacks.onAdminRemoveModerator || noop,
|
||||
onAdminRemoveWhitelist: callbacks.onAdminRemoveWhitelist || noop,
|
||||
onAdminRemoveBlacklist: callbacks.onAdminRemoveBlacklist || noop,
|
||||
onHistoryReady: callbacks.onHistoryReady || noop,
|
||||
};
|
||||
|
||||
this.profileModal = null;
|
||||
this.closeButton = null;
|
||||
this.logoutButton = null;
|
||||
this.channelLink = null;
|
||||
this.addAccountButton = null;
|
||||
this.navButtons = {
|
||||
account: null,
|
||||
relays: null,
|
||||
wallet: null,
|
||||
blocked: null,
|
||||
history: null,
|
||||
admin: null,
|
||||
};
|
||||
this.panes = {
|
||||
account: null,
|
||||
relays: null,
|
||||
wallet: null,
|
||||
blocked: null,
|
||||
history: null,
|
||||
admin: null,
|
||||
};
|
||||
this.relayList = null;
|
||||
this.relayInput = null;
|
||||
this.relayAddButton = null;
|
||||
this.relayRestoreButton = null;
|
||||
this.blockList = null;
|
||||
this.blockInput = null;
|
||||
this.blockAddButton = null;
|
||||
this.walletUriInput = null;
|
||||
this.walletDefaultZapInput = null;
|
||||
this.walletSaveButton = null;
|
||||
this.walletTestButton = null;
|
||||
this.walletDisconnectButton = null;
|
||||
this.adminAddModeratorButton = null;
|
||||
this.adminModeratorInput = null;
|
||||
this.adminAddWhitelistButton = null;
|
||||
this.adminWhitelistInput = null;
|
||||
this.adminAddBlacklistButton = null;
|
||||
this.adminBlacklistInput = null;
|
||||
|
||||
this.profileHistoryRenderer = null;
|
||||
this.boundKeydown = null;
|
||||
this.boundFocusIn = null;
|
||||
this.focusableElements = [];
|
||||
this.activeProfilePane = "account";
|
||||
this.isWalletPaneBusy = false;
|
||||
this.adminEmptyMessages = new Map();
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (!(this.modalContainer instanceof HTMLElement)) {
|
||||
throw new Error("profile modal container missing");
|
||||
}
|
||||
|
||||
const response = await fetch("components/profile-modal.html");
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load profile modal HTML (${response.status})`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.innerHTML = html;
|
||||
this.removeTrackingScripts(wrapper);
|
||||
this.modalContainer.appendChild(wrapper);
|
||||
|
||||
this.cacheDomReferences();
|
||||
this.registerEventListeners();
|
||||
this.updateFocusTrap();
|
||||
this.callbacks.onPaneShown(this.activeProfilePane, { controller: this });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
cacheDomReferences() {
|
||||
this.profileModal = document.getElementById("profileModal") || null;
|
||||
this.closeButton = document.getElementById("closeProfileModal") || null;
|
||||
this.logoutButton = document.getElementById("profileLogoutBtn") || null;
|
||||
this.channelLink = document.getElementById("profileChannelLink") || null;
|
||||
this.addAccountButton =
|
||||
document.getElementById("profileAddAccountBtn") || null;
|
||||
|
||||
this.navButtons.account =
|
||||
document.getElementById("profileNavAccount") || null;
|
||||
this.navButtons.relays = document.getElementById("profileNavRelays") || null;
|
||||
this.navButtons.wallet = document.getElementById("profileNavWallet") || null;
|
||||
this.navButtons.blocked =
|
||||
document.getElementById("profileNavBlocked") || null;
|
||||
this.navButtons.history =
|
||||
document.getElementById("profileNavHistory") || null;
|
||||
this.navButtons.admin = document.getElementById("profileNavAdmin") || null;
|
||||
|
||||
this.panes.account = document.getElementById("profilePaneAccount") || null;
|
||||
this.panes.relays = document.getElementById("profilePaneRelays") || null;
|
||||
this.panes.wallet = document.getElementById("profilePaneWallet") || null;
|
||||
this.panes.blocked = document.getElementById("profilePaneBlocked") || null;
|
||||
this.panes.history = document.getElementById("profilePaneHistory") || null;
|
||||
this.panes.admin = document.getElementById("profilePaneAdmin") || null;
|
||||
|
||||
this.relayList = document.getElementById("relayList") || null;
|
||||
this.relayInput = document.getElementById("relayInput") || null;
|
||||
this.relayAddButton = document.getElementById("addRelayBtn") || null;
|
||||
this.relayRestoreButton =
|
||||
document.getElementById("restoreRelaysBtn") || null;
|
||||
|
||||
this.blockList = document.getElementById("blockedList") || null;
|
||||
this.blockInput = document.getElementById("blockedInput") || null;
|
||||
this.blockAddButton = document.getElementById("addBlockedBtn") || null;
|
||||
|
||||
this.walletUriInput = document.getElementById("profileWalletUri") || null;
|
||||
this.walletDefaultZapInput =
|
||||
document.getElementById("profileWalletDefaultZap") || null;
|
||||
this.walletSaveButton =
|
||||
document.getElementById("profileWalletSave") || null;
|
||||
this.walletTestButton =
|
||||
document.getElementById("profileWalletTest") || null;
|
||||
this.walletDisconnectButton =
|
||||
document.getElementById("profileWalletDisconnect") || null;
|
||||
|
||||
this.adminAddModeratorButton =
|
||||
document.getElementById("adminAddModeratorBtn") || null;
|
||||
this.adminModeratorInput =
|
||||
document.getElementById("adminModeratorInput") || null;
|
||||
this.adminAddWhitelistButton =
|
||||
document.getElementById("adminAddWhitelistBtn") || null;
|
||||
this.adminWhitelistInput =
|
||||
document.getElementById("adminWhitelistInput") || null;
|
||||
this.adminAddBlacklistButton =
|
||||
document.getElementById("adminAddBlacklistBtn") || null;
|
||||
this.adminBlacklistInput =
|
||||
document.getElementById("adminBlacklistInput") || null;
|
||||
|
||||
if (!this.profileHistoryRenderer && this.createWatchHistoryRenderer) {
|
||||
this.profileHistoryRenderer = this.createWatchHistoryRenderer({
|
||||
viewSelector: "#profilePaneHistory",
|
||||
gridSelector: "#profileHistoryGrid",
|
||||
loadingSelector: "#profileHistoryLoading",
|
||||
statusSelector: "#profileHistoryStatus",
|
||||
emptySelector: "#profileHistoryEmpty",
|
||||
sentinelSelector: "#profileHistorySentinel",
|
||||
scrollContainerSelector: "#profileHistoryScroll",
|
||||
errorBannerSelector: "#profileHistoryError",
|
||||
clearButtonSelector: "#profileHistoryClear",
|
||||
republishButtonSelector: "#profileHistoryRepublish",
|
||||
featureBannerSelector: "#profileHistoryFeatureBanner",
|
||||
toastRegionSelector: "#profileHistoryToastRegion",
|
||||
sessionWarningSelector: "#profileHistorySessionWarning",
|
||||
metadataToggleSelector: "#profileHistoryMetadataToggle",
|
||||
metadataThumbSelector: "#profileHistoryMetadataThumb",
|
||||
metadataLabelSelector: "#profileHistoryMetadataLabel",
|
||||
metadataDescriptionSelector: "#profileHistoryMetadataDescription",
|
||||
remove: (payload) => this.callbacks.onHistoryReady(payload, this),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
registerEventListeners() {
|
||||
if (this.closeButton instanceof HTMLElement) {
|
||||
this.closeButton.addEventListener("click", () => {
|
||||
this.hide();
|
||||
this.callbacks.onClose(this);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.logoutButton instanceof HTMLElement) {
|
||||
this.logoutButton.addEventListener("click", async () => {
|
||||
try {
|
||||
await this.callbacks.onLogout(this);
|
||||
} catch (error) {
|
||||
this.showError("Failed to logout. Please try again.");
|
||||
}
|
||||
this.hide();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.channelLink instanceof HTMLElement) {
|
||||
this.channelLink.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
this.callbacks.onChannelLink(this.channelLink, this);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.addAccountButton instanceof HTMLElement) {
|
||||
this.addAccountButton.addEventListener("click", () => {
|
||||
this.callbacks.onAddAccount(this);
|
||||
});
|
||||
}
|
||||
|
||||
Object.entries(this.navButtons).forEach(([name, button]) => {
|
||||
if (button instanceof HTMLElement) {
|
||||
button.addEventListener("click", () => {
|
||||
this.selectPane(name);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (this.relayAddButton instanceof HTMLElement) {
|
||||
this.relayAddButton.addEventListener("click", () => {
|
||||
this.callbacks.onAddRelay(this.relayInput, this);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.relayRestoreButton instanceof HTMLElement) {
|
||||
this.relayRestoreButton.addEventListener("click", () => {
|
||||
this.callbacks.onRestoreRelays(this);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.blockAddButton instanceof HTMLElement) {
|
||||
this.blockAddButton.addEventListener("click", () => {
|
||||
this.callbacks.onAddBlocked(this.blockInput, this);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.walletUriInput instanceof HTMLElement) {
|
||||
this.walletUriInput.addEventListener("input", () => {
|
||||
this.callbacks.onWalletInputChange?.(this.walletUriInput, this);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.walletDefaultZapInput instanceof HTMLElement) {
|
||||
this.walletDefaultZapInput.addEventListener("input", () => {
|
||||
this.callbacks.onWalletInputChange?.(this.walletDefaultZapInput, this);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.walletSaveButton instanceof HTMLElement) {
|
||||
this.walletSaveButton.addEventListener("click", () => {
|
||||
this.callbacks.onWalletSave(this);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.walletTestButton instanceof HTMLElement) {
|
||||
this.walletTestButton.addEventListener("click", () => {
|
||||
this.callbacks.onWalletTest(this);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.walletDisconnectButton instanceof HTMLElement) {
|
||||
this.walletDisconnectButton.addEventListener("click", () => {
|
||||
this.callbacks.onWalletDisconnect(this);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.adminAddModeratorButton instanceof HTMLElement) {
|
||||
this.adminAddModeratorButton.addEventListener("click", () => {
|
||||
this.callbacks.onAdminAddModerator(this.adminModeratorInput, this);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.adminModeratorInput instanceof HTMLElement) {
|
||||
this.adminModeratorInput.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
this.callbacks.onAdminAddModerator(this.adminModeratorInput, this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.adminAddWhitelistButton instanceof HTMLElement) {
|
||||
this.adminAddWhitelistButton.addEventListener("click", () => {
|
||||
this.callbacks.onAdminAddWhitelist(this.adminWhitelistInput, this);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.adminWhitelistInput instanceof HTMLElement) {
|
||||
this.adminWhitelistInput.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
this.callbacks.onAdminAddWhitelist(this.adminWhitelistInput, this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.adminAddBlacklistButton instanceof HTMLElement) {
|
||||
this.adminAddBlacklistButton.addEventListener("click", () => {
|
||||
this.callbacks.onAdminAddBlacklist(this.adminBlacklistInput, this);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.adminBlacklistInput instanceof HTMLElement) {
|
||||
this.adminBlacklistInput.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
this.callbacks.onAdminAddBlacklist(this.adminBlacklistInput, this);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
selectPane(name = "account") {
|
||||
const normalized = typeof name === "string" ? name.toLowerCase() : "account";
|
||||
if (this.activeProfilePane === normalized) {
|
||||
this.callbacks.onSelectPane(normalized, { controller: this });
|
||||
return;
|
||||
}
|
||||
|
||||
Object.entries(this.panes).forEach(([key, pane]) => {
|
||||
if (pane instanceof HTMLElement) {
|
||||
const isActive = key === normalized;
|
||||
pane.classList.toggle("hidden", !isActive);
|
||||
pane.setAttribute("aria-hidden", String(!isActive));
|
||||
}
|
||||
});
|
||||
|
||||
Object.entries(this.navButtons).forEach(([key, button]) => {
|
||||
if (button instanceof HTMLElement) {
|
||||
const isActive = key === normalized;
|
||||
button.setAttribute("aria-selected", isActive ? "true" : "false");
|
||||
button.classList.toggle("bg-gray-800", isActive);
|
||||
button.classList.toggle("text-white", isActive);
|
||||
button.classList.toggle("text-gray-400", !isActive);
|
||||
}
|
||||
});
|
||||
|
||||
this.activeProfilePane = normalized;
|
||||
this.updateFocusTrap();
|
||||
this.callbacks.onSelectPane(normalized, { controller: this });
|
||||
this.callbacks.onPaneShown(normalized, { controller: this });
|
||||
}
|
||||
|
||||
updateFocusTrap() {
|
||||
if (!(this.profileModal instanceof HTMLElement)) {
|
||||
this.focusableElements = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const selector =
|
||||
'a[href], button:not([disabled]), textarea, input, select, [tabindex]:not([tabindex="-1"])';
|
||||
const nodes = Array.from(
|
||||
this.profileModal.querySelectorAll(selector),
|
||||
);
|
||||
this.focusableElements = nodes.filter((node) => {
|
||||
if (!(node instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
const style = window.getComputedStyle(node);
|
||||
if (style.display === "none" || style.visibility === "hidden") {
|
||||
return false;
|
||||
}
|
||||
if (node.hasAttribute("disabled")) {
|
||||
return false;
|
||||
}
|
||||
if (node.getAttribute("aria-hidden") === "true") {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
this.bindFocusTrap();
|
||||
}
|
||||
|
||||
bindFocusTrap() {
|
||||
if (!(this.profileModal instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.boundKeydown) {
|
||||
this.boundKeydown = (event) => {
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
this.hide();
|
||||
this.callbacks.onClose(this);
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key !== "Tab") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.focusableElements.length) {
|
||||
event.preventDefault();
|
||||
if (typeof this.profileModal.focus === "function") {
|
||||
this.profileModal.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const first = this.focusableElements[0];
|
||||
const last = this.focusableElements[this.focusableElements.length - 1];
|
||||
const active = document.activeElement;
|
||||
|
||||
if (event.shiftKey) {
|
||||
if (active === first || !this.profileModal.contains(active)) {
|
||||
event.preventDefault();
|
||||
if (typeof last?.focus === "function") {
|
||||
last.focus();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (active === last) {
|
||||
event.preventDefault();
|
||||
if (typeof first?.focus === "function") {
|
||||
first.focus();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.boundFocusIn) {
|
||||
this.boundFocusIn = (event) => {
|
||||
if (
|
||||
!this.profileModal ||
|
||||
this.profileModal.classList.contains("hidden") ||
|
||||
this.profileModal.contains(event.target)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = this.focusableElements[0] || this.profileModal;
|
||||
if (typeof target?.focus === "function") {
|
||||
target.focus();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.profileModal.addEventListener("keydown", this.boundKeydown);
|
||||
document.addEventListener("focusin", this.boundFocusIn);
|
||||
}
|
||||
|
||||
open(pane = "account") {
|
||||
if (!(this.profileModal instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.profileModal.classList.remove("hidden");
|
||||
this.profileModal.setAttribute("aria-hidden", "false");
|
||||
this.setGlobalModalState("profile", true);
|
||||
this.selectPane(pane);
|
||||
|
||||
const focusTarget = this.focusableElements[0] || this.profileModal;
|
||||
window.requestAnimationFrame(() => {
|
||||
if (typeof focusTarget?.focus === "function") {
|
||||
focusTarget.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hide() {
|
||||
if (!(this.profileModal instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.profileModal.classList.add("hidden");
|
||||
this.profileModal.setAttribute("aria-hidden", "true");
|
||||
this.setGlobalModalState("profile", false);
|
||||
|
||||
if (this.boundKeydown) {
|
||||
this.profileModal.removeEventListener("keydown", this.boundKeydown);
|
||||
}
|
||||
if (this.boundFocusIn) {
|
||||
document.removeEventListener("focusin", this.boundFocusIn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ProfileModalController;
|
||||
Reference in new issue
Block a user