| 1 |
( function () { |
| 2 |
var el = wp.element.createElement; |
| 3 |
var Fragment = wp.element.Fragment; |
| 4 |
var useState = wp.element.useState; |
| 5 |
var registerBlockType = wp.blocks.registerBlockType; |
| 6 |
var useBlockProps = wp.blockEditor.useBlockProps; |
| 7 |
var InspectorControls = wp.blockEditor.InspectorControls; |
| 8 |
var PanelColorSettings = wp.blockEditor.PanelColorSettings; |
| 9 |
var PanelBody = wp.components.PanelBody; |
| 10 |
var TextControl = wp.components.TextControl; |
| 11 |
var ToggleControl = wp.components.ToggleControl; |
| 12 |
var RangeControl = wp.components.RangeControl; |
| 13 |
var SelectControl = wp.components.SelectControl; |
| 14 |
var Button = wp.components.Button; |
| 15 |
var __ = wp.i18n.__; |
| 16 |
|
| 17 |
function getTypographyControl() { |
| 18 |
return (window.bkbgTypographyControl || function () { return null; }); |
| 19 |
} |
| 20 |
|
| 21 |
function _tv(typo, prefix) { |
| 22 |
if (!typo) return {}; |
| 23 |
var s = {}; |
| 24 |
if (typo.family) s[prefix + 'font-family'] = "'" + typo.family + "', sans-serif"; |
| 25 |
if (typo.weight) s[prefix + 'font-weight'] = typo.weight; |
| 26 |
if (typo.transform) s[prefix + 'text-transform'] = typo.transform; |
| 27 |
if (typo.style) s[prefix + 'font-style'] = typo.style; |
| 28 |
if (typo.decoration) s[prefix + 'text-decoration'] = typo.decoration; |
| 29 |
var su = typo.sizeUnit || 'px'; |
| 30 |
if (typo.sizeDesktop !== '' && typo.sizeDesktop != null) s[prefix + 'font-size-d'] = typo.sizeDesktop + su; |
| 31 |
if (typo.sizeTablet !== '' && typo.sizeTablet != null) s[prefix + 'font-size-t'] = typo.sizeTablet + su; |
| 32 |
if (typo.sizeMobile !== '' && typo.sizeMobile != null) s[prefix + 'font-size-m'] = typo.sizeMobile + su; |
| 33 |
var lhu = typo.lineHeightUnit || ''; |
| 34 |
if (typo.lineHeightDesktop !== '' && typo.lineHeightDesktop != null) s[prefix + 'line-height-d'] = typo.lineHeightDesktop + lhu; |
| 35 |
if (typo.lineHeightTablet !== '' && typo.lineHeightTablet != null) s[prefix + 'line-height-t'] = typo.lineHeightTablet + lhu; |
| 36 |
if (typo.lineHeightMobile !== '' && typo.lineHeightMobile != null) s[prefix + 'line-height-m'] = typo.lineHeightMobile + lhu; |
| 37 |
var lsu = typo.letterSpacingUnit || 'px'; |
| 38 |
if (typo.letterSpacingDesktop !== '' && typo.letterSpacingDesktop != null) s[prefix + 'letter-spacing-d'] = typo.letterSpacingDesktop + lsu; |
| 39 |
if (typo.letterSpacingTablet !== '' && typo.letterSpacingTablet != null) s[prefix + 'letter-spacing-t'] = typo.letterSpacingTablet + lsu; |
| 40 |
if (typo.letterSpacingMobile !== '' && typo.letterSpacingMobile != null) s[prefix + 'letter-spacing-m'] = typo.letterSpacingMobile + lsu; |
| 41 |
var wsu = typo.wordSpacingUnit || 'px'; |
| 42 |
if (typo.wordSpacingDesktop !== '' && typo.wordSpacingDesktop != null) s[prefix + 'word-spacing-d'] = typo.wordSpacingDesktop + wsu; |
| 43 |
if (typo.wordSpacingTablet !== '' && typo.wordSpacingTablet != null) s[prefix + 'word-spacing-t'] = typo.wordSpacingTablet + wsu; |
| 44 |
if (typo.wordSpacingMobile !== '' && typo.wordSpacingMobile != null) s[prefix + 'word-spacing-m'] = typo.wordSpacingMobile + wsu; |
| 45 |
return s; |
| 46 |
} |
| 47 |
|
| 48 |
// ── bracket logic ──────────────────────────────────────────────────────────── |
| 49 |
function getRounds(bracketSize) { |
| 50 |
var rounds = []; |
| 51 |
var n = bracketSize; |
| 52 |
while (n >= 2) { rounds.push(n / 2); n = n / 2; } |
| 53 |
return rounds; // e.g. [4, 2, 1] for bracketSize=8 |
| 54 |
} |
| 55 |
|
| 56 |
function getMatchKey(r, m) { return 'r' + r + '_m' + m; } |
| 57 |
|
| 58 |
function getMatchTeams(r, m, teams, winners) { |
| 59 |
if (r === 0) { |
| 60 |
return [teams[m * 2] || '?', teams[m * 2 + 1] || '?']; |
| 61 |
} |
| 62 |
var t1key = getMatchKey(r - 1, m * 2); |
| 63 |
var t2key = getMatchKey(r - 1, m * 2 + 1); |
| 64 |
return [winners[t1key] || '?', winners[t2key] || '?']; |
| 65 |
} |
| 66 |
|
| 67 |
function setWinner(winners, r, m, team) { |
| 68 |
var newWinners = Object.assign({}, winners); |
| 69 |
var key = getMatchKey(r, m); |
| 70 |
// If winner changes, clear downstream |
| 71 |
if (newWinners[key] !== team) { |
| 72 |
newWinners[key] = team; |
| 73 |
// Clear all keys that depended on this match |
| 74 |
clearDownstream(newWinners, r, m); |
| 75 |
} |
| 76 |
return newWinners; |
| 77 |
} |
| 78 |
|
| 79 |
function clearDownstream(winners, r, m) { |
| 80 |
// The result of (r, m) feeds into (r+1, Math.floor(m/2)) |
| 81 |
var nextR = r + 1; |
| 82 |
var nextM = Math.floor(m / 2); |
| 83 |
var nextKey = getMatchKey(nextR, nextM); |
| 84 |
// Only clear if there's a winner set downstream |
| 85 |
if (winners[nextKey]) { |
| 86 |
delete winners[nextKey]; |
| 87 |
clearDownstream(winners, nextR, nextM); |
| 88 |
} |
| 89 |
} |
| 90 |
|
| 91 |
// ── Match component ────────────────────────────────────────────────────────── |
| 92 |
function Match(props) { |
| 93 |
var a = props.a; |
| 94 |
var roundIdx = props.roundIdx; |
| 95 |
var matchIdx = props.matchIdx; |
| 96 |
var teams = props.teams; |
| 97 |
var winners = props.winners; |
| 98 |
var setAttributes = props.setAttributes; |
| 99 |
var isLast = props.isLast; // last match in last round |
| 100 |
|
| 101 |
var matchTeams = getMatchTeams(roundIdx, matchIdx, teams, winners); |
| 102 |
var winnerKey = getMatchKey(roundIdx, matchIdx); |
| 103 |
var currentWinner = winners[winnerKey] || null; |
| 104 |
|
| 105 |
function pickWinner(team) { |
| 106 |
if (team === '?') return; |
| 107 |
var newWinners = setWinner(Object.assign({}, winners), roundIdx, matchIdx, team); |
| 108 |
setAttributes({ winners: newWinners }); |
| 109 |
} |
| 110 |
|
| 111 |
return el('div', { className: 'bkbg-bk-match', style: { borderRadius: a.borderRadius + 'px', borderColor: a.matchBorderColor } }, |
| 112 |
matchTeams.map(function (team, ti) { |
| 113 |
var isWinner = currentWinner === team && team !== '?'; |
| 114 |
var isLoser = currentWinner && currentWinner !== team && team !== '?'; |
| 115 |
var available = team !== '?'; |
| 116 |
|
| 117 |
return el('div', { |
| 118 |
key: ti, |
| 119 |
className: 'bkbg-bk-team' + (isWinner ? ' is-winner' : '') + (isLoser ? ' is-loser' : '') + (available ? ' is-available' : ''), |
| 120 |
style: { |
| 121 |
background: isWinner ? a.winnerBg : a.matchBg, |
| 122 |
color: isWinner ? a.winnerColor : (isLoser ? a.loserColor : a.teamColor), |
| 123 |
borderColor: isWinner ? a.winnerBorderColor : 'transparent', |
| 124 |
cursor: available ? 'pointer' : 'default' |
| 125 |
}, |
| 126 |
onClick: function () { if (available) pickWinner(team); }, |
| 127 |
title: available ? __('Click to set as winner') : '' |
| 128 |
}, |
| 129 |
isWinner && el('span', { className: 'bkbg-bk-win-icon' }, '� |
| 130 |
'), |
| 131 |
el('span', { className: 'bkbg-bk-team-name' }, team) |
| 132 |
); |
| 133 |
}) |
| 134 |
); |
| 135 |
} |
| 136 |
|
| 137 |
// ── BracketPreview ─────────────────────────────────────────────────────────── |
| 138 |
function BracketPreview(props) { |
| 139 |
var a = props.a; |
| 140 |
var setAttributes = props.setAttributes; |
| 141 |
|
| 142 |
var rounds = getRounds(a.bracketSize); |
| 143 |
var numRounds = rounds.length; |
| 144 |
|
| 145 |
// Default round labels: expand if needed |
| 146 |
var defaultLabels = ['Round of ' + a.bracketSize, 'Quarterfinals', 'Semifinals', 'Final']; |
| 147 |
function getRoundLabel(i) { |
| 148 |
if (a.roundLabels && a.roundLabels[i] !== undefined) return a.roundLabels[i]; |
| 149 |
if (i === numRounds - 1) return 'Final'; |
| 150 |
if (i === numRounds - 2) return 'Semifinals'; |
| 151 |
return 'Round ' + (i + 1); |
| 152 |
} |
| 153 |
|
| 154 |
// Champion |
| 155 |
var finalMatchKey = getMatchKey(numRounds - 1, 0); |
| 156 |
var champion = a.winners[finalMatchKey] || null; |
| 157 |
|
| 158 |
return el('div', { className: 'bkbg-bk-block', style: { background: a.bgColor, borderRadius: a.borderRadius + 'px' } }, |
| 159 |
a.showTitle && el('h3', { |
| 160 |
className: 'bkbg-bk-title', |
| 161 |
style: { color: a.titleColor }, |
| 162 |
contentEditable: true, |
| 163 |
suppressContentEditableWarning: true, |
| 164 |
onBlur: function (e) { setAttributes({ bracketTitle: e.target.textContent }); } |
| 165 |
}, a.bracketTitle), |
| 166 |
el('div', { className: 'bkbg-bk-grid' }, |
| 167 |
rounds.map(function (matchCount, ri) { |
| 168 |
return el('div', { key: ri, className: 'bkbg-bk-round' }, |
| 169 |
a.showRoundLabels && el('div', { className: 'bkbg-bk-round-label', style: { color: a.roundLabelColor } }, |
| 170 |
getRoundLabel(ri) |
| 171 |
), |
| 172 |
el('div', { className: 'bkbg-bk-matches' }, |
| 173 |
Array.from({ length: matchCount }, function (_, mi) { |
| 174 |
return el(Match, { |
| 175 |
key: mi, |
| 176 |
a: a, |
| 177 |
roundIdx: ri, |
| 178 |
matchIdx: mi, |
| 179 |
teams: a.teams, |
| 180 |
winners: a.winners, |
| 181 |
setAttributes: setAttributes |
| 182 |
}); |
| 183 |
}) |
| 184 |
) |
| 185 |
); |
| 186 |
}), |
| 187 |
// Champion column |
| 188 |
el('div', { className: 'bkbg-bk-round bkbg-bk-champion-col' }, |
| 189 |
a.showRoundLabels && el('div', { className: 'bkbg-bk-round-label', style: { color: a.roundLabelColor } }, ''), |
| 190 |
el('div', { className: 'bkbg-bk-matches' }, |
| 191 |
el('div', { |
| 192 |
className: 'bkbg-bk-champion', |
| 193 |
style: { |
| 194 |
background: a.championBg, |
| 195 |
borderColor: a.championBorderColor, |
| 196 |
color: a.championColor, |
| 197 |
borderRadius: a.borderRadius + 'px' |
| 198 |
} |
| 199 |
}, |
| 200 |
el('div', { className: 'bkbg-bk-champion-label' }, a.winnerLabel), |
| 201 |
el('div', { className: 'bkbg-bk-champion-name' }, champion || '—') |
| 202 |
) |
| 203 |
) |
| 204 |
) |
| 205 |
) |
| 206 |
); |
| 207 |
} |
| 208 |
|
| 209 |
// ── register ───────────────────────────────────────────────────────────────── |
| 210 |
registerBlockType('blockenberg/bracket', { |
| 211 |
edit: function (props) { |
| 212 |
var a = props.attributes; |
| 213 |
var setAttributes = props.setAttributes; |
| 214 |
|
| 215 |
function updateTeam(idx, val) { |
| 216 |
var newTeams = a.teams.slice(); |
| 217 |
newTeams[idx] = val; |
| 218 |
setAttributes({ teams: newTeams }); |
| 219 |
} |
| 220 |
|
| 221 |
function updateRoundLabel(idx, val) { |
| 222 |
var newLabels = (a.roundLabels || []).slice(); |
| 223 |
while (newLabels.length <= idx) newLabels.push(''); |
| 224 |
newLabels[idx] = val; |
| 225 |
setAttributes({ roundLabels: newLabels }); |
| 226 |
} |
| 227 |
|
| 228 |
var rounds = getRounds(a.bracketSize); |
| 229 |
|
| 230 |
return el(Fragment, null, |
| 231 |
el(InspectorControls, null, |
| 232 |
el(PanelBody, { title: __('Bracket Setup'), initialOpen: true }, |
| 233 |
el(SelectControl, { |
| 234 |
label: __('Bracket size'), |
| 235 |
value: String(a.bracketSize), |
| 236 |
__nextHasNoMarginBottom: true, |
| 237 |
options: [ |
| 238 |
{ value: '4', label: '4 Teams (2 rounds)' }, |
| 239 |
{ value: '8', label: '8 Teams (3 rounds)' }, |
| 240 |
{ value: '16', label: '16 Teams (4 rounds)' } |
| 241 |
], |
| 242 |
onChange: function (v) { |
| 243 |
var size = parseInt(v, 10); |
| 244 |
var newTeams = Array.from({ length: size }, function (_, i) { |
| 245 |
return a.teams[i] || 'Team ' + (i + 1); |
| 246 |
}); |
| 247 |
setAttributes({ bracketSize: size, teams: newTeams, winners: {} }); |
| 248 |
} |
| 249 |
}), |
| 250 |
el(Button, { |
| 251 |
variant: 'secondary', |
| 252 |
isSmall: true, |
| 253 |
onClick: function () { setAttributes({ winners: {} }); }, |
| 254 |
style: { marginTop: '6px' } |
| 255 |
}, __('Reset all results')) |
| 256 |
), |
| 257 |
el(PanelBody, { title: __('Teams'), initialOpen: true }, |
| 258 |
a.teams.map(function (team, i) { |
| 259 |
return el(TextControl, { |
| 260 |
key: i, |
| 261 |
label: __('Team ') + (i + 1), |
| 262 |
value: team, |
| 263 |
__nextHasNoMarginBottom: true, |
| 264 |
onChange: function (v) { updateTeam(i, v); } |
| 265 |
}); |
| 266 |
}) |
| 267 |
), |
| 268 |
el(PanelBody, { title: __('Round Labels'), initialOpen: false }, |
| 269 |
rounds.map(function (_, ri) { |
| 270 |
var def = ri === rounds.length - 1 ? 'Final' : (ri === rounds.length - 2 ? 'Semifinals' : 'Round ' + (ri + 1)); |
| 271 |
return el(TextControl, { |
| 272 |
key: ri, |
| 273 |
label: __('Round ') + (ri + 1), |
| 274 |
value: (a.roundLabels && a.roundLabels[ri]) || def, |
| 275 |
__nextHasNoMarginBottom: true, |
| 276 |
onChange: function (v) { updateRoundLabel(ri, v); } |
| 277 |
}); |
| 278 |
}), |
| 279 |
el(TextControl, { |
| 280 |
label: __('Winner label'), |
| 281 |
value: a.winnerLabel, |
| 282 |
__nextHasNoMarginBottom: true, |
| 283 |
onChange: function (v) { setAttributes({ winnerLabel: v }); } |
| 284 |
}) |
| 285 |
), |
| 286 |
el(PanelBody, { title: __('Display'), initialOpen: false }, |
| 287 |
el(ToggleControl, { |
| 288 |
label: __('Show bracket title'), |
| 289 |
checked: a.showTitle, |
| 290 |
__nextHasNoMarginBottom: true, |
| 291 |
onChange: function (v) { setAttributes({ showTitle: v }); } |
| 292 |
}), |
| 293 |
el(ToggleControl, { |
| 294 |
label: __('Show round labels'), |
| 295 |
checked: a.showRoundLabels, |
| 296 |
__nextHasNoMarginBottom: true, |
| 297 |
onChange: function (v) { setAttributes({ showRoundLabels: v }); } |
| 298 |
}), |
| 299 |
el(RangeControl, { |
| 300 |
label: __('Border radius (px)'), |
| 301 |
value: a.borderRadius, |
| 302 |
min: 0, max: 20, |
| 303 |
__nextHasNoMarginBottom: true, |
| 304 |
onChange: function (v) { setAttributes({ borderRadius: v }); } |
| 305 |
}) |
| 306 |
), |
| 307 |
el(PanelBody, { title: __('Typography', 'blockenberg'), initialOpen: false }, |
| 308 |
el(getTypographyControl(), { label: __('Title', 'blockenberg'), value: a.typoTitle, onChange: function (v) { setAttributes({ typoTitle: v }); } }), |
| 309 |
el(getTypographyControl(), { label: __('Team Name', 'blockenberg'), value: a.typoTeam, onChange: function (v) { setAttributes({ typoTeam: v }); } }), |
| 310 |
el(getTypographyControl(), { label: __('Round Label', 'blockenberg'), value: a.typoLabel, onChange: function (v) { setAttributes({ typoLabel: v }); } }) |
| 311 |
), |
| 312 |
el(PanelColorSettings, { |
| 313 |
title: __('Colors'), |
| 314 |
initialOpen: false, |
| 315 |
colorSettings: [ |
| 316 |
{ label: __('Background'), value: a.bgColor, onChange: function (v) { setAttributes({ bgColor: v || '#f8fafc' }); } }, |
| 317 |
{ label: __('Accent'), value: a.accentColor, onChange: function (v) { setAttributes({ accentColor: v || '#f59e0b' }); } }, |
| 318 |
{ label: __('Title'), value: a.titleColor, onChange: function (v) { setAttributes({ titleColor: v || '#0f172a' }); } }, |
| 319 |
{ label: __('Match border'), value: a.matchBorderColor, onChange: function (v) { setAttributes({ matchBorderColor: v || '#cbd5e1' }); } }, |
| 320 |
{ label: __('Match background'), value: a.matchBg, onChange: function (v) { setAttributes({ matchBg: v || '#ffffff' }); } }, |
| 321 |
{ label: __('Team text'), value: a.teamColor, onChange: function (v) { setAttributes({ teamColor: v || '#334155' }); } }, |
| 322 |
{ label: __('Winner background'), value: a.winnerBg, onChange: function (v) { setAttributes({ winnerBg: v || '#fef9c3' }); } }, |
| 323 |
{ label: __('Winner text'), value: a.winnerColor, onChange: function (v) { setAttributes({ winnerColor: v || '#713f12' }); } }, |
| 324 |
{ label: __('Winner border'), value: a.winnerBorderColor, onChange: function (v) { setAttributes({ winnerBorderColor: v || '#fbbf24' }); } }, |
| 325 |
{ label: __('Loser text'), value: a.loserColor, onChange: function (v) { setAttributes({ loserColor: v || '#cbd5e1' }); } }, |
| 326 |
{ label: __('Connector lines'), value: a.connectorColor, onChange: function (v) { setAttributes({ connectorColor: v || '#cbd5e1' }); } }, |
| 327 |
{ label: __('Champion bg'), value: a.championBg, onChange: function (v) { setAttributes({ championBg: v || '#fffbeb' }); } }, |
| 328 |
{ label: __('Champion border'), value: a.championBorderColor, onChange: function (v) { setAttributes({ championBorderColor: v || '#f59e0b' }); } }, |
| 329 |
{ label: __('Champion text'), value: a.championColor, onChange: function (v) { setAttributes({ championColor: v || '#92400e' }); } } |
| 330 |
] |
| 331 |
}) |
| 332 |
), |
| 333 |
el('div', useBlockProps({ className: 'bkbg-bk-editor-wrap', style: Object.assign({}, _tv(a.typoTitle, '--bkbg-bkt-title-'), _tv(a.typoTeam, '--bkbg-bkt-team-'), _tv(a.typoLabel, '--bkbg-bkt-label-')) }), |
| 334 |
el(BracketPreview, { a: a, setAttributes: setAttributes }) |
| 335 |
) |
| 336 |
); |
| 337 |
}, |
| 338 |
|
| 339 |
save: function (props) { |
| 340 |
return el('div', useBlockProps.save(), |
| 341 |
el('div', { |
| 342 |
className: 'bkbg-bk-app', |
| 343 |
'data-opts': JSON.stringify(props.attributes) |
| 344 |
}) |
| 345 |
); |
| 346 |
} |
| 347 |
}); |
| 348 |
}() ); |
| 349 |
|