@usssa/component-library 1.0.0-beta.9 → 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.
- package/README.md +1 -1
- package/package.json +4 -3
- package/src/components/core/UBracket.vue +697 -167
- package/src/components/core/UBtnIcon.vue +7 -3
- package/src/components/core/UChip.vue +2 -2
- package/src/components/core/UDate.vue +43 -9
- package/src/components/core/UDrawer/UDrawer.vue +6 -0
- package/src/components/core/UDrawer/UDrawerMenuItem.vue +18 -12
- package/src/components/core/UEventCard.vue +47 -14
- package/src/components/core/UExpansionStd.vue +141 -35
- package/src/components/core/UExpansionTableStd.vue +6 -2
- package/src/components/core/UInputAddressLookup.vue +41 -11
- package/src/components/core/UInputPhoneStd.vue +8 -1
- package/src/components/core/UInputTextStd.vue +5 -0
- package/src/components/core/UMatchup.vue +404 -0
- package/src/components/core/UMenuItem.vue +3 -6
- package/src/components/core/UMultiSelectStd.vue +15 -4
- package/src/components/core/USelectStd.vue +21 -2
- package/src/components/core/UTable/UTable.vue +911 -744
- package/src/components/core/UTableStd.vue +175 -74
- package/src/components/core/UTooltip.vue +1 -0
- package/src/components/core/UVenueCard.vue +221 -0
- package/src/components/index.js +4 -0
- package/src/composables/useNotify.js +2 -2
- package/src/css/app.sass +13 -12
- package/src/utils/bracket.json +1498 -312
|
@@ -5,7 +5,7 @@ import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
|
|
|
5
5
|
import { UBtnIcon, UBtnStd, UChip, UMenuItem, USheet } from '../../components'
|
|
6
6
|
import { useScreenType } from '../../composables/useScreenType'
|
|
7
7
|
|
|
8
|
-
const emit = defineEmits(['open-expand'])
|
|
8
|
+
const emit = defineEmits(['open-expand', 'schedule-click'])
|
|
9
9
|
|
|
10
10
|
defineOptions({
|
|
11
11
|
name: 'UBracket',
|
|
@@ -52,10 +52,15 @@ const props = defineProps({
|
|
|
52
52
|
teamSheetApplyLabel: { type: String, default: 'Select Team' },
|
|
53
53
|
teamSheetCancelLabel: { type: String, default: 'Cancel' },
|
|
54
54
|
teamSheetHeading: { type: String, default: 'Select Team' },
|
|
55
|
+
venueSheetHeading: { type: String, default: 'Venue Detail' },
|
|
55
56
|
vGap: {
|
|
56
57
|
type: Number,
|
|
57
58
|
default: 50,
|
|
58
59
|
},
|
|
60
|
+
viewMatchupDetailText: {
|
|
61
|
+
type: String,
|
|
62
|
+
default: 'View Matchup Details',
|
|
63
|
+
},
|
|
59
64
|
zoomInTooltip: {
|
|
60
65
|
type: String,
|
|
61
66
|
default: 'Zoom In',
|
|
@@ -73,6 +78,7 @@ const bracketSides = [
|
|
|
73
78
|
]
|
|
74
79
|
const nodeHeight = 40
|
|
75
80
|
const nodeWidth = 240
|
|
81
|
+
|
|
76
82
|
const self = 'bottom middle'
|
|
77
83
|
const svgWidth = 1400
|
|
78
84
|
|
|
@@ -80,6 +86,12 @@ const activeRound = ref({ roundNo: 1, columnNo: 3 })
|
|
|
80
86
|
const bracketSheet = ref([])
|
|
81
87
|
const currentTransform = ref(d3.zoomIdentity)
|
|
82
88
|
const gRef = ref(null)
|
|
89
|
+
const layoutCache = ref({
|
|
90
|
+
contentWidth: 0,
|
|
91
|
+
contentHeight: 0,
|
|
92
|
+
maxRow: 0,
|
|
93
|
+
numColumns: 0,
|
|
94
|
+
})
|
|
83
95
|
const nodeGridPositions = ref({})
|
|
84
96
|
const openMenu = ref(false)
|
|
85
97
|
const openRoundMenu = ref(false)
|
|
@@ -92,6 +104,7 @@ const tempSelectedTeamId = ref('')
|
|
|
92
104
|
const teamSheet = ref([])
|
|
93
105
|
const tooltipState = ref({ visible: false, x: 0, y: 0, text: '' })
|
|
94
106
|
const zoomRef = ref(null)
|
|
107
|
+
const locationSheet = ref([])
|
|
95
108
|
|
|
96
109
|
const selectedTeamName = computed({
|
|
97
110
|
get() {
|
|
@@ -109,67 +122,62 @@ const isTeamSelected = computed(() => selectedTeamName.value.length > 0)
|
|
|
109
122
|
|
|
110
123
|
// Flatten for rendering logic
|
|
111
124
|
const games = computed(() =>
|
|
112
|
-
|
|
125
|
+
normalizedBracketData.value.round.flatMap((r, i) =>
|
|
113
126
|
r.game.map((m) => ({ ...m, round: r.roundNo }))
|
|
114
127
|
)
|
|
115
128
|
)
|
|
116
129
|
|
|
130
|
+
// Use normalized bracket data for all rendering and layout logic
|
|
131
|
+
const normalizedBracketData = computed(() =>
|
|
132
|
+
normalizeBracketData(props.bracketData)
|
|
133
|
+
)
|
|
134
|
+
|
|
117
135
|
const roundLabelStyles = computed(() => {
|
|
118
136
|
const t = currentTransform.value || d3.zoomIdentity
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
137
|
+
// Group rounds by normalized column number
|
|
138
|
+
const roundsByColumn = normalizedBracketData.value.round.reduce(
|
|
139
|
+
(acc, round) => {
|
|
140
|
+
const col = Number(round.columnNo)
|
|
141
|
+
if (!acc[col]) {
|
|
142
|
+
acc[col] = []
|
|
143
|
+
}
|
|
144
|
+
acc[col].push(round)
|
|
145
|
+
return acc
|
|
146
|
+
},
|
|
147
|
+
{}
|
|
148
|
+
)
|
|
149
|
+
// Get sorted normalized column numbers
|
|
150
|
+
const sortedColumns = Object.keys(roundsByColumn)
|
|
151
|
+
.map(Number)
|
|
152
|
+
.sort((a, b) => a - b)
|
|
133
153
|
const styles = []
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
const columnIndex = sortedColumns.indexOf(col.toString())
|
|
154
|
+
// Generate styles for each round using normalized column
|
|
155
|
+
normalizedBracketData.value.round.forEach((round, idx) => {
|
|
156
|
+
const col = Number(round.columnNo)
|
|
157
|
+
const columnIndex = sortedColumns.indexOf(col)
|
|
139
158
|
const isLastColumn = columnIndex === sortedColumns.length - 1
|
|
140
|
-
|
|
141
159
|
// The base X position is the same for all rounds in the same column
|
|
142
160
|
const baseX = (col - 1) * props.hGap
|
|
143
|
-
|
|
144
|
-
// The width of the label container should be the horizontal gap
|
|
145
161
|
const baseWidth = props.hGap
|
|
146
|
-
|
|
147
162
|
// Apply zoom and pan transformations
|
|
148
163
|
const left = t.applyX(baseX)
|
|
149
164
|
const width = baseWidth * t.k
|
|
150
|
-
|
|
151
165
|
const style = {
|
|
152
166
|
left: left + 'px',
|
|
153
167
|
width: width + 'px',
|
|
154
168
|
}
|
|
155
|
-
|
|
156
|
-
// Add a border to all but the last column
|
|
157
169
|
if (!isLastColumn) {
|
|
158
170
|
style.borderRight = '1px solid #B9E0FF'
|
|
159
171
|
}
|
|
160
|
-
|
|
161
|
-
// Find the original index to push the style in the correct order for the template v-for
|
|
162
|
-
const originalIndex = props.bracketData.round.findIndex((r) => r === round)
|
|
163
|
-
styles[originalIndex] = style
|
|
172
|
+
styles[idx] = style
|
|
164
173
|
})
|
|
165
|
-
|
|
166
174
|
return styles
|
|
167
175
|
})
|
|
168
176
|
|
|
169
|
-
// Compute teams for toolbar selection from bracket data (selectionType: 'Seed')
|
|
177
|
+
// Compute teams for toolbar selection from normalized bracket data (selectionType: 'Seed')
|
|
170
178
|
const toolbarTeams = computed(() => {
|
|
171
179
|
const seedTeams = []
|
|
172
|
-
|
|
180
|
+
normalizedBracketData.value.round.forEach((round) => {
|
|
173
181
|
if (Array.isArray(round.game)) {
|
|
174
182
|
round.game.forEach((game) => {
|
|
175
183
|
if (Array.isArray(game.team)) {
|
|
@@ -200,7 +208,7 @@ const applyBracketSheetSelection = () => {
|
|
|
200
208
|
|
|
201
209
|
const applyRoundSheetSelection = () => {
|
|
202
210
|
const selectedRound = props.bracketData.round.find(
|
|
203
|
-
(r) => `${r.roundNo}-${r.
|
|
211
|
+
(r) => `${r.roundNo}-${r.roundName}` === tempSelectedRoundId.value
|
|
204
212
|
)
|
|
205
213
|
if (selectedRound) scrollToRound(selectedRound)
|
|
206
214
|
roundSheet.value = []
|
|
@@ -209,6 +217,7 @@ const applyRoundSheetSelection = () => {
|
|
|
209
217
|
const applyTeamSheetSelection = () => {
|
|
210
218
|
selectedTeamId.value = tempSelectedTeamId.value
|
|
211
219
|
teamSheet.value = []
|
|
220
|
+
resetZoom()
|
|
212
221
|
renderBracket()
|
|
213
222
|
}
|
|
214
223
|
|
|
@@ -225,13 +234,12 @@ const cancelTeamSheetSelection = () => {
|
|
|
225
234
|
tempSelectedTeamId.value = selectedTeamId.value
|
|
226
235
|
}
|
|
227
236
|
|
|
228
|
-
// Compute all node positions
|
|
237
|
+
// Compute all node positions and cache layout values
|
|
229
238
|
const computeBracketLayout = () => {
|
|
230
239
|
const newPositions = {}
|
|
231
240
|
|
|
232
241
|
// Scan through all games to find every node with an explicit position
|
|
233
242
|
games.value.forEach((game) => {
|
|
234
|
-
// 1. Position the source teams for the game
|
|
235
243
|
game.team.forEach((team) => {
|
|
236
244
|
if (typeof team === 'object' && team.teamName && team.position) {
|
|
237
245
|
const compositeId = `${team.teamName}_${team.position.columnNo}_${team.position.rowNo}`
|
|
@@ -241,8 +249,6 @@ const computeBracketLayout = () => {
|
|
|
241
249
|
}
|
|
242
250
|
}
|
|
243
251
|
})
|
|
244
|
-
|
|
245
|
-
// 2. Position the destination (winner) node for the game
|
|
246
252
|
if (
|
|
247
253
|
game.winnerDestination &&
|
|
248
254
|
typeof game.winnerDestination === 'object' &&
|
|
@@ -261,8 +267,6 @@ const computeBracketLayout = () => {
|
|
|
261
267
|
}
|
|
262
268
|
}
|
|
263
269
|
}
|
|
264
|
-
|
|
265
|
-
// 3. Position the loser destination node for the game
|
|
266
270
|
if (
|
|
267
271
|
game.loserDestination &&
|
|
268
272
|
typeof game.loserDestination === 'object' &&
|
|
@@ -282,8 +286,6 @@ const computeBracketLayout = () => {
|
|
|
282
286
|
}
|
|
283
287
|
}
|
|
284
288
|
})
|
|
285
|
-
|
|
286
|
-
// Add winnerPlace and loserPlace nodes from each round
|
|
287
289
|
props.bracketData.round.forEach((round) => {
|
|
288
290
|
if (Array.isArray(round.winnerPlace)) {
|
|
289
291
|
round.winnerPlace.forEach((place) => {
|
|
@@ -308,8 +310,23 @@ const computeBracketLayout = () => {
|
|
|
308
310
|
})
|
|
309
311
|
}
|
|
310
312
|
})
|
|
311
|
-
|
|
312
313
|
nodeGridPositions.value = newPositions
|
|
314
|
+
|
|
315
|
+
// Cache layout values
|
|
316
|
+
const uniqueColumns = [
|
|
317
|
+
...new Set(props.bracketData.round.map((r) => r.columnNo)),
|
|
318
|
+
]
|
|
319
|
+
const numColumns = uniqueColumns.length
|
|
320
|
+
let maxRow = 0
|
|
321
|
+
Object.values(newPositions).forEach(({ row }) => {
|
|
322
|
+
if (row > maxRow) maxRow = row
|
|
323
|
+
})
|
|
324
|
+
layoutCache.value.numColumns = numColumns
|
|
325
|
+
layoutCache.value.maxRow = maxRow
|
|
326
|
+
layoutCache.value.contentWidth =
|
|
327
|
+
50 + (numColumns - 1) * props.hGap + nodeWidth + 50
|
|
328
|
+
layoutCache.value.contentHeight =
|
|
329
|
+
50 + (maxRow - 1) * props.vGap + nodeHeight + 50
|
|
313
330
|
}
|
|
314
331
|
|
|
315
332
|
const descriptiveRoundName = (round) => {
|
|
@@ -442,6 +459,136 @@ const getTeamPath = (teamId) => {
|
|
|
442
459
|
}
|
|
443
460
|
}
|
|
444
461
|
|
|
462
|
+
const normalizeBracketData = (bracketData) => {
|
|
463
|
+
const minCol = bracketData.minColumn
|
|
464
|
+
? Number(bracketData.minColumn)
|
|
465
|
+
: Math.min(...bracketData.round.map((r) => Number(r.columnNo)))
|
|
466
|
+
const minRow = bracketData.minRow
|
|
467
|
+
? Number(bracketData.minRow)
|
|
468
|
+
: Math.min(
|
|
469
|
+
...bracketData.round
|
|
470
|
+
.flatMap((r) =>
|
|
471
|
+
(r.game || []).flatMap((g) =>
|
|
472
|
+
(g.team || []).map((t) => Number(t.position?.rowNo) || 0)
|
|
473
|
+
)
|
|
474
|
+
)
|
|
475
|
+
.filter((n) => n > 0)
|
|
476
|
+
)
|
|
477
|
+
|
|
478
|
+
// Normalize all positions so columns/rows start from 1
|
|
479
|
+
const normRounds = []
|
|
480
|
+
for (const round of bracketData.round) {
|
|
481
|
+
const normGames = []
|
|
482
|
+
if (Array.isArray(round.game)) {
|
|
483
|
+
for (const game of round.game) {
|
|
484
|
+
const normTeams = []
|
|
485
|
+
if (Array.isArray(game.team)) {
|
|
486
|
+
for (const team of game.team) {
|
|
487
|
+
if (team.position) {
|
|
488
|
+
normTeams.push({
|
|
489
|
+
...team,
|
|
490
|
+
position: {
|
|
491
|
+
columnNo: Number(team.position.columnNo) - minCol + 1,
|
|
492
|
+
rowNo: Number(team.position.rowNo) - minRow + 1,
|
|
493
|
+
},
|
|
494
|
+
})
|
|
495
|
+
} else {
|
|
496
|
+
normTeams.push(team)
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
let normWinnerDest = game.winnerDestination
|
|
501
|
+
if (
|
|
502
|
+
normWinnerDest &&
|
|
503
|
+
normWinnerDest.team &&
|
|
504
|
+
normWinnerDest.team.position
|
|
505
|
+
) {
|
|
506
|
+
normWinnerDest = {
|
|
507
|
+
...normWinnerDest,
|
|
508
|
+
team: {
|
|
509
|
+
...normWinnerDest.team,
|
|
510
|
+
position: {
|
|
511
|
+
columnNo:
|
|
512
|
+
Number(normWinnerDest.team.position.columnNo) - minCol + 1,
|
|
513
|
+
rowNo: Number(normWinnerDest.team.position.rowNo) - minRow + 1,
|
|
514
|
+
},
|
|
515
|
+
},
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
let normLoserDest = game.loserDestination
|
|
519
|
+
if (
|
|
520
|
+
normLoserDest &&
|
|
521
|
+
normLoserDest.team &&
|
|
522
|
+
normLoserDest.team.position
|
|
523
|
+
) {
|
|
524
|
+
normLoserDest = {
|
|
525
|
+
...normLoserDest,
|
|
526
|
+
team: {
|
|
527
|
+
...normLoserDest.team,
|
|
528
|
+
position: {
|
|
529
|
+
columnNo:
|
|
530
|
+
Number(normLoserDest.team.position.columnNo) - minCol + 1,
|
|
531
|
+
rowNo: Number(normLoserDest.team.position.rowNo) - minRow + 1,
|
|
532
|
+
},
|
|
533
|
+
},
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
normGames.push({
|
|
537
|
+
...game,
|
|
538
|
+
team: normTeams,
|
|
539
|
+
winnerDestination: normWinnerDest,
|
|
540
|
+
loserDestination: normLoserDest,
|
|
541
|
+
})
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// Normalize winnerPlace/loserPlace
|
|
545
|
+
const normWinnerPlace = []
|
|
546
|
+
if (Array.isArray(round.winnerPlace)) {
|
|
547
|
+
for (const place of round.winnerPlace) {
|
|
548
|
+
if (place.position) {
|
|
549
|
+
normWinnerPlace.push({
|
|
550
|
+
...place,
|
|
551
|
+
position: {
|
|
552
|
+
columnNo: Number(place.position.columnNo) - minCol + 1,
|
|
553
|
+
rowNo: Number(place.position.rowNo) - minRow + 1,
|
|
554
|
+
},
|
|
555
|
+
})
|
|
556
|
+
} else {
|
|
557
|
+
normWinnerPlace.push(place)
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const normLoserPlace = []
|
|
562
|
+
if (Array.isArray(round.loserPlace)) {
|
|
563
|
+
for (const place of round.loserPlace) {
|
|
564
|
+
if (place.position) {
|
|
565
|
+
normLoserPlace.push({
|
|
566
|
+
...place,
|
|
567
|
+
position: {
|
|
568
|
+
columnNo: Number(place.position.columnNo) - minCol + 1,
|
|
569
|
+
rowNo: Number(place.position.rowNo) - minRow + 1,
|
|
570
|
+
},
|
|
571
|
+
})
|
|
572
|
+
} else {
|
|
573
|
+
normLoserPlace.push(place)
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
normRounds.push({
|
|
578
|
+
...round,
|
|
579
|
+
columnNo: Number(round.columnNo) - minCol + 1,
|
|
580
|
+
roundName: round.roundName,
|
|
581
|
+
game: normGames,
|
|
582
|
+
winnerPlace: normWinnerPlace,
|
|
583
|
+
loserPlace: normLoserPlace,
|
|
584
|
+
})
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
...bracketData,
|
|
588
|
+
round: normRounds,
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
445
592
|
const openExpandView = () => {
|
|
446
593
|
emit('open-expand')
|
|
447
594
|
}
|
|
@@ -656,11 +803,8 @@ const renderBracket = () => {
|
|
|
656
803
|
nodeGroups
|
|
657
804
|
.append('text')
|
|
658
805
|
.attr('class', 'node-label')
|
|
659
|
-
.attr('x', (d) => d.x +
|
|
806
|
+
.attr('x', (d) => d.x + (seedTeamIds.includes(d.id) ? 50 : 10)) // Move label right only for seed teams
|
|
660
807
|
.attr('y', (d) => d.y + nodeHeight / 2 + 5)
|
|
661
|
-
.attr('text-anchor', (d) =>
|
|
662
|
-
seedTeamIds.includes(d.id) ? 'start' : 'middle'
|
|
663
|
-
)
|
|
664
808
|
.text((d) => d.label)
|
|
665
809
|
|
|
666
810
|
// Add '-' text to the right side for seed teams
|
|
@@ -700,44 +844,131 @@ const renderBracket = () => {
|
|
|
700
844
|
const destPos = getNodePosition(destId)
|
|
701
845
|
|
|
702
846
|
if (pos1 && pos2 && destPos) {
|
|
703
|
-
//
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
const
|
|
847
|
+
// Use the leftmost node's x for alignment
|
|
848
|
+
const nodeX1 = pos1.x
|
|
849
|
+
const nodeX2 = pos2.x
|
|
850
|
+
const nodeY1 = pos1.y
|
|
851
|
+
const nodeY2 = pos2.y
|
|
852
|
+
|
|
853
|
+
const startX = Math.min(nodeX1, nodeX2)
|
|
854
|
+
const topY = Math.min(nodeY1, nodeY2)
|
|
855
|
+
const bottomY = Math.max(nodeY1, nodeY2) + nodeHeight
|
|
856
|
+
// Move details upward by 12px so it doesn't touch the node
|
|
857
|
+
const centerY = (topY + bottomY) / 2 - 12
|
|
858
|
+
|
|
859
|
+
const detailsGroup = g.append('g').attr('transform', `translate(${startX}, ${centerY})`)
|
|
707
860
|
|
|
708
|
-
|
|
861
|
+
// Remove the white background rect (unwanted)
|
|
862
|
+
// detailsGroup.append('rect') ... (removed)
|
|
709
863
|
|
|
710
|
-
// Add the date text
|
|
711
|
-
|
|
864
|
+
// Add the date text
|
|
865
|
+
detailsGroup
|
|
712
866
|
.append('text')
|
|
713
867
|
.attr('class', 'match-date text-overline-xs text-description')
|
|
714
|
-
.attr('x',
|
|
715
|
-
.attr('y',
|
|
716
|
-
.attr('text-anchor', '
|
|
868
|
+
.attr('x', 0)
|
|
869
|
+
.attr('y', -6)
|
|
870
|
+
.attr('text-anchor', 'start')
|
|
717
871
|
.attr('dominant-baseline', 'middle')
|
|
718
872
|
.text(game.date)
|
|
719
873
|
|
|
720
|
-
// Add
|
|
721
|
-
|
|
874
|
+
// Add venue text below date text from the 'venue' key
|
|
875
|
+
if (game.venue) {
|
|
876
|
+
// We'll use a <foreignObject> with a div, and check for overflow after rendering
|
|
877
|
+
const venueFO = detailsGroup
|
|
878
|
+
.append('foreignObject')
|
|
879
|
+
.attr('x', 0)
|
|
880
|
+
.attr('y', 2)
|
|
881
|
+
.attr('width', 200)
|
|
882
|
+
.attr('height', 30)
|
|
883
|
+
.append('xhtml:div')
|
|
884
|
+
.attr('class', 'text-body-xxs text-description match-venue venue-ellipsis')
|
|
885
|
+
.style('word-break', 'break-word')
|
|
886
|
+
.style('white-space', 'normal')
|
|
887
|
+
.style('text-align', 'start')
|
|
888
|
+
.style('width', '100%')
|
|
889
|
+
.style('overflow', 'hidden')
|
|
890
|
+
.style('display', '-webkit-box')
|
|
891
|
+
.style('-webkit-line-clamp', 2)
|
|
892
|
+
.style('-webkit-box-orient', 'vertical')
|
|
893
|
+
.text(game.venue);
|
|
894
|
+
|
|
895
|
+
// After nextTick, check for vertical overflow and add tooltip if needed
|
|
896
|
+
nextTick(() => {
|
|
897
|
+
// venueFO is a D3 selection, so .node() gives us the div
|
|
898
|
+
const div = venueFO.node();
|
|
899
|
+
if (div && div.scrollHeight > div.clientHeight + 1) {
|
|
900
|
+
// Add ellipsis via CSS (already set), and tooltip on hover
|
|
901
|
+
d3.select(div)
|
|
902
|
+
.on('mouseover', function (event) {
|
|
903
|
+
const rect = this.getBoundingClientRect()
|
|
904
|
+
const svgRect = svgRef.value.getBoundingClientRect()
|
|
905
|
+
tooltipState.value = {
|
|
906
|
+
visible: true,
|
|
907
|
+
x: rect.left - svgRect.left + rect.width / 2,
|
|
908
|
+
y: rect.top - svgRect.top - 40,
|
|
909
|
+
text: game.venue,
|
|
910
|
+
}
|
|
911
|
+
})
|
|
912
|
+
.on('mouseout', function () {
|
|
913
|
+
tooltipState.value = { ...tooltipState.value, visible: false }
|
|
914
|
+
})
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
const locationIcon = detailsGroup
|
|
722
920
|
.append('image')
|
|
723
921
|
.attr('href', '/icons/field-location.svg')
|
|
724
|
-
.attr('x',
|
|
725
|
-
.attr('y', -
|
|
922
|
+
.attr('x', 200)
|
|
923
|
+
.attr('y', -8)
|
|
726
924
|
.attr('width', 24)
|
|
727
925
|
.attr('height', 24)
|
|
728
926
|
.attr('class', 'location-icon')
|
|
729
927
|
.style('cursor', 'pointer')
|
|
730
928
|
|
|
929
|
+
const scheduleIcon = detailsGroup
|
|
930
|
+
.append('image')
|
|
931
|
+
.attr('href', '/icons/scoreboard.svg')
|
|
932
|
+
.attr('x', 240)
|
|
933
|
+
.attr('y', -8)
|
|
934
|
+
.attr('width', 24)
|
|
935
|
+
.attr('height', 24)
|
|
936
|
+
.attr('class', 'info-icon')
|
|
937
|
+
.style('cursor', 'pointer')
|
|
938
|
+
|
|
731
939
|
locationIcon.on('click', function (event) {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
940
|
+
if ($screen.value.isMobile) {
|
|
941
|
+
locationSheet.value.push({ open: true, height: 150 })
|
|
942
|
+
} else {
|
|
943
|
+
const iconRect = this.getBoundingClientRect()
|
|
944
|
+
const svgRect = svgRef.value.getBoundingClientRect()
|
|
945
|
+
tooltipState.value = {
|
|
946
|
+
visible: true,
|
|
947
|
+
x: iconRect.left - svgRect.left + iconRect.width / 2,
|
|
948
|
+
y: iconRect.top - svgRect.top - 40,
|
|
949
|
+
text: game.location || 'Dallas, TX',
|
|
950
|
+
}
|
|
739
951
|
}
|
|
740
952
|
})
|
|
953
|
+
|
|
954
|
+
// Tooltip on hover for schedule icon
|
|
955
|
+
scheduleIcon
|
|
956
|
+
.on('mouseover', function (event) {
|
|
957
|
+
const iconRect = this.getBoundingClientRect()
|
|
958
|
+
const svgRect = svgRef.value.getBoundingClientRect()
|
|
959
|
+
tooltipState.value = {
|
|
960
|
+
visible: true,
|
|
961
|
+
x: iconRect.left - svgRect.left + iconRect.width / 2,
|
|
962
|
+
y: iconRect.top - svgRect.top - 40,
|
|
963
|
+
text: props.viewMatchupDetailText,
|
|
964
|
+
}
|
|
965
|
+
})
|
|
966
|
+
.on('mouseout', function () {
|
|
967
|
+
tooltipState.value = { ...tooltipState.value, visible: false }
|
|
968
|
+
})
|
|
969
|
+
.on('click', function (event) {
|
|
970
|
+
emit('schedule-click', game)
|
|
971
|
+
})
|
|
741
972
|
}
|
|
742
973
|
})
|
|
743
974
|
|
|
@@ -765,6 +996,39 @@ const renderBracket = () => {
|
|
|
765
996
|
(winnerNodePosition.x !== 0 || winnerNodePosition.y !== 0)
|
|
766
997
|
) {
|
|
767
998
|
const { x: mx, y: my } = winnerNodePosition
|
|
999
|
+
|
|
1000
|
+
let labelPlaced = false
|
|
1001
|
+
|
|
1002
|
+
const sourcePositions = (game.team || [])
|
|
1003
|
+
.map((source) => {
|
|
1004
|
+
if (
|
|
1005
|
+
typeof source === 'object' &&
|
|
1006
|
+
source.teamName &&
|
|
1007
|
+
source.position
|
|
1008
|
+
) {
|
|
1009
|
+
const id = `${source.teamName}_${source.position.columnNo}_${source.position.rowNo}`
|
|
1010
|
+
return getNodePosition(id)
|
|
1011
|
+
}
|
|
1012
|
+
return null
|
|
1013
|
+
})
|
|
1014
|
+
.filter(Boolean)
|
|
1015
|
+
|
|
1016
|
+
let triX = 0, triY = 0
|
|
1017
|
+
if (sourcePositions.length === 2) {
|
|
1018
|
+
if (mx > sourcePositions[0].x) {
|
|
1019
|
+
// Winner to the right
|
|
1020
|
+
triX = (sourcePositions[0].x + nodeWidth + mx - edgeMargin) / 2
|
|
1021
|
+
} else {
|
|
1022
|
+
// Winner to the left
|
|
1023
|
+
triX = (sourcePositions[0].x - edgeMargin + mx + nodeWidth) / 2
|
|
1024
|
+
}
|
|
1025
|
+
// Always use the vertical center of the destination node
|
|
1026
|
+
triY = my + nodeHeight / 2
|
|
1027
|
+
} else if (sourcePositions.length === 1) {
|
|
1028
|
+
triX = (sourcePositions[0].x + mx) / 2
|
|
1029
|
+
triY = my + nodeHeight / 2
|
|
1030
|
+
}
|
|
1031
|
+
|
|
768
1032
|
game.team.forEach((source) => {
|
|
769
1033
|
let sourceNodeId
|
|
770
1034
|
if (
|
|
@@ -807,6 +1071,47 @@ const renderBracket = () => {
|
|
|
807
1071
|
.attr('d', path.toString())
|
|
808
1072
|
.attr('stroke-linecap', 'round')
|
|
809
1073
|
})
|
|
1074
|
+
|
|
1075
|
+
// Place the gameAliasName at the trisection point (only once per edge group)
|
|
1076
|
+
if (game.gameAliasName && !labelPlaced && sourcePositions.length) {
|
|
1077
|
+
const aliasText = game.gameAliasName.replace(/-/g, '').trim();
|
|
1078
|
+
const padding = 6 // px, padding inside the square
|
|
1079
|
+
const tempText = g.append('text')
|
|
1080
|
+
.attr('class', 'text-caption-xs game-number')
|
|
1081
|
+
.attr('x', triX)
|
|
1082
|
+
.attr('y', triY)
|
|
1083
|
+
.attr('text-anchor', 'middle')
|
|
1084
|
+
.attr('dominant-baseline', 'middle')
|
|
1085
|
+
.style('visibility', 'hidden')
|
|
1086
|
+
.text(aliasText)
|
|
1087
|
+
const bbox = tempText.node().getBBox()
|
|
1088
|
+
tempText.remove()
|
|
1089
|
+
const size = Math.max(bbox.width, bbox.height) + padding
|
|
1090
|
+
|
|
1091
|
+
// Draw white square background with edge color border
|
|
1092
|
+
g.append('rect')
|
|
1093
|
+
.attr('x', triX - size / 2)
|
|
1094
|
+
.attr('y', triY - size / 2)
|
|
1095
|
+
.attr('width', size)
|
|
1096
|
+
.attr('height', size)
|
|
1097
|
+
.attr('rx', 4)
|
|
1098
|
+
.attr('fill', '#fff')
|
|
1099
|
+
.attr('stroke', getComputedStyle(document.documentElement)
|
|
1100
|
+
.getPropertyValue('--q-color-neutral-4') || '#b9e0ff')
|
|
1101
|
+
.attr('stroke-width', 4)
|
|
1102
|
+
.attr('class', 'edge-alias-bg')
|
|
1103
|
+
|
|
1104
|
+
// Draw the label text
|
|
1105
|
+
g.append('text')
|
|
1106
|
+
.attr('class', 'text-caption-xs text-description game-alias-label')
|
|
1107
|
+
.attr('x', triX)
|
|
1108
|
+
.attr('y', triY)
|
|
1109
|
+
.attr('text-anchor', 'middle')
|
|
1110
|
+
.attr('dominant-baseline', 'middle')
|
|
1111
|
+
.style('pointer-events', 'none')
|
|
1112
|
+
.text(aliasText)
|
|
1113
|
+
labelPlaced = true
|
|
1114
|
+
}
|
|
810
1115
|
}
|
|
811
1116
|
}
|
|
812
1117
|
|
|
@@ -831,6 +1136,32 @@ const renderBracket = () => {
|
|
|
831
1136
|
(loserNodePosition.x !== 0 || loserNodePosition.y !== 0)
|
|
832
1137
|
) {
|
|
833
1138
|
const { x: lx, y: ly } = loserNodePosition
|
|
1139
|
+
|
|
1140
|
+
let labelPlaced = false
|
|
1141
|
+
|
|
1142
|
+
const sourcePositions = (game.team || [])
|
|
1143
|
+
.map((source) => {
|
|
1144
|
+
if (
|
|
1145
|
+
typeof source === 'object' &&
|
|
1146
|
+
source.teamName &&
|
|
1147
|
+
source.position
|
|
1148
|
+
) {
|
|
1149
|
+
const id = `${source.teamName}_${source.position.columnNo}_${source.position.rowNo}`
|
|
1150
|
+
return getNodePosition(id)
|
|
1151
|
+
}
|
|
1152
|
+
return null
|
|
1153
|
+
})
|
|
1154
|
+
.filter(Boolean)
|
|
1155
|
+
|
|
1156
|
+
let triX = 0, triY = 0
|
|
1157
|
+
if (sourcePositions.length === 2) {
|
|
1158
|
+
triX = (sourcePositions[0].x - edgeMargin + lx + nodeWidth) / 2
|
|
1159
|
+
triY = ly + nodeHeight / 2
|
|
1160
|
+
} else if (sourcePositions.length === 1) {
|
|
1161
|
+
triX = (sourcePositions[0].x + lx) / 2
|
|
1162
|
+
triY = ly + nodeHeight / 2
|
|
1163
|
+
}
|
|
1164
|
+
|
|
834
1165
|
game.team.forEach((source) => {
|
|
835
1166
|
let sourceNodeId
|
|
836
1167
|
if (
|
|
@@ -861,39 +1192,141 @@ const renderBracket = () => {
|
|
|
861
1192
|
.attr('d', path.toString())
|
|
862
1193
|
.attr('stroke-linecap', 'round') // Changed to rounded end
|
|
863
1194
|
})
|
|
1195
|
+
|
|
1196
|
+
// Place the gameAliasName at the trisection point (only once per edge group)
|
|
1197
|
+
if (game.gameAliasName && !labelPlaced && sourcePositions.length) {
|
|
1198
|
+
// Remove hyphens from gameAliasName
|
|
1199
|
+
const aliasText = game.gameAliasName.replace(/-/g, '').trim();
|
|
1200
|
+
// Calculate text size for the background rect
|
|
1201
|
+
const fontSize = 13 // px, matches .edge-alias-label font-size
|
|
1202
|
+
const padding = 6 // px, padding inside the square
|
|
1203
|
+
// Temporary text element to measure width
|
|
1204
|
+
const tempText = g.append('text')
|
|
1205
|
+
.attr('class', 'text-caption-xs text-description')
|
|
1206
|
+
.attr('x', triX)
|
|
1207
|
+
.attr('y', triY)
|
|
1208
|
+
.attr('text-anchor', 'middle')
|
|
1209
|
+
.attr('dominant-baseline', 'middle')
|
|
1210
|
+
.style('visibility', 'hidden')
|
|
1211
|
+
.text(aliasText)
|
|
1212
|
+
const bbox = tempText.node().getBBox()
|
|
1213
|
+
tempText.remove()
|
|
1214
|
+
const size = Math.max(bbox.width, bbox.height) + padding
|
|
1215
|
+
|
|
1216
|
+
// Draw white square background with edge color border
|
|
1217
|
+
g.append('rect')
|
|
1218
|
+
.attr('x', triX - size / 2)
|
|
1219
|
+
.attr('y', triY - size / 2)
|
|
1220
|
+
.attr('width', size)
|
|
1221
|
+
.attr('height', size)
|
|
1222
|
+
.attr('rx', 5)
|
|
1223
|
+
.attr('fill', '#fff')
|
|
1224
|
+
.attr('stroke', getComputedStyle(document.documentElement)
|
|
1225
|
+
.getPropertyValue('--q-color-neutral-4') || '#b9e0ff')
|
|
1226
|
+
.attr('stroke-width', 4)
|
|
1227
|
+
.attr('class', 'edge-alias-bg')
|
|
1228
|
+
|
|
1229
|
+
// Draw the label text
|
|
1230
|
+
g.append('text')
|
|
1231
|
+
.attr('class', 'text-caption-xs text-description game-alias-label')
|
|
1232
|
+
.attr('x', triX)
|
|
1233
|
+
.attr('y', triY)
|
|
1234
|
+
.attr('text-anchor', 'middle')
|
|
1235
|
+
.attr('dominant-baseline', 'middle')
|
|
1236
|
+
.style('pointer-events', 'none')
|
|
1237
|
+
.text(aliasText)
|
|
1238
|
+
labelPlaced = true
|
|
1239
|
+
}
|
|
864
1240
|
}
|
|
865
|
-
}
|
|
866
|
-
|
|
1241
|
+
} // <-- FIX: close the LOSER edges block here
|
|
1242
|
+
|
|
1243
|
+
}) // <-- close games.value.forEach
|
|
867
1244
|
|
|
868
1245
|
// After all edges are drawn, move highlighted edges to top
|
|
869
1246
|
nextTick(() => {
|
|
870
1247
|
const gEl = gRef.value
|
|
871
1248
|
if (gEl) {
|
|
1249
|
+
// Move highlighted edges to top
|
|
872
1250
|
const highlightedEdges = gEl.querySelectorAll('.edge-path.highlighted')
|
|
873
1251
|
highlightedEdges.forEach((el) => {
|
|
874
1252
|
gEl.appendChild(el)
|
|
875
1253
|
})
|
|
1254
|
+
// Move game-alias boxes (rects and texts) to top after edges
|
|
1255
|
+
const aliasRects = gEl.querySelectorAll('.edge-alias-bg')
|
|
1256
|
+
const aliasLabels = gEl.querySelectorAll('.game-alias-label')
|
|
1257
|
+
aliasRects.forEach((el) => {
|
|
1258
|
+
gEl.appendChild(el)
|
|
1259
|
+
})
|
|
1260
|
+
aliasLabels.forEach((el) => {
|
|
1261
|
+
gEl.appendChild(el)
|
|
1262
|
+
})
|
|
876
1263
|
}
|
|
877
1264
|
})
|
|
878
1265
|
}
|
|
879
1266
|
|
|
880
|
-
|
|
881
|
-
|
|
1267
|
+
function resetZoom() {
|
|
1268
|
+
const svg = d3.select(svgRef.value)
|
|
1269
|
+
if (zoomRef.value && svgRef.value) {
|
|
1270
|
+
const svgElement = svgRef.value
|
|
1271
|
+
const { width, height } = svgElement.getBoundingClientRect()
|
|
1272
|
+
const centerY = height / 2
|
|
1273
|
+
|
|
1274
|
+
const k = 1
|
|
1275
|
+
const contentWidth = layoutCache.value.contentWidth
|
|
1276
|
+
const contentHeight = layoutCache.value.contentHeight
|
|
1277
|
+
const viewportWidth = svgElement.clientWidth
|
|
1278
|
+
const viewportHeight = svgElement.clientHeight
|
|
1279
|
+
const minTx = viewportWidth - contentWidth * k
|
|
1280
|
+
const maxTx = 0
|
|
1281
|
+
const minTy = viewportHeight - contentHeight * k
|
|
1282
|
+
const maxTy = 0
|
|
1283
|
+
|
|
1284
|
+
// Center on selected team if available, else round 1
|
|
1285
|
+
let tx, ty
|
|
1286
|
+
let targetX = null,
|
|
1287
|
+
targetY = null
|
|
1288
|
+
if (selectedTeamId.value) {
|
|
1289
|
+
const pos = getNodePosition(selectedTeamId.value)
|
|
1290
|
+
targetX = pos.x + nodeWidth / 2
|
|
1291
|
+
targetY = pos.y + nodeHeight / 2
|
|
1292
|
+
} else {
|
|
1293
|
+
const firstRound = props.bracketData.round?.[0]
|
|
1294
|
+
if (firstRound) {
|
|
1295
|
+
targetX = (firstRound.columnNo - 1) * props.hGap + props.hGap / 2
|
|
1296
|
+
targetY = centerY
|
|
1297
|
+
} else {
|
|
1298
|
+
targetX = viewportWidth / 2
|
|
1299
|
+
targetY = centerY
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
tx = viewportWidth / 2 - targetX * k
|
|
1303
|
+
ty = viewportHeight / 2 - targetY * k
|
|
1304
|
+
tx = minTx > 0 ? minTx / 2 : Math.max(minTx, Math.min(maxTx, tx))
|
|
1305
|
+
ty = minTy > 0 ? minTy / 2 : Math.max(minTy, Math.min(maxTy, ty))
|
|
1306
|
+
const clampedTransform = d3.zoomIdentity.translate(tx, ty).scale(k)
|
|
1307
|
+
svg
|
|
1308
|
+
.transition()
|
|
1309
|
+
.duration(400)
|
|
1310
|
+
.ease(d3.easeCubic)
|
|
1311
|
+
.call(zoomRef.value.transform, clampedTransform)
|
|
1312
|
+
d3.select(gRef.value).attr('transform', clampedTransform)
|
|
1313
|
+
currentTransform.value = clampedTransform
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
882
1316
|
|
|
883
|
-
|
|
1317
|
+
const scrollToBracketSide = (side) => {
|
|
1318
|
+
if (!normalizedBracketData.value || !normalizedBracketData.value.round) return
|
|
1319
|
+
const firstRound = normalizedBracketData.value.round[0]
|
|
884
1320
|
if (!firstRound || !firstRound.game || firstRound.game.length === 0) return
|
|
885
|
-
|
|
886
1321
|
const firstMatch = firstRound.game[0]
|
|
887
1322
|
let targetColumn = null
|
|
888
|
-
|
|
889
1323
|
if (side === 'winner' && firstMatch.winnerDestination) {
|
|
890
1324
|
targetColumn = firstMatch.winnerDestination.team.position.columnNo
|
|
891
1325
|
} else if (side === 'loser' && firstMatch.loserDestination) {
|
|
892
1326
|
targetColumn = firstMatch.loserDestination.team.position.columnNo
|
|
893
1327
|
}
|
|
894
|
-
|
|
895
1328
|
if (targetColumn) {
|
|
896
|
-
const targetRound =
|
|
1329
|
+
const targetRound = normalizedBracketData.value.round.find(
|
|
897
1330
|
(r) => r.columnNo === targetColumn
|
|
898
1331
|
)
|
|
899
1332
|
if (targetRound) {
|
|
@@ -904,41 +1337,26 @@ const scrollToBracketSide = (side) => {
|
|
|
904
1337
|
|
|
905
1338
|
const scrollToRound = (round) => {
|
|
906
1339
|
activeRound.value = { roundNo: round.roundNo, columnNo: round.columnNo }
|
|
907
|
-
openRoundMenu.value = false
|
|
1340
|
+
openRoundMenu.value = false
|
|
908
1341
|
const svg = d3.select(svgRef.value)
|
|
909
1342
|
const zoomBehavior = zoomRef.value
|
|
910
1343
|
if (!zoomBehavior || !svgRef.value) return
|
|
911
1344
|
|
|
912
|
-
|
|
913
|
-
const roundX = (round.columnNo - 1) * props.hGap + props.hGap / 2
|
|
1345
|
+
const normalizedRound = normalizedBracketData.value.round
|
|
914
1346
|
|
|
915
|
-
|
|
916
|
-
|
|
1347
|
+
const mainRound = normalizedRound.find(
|
|
1348
|
+
(r) => r.roundName == round.roundName && r.roundNo == round.roundNo
|
|
1349
|
+
)
|
|
917
1350
|
|
|
918
|
-
|
|
919
|
-
|
|
1351
|
+
const roundX = (mainRound.columnNo - 1) * props.hGap + props.hGap / 2
|
|
1352
|
+
const viewportWidth = svgRef.value.clientWidth
|
|
920
1353
|
const liveTransform = d3.zoomTransform(svg.node())
|
|
921
1354
|
const { k: currentK, y: currentY } = liveTransform
|
|
922
|
-
|
|
923
|
-
// Determine content dimensions.
|
|
924
|
-
const uniqueColumns = [
|
|
925
|
-
...new Set(props.bracketData.round.map((r) => r.columnNo)),
|
|
926
|
-
]
|
|
927
|
-
const numColumns = uniqueColumns.length
|
|
928
|
-
const contentWidth = 50 + (numColumns - 1) * props.hGap + nodeWidth + 50
|
|
929
|
-
|
|
930
|
-
// Calculate the new x-translate to center the round's center in the viewport.
|
|
931
1355
|
const targetX = viewportWidth / 2 - roundX * currentK
|
|
932
|
-
|
|
933
1356
|
const clampedX = targetX
|
|
934
|
-
|
|
935
|
-
// Create the new transform, keeping the current scale and y-pan.
|
|
936
|
-
|
|
937
1357
|
const newTransform = d3.zoomIdentity
|
|
938
1358
|
.translate(clampedX, currentY)
|
|
939
1359
|
.scale(currentK)
|
|
940
|
-
|
|
941
|
-
// Apply the new transform with a smooth transition.
|
|
942
1360
|
svg.transition().duration(750).call(zoomBehavior.transform, newTransform)
|
|
943
1361
|
}
|
|
944
1362
|
|
|
@@ -953,10 +1371,20 @@ const setSelectedTeamId = (teamId) => {
|
|
|
953
1371
|
const team = toolbarTeams.value.find((t) => t.id === teamId)
|
|
954
1372
|
if (team) {
|
|
955
1373
|
selectedTeamId.value = team.id
|
|
1374
|
+
// Reset zoom to initial scale and center
|
|
1375
|
+
resetZoom()
|
|
956
1376
|
}
|
|
957
1377
|
openMenu.value = false
|
|
958
1378
|
}
|
|
959
1379
|
|
|
1380
|
+
const toggleBracketSideInSheet = (side) => {
|
|
1381
|
+
tempSelectedBracketSide.value = side
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
const toggleRoundInSheet = (round) => {
|
|
1385
|
+
tempSelectedRoundId.value = `${round.roundNo}-${round.roundName}`
|
|
1386
|
+
}
|
|
1387
|
+
|
|
960
1388
|
const toggleTeamInSheet = (teamId) => {
|
|
961
1389
|
if (teamId === tempSelectedTeamId.value) {
|
|
962
1390
|
tempSelectedTeamId.value = ''
|
|
@@ -965,41 +1393,59 @@ const toggleTeamInSheet = (teamId) => {
|
|
|
965
1393
|
tempSelectedTeamId.value = teamId
|
|
966
1394
|
}
|
|
967
1395
|
|
|
968
|
-
const toggleBracketSideInSheet = (side) => {
|
|
969
|
-
tempSelectedBracketSide.value = side
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
const toggleRoundInSheet = (round) => {
|
|
973
|
-
tempSelectedRoundId.value = `${round.roundNo}-${round.columnNo}`
|
|
974
|
-
}
|
|
975
|
-
|
|
976
1396
|
const zoomIn = () => {
|
|
977
1397
|
const svg = d3.select(svgRef.value)
|
|
978
|
-
if (zoomRef.value) {
|
|
1398
|
+
if (zoomRef.value && svgRef.value) {
|
|
1399
|
+
const svgElement = svgRef.value
|
|
1400
|
+
const { width, height } = svgElement.getBoundingClientRect()
|
|
1401
|
+
const centerX = width / 2
|
|
1402
|
+
const centerY = height / 2
|
|
1403
|
+
|
|
979
1404
|
// Increase scale by 20%
|
|
980
|
-
const
|
|
981
|
-
const
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1405
|
+
const prevScale = currentTransform.value.k || 1
|
|
1406
|
+
const newScale = Math.min(prevScale * 1.2, 2.5)
|
|
1407
|
+
|
|
1408
|
+
// Calculate new translation to keep center in view
|
|
1409
|
+
const newX =
|
|
1410
|
+
centerX - (centerX - currentTransform.value.x) * (newScale / prevScale)
|
|
1411
|
+
const newY =
|
|
1412
|
+
centerY - (centerY - currentTransform.value.y) * (newScale / prevScale)
|
|
1413
|
+
|
|
1414
|
+
const newTransform = d3.zoomIdentity.translate(newX, newY).scale(newScale)
|
|
1415
|
+
|
|
1416
|
+
svg
|
|
1417
|
+
.transition()
|
|
1418
|
+
.duration(200)
|
|
1419
|
+
.ease(d3.easeCubic)
|
|
1420
|
+
.call(zoomRef.value.transform, newTransform)
|
|
988
1421
|
}
|
|
989
1422
|
}
|
|
990
1423
|
|
|
991
1424
|
const zoomOut = () => {
|
|
992
1425
|
const svg = d3.select(svgRef.value)
|
|
993
|
-
if (zoomRef.value) {
|
|
1426
|
+
if (zoomRef.value && svgRef.value) {
|
|
1427
|
+
const svgElement = svgRef.value
|
|
1428
|
+
const { width, height } = svgElement.getBoundingClientRect()
|
|
1429
|
+
const centerX = width / 2
|
|
1430
|
+
const centerY = height / 2
|
|
1431
|
+
|
|
994
1432
|
// Decrease scale by 20%
|
|
995
|
-
const
|
|
996
|
-
const
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1433
|
+
const prevScale = currentTransform.value.k || 1
|
|
1434
|
+
const newScale = Math.max(prevScale / 1.2, 0.5)
|
|
1435
|
+
|
|
1436
|
+
// Calculate new translation to keep center in view
|
|
1437
|
+
const newX =
|
|
1438
|
+
centerX - (centerX - currentTransform.value.x) * (newScale / prevScale)
|
|
1439
|
+
const newY =
|
|
1440
|
+
centerY - (centerY - currentTransform.value.y) * (newScale / prevScale)
|
|
1441
|
+
|
|
1442
|
+
const newTransform = d3.zoomIdentity.translate(newX, newY).scale(newScale)
|
|
1443
|
+
|
|
1444
|
+
svg
|
|
1445
|
+
.transition()
|
|
1446
|
+
.duration(200)
|
|
1447
|
+
.ease(d3.easeCubic)
|
|
1448
|
+
.call(zoomRef.value.transform, newTransform)
|
|
1003
1449
|
}
|
|
1004
1450
|
}
|
|
1005
1451
|
|
|
@@ -1013,6 +1459,7 @@ watch(
|
|
|
1013
1459
|
reinitZoomAndLayout()
|
|
1014
1460
|
}
|
|
1015
1461
|
)
|
|
1462
|
+
|
|
1016
1463
|
watch(
|
|
1017
1464
|
() => props.vGap,
|
|
1018
1465
|
() => {
|
|
@@ -1060,7 +1507,7 @@ onMounted(() => {
|
|
|
1060
1507
|
|
|
1061
1508
|
// Determine content dimensions
|
|
1062
1509
|
const uniqueColumns = [
|
|
1063
|
-
...new Set(
|
|
1510
|
+
...new Set(normalizedBracketData.value.round.map((r) => r.columnNo)),
|
|
1064
1511
|
]
|
|
1065
1512
|
const numColumns = uniqueColumns.length
|
|
1066
1513
|
const contentWidth = 50 + (numColumns + 2) * props.hGap + nodeWidth + 50
|
|
@@ -1075,35 +1522,55 @@ onMounted(() => {
|
|
|
1075
1522
|
const zoomBehavior = d3
|
|
1076
1523
|
.zoom()
|
|
1077
1524
|
.scaleExtent([0.5, 2.5])
|
|
1078
|
-
//
|
|
1079
|
-
// while allowing other events like mouse clicks to pass through.
|
|
1525
|
+
// Custom filter to intercept wheel events and always zoom centered
|
|
1080
1526
|
.filter((event) => {
|
|
1081
1527
|
if (event.type === 'wheel') {
|
|
1082
1528
|
event.preventDefault()
|
|
1529
|
+
// Manually handle wheel zoom centered on SVG
|
|
1530
|
+
const svgElement = svgRef.value
|
|
1531
|
+
const { width, height } = svgElement.getBoundingClientRect()
|
|
1532
|
+
const centerX = width / 2
|
|
1533
|
+
const centerY = height / 2
|
|
1534
|
+
const prevScale = currentTransform.value.k || 1
|
|
1535
|
+
let newScale = prevScale
|
|
1536
|
+
// D3 uses event.deltaY for wheel direction
|
|
1537
|
+
if (event.deltaY < 0) {
|
|
1538
|
+
newScale = Math.min(prevScale * 1.2, 2.5)
|
|
1539
|
+
} else {
|
|
1540
|
+
newScale = Math.max(prevScale / 1.2, 0.5)
|
|
1541
|
+
}
|
|
1542
|
+
// Calculate new translation to keep center in view
|
|
1543
|
+
const newX =
|
|
1544
|
+
centerX -
|
|
1545
|
+
(centerX - currentTransform.value.x) * (newScale / prevScale)
|
|
1546
|
+
const newY =
|
|
1547
|
+
centerY -
|
|
1548
|
+
(centerY - currentTransform.value.y) * (newScale / prevScale)
|
|
1549
|
+
const newTransform = d3.zoomIdentity
|
|
1550
|
+
.translate(newX, newY)
|
|
1551
|
+
.scale(newScale)
|
|
1552
|
+
d3.select(svgRef.value)
|
|
1553
|
+
.transition()
|
|
1554
|
+
.duration(200)
|
|
1555
|
+
.ease(d3.easeCubic)
|
|
1556
|
+
.call(zoomBehavior.transform, newTransform)
|
|
1557
|
+
return false // Prevent default D3 wheel zoom
|
|
1083
1558
|
}
|
|
1084
|
-
// Allow click-and-drag panning
|
|
1085
1559
|
return !event.ctrlKey || event.type === 'wheel'
|
|
1086
1560
|
})
|
|
1087
|
-
// We clamp manually in the zoom event instead of using translateExtent
|
|
1088
1561
|
.on('zoom', (event) => {
|
|
1089
1562
|
let { transform } = event
|
|
1090
1563
|
const { k } = transform
|
|
1091
|
-
tooltipState.value.visible = false
|
|
1092
|
-
|
|
1093
|
-
// Calculate boundaries for clamping.
|
|
1564
|
+
tooltipState.value.visible = false
|
|
1094
1565
|
const minTx = viewportWidth - contentWidth * k
|
|
1095
1566
|
const maxTx = 0
|
|
1096
1567
|
const minTy = viewportHeight - contentHeight * k
|
|
1097
1568
|
const maxTy = 0
|
|
1098
|
-
|
|
1099
1569
|
const tx =
|
|
1100
1570
|
minTx > 0 ? minTx / 2 : Math.max(minTx, Math.min(maxTx, transform.x))
|
|
1101
1571
|
const ty =
|
|
1102
1572
|
minTy > 0 ? minTy / 2 : Math.max(minTy, Math.min(maxTy, transform.y))
|
|
1103
|
-
|
|
1104
|
-
// Create a new clamped transform
|
|
1105
1573
|
const clampedTransform = d3.zoomIdentity.translate(tx, ty).scale(k)
|
|
1106
|
-
|
|
1107
1574
|
g.attr('transform', clampedTransform)
|
|
1108
1575
|
currentTransform.value = clampedTransform
|
|
1109
1576
|
})
|
|
@@ -1111,19 +1578,40 @@ onMounted(() => {
|
|
|
1111
1578
|
svg.call(zoomBehavior)
|
|
1112
1579
|
zoomRef.value = zoomBehavior
|
|
1113
1580
|
|
|
1114
|
-
// Focus on round
|
|
1115
|
-
const firstRound =
|
|
1581
|
+
// Focus on first normalized round when bracket loads
|
|
1582
|
+
const firstRound = normalizedBracketData.value.round?.[0]
|
|
1116
1583
|
if (firstRound) scrollToRound(firstRound)
|
|
1117
1584
|
|
|
1118
1585
|
renderBracket()
|
|
1119
1586
|
|
|
1120
|
-
// Add window resize listener
|
|
1121
1587
|
window.addEventListener('resize', reinitZoomAndLayout)
|
|
1122
1588
|
})
|
|
1123
1589
|
|
|
1124
1590
|
onUnmounted(() => {
|
|
1125
1591
|
window.removeEventListener('resize', reinitZoomAndLayout)
|
|
1126
|
-
|
|
1592
|
+
})
|
|
1593
|
+
|
|
1594
|
+
watch(
|
|
1595
|
+
() => props.bracketData,
|
|
1596
|
+
() => {
|
|
1597
|
+
// Reset selections on bracket data change
|
|
1598
|
+
selectedTeamId.value = ''
|
|
1599
|
+
tempSelectedTeamId.value = ''
|
|
1600
|
+
tempSelectedRoundId.value = ''
|
|
1601
|
+
tempSelectedBracketSide.value = ''
|
|
1602
|
+
bracketSheet.value = []
|
|
1603
|
+
roundSheet.value = []
|
|
1604
|
+
computeBracketLayout()
|
|
1605
|
+
renderBracket()
|
|
1606
|
+
},
|
|
1607
|
+
{ deep: true }
|
|
1608
|
+
)
|
|
1609
|
+
|
|
1610
|
+
watch(currentTransform, (newVal) => {
|
|
1611
|
+
// Only update on explicit user zoom/pan, not on initial mount
|
|
1612
|
+
if (gRef.value) {
|
|
1613
|
+
d3.select(gRef.value).attr('transform', newVal)
|
|
1614
|
+
}
|
|
1127
1615
|
})
|
|
1128
1616
|
</script>
|
|
1129
1617
|
|
|
@@ -1152,20 +1640,20 @@ onUnmounted(() => {
|
|
|
1152
1640
|
:ripple="false"
|
|
1153
1641
|
size="sm"
|
|
1154
1642
|
@click="selectedTeamName = ''"
|
|
1155
|
-
|
|
1643
|
+
/>
|
|
1156
1644
|
</div>
|
|
1157
1645
|
</div>
|
|
1158
1646
|
</div>
|
|
1159
1647
|
<div class="sticky-round-labels">
|
|
1160
1648
|
<div
|
|
1161
1649
|
v-for="(round, i) in bracketData.round"
|
|
1162
|
-
:key="`${round.roundNo}-${round.columnNo}`"
|
|
1163
1650
|
class="round-label text-body-sm"
|
|
1164
1651
|
:class="{
|
|
1165
1652
|
'active-round':
|
|
1166
1653
|
activeRound.roundNo === round.roundNo &&
|
|
1167
1654
|
activeRound.columnNo === round.columnNo,
|
|
1168
1655
|
}"
|
|
1656
|
+
:key="`${round.roundNo}-${round.columnNo}`"
|
|
1169
1657
|
:style="roundLabelStyles[i]"
|
|
1170
1658
|
@click="scrollToRound(round)"
|
|
1171
1659
|
>
|
|
@@ -1187,7 +1675,7 @@ onUnmounted(() => {
|
|
|
1187
1675
|
width="24"
|
|
1188
1676
|
>
|
|
1189
1677
|
<rect fill="#F5F7F9" height="24" width="24" x="0" y="0" />
|
|
1190
|
-
<circle cx="12" cy="12" fill="#d3d3d3" r="1.5"
|
|
1678
|
+
<circle class="dot" cx="12" cy="12" fill="#d3d3d3" r="1.5" />
|
|
1191
1679
|
</pattern>
|
|
1192
1680
|
</defs>
|
|
1193
1681
|
<rect
|
|
@@ -1217,27 +1705,27 @@ onUnmounted(() => {
|
|
|
1217
1705
|
>
|
|
1218
1706
|
<UBtnIcon
|
|
1219
1707
|
iconClass="fa-kit-duotone fa-zoom-out"
|
|
1220
|
-
ariaLabel="Zoom Out"
|
|
1221
1708
|
:anchor="anchor"
|
|
1709
|
+
ariaLabel="Zoom Out"
|
|
1222
1710
|
:self="self"
|
|
1223
1711
|
size="md"
|
|
1224
1712
|
:tooltip="zoomOutTooltip"
|
|
1225
1713
|
@click="zoomOut"
|
|
1226
|
-
|
|
1714
|
+
/>
|
|
1227
1715
|
<UBtnIcon
|
|
1228
1716
|
iconClass="fa-kit-duotone fa-zoom"
|
|
1229
|
-
ariaLabel="Zoom In"
|
|
1230
1717
|
:anchor="anchor"
|
|
1718
|
+
ariaLabel="Zoom In"
|
|
1231
1719
|
:self="self"
|
|
1232
1720
|
size="md"
|
|
1233
1721
|
:tooltip="zoomInTooltip"
|
|
1234
1722
|
@click="zoomIn"
|
|
1235
|
-
|
|
1723
|
+
/>
|
|
1236
1724
|
<UBtnIcon
|
|
1237
1725
|
v-if="!$screen.isMobile"
|
|
1238
1726
|
iconClass="fa-kit-duotone fa-bracket"
|
|
1239
|
-
ariaLabel="Rounds"
|
|
1240
1727
|
:anchor="anchor"
|
|
1728
|
+
ariaLabel="Rounds"
|
|
1241
1729
|
:self="self"
|
|
1242
1730
|
size="md"
|
|
1243
1731
|
:tooltip="roundTooltip"
|
|
@@ -1258,9 +1746,9 @@ onUnmounted(() => {
|
|
|
1258
1746
|
:key="`${round.roundNo}-${round.columnNo}`"
|
|
1259
1747
|
@click="scrollToRound(round)"
|
|
1260
1748
|
>
|
|
1261
|
-
<q-item-section>
|
|
1262
|
-
descriptiveRoundName(round)
|
|
1263
|
-
|
|
1749
|
+
<q-item-section>
|
|
1750
|
+
{{ descriptiveRoundName(round) }}
|
|
1751
|
+
</q-item-section>
|
|
1264
1752
|
</q-item>
|
|
1265
1753
|
</q-list>
|
|
1266
1754
|
</q-menu>
|
|
@@ -1269,18 +1757,18 @@ onUnmounted(() => {
|
|
|
1269
1757
|
<UBtnIcon
|
|
1270
1758
|
v-if="$screen.isMobile"
|
|
1271
1759
|
iconClass="fa-kit-duotone fa-bracket"
|
|
1272
|
-
ariaLabel="Rounds"
|
|
1273
1760
|
:anchor="anchor"
|
|
1761
|
+
ariaLabel="Rounds"
|
|
1274
1762
|
:self="self"
|
|
1275
1763
|
size="md"
|
|
1276
1764
|
:tooltip="roundTooltip"
|
|
1277
1765
|
@click="openRoundSheet"
|
|
1278
1766
|
/>
|
|
1279
1767
|
<UBtnIcon
|
|
1280
|
-
v-if="!$screen.isMobile"
|
|
1768
|
+
v-if="!$screen.isMobile && toolbarTeams.length"
|
|
1281
1769
|
iconClass="fa-kit-duotone fa-jersey"
|
|
1282
|
-
ariaLabel="Select Team"
|
|
1283
1770
|
:anchor="anchor"
|
|
1771
|
+
ariaLabel="Select Team"
|
|
1284
1772
|
:self="self"
|
|
1285
1773
|
size="md"
|
|
1286
1774
|
:tooltip="teamSelectTooltip"
|
|
@@ -1306,7 +1794,7 @@ onUnmounted(() => {
|
|
|
1306
1794
|
</template>
|
|
1307
1795
|
</UBtnIcon>
|
|
1308
1796
|
<UBtnIcon
|
|
1309
|
-
v-if="$screen.isMobile"
|
|
1797
|
+
v-if="$screen.isMobile && toolbarTeams.length"
|
|
1310
1798
|
iconClass="fa-kit-duotone fa-jersey"
|
|
1311
1799
|
:anchor="anchor"
|
|
1312
1800
|
ariaLabel="Select Team"
|
|
@@ -1314,8 +1802,7 @@ onUnmounted(() => {
|
|
|
1314
1802
|
size="md"
|
|
1315
1803
|
:tooltip="teamSelectTooltip"
|
|
1316
1804
|
@click="openTeamOptions"
|
|
1317
|
-
|
|
1318
|
-
</UBtnIcon>
|
|
1805
|
+
/>
|
|
1319
1806
|
<UBtnIcon
|
|
1320
1807
|
v-if="!props.isExpandedView"
|
|
1321
1808
|
iconClass="fa-kit-duotone fa-expand"
|
|
@@ -1325,7 +1812,7 @@ onUnmounted(() => {
|
|
|
1325
1812
|
size="md"
|
|
1326
1813
|
:tooltip="expandTooltip"
|
|
1327
1814
|
@click="openExpandView"
|
|
1328
|
-
|
|
1815
|
+
/>
|
|
1329
1816
|
</div>
|
|
1330
1817
|
</div>
|
|
1331
1818
|
</div>
|
|
@@ -1395,14 +1882,14 @@ onUnmounted(() => {
|
|
|
1395
1882
|
in-sheet
|
|
1396
1883
|
:label="descriptiveRoundName(round)"
|
|
1397
1884
|
:selected="
|
|
1398
|
-
tempSelectedRoundId === `${round.roundNo}-${round.
|
|
1885
|
+
tempSelectedRoundId === `${round.roundNo}-${round.roundName}`
|
|
1399
1886
|
"
|
|
1400
1887
|
@onClick="toggleRoundInSheet(round)"
|
|
1401
1888
|
>
|
|
1402
1889
|
<template #trailing_slot>
|
|
1403
1890
|
<q-icon
|
|
1404
1891
|
v-if="
|
|
1405
|
-
tempSelectedRoundId === `${round.roundNo}-${round.
|
|
1892
|
+
tempSelectedRoundId === `${round.roundNo}-${round.roundName}`
|
|
1406
1893
|
"
|
|
1407
1894
|
class="fa-kit-duotone fa-circle-check"
|
|
1408
1895
|
color="primary"
|
|
@@ -1471,6 +1958,26 @@ onUnmounted(() => {
|
|
|
1471
1958
|
/>
|
|
1472
1959
|
</template>
|
|
1473
1960
|
</USheet>
|
|
1961
|
+
<USheet
|
|
1962
|
+
v-model:dialogs="locationSheet"
|
|
1963
|
+
dialog-class="bracket-sheet"
|
|
1964
|
+
:heading="venueSheetHeading"
|
|
1965
|
+
show-action-buttons
|
|
1966
|
+
>
|
|
1967
|
+
<template #content>
|
|
1968
|
+
<div class="text-body-xl text-dark">
|
|
1969
|
+
Dallas, TX
|
|
1970
|
+
</div>
|
|
1971
|
+
</template>
|
|
1972
|
+
<template #action_primary_one>
|
|
1973
|
+
<UBtnStd
|
|
1974
|
+
class="full-width"
|
|
1975
|
+
color="primary"
|
|
1976
|
+
label="Close"
|
|
1977
|
+
@onClick="locationSheet = []"
|
|
1978
|
+
/>
|
|
1979
|
+
</template>
|
|
1980
|
+
</USheet>
|
|
1474
1981
|
</template>
|
|
1475
1982
|
|
|
1476
1983
|
<style scoped lang="sass">
|
|
@@ -1509,7 +2016,7 @@ onUnmounted(() => {
|
|
|
1509
2016
|
font-weight: normal
|
|
1510
2017
|
color: $primary
|
|
1511
2018
|
background: #EEF8FF
|
|
1512
|
-
height:
|
|
2019
|
+
height: $md
|
|
1513
2020
|
user-select: none
|
|
1514
2021
|
cursor: pointer
|
|
1515
2022
|
z-index: 10
|
|
@@ -1531,17 +2038,17 @@ svg
|
|
|
1531
2038
|
.main-content-dialog
|
|
1532
2039
|
padding-top: $ba !important
|
|
1533
2040
|
.q-item
|
|
1534
|
-
margin-bottom: $xs
|
|
2041
|
+
margin-bottom: $xs !important
|
|
1535
2042
|
|
|
1536
2043
|
.bracket-toolbar
|
|
1537
|
-
bottom:
|
|
2044
|
+
bottom: $ba /* Position from the bottom */
|
|
1538
2045
|
left: 50% /* Center horizontally */
|
|
1539
2046
|
transform: translateX(-50%) /* Adjust for centering */
|
|
1540
2047
|
display: flex
|
|
1541
2048
|
justify-content: center
|
|
1542
2049
|
align-items: center
|
|
1543
2050
|
gap: $xs
|
|
1544
|
-
height:
|
|
2051
|
+
height: $lg
|
|
1545
2052
|
background: $neutral-1
|
|
1546
2053
|
border: 1px solid #e0e0e0
|
|
1547
2054
|
border-radius: $sm /* Rounded corners */
|
|
@@ -1587,6 +2094,15 @@ svg
|
|
|
1587
2094
|
:deep(.match-date)
|
|
1588
2095
|
fill: #566176
|
|
1589
2096
|
|
|
2097
|
+
:deep(.match-venue)
|
|
2098
|
+
fill: #566176
|
|
2099
|
+
|
|
2100
|
+
:deep(.game-number)
|
|
2101
|
+
fill: #566176
|
|
2102
|
+
|
|
2103
|
+
:deep(.game-alias-label)
|
|
2104
|
+
fill: #566176
|
|
2105
|
+
|
|
1590
2106
|
:deep(.edge-path)
|
|
1591
2107
|
fill: none !important
|
|
1592
2108
|
stroke: $neutral-4
|
|
@@ -1598,6 +2114,7 @@ svg
|
|
|
1598
2114
|
opacity: 1
|
|
1599
2115
|
|
|
1600
2116
|
:deep(.dot)
|
|
2117
|
+
|
|
1601
2118
|
opacity: 0.6
|
|
1602
2119
|
|
|
1603
2120
|
:deep(.remove-team .button-label)
|
|
@@ -1648,4 +2165,17 @@ svg
|
|
|
1648
2165
|
pointer-events: auto
|
|
1649
2166
|
box-shadow: 0 2px 8px rgba(0,0,0,0.15)
|
|
1650
2167
|
overflow: hidden
|
|
2168
|
+
|
|
2169
|
+
:deep(.edge-alias-bg)
|
|
2170
|
+
shape-rendering: inherit
|
|
2171
|
+
stroke: $neutral-4 !important
|
|
2172
|
+
stroke-width: 2 !important
|
|
2173
|
+
fill: #fff !important
|
|
2174
|
+
|
|
2175
|
+
:deep(.venue-ellipsis)
|
|
2176
|
+
overflow: hidden
|
|
2177
|
+
text-overflow: ellipsis
|
|
2178
|
+
display: -webkit-box
|
|
2179
|
+
-webkit-line-clamp: 2
|
|
2180
|
+
-webkit-box-orient: vertical
|
|
1651
2181
|
</style>
|