BUILD153 — исправление выпадающих меню в настройках

This commit is contained in:
2026-09-18 21:12:49 +03:00
parent 5a20245f5e
commit 4f403284e3
3 changed files with 139139 additions and 246596 deletions

File diff suppressed because it is too large Load Diff

2
app.py
View File

@@ -30,7 +30,7 @@ from ui_builder import install_ui_builder
from khl_site.khl_data_center import APP as khl_site_app
BASE_DIR = Path(__file__).resolve().parent
BUILD_VERSION = "2026.09.18.10"
BUILD_VERSION = "2026.09.18.11"
# compatibility: BUILD_VERSION = "2026.08.26.1"
# compatibility: BUILD_VERSION = "2026.08.25.1"
# compatibility: BUILD_VERSION = "2026.08.24.15"

View File

@@ -1,3 +1,4 @@
// 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.
// BUILD151: Schedule ONLINE is a toggle; one-second live polling uses isolated schedule_live.
// BUILD150: Schedule ONLINE button refreshes score/status from language-specific per-game JSON.
@@ -2028,7 +2029,19 @@
closeControlPopover();
}, true);
document.addEventListener("keydown", (event) => { if (event.key === "Escape" && activeControlPopover) closeControlPopover(); }, true);
window.addEventListener("resize", closeControlPopover);
window.addEventListener("resize", () => {
if (!activeControlPopover) return;
// BUILD153: opening a styled dropdown inside full-screen settings can
// change the document scrollbar and produce a tiny resize event. The
// old handler immediately closed the dropdown, which made settings
// selects unusable. While settings are open, keep the popover alive
// and only recalculate its position.
if (hockeySettingsDataRefreshPaused() && activeControlPopover.anchor?.isConnected) {
positionControlPopover(activeControlPopover.anchor, activeControlPopover.popup);
return;
}
closeControlPopover();
});
window.addEventListener("scroll", () => activeControlPopover && positionControlPopover(activeControlPopover.anchor, activeControlPopover.popup), true);
}
@@ -2684,6 +2697,12 @@ function startCustomTooltips() {
}
async function loadData(showToast = false) {
// BUILD153: settings are a hard UI/data pause. Do not start a generic
// data reload while an operator is choosing values in a settings modal.
if (hockeySettingsDataRefreshPaused()) {
state.hockeySettingsReloadPending = true;
return clone(state.data);
}
try {
state.data = await api(`/data/${encodeURIComponent(state.config.data_source)}`);
state.dataPaths = flattenPaths(state.data);
@@ -5733,11 +5752,21 @@ function mergeExternalData(target, patch) {
function applyExternalDataPatch(patch, { render = true } = {}) {
state.data = mergeExternalData(state.data, patch || {});
state.dataPaths = flattenPaths(state.data);
if (el.rawData) el.rawData.textContent = JSON.stringify(state.data, null, 2);
renderDataPaths();
if (render) {
renderCanvas();
renderRuntime();
// BUILD153: data may still arrive from an already-started request while the
// operator is inside settings. Keep the newest values in memory, but do
// not mutate any DOM until settings close. Rebuilding runtime/editor DOM
// can otherwise destroy focus and close styled select popovers.
const settingsPaused = hockeySettingsDataRefreshPaused();
if (settingsPaused) {
if (render) state.hockeySettingsRenderPending = true;
} else {
if (el.rawData) el.rawData.textContent = JSON.stringify(state.data, null, 2);
renderDataPaths();
if (render) {
renderCanvas();
renderRuntime();
}
}
window.dispatchEvent(
new CustomEvent("ui-builder:data-patched", { detail: clone(patch || {}) })
@@ -13830,6 +13859,12 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
function renderRuntime() {
if (!el.runtimeStage || (!state.preview && boot.mode !== "runtime")) return;
// BUILD153: never rebuild the runtime DOM underneath a settings modal.
// Incoming data is retained and rendered once after the modal closes.
if (hockeySettingsDataRefreshPaused()) {
state.hockeySettingsRenderPending = true;
return;
}
const activeShootoutSearch = document.activeElement?.matches?.("[data-shootout-search]")
? {
side: document.activeElement.dataset.shootoutSearch,
@@ -15419,7 +15454,25 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
const paused = Boolean(event?.detail?.paused);
if (paused) {
stopQuickPanelOverlayPolling();
} else if (boot.mode === "runtime") {
return;
}
// BUILD153: flush all DOM work exactly once after settings disappear.
// This preserves focus/popovers for the whole editing session.
if (state.hockeySettingsRenderPending) {
state.hockeySettingsRenderPending = false;
if (el.rawData) el.rawData.textContent = JSON.stringify(state.data, null, 2);
renderDataPaths();
renderCanvas();
renderRuntime();
}
if (state.hockeySettingsReloadPending) {
state.hockeySettingsReloadPending = false;
loadData(false).catch((error) => console.warn("[Hockey] deferred data reload failed:", error));
}
if (boot.mode === "runtime") {
state.hockeySettingsGameRefreshPending = false;
startQuickPanelOverlayPolling();
refreshHockeyRuntimeAfterSettings();
}
@@ -15429,6 +15482,14 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
refreshHockeyRuntimeAfterSettings();
});
window.addEventListener("hockey:game-selected", (event) => {
// BUILD153: an in-flight Stat2TV request can finish after settings have
// already opened. Ignore its runtime side effects until the settings
// session ends; the fresh selected-game state will be reconciled on
// resume.
if (hockeySettingsDataRefreshPaused()) {
state.hockeySettingsGameRefreshPending = true;
return;
}
const gameId = String(event?.detail?.game?.external_id || event?.detail?.game?.id || "").trim();
if (!gameId) return;
// Live Stat2TV polling emits this event every second. Timer state must