diff --git a/ui_builder/static/app.js b/ui_builder/static/app.js index 00baf44..7b85fe6 100644 --- a/ui_builder/static/app.js +++ b/ui_builder/static/app.js @@ -1,5 +1,6 @@ // BUILD159_COUNTDOWN_TRANSITION_FIXES // BUILD155: all hockey game/penalty timers use native vMix Countdown; no per-second SetText mirroring. +// BUILD161: live data refresh cannot rebuild an open infraction picker on the main Game tab. // BUILD160: strict settings freeze; background vMix inventory refresh never rebuilds an open settings form. // BUILD153: settings freeze all data-driven DOM rendering and keep dropdown popovers open. // BUILD152: all browser-side hockey data polling pauses while operator settings are open. @@ -161,6 +162,7 @@ timerEngineStarted: false, hockeyPenaltyBoards: {}, hockeyPenaltyBoardNodes: new Map(), + hockeyInfractionMenuRenderPending: false, hockeyGameControl: {}, hockeyGameControlLoading: new Set(), hockeyTimerGameId: "", @@ -7432,6 +7434,7 @@ function openTimerQuickEditor(focusActionId = "") { history: [], historyOpen: false, search: { home: "", away: "", infractions: "" }, + infractionDropdownOpen: false, lastPersistAt: 0 }; @@ -8074,6 +8077,19 @@ function openTimerQuickEditor(focusActionId = "") { function refreshHockeyBoardNodes(component) { const nodes = state.hockeyPenaltyBoardNodes.get(component.action_id) || []; + const board = ensureHockeyBoardState(component); + const menuOpen = Boolean(board?.infractionDropdownOpen) + || [...nodes].some((node) => Boolean(node?.querySelector?.(".hpd-infraction-dropdown[open]"))); + + // BUILD161: do not rebuild the penalty dashboard under an open infraction menu. + // Live match polling and penalty-state refreshes can still update state/tickers, + // but the operator's dropdown/search stays physically intact until it is closed. + if (menuOpen) { + state.hockeyInfractionMenuRenderPending = true; + updateHockeyBoardTickerNodes(component); + return; + } + nodes.forEach((node) => renderHockeyPenaltyDashboard(node, component, true)); } @@ -8362,7 +8378,15 @@ function hockeyInfractionRow(component, infraction, runtime) { `; if (runtime) { - row.addEventListener("click", () => chooseHockeyInfraction(component, infraction)); + row.addEventListener("click", () => { + // A deliberate choice may close the menu; only background refreshes are frozen. + const dropdown = row.closest(".hpd-infraction-dropdown"); + if (dropdown) { + board.infractionDropdownOpen = false; + dropdown.open = false; + } + chooseHockeyInfraction(component, infraction); + }); row.addEventListener("dragstart", (dragEvent) => { setHockeyDragData(dragEvent, "infraction", infraction); row.classList.add("is-dragging"); @@ -8406,6 +8430,7 @@ function hockeyInfractionDropdown(component, runtime) { const board = ensureHockeyBoardState(component); const details = document.createElement("details"); details.className = "hpd-infraction-dropdown"; + details.open = Boolean(board.infractionDropdownOpen); const summary = document.createElement("summary"); summary.innerHTML = ` @@ -8444,7 +8469,19 @@ function hockeyInfractionDropdown(component, runtime) { }); details.addEventListener("toggle", () => { - if (details.open) requestAnimationFrame(() => search.focus()); + board.infractionDropdownOpen = details.open; + if (details.open) { + requestAnimationFrame(() => search.focus({ preventScroll: true })); + return; + } + + // Apply the freshest match data only after the operator has finished working + // with the menu. This avoids the 1-second online refresh closing the dropdown + // or resetting the search cursor midway through selection. + if (state.hockeyInfractionMenuRenderPending) { + state.hockeyInfractionMenuRenderPending = false; + requestAnimationFrame(() => renderRuntime()); + } }); return details; } @@ -14257,6 +14294,13 @@ function renderHockeyPenaltyDashboard(node, component, runtime) { state.hockeySettingsRenderPending = true; return; } + // BUILD161: while the infraction picker is open on the main Game tab, keep the + // current DOM untouched. Online match polling may continue in the background; + // the newest data is rendered immediately after the picker is closed. + if (el.runtimeStage.querySelector?.(".hpd-infraction-dropdown[open]")) { + state.hockeyInfractionMenuRenderPending = true; + return; + } // Keep manual text editing stable while background match-data refreshes arrive. if (state.activeTab === "custom_pluses" && document.activeElement?.closest?.(".hcp-workspace")