1import { Controller } from "@hotwired/stimulus"; 2 3// Tournament Admin â Bracket Seed List 4// ============================================================= 5// Stimulus controller for the "Seed Teams" card on the 6// admin_bracket_viewer partial (brackets_page). 7// 8// Surfaces: 9// - Inline seed_number editing per team 10// - "Auto-seed by registration order" button (1..N in row order) 11// - "Clear seeds" button (sets all to nil) 12// 13// All writes go through teams_v2#update_team via PATCH. The URL template 14// has a literal `TEAM_ID` placeholder that we substitute per row â that 15// pattern is the one we use elsewhere in the V2 teams UI so the controller 16// stays Turbo-Frame-safe. 17// 18// Why a Stimulus controller (not inline <script>): inline scripts inside 19// the Turbo Frame on brackets_page survive the initial load but are NOT 20// re-executed when the frame re-renders after a division switch. Stimulus 21// auto-connects on every DOM insertion. 22 23export default class extends Controller { 24 static targets = ["list", "row", "seedInput", "status"]; 25 static values = {
26 updateUrlTemplate: String, 27 csrf: String, 28 divisionId: String, 29 }; 30 31 connect() { 32 // Initial render is server-rendered. Drag state lives here. 33 this.draggingId = null; 34 } 35 36 // ââ Drag and drop ââââââââââââââââââââââââââââââââââââââââââââââ 37 // HTML5 native DnD. On drop we (a) reorder the DOM, (b) re-assign 38 // 1..N seeds in the new visual order, (c) PATCH each team in 39 // sequence. No external library â keeps the controller small and 40 // Turbo-Frame-safe. 41 42 onDragStart(event) { 43 const row = event.target.closest("[data-team-id]"); 44 if (!row) return; 45 this.draggingId = row.dataset.teamId; 46 row.classList.add("is-dragging"); 47 if (event.dataTransfer) { 48 event.dataTransfer.effectAllowed = "move"; 49 // Some browsers require a non-empty dataTransfer payload for 50 // drag to fire properly. 51 event.dataTransfer.setData("text/plain", this.draggingId); 52 } 53 } 54 55 onDragEnd(event) { 56 const row = event.target.closest("[data-team-id]"); 57 if (row) row.classList.remove("is-dragging"); 58 this.draggingId = null; 59 // Clear any lingering drop-target highlights. 60 this.listTarget.querySelectorAll(".is-drop-target").forEach((el) => el.classList.remove("is-drop-target")); 61 } 62 63 onDragEnter(event) { 64 const row = event.target.closest("[data-team-id]"); 65 if (!row || row.dataset.teamId === this.draggingId) return; 66 row.classList.add("is-drop-target"); 67 } 68 69 onDragLeave(event) { 70 const row = event.target.closest("[data-team-id]"); 71 if (!row) return; 72 // The relatedTarget can be a child element of the same row during 73 // sub-element drag-enter/leave â only clear when truly leaving the 74 // row's bounding box. 75 if (event.relatedTarget && row.contains(event.relatedTarget)) return; 76 row.classList.remove("is-drop-target"); 77 } 78 79 onDragOver(event) { 80 // Required to allow drop. preventDefault enables the drop event. 81 event.preventDefault(); 82 if (event.dataTransfer) event.dataTransfer.dropEffect = "move"; 83 } 84 85 onDrop(event) { 86 event.preventDefault(); 87 if (!this.draggingId) return; 88 const draggingRow = this.listTarget.querySelector(`[data-team-id="${this.draggingId}"]`); 89 const targetRow = event.target.closest("[data-team-id]"); 90 if (!draggingRow || !targetRow || draggingRow === targetRow) { 91 this.#clearDropTargets(); 92 return; 93 } 94 95 // Insert above or below based on the cursor position within the 96 // target row. Above-half â insert before; below-half â insert after. 97 const rect = targetRow.getBoundingClientRect(); 98 const cursorY = event.clientY; 99 const isAbove = cursorY < rect.top + rect.height / 2; 100 if (isAbove) { 101 this.listTarget.insertBefore(draggingRow, targetRow); 102 } else { 103 this.listTarget.insertBefore(draggingRow, targetRow.nextSibling); 104 } 105 this.#clearDropTargets(); 106 107 // Re-seed every row 1..N based on the new visual order. 108 this.#reseedAllInOrder(); 109 } 110 111 #clearDropTargets() { 112 this.listTarget.querySelectorAll(".is-drop-target").forEach((el) => el.classList.remove("is-drop-target")); 113 } 114 115 // Assigns sequential seeds (1..N) by visual order and PATCHes each 116 // row whose seed actually changed. 117 #reseedAllInOrder() { 118 const rows = Array.from(this.listTarget.querySelectorAll("[data-team-id]")); 119 rows.forEach((row, idx) => { 120 const teamId = row.dataset.teamId; 121 const seed = idx + 1; 122 const input = row.querySelector("[data-bracket-seed-list-target='seedInput']"); 123 const currentVal = input ? parseInt(input.value, 10) : NaN; 124 if (input) input.value = seed; 125 if (currentVal !== seed) { 126 this.#patchSeed(teamId, seed, row); 127 } 128 }); 129 } 130 131 // Called when a seed input changes (or blurs). 132 onSeedChange(event) { 133 const input = event.target; 134 const row = input.closest("[data-team-id]"); 135 if (!row) return; 136 const teamId = row.dataset.teamId; 137 const raw = input.value.trim(); 138 const seedNumber = raw === "" ? null : parseInt(raw, 10); 139 if (raw !== "" && (Number.isNaN(seedNumber) || seedNumber < 1)) { 140 this.#setStatus(row, "error", "Invalid"); 141 return; 142 } 143 this.#patchSeed(teamId, seedNumber, row); 144 } 145 146 // Set seeds 1..N in current row order. 147 autoSeedByCreatedAt(event) { 148 event.preventDefault(); 149 const rows = Array.from(this.listTarget.querySelectorAll("[data-team-id]")); 150 rows.forEach((row, idx) => { 151 const teamId = row.dataset.teamId; 152 const seed = idx + 1; 153 const input = row.querySelector("[data-bracket-seed-list-target='seedInput']"); 154 if (input) input.value = seed; 155 this.#patchSeed(teamId, seed, row); 156 }); 157 } 158 159 // Clear every team's seed (set to null). 160 clearSeeds(event) { 161 event.preventDefault(); 162 if (!window.confirm("Clear seed numbers for every team in this division?")) return; 163 const rows = Array.from(this.listTarget.querySelectorAll("[data-team-id]")); 164 rows.forEach((row) => { 165 const teamId = row.dataset.teamId; 166 const input = row.querySelector("[data-bracket-seed-list-target='seedInput']"); 167 if (input) input.value = ""; 168 this.#patchSeed(teamId, null, row); 169 }); 170 } 171 172 // ââ private ââââââââââââââââââââââââââââââââââââââââââââââââââââ 173 174 #patchSeed(teamId, seedNumber, row) { 175 const url = this.updateUrlTemplateValue.replace("TEAM_ID", teamId); 176 this.#setStatus(row, "saving", "Savingâ¦"); 177 fetch(url, { 178 method: "PATCH", 179 headers: { 180 "Content-Type": "application/json", 181 Accept: "application/json", 182 "X-CSRF-Token": this.csrfValue, 183 }, 184 body: JSON.stringify({ seed_number: seedNumber }), 185 }) 186 .then((r) => r.json().then((data) => ({ ok: r.ok, data }))) 187 .then(({ ok, data }) => { 188 if (ok) { 189 this.#setStatus(row, "saved", "Saved"); 190 setTimeout(() => this.#setStatus(row, "", ""), 900); 191 } else { 192 this.#setStatus(row, "error", data?.error || "Failed"); 193 } 194 }) 195 .catch(() => this.#setStatus(row, "error", "Network")); 196 } 197 198 #setStatus(row, kind, text) { 199 const status = row.querySelector("[data-bracket-seed-list-target='status']"); 200 if (!status) return; 201 status.textContent = text; 202 status.className = "tap-seed-row__status"; 203 if (kind) status.classList.add(`tap-seed-row__status--${kind}`); 204 } 205};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.