перетягивание внутри drap-and-drop блоков и поправлена настройка панели кнопок с блоками

This commit is contained in:
2026-08-20 12:56:49 +03:00
parent 72d89ec02b
commit 484c2363c9
3 changed files with 210 additions and 20 deletions

View File

@@ -122,6 +122,7 @@
hockeyTimerDirty: false,
hockeyTimerRevision: 0,
hockeyDragPlayer: null,
hockeyPlayerSelectionDrag: null,
hockeyDragInfraction: null,
hockeyDragPreset: null,
editorAuthTimer: null,
@@ -7648,6 +7649,7 @@ function openHockeyPrematchButtonsEditor() {
let dragButtonId = "";
let dragSelectorId = "";
let activeEditorGroupId = String(state.quickPanelActiveTab || "");
let activeEditorSection = "buttons";
if (activeEditorGroupId === "__ungrouped") activeEditorGroupId = "";
if (activeEditorGroupId && !draftGroups.some((group) => group.id === activeEditorGroupId)) activeEditorGroupId = draftGroups[0]?.id || "";
const collapsedEditorGroups = new Set();
@@ -7755,22 +7757,31 @@ function openHockeyPrematchButtonsEditor() {
draftPlayerPanels = normalizePlayerSelectionPanels(draftPlayerPanels);
showSettingsModal("Нижняя панель · кнопки", `
<div class="prematch-editor">
<div class="prematch-editor-head">
<p>Создавайте вкладки, кнопки и маленькие параметры рядом с кнопками. Значение параметра попадает в Mapping/SQL до запуска Shortcut Sequence.</p>
<div class="prematch-editor-head-actions">
<button type="button" class="btn" data-prematch-add-group>+ Вкладка</button>
<button type="button" class="btn" data-quick-selector-add>+ Переключатель</button>
<button type="button" class="btn btn-accent" data-prematch-add>+ Кнопка</button>
</div>
</div>
<div class="prematch-editor-list">
${renderGroup(null, -1)}
${draftGroups.map((group, index) => renderGroup(group, index)).join("")}
</div>
<section class="player-selection-editor">
<header><div><span>DRAG & DROP ИГРОКОВ</span><strong>Блоки выбора игроков</strong><small>Название ID автоматически формирует идентификаторы Mapping: player_select.&lt;ID&gt;.player1_id и т. д.</small></div><button type="button" class="btn" data-player-panel-add>+ Блок игроков</button></header>
<div class="player-selection-editor-list">${draftPlayerPanels.length ? draftPlayerPanels.map(renderPlayerPanelRow).join("") : `<div class="prematch-group-empty">Блоков игроков пока нет</div>`}</div>
</section>
<nav class="prematch-editor-tabs" aria-label="Раздел настроек нижней панели">
<button type="button" class="prematch-editor-tab ${activeEditorSection === "buttons" ? "active" : ""}" data-prematch-editor-tab="buttons">Кнопки</button>
<button type="button" class="prematch-editor-tab ${activeEditorSection === "players" ? "active" : ""}" data-prematch-editor-tab="players">Блоки игроков <span>${draftPlayerPanels.length}</span></button>
</nav>
${activeEditorSection === "buttons" ? `
<section class="prematch-editor-section" data-prematch-editor-section="buttons">
<div class="prematch-editor-head">
<p>Создавайте вкладки, кнопки и маленькие параметры рядом с кнопками. Значение параметра попадает в Mapping/SQL до запуска Shortcut Sequence.</p>
<div class="prematch-editor-head-actions">
<button type="button" class="btn" data-prematch-add-group>+ Вкладка</button>
<button type="button" class="btn" data-quick-selector-add>+ Переключатель</button>
<button type="button" class="btn btn-accent" data-prematch-add>+ Кнопка</button>
</div>
</div>
<div class="prematch-editor-list">
${renderGroup(null, -1)}
${draftGroups.map((group, index) => renderGroup(group, index)).join("")}
</div>
</section>
` : `
<section class="player-selection-editor prematch-editor-section" data-prematch-editor-section="players">
<header><div><span>DRAG & DROP ИГРОКОВ</span><strong>Блоки выбора игроков</strong><small>Создание сравнений, «3 звезды» и одиночных выборов. ID автоматически формирует Mapping: player_select.&lt;ID&gt;.player1_id и т. д.</small></div><button type="button" class="btn" data-player-panel-add>+ Блок игроков</button></header>
<div class="player-selection-editor-list">${draftPlayerPanels.length ? draftPlayerPanels.map(renderPlayerPanelRow).join("") : `<div class="prematch-group-empty">Блоков игроков пока нет</div>`}</div>
</section>
`}
<div class="prematch-editor-actions">
<button type="button" class="btn" data-prematch-cancel>Отмена</button>
<button type="button" class="btn btn-accent" data-prematch-save>Сохранить и опубликовать</button>
@@ -7778,6 +7789,13 @@ function openHockeyPrematchButtonsEditor() {
</div>
`, { className: "modal-prematch-full" });
document.querySelectorAll("[data-prematch-editor-tab]").forEach((button) => button.addEventListener("click", () => {
const section = String(button.dataset.prematchEditorTab || "buttons");
if (!["buttons", "players"].includes(section) || section === activeEditorSection) return;
activeEditorSection = section;
renderEditor();
}));
document.querySelector("[data-prematch-add-group]")?.addEventListener("click", () => {
if (draftGroups.length >= 24) return toast("Можно создать до 24 вкладок", true);
const newGroup = { id: `group_${draftGroups.length + 1}`, label: `Вкладка ${draftGroups.length + 1}`, sort_order: draftGroups.length * 10, enabled: true };
@@ -8363,7 +8381,7 @@ function hockeyPlayerSelectionTeamId(player) {
if (!player) return "";
const side = player.side === "away" ? "away" : player.side === "home" ? "home" : "";
const raw = player.raw || {};
const direct = raw.team_external_id || raw.team_id || raw.club_external_id || raw.club_id || raw.team?.external_id || raw.team?.id || "";
const direct = player.teamId || player.team_id || raw.team_external_id || raw.team_id || raw.club_external_id || raw.club_id || raw.team?.external_id || raw.team?.id || "";
if (direct) return String(direct);
if (!side) return "";
const team = getByPath(state.data, `hockey.selected_game.${side}`) || {};
@@ -8456,6 +8474,27 @@ async function hockeySetPlayerSelectionSlot(panel, slot, player) {
return true;
}
async function hockeySwapPlayerSelectionSlots(panel, fromSlot, toSlot) {
const sourceSlot = clamp(Number(fromSlot) || 1, 1, panel.slots);
const targetSlot = clamp(Number(toSlot) || 1, 1, panel.slots);
if (sourceSlot === targetSlot) return false;
const source = hockeyPlayerSelectionSlot(panel, sourceSlot);
if (!source.id && !source.dbId) return false;
const target = hockeyPlayerSelectionSlot(panel, targetSlot);
const sourcePlayer = { externalId: source.id, dbId: source.dbId, teamId: source.teamId, side: source.side, number: source.number, name: source.name };
const targetPlayer = (target.id || target.dbId)
? { externalId: target.id, dbId: target.dbId, teamId: target.teamId, side: target.side, number: target.number, name: target.name }
: null;
const patch = {
...hockeyPlayerSelectionPatch(panel, targetSlot, sourcePlayer),
...hockeyPlayerSelectionPatch(panel, sourceSlot, targetPlayer),
};
await hockeySetMatchValues(patch, { refreshMapping: true });
if (panel.sync_selected_player) await hockeySyncLegacySelectedPlayer(sourcePlayer, source.teamId);
toast(targetPlayer ? `${panel.label}: игроки ${sourceSlot} и ${targetSlot} поменяны местами` : `${panel.label}: игрок перемещён ${sourceSlot}${targetSlot}`);
return true;
}
async function hockeyClearPlayerSelectionSlot(panel, slot) {
await hockeySetMatchValues(hockeyPlayerSelectionPatch(panel, slot, null), { refreshMapping: true });
if (panel.sync_selected_player) await hockeySyncLegacySelectedPlayer(null, "");
@@ -8569,10 +8608,10 @@ function renderHockeyPlayerSelectionWindows() {
const sideLabel = slot.side === "home" ? "HOME" : slot.side === "away" ? "AWAY" : "";
const title = [slot.number ? `#${slot.number}` : "", slot.name].filter(Boolean).join(" ") || (filledSlot ? `Игрок ${number}` : "Перетащите игрока");
const ids = filledSlot ? [sideLabel, slot.id ? `ID ${slot.id}` : "", slot.dbId ? `DB ${slot.dbId}` : ""].filter(Boolean).join(" · ") : "из состава слева или справа";
return `<div class="hockey-player-select-slot ${filledSlot ? "is-filled" : ""}" data-player-panel-slot="${number}" data-player-panel-id="${escapeHtml(panel.id)}">
return `<div class="hockey-player-select-slot ${filledSlot ? "is-filled" : ""}" data-player-panel-slot="${number}" data-player-panel-id="${escapeHtml(panel.id)}" ${filledSlot ? `draggable="true" title="Перетащите на другой слот, чтобы поменять игроков местами"` : ""}>
<span class="hockey-player-select-index">${number}</span>
<div><strong>${escapeHtml(title)}</strong><small>${escapeHtml(ids)}</small></div>
${filledSlot ? `<button type="button" data-player-panel-clear-slot="${number}" data-player-panel-id="${escapeHtml(panel.id)}" title="Очистить слот">×</button>` : `<i>DROP</i>`}
${filledSlot ? `<span class="hockey-player-select-drag" aria-hidden="true">⋮⋮</span><button type="button" data-player-panel-clear-slot="${number}" data-player-panel-id="${escapeHtml(panel.id)}" title="Очистить слот">×</button>` : `<i>DROP</i>`}
</div>`;
}).join("")}
</div>
@@ -8584,7 +8623,29 @@ function renderHockeyPlayerSelectionWindows() {
renderRuntime();
});
node.querySelectorAll("[data-player-panel-slot]").forEach((slotNode) => {
slotNode.addEventListener("dragstart", (event) => {
const slot = Number(slotNode.dataset.playerPanelSlot || 1);
const current = hockeyPlayerSelectionSlot(panel, slot);
if (!current.id && !current.dbId) { event.preventDefault(); return; }
state.hockeyPlayerSelectionDrag = { panelId: panel.id, slot };
event.dataTransfer.effectAllowed = "move";
event.dataTransfer.setData("application/x-ui-builder-hockey-player-selection-slot", JSON.stringify(state.hockeyPlayerSelectionDrag));
event.dataTransfer.setData("text/plain", current.name || current.id || `player-${slot}`);
slotNode.classList.add("is-dragging");
});
slotNode.addEventListener("dragend", () => {
state.hockeyPlayerSelectionDrag = null;
slotNode.classList.remove("is-dragging");
node.querySelectorAll(".hockey-player-select-slot.is-over").forEach((item) => item.classList.remove("is-over"));
});
slotNode.addEventListener("dragover", (event) => {
const internal = state.hockeyPlayerSelectionDrag;
if (internal && internal.panelId === panel.id) {
event.preventDefault();
event.dataTransfer.dropEffect = "move";
slotNode.classList.add("is-over");
return;
}
const player = state.hockeyDragPlayer || readHockeyDragData(event, "player");
if (!player) return;
event.preventDefault();
@@ -8595,9 +8656,17 @@ function renderHockeyPlayerSelectionWindows() {
slotNode.addEventListener("drop", async (event) => {
event.preventDefault();
slotNode.classList.remove("is-over");
const targetSlot = Number(slotNode.dataset.playerPanelSlot || 1);
const internal = state.hockeyPlayerSelectionDrag;
if (internal && internal.panelId === panel.id) {
const sourceSlot = Number(internal.slot || 1);
state.hockeyPlayerSelectionDrag = null;
await hockeySwapPlayerSelectionSlots(panel, sourceSlot, targetSlot);
return;
}
const player = state.hockeyDragPlayer || readHockeyDragData(event, "player");
if (!player) return;
await hockeySetPlayerSelectionSlot(panel, Number(slotNode.dataset.playerPanelSlot || 1), player);
await hockeySetPlayerSelectionSlot(panel, targetSlot, player);
});
});
node.querySelectorAll("[data-player-panel-clear-slot]").forEach((button) => button.addEventListener("click", async (event) => {