@opndev/tennis-match-center 0.0.1 → 0.0.3
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 +58 -1
- package/lib/auto-planner.mjs +5 -32
- package/lib/components/day-grid.mjs +300 -0
- package/lib/components/gameday-board.mjs +1789 -0
- package/lib/components/gameday-card.mjs +356 -0
- package/lib/components/gameday-list.mjs +199 -0
- package/lib/components/match.mjs +385 -0
- package/lib/components/participant.mjs +101 -0
- package/lib/components/planning-board.mjs +914 -131
- package/lib/components/roster-board.mjs +42 -2
- 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
package/Changes
CHANGED
|
@@ -1,4 +1,61 @@
|
|
|
1
|
-
Revision history for@opndev/tennis-match-center
|
|
1
|
+
Revision history for @opndev/tennis-match-center
|
|
2
|
+
|
|
3
|
+
0.0.3 2026-10-08 15:31:37Z
|
|
4
|
+
|
|
5
|
+
* Auto score on the gameday desk. Enter in a score box fills the other
|
|
6
|
+
side with 7 when one side is under 7 and the other is empty, then
|
|
7
|
+
ends the match. Leaving a score box, by Tab or a click, only
|
|
8
|
+
prefills the 7 and leaves the match open, so a click on the end
|
|
9
|
+
button is not lost. With both boxes filled, Enter just ends the
|
|
10
|
+
match.
|
|
11
|
+
|
|
12
|
+
* The next-on-court dialog lists the match planned for the court that
|
|
13
|
+
came free first and picks it by default, with the longest wait as
|
|
14
|
+
the alternative below it. Enter in the dialog presses Play, and a
|
|
15
|
+
held Enter that repeats into the dialog is ignored.
|
|
16
|
+
|
|
17
|
+
* The match card says where a match is next to its court: Not before a
|
|
18
|
+
time, Go to court, On court or Final. A host states it with the new
|
|
19
|
+
`state` and `not-before` attributes, because next up depends on the
|
|
20
|
+
other matches on the court. Without a state the card reads started
|
|
21
|
+
and ended as before, and a walkover or retirement still says what
|
|
22
|
+
it was.
|
|
23
|
+
|
|
24
|
+
* The roster board says when each group starts: the first planned
|
|
25
|
+
match of each day it plays, from a `starts` list the roster data
|
|
26
|
+
can carry, such as `Mon 10 Nov, 13:00 / Tue 11 Nov, 09:30`. A group
|
|
27
|
+
nothing is planned for says nothing.
|
|
28
|
+
|
|
29
|
+
* The day grid sizes its hours and cells by the CSS variable
|
|
30
|
+
--planning-scale. A host can make a day more compact or roomier
|
|
31
|
+
with one variable, and a day already drawn follows it without a
|
|
32
|
+
redraw. The default scale is 1, as before.
|
|
33
|
+
|
|
34
|
+
0.0.2 2026-09-21 03:14:21Z
|
|
35
|
+
|
|
36
|
+
* Add <tennis-gameday-board>, the officials desk: one day split into
|
|
37
|
+
finished, started and pending, with a score box per set the
|
|
38
|
+
division's format expects. Keyboard first, a line tabs score, other
|
|
39
|
+
score, end match, and ending one puts the desk on the next match
|
|
40
|
+
waiting. Changes live in the browser only until the write endpoints
|
|
41
|
+
exist.
|
|
42
|
+
|
|
43
|
+
* Add <tennis-match> and <tennis-participant>: one match with its
|
|
44
|
+
court, stage, state and score, and the two sides that played it.
|
|
45
|
+
The winner is derived from the sets and marked with a `won`
|
|
46
|
+
attribute. Any child carrying `side` is treated as a side, so an
|
|
47
|
+
application with its own participant element keeps using it. Laid
|
|
48
|
+
out like the scheduling board's cell: stage badge, division, then
|
|
49
|
+
both sides on shared rows with the vs between them.
|
|
50
|
+
|
|
51
|
+
* Declare @skirbi/semtic as a peer dependency. The planning and
|
|
52
|
+
schedule boards have built <semtic-tabbed> since they were written
|
|
53
|
+
without the package saying so.
|
|
54
|
+
|
|
55
|
+
* A started match no longer changes court from a select. The court
|
|
56
|
+
becomes a button that opens a <semtic-modal> to confirm the move,
|
|
57
|
+
with a reset back to the planned court. The planned court is never
|
|
58
|
+
rewritten.
|
|
2
59
|
|
|
3
60
|
0.0.1 2026-09-16 00:12:43Z
|
|
4
61
|
|
package/lib/auto-planner.mjs
CHANGED
|
@@ -67,6 +67,9 @@ import { MATCH_MINUTES } from './match-time.mjs'
|
|
|
67
67
|
* params.courts
|
|
68
68
|
* @param {Array<{key: string, lit_required: boolean}>} params.slots
|
|
69
69
|
* Hourly columns, in order.
|
|
70
|
+
* @param {number} [params.lightBuffer] How long before dark an unlit
|
|
71
|
+
* court stops taking new play. Zero holds one open until the light
|
|
72
|
+
* itself goes; left out, PlanningDay's own default applies.
|
|
70
73
|
* @param {string[]} params.days Y-m-d, in order.
|
|
71
74
|
* @param {Array<Object>} params.competitions Each carries id, is_pro,
|
|
72
75
|
* games_needed {group, knockout}, games_needed_by_date,
|
|
@@ -93,6 +96,7 @@ export function autoFillPlanning(params) {
|
|
|
93
96
|
courtOverrides = [],
|
|
94
97
|
darkFrom = null,
|
|
95
98
|
dayEnd = '24:00',
|
|
99
|
+
lightBuffer = undefined,
|
|
96
100
|
groupSlotsFromSessions = false,
|
|
97
101
|
splitRounds = false,
|
|
98
102
|
} = params
|
|
@@ -828,6 +832,7 @@ export function autoFillPlanning(params) {
|
|
|
828
832
|
occupancy: taken,
|
|
829
833
|
plan,
|
|
830
834
|
darkFromMinutes,
|
|
835
|
+
lightBufferMinutes: lightBuffer,
|
|
831
836
|
})
|
|
832
837
|
|
|
833
838
|
assignKnockout(day, {
|
|
@@ -888,26 +893,6 @@ export function autoFillPlanning(params) {
|
|
|
888
893
|
return knockoutOverflow(competitions, retried) < overflow ? retried : result
|
|
889
894
|
}
|
|
890
895
|
|
|
891
|
-
/**
|
|
892
|
-
* How much of the event's knockout did not fit where it was meant to:
|
|
893
|
-
* capacity nobody could place at all, plus every cell that landed on
|
|
894
|
-
* a day its competition's knockout was never scheduled for.
|
|
895
|
-
*
|
|
896
|
-
* A competition with no knockout session of its own is not counted -
|
|
897
|
-
* it was never given a day to overflow from.
|
|
898
|
-
*/
|
|
899
|
-
/**
|
|
900
|
-
* When a session opens, allowing for the rhythm runs are laid out on.
|
|
901
|
-
*
|
|
902
|
-
* Runs start on a section boundary, so a session opening part way
|
|
903
|
-
* through one is due on at the next: that is the rhythm, not lateness.
|
|
904
|
-
*
|
|
905
|
-
* @param {Array} own The session's own slots, in order.
|
|
906
|
-
* @param {string} startTime The session's configured start.
|
|
907
|
-
* @param {number} runLength Slots to a run in this phase.
|
|
908
|
-
* @returns {?Object} The slot it is due on, or null where the day has
|
|
909
|
-
* no such slot at all.
|
|
910
|
-
*/
|
|
911
896
|
/**
|
|
912
897
|
* Whether a group run has to have a lit court.
|
|
913
898
|
*
|
|
@@ -1092,18 +1077,6 @@ function groupKey(competitionId, date) {
|
|
|
1092
1077
|
}
|
|
1093
1078
|
|
|
1094
1079
|
|
|
1095
|
-
/**
|
|
1096
|
-
* Hands out one court per eligible entry per pass, looping until
|
|
1097
|
-
* either the court list or every entry's remaining need is exhausted -
|
|
1098
|
-
* so a lone eligible competition gets every court offered, and several
|
|
1099
|
-
* eligible competitions split them round by round in priority order.
|
|
1100
|
-
*
|
|
1101
|
-
* A group-phase entry claims a whole run of consecutive slots on one
|
|
1102
|
-
* court at once (group_run_length - a 5-team group's 5 matches), and
|
|
1103
|
-
* only on a court that can host every slot of that run. A court that
|
|
1104
|
-
* runs out of light partway through cannot host the group at all, so
|
|
1105
|
-
* it is never given the run's opening slot either.
|
|
1106
|
-
*/
|
|
1107
1080
|
/**
|
|
1108
1081
|
* Which queued run belongs on a court that has already been taken.
|
|
1109
1082
|
*
|
|
@@ -0,0 +1,300 @@
|
|
|
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
|
+
/**
|
|
8
|
+
* Pixels per minute of real time. A 45-minute slot - the shortest
|
|
9
|
+
* common match length - needs room for four lines (competition,
|
|
10
|
+
* division, group, round) plus padding.
|
|
11
|
+
*/
|
|
12
|
+
export const PX_PER_MINUTE = 64 / 45
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The stretch of the day worth drawing: the earliest row's start to
|
|
16
|
+
* the latest row's end.
|
|
17
|
+
*
|
|
18
|
+
* @param {Array} rows
|
|
19
|
+
* @returns {{start: number, end: number}}
|
|
20
|
+
*/
|
|
21
|
+
export function dayRange(rows) {
|
|
22
|
+
const starts = rows.map(row => row.start_minutes).filter(minutes => minutes != null)
|
|
23
|
+
const ends = rows.map(row => row.end_minutes).filter(minutes => minutes != null)
|
|
24
|
+
|
|
25
|
+
return starts.length === 0
|
|
26
|
+
? { start: 0, end: 0 }
|
|
27
|
+
: { start: Math.min(...starts), end: Math.max(...ends) }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* <tennis-day-grid>
|
|
32
|
+
*
|
|
33
|
+
* One day of one board: the hours down the side, a column per court,
|
|
34
|
+
* and a cell for every slot, drawn to scale from real minutes rather
|
|
35
|
+
* than as uniform rows - a 90-minute match reads twice as tall as a
|
|
36
|
+
* 45-minute one, and an idle stretch between two competitions' clocks
|
|
37
|
+
* shows as real blank space.
|
|
38
|
+
*
|
|
39
|
+
* The chrome only. What goes in a cell, and what any gesture on it
|
|
40
|
+
* means, belongs to whichever board is drawing: the grid reports that
|
|
41
|
+
* a cell was pressed, entered or right-clicked and leaves the rest
|
|
42
|
+
* alone. Planning refuses a drop onto an occupied cell where
|
|
43
|
+
* Scheduling swaps the two, and neither rule belongs here.
|
|
44
|
+
*/
|
|
45
|
+
export class TennisDayGridElement extends HTMLElementSugar {
|
|
46
|
+
static tag = 'tennis-day-grid'
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Draws a day, replacing whatever was drawn before.
|
|
50
|
+
*
|
|
51
|
+
* @param {Object} day
|
|
52
|
+
* @param {string} day.date
|
|
53
|
+
* @param {Array} day.courts
|
|
54
|
+
* @param {Array} day.rows Slots, in order.
|
|
55
|
+
* @param {Function} day.cellFor Called per court and row, and hands
|
|
56
|
+
* back what that cell is: `blocked` for a cell that may not be
|
|
57
|
+
* used at all, `minutes` where what is in it runs longer than the
|
|
58
|
+
* row, `content` for the element to put inside it, and `filled`
|
|
59
|
+
* where what is in it counts as play - a scheduled cell waiting
|
|
60
|
+
* for a match draws something and holds nothing.
|
|
61
|
+
* @param {number} [day.scale] Pixels per minute. A board whose cell
|
|
62
|
+
* carries four names needs more room per hour than one carrying a
|
|
63
|
+
* division and a group.
|
|
64
|
+
* @param {boolean} [day.compact] Narrow the courts nothing is on.
|
|
65
|
+
*/
|
|
66
|
+
draw({ date, courts, rows, cellFor, scale = PX_PER_MINUTE, compact = false }) {
|
|
67
|
+
const range = dayRange(rows)
|
|
68
|
+
|
|
69
|
+
this.date = date
|
|
70
|
+
this.rows = rows
|
|
71
|
+
this.scale = scale
|
|
72
|
+
this.dataset.day = date
|
|
73
|
+
this.setAttribute('date', date)
|
|
74
|
+
this.toggleAttribute('compact', compact)
|
|
75
|
+
this.style.setProperty('--planning-day-height', this.lengthFor(range.end - range.start))
|
|
76
|
+
|
|
77
|
+
this.replaceChildren(
|
|
78
|
+
this.timeColumn(rows, range),
|
|
79
|
+
...courts.map((court, courtIndex) =>
|
|
80
|
+
this.courtColumn(court, courtIndex, rows, range, cellFor)
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The hours down the side. Its own column rather than a cell in
|
|
87
|
+
* every row, so a court knows nothing about how the day is labelled.
|
|
88
|
+
*/
|
|
89
|
+
timeColumn(rows, range) {
|
|
90
|
+
const column = document.createElement('planning-times')
|
|
91
|
+
|
|
92
|
+
// Stands in for the court name, so the hours line up with the
|
|
93
|
+
// slots rather than riding a row above them.
|
|
94
|
+
column.appendChild(document.createElement('planning-times-head'))
|
|
95
|
+
|
|
96
|
+
const body = document.createElement('planning-rows')
|
|
97
|
+
body.style.setProperty('--planning-day-height', this.lengthFor(range.end - range.start))
|
|
98
|
+
|
|
99
|
+
for (const slot of rows) {
|
|
100
|
+
const time = document.createElement('planning-time')
|
|
101
|
+
|
|
102
|
+
time.setAttribute('start', slot.key)
|
|
103
|
+
time.style.top = this.lengthFor(slot.start_minutes - range.start)
|
|
104
|
+
time.style.height = this.lengthFor(slot.end_minutes - slot.start_minutes)
|
|
105
|
+
time.textContent = slot.label ?? slot.key
|
|
106
|
+
|
|
107
|
+
column.appendChild(body).appendChild(time)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return column
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
courtColumn(court, courtIndex, rows, range, cellFor) {
|
|
114
|
+
const column = document.createElement('planning-court')
|
|
115
|
+
|
|
116
|
+
column.setAttribute('court', court.name)
|
|
117
|
+
column.dataset.courtId = court.id
|
|
118
|
+
|
|
119
|
+
if (court.lit) {
|
|
120
|
+
column.setAttribute('lit', '')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (court.pro) {
|
|
124
|
+
column.setAttribute('pro', '')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const heading = document.createElement('planning-court-name')
|
|
128
|
+
heading.textContent = court.name
|
|
129
|
+
column.appendChild(heading)
|
|
130
|
+
|
|
131
|
+
const body = document.createElement('planning-rows')
|
|
132
|
+
body.style.setProperty('--planning-day-height', this.lengthFor(range.end - range.start))
|
|
133
|
+
|
|
134
|
+
// A row's own span is only ever the finest clock running that day
|
|
135
|
+
// - a real match longer than that (Team Cup's 90 minutes among
|
|
136
|
+
// someone else's 45) draws its own cell taller instead, covering
|
|
137
|
+
// however many of these rows its real length reaches. Those
|
|
138
|
+
// covered rows are this court's alone to skip.
|
|
139
|
+
let coveredUntil = -Infinity
|
|
140
|
+
let played = false
|
|
141
|
+
|
|
142
|
+
rows.forEach((slot, rowIndex) => {
|
|
143
|
+
if (slot.start_minutes < coveredUntil) {
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const cell = cellFor(court, courtIndex, slot, rowIndex) ?? {}
|
|
148
|
+
const element = this.cellElement(cell, slot, rowIndex, courtIndex, range)
|
|
149
|
+
|
|
150
|
+
played = played || (cell.filled ?? Boolean(cell.content))
|
|
151
|
+
|
|
152
|
+
body.appendChild(element)
|
|
153
|
+
|
|
154
|
+
if (!cell.blocked && slot.start_minutes != null) {
|
|
155
|
+
coveredUntil = slot.start_minutes + (cell.minutes ?? (slot.end_minutes - slot.start_minutes))
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
column.appendChild(body)
|
|
160
|
+
|
|
161
|
+
// A court with nothing on it from the first row to the last is
|
|
162
|
+
// still a court - it says where the row is and it can be dropped
|
|
163
|
+
// on - but it need not hold the width of one that is working. The
|
|
164
|
+
// grid only says so; what that is worth is the stylesheet's.
|
|
165
|
+
column.toggleAttribute('empty', !played)
|
|
166
|
+
|
|
167
|
+
return column
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
cellElement(cell, slot, rowIndex, courtIndex, range) {
|
|
171
|
+
const element = document.createElement('planning-slot')
|
|
172
|
+
const minutes = cell.minutes ?? (slot.end_minutes - slot.start_minutes)
|
|
173
|
+
|
|
174
|
+
element.setAttribute('start', slot.key)
|
|
175
|
+
element.style.top = this.lengthFor(slot.start_minutes - range.start)
|
|
176
|
+
element.style.height = this.lengthFor(minutes)
|
|
177
|
+
|
|
178
|
+
if (slot.start_minutes != null) {
|
|
179
|
+
element.setAttribute('duration', minutes)
|
|
180
|
+
element.dataset.spanEnd = slot.start_minutes + minutes
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// A blocked cell is chrome and nothing else: no row, no column,
|
|
184
|
+
// nothing to press. Every board agrees on that much.
|
|
185
|
+
if (cell.blocked) {
|
|
186
|
+
element.setAttribute('blocked', '')
|
|
187
|
+
|
|
188
|
+
return element
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
element.dataset.row = rowIndex
|
|
192
|
+
element.dataset.col = courtIndex
|
|
193
|
+
|
|
194
|
+
element.addEventListener('mousedown', event => {
|
|
195
|
+
this.report('press', event, rowIndex, courtIndex, {
|
|
196
|
+
// Whether the press landed on what is in the cell rather than
|
|
197
|
+
// on the cell itself, which is the difference between taking
|
|
198
|
+
// hold of a match and reaching past it to the grid.
|
|
199
|
+
onContent: event.target !== element,
|
|
200
|
+
button: event.button,
|
|
201
|
+
ctrlKey: event.ctrlKey,
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
element.addEventListener('mouseenter', event => {
|
|
206
|
+
this.report('enter', event, rowIndex, courtIndex)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
element.addEventListener('contextmenu', event => {
|
|
210
|
+
this.report('menu', event, rowIndex, courtIndex, {
|
|
211
|
+
x: event.clientX ?? 0,
|
|
212
|
+
y: event.clientY ?? 0,
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
if (cell.content) {
|
|
217
|
+
element.appendChild(cell.content)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return element
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
report(what, event, row, col, extra = {}) {
|
|
224
|
+
if (what !== 'enter') {
|
|
225
|
+
event.preventDefault()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
this.dispatchEvent(new CustomEvent(`day-grid:${what}`, {
|
|
229
|
+
bubbles: true,
|
|
230
|
+
detail: { date: this.date, row, col, ...extra },
|
|
231
|
+
}))
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
pxFor(minutes) {
|
|
235
|
+
return minutes * (this.scale ?? PX_PER_MINUTE)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* The same size as a CSS length that follows --planning-scale, so a
|
|
240
|
+
* host can make the whole day more compact (or roomier) with one
|
|
241
|
+
* variable, and a day already drawn follows it without a redraw.
|
|
242
|
+
*
|
|
243
|
+
* @param {number} minutes
|
|
244
|
+
* @returns {string}
|
|
245
|
+
*/
|
|
246
|
+
lengthFor(minutes) {
|
|
247
|
+
return `calc(${this.pxFor(minutes)}px * var(--planning-scale, 1))`
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Shows what is being dragged and where it would land. The boards
|
|
252
|
+
* decide what a drag means; showing one is the same job on both.
|
|
253
|
+
*
|
|
254
|
+
* @param {?Set<string>} from Cells being carried.
|
|
255
|
+
* @param {?string} over Cell under the cursor.
|
|
256
|
+
*/
|
|
257
|
+
showDrag(from, over) {
|
|
258
|
+
this.toggleAttribute('dragging', Boolean(from?.size))
|
|
259
|
+
|
|
260
|
+
for (const cell of this.querySelectorAll('planning-slot[data-row]')) {
|
|
261
|
+
const key = `${cell.dataset.row},${cell.dataset.col}`
|
|
262
|
+
|
|
263
|
+
cell.toggleAttribute('data-dragging', Boolean(from?.has(key)))
|
|
264
|
+
cell.toggleAttribute('data-drop', over === key && !from?.has(key))
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Shows which cells are picked out. Toggled directly rather than by
|
|
270
|
+
* redrawing, so dragging a selection across a tournament's worth of
|
|
271
|
+
* cells stays smooth.
|
|
272
|
+
*
|
|
273
|
+
* @param {Set<string>} keys "row,col"
|
|
274
|
+
*/
|
|
275
|
+
paint(keys) {
|
|
276
|
+
for (const cell of this.querySelectorAll('planning-slot[data-row]')) {
|
|
277
|
+
cell.toggleAttribute(
|
|
278
|
+
'data-selected',
|
|
279
|
+
keys.has(`${cell.dataset.row},${cell.dataset.col}`)
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* The cells as drawn, court by court, for a board that has
|
|
286
|
+
* something to say about a run of them.
|
|
287
|
+
*
|
|
288
|
+
* @returns {Array}
|
|
289
|
+
*/
|
|
290
|
+
columns() {
|
|
291
|
+
return [...this.querySelectorAll('planning-court')].map(column => ({
|
|
292
|
+
courtId: Number(column.dataset.courtId),
|
|
293
|
+
cells: [...column.querySelectorAll('planning-slot')].map(element => ({
|
|
294
|
+
element,
|
|
295
|
+
row: element.dataset.row == null ? null : Number(element.dataset.row),
|
|
296
|
+
})),
|
|
297
|
+
}))
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|