/** * jQuery Bracket * * Copyright (c) 2011-2016, Teijo Laine, * http://aropupu.fi/bracket/ * * Licenced under the MIT licence */ var __extends = (this && this.__extends) || (function () { var extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; return function (d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); }; })(); /// (function ($) { var Option = (function () { function Option(val) { this.val = val; if (val instanceof Option) { throw new Error('Trying to wrap Option into an Option'); } if (this.val === undefined) { throw new Error('Option cannot contain undefined'); } } Option.of = function (value) { return new Option(value); }; Option.empty = function () { return new Option(null); }; Option.prototype.get = function () { if (this.val === null) { throw new Error('Trying to get() empty Option'); } return this.val; }; Option.prototype.orElse = function (defaultValue) { return (this.val === null) ? defaultValue : this.val; }; Option.prototype.orElseGet = function (defaultProvider) { return (this.val === null) ? defaultProvider() : this.val; }; Option.prototype.map = function (f) { return (this.val === null) ? Option.empty() : new Option(f(this.val)); }; Option.prototype.forEach = function (f) { if (this.val !== null) { f(this.val); } return this; }; Option.prototype.toNull = function () { return (this.val === null) ? null : this.val; }; Option.prototype.isEmpty = function () { return this.val === null; }; return Option; }()); var Score = (function (_super) { __extends(Score, _super); function Score() { return _super !== null && _super.apply(this, arguments) || this; } Score.of = function (val) { var type = typeof (val); var expected = 'number'; if (val !== null && type !== expected) { throw new Error("Invalid score format, expected " + expected + ", got " + type); } return Option.of(val); }; Score.empty = function () { return Option.empty(); }; return Score; }(Option)); var ResultObject = (function () { function ResultObject(first, second, userData) { this.first = first; this.second = second; this.userData = userData; if (!first || !second) { throw new Error('Cannot create ResultObject with undefined scores'); } return; } return ResultObject; }()); var BranchType; (function (BranchType) { BranchType[BranchType["TBD"] = 0] = "TBD"; BranchType[BranchType["BYE"] = 1] = "BYE"; })(BranchType || (BranchType = {})); var Order = (function () { function Order(isFirst) { this.isFirst = isFirst; } Order.first = function () { return new Order(true); }; Order.second = function () { return new Order(false); }; Order.prototype.map = function (first, second) { return this.isFirst ? first : second; }; return Order; }()); var TeamBlock = (function () { function TeamBlock(source, // Where base of the information propagated from name, order, seed, score) { this.source = source; this.name = name; this.order = order; this.seed = seed; this.score = score; } // Recursively check if branch ends into a BYE TeamBlock.prototype.emptyBranch = function () { if (!this.name.isEmpty()) { return BranchType.TBD; } else { try { return this.source().emptyBranch(); } catch (e) { if (e instanceof EndOfBranchException) { return BranchType.BYE; } else { throw new Error('Unexpected exception type'); } } } }; return TeamBlock; }()); // http://stackoverflow.com/questions/18082/validate-numbers-in-javascript-isnumeric function isNumber(n) { return !isNaN(parseFloat(n)) && isFinite(n); } function EndOfBranchException() { this.message = 'Root of information for this team'; this.name = 'EndOfBranchException'; } var MatchResult = (function () { function MatchResult(a, b) { this.a = a; this.b = b; return; } MatchResult.teamsInResultOrder = function (match) { var aBye = match.a.name.isEmpty(); var bBye = match.b.name.isEmpty(); if (bBye && !aBye) { if (match.b.emptyBranch() === BranchType.BYE) { return [match.a, match.b]; } else { return []; } } else if (aBye && !bBye) { if (match.a.emptyBranch() === BranchType.BYE) { return [match.b, match.a]; } else { return []; } } else if (!match.a.score.isEmpty() && !match.b.score.isEmpty()) { if (match.a.score.get() > match.b.score.get()) { return [match.a, match.b]; } else if (match.a.score.get() < match.b.score.get()) { return [match.b, match.a]; } } return []; }; // Arbitrary (either parent) source is required so that branch emptiness // can be determined by traversing to the beginning. MatchResult.emptyTeam = function (source) { return new TeamBlock(source, Option.empty(), Option.empty(), Option.empty(), Score.empty()); }; MatchResult.prototype.winner = function () { return MatchResult.teamsInResultOrder(this)[0] || MatchResult.emptyTeam(this.a.source); }; MatchResult.prototype.loser = function () { return MatchResult.teamsInResultOrder(this)[1] || MatchResult.emptyTeam(this.a.source); }; return MatchResult; }()); function depth(a) { function df(a, d) { if (a instanceof Array) { return df(a[0], d + 1); } return d; } return df(a, 0); } function wrap(a, d) { if (d > 0) { a = wrap([a], d - 1); } return a; } function trackHighlighter(teamIndex, cssClass, container) { var elements = container.find('.team[data-teamid=' + teamIndex + ']'); var addedClass = !cssClass ? 'highlight' : cssClass; return { highlight: function () { elements.each(function () { $(this).addClass(addedClass); if ($(this).hasClass('win')) { $(this).parent().find('.connector').addClass(addedClass); } }); }, deHighlight: function () { elements.each(function () { $(this).removeClass(addedClass); $(this).parent().find('.connector').removeClass(addedClass); }); } }; } function postProcess(container, w, f) { var source = f || w; var winner = source.winner(); var loser = source.loser(); if (winner && loser) { if (!winner.name.isEmpty()) { trackHighlighter(winner.seed.get(), 'highlightWinner', container).highlight(); } if (!loser.name.isEmpty()) { trackHighlighter(loser.seed.get(), 'highlightLoser', container).highlight(); } } container.find('.team').mouseover(function () { var teamId = $(this).attr('data-teamid'); // Don't highlight BYEs if (teamId === undefined) { return; } var track = trackHighlighter(parseInt(teamId, 10), null, container); track.highlight(); $(this).mouseout(function () { track.deHighlight(); $(this).unbind('mouseout'); }); }); } function defaultEdit(span, data, done) { var input = $(''); input.val(data); span.empty().append(input); input.focus(); input.blur(function () { done(input.val()); }); input.keydown(function (e) { var key = (e.keyCode || e.which); if (key === 9 /*tab*/ || key === 13 /*return*/ || key === 27 /*esc*/) { e.preventDefault(); done(input.val(), (key !== 27)); } }); } function defaultRender(container, team, score, state) { switch (state) { case 'empty-bye': container.append('BYE'); return; case 'empty-tbd': container.append('TBD'); return; case 'entry-no-score': case 'entry-default-win': case 'entry-complete': container.append(team); return; } } function winnerBubbles(match) { var el = match.el; var winner = el.find('.team.win'); winner.append('
1st
'); var loser = el.find('.team.lose'); loser.append('
2nd
'); return true; } function consolationBubbles(match) { var el = match.el; var winner = el.find('.team.win'); winner.append('
3rd
'); var loser = el.find('.team.lose'); loser.append('
4th
'); return true; } var winnerMatchSources = function (teams, m) { return function () { return [ { source: function () { return new TeamBlock(function () { throw new EndOfBranchException(); }, teams[m][0], Option.of(Order.first()), Option.of(m * 2), Score.empty()); } }, { source: function () { return new TeamBlock(function () { throw new EndOfBranchException(); }, teams[m][1], Option.of(Order.second()), Option.of(m * 2 + 1), Score.empty()); } } ]; }; }; var winnerAlignment = function (match, skipConsolationRound) { return function (tC) { tC.css('top', ''); tC.css('position', 'absolute'); if (skipConsolationRound) { tC.css('top', (match.el.height() / 2 - tC.height() / 2) + 'px'); } else { tC.css('bottom', (-tC.height() / 2) + 'px'); } }; }; function prepareWinners(winners, teams, isSingleElimination, opts, skipGrandFinalComeback) { var roundCount = Math.log(teams.length * 2) / Math.log(2); var matchCount = teams.length; var round; for (var r = 0; r < roundCount; r += 1) { round = winners.addRound(Option.empty()); for (var m = 0; m < matchCount; m += 1) { var teamCb = (r === 0) ? winnerMatchSources(teams, m) : null; if (!(r === roundCount - 1 && isSingleElimination) && !(r === roundCount - 1 && skipGrandFinalComeback)) { round.addMatch(teamCb, Option.empty()); } else { var match = round.addMatch(teamCb, Option.of(winnerBubbles)); if (!skipGrandFinalComeback) { match.setAlignCb(winnerAlignment(match, opts.skipConsolationRound)); } } } matchCount /= 2; } if (isSingleElimination) { winners.final().setConnectorCb(Option.empty()); if (teams.length > 1 && !opts.skipConsolationRound) { var prev = winners.final().getRound().prev(); var third_1 = prev.map(function (p) { return function () { return p.match(0).loser(); }; }).toNull(); var fourth_1 = prev.map(function (p) { return function () { return p.match(1).loser(); }; }).toNull(); var consol_1 = round.addMatch(function () { return [ { source: third_1 }, { source: fourth_1 } ]; }, Option.of(consolationBubbles)); consol_1.setAlignCb(function (tC) { var height = (winners.el.height()) / 2; consol_1.el.css('height', (height) + 'px'); var topShift = tC.height() / 2 + opts.matchMargin; tC.css('top', (topShift) + 'px'); }); consol_1.setConnectorCb(Option.empty()); } } } var loserMatchSources = function (winners, losers, matchCount, m, n, r) { return function () { /* first round comes from winner bracket */ if (n % 2 === 0 && r === 0) { return [ { source: function () { return winners.round(0).match(m * 2).loser(); } }, { source: function () { return winners.round(0).match(m * 2 + 1).loser(); } } ]; } else { /* To maximize the time it takes for two teams to play against * eachother twice, WB losers are assigned in reverse order * every second round of LB */ var winnerMatch_1 = (r % 2 === 0) ? (matchCount - m - 1) : m; return [ { source: function () { return losers.round(r * 2).match(m).winner(); } }, { source: function () { return winners.round(r + 1).match(winnerMatch_1).loser(); } } ]; } }; }; var loserAlignment = function (teamCon, match) { return function () { return teamCon.css('top', (match.el.height() / 2 - teamCon.height() / 2) + 'px'); }; }; function prepareLosers(winners, losers, teamCount, skipGrandFinalComeback, centerConnectors) { var roundCount = Math.log(teamCount * 2) / Math.log(2) - 1; var matchCount = teamCount / 2; for (var r = 0; r < roundCount; r += 1) { /* if player cannot rise back to grand final, last round of loser * bracket will be player between two LB players, eliminating match * between last WB loser and current LB winner */ var subRounds = (skipGrandFinalComeback && r === (roundCount - 1) ? 1 : 2); for (var n = 0; n < subRounds; n += 1) { var round = losers.addRound(Option.empty()); for (var m = 0; m < matchCount; m += 1) { var teamCb = (!(n % 2 === 0 && r !== 0)) ? loserMatchSources(winners, losers, matchCount, m, n, r) : null; var isLastMatch = r === roundCount - 1 && skipGrandFinalComeback; var match = round.addMatch(teamCb, Option.of(isLastMatch ? consolationBubbles : null)); match.setAlignCb(loserAlignment(match.el.find('.teamContainer'), match)); if (isLastMatch) { // Override default connector match.setConnectorCb(Option.empty()); } else if (r < roundCount - 1 || n < 1) { var cb = (n % 2 === 0) ? function (tC, match) { // inside lower bracket var connectorOffset = tC.height() / 4; var center = { height: 0, shift: connectorOffset * 2 }; return match.winner().order .map(function (order) { return order.map(centerConnectors ? center : { height: 0, shift: connectorOffset }, centerConnectors ? center : { height: -connectorOffset * 2, shift: connectorOffset }); }) .orElse(center); } : null; match.setConnectorCb(Option.of(cb)); } } } matchCount /= 2; } } function prepareFinals(finals, winners, losers, opts, topCon, resizeContainer) { var round = finals.addRound(Option.empty()); var match = round.addMatch(function () { return [ { source: function () { return winners.winner(); } }, { source: function () { return losers.winner(); } } ]; }, Option.of(function (match) { /* Track if container has been resized for final rematch */ var _isResized = false; /* LB winner won first final match, need a new one */ if (!opts.skipSecondaryFinal && (!match.winner().name.isEmpty() && match.winner().name === losers.winner().name)) { if (finals.size() === 2) { return false; } /* This callback is ugly, would be nice to make more sensible solution */ var doRenderCb = function () { var rematch = ((!match.winner().name.isEmpty() && match.winner().name === losers.winner().name)); if (_isResized === false) { if (rematch) { _isResized = true; resizeContainer(); } } if (!rematch && _isResized) { _isResized = false; finals.dropRound(); resizeContainer(); } return rematch; }; var round_1 = finals.addRound(Option.of(doRenderCb)); /* keep order the same, WB winner top, LB winner below */ var match2_1 = round_1.addMatch(function () { return [ { source: function () { return match.first(); } }, { source: function () { return match.second(); } } ]; }, Option.of(winnerBubbles)); match.setConnectorCb(Option.of(function (tC) { return { height: 0, shift: tC.height() / 2 }; })); match2_1.setConnectorCb(Option.empty()); match2_1.setAlignCb(function (tC) { var height = (winners.el.height() + losers.el.height()); match2_1.el.css('height', (height) + 'px'); var topShift = (winners.el.height() / 2 + winners.el.height() + losers.el.height() / 2) / 2 - tC.height(); tC.css('top', (topShift) + 'px'); }); return false; } else { if (finals.size() === 2) { finals.dropRound(); } else if (finals.size() > 2) { throw new Error('Unexpected number of final rounds'); } return winnerBubbles(match); } })); match.setAlignCb(function (tC) { var height = (winners.el.height() + losers.el.height()); if (!opts.skipConsolationRound) { height /= 2; } match.el.css('height', (height) + 'px'); var topShift = (winners.el.height() / 2 + winners.el.height() + losers.el.height() / 2) / 2 - tC.height(); tC.css('top', (topShift) + 'px'); }); if (!opts.skipConsolationRound) { var prev_1 = losers.final().getRound().prev(); var consol_2 = round.addMatch(function () { return [ { source: function () { return prev_1.get().match(0).loser(); } }, { source: function () { return losers.loser(); } } ]; }, Option.of(consolationBubbles)); consol_2.setAlignCb(function (tC) { var height = (winners.el.height() + losers.el.height()) / 2; consol_2.el.css('height', (height) + 'px'); var topShift = (winners.el.height() / 2 + winners.el.height() + losers.el.height() / 2) / 2 + tC.height() / 2 - height; tC.css('top', (topShift) + 'px'); }); match.setConnectorCb(Option.empty()); consol_2.setConnectorCb(Option.empty()); } winners.final().setConnectorCb(Option.of(function (tC) { var connectorOffset = tC.height() / 4; var topShift = (winners.el.height() / 2 + winners.el.height() + losers.el.height() / 2) / 2 - tC.height() / 2; var matchupOffset = topShift - winners.el.height() / 2; var _a = winners.winner().order .map(function (order) { return order.map({ height: matchupOffset + connectorOffset * (opts.centerConnectors ? 2 : 1), shift: connectorOffset * (opts.centerConnectors ? 2 : 1) }, { height: matchupOffset + connectorOffset * (opts.centerConnectors ? 2 : 0), shift: connectorOffset * (opts.centerConnectors ? 2 : 3) }); }) .orElse({ height: matchupOffset + connectorOffset * (opts.centerConnectors ? 2 : 1), shift: connectorOffset * 2 }), height = _a.height, shift = _a.shift; height -= tC.height() / 2; return { height: height, shift: shift }; })); losers.final().setConnectorCb(Option.of(function (tC) { var connectorOffset = tC.height() / 4; var topShift = (winners.el.height() / 2 + winners.el.height() + losers.el.height() / 2) / 2 - tC.height() / 2; var matchupOffset = topShift - winners.el.height() / 2; var _a = losers.winner().order .map(function (order) { return order.map({ height: matchupOffset + connectorOffset * (opts.centerConnectors ? 2 : 0), shift: connectorOffset * (opts.centerConnectors ? 2 : 3) }, { height: matchupOffset + connectorOffset * 2, shift: connectorOffset * (opts.centerConnectors ? 2 : 1) }); }) .orElse({ height: matchupOffset + connectorOffset * (opts.centerConnectors ? 2 : 1), shift: connectorOffset * 2 }), height = _a.height, shift = _a.shift; height += tC.height() / 2; return { height: -height, shift: -shift }; })); } var Round = (function () { function Round(bracket, previousRound, roundNumber, // TODO: results should be enforced to be correct by now _results, doRenderCb, mkMatch, isFirstBracket, opts) { this.bracket = bracket; this.previousRound = previousRound; this.roundNumber = roundNumber; this._results = _results; this.doRenderCb = doRenderCb; this.mkMatch = mkMatch; this.isFirstBracket = isFirstBracket; this.opts = opts; this.containerWidth = this.opts.teamWidth + this.opts.scoreWidth; this.roundCon = $("
"); this.matches = []; } Object.defineProperty(Round.prototype, "el", { get: function () { return this.roundCon; }, enumerable: true, configurable: true }); Round.prototype.addMatch = function (teamCb, renderCb) { var _this = this; var matchIdx = this.matches.length; var teams = (teamCb !== null) ? teamCb() : [ { source: function () { return _this.bracket.round(_this.roundNumber - 1).match(matchIdx * 2).winner(); } }, { source: function () { return _this.bracket.round(_this.roundNumber - 1).match(matchIdx * 2 + 1).winner(); } } ]; var teamA = function () { return teams[0].source(); }; var teamB = function () { return teams[1].source(); }; var matchResult = new MatchResult(new TeamBlock(teamA, teamA().name, Option.of(Order.first()), teamA().seed, Score.empty()), new TeamBlock(teamB, teamB().name, Option.of(Order.second()), teamB().seed, Score.empty())); var match = this.mkMatch(this, matchResult, matchIdx, this._results.map(function (r) { return r[matchIdx] === undefined ? null : r[matchIdx]; }), renderCb, this.isFirstBracket, this.opts); this.matches.push(match); return match; }; Round.prototype.match = function (id) { return this.matches[id]; }; Round.prototype.prev = function () { return this.previousRound; }; Round.prototype.size = function () { return this.matches.length; }; Round.prototype.render = function () { this.roundCon.empty(); if (!this.doRenderCb.isEmpty() && !this.doRenderCb.get()()) { return; } this.roundCon.appendTo(this.bracket.el); this.matches.forEach(function (m) { return m.render(); }); }; Round.prototype.results = function () { return this.matches.reduce(function (agg, m) { return agg.concat([m.results()]); }, []); }; return Round; }()); var Bracket = (function () { function Bracket(bracketCon, initResults, mkMatch, isFirstBracket, opts) { this.bracketCon = bracketCon; this.initResults = initResults; this.mkMatch = mkMatch; this.isFirstBracket = isFirstBracket; this.opts = opts; this.rounds = []; } Object.defineProperty(Bracket.prototype, "el", { get: function () { return this.bracketCon; }, enumerable: true, configurable: true }); Bracket.prototype.addRound = function (doRenderCb) { var id = this.rounds.length; var previous = (id > 0) ? Option.of(this.rounds[id - 1]) : Option.empty(); // Rounds may be undefined if init score array does not match number of teams var roundResults = this.initResults .map(function (r) { return (r[id] === undefined) ? new ResultObject(Score.empty(), Score.empty(), undefined) : r[id]; }); var round = new Round(this, previous, id, roundResults, doRenderCb, this.mkMatch, this.isFirstBracket, this.opts); this.rounds.push(round); return round; }; Bracket.prototype.dropRound = function () { this.rounds.pop(); }; Bracket.prototype.round = function (id) { return this.rounds[id]; }; Bracket.prototype.size = function () { return this.rounds.length; }; Bracket.prototype.final = function () { return this.rounds[this.rounds.length - 1].match(0); }; Bracket.prototype.winner = function () { return this.rounds[this.rounds.length - 1].match(0).winner(); }; Bracket.prototype.loser = function () { return this.rounds[this.rounds.length - 1].match(0).loser(); }; Bracket.prototype.render = function () { this.bracketCon.empty(); /* Length of 'rounds' can increase during render in special case when LB win in finals adds new final round in match render callback. Therefore length must be read on each iteration. */ for (var i = 0; i < this.rounds.length; i += 1) { this.rounds[i].render(); } }; Bracket.prototype.results = function () { return this.rounds.reduce(function (agg, r) { return agg.concat([r.results()]); }, []); }; return Bracket; }()); function connector(roundMargin, connector, teamCon, align) { var height = connector.height, shift = connector.shift; var width = roundMargin / 2; var drop = true; // drop: // [team]'\ // \_[team] // !drop: // /'[team] // [team]_/ if (height < 0) { drop = false; height = -height; } /* straight lines are prettier */ if (height < 2) { height = 0; } var src = $('
').appendTo(teamCon); src.css('height', height); src.css('width', width + 'px'); src.css(align, (-width - 2) + 'px'); // Subtract 1 due to line thickness and alignment mismatch caused by // combining top and bottom alignment if (shift >= 0) { src.css('top', (shift - 1) + 'px'); } else { src.css('bottom', (-shift - 1) + 'px'); } if (drop) { src.css('border-bottom', 'none'); } else { src.css('border-top', 'none'); } var dst = $('
').appendTo(src); dst.css('width', width + 'px'); dst.css(align, -width + 'px'); if (drop) { dst.css('bottom', '0px'); } else { dst.css('top', '0px'); } return src; } function countRounds(teamCount, isSingleElimination, skipGrandFinalComeback, skipSecondaryFinal, results) { if (isSingleElimination) { return Math.log(teamCount * 2) / Math.log(2); } else if (skipGrandFinalComeback) { return Math.max(2, (Math.log(teamCount * 2) / Math.log(2) - 1) * 2 - 1); // DE - grand finals } else { // Loser bracket winner has won first match in grand finals, // this requires a new match unless explicitely skipped var hasGrandFinalRematch = (!skipSecondaryFinal && (results.length === 3 && results[2].length === 2)); return (Math.log(teamCount * 2) / Math.log(2) - 1) * 2 + 1 + (hasGrandFinalRematch ? 1 : 0); // DE + grand finals } } function exportData(data) { var output = $.extend(true, {}, data); output.teams = output.teams.map(function (ts) { return ts.map(function (t) { return t.toNull(); }); }); output.results = output.results .map(function (brackets) { return brackets .map(function (rounds) { return rounds .map(function (matches) { var matchData = [matches.first.toNull(), matches.second.toNull()]; if (matches.userData !== undefined) { matchData.push(matches.userData); } return matchData; }); }); }); return output; } var ResultId = (function () { function ResultId() { this.counter = 0; } ResultId.prototype.get = function () { return this.counter; }; ResultId.prototype.getNext = function () { return ++this.counter; }; ResultId.prototype.reset = function () { this.counter = 0; }; return ResultId; }()); function teamElement(roundNumber, match, team, opponent, isReady, isFirstBracket, opts, resultId, topCon, renderAll) { var resultIdAttribute = team.name.isEmpty() || opponent.name.isEmpty() ? '' : "data-resultid=\"result-" + resultId.getNext() + "\""; var sEl = $("
"); var score = (team.name.isEmpty() || opponent.name.isEmpty() || !isReady) ? Option.empty() : team.score.map(function (s) { return "" + s; }); var scoreString = score.orElse('--'); sEl.text(scoreString); var entryState = team.name .map(function () { return score .map(function () { return 'entry-complete'; }) .orElseGet(function () { return (opponent.emptyBranch() === BranchType.BYE) ? 'entry-default-win' : 'entry-no-score'; }); }) .orElseGet(function () { var type = team.emptyBranch(); switch (type) { case BranchType.BYE: return 'empty-bye'; case BranchType.TBD: return 'empty-tbd'; default: throw new Error("Unexpected branch type " + type); } }); var tEl = $("
"); var nEl = $("
").appendTo(tEl); opts.decorator.render(nEl, team.name.toNull(), scoreString, entryState); team.seed.forEach(function (seed) { tEl.attr('data-teamid', seed); }); if (team.name.isEmpty()) { tEl.addClass('na'); } else if (match.winner().name === team.name) { tEl.addClass('win'); } else if (match.loser().name === team.name) { tEl.addClass('lose'); } tEl.append(sEl); // Only first round of BYEs can be edited if ((!team.name.isEmpty() || (team.name.isEmpty() && roundNumber === 0 && isFirstBracket)) && typeof (opts.save) === 'function') { if (!opts.disableTeamEdit) { nEl.addClass('editable'); nEl.click(function () { var span = $(this); function editor() { function done_fn(val, next) { // Needs to be taken before possible null is assigned below var teamId = team.seed.get(); opts.init.teams[~~(teamId / 2)][teamId % 2] = Option.of(val || null); renderAll(true); span.click(editor); var labels = opts.el.find('.team[data-teamid=' + (teamId + 1) + '] div.label:first'); if (labels.length && next === true && roundNumber === 0) { $(labels).click(); } } span.unbind(); opts.decorator.edit(span, team.name.toNull(), done_fn); } editor(); }); } if (!team.name.isEmpty() && !opponent.name.isEmpty() && isReady) { var rId_1 = resultId.get(); sEl.addClass('editable'); sEl.click(function () { var span = $(this); function editor() { span.unbind(); var score = !isNumber(team.score) ? '0' : span.text(); var input = $(''); input.val(score); span.empty().append(input); input.focus().select(); input.keydown(function (e) { if (!isNumber($(this).val())) { $(this).addClass('error'); } else { $(this).removeClass('error'); } var key = (e.keyCode || e.which); if (key === 9 || key === 13 || key === 27) { e.preventDefault(); $(this).blur(); if (key === 27) { return; } var next = topCon.find('div.score[data-resultid=result-' + (rId_1 + 1) + ']'); if (next) { next.click(); } } }); input.blur(function () { var val = input.val(); if ((!val || !isNumber(val)) && !isNumber(team.score)) { val = '0'; } else if ((!val || !isNumber(val)) && isNumber(team.score)) { val = team.score; } span.html(val); if (isNumber(val)) { team.score = Score.of(parseInt(val, 10)); renderAll(true); } span.click(editor); }); } editor(); }); } } return tEl; } var Match = (function () { function Match(round, match, seed, results, renderCb, isFirstBracket, opts, resultId, topCon, renderAll) { this.round = round; this.match = match; this.seed = seed; this.renderCb = renderCb; this.isFirstBracket = isFirstBracket; this.opts = opts; this.resultId = resultId; this.topCon = topCon; this.renderAll = renderAll; this.connectorCb = Option.empty(); this.matchCon = $('
'); this.teamCon = $('
'); this.alignCb = null; this.matchUserData = !results.isEmpty() ? results.get().userData : undefined; if (!opts.save) { // The hover and click callbacks are bound by jQuery to the element var userData_1 = this.matchUserData; if (opts.onMatchHover) { this.teamCon.hover(function () { opts.onMatchHover(userData_1, true); }, function () { opts.onMatchHover(userData_1, false); }); } if (opts.onMatchClick) { this.teamCon.click(function () { opts.onMatchClick(userData_1); }); } } match.a.name = match.a.source().name; match.b.name = match.b.source().name; match.a.score = results.map(function (r) { return r.first.toNull(); }); match.b.score = results.map(function (r) { return r.second.toNull(); }); /* match has score even though teams haven't yet been decided */ /* todo: would be nice to have in preload check, maybe too much work */ if ((!match.a.name || !match.b.name) && (isNumber(match.a.score) || isNumber(match.b.score))) { console.log('ERROR IN SCORE DATA: ' + match.a.source().name + ': ' + match.a.score + ', ' + match.b.source().name + ': ' + match.b.score); match.a.score = match.b.score = Score.empty(); } } Object.defineProperty(Match.prototype, "el", { get: function () { return this.matchCon; }, enumerable: true, configurable: true }); Match.prototype.getRound = function () { return this.round; }; Match.prototype.setConnectorCb = function (cb) { this.connectorCb = cb; }; Match.prototype.connect = function (cb) { var _this = this; var align = this.opts.dir === 'lr' ? 'right' : 'left'; var connectorOffset = this.teamCon.height() / 4; var matchupOffset = this.matchCon.height() / 2; var result = cb .map(function (connectorCb) { return connectorCb(_this.teamCon, _this); }) .orElseGet(function () { if (_this.seed % 2 === 0) { return _this.winner().order .map(function (order) { return order.map({ shift: connectorOffset * (_this.opts.centerConnectors ? 2 : 1), height: matchupOffset }, { shift: connectorOffset * (_this.opts.centerConnectors ? 2 : 3), height: matchupOffset - connectorOffset * (_this.opts.centerConnectors ? 0 : 2) }); }) .orElse({ shift: connectorOffset * 2, height: matchupOffset - connectorOffset * (_this.opts.centerConnectors ? 0 : 1) }); } else { return _this.winner().order .map(function (order) { return order.map({ shift: -connectorOffset * (_this.opts.centerConnectors ? 2 : 3), height: -matchupOffset + connectorOffset * (_this.opts.centerConnectors ? 0 : 2) }, { shift: -connectorOffset * (_this.opts.centerConnectors ? 2 : 1), height: -matchupOffset }); }) .orElse({ shift: -connectorOffset * 2, height: -matchupOffset + connectorOffset * (_this.opts.centerConnectors ? 0 : 1) }); } }); this.teamCon.append(connector(this.opts.roundMargin, result, this.teamCon, align)); }; Match.prototype.winner = function () { return this.match.winner(); }; Match.prototype.loser = function () { return this.match.loser(); }; Match.prototype.first = function () { return this.match.a; }; Match.prototype.second = function () { return this.match.b; }; Match.prototype.setAlignCb = function (cb) { this.alignCb = cb; }; Match.prototype.render = function () { var _this = this; this.matchCon.empty(); this.teamCon.empty(); // This shouldn't be done at render-time this.match.a.name = this.match.a.source().name; this.match.b.name = this.match.b.source().name; this.match.a.seed = this.match.a.source().seed; this.match.b.seed = this.match.b.source().seed; var isDoubleBye = this.match.a.name.isEmpty() && this.match.b.name.isEmpty(); if (isDoubleBye) { this.teamCon.addClass('np'); } else if (!this.match.winner().name) { this.teamCon.addClass('np'); } else { this.teamCon.removeClass('np'); } // Coerce truthy/falsy "isset()" for Typescript var isReady = !this.match.a.name.isEmpty() && !this.match.b.name.isEmpty(); this.teamCon.append(teamElement(this.round.roundNumber, this.match, this.match.a, this.match.b, isReady, this.isFirstBracket, this.opts, this.resultId, this.topCon, this.renderAll)); this.teamCon.append(teamElement(this.round.roundNumber, this.match, this.match.b, this.match.a, isReady, this.isFirstBracket, this.opts, this.resultId, this.topCon, this.renderAll)); this.matchCon.appendTo(this.round.el); this.matchCon.append(this.teamCon); this.el.css('height', (this.round.bracket.el.height() / this.round.size()) + 'px'); this.teamCon.css('top', (this.el.height() / 2 - this.teamCon.height() / 2) + 'px'); /* todo: move to class */ if (this.alignCb !== null) { this.alignCb(this.teamCon); } var isLast = this.renderCb.map(function (cb) { return cb(_this); }).orElse(false); if (!isLast) { this.connect(this.connectorCb); } }; Match.prototype.results = function () { // Either team is bye -> reset (mutate) scores from that match var hasBye = this.match.a.name.isEmpty() || this.match.b.name.isEmpty(); if (hasBye) { this.match.a.score = this.match.b.score = Score.empty(); } return new ResultObject(this.match.a.score, this.match.b.score, this.matchUserData); }; return Match; }()); var undefinedToNull = function (value) { return value === undefined ? null : value; }; var wrapResults = function (initResults) { return initResults .map(function (brackets) { return brackets .map(function (rounds) { return rounds .map(function (matches) { return new ResultObject(Score.of(undefinedToNull(matches[0])), Score.of(undefinedToNull(matches[1])), matches[2]); }); }); }); }; var JqueryBracket = function (opts) { var resultId = new ResultId(); var data = opts.init; var isSingleElimination = (data.results.length <= 1); // 45 === team height x2 + 1px margin var height = data.teams.length * 45 + data.teams.length * opts.matchMargin; var topCon = $('
').appendTo(opts.el.empty()); function resizeContainer() { var roundCount = countRounds(data.teams.length, isSingleElimination, opts.skipGrandFinalComeback, opts.skipSecondaryFinal, data.results); if (!opts.disableToolbar) { topCon.css('width', roundCount * (opts.teamWidth + opts.scoreWidth + opts.roundMargin) + 40); } else { topCon.css('width', roundCount * (opts.teamWidth + opts.scoreWidth + opts.roundMargin) + 10); } // reserve space for consolation round if (isSingleElimination && data.teams.length <= 2 && !opts.skipConsolationRound) { topCon.css('height', height + 40); } } var w, l, f; function renderAll(save) { resultId.reset(); w.render(); if (l) { l.render(); } if (f && !opts.skipGrandFinalComeback) { f.render(); } if (!opts.disableHighlight) { postProcess(topCon, w, f); } if (save) { data.results[0] = w.results(); if (l) { data.results[1] = l.results(); } if (f && !opts.skipGrandFinalComeback) { data.results[2] = f.results(); } // Loser bracket comeback in finals might require a new round resizeContainer(); if (opts.save) { opts.save(exportData(data), opts.userData); } } } if (opts.skipSecondaryFinal && isSingleElimination) { $.error('skipSecondaryFinal setting is viable only in double elimination mode'); } if (!opts.disableToolbar) { embedEditButtons(topCon, data, opts); } var fEl, wEl, lEl; if (isSingleElimination) { wEl = $('
').appendTo(topCon); } else { if (!opts.skipGrandFinalComeback) { fEl = $('
').appendTo(topCon); } wEl = $('
').appendTo(topCon); lEl = $('
').appendTo(topCon); } wEl.css('height', height); if (lEl) { lEl.css('height', wEl.height() / 2); } resizeContainer(); var mkMatch = function (round, match, seed, results, renderCb, isFirstBracket, opts) { return new Match(round, match, seed, results, renderCb, isFirstBracket, opts, resultId, topCon, renderAll); }; w = new Bracket(wEl, Option.of(data.results[0] || null), mkMatch, true, opts); if (!isSingleElimination) { l = new Bracket(lEl, Option.of(data.results[1] || null), mkMatch, false, opts); if (!opts.skipGrandFinalComeback) { f = new Bracket(fEl, Option.of(data.results[2] || null), mkMatch, false, opts); } } prepareWinners(w, data.teams, isSingleElimination, opts, opts.skipGrandFinalComeback && !isSingleElimination); if (!isSingleElimination) { prepareLosers(w, l, data.teams.length, opts.skipGrandFinalComeback, opts.centerConnectors); if (!opts.skipGrandFinalComeback) { prepareFinals(f, w, l, opts, topCon, resizeContainer); } } renderAll(false); // LBEdit: I NEEED this return { data: function () { return exportData(opts.init); } }; }; function embedEditButtons(topCon, data, opts) { var tools = $('
').appendTo(topCon); var inc = $('+').appendTo(tools); inc.click(function () { var len = data.teams.length; for (var i = 0; i < len; i += 1) { data.teams.push([Option.empty(), Option.empty()]); } return JqueryBracket(opts); }); if (data.teams.length > 1 && data.results.length === 1 || data.teams.length > 2 && data.results.length === 3) { var dec = $('-').appendTo(tools); dec.click(function () { if (data.teams.length > 1) { data.teams = data.teams.slice(0, data.teams.length / 2); return JqueryBracket(opts); } }); } if (data.results.length === 1 && data.teams.length > 1) { var type = $('de').appendTo(tools); type.click(function () { if (data.teams.length > 1 && data.results.length < 3) { data.results.push([], []); return JqueryBracket(opts); } }); } else if (data.results.length === 3 && data.teams.length > 1) { var type = $('se').appendTo(tools); type.click(function () { if (data.results.length === 3) { data.results = data.results.slice(0, 1); return JqueryBracket(opts); } }); } } var assertNumber = function (opts, field) { if (opts.hasOwnProperty(field)) { var expectedType = 'number'; var type = typeof (opts[field]); if (type !== expectedType) { throw new Error("Option \"" + field + "\" is " + type + " instead of " + expectedType); } } }; var assertBoolean = function (opts, field) { var value = opts[field]; var expectedType = 'boolean'; var type = typeof (value); if (type !== expectedType) { throw new Error("Value of " + field + " must be boolean, got " + expectedType + ", got " + type); } }; var assertGt = function (expected, opts, field) { var value = opts[field]; if (value < expected) { throw new Error("Value of " + field + " must be greater than " + expected + ", got " + value); } }; var isPow2 = function (x) { return (x & (x - 1)); }; var methods = { init: function (originalOpts) { var opts = $.extend(true, {}, originalOpts); // Do not mutate inputs if (!opts) { throw Error('Options not set'); } if (!opts.init && !opts.save) { throw Error('No bracket data or save callback given'); } if (opts.userData === undefined) { opts.userData = null; } if (opts.decorator && (!opts.decorator.edit || !opts.decorator.render)) { throw Error('Invalid decorator input'); } else if (!opts.decorator) { opts.decorator = { edit: defaultEdit, render: defaultRender }; } if (!opts.init) { opts.init = { teams: [ [Option.empty(), Option.empty()] ], results: [] }; } var that = this; opts.el = this; if (opts.save && (opts.onMatchClick || opts.onMatchHover)) { $.error('Match callbacks may not be passed in edit mode (in conjunction with save callback)'); } var disableToolbarType = typeof (opts.disableToolbar); var disableToolbarGiven = opts.hasOwnProperty('disableToolbar'); if (disableToolbarGiven && disableToolbarType !== 'boolean') { $.error("disableToolbar must be a boolean, got " + disableToolbarType); } if (!opts.save && disableToolbarGiven) { $.error('disableToolbar can be used only if the bracket is editable, i.e. "save" callback given'); } if (!disableToolbarGiven) { opts.disableToolbar = (opts.save === undefined); } var disableTeamEditType = typeof (opts.disableTeamEdit); var disableTeamEditGiven = opts.hasOwnProperty('disableTeamEdit'); if (disableTeamEditGiven && disableTeamEditType !== 'boolean') { $.error("disableTeamEdit must be a boolean, got " + disableTeamEditType); } if (!opts.save && disableTeamEditGiven) { $.error('disableTeamEdit can be used only if the bracket is editable, i.e. "save" callback given'); } if (!disableTeamEditGiven) { opts.disableTeamEdit = false; } if (!opts.disableToolbar && opts.disableTeamEdit) { $.error('disableTeamEdit requires also resizing to be disabled, initialize with "disableToolbar: true"'); } /* wrap data to into necessary arrays */ var r = wrap(opts.init.results, 4 - depth(opts.init.results)); opts.init.results = wrapResults(r); assertNumber(opts, 'teamWidth'); assertNumber(opts, 'scoreWidth'); assertNumber(opts, 'roundMargin'); assertNumber(opts, 'matchMargin'); if (!opts.hasOwnProperty('teamWidth')) { opts.teamWidth = 70; } if (!opts.hasOwnProperty('scoreWidth')) { opts.scoreWidth = 30; } if (!opts.hasOwnProperty('roundMargin')) { opts.roundMargin = 40; } if (!opts.hasOwnProperty('matchMargin')) { opts.matchMargin = 20; } assertGt(0, opts, 'teamWidth'); assertGt(0, opts, 'scoreWidth'); assertGt(0, opts, 'roundMargin'); assertGt(0, opts, 'matchMargin'); if (!opts.hasOwnProperty('centerConnectors')) { opts.centerConnectors = false; } assertBoolean(opts, 'centerConnectors'); if (!opts.hasOwnProperty('disableHighlight')) { opts.disableHighlight = false; } assertBoolean(opts, 'disableHighlight'); var log2Result = isPow2(opts.init.teams.length); if (log2Result !== Math.floor(log2Result)) { $.error("\"teams\" property must have 2^n number of team pairs, i.e. 1, 2, 4, etc. Got " + opts.init.teams.length + " team pairs."); } opts.dir = opts.dir || 'lr'; opts.init.teams = !opts.init.teams || opts.init.teams.length === 0 ? [[null, null]] : opts.init.teams; opts.init.teams = opts.init.teams.map(function (ts) { return ts.map(function (t) { return t === null ? Option.empty() : Option.of(t); }); }); opts.skipConsolationRound = opts.skipConsolationRound || false; opts.skipSecondaryFinal = opts.skipSecondaryFinal || false; if (opts.dir !== 'lr' && opts.dir !== 'rl') { $.error('Direction must be either: "lr" or "rl"'); } var bracket = JqueryBracket(opts); $(this).data('bracket', { target: that, obj: bracket }); return bracket; }, data: function () { var bracket = $(this).data('bracket'); return bracket.obj.data(); } }; $.fn.bracket = function (method) { if (methods[method]) { return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); } else if (typeof method === 'object' || !method) { return methods.init.apply(this, arguments); } else { $.error('Method ' + method + ' does not exist on jQuery.bracket'); } }; })(jQuery);