import { applyAssetStatusDotState, createWorkflowDot } from "../../../components/Badges.js"; import { post, getAssetMetadata, vectorIndexAsset } from "../../../api/client.js"; import { ENDPOINTS } from "../../../api/endpoints.js"; import { pickRootId } from "../../../utils/ids.js"; import { safeClosest } from "../../../utils/dom.js"; import { comfyToast } from "../../../app/toast.js"; import { t } from "../../../app/i18n.js"; import { createPanelStateBridge } from "../../../stores/panelStateBridge.js"; const RESCAN_FLAG = "_mjrRescanning"; const RESCAN_TTL_MS = 1500; const _isPositiveIntId = (value: any) => /^\d+$/.test(String(value ?? "").trim()); async function rescanSingleAsset({ card, asset, sidebar, onAssetUpdated }: Record): Promise { if (!card || !asset) return; if ((globalThis as any)._mjrMaintenanceActive) return; try { const now = Date.now(); const last = Number(card[RESCAN_FLAG] || 0) || 0; if (now - last < RESCAN_TTL_MS) return; card[RESCAN_FLAG] = now; } catch (e) { console.debug?.(e); } const dot = card.querySelector(".mjr-workflow-dot"); try { if (dot) { applyAssetStatusDotState(dot, "pending", "Pending: metadata refresh in progress", { asset, }); dot.classList.add("mjr-pulse-animation"); dot.style.cursor = "progress"; dot.title = t("tooltip.pendingRefresh", "Pending: metadata refresh in progress"); } } catch (e) { console.debug?.(e); } try { comfyToast( t("toast.rescanUpdatingAiIndex", "Rescanning file + updating AI index..."), "info", 2200, ); } catch (e) { console.debug?.(e); } const fileEntry = { filename: asset.filename, subfolder: asset.subfolder || "", type: String(asset.type || "output").toLowerCase(), root_id: pickRootId(asset) || undefined, }; let updated: any = null; let indexOk = false; let vectorAttempted = false; let vectorIndexOk = false; try { const indexRes = await post(ENDPOINTS.INDEX_FILES, { files: [fileEntry], incremental: false, }); indexOk = !!indexRes?.ok; if (!indexOk && dot) { applyAssetStatusDotState(dot, "error", "Error: metadata refresh failed", { asset }); dot.classList.remove("mjr-pulse-animation"); dot.style.cursor = ""; } // Keep AI search index synced with manual per-asset rescan from the card dot. // Best-effort: do not fail metadata refresh if vector indexing fails or is unavailable. if (_isPositiveIntId(asset?.id)) { vectorAttempted = true; try { const vectorRes = await vectorIndexAsset(asset.id); vectorIndexOk = !!vectorRes?.ok; } catch (e) { console.debug?.(e); } } if (indexOk && asset.id != null) { const fresh = await getAssetMetadata(asset.id); if (fresh?.ok && fresh.data) { updated = { ...asset, ...fresh.data }; } } } catch { try { if (dot) { applyAssetStatusDotState(dot, "error", "Error: metadata refresh failed", { asset }); dot.classList.remove("mjr-pulse-animation"); dot.style.cursor = ""; } } catch (e) { console.debug?.(e); } } try { if (updated) { (card as any)._mjrAsset = updated; if (typeof onAssetUpdated === "function") onAssetUpdated(updated); try { if (sidebar?.classList?.contains?.("is-open")) { const curId = sidebar?._currentAsset?.id ?? sidebar?._currentFullAsset?.id ?? null; if (curId != null && String(curId) === String(updated.id)) { sidebar._mjrNeedsRefresh = true; } } } catch (e) { console.debug?.(e); } } } finally { try { if (dot && dot.isConnected) { const restored = createWorkflowDot(updated || asset); if (restored) dot.replaceWith(restored); } } catch { try { if (dot) dot.style.cursor = ""; } catch (e) { console.debug?.(e); } } } try { if (indexOk && vectorIndexOk) { comfyToast( t( "toast.metadataVectorUpdated", "Metadata + AI vector index updated for this asset.", ), "success", 2200, ); } else if (indexOk && vectorAttempted && !vectorIndexOk) { comfyToast( t( "toast.metadataUpdatedVectorFailed", "Metadata updated. AI vector index could not be updated.", ), "warning", 2600, ); } } catch (e) { console.debug?.(e); } return updated; } function setSelectedCard(gridContainer: any, selectedCard: any) { if (!gridContainer) return; try { const prev = gridContainer.querySelector(".mjr-asset-card.is-selected"); if (prev && prev !== selectedCard) { prev.classList.remove("is-selected"); prev.setAttribute?.("aria-selected", "false"); } } catch (e) { console.debug?.(e); } if (selectedCard) { selectedCard.classList.add("is-selected"); selectedCard.setAttribute?.("aria-selected", "true"); try { const id = selectedCard.dataset?.mjrAssetId; if (id) gridContainer.dataset.mjrSelectedAssetId = String(id); } catch (e) { console.debug?.(e); } try { selectedCard.focus?.({ preventScroll: true }); } catch { try { selectedCard.focus?.(); } catch (e) { console.debug?.(e); } } try { selectedCard.scrollIntoView?.({ block: "nearest", inline: "nearest" }); } catch (e) { console.debug?.(e); } } } const escapeAttributeValue = (value: any) => { if (value == null) return ""; try { if (typeof CSS !== "undefined" && typeof CSS.escape === "function") { return CSS.escape(String(value)); } } catch (e) { console.debug?.(e); } return String(value).replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, "\\$1"); }; function queryCardById(gridContainer: any, assetId: any) { if (!gridContainer || !assetId) return null; try { return gridContainer.querySelector( `.mjr-asset-card[data-mjr-asset-id="${escapeAttributeValue(assetId)}"]`, ); } catch (e) { console.debug?.(e); } try { return gridContainer.querySelector( `.mjr-asset-card[data-mjr-asset-id="${escapeAttributeValue(assetId)}"]`, ); } catch (e) { console.debug?.(e); } return null; } function getActiveCardElement(gridContainer: any) { if (!gridContainer) return null; const activeId = gridContainer.dataset?.mjrSelectedAssetId; if (activeId) { const card = queryCardById(gridContainer, activeId); if (card) return card; } const selectedIds = gridContainer.dataset?.mjrSelectedAssetIds; if (selectedIds) { try { const parsed = JSON.parse(selectedIds); if (Array.isArray(parsed) && parsed.length) { const card = queryCardById(gridContainer, parsed[0]); if (card) return card; } } catch (e) { console.debug?.(e); } } return gridContainer.querySelector(".mjr-asset-card.is-selected"); } function focusCardElement(card: any, { preventScroll = true } = {}) { if (!card) return; try { card.focus?.({ preventScroll }); } catch { try { card.focus?.(); } catch (e) { console.debug?.(e); } } } function focusActiveCard(gridContainer: any, { force = false } = {}) { if (!gridContainer) return; if (!force && !gridContainer._mjrFocusWithin) return; const card = getActiveCardElement(gridContainer); if (!card) return; focusCardElement(card); gridContainer._mjrFocusWithin = true; } function ensureSelectionVisible(gridContainer: any) { if (!gridContainer) return; try { const selected = getActiveCardElement(gridContainer); if (!selected) return; const root = gridContainer.parentElement; if ( root && typeof root.getBoundingClientRect === "function" && typeof selected.getBoundingClientRect === "function" ) { const rootRect = root.getBoundingClientRect(); const selRect = selected.getBoundingClientRect(); const fullyVisible = selRect.top >= rootRect.top && selRect.bottom <= rootRect.bottom; if (fullyVisible) return; } selected.scrollIntoView?.({ block: "nearest", inline: "nearest" }); } catch (e) { console.debug?.(e); } } function scheduleEnsureSelectionVisible(gridContainer: any) { if (gridContainer) { try { gridContainer._mjrAutoRevealSelectionUntil = Date.now() + 900; } catch (e) { console.debug?.(e); } } const runOnce = () => { try { ensureSelectionVisible(gridContainer); } catch (e) { console.debug?.(e); } try { focusActiveCard(gridContainer); } catch (e) { console.debug?.(e); } }; try { requestAnimationFrame(() => { runOnce(); // Second pass after layout settles (sidebar open/close). setTimeout(() => { runOnce(); }, 50); }); } catch { runOnce(); } } function setCardSelected(card: any, selected: any) { if (!card) return; if (selected) { card.classList.add("is-selected"); card.setAttribute?.("aria-selected", "true"); } else { card.classList.remove("is-selected"); card.setAttribute?.("aria-selected", "false"); } } function getAllCards(gridContainer: any) { try { if (typeof gridContainer?._mjrGetRenderedCards === "function") { const cards = gridContainer._mjrGetRenderedCards(); if (Array.isArray(cards) && cards.length) return cards; } } catch { return []; } try { return Array.from(gridContainer.querySelectorAll(".mjr-asset-card")); } catch { return []; } } function updateSelectedIdsDataset(gridContainer: any) { if (!gridContainer) return; try { const ids = getAllCards(gridContainer) .filter((c) => c?.classList?.contains?.("is-selected")) .map((c) => c?.dataset?.mjrAssetId) .filter(Boolean); if (ids.length) { gridContainer.dataset.mjrSelectedAssetIds = JSON.stringify(ids); if (!gridContainer.dataset.mjrSelectedAssetId && ids[0]) { gridContainer.dataset.mjrSelectedAssetId = String(ids[0]); } } else { delete gridContainer.dataset.mjrSelectedAssetIds; delete gridContainer.dataset.mjrSelectedAssetId; } } catch (e) { console.debug?.(e); } } function applySelection(gridContainer: any, ids: any, activeId = "") { if (!gridContainer) return []; const list = Array.from(ids || []) .map(String) .filter(Boolean); try { if (typeof gridContainer?._mjrSetSelection === "function") { gridContainer._mjrSetSelection(list, activeId || list[0] || ""); return list; } } catch (e) { console.debug?.(e); } try { gridContainer.dataset.mjrSelectedAssetIds = list.length ? JSON.stringify(list) : ""; if (list.length) gridContainer.dataset.mjrSelectedAssetId = String(activeId || list[0] || ""); else { delete gridContainer.dataset.mjrSelectedAssetIds; delete gridContainer.dataset.mjrSelectedAssetId; } } catch (e) { console.debug?.(e); } try { for (const card of getAllCards(gridContainer)) { const id = (card as any)?.dataset?.mjrAssetId; const selected = id && list.includes(String(id)); setCardSelected(card, !!selected); } } catch (e) { console.debug?.(e); } try { gridContainer.dispatchEvent?.( new CustomEvent("mjr:selection-changed", { detail: { selectedIds: list, activeId: activeId || list[0] || "" }, }), ); } catch (e) { console.debug?.(e); } return list; } function syncSelectionState({ gridContainer, state, activeId, readState = null, writeState = null, }: Record = {}) { if ((!state && !writeState) || !gridContainer) return; try { let selected: any[] = []; if (gridContainer.dataset?.mjrSelectedAssetIds) { try { const parsed = JSON.parse(gridContainer.dataset.mjrSelectedAssetIds); if (Array.isArray(parsed)) selected = parsed.map(String).filter(Boolean); } catch (e) { console.debug?.(e); } } else if (gridContainer.dataset?.mjrSelectedAssetId) { selected = [String(gridContainer.dataset.mjrSelectedAssetId)].filter(Boolean); } else { selected = getAllCards(gridContainer) .filter((c) => c?.classList?.contains?.("is-selected")) .map((c) => c?.dataset?.mjrAssetId) .filter(Boolean) .map(String); } if (typeof writeState === "function") { writeState("selectedAssetIds", selected); } else { state.selectedAssetIds = selected; } const prevActiveId = typeof readState === "function" ? String(readState("activeAssetId", "") || "") : String(state?.activeAssetId || ""); if (activeId != null) { if (typeof writeState === "function") writeState("activeAssetId", String(activeId)); else state.activeAssetId = String(activeId); } else if (prevActiveId && selected.includes(prevActiveId)) { // keep existing active } else { if (typeof writeState === "function") writeState("activeAssetId", selected[0] || ""); else state.activeAssetId = selected[0] || ""; } } catch (e) { console.debug?.(e); } } export function bindSidebarOpen({ gridContainer, sidebar, createRatingBadge, createTagsBadge, showAssetInSidebar, closeSidebar, state, }: { gridContainer: any; sidebar: any; createRatingBadge: any; createTagsBadge: any; showAssetInSidebar: any; closeSidebar: any; state: any }) { const { read, write } = createPanelStateBridge(state, [ "activeAssetId", "selectedAssetIds", "sidebarOpen", ]); if (!gridContainer) return { dispose: () => {} }; if (gridContainer._mjrSidebarOpenBound) { return { dispose: gridContainer._mjrSidebarOpenDispose || (() => {}) }; } gridContainer._mjrSidebarOpenBound = true; const onGridFocusIn = (event: any) => { gridContainer._mjrFocusWithin = true; const card = safeClosest(event.target, ".mjr-asset-card"); if (card) { try { gridContainer._mjrLastFocusedCardId = (card as HTMLElement).dataset?.mjrAssetId || ""; } catch (e) { console.debug?.(e); } } }; const onGridFocusOut = (event: any) => { const related = event?.relatedTarget; if (related && gridContainer.contains(related)) return; gridContainer._mjrFocusWithin = false; }; try { gridContainer.addEventListener("focusin", onGridFocusIn); gridContainer.addEventListener("focusout", onGridFocusOut); } catch (e) { console.debug?.(e); } // Keep selection visible when the panel/grid resizes (column wrap changes can move the selected card). // The VirtualGrid repositions cards after a 100ms debounce, so we need multiple passes // to ensure we scroll after the grid has finished re-laying out. try { if (!gridContainer._mjrSelectionResizeObserver) { const scrollRoot = gridContainer.parentElement; let raf: any = null; let timer: any = null; const shouldAutoRevealSelection = () => { try { return Date.now() <= Number(gridContainer?._mjrAutoRevealSelectionUntil || 0); } catch { return false; } }; const schedule = () => { if (raf) cancelAnimationFrame(raf); if (timer) clearTimeout(timer); // First pass: immediate after layout raf = requestAnimationFrame(() => { raf = null; if (shouldAutoRevealSelection()) { ensureSelectionVisible(gridContainer); } }); // Second pass: after VirtualGrid debounce (100ms) + render timer = setTimeout(() => { timer = null; if (shouldAutoRevealSelection()) { ensureSelectionVisible(gridContainer); } }, 160); }; const ro = new ResizeObserver(() => schedule()); if (scrollRoot) ro.observe(scrollRoot); ro.observe(gridContainer); gridContainer._mjrSelectionResizeObserver = ro; } } catch (e) { console.debug?.(e); } const applyAssetUpdateToCard = (card: any, asset: any, updatedAsset: any) => { // Keep workflow/gen flags (and dot) in sync when backend self-heals metadata. try { const nextHasWorkflow = updatedAsset?.has_workflow ?? updatedAsset?.hasWorkflow ?? asset.has_workflow; const nextHasGen = updatedAsset?.has_generation_data ?? updatedAsset?.hasGenerationData ?? asset.has_generation_data; if (nextHasWorkflow !== undefined) asset.has_workflow = nextHasWorkflow; if (nextHasGen !== undefined) asset.has_generation_data = nextHasGen; const oldDot = card.querySelector(".mjr-workflow-dot"); const newDot = createWorkflowDot(asset); if (oldDot && newDot) oldDot.replaceWith(newDot); } catch (e) { console.debug?.(e); } if (updatedAsset?.rating !== undefined && updatedAsset.rating !== asset.rating) { asset.rating = updatedAsset.rating; const thumb = card.querySelector(".mjr-thumb"); const oldRatingBadge = card.querySelector(".mjr-rating-badge"); const newBadge = createRatingBadge(updatedAsset.rating || 0); if (oldRatingBadge && newBadge) { oldRatingBadge.replaceWith(newBadge); } else if (oldRatingBadge && !newBadge) { oldRatingBadge.remove(); } else if (!oldRatingBadge && newBadge && thumb) { thumb.appendChild(newBadge); } } if ( updatedAsset?.tags && JSON.stringify(updatedAsset.tags) !== JSON.stringify(asset.tags) ) { asset.tags = updatedAsset.tags; const oldTagsBadge = card.querySelector(".mjr-tags-badge"); if (oldTagsBadge) { const newBadge = createTagsBadge(updatedAsset.tags || []); oldTagsBadge.replaceWith(newBadge); } } }; const onClick = async (e: any) => { if (e.defaultPrevented) return; const card = safeClosest(e.target, ".mjr-asset-card"); if (!card) return; // Click on workflow dot triggers a forced re-index of this one file (no directory scan). const dot = safeClosest(e.target, ".mjr-workflow-dot"); if (dot && card.contains(dot)) { const asset = (card as any)._mjrAsset; if (!asset) return; e.preventDefault(); e.stopPropagation(); // Ensure this card is selected, but do not open the sidebar from dot clicks. const id = (card as any)?.dataset?.mjrAssetId || ""; applySelection(gridContainer, id ? [id] : [], id); syncSelectionState({ gridContainer, state, activeId: id, readState: read, writeState: write, }); const updated = await rescanSingleAsset({ card, asset, sidebar, onAssetUpdated: (u: any) => applyAssetUpdateToCard(card, asset, u), }); // Refresh sidebar only if it is already open on this asset. try { const curId = sidebar?._currentAsset?.id ?? sidebar?._currentFullAsset?.id ?? null; if ( updated && sidebar?.classList?.contains?.("is-open") && curId != null && String(curId) === String(updated.id) ) { await showAssetInSidebar(sidebar, updated, (u: any) => applyAssetUpdateToCard(card, asset, u), ); } } catch (e) { console.debug?.(e); } return; } const interactive = safeClosest(e.target, "a, button, input, textarea, select, label"); if (interactive && card.contains(interactive)) return; const asset = (card as any)._mjrAsset; if (!asset) return; if (gridContainer?._mjrSelectionManagedByVue) { const clickedId = String((card as any)?.dataset?.mjrAssetId || "").trim(); syncSelectionState({ gridContainer, state, activeId: clickedId, readState: read, writeState: write, }); try { if (sidebar?.classList?.contains?.("is-open")) { await showAssetInSidebar(sidebar, asset, (updatedAsset: any) => applyAssetUpdateToCard(card, asset, updatedAsset), ); try { write("sidebarOpen", true); } catch (e) { console.debug?.(e); } } } catch (e) { console.debug?.(e); } return; } e.preventDefault(); e.stopPropagation(); const isMulti = !!(e.ctrlKey || e.metaKey || e.shiftKey); if (!isMulti) { const alreadySelected = card.classList.contains("is-selected"); if (alreadySelected) { applySelection(gridContainer, [], ""); syncSelectionState({ gridContainer, state, activeId: "", readState: read, writeState: write, }); return; } const id = (card as any)?.dataset?.mjrAssetId; applySelection(gridContainer, id ? [id] : [], id || ""); syncSelectionState({ gridContainer, state, activeId: id || "", readState: read, writeState: write, }); // When the sidebar is already open, clicking a different card navigates details. try { if (sidebar?.classList?.contains?.("is-open")) { showAssetInSidebar(sidebar, asset, (updatedAsset: any) => applyAssetUpdateToCard(card, asset, updatedAsset), ); } } catch (e) { console.debug?.(e); } } else { const cards = getAllCards(gridContainer); const idx = cards.indexOf(card); if (e.shiftKey) { let allAssets: any[] = []; try { if (typeof gridContainer?._mjrGetAssets === "function") { allAssets = gridContainer._mjrGetAssets() || []; } } catch (e) { console.debug?.(e); } if (Array.isArray(allAssets) && allAssets.length) { const id = (card as any)?.dataset?.mjrAssetId || ""; const targetIndex = allAssets.findIndex( (a) => String(a?.id || "") === String(id), ); const last = Number(gridContainer._mjrLastSelectedIndex); const anchor = Number.isFinite(last) ? last : targetIndex; if (targetIndex >= 0 && anchor >= 0) { const start = Math.min(anchor, targetIndex); const end = Math.max(anchor, targetIndex); const rangeIds = allAssets .slice(start, end + 1) .map((a) => String(a?.id || "")) .filter(Boolean); applySelection(gridContainer, rangeIds, id || rangeIds[0] || ""); gridContainer._mjrLastSelectedIndex = targetIndex; syncSelectionState({ gridContainer, state, activeId: id || "", readState: read, writeState: write, }); return; } } } const clickedId = String((card as any)?.dataset?.mjrAssetId || "").trim(); if (idx >= 0) { gridContainer._mjrLastSelectedIndex = idx; } // Build selection from dataset first (VirtualGrid-safe), then fallback to DOM. const selectedSet = new Set(); try { const raw = gridContainer.dataset?.mjrSelectedAssetIds; if (raw) { const parsed = JSON.parse(raw); if (Array.isArray(parsed)) { for (const id of parsed) { const sid = String(id || "").trim(); if (sid) selectedSet.add(sid); } } } else { const single = String(gridContainer.dataset?.mjrSelectedAssetId || "").trim(); if (single) selectedSet.add(single); } } catch (e) { console.debug?.(e); } if (!selectedSet.size) { for (const c of cards) { const sid = String(c?.dataset?.mjrAssetId || "").trim(); if (!sid) continue; if (c.classList?.contains?.("is-selected")) selectedSet.add(sid); } } if (clickedId) { if (selectedSet.has(clickedId)) selectedSet.delete(clickedId); else selectedSet.add(clickedId); } const nextList = Array.from(selectedSet); const nextActiveId = selectedSet.has(clickedId) ? clickedId : nextList[0] || ""; // Use canonical selection setter so both grid + window selection events are fired. applySelection(gridContainer, nextList, nextActiveId); syncSelectionState({ gridContainer, state, activeId: nextActiveId, readState: read, writeState: write, }); return; } // Click only selects the asset (sidebar opens via hotkey/context menu). try { ensureSelectionVisible(gridContainer); (card as any)?.focus?.({ preventScroll: true }); } catch (e) { console.debug?.(e); } }; gridContainer.addEventListener("click", onClick); const openDetailsForSelection = async () => { const activeId = String(read("activeAssetId", "") || ""); let card: any = null; if (activeId) { try { card = gridContainer.querySelector( `.mjr-asset-card[data-mjr-asset-id="${escapeAttributeValue(activeId)}"]`, ); } catch { try { card = gridContainer.querySelector( `.mjr-asset-card[data-mjr-asset-id="${escapeAttributeValue(activeId)}"]`, ); } catch (e) { console.debug?.(e); } } } if (!card) { card = gridContainer.querySelector(".mjr-asset-card.is-selected"); } const asset = card?._mjrAsset; if (!asset) return; // Ensure the card keeps selection and focus when the sidebar opens // (needed when triggered from the global hotkey which bypasses the grid keydown handler) setSelectedCard(gridContainer, card); updateSelectedIdsDataset(gridContainer); syncSelectionState({ gridContainer, state, activeId: (card as any)?.dataset?.mjrAssetId, readState: read, writeState: write, }); try { const isOpen = !!sidebar?.classList?.contains?.("is-open"); const curId = sidebar?._currentAsset?.id ?? sidebar?._currentFullAsset?.id ?? null; if ( isOpen && curId != null && String(curId) === String(asset.id) && typeof closeSidebar === "function" ) { closeSidebar(sidebar); // Persist sidebar closed state try { write("sidebarOpen", false); } catch (e) { console.debug?.(e); } focusActiveCard(gridContainer, { force: true }); scheduleEnsureSelectionVisible(gridContainer); return; } } catch (e) { console.debug?.(e); } try { await showAssetInSidebar(sidebar, asset, (updatedAsset: any) => applyAssetUpdateToCard(card, asset, updatedAsset), ); // Persist sidebar open state try { write("sidebarOpen", true); } catch (e) { console.debug?.(e); } scheduleEnsureSelectionVisible(gridContainer); (card as any)?.focus?.({ preventScroll: true }); } catch (e) { console.debug?.(e); } }; const refreshActiveAsset = async () => { try { const isOpen = !!sidebar?.classList?.contains?.("is-open"); if (!isOpen) return false; } catch { return false; } const activeId = String(read("activeAssetId", "") || "").trim(); if (!activeId) return false; let card: any = null; try { card = queryCardById(gridContainer, activeId); } catch (e) { console.debug?.(e); } const asset = card?._mjrAsset; if (!asset) return false; try { await showAssetInSidebar(sidebar, asset, (updatedAsset: any) => applyAssetUpdateToCard(card, asset, updatedAsset), ); try { write("sidebarOpen", true); } catch (e) { console.debug?.(e); } return true; } catch (e) { console.debug?.(e); } return false; }; let selectionRefreshQueued = false; const onSelectionChanged = () => { try { if (!sidebar?.classList?.contains?.("is-open")) return; } catch { return; } if (selectionRefreshQueued) return; selectionRefreshQueued = true; queueMicrotask(async () => { selectionRefreshQueued = false; try { await refreshActiveAsset(); } catch (e) { console.debug?.(e); } }); }; gridContainer.addEventListener("mjr:selection-changed", onSelectionChanged); // Keyboard accessibility: toggle details from focused card. const onKeyDown = (e: any) => { if (e.defaultPrevented) return; const lower = e.key?.toLowerCase?.() || ""; if (lower !== "d") return; if (e.ctrlKey || e.metaKey || e.altKey) return; const isTypingTarget = e.target?.isContentEditable || e.target?.closest?.("input, textarea, select, [contenteditable='true']"); if (isTypingTarget) return; const card = safeClosest(e.target, ".mjr-asset-card"); if (!card) return; const asset = (card as any)._mjrAsset; if (!asset) return; e.preventDefault(); e.stopPropagation(); setSelectedCard(gridContainer, card); updateSelectedIdsDataset(gridContainer); syncSelectionState({ gridContainer, state, activeId: (card as any)?.dataset?.mjrAssetId, readState: read, writeState: write, }); openDetailsForSelection(); }; gridContainer.addEventListener("keydown", onKeyDown); // Listen for sidebar close events (e.g. from the close button in sidebar header) const onSidebarClosed = () => { try { write("sidebarOpen", false); } catch (e) { console.debug?.(e); } }; sidebar?.addEventListener?.("mjr:sidebar-closed", onSidebarClosed); try { gridContainer._mjrOpenDetails = openDetailsForSelection; } catch (e) { console.debug?.(e); } const dispose = () => { try { gridContainer.removeEventListener("click", onClick); } catch (e) { console.debug?.(e); } try { gridContainer.removeEventListener("focusin", onGridFocusIn); } catch (e) { console.debug?.(e); } try { gridContainer.removeEventListener("focusout", onGridFocusOut); } catch (e) { console.debug?.(e); } try { gridContainer.removeEventListener("keydown", onKeyDown); } catch (e) { console.debug?.(e); } try { gridContainer.removeEventListener("mjr:selection-changed", onSelectionChanged); } catch (e) { console.debug?.(e); } try { sidebar?.removeEventListener?.("mjr:sidebar-closed", onSidebarClosed); } catch (e) { console.debug?.(e); } try { if (gridContainer._mjrOpenDetails === openDetailsForSelection) { delete gridContainer._mjrOpenDetails; } } catch (e) { console.debug?.(e); } try { gridContainer._mjrSelectionResizeObserver?.disconnect?.(); } catch (e) { console.debug?.(e); } try { delete gridContainer._mjrSelectionResizeObserver; } catch (e) { console.debug?.(e); } try { delete gridContainer._mjrSidebarOpenBound; } catch (e) { console.debug?.(e); } try { delete gridContainer._mjrSidebarOpenDispose; } catch (e) { console.debug?.(e); } }; gridContainer._mjrSidebarOpenDispose = dispose; return { dispose, toggleDetails: openDetailsForSelection, refreshActiveAsset }; }