").addClass("match-container");
matchContainer.append(matchDiv);
roundDiv.append(matchContainer);match.serial = serial;
serialMap[`${group}-${serial}`] = {
...match,
group,
column,
serial
};groupMatches[column].push(match);
});columnsContainer.append(roundDiv);
if (columnIndex < columns.length - 1) {
columnsContainer.append($("
").addClass("connector-column"));
}
});groupDiv.append(columnsContainer);
groupScrollWrapper.append(groupDiv);setTimeout(() => {
positionMatches(groupMatches, group);
waitForLayoutAndDraw(serialMap, 20);
}, 100);
}bracketDiv.append(groupScrollWrapper);const observer = new MutationObserver(() => {
const visible = $(".bracket_wrapper:visible").length > 0;
if (visible) {
setTimeout(() => {
waitForLayoutAndDraw(window._lastSerialMap || {}, 20);
}, 100);
}
});observer.observe(document.body, { childList: true, subtree: true });
window.globalSerialMap = serialMap;
}
function positionMatches(groupMatches, group) {
const levels = Object.keys(groupMatches).sort((a, b) => +a - +b);
const matchCenters = {};
const spacing = 200;
let bottomMost = 0;const totalMatches = Object.values(groupMatches).flat().length;
const isFiveTeamLayout = totalMatches === 4;
const isSixTeamLayout = totalMatches === 5;
if (isSixTeamLayout) {
const matchCenters = {};
let bottomMost = 0;// Step 1: Position initial matches
const serialPositions = {
1: 0,
2: 100,
3: 200
};// Position matches 1-3
Object.entries(groupMatches).forEach(([level, matches]) => {
matches.forEach((match) => {
const matchId = `match-serial-${group}-${match.serial}`;
const $el = $(`#${matchId}`).closest(".match-container");if (!$el.length) return;const y = serialPositions[match.serial] ?? 0;
$el.css({ position: "absolute", top: `${y}px` });matchCenters[match.serial] = y + $el.outerHeight() / 2;
bottomMost = Math.max(bottomMost, y + $el.outerHeight());
});
});// Match 4: Between 1 and 2
const match4Id = `match-serial-${group}-4`;
const $match4 = $(`#${match4Id}`).closest(".match-container");
if ($match4.length) {
const avgY = (matchCenters[1] + matchCenters[2]) / 2 - $match4.outerHeight() / 2;
$match4.css({ position: "absolute", top: `${avgY}px` });
matchCenters[4] = avgY + $match4.outerHeight() / 2;
bottomMost = Math.max(bottomMost, avgY + $match4.outerHeight());
}// Match 5: Between 3 and 4
const match5Id = `match-serial-${group}-5`;
const $match5 = $(`#${match5Id}`).closest(".match-container");
if ($match5.length) {
const avgY = (matchCenters[3] + matchCenters[4]) / 2 - $match5.outerHeight() / 2;
$match5.css({ position: "absolute", top: `${avgY}px` });
matchCenters[5] = avgY + $match5.outerHeight() / 2;
bottomMost = Math.max(bottomMost, avgY + $match5.outerHeight());
}// Step 4: Draw connector lines
drawConnectorLine(group, 1, 4);
drawConnectorLine(group, 2, 4);
drawConnectorLine(group, 3, 5);
drawConnectorLine(group, 4, 5);// Step 5: Set container height
$(`.group[data-group="${group}"]`).css("height", bottomMost + 80);
}
function drawConnectorLine(group, fromSerial, toSerial) {
const fromEl = $(`#match-serial-${group}-${fromSerial}`).closest(".match-container");
const toEl = $(`#match-serial-${group}-${toSerial}`).closest(".match-container");if (!fromEl.length || !toEl.length) {
console.warn(`Cannot draw line: from ${fromSerial} or to ${toSerial} not found.`);
return;
}const $group = $(`.group[data-group="${group}"]`);
const $svg = $group.find("svg.connector-layer");if (!$svg.length) {
console.warn(`Missing SVG layer in group ${group}`);
return;
}const fromOffset = fromEl.position();
const toOffset = toEl.position();const fromX = fromEl.position().left + fromEl.outerWidth();
const fromY = fromOffset.top + fromEl.outerHeight() / 2;
const toX = toEl.position().left;
const toY = toOffset.top + toEl.outerHeight() / 2;console.log(`Drawing line: ${fromSerial} → ${toSerial}`, { fromX, fromY, toX, toY });// Create SVG line element
const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
line.setAttribute("x1", fromX);
line.setAttribute("y1", fromY);
line.setAttribute("x2", toX);
line.setAttribute("y2", toY);
line.setAttribute("stroke", "#333");
line.setAttribute("stroke-width", "2");$svg.append(line);
}// Positioning for 5-team layout
if (isFiveTeamLayout) {
const serialPositions = {
1: 200, // First round (Play-in)
3: 80, // Semifinal 1 (top)
2: 320, // Semifinal 2 (bottom)
4: 200 // Final (centered between semi 1 and 2)
};Object.entries(groupMatches).forEach(([level, matches]) => {
matches.forEach((match) => {
const matchId = `match-serial-${group}-${match.serial}`;
const $el = $(`#${matchId}`).closest(".match-container");if (!$el.length) return;const y = serialPositions[match.serial] ?? 0;
$el.css({ position: "absolute", top: `${y}px` });matchCenters[match.serial] = y + $el.outerHeight() / 2;
bottomMost = Math.max(bottomMost, y + $el.outerHeight());
});
});$(`.group[data-group="${group}"]`).css("height", bottomMost + 160);
return;
}// Default layout for other brackets
levels.forEach((level, levelIndex) => {
const matches = groupMatches[level];matches.forEach((match, i) => {
const matchId = `match-serial-${group}-${match.serial}`;
const $el = $(`#${matchId}`).closest(".match-container");if (!$el.length) return;if (levelIndex === 0) {
const pairIndex = Math.floor(i / 2);
const y = i * spacing + pairIndex * 40;
$el.css({ position: "absolute", top: `${y}px` });
matchCenters[match.serial] = y + $el.outerHeight() / 2;
bottomMost = Math.max(bottomMost, y + $el.outerHeight());
} else {
const prevLevel = levels[levelIndex - 1];
const prevMatches = groupMatches[prevLevel];
const prev1 = prevMatches[i * 2];
const prev2 = prevMatches[i * 2 + 1];if (prev1 && prev2) {
const center1 = matchCenters[prev1.serial];
const center2 = matchCenters[prev2.serial];
const centerY = (center1 + center2) / 2;
const topOffset = centerY - $el.outerHeight() / 2;
$el.css({ position: "absolute", top: `${topOffset}px` });
matchCenters[match.serial] = centerY;
bottomMost = Math.max(bottomMost, topOffset + $el.outerHeight());
}
}
});
});$(`.group[data-group="${group}"]`).css("height", bottomMost + 180);
}
function debounce(func, delay) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), delay);
};
}
function waitForLayoutAndDraw(serialMap, maxTries = 10) {
if (window._isDrawingScheduled) return;window._isDrawingScheduled = true;
window._lastSerialMap = serialMap;let tries = 0;function tryDraw() {
const wrapper = document.querySelector(".bracket_wrapper");if (!wrapper) {
if (tries < maxTries) {
tries++;
requestAnimationFrame(tryDraw);
} else {
window._isDrawingScheduled = false; // Reset if failed after all tries
}
return;
}const rect = wrapper.getBoundingClientRect();
if (rect.width < 100 || rect.height < 100) {
if (tries < maxTries) {
tries++;
requestAnimationFrame(tryDraw);
} else {
window._isDrawingScheduled = false;
}
return;
}// ✅ Only draw once per layout session
if (!wrapper.dataset.hasDrawn) {
drawBracketConnectors(serialMap, true);
wrapper.dataset.hasDrawn = "true";
}// 🟢 Reset the flag after a short time (if needed later)
setTimeout(() => {
window._isDrawingScheduled = false;
wrapper.dataset.hasDrawn = ""; // Allow future redraws if needed
}, 1000);// 🟢 Add scroll listener once
if (!wrapper.dataset.scrollListenerAdded) {
console.log("Ketan testing");
wrapper.addEventListener(
"scroll",
debounce(() => drawBracketConnectors(window._lastSerialMap, true), 100)
);
wrapper.dataset.scrollListenerAdded = "true";
}
}requestAnimationFrame(tryDraw);
}function drawBracketConnectors(serialMap, forceRedraw = false) {
console.log("Ketan test");
const svgNS = "http://www.w3.org/2000/svg";
let svg = document.getElementById("bracket-connector-svg");
const wrapper = document.querySelector(".bracket_wrapper");const wrapperRect = wrapper.getBoundingClientRect();
const scrollLeft = wrapper.scrollLeft;
const scrollTop = wrapper.scrollTop;if (!svg) {
svg = document.createElementNS(svgNS, "svg");
svg.setAttribute("id", "bracket-connector-svg");
svg.style.position = "absolute";
svg.style.top = "0";
svg.style.left = "0";
svg.style.pointerEvents = "none";
wrapper.appendChild(svg);
}svg.style.width = wrapper.scrollWidth + "px";
svg.style.height = wrapper.scrollHeight + "px";const layoutHash = JSON.stringify(
Object.values(serialMap).map(m => {
const el = document.getElementById(`match-serial-${m.group}-${m.serial}`);
const rect = el?.getBoundingClientRect();
return {
serial: m.serial,
group: m.group,
column: m.column,
rect: rect ? {
top: rect.top,
left: rect.left,
width: rect.width,
height: rect.height
} : null
};
})
);if (!forceRedraw && window._lastLayoutHash === layoutHash) return;
window._lastLayoutHash = layoutHash;while (svg.firstChild) svg.removeChild(svg.firstChild);const groupMap = {};
for (const serial in serialMap) {
const match = serialMap[serial];
if (!groupMap[match.group]) groupMap[match.group] = [];
groupMap[match.group].push(match);
}for (const group in groupMap) {
const matches = groupMap[group].sort((a, b) => +a.serial - +b.serial);
const matchMap = {};matches.forEach(match => {
matchMap[match.serial] = match;
match.el = document.getElementById(`match-serial-${group}-${match.serial}`);
match.rect = match.el?.getBoundingClientRect();
});const isFiveMatch = matches.length === 4;
const isThreeTeam = matches.length === 2 && matchMap["1"] && matchMap["2"];if (isThreeTeam) {
console.log("This is a testing");
const fromMatch = matchMap[1];
const toMatch = matchMap[2];if (fromMatch?.rect && toMatch?.rect) {
drawLConnectorCustom(fromMatch.rect, toMatch.rect, "right", 40);
}continue; // only skip current group
}
if (matches.length === 5) {
const m1 = matchMap[1], m2 = matchMap[2];
const m3 = matchMap[3], m4 = matchMap[4], m5 = matchMap[5];if (m1?.rect && m4?.rect) drawLConnectorCustom(m1.rect, m4.rect, "top", 40);
if (m2?.rect && m4?.rect) drawLConnectorCustom(m2.rect, m4.rect, "bottom", 40);
if (m3?.rect && m5?.rect) drawLConnectorCustom(m3.rect, m5.rect, "top", 40);
if (m4?.rect && m5?.rect) drawLConnectorCustom(m4.rect, m5.rect, "bottom", 40);
}if (isFiveMatch) {
const s1 = matchMap[1], s2 = matchMap[2], s3 = matchMap[3], s4 = matchMap[4];if (s1?.rect && s3?.rect) drawLConnectorCustom(s1.rect, s3.rect, "bottom");
if (s2?.rect && s4?.rect) drawLConnectorCustom(s2.rect, s4.rect, "top", 40);
if (s3?.rect && s4?.rect) drawLConnectorCustom(s3.rect, s4.rect, "bottom", 80);continue; // use continue, not return — don't break whole function
}const columns = [...new Set(matches.map(m => +m.column))].sort((a, b) => a - b);for (let colIndex = 1; colIndex < columns.length; colIndex++) {
const currentCol = columns[colIndex];
const prevCol = columns[colIndex - 1];const currentMatches = matches.filter(m => +m.column === currentCol);
const prevMatches = matches.filter(m => +m.column === prevCol);for (let i = 0; i < currentMatches.length; i++) {
const targetMatch = currentMatches[i];
const input1 = prevMatches[i * 2];
const input2 = prevMatches[i * 2 + 1];if (input1?.rect && input2?.rect && targetMatch?.rect) {
drawLConnectorCustom(input1.rect, targetMatch.rect, "top", 40);
drawLConnectorCustom(input2.rect, targetMatch.rect, "bottom", 40);
} else if ((input1?.rect || input2?.rect) && targetMatch?.rect) {
const fromRect = input1?.rect || input2?.rect;
const direction = input1?.rect ? "top" : "bottom";
drawLConnectorCustom(fromRect, targetMatch.rect, direction, 40);
}
}
}function drawLConnectorCustom(fromRect, toRect, toPoint = "center", elbowOffset = 40) {
const startX = fromRect.right - wrapperRect.left + scrollLeft;
const startY = fromRect.top + fromRect.height / 2 - wrapperRect.top + scrollTop;let toY;
const headerOffset = 48;if (toPoint === "top") {
toY = toRect.top + headerOffset + 10 - wrapperRect.top + scrollTop;
} else if (toPoint === "bottom") {
toY = toRect.bottom - 10 - wrapperRect.top + scrollTop;
} else {
toY = toRect.top + toRect.height / 2 - wrapperRect.top + scrollTop;
}const endX = toRect.left - wrapperRect.left + scrollLeft + 5;const path = document.createElementNS(svgNS, "path");
path.setAttribute("stroke", "#999");
path.setAttribute("stroke-width", "1.5");
path.setAttribute("fill", "none");
path.setAttribute("stroke-linecap", "round");const d = `
M ${startX},${startY}
H ${endX}
V ${toY}
`;path.setAttribute("d", d.trim().replace(/\s+/g, " "));
svg.appendChild(path);
}function drawTConnector(fromRect, toRect) {
const startX = fromRect.right - wrapperRect.left + scrollLeft;
const startY = fromRect.top + fromRect.height / 2 - wrapperRect.top + scrollTop;const endX = toRect.left - wrapperRect.left + scrollLeft;
const endY = toRect.top + toRect.height / 2 - wrapperRect.top + scrollTop;
const midX = (startX + endX) / 2;const path1 = document.createElementNS(svgNS, "path");
path1.setAttribute("stroke", "#999");
path1.setAttribute("stroke-width", "1.5");
path1.setAttribute("fill", "none");
path1.setAttribute("stroke-linecap", "round");
path1.setAttribute("d", `M ${startX},${startY} H ${midX}`);
svg.appendChild(path1);const path2 = document.createElementNS(svgNS, "path");
path2.setAttribute("stroke", "#999");
path2.setAttribute("stroke-width", "1.5");
path2.setAttribute("fill", "none");
path2.setAttribute("stroke-linecap", "round");
path2.setAttribute("d", `M ${endX},${endY} H ${midX}`);
svg.appendChild(path2);const verticalLine = document.createElementNS(svgNS, "path");
verticalLine.setAttribute("stroke", "#999");
verticalLine.setAttribute("stroke-width", "1.5");
verticalLine.setAttribute("fill", "none");
verticalLine.setAttribute("stroke-linecap", "round");
verticalLine.setAttribute("d", `M ${midX},${startY} V ${endY}`);
svg.appendChild(verticalLine);
}function drawHorizontalConnector(fromRect, toRect) {
const startX = fromRect.right - wrapperRect.left + scrollLeft;
const startY = fromRect.top + fromRect.height / 2 - wrapperRect.top + scrollTop;const endX = toRect.left - wrapperRect.left + scrollLeft;const path = document.createElementNS(svgNS, "path");
path.setAttribute("stroke", "#999");
path.setAttribute("stroke-width", "1.5");
path.setAttribute("fill", "none");
path.setAttribute("stroke-linecap", "round");const d = `M ${startX},${startY} H ${endX}`;
path.setAttribute("d", d.trim().replace(/\s+/g, " "));
svg.appendChild(path);
}function drawRightThenVertical(fromRect, toRect, align = "top") {
const startX = fromRect.right - wrapperRect.left + scrollLeft;
const startY = fromRect.top + fromRect.height / 2 - wrapperRect.top + scrollTop;
const elbowX = startX + 40;const toY = align === "top"
? toRect.top - wrapperRect.top + scrollTop
: toRect.bottom - wrapperRect.top + scrollTop;const toX = toRect.left - wrapperRect.left + scrollLeft;const path = document.createElementNS(svgNS, "path");
path.setAttribute("stroke", "#888");
path.setAttribute("stroke-width", "4");
path.setAttribute("fill", "none");
path.setAttribute("stroke-linejoin", "round");
path.setAttribute("stroke-linecap", "round");const d = `
M ${startX},${startY}
H ${elbowX}
V ${toY}
H ${toX}
`;
path.setAttribute("d", d.trim().replace(/\s+/g, " "));
svg.appendChild(path);
}function drawLine(from, to) {
if (!from?.rect || !to?.rect) return;const x1 = from.rect.right + scrollLeft - wrapperRect.left;
const y1 = from.rect.top + from.rect.height / 2 + scrollTop - wrapperRect.top;const x2 = to.rect.left + scrollLeft - wrapperRect.left;
const y2 = to.rect.top + to.rect.height / 2 + scrollTop - wrapperRect.top;const midX = (x1 + x2) / 2;const path = document.createElementNS(svgNS, "path");
path.setAttribute("stroke", "#999");
path.setAttribute("stroke-width", "2");
path.setAttribute("fill", "none");
path.setAttribute("d", `M ${x1},${y1} H ${midX} V ${y2} H ${x2}`);
svg.appendChild(path);
}function drawDefaultConnect(input1, input2, targetMatch) {
if (!input1?.rect || !input2?.rect || !targetMatch?.rect) return;const x1 = input1.rect.right + scrollLeft - wrapperRect.left;
const y1 = input1.rect.top + input1.rect.height / 2 + scrollTop - wrapperRect.top;const x2 = input2.rect.right + scrollLeft - wrapperRect.left;
const y2 = input2.rect.top + input2.rect.height / 2 + scrollTop - wrapperRect.top;const toX = targetMatch.rect.left + scrollLeft - wrapperRect.left;
const toY = targetMatch.rect.top + targetMatch.rect.height / 2 + scrollTop - wrapperRect.top;const midX = (x1 + toX) / 2;
const midY = (y1 + y2) / 2;const path = document.createElementNS(svgNS, "path");
path.setAttribute("stroke", "#999");
path.setAttribute("stroke-width", "2");
path.setAttribute("fill", "none");const d = `
M ${x1},${y1}
H ${midX}
M ${x2},${y2}
H ${midX}
M ${midX},${y1}
V ${y2}
M ${midX},${midY}
H ${toX}
`;
path.setAttribute("d", d.trim().replace(/\s+/g, ' '));
svg.appendChild(path);
}
}
}