@opndev/tennis-match-center 0.0.1 → 0.0.2
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/Changes +26 -0
- package/lib/auto-planner.mjs +5 -32
- package/lib/components/day-grid.mjs +288 -0
- package/lib/components/gameday-board.mjs +1691 -0
- package/lib/components/gameday-card.mjs +325 -0
- package/lib/components/gameday-list.mjs +199 -0
- package/lib/components/match.mjs +368 -0
- package/lib/components/participant.mjs +101 -0
- package/lib/components/planning-board.mjs +914 -131
- package/lib/components/schedule-board.mjs +219 -50
- package/lib/day-rows.mjs +19 -3
- package/lib/gameday-dialog.mjs +93 -0
- package/lib/gameday-facts.mjs +305 -0
- package/lib/gameday-next.mjs +164 -0
- package/lib/gameday-parts.mjs +112 -0
- package/lib/index.mjs +1 -3
- package/lib/knockout-draw.mjs +4 -0
- package/lib/knockout-planner.mjs +99 -5
- package/lib/lighting.mjs +50 -4
- package/lib/plan-placement.mjs +54 -3
- package/lib/planning-day.mjs +15 -9
- package/lib/webcomponents.mjs +12 -0
- package/package.json +6 -1
- package/opndev-tennis-match-center-0.0.1.tgz +0 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesley@opndev.io>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { HTMLElementSugar } from '@skirbi/sugar'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
courtName,
|
|
9
|
+
divisionOf,
|
|
10
|
+
effectiveCourtId,
|
|
11
|
+
formatFor,
|
|
12
|
+
hueOf,
|
|
13
|
+
movedCourt,
|
|
14
|
+
segmentOf,
|
|
15
|
+
stageOf,
|
|
16
|
+
startTimeOf,
|
|
17
|
+
stateOf,
|
|
18
|
+
teamName,
|
|
19
|
+
tookOf,
|
|
20
|
+
} from '../gameday-facts.mjs'
|
|
21
|
+
|
|
22
|
+
import { scoreField, teamCell, textTag } from '../gameday-parts.mjs'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* <tennis-gameday-card> - one match on the officials desk.
|
|
26
|
+
*
|
|
27
|
+
* Given a match, the day it is in and the shell describing the event,
|
|
28
|
+
* it draws the line and says what was pressed. It decides nothing: a
|
|
29
|
+
* card never starts a match, ends one or moves a court, it reports
|
|
30
|
+
* that somebody asked for that and the board decides what happens.
|
|
31
|
+
*
|
|
32
|
+
* The shape is a template, `tennis-gameday-card-template`, so a host
|
|
33
|
+
* that wants the line laid out differently authors its own without
|
|
34
|
+
* touching this. Every part is a named hook and a hook that is not in
|
|
35
|
+
* the host's template is simply not drawn, which is what makes an
|
|
36
|
+
* override an override and not a fork.
|
|
37
|
+
*
|
|
38
|
+
* Fired, all bubbling, all carrying `{ match }`:
|
|
39
|
+
*
|
|
40
|
+
* gameday-card:score a score box was typed in
|
|
41
|
+
* gameday-card:action end the match, or reopen it
|
|
42
|
+
* gameday-card:start start the match, or take that back
|
|
43
|
+
* gameday-card:court the court was asked about
|
|
44
|
+
*/
|
|
45
|
+
export class TennisGamedayCardElement extends HTMLElementSugar {
|
|
46
|
+
static tag = 'tennis-gameday-card'
|
|
47
|
+
|
|
48
|
+
static HtmlTemplate = ['tennis-gameday-card-template', function () {
|
|
49
|
+
return this.tpl(`
|
|
50
|
+
<span data-identifier></span>
|
|
51
|
+
<span data-badge></span>
|
|
52
|
+
<span data-division></span>
|
|
53
|
+
<span data-took></span>
|
|
54
|
+
<tennis-participant data-home></tennis-participant>
|
|
55
|
+
<span data-scores data-side="home"></span>
|
|
56
|
+
<span data-vs>vs</span>
|
|
57
|
+
<span data-scores data-side="visitor"></span>
|
|
58
|
+
<tennis-participant data-visitor></tennis-participant>
|
|
59
|
+
<button type="button" data-action></button>
|
|
60
|
+
<span data-court-slot></span>
|
|
61
|
+
<button type="button" lead data-start></button>
|
|
62
|
+
`)
|
|
63
|
+
}]
|
|
64
|
+
|
|
65
|
+
connectedCallback() {
|
|
66
|
+
super.connectedCallback()
|
|
67
|
+
|
|
68
|
+
if (!this._drawn) {
|
|
69
|
+
this._drawn = true
|
|
70
|
+
this.appendChild(this.renderFromTemplate())
|
|
71
|
+
|
|
72
|
+
this.addEventListener('input', event => {
|
|
73
|
+
const input = event.target.closest('[data-scores] input')
|
|
74
|
+
|
|
75
|
+
if (input) {
|
|
76
|
+
this.say('score', {
|
|
77
|
+
set: Number(input.dataset.set),
|
|
78
|
+
side: input.closest('[data-scores]').dataset.side,
|
|
79
|
+
value: input.value,
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
this.addEventListener('click', event => {
|
|
85
|
+
const pressed = event.target.closest(
|
|
86
|
+
'[data-court-open], [data-start], [data-action]'
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if (!pressed || !this.contains(pressed)) {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.say(
|
|
94
|
+
pressed.hasAttribute('data-court-open') ? 'court'
|
|
95
|
+
: pressed.hasAttribute('data-start') ? 'start'
|
|
96
|
+
: 'action'
|
|
97
|
+
)
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
this.render()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {string} what
|
|
106
|
+
* @param {object} detail
|
|
107
|
+
*/
|
|
108
|
+
say(what, detail = {}) {
|
|
109
|
+
this.dispatchEvent(new CustomEvent(`gameday-card:${what}`, {
|
|
110
|
+
bubbles: true,
|
|
111
|
+
detail: { match: this.match, ...detail },
|
|
112
|
+
}))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
render() {
|
|
116
|
+
const match = this.match
|
|
117
|
+
const segment = segmentOf(match)
|
|
118
|
+
const hue = hueOf(this.shell, match)
|
|
119
|
+
|
|
120
|
+
this.dataset.matchId = match.id
|
|
121
|
+
this.dataset.matchIdentifier = match.identifier ?? ''
|
|
122
|
+
this.dataset.status = match.status
|
|
123
|
+
this.dataset.segment = segment
|
|
124
|
+
|
|
125
|
+
if (movedCourt(match)) {
|
|
126
|
+
this.dataset.moved = ''
|
|
127
|
+
} else {
|
|
128
|
+
delete this.dataset.moved
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (hue !== undefined && hue !== null) {
|
|
132
|
+
this.style.setProperty('--division-hue', String(hue))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
this.write('[data-identifier]', match.identifier ?? '')
|
|
136
|
+
this.write('[data-badge]', stageOf(this.day, match))
|
|
137
|
+
this.write('[data-division]', divisionOf(this.shell, match))
|
|
138
|
+
this.write('[data-took]', tookOf(match))
|
|
139
|
+
|
|
140
|
+
this.drawSide('[data-home]', 'home', match.home_team_id)
|
|
141
|
+
this.drawSide('[data-visitor]', 'visitor', match.visitor_team_id)
|
|
142
|
+
|
|
143
|
+
this.drawScores('home')
|
|
144
|
+
this.drawScores('visitor')
|
|
145
|
+
|
|
146
|
+
this.drawAction(segment)
|
|
147
|
+
this.drawStart(segment)
|
|
148
|
+
this.drawCourt(segment)
|
|
149
|
+
|
|
150
|
+
this.title = this.summary()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @param {string} selector
|
|
155
|
+
* @param {string} text
|
|
156
|
+
*/
|
|
157
|
+
write(selector, text) {
|
|
158
|
+
const hook = this.$(selector)
|
|
159
|
+
|
|
160
|
+
if (hook) {
|
|
161
|
+
hook.textContent = text
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A side is a <tennis-participant> the template already holds, so
|
|
167
|
+
* only what it says changes. A host that dropped the element gets a
|
|
168
|
+
* fresh one in the hook it left behind.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} selector
|
|
171
|
+
* @param {string} role
|
|
172
|
+
* @param {number|null} teamId
|
|
173
|
+
*/
|
|
174
|
+
drawSide(selector, role, teamId) {
|
|
175
|
+
const hook = this.$(selector)
|
|
176
|
+
|
|
177
|
+
if (!hook) {
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
hook.replaceWith(teamCell(this.day, role, teamId))
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* One side's score: a box per set the division's format expects,
|
|
186
|
+
* beside that side's name.
|
|
187
|
+
*
|
|
188
|
+
* A finished match is read only. The result has been reported and
|
|
189
|
+
* filed, so correcting it is a decision an official makes through
|
|
190
|
+
* the board, not something a stray keystroke can do.
|
|
191
|
+
*
|
|
192
|
+
* @param {string} side
|
|
193
|
+
*/
|
|
194
|
+
drawScores(side) {
|
|
195
|
+
const column = this.$(`[data-scores][data-side="${side}"]`)
|
|
196
|
+
|
|
197
|
+
if (!column) {
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const sets = formatFor(this.shell, this.match)?.sets ?? [{}]
|
|
202
|
+
const filed = segmentOf(this.match) === 'finished'
|
|
203
|
+
|
|
204
|
+
if (filed) {
|
|
205
|
+
column.dataset.filed = ''
|
|
206
|
+
} else {
|
|
207
|
+
delete column.dataset.filed
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
column.replaceChildren(...sets.map((set, index) => scoreField({
|
|
211
|
+
set: index + 1,
|
|
212
|
+
value: this.match.sets[index]?.[`${side}_games`],
|
|
213
|
+
label: set.label,
|
|
214
|
+
disabled: filed,
|
|
215
|
+
})))
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Where the run of tabs lands, so it is the one that has to look
|
|
220
|
+
* like the button you are about to press blind.
|
|
221
|
+
*
|
|
222
|
+
* @param {string} segment
|
|
223
|
+
*/
|
|
224
|
+
drawAction(segment) {
|
|
225
|
+
const button = this.$('[data-action]')
|
|
226
|
+
|
|
227
|
+
if (!button) {
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
button.textContent = segment === 'finished' ? 'Reopen' : 'End match'
|
|
232
|
+
button.removeAttribute(segment === 'finished' ? 'lead' : 'support')
|
|
233
|
+
if (segment === 'finished') {
|
|
234
|
+
button.setAttribute('support', '')
|
|
235
|
+
}
|
|
236
|
+
else if (segment === 'started') {
|
|
237
|
+
button.setAttribute('lead', '')
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
button.setAttribute('support', '')
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Starting a match is not the thing an official came to the line to
|
|
246
|
+
* do, so it does not compete with End match for the eye. Always
|
|
247
|
+
* drawn, so a line keeps the same cells whatever state it is in.
|
|
248
|
+
*
|
|
249
|
+
* @param {string} segment
|
|
250
|
+
*/
|
|
251
|
+
drawStart(segment) {
|
|
252
|
+
const button = this.$('[data-start]')
|
|
253
|
+
|
|
254
|
+
if (!button) {
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
button.textContent = segment === 'started' ? 'Undo start' : 'Start'
|
|
259
|
+
button.removeAttribute(segment === 'started' ? 'lead' : 'support')
|
|
260
|
+
button.setAttribute(segment === 'started' ? 'support' : 'lead', '')
|
|
261
|
+
button.disabled = segment === 'finished'
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* A played match is not going to be moved anywhere, so its court is
|
|
266
|
+
* a badge and not something to press. Everything else is one button
|
|
267
|
+
* into one dialog, whether the match has started or not: moving a
|
|
268
|
+
* match is one job and not two depending on when you do it.
|
|
269
|
+
*
|
|
270
|
+
* @param {string} segment
|
|
271
|
+
*/
|
|
272
|
+
drawCourt(segment) {
|
|
273
|
+
const hook = this._court ?? this.$('[data-court-slot]')
|
|
274
|
+
|
|
275
|
+
if (!hook) {
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const said = courtName(this.shell, effectiveCourtId(this.match))
|
|
280
|
+
let control
|
|
281
|
+
|
|
282
|
+
if (segment === 'finished') {
|
|
283
|
+
control = textTag('span', 'court', said)
|
|
284
|
+
} else {
|
|
285
|
+
control = document.createElement('button')
|
|
286
|
+
control.type = 'button'
|
|
287
|
+
|
|
288
|
+
// How loudly a court asks to be looked at, in pinta's own words:
|
|
289
|
+
// the [lead] while it is being played on the court that was
|
|
290
|
+
// planned, an [extra] otherwise. Pinta draws both; the card only
|
|
291
|
+
// says which.
|
|
292
|
+
const leads = segment === 'started' && !movedCourt(this.match)
|
|
293
|
+
|
|
294
|
+
control.setAttribute(leads ? 'lead' : 'extra', '')
|
|
295
|
+
control.dataset.court = ''
|
|
296
|
+
control.dataset.courtOpen = ''
|
|
297
|
+
control.textContent = said
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
hook.replaceWith(control)
|
|
301
|
+
this._court = control
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* The line in one sentence, for a tooltip.
|
|
306
|
+
*
|
|
307
|
+
* Who against who, where, and when: the three things somebody says
|
|
308
|
+
* when they walk up to the desk. The line itself has to fit ten
|
|
309
|
+
* columns, so a name it had to cut is readable here.
|
|
310
|
+
*
|
|
311
|
+
* @returns {string}
|
|
312
|
+
*/
|
|
313
|
+
summary() {
|
|
314
|
+
const match = this.match
|
|
315
|
+
const sides = `${teamName(this.day, match.home_team_id)} vs ${teamName(this.day, match.visitor_team_id)}`
|
|
316
|
+
const when = tookOf(match) || startTimeOf(match)
|
|
317
|
+
|
|
318
|
+
const said = [sides, courtName(this.shell, effectiveCourtId(match)), when]
|
|
319
|
+
.map(part => String(part || '').trim())
|
|
320
|
+
.filter(Boolean)
|
|
321
|
+
.join(', ')
|
|
322
|
+
|
|
323
|
+
return `${stateOf(match)}: ${said}`
|
|
324
|
+
}
|
|
325
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesley@opndev.io>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { HTMLElementSugar } from '@skirbi/sugar'
|
|
6
|
+
|
|
7
|
+
import { segmentOf, startTimeOf } from '../gameday-facts.mjs'
|
|
8
|
+
import { textTag } from '../gameday-parts.mjs'
|
|
9
|
+
|
|
10
|
+
import { TennisGamedayCardElement } from './gameday-card.mjs'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* <tennis-gameday-list data-section="pending" heading="Pending">
|
|
14
|
+
*
|
|
15
|
+
* One part of the desk: the matches in one state, grouped by the time
|
|
16
|
+
* they start, each group folding away on its own.
|
|
17
|
+
*
|
|
18
|
+
* The desk has three of these and they differ only in which matches
|
|
19
|
+
* they hold and what they are called, which is why they are one
|
|
20
|
+
* component and not three blocks of the same markup.
|
|
21
|
+
*
|
|
22
|
+
* A block folds with <details> rather than a button of our own: it
|
|
23
|
+
* opens on Enter and on Space, and says whether it is open, without
|
|
24
|
+
* any of that being written here.
|
|
25
|
+
*/
|
|
26
|
+
export class TennisGamedayListElement extends HTMLElementSugar {
|
|
27
|
+
static tag = 'tennis-gameday-list'
|
|
28
|
+
|
|
29
|
+
static attributeDefs = {
|
|
30
|
+
heading: {},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
connectedCallback() {
|
|
34
|
+
super.connectedCallback()
|
|
35
|
+
|
|
36
|
+
this.appendChild(this.title())
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* What this list is called. A host that authored its own heading
|
|
41
|
+
* keeps it; the attribute is only the default.
|
|
42
|
+
*
|
|
43
|
+
* @returns {HTMLElement}
|
|
44
|
+
*/
|
|
45
|
+
title() {
|
|
46
|
+
return this.$('h2') ?? textTag('h2', 'heading', this.getConfig().heading ?? '')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Which state this list holds.
|
|
51
|
+
*
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
get segment() {
|
|
55
|
+
return this.dataset.section ?? ''
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Draw the matches that belong here, and say nothing at all when
|
|
60
|
+
* none do.
|
|
61
|
+
*
|
|
62
|
+
* @param {Array<object>} matches
|
|
63
|
+
* @param {{day: object, shell: object, folded: Set<string>, openFirst: ?number}} desk
|
|
64
|
+
* openFirst - how many blocks from the top may open. Left out, all
|
|
65
|
+
* of them.
|
|
66
|
+
*/
|
|
67
|
+
show(matches, { day, shell, folded, openFirst }) {
|
|
68
|
+
this.day = day
|
|
69
|
+
this.shell = shell
|
|
70
|
+
this.folded = folded
|
|
71
|
+
this.openFirst = openFirst ?? Infinity
|
|
72
|
+
|
|
73
|
+
this.hidden = !matches.length
|
|
74
|
+
|
|
75
|
+
// One operation, so the page loses its height once rather than
|
|
76
|
+
// once per block. The board puts the scroll back after, and it can
|
|
77
|
+
// only do that if the list stops changing height under it.
|
|
78
|
+
this.replaceChildren(this.title(), ...this.timeBlocks(matches))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @returns {Array<HTMLDetailsElement>}
|
|
83
|
+
*/
|
|
84
|
+
blocks() {
|
|
85
|
+
return this.$$('[data-time]')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @returns {boolean}
|
|
90
|
+
*/
|
|
91
|
+
anyOpen() {
|
|
92
|
+
return this.blocks().some(block => block.open)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {boolean} shut
|
|
97
|
+
*/
|
|
98
|
+
foldAll(shut) {
|
|
99
|
+
for (const block of this.blocks()) {
|
|
100
|
+
block.open = !shut
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Which blocks are folded shut right now, read off the list before
|
|
106
|
+
* it is redrawn.
|
|
107
|
+
*
|
|
108
|
+
* Asked of the DOM rather than tracked as it happens: <details> is
|
|
109
|
+
* opened by the browser, and its toggle event neither bubbles nor
|
|
110
|
+
* fires the same way everywhere. What is on screen is the one answer
|
|
111
|
+
* that is always right.
|
|
112
|
+
*/
|
|
113
|
+
rememberFolds() {
|
|
114
|
+
for (const block of this.blocks()) {
|
|
115
|
+
const key = `${this.segment}:${block.dataset.time}`
|
|
116
|
+
|
|
117
|
+
if (block.open) {
|
|
118
|
+
this.folded.delete(key)
|
|
119
|
+
} else {
|
|
120
|
+
this.folded.add(key)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The matches as start times, each holding what starts then.
|
|
127
|
+
*
|
|
128
|
+
* @param {Array<object>} matches
|
|
129
|
+
* @returns {Array<HTMLDetailsElement>}
|
|
130
|
+
*/
|
|
131
|
+
timeBlocks(matches) {
|
|
132
|
+
const byTime = new Map()
|
|
133
|
+
|
|
134
|
+
for (const match of matches) {
|
|
135
|
+
const time = startTimeOf(match)
|
|
136
|
+
|
|
137
|
+
if (!byTime.has(time)) {
|
|
138
|
+
byTime.set(time, [])
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
byTime.get(time).push(match)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return [...byTime.entries()].map(
|
|
145
|
+
([time, block], index) => this.timeBlock(time, block, index)
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {string} time
|
|
151
|
+
* @param {Array<object>} matches
|
|
152
|
+
* @param {number} index - where this block sits in the list
|
|
153
|
+
* @returns {HTMLDetailsElement}
|
|
154
|
+
*/
|
|
155
|
+
timeBlock(time, matches, index) {
|
|
156
|
+
const block = document.createElement('details')
|
|
157
|
+
const summary = document.createElement('summary')
|
|
158
|
+
const onCourt = matches.filter(match => segmentOf(match) === 'started')
|
|
159
|
+
|
|
160
|
+
summary.appendChild(textTag('span', 'blockTime', time))
|
|
161
|
+
|
|
162
|
+
// A shut block still has to say it is holding a match somebody is
|
|
163
|
+
// playing right now, because that is the one an official is about
|
|
164
|
+
// to be asked about.
|
|
165
|
+
if (onCourt.length) {
|
|
166
|
+
summary.appendChild(textTag(
|
|
167
|
+
'span', 'blockOnCourt', `${onCourt.length} on court`
|
|
168
|
+
))
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
summary.appendChild(textTag('span', 'blockCount', String(matches.length)))
|
|
172
|
+
|
|
173
|
+
block.dataset.time = time
|
|
174
|
+
block.dataset.segment = this.segment
|
|
175
|
+
block.open = index < this.openFirst
|
|
176
|
+
&& !this.folded.has(`${this.segment}:${time}`)
|
|
177
|
+
block.appendChild(summary)
|
|
178
|
+
|
|
179
|
+
for (const match of matches) {
|
|
180
|
+
block.appendChild(this.card(match))
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return block
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* @param {object} match
|
|
188
|
+
* @returns {HTMLElement}
|
|
189
|
+
*/
|
|
190
|
+
card(match) {
|
|
191
|
+
const card = document.createElement(TennisGamedayCardElement.tag)
|
|
192
|
+
|
|
193
|
+
card.match = match
|
|
194
|
+
card.day = this.day
|
|
195
|
+
card.shell = this.shell
|
|
196
|
+
|
|
197
|
+
return card
|
|
198
|
+
}
|
|
199
|
+
}
|