настройки Mapping

This commit is contained in:
2026-09-24 15:37:13 +03:00
parent 7c85d83627
commit b59434abaf

View File

@@ -134,6 +134,10 @@
shortcutInventory: { device_id: "", device_name: "", online: false, vmix_connected: false, inventory: { inputs: [] }, devices: [] },
shortcutInventoryLoading: false,
shortcutEditorScrollTop: 0,
// BUILD162: an async vMix inventory response must never rewrite Mapping
// controls after the operator has started editing the settings modal.
shortcutEditorInteractionRevision: 0,
shortcutInventoryRefreshDeferred: false,
preparedTitles: [],
preparedTitleInventory: { device_id: "", device_name: "", inventory: { inputs: [] } },
preparedTitlesLoading: false,
@@ -1331,6 +1335,15 @@
popup.remove();
anchor?.classList.remove("control-popover-open");
activeControlPopover = null;
// BUILD162: if vMix inventory arrived while Mapping was being edited,
// apply it only after the open popover is gone. This keeps the current
// dropdown usable without leaving the inventory permanently stale.
const editor = document.querySelector(".shortcut-sequence-editor");
if (editor?.isConnected && state.shortcutInventoryRefreshDeferred && !shortcutEditorHasActiveInteraction(editor)) {
state.shortcutInventoryRefreshDeferred = false;
refreshShortcutEditorInventoryControls(editor, { force: true });
}
}
function positionControlPopover(anchor, popup) {
@@ -15242,8 +15255,17 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
if (!target) return;
target[field] = control.type === "checkbox" ? control.checked : control.value;
if (field === "input") {
// BUILD162: changing the Mapping Input must not rebuild the whole
// shortcut card. A full rerender destroys the styled popover,
// closes the menu and makes the operator lose the current context.
target.selected_name = "";
rerender();
const row = control.closest("[data-penalty-target-row]");
const selectedName = row?.querySelector('[data-penalty-target-field="selected_name"]');
if (selectedName) {
selectedName.innerHTML = vmixTextSelectedNameOptions(target.input, "");
selectedName.value = "";
refreshEnhancedControl(selectedName);
}
}
});
});
@@ -15442,8 +15464,26 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
return true;
}
function refreshShortcutEditorInventoryControls(editor) {
function markShortcutEditorInteraction() {
state.shortcutEditorInteractionRevision += 1;
}
function shortcutEditorHasActiveInteraction(editor) {
if (!editor?.isConnected) return false;
if (activeControlPopover?.anchor?.isConnected && editor.contains(activeControlPopover.anchor)) return true;
const active = document.activeElement;
if (!active || !editor.contains(active)) return false;
// A focused text field can safely coexist with a vMix inventory refresh;
// only a native select/popover interaction is sensitive to option rebuilds.
return Boolean(active.matches?.("select, .css-select-trigger"));
}
function refreshShortcutEditorInventoryControls(editor, { force = false } = {}) {
if (!editor?.isConnected) return;
if (!force && shortcutEditorHasActiveInteraction(editor)) {
state.shortcutInventoryRefreshDeferred = true;
return;
}
const status = editor.querySelector("[data-vmix-inventory-status]");
if (status) status.textContent = shortcutInventoryLabel();
@@ -15542,6 +15582,24 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
const modalCard = editor?.closest(".modal-card");
const searchInput = editor.querySelector("[data-shortcut-search]");
// BUILD162: remember that the operator has started interacting with Mapping.
// A late vMix inventory response may update state, but it must not rewrite
// the controls while the operator is choosing an Input/Text/Overlay.
editor.addEventListener("pointerdown", (event) => {
if (event.target?.closest?.("input, textarea, select, button, .css-select-trigger")) {
markShortcutEditorInteraction();
}
}, true);
editor.addEventListener("focusin", (event) => {
if (event.target?.matches?.("input, textarea, select, button")) markShortcutEditorInteraction();
}, true);
editor.addEventListener("input", (event) => {
if (event.target?.matches?.("input, textarea, select")) markShortcutEditorInteraction();
}, true);
editor.addEventListener("change", (event) => {
if (event.target?.matches?.("input, textarea, select")) markShortcutEditorInteraction();
}, true);
const render = ({ focusId = "" } = {}) => {
const previousScroll = modalCard?.scrollTop ?? state.shortcutEditorScrollTop ?? 0;
const list = editor.querySelector(".shortcut-sequence-list");
@@ -15604,16 +15662,28 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
});
editor.querySelector("[data-open-shortcut-reference]").addEventListener("click", showShortcutsReference);
editor.querySelector("[data-refresh-vmix-inventory]").addEventListener("click", async () => {
// Explicit refresh is an operator action, so it is allowed to rebuild
// the inventory after the request. The active control itself is still
// protected by refreshShortcutInventorySelect().
await loadShortcutInventory();
refreshShortcutEditorInventoryControls(editor);
state.shortcutInventoryRefreshDeferred = false;
refreshShortcutEditorInventoryControls(editor, { force: true });
});
editor.querySelector("[data-save-shortcuts]").addEventListener("click", () => saveConfig());
editor.querySelector("[data-close-shortcuts]").addEventListener("click", closeModal);
render();
const inventoryInteractionRevision = state.shortcutEditorInteractionRevision;
loadShortcutInventory({ silent: true }).then(() => {
// BUILD160: do not replace the whole settings DOM after the async vMix
// inventory request completes. Replacing it can close an already-open
// dropdown and visually restore defaults while the operator is editing.
// BUILD160/162: inventory may arrive after the modal is already in use.
// Never rewrite Mapping controls if the operator has interacted since
// the request started. The current DOM/state remains authoritative for
// this editing session; the next opening gets a fresh inventory.
if (!editor.isConnected) return;
if (state.shortcutEditorInteractionRevision !== inventoryInteractionRevision || shortcutEditorHasActiveInteraction(editor)) {
state.shortcutInventoryRefreshDeferred = true;
return;
}
state.shortcutInventoryRefreshDeferred = false;
refreshShortcutEditorInventoryControls(editor);
});
}