foldkit-mixins-data-grid 0.2.14 → 0.3.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/dist/fillDrag.d.ts +30 -0
- package/dist/fillDrag.d.ts.map +1 -0
- package/dist/fillDrag.js +178 -0
- package/dist/fillDrag.js.map +1 -0
- package/dist/headerDrag.d.ts +34 -0
- package/dist/headerDrag.d.ts.map +1 -0
- package/dist/headerDrag.js +112 -0
- package/dist/headerDrag.js.map +1 -0
- package/dist/holdFocus.d.ts +11 -0
- package/dist/holdFocus.d.ts.map +1 -0
- package/dist/holdFocus.js +31 -0
- package/dist/holdFocus.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/keepFocus.d.ts +9 -0
- package/dist/keepFocus.d.ts.map +1 -0
- package/dist/keepFocus.js +20 -0
- package/dist/keepFocus.js.map +1 -0
- package/dist/marks.d.ts +69 -0
- package/dist/marks.d.ts.map +1 -0
- package/dist/marks.js +90 -0
- package/dist/marks.js.map +1 -0
- package/dist/moreOnScroll.d.ts +12 -0
- package/dist/moreOnScroll.d.ts.map +1 -0
- package/dist/moreOnScroll.js +17 -0
- package/dist/moreOnScroll.js.map +1 -0
- package/dist/nearEnd.d.ts +15 -0
- package/dist/nearEnd.d.ts.map +1 -0
- package/dist/nearEnd.js +33 -0
- package/dist/nearEnd.js.map +1 -0
- package/dist/press.d.ts +31 -0
- package/dist/press.d.ts.map +1 -0
- package/dist/press.js +53 -0
- package/dist/press.js.map +1 -0
- package/dist/slots.d.ts +85 -0
- package/dist/slots.d.ts.map +1 -0
- package/dist/slots.js +289 -0
- package/dist/slots.js.map +1 -0
- package/dist/style.d.ts +555 -0
- package/dist/style.d.ts.map +1 -0
- package/dist/style.js +224 -0
- package/dist/style.js.map +1 -0
- package/dist/view.d.ts +140 -0
- package/dist/view.d.ts.map +1 -0
- package/dist/{index.mjs → view.js} +10 -995
- package/dist/view.js.map +1 -0
- package/package.json +12 -12
- package/dist/index.d.mts +0 -321
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs.map +0 -1
|
@@ -1,906 +1,14 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Theme } from "foldkit-mixins/theme";
|
|
1
|
+
import { Match, Option } from "effect";
|
|
3
2
|
import * as Mount from "foldkit/mount";
|
|
4
|
-
import {
|
|
3
|
+
import { GridSlots } from "./slots.js";
|
|
4
|
+
import { SlotView } from "foldkit-mixins";
|
|
5
|
+
import { fillHandleAttribute } from "./fillDrag.js";
|
|
5
6
|
import { CellEditor, Clipboard, Columns, Fill, GridFocus, GridSelection, GridViewport, RowCount, RowStatus, VirtualGrid, addressableRows } from "foldkit-data-grid";
|
|
6
7
|
import { Move } from "foldkit-primitives/dom";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const AriaColIndex = Attr.make("aria-colindex");
|
|
12
|
-
const AriaActiveDescendant = Attr.make("aria-activedescendant");
|
|
13
|
-
const Tabindex = Attr.make("tabindex");
|
|
14
|
-
const AriaValueNow = Attr.make("aria-valuenow");
|
|
15
|
-
const AriaValueMin = Attr.make("aria-valuemin");
|
|
16
|
-
const AriaValueMax = Attr.make("aria-valuemax");
|
|
17
|
-
const Id = Attr.make("id");
|
|
18
|
-
const MouseDown = Event.make("mousedown");
|
|
19
|
-
const AriaHasPopup = Attr.make("aria-haspopup");
|
|
20
|
-
const geometry = [
|
|
21
|
-
"boxSizing",
|
|
22
|
-
"width",
|
|
23
|
-
"height",
|
|
24
|
-
"minWidth",
|
|
25
|
-
"maxWidth",
|
|
26
|
-
"flex",
|
|
27
|
-
"position",
|
|
28
|
-
"insetInlineStart",
|
|
29
|
-
"insetInlineEnd",
|
|
30
|
-
"top"
|
|
31
|
-
];
|
|
32
|
-
const GridSlots = Slots.define({
|
|
33
|
-
/** The scroll container: `role="grid"`, the one tab stop, and the keyboard. */
|
|
34
|
-
root: Slot.make({
|
|
35
|
-
capability: Capability.Focusable,
|
|
36
|
-
events: [Event.KeyDown],
|
|
37
|
-
attributes: [
|
|
38
|
-
Attr.Role,
|
|
39
|
-
Attr.AriaLabel,
|
|
40
|
-
AriaRowCount,
|
|
41
|
-
AriaColCount,
|
|
42
|
-
AriaActiveDescendant
|
|
43
|
-
],
|
|
44
|
-
protected: {
|
|
45
|
-
events: [Event.KeyDown],
|
|
46
|
-
attributes: [
|
|
47
|
-
Attr.Role,
|
|
48
|
-
Tabindex,
|
|
49
|
-
Id,
|
|
50
|
-
AriaRowCount,
|
|
51
|
-
AriaColCount,
|
|
52
|
-
AriaActiveDescendant
|
|
53
|
-
],
|
|
54
|
-
style: ["overflow", "position"]
|
|
55
|
-
}
|
|
56
|
-
}),
|
|
57
|
-
/** The header row's group; it stays at the top while the body scrolls. */
|
|
58
|
-
header: Slot.make({
|
|
59
|
-
capability: Capability.Container,
|
|
60
|
-
protected: {
|
|
61
|
-
attributes: [Attr.Role],
|
|
62
|
-
style: ["position", "top"]
|
|
63
|
-
}
|
|
64
|
-
}),
|
|
65
|
-
headerRow: Slot.make({
|
|
66
|
-
capability: Capability.Container,
|
|
67
|
-
protected: {
|
|
68
|
-
attributes: [Attr.Role, AriaRowIndex],
|
|
69
|
-
style: geometry
|
|
70
|
-
}
|
|
71
|
-
}),
|
|
72
|
-
/**
|
|
73
|
-
* One per drawn column. `data-pinned` is `start` or `end` on a pinned
|
|
74
|
-
* column; while a header is dragged it has `data-dragging` and follows the
|
|
75
|
-
* pointer by a `transform`, and the shown neighbour it would land beside
|
|
76
|
-
* has `data-drop`, `before` or `after`.
|
|
77
|
-
*/
|
|
78
|
-
headerCell: Slot.make({
|
|
79
|
-
capability: Capability.Base,
|
|
80
|
-
protected: {
|
|
81
|
-
attributes: [Attr.Role, AriaColIndex],
|
|
82
|
-
style: [...geometry, "transform"]
|
|
83
|
-
}
|
|
84
|
-
}),
|
|
85
|
-
/**
|
|
86
|
-
* A column's resize handle at its end edge: a `role="separator"` with its
|
|
87
|
-
* width as `aria-valuenow`, dragged with the pointer, or stepped with the
|
|
88
|
-
* arrow keys once focused. It is out of the tab order, so the grid stays
|
|
89
|
-
* one tab stop. Only resizable columns have one.
|
|
90
|
-
*/
|
|
91
|
-
resizeHandle: Slot.make({
|
|
92
|
-
capability: Capability.Focusable,
|
|
93
|
-
events: [Event.KeyDown],
|
|
94
|
-
protected: {
|
|
95
|
-
events: [Event.KeyDown],
|
|
96
|
-
attributes: [
|
|
97
|
-
Attr.Role,
|
|
98
|
-
Tabindex,
|
|
99
|
-
AriaValueNow,
|
|
100
|
-
AriaValueMin,
|
|
101
|
-
AriaValueMax
|
|
102
|
-
],
|
|
103
|
-
style: [
|
|
104
|
-
"position",
|
|
105
|
-
"insetInlineEnd",
|
|
106
|
-
"top",
|
|
107
|
-
"bottom"
|
|
108
|
-
]
|
|
109
|
-
}
|
|
110
|
-
}),
|
|
111
|
-
body: Slot.make({
|
|
112
|
-
capability: Capability.Container,
|
|
113
|
-
protected: {
|
|
114
|
-
attributes: [Attr.Role],
|
|
115
|
-
style: geometry
|
|
116
|
-
}
|
|
117
|
-
}),
|
|
118
|
-
/** One per drawn row; `aria-rowindex` is its place among every row, drawn or not. */
|
|
119
|
-
row: Slot.make({
|
|
120
|
-
capability: Capability.Container,
|
|
121
|
-
protected: {
|
|
122
|
-
attributes: [Attr.Role, AriaRowIndex],
|
|
123
|
-
style: geometry
|
|
124
|
-
}
|
|
125
|
-
}),
|
|
126
|
-
/** One per drawn cell; `data-focused` marks the current one, `data-pinned` a pinned one. */
|
|
127
|
-
cell: Slot.make({
|
|
128
|
-
capability: Capability.Base,
|
|
129
|
-
events: [MouseDown],
|
|
130
|
-
protected: {
|
|
131
|
-
events: [MouseDown],
|
|
132
|
-
attributes: [
|
|
133
|
-
Attr.Role,
|
|
134
|
-
Id,
|
|
135
|
-
AriaColIndex
|
|
136
|
-
],
|
|
137
|
-
style: geometry
|
|
138
|
-
}
|
|
139
|
-
}),
|
|
140
|
-
/** A row counted but not loaded yet, drawn at its height for a skeleton to style. */
|
|
141
|
-
placeholder: Slot.make({
|
|
142
|
-
capability: Capability.Base,
|
|
143
|
-
protected: { style: ["height", "width"] }
|
|
144
|
-
}),
|
|
145
|
-
/**
|
|
146
|
-
* The text field a cell is edited in, drawn in place of its content while
|
|
147
|
-
* the edit lasts; `aria-invalid` when the column refused the draft.
|
|
148
|
-
*/
|
|
149
|
-
editor: Slot.make({
|
|
150
|
-
capability: Capability.TextInput,
|
|
151
|
-
events: [
|
|
152
|
-
Event.Input,
|
|
153
|
-
Event.KeyDown,
|
|
154
|
-
Event.Blur
|
|
155
|
-
],
|
|
156
|
-
protected: { events: [
|
|
157
|
-
Event.Input,
|
|
158
|
-
Event.KeyDown,
|
|
159
|
-
Event.Blur
|
|
160
|
-
] }
|
|
161
|
-
}),
|
|
162
|
-
/**
|
|
163
|
-
* Why the column refused a draft, shown below the field while the edit
|
|
164
|
-
* lasts; the field names it with `aria-describedby`.
|
|
165
|
-
*/
|
|
166
|
-
editorError: Slot.make({ capability: Capability.Base }),
|
|
167
|
-
/**
|
|
168
|
-
* The editor of a column whose schema's text is one of a few literals,
|
|
169
|
-
* drawn in place of the cell's content while the edit lasts, like
|
|
170
|
-
* `editor`: a `role="combobox"` showing the draft, its options in
|
|
171
|
-
* `choiceList` (or, with `choiceEditor: 'native'`, a `select`).
|
|
172
|
-
*/
|
|
173
|
-
choice: Slot.make({
|
|
174
|
-
capability: Capability.Focusable,
|
|
175
|
-
events: [
|
|
176
|
-
Event.Change,
|
|
177
|
-
Event.KeyDown,
|
|
178
|
-
Event.Blur
|
|
179
|
-
],
|
|
180
|
-
protected: {
|
|
181
|
-
events: [
|
|
182
|
-
Event.Change,
|
|
183
|
-
Event.KeyDown,
|
|
184
|
-
Event.Blur
|
|
185
|
-
],
|
|
186
|
-
attributes: [
|
|
187
|
-
Attr.Role,
|
|
188
|
-
Tabindex,
|
|
189
|
-
AriaActiveDescendant
|
|
190
|
-
]
|
|
191
|
-
}
|
|
192
|
-
}),
|
|
193
|
-
/**
|
|
194
|
-
* A choice's options, `role="listbox"`, below the cell, or above it when
|
|
195
|
-
* the cell is low in the view (`data-place="above"`).
|
|
196
|
-
*/
|
|
197
|
-
choiceList: Slot.make({
|
|
198
|
-
capability: Capability.Base,
|
|
199
|
-
protected: {
|
|
200
|
-
attributes: [Attr.Role, Id],
|
|
201
|
-
style: ["position"]
|
|
202
|
-
}
|
|
203
|
-
}),
|
|
204
|
-
/** One literal a `choice` offers: `role="option"`, `aria-selected` on the draft. */
|
|
205
|
-
choiceOption: Slot.make({
|
|
206
|
-
capability: Capability.Base,
|
|
207
|
-
events: [Event.Click],
|
|
208
|
-
protected: {
|
|
209
|
-
events: [Event.Click],
|
|
210
|
-
attributes: [Attr.Role, Id]
|
|
211
|
-
}
|
|
212
|
-
}),
|
|
213
|
-
/** Said in place of rows when there are none: empty, loading, or failed. */
|
|
214
|
-
status: Slot.make({ capability: Capability.Base }),
|
|
215
|
-
/**
|
|
216
|
-
* The button on a column's header that opens its menu, when the view is
|
|
217
|
-
* given `columnMenu`: `aria-haspopup="menu"` and `aria-expanded`, out of
|
|
218
|
-
* the tab order like the grid's other controls.
|
|
219
|
-
*/
|
|
220
|
-
menuButton: Slot.make({
|
|
221
|
-
capability: Capability.Interactive,
|
|
222
|
-
events: [Event.Click],
|
|
223
|
-
attributes: [Attr.AriaLabel],
|
|
224
|
-
protected: {
|
|
225
|
-
events: [Event.Click],
|
|
226
|
-
attributes: [
|
|
227
|
-
AriaHasPopup,
|
|
228
|
-
Attr.AriaExpanded,
|
|
229
|
-
Tabindex
|
|
230
|
-
]
|
|
231
|
-
}
|
|
232
|
-
}),
|
|
233
|
-
/**
|
|
234
|
-
* A column's open menu, below its header: a `role="menu"` that takes focus
|
|
235
|
-
* and points at its active item with `aria-activedescendant`, as the grid
|
|
236
|
-
* points at its cell. Focus leaving it closes it.
|
|
237
|
-
*/
|
|
238
|
-
menu: Slot.make({
|
|
239
|
-
capability: Capability.Focusable,
|
|
240
|
-
events: [Event.KeyDown],
|
|
241
|
-
protected: {
|
|
242
|
-
events: [Event.KeyDown],
|
|
243
|
-
attributes: [
|
|
244
|
-
Attr.Role,
|
|
245
|
-
Tabindex,
|
|
246
|
-
AriaActiveDescendant
|
|
247
|
-
],
|
|
248
|
-
style: ["position"]
|
|
249
|
-
}
|
|
250
|
-
}),
|
|
251
|
-
/** One of a menu's items; `data-active` marks the one the keyboard is on. */
|
|
252
|
-
menuItem: Slot.make({
|
|
253
|
-
capability: Capability.Base,
|
|
254
|
-
events: [Event.Click],
|
|
255
|
-
protected: {
|
|
256
|
-
events: [Event.Click],
|
|
257
|
-
attributes: [Attr.Role, Id]
|
|
258
|
-
}
|
|
259
|
-
}),
|
|
260
|
-
/**
|
|
261
|
-
* The button on a sortable column's header: a click sends its sort Message.
|
|
262
|
-
* `data-sort` is `asc` or `desc` while the column is sorted.
|
|
263
|
-
*/
|
|
264
|
-
sort: Slot.make({
|
|
265
|
-
capability: Capability.Interactive,
|
|
266
|
-
events: [Event.Click]
|
|
267
|
-
}),
|
|
268
|
-
/**
|
|
269
|
-
* The square at the end corner of the range, or of the focused cell, in a
|
|
270
|
-
* grid that edits: dragged, it fills the cells it is drawn over. Pointer
|
|
271
|
-
* only and hidden from assistive technology; Ctrl+D and Ctrl+R fill by key.
|
|
272
|
-
*/
|
|
273
|
-
fillHandle: Slot.make({
|
|
274
|
-
capability: Capability.Base,
|
|
275
|
-
protected: { style: [
|
|
276
|
-
"position",
|
|
277
|
-
"insetInlineEnd",
|
|
278
|
-
"insetBlockEnd"
|
|
279
|
-
] }
|
|
280
|
-
}),
|
|
281
|
-
/** Below the rows: a failure that left them on screen, and the More button. */
|
|
282
|
-
footer: Slot.make({ capability: Capability.Container }),
|
|
283
|
-
retry: Slot.make({
|
|
284
|
-
capability: Capability.Interactive,
|
|
285
|
-
events: [Event.Click]
|
|
286
|
-
}),
|
|
287
|
-
more: Slot.make({
|
|
288
|
-
capability: Capability.Interactive,
|
|
289
|
-
events: [Event.Click]
|
|
290
|
-
})
|
|
291
|
-
});
|
|
292
|
-
// src/style.ts
|
|
293
|
-
/**
|
|
294
|
-
* The grid's default look, for a page that ships `foldkit-mixins/theme`'s
|
|
295
|
-
* tokens and palette. It only paints: the geometry the virtual window
|
|
296
|
-
* depends on is the view's, and the slots refuse a Style that sets it.
|
|
297
|
-
*/
|
|
298
|
-
/** The tokens the grid's styles draw with; the marks' too (`marks.ts`). */
|
|
299
|
-
const ref = Theme.ref(Theme.standard());
|
|
300
|
-
const line = `${ref.border.thin} solid ${ref.outline.subtle}`;
|
|
301
|
-
const faint = `${ref.border.thin} solid color-mix(in oklch, ${ref.outline.subtle} 55%, transparent)`;
|
|
302
|
-
const pinnedEdge = [Style.nest("&[data-pinned-edge=\"start\"]", { borderInlineEnd: `${ref.border.thick} solid ${ref.outline.default}` }), Style.nest("&[data-pinned-edge=\"end\"]", { borderInlineStart: `${ref.border.thick} solid ${ref.outline.default}` })];
|
|
303
|
-
const field = Style.compose(Style.self({
|
|
304
|
-
background: ref.surface.base,
|
|
305
|
-
border: "none",
|
|
306
|
-
caretColor: ref.outline.focus,
|
|
307
|
-
color: "inherit",
|
|
308
|
-
font: "inherit",
|
|
309
|
-
outline: `${ref.border.thick} solid ${ref.outline.focus}`,
|
|
310
|
-
outlineOffset: `calc(-1 * ${ref.border.thick})`,
|
|
311
|
-
paddingInline: ref.space.sm
|
|
312
|
-
}), Style.media("(pointer: coarse)", { fontSize: "max(1rem, 1em)" }));
|
|
313
|
-
const refused = Style.nest("&[aria-invalid=\"true\"]", {
|
|
314
|
-
outlineColor: ref.error.default,
|
|
315
|
-
caretColor: ref.error.default
|
|
316
|
-
});
|
|
317
|
-
/** Every piece is in the `components` layer, so an application's own layer wins. */
|
|
318
|
-
const GridStyle = Style.forSlots(GridSlots)({
|
|
319
|
-
root: Style.compose(Style.self({
|
|
320
|
-
background: ref.surface.base,
|
|
321
|
-
color: ref.text.default,
|
|
322
|
-
fontVariantNumeric: "tabular-nums"
|
|
323
|
-
}), Style.pseudo(":focus-visible", { outline: `${ref.border.thick} solid ${ref.outline.focus}` })),
|
|
324
|
-
header: Style.self({
|
|
325
|
-
zIndex: "2",
|
|
326
|
-
background: ref.surface.muted
|
|
327
|
-
}),
|
|
328
|
-
headerCell: Style.compose(Style.self({
|
|
329
|
-
alignItems: "center",
|
|
330
|
-
background: ref.surface.muted,
|
|
331
|
-
borderBlockEnd: line,
|
|
332
|
-
display: "flex",
|
|
333
|
-
fontSize: ref.size.sm,
|
|
334
|
-
fontWeight: ref.weight.semibold,
|
|
335
|
-
paddingInline: ref.space.sm,
|
|
336
|
-
whiteSpace: "nowrap",
|
|
337
|
-
userSelect: "none"
|
|
338
|
-
}), Style.nest("&[data-pinned]", { zIndex: "1" }), ...pinnedEdge, Style.nest("&[aria-readonly=\"true\"]::before", {
|
|
339
|
-
background: ref.text.muted,
|
|
340
|
-
content: "\"\"",
|
|
341
|
-
flex: "none",
|
|
342
|
-
height: "0.75em",
|
|
343
|
-
marginInlineEnd: ref.space["2xs"],
|
|
344
|
-
mask: `${ref.icon.lock} center / contain no-repeat`,
|
|
345
|
-
width: "0.75em"
|
|
346
|
-
}), Style.nest("&[data-dragging]", {
|
|
347
|
-
zIndex: "3",
|
|
348
|
-
background: ref.surface.default,
|
|
349
|
-
opacity: "0.9",
|
|
350
|
-
cursor: "grabbing"
|
|
351
|
-
}), Style.nest("&[data-drop=\"before\"]", { borderInlineStart: `${ref.border.thick} solid ${ref.outline.focus}` }), Style.nest("&[data-drop=\"after\"]", { borderInlineEnd: `${ref.border.thick} solid ${ref.outline.focus}` })),
|
|
352
|
-
cell: Style.compose(Style.self({
|
|
353
|
-
alignItems: "center",
|
|
354
|
-
borderBlockEnd: line,
|
|
355
|
-
borderInlineEnd: faint,
|
|
356
|
-
display: "flex",
|
|
357
|
-
overflow: "hidden",
|
|
358
|
-
paddingInline: ref.space.sm,
|
|
359
|
-
textOverflow: "ellipsis",
|
|
360
|
-
whiteSpace: "nowrap"
|
|
361
|
-
}), Style.nest("&[data-pinned]", {
|
|
362
|
-
background: ref.surface.base,
|
|
363
|
-
zIndex: "1"
|
|
364
|
-
}), ...pinnedEdge, Style.nest("&[data-editable]", { cursor: "text" }), Style.nest("&[aria-readonly=\"true\"]", {
|
|
365
|
-
color: ref.text.muted,
|
|
366
|
-
cursor: "default"
|
|
367
|
-
}), Style.nest("&[data-editing]", {
|
|
368
|
-
boxShadow: ref.shadow.md,
|
|
369
|
-
overflow: "visible",
|
|
370
|
-
paddingInline: "0",
|
|
371
|
-
zIndex: "2"
|
|
372
|
-
}), Style.nest("&[data-focused=\"true\"]", {
|
|
373
|
-
outline: `${ref.border.thick} solid ${ref.outline.focus}`,
|
|
374
|
-
outlineOffset: `calc(-1 * ${ref.border.thick})`
|
|
375
|
-
}), Style.nest("&[data-fill=\"target\"]", {
|
|
376
|
-
outline: `${ref.border.thin} dashed ${ref.outline.focus}`,
|
|
377
|
-
outlineOffset: `calc(-1 * ${ref.border.thin})`
|
|
378
|
-
}), Style.nest("&[data-mark]", { backgroundImage: `radial-gradient(circle at calc(100% - 6px) 6px, ${ref.outline.focus} 3px, transparent 3.5px)` })),
|
|
379
|
-
editor: Style.compose(field, refused),
|
|
380
|
-
choice: Style.compose(field, refused, Style.self({
|
|
381
|
-
display: "flex",
|
|
382
|
-
alignItems: "center",
|
|
383
|
-
cursor: "default"
|
|
384
|
-
})),
|
|
385
|
-
choiceList: Style.compose(Style.self({
|
|
386
|
-
zIndex: "3",
|
|
387
|
-
minWidth: "100%",
|
|
388
|
-
maxHeight: "16rem",
|
|
389
|
-
overflowY: "auto",
|
|
390
|
-
margin: "0",
|
|
391
|
-
padding: ref.space["3xs"],
|
|
392
|
-
listStyle: "none",
|
|
393
|
-
background: ref.surface.base,
|
|
394
|
-
border: `${ref.border.thin} solid ${ref.outline.default}`,
|
|
395
|
-
borderRadius: ref.radius.md,
|
|
396
|
-
boxShadow: ref.shadow.lg
|
|
397
|
-
})),
|
|
398
|
-
choiceOption: Style.compose(Style.self({
|
|
399
|
-
padding: `${ref.space["2xs"]} ${ref.space.sm}`,
|
|
400
|
-
borderRadius: ref.radius.sm,
|
|
401
|
-
cursor: "pointer",
|
|
402
|
-
whiteSpace: "nowrap"
|
|
403
|
-
}), Style.nest("&:hover", { background: ref.surface.subtle }), Style.nest("&[aria-selected=\"true\"]", {
|
|
404
|
-
background: ref.accent.subtle,
|
|
405
|
-
color: ref.accent.ink
|
|
406
|
-
}), Style.media("(pointer: coarse)", {
|
|
407
|
-
minHeight: "44px",
|
|
408
|
-
display: "flex",
|
|
409
|
-
alignItems: "center"
|
|
410
|
-
})),
|
|
411
|
-
editorError: Style.self({
|
|
412
|
-
background: ref.error.default,
|
|
413
|
-
borderRadius: ref.radius.md,
|
|
414
|
-
boxShadow: ref.shadow.lg,
|
|
415
|
-
color: ref.error["on-fill"],
|
|
416
|
-
fontSize: ref.size.xs,
|
|
417
|
-
fontWeight: ref.weight.medium,
|
|
418
|
-
insetInlineStart: "0",
|
|
419
|
-
paddingBlock: ref.space["3xs"],
|
|
420
|
-
paddingInline: ref.space.xs,
|
|
421
|
-
pointerEvents: "none",
|
|
422
|
-
position: "absolute",
|
|
423
|
-
top: `calc(100% + ${ref.space["3xs"]})`,
|
|
424
|
-
whiteSpace: "nowrap",
|
|
425
|
-
zIndex: "5"
|
|
426
|
-
}),
|
|
427
|
-
resizeHandle: Style.compose(Style.self({
|
|
428
|
-
background: `linear-gradient(to right, transparent calc(100% - 1px), color-mix(in oklch, ${ref.outline.subtle} 70%, transparent) calc(100% - 1px))`,
|
|
429
|
-
WebkitTouchCallout: "none"
|
|
430
|
-
}), Style.pseudo(":hover", { background: `linear-gradient(to right, transparent calc(100% - 2px), ${ref.outline.focus} calc(100% - 2px))` }), Style.at("@media (pointer: coarse)", Style.nest("&::before", {
|
|
431
|
-
content: "\"\"",
|
|
432
|
-
position: "absolute",
|
|
433
|
-
insetBlock: "0",
|
|
434
|
-
insetInlineStart: "-12px",
|
|
435
|
-
insetInlineEnd: "-4px"
|
|
436
|
-
}))),
|
|
437
|
-
fillHandle: Style.compose(Style.self({
|
|
438
|
-
width: "7px",
|
|
439
|
-
height: "7px",
|
|
440
|
-
background: ref.outline.focus,
|
|
441
|
-
border: `1px solid ${ref.surface.base}`,
|
|
442
|
-
cursor: "crosshair",
|
|
443
|
-
zIndex: "3",
|
|
444
|
-
touchAction: "none",
|
|
445
|
-
WebkitTouchCallout: "none"
|
|
446
|
-
}), Style.at("@media (pointer: coarse)", Style.nest("&::before", {
|
|
447
|
-
content: "\"\"",
|
|
448
|
-
position: "absolute",
|
|
449
|
-
inset: "-18px"
|
|
450
|
-
}))),
|
|
451
|
-
menuButton: Style.compose(Style.self({
|
|
452
|
-
background: "none",
|
|
453
|
-
border: "none",
|
|
454
|
-
color: ref.text.muted,
|
|
455
|
-
cursor: "pointer",
|
|
456
|
-
marginInlineStart: "auto",
|
|
457
|
-
paddingInline: ref.space.xs
|
|
458
|
-
}), Style.nest("&[aria-expanded=\"true\"]", { color: ref.text.default })),
|
|
459
|
-
menu: Style.compose(Style.self({
|
|
460
|
-
background: ref.surface.base,
|
|
461
|
-
border: line,
|
|
462
|
-
borderRadius: ref.radius.md,
|
|
463
|
-
fontWeight: ref.weight.normal,
|
|
464
|
-
minWidth: "10rem",
|
|
465
|
-
paddingBlock: ref.space.xs,
|
|
466
|
-
zIndex: "4"
|
|
467
|
-
}), Style.pseudo(":focus-visible", { outline: `${ref.border.thick} solid ${ref.outline.focus}` })),
|
|
468
|
-
menuItem: Style.compose(Style.self({
|
|
469
|
-
cursor: "pointer",
|
|
470
|
-
paddingBlock: ref.space.xs,
|
|
471
|
-
paddingInline: ref.space.sm
|
|
472
|
-
}), Style.nest("&[data-active=\"true\"]", { background: ref.surface.muted })),
|
|
473
|
-
sort: Style.compose(Style.self({
|
|
474
|
-
alignItems: "center",
|
|
475
|
-
background: "none",
|
|
476
|
-
border: "none",
|
|
477
|
-
color: "inherit",
|
|
478
|
-
cursor: "pointer",
|
|
479
|
-
display: "inline-flex",
|
|
480
|
-
font: "inherit",
|
|
481
|
-
gap: ref.space.xs,
|
|
482
|
-
minWidth: "0",
|
|
483
|
-
overflow: "hidden",
|
|
484
|
-
padding: "0",
|
|
485
|
-
textOverflow: "ellipsis",
|
|
486
|
-
whiteSpace: "nowrap"
|
|
487
|
-
}), Style.nest("&::after", {
|
|
488
|
-
borderBlockEnd: `1.5px solid currentColor`,
|
|
489
|
-
borderInlineEnd: `1.5px solid currentColor`,
|
|
490
|
-
content: "\"\"",
|
|
491
|
-
flex: "none",
|
|
492
|
-
height: "0.4em",
|
|
493
|
-
opacity: "0",
|
|
494
|
-
transform: "translateY(-0.15em) rotate(45deg)",
|
|
495
|
-
transition: `opacity ${ref.motion.fast} ${ref.motion.ease}, transform calc(${ref.motion.fast} * ${ref.knob.travel}) ${ref.motion.ease}`,
|
|
496
|
-
width: "0.4em"
|
|
497
|
-
}), Style.nest("&:hover::after", { opacity: "0.35" }), Style.nest("&[data-sort=\"desc\"]::after", { opacity: "1" }), Style.nest("&[data-sort=\"asc\"]::after", {
|
|
498
|
-
opacity: "1",
|
|
499
|
-
transform: "translateY(0.1em) rotate(-135deg)"
|
|
500
|
-
})),
|
|
501
|
-
placeholder: Style.self({ background: ref.surface.subtle }),
|
|
502
|
-
status: Style.self({
|
|
503
|
-
color: ref.text.muted,
|
|
504
|
-
padding: ref.space.md
|
|
505
|
-
})
|
|
506
|
-
}, { layer: Layers.standard.layer("components") });
|
|
507
|
-
// src/nearEnd.ts
|
|
508
|
-
const NearEnd = Schema.TaggedStruct("NearEnd", {});
|
|
509
|
-
/** How far ahead of the end the next rows are asked for, so they arrive before they are needed. */
|
|
510
|
-
const ahead = "200px";
|
|
511
|
-
/**
|
|
512
|
-
* The element coming within 200px of the grid's visible box: one report each
|
|
513
|
-
* time it comes in, measured against the scroll container it sits in, not
|
|
514
|
-
* the page. Not `watching` (a load in flight), or no observer (SSR, an old
|
|
515
|
-
* browser), it reports nothing.
|
|
516
|
-
*/
|
|
517
|
-
const Nearing = Mount.defineStream("DataGridNearEnd", {
|
|
518
|
-
args: { watching: Schema.Boolean },
|
|
519
|
-
messages: [NearEnd],
|
|
520
|
-
execute: ({ element, watching }) => Stream.callback((queue) => Effect.gen(function* () {
|
|
521
|
-
const Observer = globalThis.IntersectionObserver;
|
|
522
|
-
if (!watching || Observer === void 0) return;
|
|
523
|
-
yield* Effect.acquireRelease(Effect.sync(() => {
|
|
524
|
-
const observer = new Observer((entries) => {
|
|
525
|
-
if (entries.some((entry) => entry.isIntersecting)) Queue.offerUnsafe(queue, NearEnd.make({}));
|
|
526
|
-
}, {
|
|
527
|
-
root: element.closest("[role=\"grid\"]"),
|
|
528
|
-
rootMargin: ahead
|
|
529
|
-
});
|
|
530
|
-
observer.observe(element);
|
|
531
|
-
return observer;
|
|
532
|
-
}), (observer) => Effect.sync(() => observer.disconnect()));
|
|
533
|
-
}))
|
|
534
|
-
});
|
|
535
|
-
// src/fillDrag.ts
|
|
536
|
-
const FillDragStarted = Schema.TaggedStruct("FillDragStarted", {});
|
|
537
|
-
const FillDraggedOver = Schema.TaggedStruct("FillDraggedOver", {
|
|
538
|
-
/** The DOM id of the gridcell under the pointer. */
|
|
539
|
-
cell: Schema.String });
|
|
540
|
-
const FillDragEnded = Schema.TaggedStruct("FillDragEnded", {
|
|
541
|
-
/** `false` when the pointer was cancelled or capture was lost. */
|
|
542
|
-
completed: Schema.Boolean });
|
|
543
|
-
/** The attribute that marks the fill handle, which the body's listener looks for. */
|
|
544
|
-
const fillHandleAttribute = "data-fill-handle";
|
|
545
|
-
/** How far a press moves before it is a fill, so a stray click fills nothing. */
|
|
546
|
-
const threshold$1 = 4;
|
|
547
|
-
/** How near the grid's edge the pointer brings a scroll, and the fastest step a frame. */
|
|
548
|
-
const edge$1 = 24;
|
|
549
|
-
const fastest = 20;
|
|
550
|
-
/**
|
|
551
|
-
* The fill handle dragged with the pointer, from the element that holds the
|
|
552
|
-
* grid's cells, so the drag outlives the handle's own cell scrolling out of
|
|
553
|
-
* the window. A press on the handle is kept from the cells (a press there
|
|
554
|
-
* would select one instead). Once it has moved 4px the pointer is captured,
|
|
555
|
-
* so the cell under it is found by where it is (`elementFromPoint`), not by
|
|
556
|
-
* the event's target, which capture makes this element. While the pointer is
|
|
557
|
-
* within 24px of the grid's edge, or past it, the grid scrolls toward it. The
|
|
558
|
-
* listeners that follow the pointer, and the scroll, live for one press.
|
|
559
|
-
*
|
|
560
|
-
* Returns the cleanup, and whether a click now is the one a drag ended with.
|
|
561
|
-
*/
|
|
562
|
-
const fillDragOn = (element, offer) => {
|
|
563
|
-
const page = element.ownerDocument;
|
|
564
|
-
const view = page.defaultView;
|
|
565
|
-
let press = Option.none();
|
|
566
|
-
let dragEnded = false;
|
|
567
|
-
const pointerOf = (event) => event.pointerId ?? 0;
|
|
568
|
-
const grid = () => Option.fromNullishOr(element.closest("[role=\"grid\"]"));
|
|
569
|
-
const probe = ({ x, y }) => Option.match(grid(), {
|
|
570
|
-
onNone: () => ({
|
|
571
|
-
x,
|
|
572
|
-
y,
|
|
573
|
-
toward: {
|
|
574
|
-
x,
|
|
575
|
-
y
|
|
576
|
-
}
|
|
577
|
-
}),
|
|
578
|
-
onSome: (box) => {
|
|
579
|
-
const rect = box.getBoundingClientRect();
|
|
580
|
-
const header = box.querySelector("[role=\"row\"][aria-rowindex=\"1\"]");
|
|
581
|
-
const top = header === null ? rect.top : header.getBoundingClientRect().bottom;
|
|
582
|
-
const clamp = (at, from, to) => Math.max(from, Math.min(to, at));
|
|
583
|
-
const right = rect.left + box.clientWidth - 2;
|
|
584
|
-
const bottom = rect.top + box.clientHeight - 2;
|
|
585
|
-
return {
|
|
586
|
-
x: clamp(x, rect.left + 1, right),
|
|
587
|
-
y: clamp(y, top + 1, bottom),
|
|
588
|
-
toward: {
|
|
589
|
-
x: (rect.left + right) / 2,
|
|
590
|
-
y: (top + bottom) / 2
|
|
591
|
-
}
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
});
|
|
595
|
-
const cellAt = ({ x, y, toward }) => {
|
|
596
|
-
const steps = 48;
|
|
597
|
-
for (let step = 0; step <= steps; step++) {
|
|
598
|
-
const found = page.elementFromPoint(x + (toward.x - x) * step / steps, y + (toward.y - y) * step / steps)?.closest("[role=\"gridcell\"]");
|
|
599
|
-
if (found !== null && found !== void 0 && found.id !== "") return Option.some(found.id);
|
|
600
|
-
}
|
|
601
|
-
return Option.none();
|
|
602
|
-
};
|
|
603
|
-
const report = (current) => {
|
|
604
|
-
const found = cellAt(probe(current.at));
|
|
605
|
-
if (Option.isNone(found) || found.value === current.over) return;
|
|
606
|
-
current.over = found.value;
|
|
607
|
-
offer(FillDraggedOver.make({ cell: found.value }));
|
|
608
|
-
};
|
|
609
|
-
const stepOf = (at, start, end) => {
|
|
610
|
-
if (at < start + edge$1) return Math.max(-20, at - (start + edge$1));
|
|
611
|
-
if (at > end - edge$1) return Math.min(fastest, at - (end - edge$1));
|
|
612
|
-
return 0;
|
|
613
|
-
};
|
|
614
|
-
const scroll = (current) => {
|
|
615
|
-
current.frame = Option.none();
|
|
616
|
-
Option.match(grid(), {
|
|
617
|
-
onNone: () => {},
|
|
618
|
-
onSome: (box) => {
|
|
619
|
-
const rect = box.getBoundingClientRect();
|
|
620
|
-
const down = stepOf(current.at.y, rect.top, rect.bottom);
|
|
621
|
-
const across = stepOf(current.at.x, rect.left, rect.right);
|
|
622
|
-
if (down === 0 && across === 0) return;
|
|
623
|
-
report(current);
|
|
624
|
-
box.scrollBy(across, down);
|
|
625
|
-
if (view !== null) current.frame = Option.some(view.requestAnimationFrame(() => scroll(current)));
|
|
626
|
-
}
|
|
627
|
-
});
|
|
628
|
-
};
|
|
629
|
-
const release = (current) => {
|
|
630
|
-
page.removeEventListener("pointermove", onMove);
|
|
631
|
-
page.removeEventListener("pointerup", onUp);
|
|
632
|
-
page.removeEventListener("pointercancel", onCancel);
|
|
633
|
-
element.removeEventListener("lostpointercapture", onCancel);
|
|
634
|
-
if (Option.isSome(current.frame)) view?.cancelAnimationFrame(current.frame.value);
|
|
635
|
-
current.frame = Option.none();
|
|
636
|
-
if (!current.dragging) return;
|
|
637
|
-
try {
|
|
638
|
-
element.releasePointerCapture(current.pointerId);
|
|
639
|
-
} catch {}
|
|
640
|
-
};
|
|
641
|
-
const finish = (event, completed) => {
|
|
642
|
-
const found = Option.filter(press, (current) => current.pointerId === pointerOf(event));
|
|
643
|
-
if (Option.isNone(found)) return;
|
|
644
|
-
press = Option.none();
|
|
645
|
-
release(found.value);
|
|
646
|
-
if (!found.value.dragging) return;
|
|
647
|
-
dragEnded = true;
|
|
648
|
-
view?.setTimeout(() => dragEnded = false, 0);
|
|
649
|
-
offer(FillDragEnded.make({ completed }));
|
|
650
|
-
};
|
|
651
|
-
const onUp = (event) => finish(event, true);
|
|
652
|
-
const onCancel = (event) => finish(event, false);
|
|
653
|
-
const onMove = (event) => {
|
|
654
|
-
const found = Option.filter(press, (current) => current.pointerId === pointerOf(event));
|
|
655
|
-
if (Option.isNone(found)) return;
|
|
656
|
-
const current = found.value;
|
|
657
|
-
const pointer = event;
|
|
658
|
-
current.at = {
|
|
659
|
-
x: pointer.clientX ?? 0,
|
|
660
|
-
y: pointer.clientY ?? 0
|
|
661
|
-
};
|
|
662
|
-
if (!current.dragging) {
|
|
663
|
-
if (Math.max(Math.abs(current.at.x - current.x), Math.abs(current.at.y - current.y)) < threshold$1) return;
|
|
664
|
-
current.dragging = true;
|
|
665
|
-
try {
|
|
666
|
-
element.setPointerCapture(current.pointerId);
|
|
667
|
-
} catch {}
|
|
668
|
-
offer(FillDragStarted.make({}));
|
|
669
|
-
}
|
|
670
|
-
report(current);
|
|
671
|
-
if (Option.isNone(current.frame)) scroll(current);
|
|
672
|
-
};
|
|
673
|
-
const onDown = (event) => {
|
|
674
|
-
const pointer = event;
|
|
675
|
-
const from = event.target;
|
|
676
|
-
if (!(from instanceof Element) || from.closest(`[data-fill-handle]`) === null) return;
|
|
677
|
-
event.stopPropagation();
|
|
678
|
-
if ((pointer.button ?? 0) !== 0 || Option.isSome(press)) return;
|
|
679
|
-
event.preventDefault();
|
|
680
|
-
const x = pointer.clientX ?? 0;
|
|
681
|
-
const y = pointer.clientY ?? 0;
|
|
682
|
-
press = Option.some({
|
|
683
|
-
pointerId: pointerOf(event),
|
|
684
|
-
x,
|
|
685
|
-
y,
|
|
686
|
-
at: {
|
|
687
|
-
x,
|
|
688
|
-
y
|
|
689
|
-
},
|
|
690
|
-
dragging: false,
|
|
691
|
-
over: "",
|
|
692
|
-
frame: Option.none()
|
|
693
|
-
});
|
|
694
|
-
page.addEventListener("pointermove", onMove);
|
|
695
|
-
page.addEventListener("pointerup", onUp);
|
|
696
|
-
page.addEventListener("pointercancel", onCancel);
|
|
697
|
-
element.addEventListener("lostpointercapture", onCancel);
|
|
698
|
-
};
|
|
699
|
-
element.addEventListener("pointerdown", onDown);
|
|
700
|
-
return {
|
|
701
|
-
dispose: () => {
|
|
702
|
-
element.removeEventListener("pointerdown", onDown);
|
|
703
|
-
if (Option.isSome(press)) release(press.value);
|
|
704
|
-
press = Option.none();
|
|
705
|
-
},
|
|
706
|
-
owns: (event) => dragEnded || event.target instanceof Element && event.target.closest(`[data-fill-handle]`) !== null
|
|
707
|
-
};
|
|
708
|
-
};
|
|
709
|
-
// src/holdFocus.ts
|
|
710
|
-
/**
|
|
711
|
-
* Focuses what the grid draws over itself while it is open (a cell's editor,
|
|
712
|
-
* with the caret after its text, or a column's menu) and hands focus back to
|
|
713
|
-
* the grid when it goes, so closing it leaves the keyboard on the grid rather
|
|
714
|
-
* than on the page's body. Focus the user moved elsewhere stays there. A
|
|
715
|
-
* choice's `select` also opens its list, so a double-click on the cell shows
|
|
716
|
-
* the choices at once rather than asking for another click.
|
|
717
|
-
*/
|
|
718
|
-
const HoldFocus = Mount.defineStream("DataGridHoldFocus", {
|
|
719
|
-
messages: [Schema.Never],
|
|
720
|
-
execute: ({ element }) => Stream.callback(() => Effect.acquireRelease(Effect.sync(() => {
|
|
721
|
-
const editor = element;
|
|
722
|
-
const grid = element.closest("[role=\"grid\"]");
|
|
723
|
-
editor.focus?.();
|
|
724
|
-
const end = editor.value?.length ?? 0;
|
|
725
|
-
editor.setSelectionRange?.(end, end);
|
|
726
|
-
if (element.tagName === "SELECT") try {
|
|
727
|
-
editor.showPicker?.();
|
|
728
|
-
} catch {}
|
|
729
|
-
return grid;
|
|
730
|
-
}), (grid) => Effect.sync(() => {
|
|
731
|
-
const active = typeof document === "undefined" ? null : document.activeElement;
|
|
732
|
-
if (active === null || active === element || active === document.body) grid?.focus?.();
|
|
733
|
-
})))
|
|
734
|
-
});
|
|
735
|
-
// src/keepFocus.ts
|
|
736
|
-
/**
|
|
737
|
-
* Keeps focus where it is when a press lands on the element: a choice's list
|
|
738
|
-
* sits beside its combobox, so a press on an option would otherwise blur the
|
|
739
|
-
* combobox, which commits the old draft before the option's click arrives.
|
|
740
|
-
* Attached only while the list is open.
|
|
741
|
-
*/
|
|
742
|
-
const KeepFocus = Mount.defineStream("DataGridKeepFocus", {
|
|
743
|
-
messages: [Schema.Never],
|
|
744
|
-
execute: ({ element }) => Stream.callback(() => Effect.acquireRelease(Effect.sync(() => {
|
|
745
|
-
const keep = (event) => event.preventDefault();
|
|
746
|
-
element.addEventListener("mousedown", keep);
|
|
747
|
-
return keep;
|
|
748
|
-
}), (keep) => Effect.sync(() => element.removeEventListener("mousedown", keep))))
|
|
749
|
-
});
|
|
750
|
-
// src/headerDrag.ts
|
|
751
|
-
const HeaderDragStarted = Schema.TaggedStruct("HeaderDragStarted", {
|
|
752
|
-
/** The dragged columnheader's DOM id. */
|
|
753
|
-
header: Schema.String });
|
|
754
|
-
const HeaderDragged = Schema.TaggedStruct("HeaderDragged", {
|
|
755
|
-
/** From where the pointer went down, in CSS pixels. */
|
|
756
|
-
deltaX: Schema.Number });
|
|
757
|
-
const HeaderDragEnded = Schema.TaggedStruct("HeaderDragEnded", {
|
|
758
|
-
/** `false` when the pointer was cancelled or capture was lost. */
|
|
759
|
-
completed: Schema.Boolean });
|
|
760
|
-
/** How far a press moves before it is a drag, so a click on a header stays a click. */
|
|
761
|
-
const threshold = 4;
|
|
762
|
-
/**
|
|
763
|
-
* Column headers dragged with the pointer, from one listener on the row that
|
|
764
|
-
* holds them. A press on a header becomes a drag once it has moved 4px, and
|
|
765
|
-
* only then is the pointer captured, so a click (on a sort button, say) stays
|
|
766
|
-
* a click. A press on a resize handle, or in a column's open menu, is not a
|
|
767
|
-
* drag. The listeners that
|
|
768
|
-
* follow the pointer are attached for the length of a press. Where a drag
|
|
769
|
-
* lands is the grid's update.
|
|
770
|
-
*/
|
|
771
|
-
const HeaderDrag = Mount.defineStream("DataGridHeaderDrag", {
|
|
772
|
-
messages: [
|
|
773
|
-
HeaderDragStarted,
|
|
774
|
-
HeaderDragged,
|
|
775
|
-
HeaderDragEnded
|
|
776
|
-
],
|
|
777
|
-
execute: ({ element }) => Stream.callback((queue) => Effect.acquireRelease(Effect.sync(() => {
|
|
778
|
-
const page = element.ownerDocument;
|
|
779
|
-
let press = Option.none();
|
|
780
|
-
const offer = (fact) => Queue.offerUnsafe(queue, fact);
|
|
781
|
-
const swallow = (event) => {
|
|
782
|
-
event.stopPropagation();
|
|
783
|
-
event.preventDefault();
|
|
784
|
-
};
|
|
785
|
-
const release = (current) => {
|
|
786
|
-
page.removeEventListener("pointermove", onMove);
|
|
787
|
-
page.removeEventListener("pointerup", onUp);
|
|
788
|
-
page.removeEventListener("pointercancel", onCancel);
|
|
789
|
-
element.removeEventListener("lostpointercapture", onCancel);
|
|
790
|
-
if (!current.dragging) return;
|
|
791
|
-
try {
|
|
792
|
-
element.releasePointerCapture(current.pointerId);
|
|
793
|
-
} catch {}
|
|
794
|
-
};
|
|
795
|
-
const pressOf = (event) => Option.filter(press, (current) => current.pointerId === (event.pointerId ?? 0));
|
|
796
|
-
const finish = (event, completed) => {
|
|
797
|
-
const found = pressOf(event);
|
|
798
|
-
if (Option.isNone(found)) return;
|
|
799
|
-
const current = found.value;
|
|
800
|
-
press = Option.none();
|
|
801
|
-
release(current);
|
|
802
|
-
if (!current.dragging) return;
|
|
803
|
-
offer(HeaderDragEnded.make({ completed }));
|
|
804
|
-
element.addEventListener("click", swallow, {
|
|
805
|
-
capture: true,
|
|
806
|
-
once: true
|
|
807
|
-
});
|
|
808
|
-
setTimeout(() => element.removeEventListener("click", swallow, true), 0);
|
|
809
|
-
};
|
|
810
|
-
const onUp = (event) => finish(event, true);
|
|
811
|
-
const onCancel = (event) => finish(event, false);
|
|
812
|
-
const onMove = (event) => {
|
|
813
|
-
const found = pressOf(event);
|
|
814
|
-
if (Option.isNone(found)) return;
|
|
815
|
-
const current = found.value;
|
|
816
|
-
const deltaX = (event.clientX ?? 0) - current.x;
|
|
817
|
-
if (!current.dragging) {
|
|
818
|
-
if (Math.abs(deltaX) < threshold) return;
|
|
819
|
-
current.dragging = true;
|
|
820
|
-
try {
|
|
821
|
-
element.setPointerCapture(current.pointerId);
|
|
822
|
-
} catch {}
|
|
823
|
-
offer(HeaderDragStarted.make({ header: current.header }));
|
|
824
|
-
}
|
|
825
|
-
offer(HeaderDragged.make({ deltaX }));
|
|
826
|
-
};
|
|
827
|
-
const onDown = (event) => {
|
|
828
|
-
const pointer = event;
|
|
829
|
-
const from = event.target;
|
|
830
|
-
if ((pointer.button ?? 0) !== 0 || Option.isSome(press)) return;
|
|
831
|
-
if (!(from instanceof Element) || from.closest("[role=\"separator\"], [role=\"menu\"]")) return;
|
|
832
|
-
const header = from.closest("[role=\"columnheader\"]");
|
|
833
|
-
if (header === null || !element.contains(header) || header.id === "") return;
|
|
834
|
-
press = Option.some({
|
|
835
|
-
pointerId: pointer.pointerId ?? 0,
|
|
836
|
-
x: pointer.clientX ?? 0,
|
|
837
|
-
header: header.id,
|
|
838
|
-
dragging: false
|
|
839
|
-
});
|
|
840
|
-
page.addEventListener("pointermove", onMove);
|
|
841
|
-
page.addEventListener("pointerup", onUp);
|
|
842
|
-
page.addEventListener("pointercancel", onCancel);
|
|
843
|
-
element.addEventListener("lostpointercapture", onCancel);
|
|
844
|
-
};
|
|
845
|
-
element.addEventListener("pointerdown", onDown);
|
|
846
|
-
return {
|
|
847
|
-
onDown,
|
|
848
|
-
current: () => press,
|
|
849
|
-
release
|
|
850
|
-
};
|
|
851
|
-
}), ({ onDown, current, release }) => Effect.sync(() => {
|
|
852
|
-
element.removeEventListener("pointerdown", onDown);
|
|
853
|
-
const left = current();
|
|
854
|
-
if (Option.isSome(left)) release(left.value);
|
|
855
|
-
})))
|
|
856
|
-
});
|
|
857
|
-
// src/press.ts
|
|
858
|
-
const CellClicked = Schema.TaggedStruct("CellClicked", {
|
|
859
|
-
/** The clicked gridcell's DOM id. */
|
|
860
|
-
cell: Schema.String,
|
|
861
|
-
shiftKey: Schema.Boolean,
|
|
862
|
-
/** Ctrl, or Meta on a Mac: a toggle. */
|
|
863
|
-
toggleKey: Schema.Boolean
|
|
864
|
-
});
|
|
865
|
-
/**
|
|
866
|
-
* Clicks on a grid's cells, with their modifier keys, and the fill handle's
|
|
867
|
-
* drag (`fillDragOn`), from listeners on the element that holds the cells
|
|
868
|
-
* rather than one per cell: one Mount, since an element holds one. A click
|
|
869
|
-
* outside a gridcell, on the fill handle, or ending a fill reports nothing;
|
|
870
|
-
* what a click means is the grid's update.
|
|
871
|
-
*/
|
|
872
|
-
const CellPress = Mount.defineStream("DataGridCellPress", {
|
|
873
|
-
messages: [
|
|
874
|
-
CellClicked,
|
|
875
|
-
FillDragStarted,
|
|
876
|
-
FillDraggedOver,
|
|
877
|
-
FillDragEnded
|
|
878
|
-
],
|
|
879
|
-
execute: ({ element }) => Stream.callback((queue) => Effect.acquireRelease(Effect.sync(() => {
|
|
880
|
-
const fill = fillDragOn(element, (fact) => Queue.offerUnsafe(queue, fact));
|
|
881
|
-
const listener = (event) => {
|
|
882
|
-
if (fill.owns(event)) return;
|
|
883
|
-
const from = event.target;
|
|
884
|
-
if (!(from instanceof Element)) return;
|
|
885
|
-
const cell = from.closest("[role=\"gridcell\"]");
|
|
886
|
-
if (cell === null || !element.contains(cell) || cell.id === "") return;
|
|
887
|
-
const modified = event;
|
|
888
|
-
Queue.offerUnsafe(queue, CellClicked.make({
|
|
889
|
-
cell: cell.id,
|
|
890
|
-
shiftKey: modified.shiftKey === true,
|
|
891
|
-
toggleKey: modified.ctrlKey === true || modified.metaKey === true
|
|
892
|
-
}));
|
|
893
|
-
};
|
|
894
|
-
element.addEventListener("click", listener);
|
|
895
|
-
return {
|
|
896
|
-
listener,
|
|
897
|
-
fill
|
|
898
|
-
};
|
|
899
|
-
}), ({ listener, fill }) => Effect.sync(() => {
|
|
900
|
-
element.removeEventListener("click", listener);
|
|
901
|
-
fill.dispose();
|
|
902
|
-
})))
|
|
903
|
-
});
|
|
8
|
+
import { HoldFocus } from "./holdFocus.js";
|
|
9
|
+
import { KeepFocus } from "./keepFocus.js";
|
|
10
|
+
import { HeaderDrag } from "./headerDrag.js";
|
|
11
|
+
import { CellPress } from "./press.js";
|
|
904
12
|
// src/view.ts
|
|
905
13
|
const px = (value) => `${value}px`;
|
|
906
14
|
/** Whether the rows' source is reading: the grid says so, and asks for nothing more. */
|
|
@@ -1748,99 +856,6 @@ define: (grid) => {
|
|
|
1748
856
|
}, { name: grid.id });
|
|
1749
857
|
} });
|
|
1750
858
|
const DataGridView = view;
|
|
1751
|
-
|
|
1752
|
-
/**
|
|
1753
|
-
* Sends the grid's `onMore` as its More button comes within 200px of the
|
|
1754
|
-
* grid's visible box, and again after each load while the button stays
|
|
1755
|
-
* there; the button stays for the keyboard. Nothing is asked while the rows'
|
|
1756
|
-
* source is busy. Attach it to a grid's view:
|
|
1757
|
-
* `DataGridView<Message>().define(Grid).pipe(MoreOnScroll)`.
|
|
1758
|
-
*/
|
|
1759
|
-
const MoreOnScroll = (view) => Behavior.attach(Behavior.forSlots(GridSlots)({ more: Behavior.slot({ mount: (input) => Mount.mapMessage(Nearing({ watching: !isBusy(input.status) }), () => input.onMore) }) }, { name: "MoreOnScroll" }))(view);
|
|
1760
|
-
// src/marks.ts
|
|
1761
|
-
/**
|
|
1762
|
-
* The marks an editable grid over a server shows on a cell, by name, styled
|
|
1763
|
-
* on the theme's tokens, with a legend drawn from the same rules: an edit not
|
|
1764
|
-
* sent, one saved that the read rows do not show yet, one the server refused,
|
|
1765
|
-
* one another device's later edit replaced, and another device's focus.
|
|
1766
|
-
* `CellMark.name` stays any string; these are the shared ones.
|
|
1767
|
-
*/
|
|
1768
|
-
const GridMarks = [
|
|
1769
|
-
"pending",
|
|
1770
|
-
"saved",
|
|
1771
|
-
"refused",
|
|
1772
|
-
"replaced",
|
|
1773
|
-
"peer"
|
|
1774
|
-
];
|
|
1775
|
-
/** A dot in the top end corner, filled or hollow: an edit on its way. */
|
|
1776
|
-
const dot = (color, hollow) => ({ backgroundImage: hollow ? `radial-gradient(circle at calc(100% - 6px) 6px, transparent 2px, ${color} 2.5px, ${color} 3.5px, transparent 4px)` : `radial-gradient(circle at calc(100% - 6px) 6px, ${color} 3px, transparent 3.5px)` });
|
|
1777
|
-
/** A 2px inset edge, and a ground: something that needs reading. */
|
|
1778
|
-
const edge = (color, ground) => ({
|
|
1779
|
-
backgroundImage: "none",
|
|
1780
|
-
boxShadow: `inset 0 0 0 ${ref.border.thick} ${color}`,
|
|
1781
|
-
...ground === void 0 ? {} : { background: ground }
|
|
1782
|
-
});
|
|
1783
|
-
const looks = {
|
|
1784
|
-
pending: dot(ref.info.default, false),
|
|
1785
|
-
saved: dot(ref.success.ink, true),
|
|
1786
|
-
refused: edge(ref.error.ink, ref.error.subtle),
|
|
1787
|
-
replaced: edge(ref.warning.ink),
|
|
1788
|
-
peer: edge(`var(--fk-grid-peer, ${ref.secondary.default})`, ref.secondary.subtle)
|
|
1789
|
-
};
|
|
1790
|
-
/** Each mark's rule on an element carrying `data-mark`, and an outline where colour is forced. */
|
|
1791
|
-
const marked = Style.compose(...GridMarks.map((mark) => Style.nest(`&[data-mark="${mark}"]`, looks[mark])), Style.at("@media (forced-colors: active)", Style.nest("&[data-mark]", { outline: `${ref.border.thick} solid CanvasText` })));
|
|
1792
|
-
const GridLegendSlots = Slots.define({
|
|
1793
|
-
/** The list: labelled, so a screen reader names what the marks are. */
|
|
1794
|
-
legend: Slot.make({ attributes: [Attr.AriaLabel] }),
|
|
1795
|
-
/** One mark and its words. */
|
|
1796
|
-
legendItem: Slot.make({}),
|
|
1797
|
-
/** The mark drawn small, as the cells draw it (`data-mark`), hidden from screen readers. */
|
|
1798
|
-
legendSwatch: Slot.make({})
|
|
1799
|
-
});
|
|
1800
|
-
const words = {
|
|
1801
|
-
label: "What the marks on a cell mean",
|
|
1802
|
-
marks: {
|
|
1803
|
-
pending: "Not sent yet",
|
|
1804
|
-
saved: "Saved, not yet in the table",
|
|
1805
|
-
refused: "Not saved",
|
|
1806
|
-
replaced: "Replaced by another device",
|
|
1807
|
-
peer: "Another device is here"
|
|
1808
|
-
}
|
|
1809
|
-
};
|
|
1810
|
-
/**
|
|
1811
|
-
* A legend of the marks, each beside a swatch drawn by the same rules as the
|
|
1812
|
-
* cells (`GridMarkStyle` styles both), so the two cannot drift apart.
|
|
1813
|
-
*/
|
|
1814
|
-
const GridLegend = () => SlotView.forMessages().define(GridLegendSlots, (input, slots, h) => {
|
|
1815
|
-
const said = input.words ?? words;
|
|
1816
|
-
return h.ul(slots.legend.attrs([h.AriaLabel(said.label)]), (input.marks ?? GridMarks).map((mark) => h.li(slots.legendItem.attrs([]), [h.span(slots.legendSwatch.attrs([h.DataAttribute("mark", mark), h.AriaHidden(true)]), []), said.marks[mark]])));
|
|
1817
|
-
});
|
|
1818
|
-
/** The marks on the grid's cells. Attach after `GridStyle`, whose plain dot it replaces by name. */
|
|
1819
|
-
const GridMarkStyle = Style.forSlots(GridSlots)({ cell: marked }, { layer: Layers.standard.layer("components") });
|
|
1820
|
-
/** The legend's look, its swatches drawn by the cells' own rules. */
|
|
1821
|
-
const GridLegendStyle = Style.forSlots(GridLegendSlots)({
|
|
1822
|
-
legend: {
|
|
1823
|
-
display: "flex",
|
|
1824
|
-
flexWrap: "wrap",
|
|
1825
|
-
gap: `${ref.space["2xs"]} ${ref.space.md}`,
|
|
1826
|
-
listStyle: "none",
|
|
1827
|
-
margin: "0",
|
|
1828
|
-
padding: "0",
|
|
1829
|
-
fontSize: ref.size.xs,
|
|
1830
|
-
color: ref.text.muted
|
|
1831
|
-
},
|
|
1832
|
-
legendItem: {
|
|
1833
|
-
display: "inline-flex",
|
|
1834
|
-
alignItems: "center",
|
|
1835
|
-
gap: ref.space.xs
|
|
1836
|
-
},
|
|
1837
|
-
legendSwatch: Style.compose(Style.inline({
|
|
1838
|
-
display: "inline-block",
|
|
1839
|
-
width: "12px",
|
|
1840
|
-
height: "12px",
|
|
1841
|
-
borderRadius: ref.radius.xs
|
|
1842
|
-
}), marked)
|
|
1843
|
-
}, { layer: Layers.standard.layer("components") });
|
|
1844
|
-
export { DataGridView, GridLegend, GridLegendSlots, GridLegendStyle, GridMarkStyle, GridMarks, GridSlots, GridStyle, MoreOnScroll };
|
|
859
|
+
export { DataGridView, isBusy };
|
|
1845
860
|
|
|
1846
|
-
//# sourceMappingURL=
|
|
861
|
+
//# sourceMappingURL=view.js.map
|