@amirhossein-shk/tournament-bracket-js 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,117 @@
1
+ @import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap");
2
+ body {
3
+ background-color: #1E1E1E;
4
+ }
5
+
6
+ #tournament-bracket {
7
+ overflow: auto;
8
+ }
9
+
10
+ .tournament-bracket-container {
11
+ position: relative;
12
+ }
13
+
14
+ .tournament-bracket-connectors {
15
+ position: absolute;
16
+ inset: 0;
17
+ z-index: 0;
18
+ }
19
+
20
+ .tournament-bracket-column {
21
+ position: absolute;
22
+ z-index: 1;
23
+ }
24
+
25
+ .match-container {
26
+ position: absolute;
27
+ z-index: 1;
28
+ }
29
+
30
+ .tournament-bracket-connector {
31
+ transition: stroke 0.2s ease;
32
+ }
33
+
34
+ .tournament-bracket-container {
35
+ display: flex;
36
+ gap: 24px;
37
+ }
38
+ .tournament-bracket-container .tournament-bracket-column {
39
+ display: flex;
40
+ flex-direction: column;
41
+ align-items: center;
42
+ gap: 40px;
43
+ justify-content: space-around;
44
+ }
45
+ .tournament-bracket-container .tournament-bracket-column .match-container {
46
+ position: relative;
47
+ border-radius: 10px;
48
+ }
49
+ .tournament-bracket-container .tournament-bracket-column .match-container .player-vs {
50
+ position: absolute;
51
+ top: calc(50% + 10px);
52
+ left: 0;
53
+ right: 0;
54
+ transform: translateY(-50%);
55
+ margin: 0 auto;
56
+ width: 20px;
57
+ height: 20px;
58
+ display: flex;
59
+ align-items: center;
60
+ justify-content: center;
61
+ border-radius: 50%;
62
+ border: 1px solid #FF6E27;
63
+ background-color: #464646;
64
+ color: #F8F9FA;
65
+ font-family: Roboto;
66
+ font-size: 12px;
67
+ font-style: normal;
68
+ font-weight: 700;
69
+ line-height: normal;
70
+ z-index: 1;
71
+ }
72
+ .tournament-bracket-container .tournament-bracket-column .match-container .tournament-bracket-player {
73
+ display: flex;
74
+ align-items: center;
75
+ justify-content: space-between;
76
+ height: 47px;
77
+ padding: 0 16px;
78
+ background-color: #464646;
79
+ color: #949494;
80
+ font-family: Roboto;
81
+ font-size: 14px;
82
+ font-style: normal;
83
+ font-weight: 600;
84
+ line-height: normal;
85
+ box-sizing: border-box;
86
+ }
87
+ .tournament-bracket-container .tournament-bracket-column .match-container .tournament-bracket-player .avatar-and-name-container {
88
+ display: flex;
89
+ gap: 8px;
90
+ align-items: center;
91
+ }
92
+ .tournament-bracket-container .tournament-bracket-column .match-container .tournament-bracket-player.player-winner {
93
+ background-color: #111111;
94
+ }
95
+ .tournament-bracket-container .tournament-bracket-column .match-container .tournament-bracket-player .avatar {
96
+ width: 32px;
97
+ height: 32px;
98
+ border-radius: 100%;
99
+ overflow: hidden;
100
+ }
101
+ .tournament-bracket-container .tournament-bracket-column .match-container .tournament-bracket-player .name {
102
+ max-width: 92px;
103
+ overflow: hidden;
104
+ text-overflow: ellipsis;
105
+ white-space: nowrap;
106
+ }
107
+ .tournament-bracket-container .tournament-bracket-column .match-container .tournament-bracket-player .score {
108
+ font-family: Roboto;
109
+ padding: 0 12px;
110
+ font-size: 20px;
111
+ font-style: normal;
112
+ font-weight: 500;
113
+ line-height: normal;
114
+ color: #F8F9FA;
115
+ }
116
+
117
+ /*# sourceMappingURL=tournament-bracket.css.map */
@@ -0,0 +1 @@
1
+ {"version":3,"sourceRoot":"","sources":["../src/tournament-bracket.scss"],"names":[],"mappings":"AAAQ;AAER;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;;AAEA;EACE;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;;AA8BA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACE;EACA;EACA;;AAGF;EACE;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA","file":"tournament-bracket.css"}
@@ -0,0 +1,445 @@
1
+ //#region src/tournament-bracket.js
2
+ function tournamentBracket(userConfig = {}) {
3
+ const DEFAULT_PLAYER = {
4
+ name: "-",
5
+ avatarUrl: "",
6
+ score: "-"
7
+ };
8
+ const DEFAULT_CONFIG = {
9
+ targetId: "tournament-bracket",
10
+ rounds: null,
11
+ distance: 42,
12
+ width: 196,
13
+ matchHeight: 94,
14
+ roundGap: 96,
15
+ avatarFallbackUrl: "data:image/svg+xml;utf8," + encodeURIComponent(`
16
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64">
17
+ <rect width="100%" height="100%" fill="#e5e7eb"/>
18
+ <text x="50%" y="54%" text-anchor="middle" font-size="28" fill="#6b7280">?</text>
19
+ </svg>
20
+ `),
21
+ connectorColor: "white",
22
+ onMatchClick: null,
23
+ onMatchUpdate: null,
24
+ onMatchFinish: null
25
+ };
26
+ function normalizeConfig(userConfig) {
27
+ return {
28
+ ...DEFAULT_CONFIG,
29
+ ...userConfig,
30
+ rounds: userConfig.rounds ?? []
31
+ };
32
+ }
33
+ const config = normalizeConfig(userConfig);
34
+ let bracketState = null;
35
+ function validateRoundsShape(rounds) {
36
+ if (!Array.isArray(rounds) || rounds.length === 0) throw new Error("rounds must be a non-empty array.");
37
+ rounds.forEach((round, roundIndex) => {
38
+ if (!round || typeof round !== "object") throw new Error(`Round at index ${roundIndex} must be an object.`);
39
+ if (!Array.isArray(round.matches)) throw new Error(`rounds[${roundIndex}].matches must be an array.`);
40
+ if (round.matches.length === 0) throw new Error(`rounds[${roundIndex}].matches must not be empty.`);
41
+ round.matches.forEach((match, matchIndex) => {
42
+ if (!match || typeof match !== "object") throw new Error(`rounds[${roundIndex}].matches[${matchIndex}] must be an object.`);
43
+ if (!Array.isArray(match.players)) throw new Error(`rounds[${roundIndex}].matches[${matchIndex}].players must be an array.`);
44
+ if (match.players.length !== 2) throw new Error(`rounds[${roundIndex}].matches[${matchIndex}] must have exactly 2 players.`);
45
+ });
46
+ });
47
+ const firstRoundMatchesCount = rounds[0].matches.length;
48
+ if (!isPowerOfTwo(firstRoundMatchesCount)) throw new Error("First round matches count must be a power of two.");
49
+ for (let i = 1; i < rounds.length; i++) {
50
+ const expectedMatchesCount = rounds[i - 1].matches.length / 2;
51
+ const actualMatchesCount = rounds[i].matches.length;
52
+ if (actualMatchesCount !== expectedMatchesCount) throw new Error(`Invalid bracket shape: rounds[${i}] must have ${expectedMatchesCount} matches, but got ${actualMatchesCount}.`);
53
+ }
54
+ if (rounds[rounds.length - 1].matches.length !== 1) throw new Error("Last round must have exactly 1 match.");
55
+ }
56
+ function validateConfig() {
57
+ const container = document.getElementById(config.targetId);
58
+ if (!container) throw new Error(`Element with id "${config.targetId}" not found`);
59
+ validateRoundsShape(config.rounds);
60
+ return container;
61
+ }
62
+ function isPowerOfTwo(number) {
63
+ return number > 0 && (number & number - 1) === 0;
64
+ }
65
+ function createId(prefix) {
66
+ if (window.crypto?.randomUUID) return `${prefix}-${window.crypto.randomUUID()}`;
67
+ return `${prefix}-${Math.random().toString(36).slice(2, 9)}`;
68
+ }
69
+ function createEmptyPlayer() {
70
+ return { ...DEFAULT_PLAYER };
71
+ }
72
+ function normalizeMatch(match) {
73
+ const isFinished = typeof match.isFinished === "boolean" ? match.isFinished : match.status === "completed";
74
+ return {
75
+ matchId: match.matchId || createId("match"),
76
+ status: match.status || (isFinished ? "completed" : "pending"),
77
+ isFinished,
78
+ players: [{
79
+ ...DEFAULT_PLAYER,
80
+ ...match.players?.[0]
81
+ }, {
82
+ ...DEFAULT_PLAYER,
83
+ ...match.players?.[1]
84
+ }]
85
+ };
86
+ }
87
+ function mergeMatchData(currentMatch, nextMatchData) {
88
+ const nextPlayers = nextMatchData.players || [];
89
+ return normalizeMatch({
90
+ ...currentMatch,
91
+ ...nextMatchData,
92
+ players: [{
93
+ ...currentMatch.players?.[0],
94
+ ...nextPlayers[0]
95
+ }, {
96
+ ...currentMatch.players?.[1],
97
+ ...nextPlayers[1]
98
+ }]
99
+ });
100
+ }
101
+ function buildBracketModel(rounds) {
102
+ return { rounds: rounds.map((round, roundIndex) => {
103
+ const matches = round.matches.map((match, matchIndex) => {
104
+ const normalized = normalizeMatch(match);
105
+ normalized.roundIndex = roundIndex;
106
+ normalized.matchIndex = matchIndex;
107
+ return normalized;
108
+ });
109
+ return {
110
+ roundId: round.roundId || createId("round"),
111
+ name: round.name || `Round ${roundIndex + 1}`,
112
+ roundIndex,
113
+ matches
114
+ };
115
+ }) };
116
+ }
117
+ function isPendingScore(score) {
118
+ return score === "-" || score === "" || score === null || score === void 0;
119
+ }
120
+ function getMatchState(match) {
121
+ const [firstPlayer, secondPlayer] = match.players;
122
+ const firstScorePending = isPendingScore(firstPlayer.score);
123
+ const secondScorePending = isPendingScore(secondPlayer.score);
124
+ if (firstScorePending && secondScorePending) return {
125
+ status: "pending",
126
+ winnerIndexes: []
127
+ };
128
+ const firstScore = Number(firstPlayer.score);
129
+ const secondScore = Number(secondPlayer.score);
130
+ if (Number.isNaN(firstScore) || Number.isNaN(secondScore)) return {
131
+ status: "invalid-score",
132
+ winnerIndexes: []
133
+ };
134
+ if (firstScore === secondScore) return {
135
+ status: "draw",
136
+ winnerIndexes: []
137
+ };
138
+ if (!match.isFinished) return {
139
+ status: "in-progress",
140
+ winnerIndexes: []
141
+ };
142
+ return {
143
+ status: "completed",
144
+ winnerIndexes: [firstScore > secondScore ? 0 : 1]
145
+ };
146
+ }
147
+ function getPlayerIdentity(player = {}) {
148
+ return player.id || player.name || "";
149
+ }
150
+ function createAdvancedPlayer(winner, currentTargetPlayer = {}, previousMatchId) {
151
+ const winnerIdentity = getPlayerIdentity(winner);
152
+ const currentIdentity = getPlayerIdentity(currentTargetPlayer);
153
+ const isSamePlayer = winnerIdentity && currentIdentity && winnerIdentity === currentIdentity;
154
+ return {
155
+ name: winner.name ?? "",
156
+ avatarUrl: winner.avatarUrl ?? "",
157
+ seed: winner.seed,
158
+ id: winner.id,
159
+ score: isSamePlayer ? currentTargetPlayer.score ?? "-" : "-",
160
+ previousMatchId
161
+ };
162
+ }
163
+ function propagateWinner(roundIndex, matchIndex) {
164
+ const match = bracketState.rounds[roundIndex]?.matches?.[matchIndex];
165
+ if (!match) return;
166
+ const matchState = getMatchState(match);
167
+ const nextRound = bracketState.rounds[roundIndex + 1];
168
+ if (!nextRound) return;
169
+ const nextMatchIndex = Math.floor(matchIndex / 2);
170
+ const nextMatch = nextRound.matches[nextMatchIndex];
171
+ if (!nextMatch) return;
172
+ const playerIndex = matchIndex % 2;
173
+ const winner = matchState.status === "completed" ? match.players[matchState.winnerIndexes[0]] : null;
174
+ if (!winner) nextMatch.players[playerIndex] = createEmptyPlayer();
175
+ else nextMatch.players[playerIndex] = createAdvancedPlayer(winner, nextMatch.players[playerIndex], match.matchId);
176
+ }
177
+ function resolveBracket() {
178
+ for (let roundIndex = 0; roundIndex < bracketState.rounds.length - 1; roundIndex++) bracketState.rounds[roundIndex].matches.forEach((match, matchIndex) => {
179
+ propagateWinner(roundIndex, matchIndex);
180
+ });
181
+ }
182
+ function createBracketLayout(bracket) {
183
+ const roundWidth = config.width;
184
+ const roundGap = config.roundGap;
185
+ const matchHeight = config.matchHeight;
186
+ const matchGap = config.distance;
187
+ const rounds = bracket.rounds.map((round, roundIndex) => {
188
+ const x = roundIndex * (roundWidth + roundGap);
189
+ return {
190
+ ...round,
191
+ x,
192
+ matches: round.matches.map((match, matchIndex) => ({
193
+ ...match,
194
+ x,
195
+ y: matchIndex * (matchHeight + matchGap),
196
+ width: roundWidth,
197
+ height: matchHeight
198
+ }))
199
+ };
200
+ });
201
+ for (let roundIndex = 1; roundIndex < rounds.length; roundIndex++) {
202
+ const previousRound = rounds[roundIndex - 1];
203
+ const currentRound = rounds[roundIndex];
204
+ currentRound.matches = currentRound.matches.map((match, matchIndex) => {
205
+ const firstSourceMatch = previousRound.matches[matchIndex * 2];
206
+ const secondSourceMatch = previousRound.matches[matchIndex * 2 + 1];
207
+ const y = (getMatchCenterY(firstSourceMatch) + getMatchCenterY(secondSourceMatch)) / 2 - matchHeight / 2;
208
+ return {
209
+ ...match,
210
+ y
211
+ };
212
+ });
213
+ }
214
+ return {
215
+ rounds,
216
+ connectors: createConnectors(rounds),
217
+ width: calculateLayoutWidth(rounds),
218
+ height: calculateLayoutHeight(rounds)
219
+ };
220
+ }
221
+ function getMatchCenterY(match) {
222
+ return match.y + match.height / 2;
223
+ }
224
+ function createConnectors(rounds) {
225
+ const connectors = [];
226
+ for (let roundIndex = 0; roundIndex < rounds.length - 1; roundIndex++) {
227
+ const currentRound = rounds[roundIndex];
228
+ const nextRound = rounds[roundIndex + 1];
229
+ currentRound.matches.forEach((match, matchIndex) => {
230
+ const targetMatch = nextRound.matches[Math.floor(matchIndex / 2)];
231
+ if (!targetMatch) return;
232
+ connectors.push({
233
+ id: createId("connector"),
234
+ from: {
235
+ x: match.x + match.width,
236
+ y: getMatchCenterY(match)
237
+ },
238
+ to: {
239
+ x: targetMatch.x,
240
+ y: getMatchCenterY(targetMatch)
241
+ }
242
+ });
243
+ });
244
+ }
245
+ return connectors;
246
+ }
247
+ function calculateLayoutWidth(rounds) {
248
+ const lastMatch = rounds[rounds.length - 1].matches[0];
249
+ return lastMatch.x + lastMatch.width;
250
+ }
251
+ function calculateLayoutHeight(rounds) {
252
+ return Math.max(...rounds.flatMap((round) => round.matches.map((match) => match.y + match.height)));
253
+ }
254
+ function clearContainer(container) {
255
+ container.textContent = "";
256
+ }
257
+ function renderBracket(container, layout) {
258
+ clearContainer(container);
259
+ const root = document.createElement("div");
260
+ root.className = "tournament-bracket-container";
261
+ root.style.position = "relative";
262
+ root.style.width = `${layout.width}px`;
263
+ root.style.height = `${layout.height}px`;
264
+ const svg = createSvgLayer(layout);
265
+ root.appendChild(svg);
266
+ layout.rounds.forEach((round) => {
267
+ root.appendChild(createRoundElement(round));
268
+ });
269
+ container.appendChild(root);
270
+ }
271
+ function createSvgLayer(layout) {
272
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
273
+ svg.classList.add("tournament-bracket-connectors");
274
+ svg.setAttribute("width", String(layout.width));
275
+ svg.setAttribute("height", String(layout.height));
276
+ svg.setAttribute("viewBox", `0 0 ${layout.width} ${layout.height}`);
277
+ svg.style.position = "absolute";
278
+ svg.style.inset = "0";
279
+ svg.style.overflow = "visible";
280
+ svg.style.pointerEvents = "none";
281
+ svg.style.zIndex = "0";
282
+ layout.connectors.forEach((connector) => {
283
+ svg.appendChild(createConnectorPath(connector));
284
+ });
285
+ return svg;
286
+ }
287
+ function createConnectorPath(connector) {
288
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
289
+ const middleX = connector.from.x + (connector.to.x - connector.from.x) / 2;
290
+ const d = [
291
+ `M ${connector.from.x} ${connector.from.y}`,
292
+ `H ${middleX}`,
293
+ `V ${connector.to.y}`,
294
+ `H ${connector.to.x}`
295
+ ].join(" ");
296
+ path.setAttribute("d", d);
297
+ path.setAttribute("fill", "none");
298
+ path.setAttribute("stroke", config.connectorColor);
299
+ path.setAttribute("stroke-width", "1");
300
+ path.setAttribute("stroke-linejoin", "round");
301
+ path.setAttribute("stroke-linecap", "round");
302
+ path.classList.add("tournament-bracket-connector");
303
+ return path;
304
+ }
305
+ function createRoundElement(round) {
306
+ const element = document.createElement("div");
307
+ element.className = "tournament-bracket-column";
308
+ element.dataset.roundId = round.roundId;
309
+ element.dataset.roundName = round.name;
310
+ element.dataset.roundIndex = String(round.roundIndex);
311
+ element.style.position = "absolute";
312
+ element.style.left = `${round.x}px`;
313
+ element.style.top = "0";
314
+ element.style.width = `${config.width}px`;
315
+ element.style.zIndex = "1";
316
+ round.matches.forEach((match) => {
317
+ element.appendChild(createMatchElement(match));
318
+ });
319
+ return element;
320
+ }
321
+ function createMatchElement(match) {
322
+ const element = document.createElement("div");
323
+ const matchState = getMatchState(match);
324
+ element.id = match.matchId;
325
+ element.className = "match-container";
326
+ element.dataset.matchStatus = matchState.status;
327
+ element.style.position = "absolute";
328
+ element.style.top = `${match.y}px`;
329
+ element.style.left = "0";
330
+ element.style.width = `${match.width}px`;
331
+ element.style.height = `${match.height}px`;
332
+ const vsElement = document.createElement("div");
333
+ vsElement.className = "player-vs";
334
+ vsElement.textContent = "VS";
335
+ element.appendChild(vsElement);
336
+ element.appendChild(createPlayerElement(match.players[0], "tournament-bracket-first-player", matchState.winnerIndexes.includes(0)));
337
+ element.appendChild(createPlayerElement(match.players[1], "tournament-bracket-second-player", matchState.winnerIndexes.includes(1)));
338
+ element.addEventListener("click", () => {
339
+ if (typeof config.onMatchClick === "function") config.onMatchClick(match);
340
+ });
341
+ return element;
342
+ }
343
+ function createPlayerElement(player, className, isWinner) {
344
+ const element = document.createElement("div");
345
+ element.classList.add("tournament-bracket-player", className);
346
+ if (isWinner) element.classList.add("player-winner");
347
+ element.style.minWidth = `${config.width}px`;
348
+ const avatarAndNameContainerElement = document.createElement("div");
349
+ avatarAndNameContainerElement.className = "avatar-and-name-container";
350
+ const avatarElement = document.createElement("img");
351
+ avatarElement.className = "avatar";
352
+ avatarElement.src = player.avatarUrl || config.avatarFallbackUrl;
353
+ avatarElement.alt = player.name || "Player avatar";
354
+ const nameElement = document.createElement("div");
355
+ nameElement.className = "name";
356
+ nameElement.textContent = player.name;
357
+ const scoreElement = document.createElement("div");
358
+ scoreElement.className = "score";
359
+ scoreElement.textContent = player.score;
360
+ avatarAndNameContainerElement.appendChild(avatarElement);
361
+ avatarAndNameContainerElement.appendChild(nameElement);
362
+ element.appendChild(avatarAndNameContainerElement);
363
+ element.appendChild(scoreElement);
364
+ return element;
365
+ }
366
+ function findMatchById(matchId) {
367
+ if (!bracketState) return null;
368
+ for (const round of bracketState.rounds) {
369
+ const foundMatch = round.matches.find((match) => match.matchId === matchId);
370
+ if (foundMatch) return foundMatch;
371
+ }
372
+ return null;
373
+ }
374
+ function init() {
375
+ const container = validateConfig();
376
+ bracketState = buildBracketModel(config.rounds);
377
+ resolveBracket();
378
+ renderBracket(container, createBracketLayout(bracketState));
379
+ }
380
+ function updateMatch(matchId, nextMatchData) {
381
+ if (!bracketState) throw new Error("Tournament bracket is not initialized");
382
+ let updatedRoundIndex = -1;
383
+ let updatedMatchIndex = -1;
384
+ bracketState.rounds = bracketState.rounds.map((round, roundIndex) => ({
385
+ ...round,
386
+ matches: round.matches.map((match, matchIndex) => {
387
+ if (match.matchId !== matchId) return match;
388
+ updatedRoundIndex = roundIndex;
389
+ updatedMatchIndex = matchIndex;
390
+ return mergeMatchData(match, {
391
+ ...nextMatchData,
392
+ matchId
393
+ });
394
+ })
395
+ }));
396
+ if (updatedRoundIndex === -1 || updatedMatchIndex === -1) throw new Error(`Match with id "${matchId}" not found`);
397
+ resolveBracket();
398
+ renderBracket(validateConfig(), createBracketLayout(bracketState));
399
+ const updatedMatch = findMatchById(matchId);
400
+ if (typeof config.onMatchUpdate === "function" && updatedMatch) config.onMatchUpdate(updatedMatch);
401
+ return updatedMatch;
402
+ }
403
+ function setMatchScore(matchId, score1, score2) {
404
+ return updateMatch(matchId, { players: [{ score: score1 }, { score: score2 }] });
405
+ }
406
+ function finishMatch(matchId, score1, score2) {
407
+ const updatedMatch = updateMatch(matchId, {
408
+ isFinished: true,
409
+ status: "completed",
410
+ players: [{ score: score1 }, { score: score2 }]
411
+ });
412
+ const matchState = getMatchState(updatedMatch);
413
+ if (matchState.status !== "completed") throw new Error(`Match "${matchId}" cannot be finished because its state is "${matchState.status}".`);
414
+ if (typeof config.onMatchFinish === "function") config.onMatchFinish(updatedMatch);
415
+ return updatedMatch;
416
+ }
417
+ function destroy() {
418
+ const container = document.getElementById(config.targetId);
419
+ if (container) clearContainer(container);
420
+ bracketState = null;
421
+ }
422
+ function getState() {
423
+ if (typeof structuredClone === "function") return structuredClone(bracketState);
424
+ return JSON.parse(JSON.stringify(bracketState));
425
+ }
426
+ return {
427
+ init,
428
+ updateMatch,
429
+ setMatchScore,
430
+ finishMatch,
431
+ destroy,
432
+ getState,
433
+ onMatchClick(fn) {
434
+ config.onMatchClick = fn;
435
+ },
436
+ onMatchUpdate(fn) {
437
+ config.onMatchUpdate = fn;
438
+ },
439
+ onMatchFinish(fn) {
440
+ config.onMatchFinish = fn;
441
+ }
442
+ };
443
+ }
444
+ //#endregion
445
+ export { tournamentBracket as default, tournamentBracket };
@@ -0,0 +1 @@
1
+ function t(t={}){const e={name:"-",avatarUrl:"",score:"-"},n={targetId:"tournament-bracket",rounds:null,distance:42,width:196,matchHeight:94,roundGap:96,avatarFallbackUrl:"data:image/svg+xml;utf8,"+encodeURIComponent('\n <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64">\n <rect width="100%" height="100%" fill="#e5e7eb"/>\n <text x="50%" y="54%" text-anchor="middle" font-size="28" fill="#6b7280">?</text>\n </svg>\n '),connectorColor:"white",onMatchClick:null,onMatchUpdate:null,onMatchFinish:null};const r=function(t){return{...n,...t,rounds:t.rounds??[]}}(t);let o=null;function a(){const t=document.getElementById(r.targetId);if(!t)throw new Error(`Element with id "${r.targetId}" not found`);return function(t){if(!Array.isArray(t)||0===t.length)throw new Error("rounds must be a non-empty array.");t.forEach((t,e)=>{if(!t||"object"!=typeof t)throw new Error(`Round at index ${e} must be an object.`);if(!Array.isArray(t.matches))throw new Error(`rounds[${e}].matches must be an array.`);if(0===t.matches.length)throw new Error(`rounds[${e}].matches must not be empty.`);t.matches.forEach((t,n)=>{if(!t||"object"!=typeof t)throw new Error(`rounds[${e}].matches[${n}] must be an object.`);if(!Array.isArray(t.players))throw new Error(`rounds[${e}].matches[${n}].players must be an array.`);if(2!==t.players.length)throw new Error(`rounds[${e}].matches[${n}] must have exactly 2 players.`)})});const e=t[0].matches.length;if(!((n=e)>0)||n&n-1)throw new Error("First round matches count must be a power of two.");var n;for(let r=1;r<t.length;r++){const e=t[r-1].matches.length/2,n=t[r].matches.length;if(n!==e)throw new Error(`Invalid bracket shape: rounds[${r}] must have ${e} matches, but got ${n}.`)}if(1!==t[t.length-1].matches.length)throw new Error("Last round must have exactly 1 match.")}(r.rounds),t}function s(t){return window.crypto?.randomUUID?`${t}-${window.crypto.randomUUID()}`:`${t}-${Math.random().toString(36).slice(2,9)}`}function c(t){const n="boolean"==typeof t.isFinished?t.isFinished:"completed"===t.status;return{matchId:t.matchId||s("match"),status:t.status||(n?"completed":"pending"),isFinished:n,players:[{...e,...t.players?.[0]},{...e,...t.players?.[1]}]}}function i(t){return"-"===t||""===t||null==t}function d(t){const[e,n]=t.players,r=i(e.score),o=i(n.score);if(r&&o)return{status:"pending",winnerIndexes:[]};const a=Number(e.score),s=Number(n.score);return Number.isNaN(a)||Number.isNaN(s)?{status:"invalid-score",winnerIndexes:[]}:a===s?{status:"draw",winnerIndexes:[]}:t.isFinished?{status:"completed",winnerIndexes:[a>s?0:1]}:{status:"in-progress",winnerIndexes:[]}}function u(t={}){return t.id||t.name||""}function h(t,n){const r=o.rounds[t]?.matches?.[n];if(!r)return;const a=d(r),s=o.rounds[t+1];if(!s)return;const c=Math.floor(n/2),i=s.matches[c];if(!i)return;const h=n%2,l="completed"===a.status?r.players[a.winnerIndexes[0]]:null;i.players[h]=l?function(t,e={},n){const r=u(t),o=u(e),a=r&&o&&r===o;return{name:t.name??"",avatarUrl:t.avatarUrl??"",seed:t.seed,id:t.id,score:a?e.score??"-":"-",previousMatchId:n}}(l,i.players[h],r.matchId):{...e}}function l(){for(let t=0;t<o.rounds.length-1;t++)o.rounds[t].matches.forEach((e,n)=>{h(t,n)})}function m(t){const e=r.width,n=r.roundGap,o=r.matchHeight,a=r.distance,s=t.rounds.map((t,r)=>{const s=r*(e+n);return{...t,x:s,matches:t.matches.map((t,n)=>({...t,x:s,y:n*(o+a),width:e,height:o}))}});for(let r=1;r<s.length;r++){const t=s[r-1],e=s[r];e.matches=e.matches.map((e,n)=>{const r=t.matches[2*n],a=t.matches[2*n+1];return{...e,y:(p(r)+p(a))/2-o/2}})}return{rounds:s,connectors:f(s),width:w(s),height:y(s)}}function p(t){return t.y+t.height/2}function f(t){const e=[];for(let n=0;n<t.length-1;n++){const r=t[n],o=t[n+1];r.matches.forEach((t,n)=>{const r=o.matches[Math.floor(n/2)];r&&e.push({id:s("connector"),from:{x:t.x+t.width,y:p(t)},to:{x:r.x,y:p(r)}})})}return e}function w(t){const e=t[t.length-1].matches[0];return e.x+e.width}function y(t){return Math.max(...t.flatMap(t=>t.matches.map(t=>t.y+t.height)))}function g(t){t.textContent=""}function x(t,e){g(t);const n=document.createElement("div");n.className="tournament-bracket-container",n.style.position="relative",n.style.width=`${e.width}px`,n.style.height=`${e.height}px`;const o=function(t){const e=document.createElementNS("http://www.w3.org/2000/svg","svg");return e.classList.add("tournament-bracket-connectors"),e.setAttribute("width",String(t.width)),e.setAttribute("height",String(t.height)),e.setAttribute("viewBox",`0 0 ${t.width} ${t.height}`),e.style.position="absolute",e.style.inset="0",e.style.overflow="visible",e.style.pointerEvents="none",e.style.zIndex="0",t.connectors.forEach(t=>{e.appendChild(function(t){const e=document.createElementNS("http://www.w3.org/2000/svg","path"),n=t.from.x+(t.to.x-t.from.x)/2,o=[`M ${t.from.x} ${t.from.y}`,`H ${n}`,`V ${t.to.y}`,`H ${t.to.x}`].join(" ");return e.setAttribute("d",o),e.setAttribute("fill","none"),e.setAttribute("stroke",r.connectorColor),e.setAttribute("stroke-width","1"),e.setAttribute("stroke-linejoin","round"),e.setAttribute("stroke-linecap","round"),e.classList.add("tournament-bracket-connector"),e}(t))}),e}(e);n.appendChild(o),e.rounds.forEach(t=>{n.appendChild(function(t){const e=document.createElement("div");return e.className="tournament-bracket-column",e.dataset.roundId=t.roundId,e.dataset.roundName=t.name,e.dataset.roundIndex=String(t.roundIndex),e.style.position="absolute",e.style.left=`${t.x}px`,e.style.top="0",e.style.width=`${r.width}px`,e.style.zIndex="1",t.matches.forEach(t=>{e.appendChild(function(t){const e=document.createElement("div"),n=d(t);e.id=t.matchId,e.className="match-container",e.dataset.matchStatus=n.status,e.style.position="absolute",e.style.top=`${t.y}px`,e.style.left="0",e.style.width=`${t.width}px`,e.style.height=`${t.height}px`;const o=document.createElement("div");return o.className="player-vs",o.textContent="VS",e.appendChild(o),e.appendChild(b(t.players[0],"tournament-bracket-first-player",n.winnerIndexes.includes(0))),e.appendChild(b(t.players[1],"tournament-bracket-second-player",n.winnerIndexes.includes(1))),e.addEventListener("click",()=>{"function"==typeof r.onMatchClick&&r.onMatchClick(t)}),e}(t))}),e}(t))}),t.appendChild(n)}function b(t,e,n){const o=document.createElement("div");o.classList.add("tournament-bracket-player",e),n&&o.classList.add("player-winner"),o.style.minWidth=`${r.width}px`;const a=document.createElement("div");a.className="avatar-and-name-container";const s=document.createElement("img");s.className="avatar",s.src=t.avatarUrl||r.avatarFallbackUrl,s.alt=t.name||"Player avatar";const c=document.createElement("div");c.className="name",c.textContent=t.name;const i=document.createElement("div");return i.className="score",i.textContent=t.score,a.appendChild(s),a.appendChild(c),o.appendChild(a),o.appendChild(i),o}function v(t,e){if(!o)throw new Error("Tournament bracket is not initialized");let n=-1,s=-1;if(o.rounds=o.rounds.map((r,o)=>({...r,matches:r.matches.map((r,a)=>r.matchId!==t?r:(n=o,s=a,function(t,e){const n=e.players||[];return c({...t,...e,players:[{...t.players?.[0],...n[0]},{...t.players?.[1],...n[1]}]})}(r,{...e,matchId:t})))})),-1===n||-1===s)throw new Error(`Match with id "${t}" not found`);l(),x(a(),m(o));const i=function(t){if(!o)return null;for(const e of o.rounds){const n=e.matches.find(e=>e.matchId===t);if(n)return n}return null}(t);return"function"==typeof r.onMatchUpdate&&i&&r.onMatchUpdate(i),i}return{init:function(){const t=a();var e;e=r.rounds,o={rounds:e.map((t,e)=>{const n=t.matches.map((t,n)=>{const r=c(t);return r.roundIndex=e,r.matchIndex=n,r});return{roundId:t.roundId||s("round"),name:t.name||`Round ${e+1}`,roundIndex:e,matches:n}})},l(),x(t,m(o))},updateMatch:v,setMatchScore:function(t,e,n){return v(t,{players:[{score:e},{score:n}]})},finishMatch:function(t,e,n){const o=v(t,{isFinished:!0,status:"completed",players:[{score:e},{score:n}]}),a=d(o);if("completed"!==a.status)throw new Error(`Match "${t}" cannot be finished because its state is "${a.status}".`);return"function"==typeof r.onMatchFinish&&r.onMatchFinish(o),o},destroy:function(){const t=document.getElementById(r.targetId);t&&g(t),o=null},getState:function(){return"function"==typeof structuredClone?structuredClone(o):JSON.parse(JSON.stringify(o))},onMatchClick(t){r.onMatchClick=t},onMatchUpdate(t){r.onMatchUpdate=t},onMatchFinish(t){r.onMatchFinish=t}}}export{t as default,t as tournamentBracket};