mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
876 lines
27 KiB
JavaScript
876 lines
27 KiB
JavaScript
const noop = () => {};
|
|
|
|
const SERVICE_CONTRACT = [
|
|
{
|
|
key: "normalizeHexPubkey",
|
|
type: "function",
|
|
description:
|
|
"Normalizes a provided pubkey (hex or npub) so profile lookups are stable.",
|
|
},
|
|
{
|
|
key: "safeEncodeNpub",
|
|
type: "function",
|
|
description:
|
|
"Encodes a hex pubkey as an npub for display. Expected to be resilient to bad input.",
|
|
},
|
|
{
|
|
key: "safeDecodeNpub",
|
|
type: "function",
|
|
description:
|
|
"Decodes an npub back to its hex form. Should return null/undefined for invalid payloads.",
|
|
},
|
|
{
|
|
key: "truncateMiddle",
|
|
type: "function",
|
|
description:
|
|
"Utility used for shortening long identifiers in the modal without losing context.",
|
|
},
|
|
{
|
|
key: "getProfileCacheEntry",
|
|
type: "function",
|
|
description:
|
|
"Returns the cached profile metadata for a normalized pubkey (if available).",
|
|
},
|
|
{
|
|
key: "batchFetchProfiles",
|
|
type: "function",
|
|
description:
|
|
"Fetches and caches profile metadata for a set of pubkeys so the modal can hydrate avatars/names in bulk.",
|
|
},
|
|
{
|
|
key: "switchProfile",
|
|
type: "function",
|
|
description:
|
|
"Switches the active application profile to the provided pubkey and updates state accordingly.",
|
|
},
|
|
{
|
|
key: "relayManager",
|
|
type: "object",
|
|
description:
|
|
"Shared relay manager instance responsible for the user\'s relay configuration.",
|
|
},
|
|
{
|
|
key: "userBlocks",
|
|
type: "object",
|
|
description: "User blocks helper used to read and mutate the local block list.",
|
|
},
|
|
{
|
|
key: "nostrClient",
|
|
type: "object",
|
|
description: "Reference to the nostr client powering subscriptions and profile fetches.",
|
|
},
|
|
{
|
|
key: "accessControl",
|
|
type: "object",
|
|
description:
|
|
"Access control module required for admin list permission checks and updates.",
|
|
},
|
|
{
|
|
key: "getCurrentUserNpub",
|
|
type: "function",
|
|
description: "Returns the active user\'s npub so UI actions can target the correct actor.",
|
|
},
|
|
{
|
|
key: "getActiveNwcSettings",
|
|
type: "function",
|
|
description:
|
|
"Reads the active Nostr Wallet Connect settings (uri/default zap) for the logged in profile.",
|
|
},
|
|
{
|
|
key: "updateActiveNwcSettings",
|
|
type: "function",
|
|
description: "Persists updates to the active wallet settings and returns the new state.",
|
|
},
|
|
{
|
|
key: "createDefaultNwcSettings",
|
|
type: "function",
|
|
description: "Creates a baseline wallet settings object when none exists.",
|
|
},
|
|
{
|
|
key: "ensureWallet",
|
|
type: "function",
|
|
description:
|
|
"Guarantees a wallet connection exists before issuing wallet related operations (connect/test).",
|
|
},
|
|
{
|
|
key: "loadVideos",
|
|
type: "function",
|
|
description: "Triggers a video reload so UI reflects profile or permission changes.",
|
|
},
|
|
{
|
|
key: "sendAdminListNotification",
|
|
type: "function",
|
|
description:
|
|
"Sends an administrative notification when moderators/whitelist/blacklist entries change.",
|
|
},
|
|
{
|
|
key: "describeAdminError",
|
|
type: "function",
|
|
description:
|
|
"Maps low-level admin errors to readable copy for surfaced error messages.",
|
|
},
|
|
{
|
|
key: "describeNotificationError",
|
|
type: "function",
|
|
description:
|
|
"Maps notification send errors to human readable descriptions for toast/banner messaging.",
|
|
},
|
|
{
|
|
key: "onAccessControlUpdated",
|
|
type: "function",
|
|
description:
|
|
"Callback invoked after access control mutations so the app can refresh dependent UI.",
|
|
},
|
|
];
|
|
|
|
const STATE_CONTRACT = [
|
|
{
|
|
key: "getSavedProfiles",
|
|
type: "function",
|
|
description:
|
|
"Returns the saved profile entries that populate the account switcher UI.",
|
|
fallback: (internal) => () => internal.savedProfiles.slice(),
|
|
},
|
|
{
|
|
key: "setSavedProfiles",
|
|
type: "function",
|
|
description:
|
|
"Replaces the saved profile entries and returns the newly stored collection.",
|
|
fallback: (internal) => (profiles = []) => {
|
|
internal.savedProfiles = Array.isArray(profiles)
|
|
? profiles.slice()
|
|
: [];
|
|
return internal.savedProfiles;
|
|
},
|
|
},
|
|
{
|
|
key: "persistSavedProfiles",
|
|
type: "function",
|
|
description:
|
|
"Persists the saved profile collection (and optionally the active profile) to storage.",
|
|
fallback: () => noop,
|
|
},
|
|
{
|
|
key: "getActivePubkey",
|
|
type: "function",
|
|
description: "Returns the currently active profile pubkey.",
|
|
fallback: (internal) => () => internal.activePubkey,
|
|
},
|
|
{
|
|
key: "setActivePubkey",
|
|
type: "function",
|
|
description: "Updates the active profile pubkey and returns the stored value.",
|
|
fallback: (internal) => (pubkey) => {
|
|
if (typeof pubkey === "string") {
|
|
const trimmed = pubkey.trim();
|
|
internal.activePubkey = trimmed ? trimmed : null;
|
|
} else {
|
|
internal.activePubkey = null;
|
|
}
|
|
return internal.activePubkey;
|
|
},
|
|
},
|
|
{
|
|
key: "getCachedSelection",
|
|
type: "function",
|
|
description:
|
|
"Reads the cached profile selection used to restore switcher focus after reloads.",
|
|
fallback: (internal) => () => internal.cachedSelection,
|
|
},
|
|
{
|
|
key: "setCachedSelection",
|
|
type: "function",
|
|
description:
|
|
"Caches the last selected profile identifier and returns the stored value.",
|
|
fallback: (internal) => (value) => {
|
|
if (typeof value === "string") {
|
|
const trimmed = value.trim();
|
|
internal.cachedSelection = trimmed || null;
|
|
} else {
|
|
internal.cachedSelection = null;
|
|
}
|
|
return internal.cachedSelection;
|
|
},
|
|
},
|
|
{
|
|
key: "getActivePane",
|
|
type: "function",
|
|
description: "Returns the identifier for the currently visible profile modal pane.",
|
|
fallback: (internal) => () => internal.activePane,
|
|
},
|
|
{
|
|
key: "setActivePane",
|
|
type: "function",
|
|
description: "Updates the active pane identifier and returns the stored value.",
|
|
fallback: (internal) => (pane) => {
|
|
if (typeof pane === "string") {
|
|
const trimmed = pane.trim().toLowerCase();
|
|
internal.activePane = trimmed || "account";
|
|
} else {
|
|
internal.activePane = "account";
|
|
}
|
|
return internal.activePane;
|
|
},
|
|
},
|
|
{
|
|
key: "getWalletBusy",
|
|
type: "function",
|
|
description:
|
|
"Indicates whether wallet-related actions are currently in-flight (disables UI as needed).",
|
|
fallback: (internal) => () => internal.walletBusy,
|
|
},
|
|
{
|
|
key: "setWalletBusy",
|
|
type: "function",
|
|
description: "Updates the wallet busy flag and returns the new boolean state.",
|
|
fallback: (internal) => (isBusy) => {
|
|
internal.walletBusy = Boolean(isBusy);
|
|
return internal.walletBusy;
|
|
},
|
|
},
|
|
];
|
|
|
|
function buildServicesContract(services = {}) {
|
|
const resolved = {};
|
|
const missing = [];
|
|
|
|
SERVICE_CONTRACT.forEach((definition) => {
|
|
const value = services[definition.key];
|
|
if (value == null) {
|
|
missing.push(`- ${definition.key}: ${definition.description}`);
|
|
return;
|
|
}
|
|
|
|
if (definition.type === "function" && typeof value !== "function") {
|
|
throw new TypeError(
|
|
`Expected service \"${definition.key}\" to be a function. Received ${typeof value}.`,
|
|
);
|
|
}
|
|
if (definition.type === "object" && (typeof value !== "object" || value === null)) {
|
|
throw new TypeError(
|
|
`Expected service \"${definition.key}\" to be an object. Received ${typeof value}.`,
|
|
);
|
|
}
|
|
|
|
resolved[definition.key] = value;
|
|
});
|
|
|
|
if (missing.length) {
|
|
throw new Error(
|
|
[
|
|
"[ProfileModalController] Missing required services for profile modal controller:",
|
|
...missing,
|
|
].join("\n"),
|
|
);
|
|
}
|
|
|
|
return Object.freeze({ ...services, ...resolved });
|
|
}
|
|
|
|
function buildStateContract(state = {}, internalState) {
|
|
const resolved = {};
|
|
|
|
STATE_CONTRACT.forEach((definition) => {
|
|
const value = state[definition.key];
|
|
if (typeof value === definition.type) {
|
|
resolved[definition.key] = value;
|
|
return;
|
|
}
|
|
|
|
if (definition.fallback) {
|
|
const fallback = definition.fallback(internalState);
|
|
if (typeof fallback === definition.type) {
|
|
resolved[definition.key] = fallback;
|
|
return;
|
|
}
|
|
}
|
|
|
|
throw new Error(
|
|
`[ProfileModalController] Missing state handler \"${definition.key}\" (${definition.description}).`,
|
|
);
|
|
});
|
|
|
|
return { ...state, ...resolved };
|
|
}
|
|
|
|
export class ProfileModalController {
|
|
constructor(options = {}) {
|
|
const {
|
|
modalContainer = null,
|
|
removeTrackingScripts = noop,
|
|
createWatchHistoryRenderer = null,
|
|
setGlobalModalState = noop,
|
|
showError = noop,
|
|
showSuccess = noop,
|
|
showStatus = noop,
|
|
callbacks = {},
|
|
services = {},
|
|
state = {},
|
|
} = options;
|
|
|
|
this.modalContainer = modalContainer;
|
|
this.removeTrackingScripts = removeTrackingScripts;
|
|
this.createWatchHistoryRenderer = createWatchHistoryRenderer;
|
|
this.setGlobalModalState = setGlobalModalState;
|
|
this.showError = showError;
|
|
this.showSuccess = showSuccess;
|
|
this.showStatus = showStatus;
|
|
|
|
this.internalState = {
|
|
savedProfiles: [],
|
|
activePubkey: null,
|
|
cachedSelection: null,
|
|
activePane: "account",
|
|
walletBusy: false,
|
|
};
|
|
|
|
this.services = buildServicesContract(services);
|
|
this.state = buildStateContract(state, this.internalState);
|
|
|
|
this.normalizeHexPubkey = this.services.normalizeHexPubkey;
|
|
this.safeEncodeNpub = this.services.safeEncodeNpub;
|
|
this.safeDecodeNpub = this.services.safeDecodeNpub;
|
|
this.truncateMiddle = this.services.truncateMiddle;
|
|
this.getProfileCacheEntry = this.services.getProfileCacheEntry;
|
|
this.batchFetchProfiles = this.services.batchFetchProfiles;
|
|
this.switchProfile = this.services.switchProfile;
|
|
this.relayManager = this.services.relayManager;
|
|
this.userBlocks = this.services.userBlocks;
|
|
this.nostrClient = this.services.nostrClient;
|
|
this.accessControl = this.services.accessControl;
|
|
this.getCurrentUserNpub = this.services.getCurrentUserNpub;
|
|
this.getActiveNwcSettings = this.services.getActiveNwcSettings;
|
|
this.updateActiveNwcSettings = this.services.updateActiveNwcSettings;
|
|
this.createDefaultNwcSettings = this.services.createDefaultNwcSettings;
|
|
this.ensureWallet = this.services.ensureWallet;
|
|
this.loadVideos = this.services.loadVideos;
|
|
this.sendAdminListNotification = this.services.sendAdminListNotification;
|
|
this.describeAdminError = this.services.describeAdminError;
|
|
this.describeNotificationError = this.services.describeNotificationError;
|
|
this.onAccessControlUpdated = this.services.onAccessControlUpdated;
|
|
|
|
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.setActivePane(this.getActivePane());
|
|
this.setWalletPaneBusy(this.isWalletBusy());
|
|
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.getActivePane(), { 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.getActivePane() === 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.setActivePane(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);
|
|
}
|
|
}
|
|
|
|
getSavedProfiles() {
|
|
return this.state.getSavedProfiles();
|
|
}
|
|
|
|
setSavedProfiles(...args) {
|
|
return this.state.setSavedProfiles(...args);
|
|
}
|
|
|
|
persistSavedProfiles(...args) {
|
|
return this.state.persistSavedProfiles(...args);
|
|
}
|
|
|
|
getActivePubkey() {
|
|
return this.state.getActivePubkey();
|
|
}
|
|
|
|
setActivePubkey(...args) {
|
|
return this.state.setActivePubkey(...args);
|
|
}
|
|
|
|
getCachedProfileSelection() {
|
|
return this.state.getCachedSelection();
|
|
}
|
|
|
|
setCachedProfileSelection(...args) {
|
|
return this.state.setCachedSelection(...args);
|
|
}
|
|
|
|
getActivePane() {
|
|
return this.state.getActivePane();
|
|
}
|
|
|
|
setActivePane(...args) {
|
|
return this.state.setActivePane(...args);
|
|
}
|
|
|
|
isWalletBusy() {
|
|
return Boolean(this.state.getWalletBusy());
|
|
}
|
|
|
|
setWalletPaneBusy(isBusy) {
|
|
return this.state.setWalletBusy(Boolean(isBusy));
|
|
}
|
|
}
|
|
|
|
export default ProfileModalController;
|