foldkit-mixins-data-grid 0.2.15 → 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.
Files changed (51) hide show
  1. package/dist/fillDrag.d.ts +30 -0
  2. package/dist/fillDrag.d.ts.map +1 -0
  3. package/dist/fillDrag.js +178 -0
  4. package/dist/fillDrag.js.map +1 -0
  5. package/dist/headerDrag.d.ts +34 -0
  6. package/dist/headerDrag.d.ts.map +1 -0
  7. package/dist/headerDrag.js +112 -0
  8. package/dist/headerDrag.js.map +1 -0
  9. package/dist/holdFocus.d.ts +11 -0
  10. package/dist/holdFocus.d.ts.map +1 -0
  11. package/dist/holdFocus.js +31 -0
  12. package/dist/holdFocus.js.map +1 -0
  13. package/dist/index.d.ts +6 -0
  14. package/dist/index.js +6 -0
  15. package/dist/keepFocus.d.ts +9 -0
  16. package/dist/keepFocus.d.ts.map +1 -0
  17. package/dist/keepFocus.js +20 -0
  18. package/dist/keepFocus.js.map +1 -0
  19. package/dist/marks.d.ts +69 -0
  20. package/dist/marks.d.ts.map +1 -0
  21. package/dist/marks.js +90 -0
  22. package/dist/marks.js.map +1 -0
  23. package/dist/moreOnScroll.d.ts +12 -0
  24. package/dist/moreOnScroll.d.ts.map +1 -0
  25. package/dist/moreOnScroll.js +17 -0
  26. package/dist/moreOnScroll.js.map +1 -0
  27. package/dist/nearEnd.d.ts +15 -0
  28. package/dist/nearEnd.d.ts.map +1 -0
  29. package/dist/nearEnd.js +33 -0
  30. package/dist/nearEnd.js.map +1 -0
  31. package/dist/press.d.ts +31 -0
  32. package/dist/press.d.ts.map +1 -0
  33. package/dist/press.js +53 -0
  34. package/dist/press.js.map +1 -0
  35. package/dist/slots.d.ts +85 -0
  36. package/dist/slots.d.ts.map +1 -0
  37. package/dist/slots.js +289 -0
  38. package/dist/slots.js.map +1 -0
  39. package/dist/style.d.ts +555 -0
  40. package/dist/style.d.ts.map +1 -0
  41. package/dist/style.js +224 -0
  42. package/dist/style.js.map +1 -0
  43. package/dist/view.d.ts +140 -0
  44. package/dist/view.d.ts.map +1 -0
  45. package/dist/view.js +861 -0
  46. package/dist/view.js.map +1 -0
  47. package/package.json +12 -12
  48. package/dist/index.d.mts +0 -321
  49. package/dist/index.d.mts.map +0 -1
  50. package/dist/index.mjs +0 -1849
  51. package/dist/index.mjs.map +0 -1
package/dist/view.js ADDED
@@ -0,0 +1,861 @@
1
+ import { Match, Option } from "effect";
2
+ import * as Mount from "foldkit/mount";
3
+ import { GridSlots } from "./slots.js";
4
+ import { SlotView } from "foldkit-mixins";
5
+ import { fillHandleAttribute } from "./fillDrag.js";
6
+ import { CellEditor, Clipboard, Columns, Fill, GridFocus, GridSelection, GridViewport, RowCount, RowStatus, VirtualGrid, addressableRows } from "foldkit-data-grid";
7
+ import { Move } from "foldkit-primitives/dom";
8
+ import { HoldFocus } from "./holdFocus.js";
9
+ import { KeepFocus } from "./keepFocus.js";
10
+ import { HeaderDrag } from "./headerDrag.js";
11
+ import { CellPress } from "./press.js";
12
+ // src/view.ts
13
+ const px = (value) => `${value}px`;
14
+ /** Whether the rows' source is reading: the grid says so, and asks for nothing more. */
15
+ const isBusy = (status) => RowStatus.match(status ?? RowStatus.Ready(), {
16
+ Ready: () => false,
17
+ Loading: () => true,
18
+ Refreshing: () => true,
19
+ Failed: () => false
20
+ });
21
+ const ariaSort = (direction) => {
22
+ if (direction === "asc") return "ascending";
23
+ if (direction === "desc") return "descending";
24
+ return "none";
25
+ };
26
+ /** How far one press of an arrow key on a resize handle moves the edge, in pixels. */
27
+ const resizeStep = 16;
28
+ const { textOf } = Columns;
29
+ const copies = /* @__PURE__ */ new WeakMap();
30
+ const remembered = (projection, key, make) => {
31
+ const known = copies.get(projection) ?? /* @__PURE__ */ new Map();
32
+ copies.set(projection, known);
33
+ const text = known.get(key);
34
+ if (text !== void 0) return text;
35
+ const made = make();
36
+ known.set(key, made);
37
+ return made;
38
+ };
39
+ /** The offset of each pinned column from its edge: the widths of the ones before it. */
40
+ const insets = (ids, width) => {
41
+ const offsets = /* @__PURE__ */ new Map();
42
+ let offset = 0;
43
+ for (const id of ids) {
44
+ offsets.set(id, offset);
45
+ offset += Math.max(0, width(id));
46
+ }
47
+ return offsets;
48
+ };
49
+ const view = () => ({
50
+ /**
51
+ * The view of one `DataGrid`. It draws the grid's columns' headers and the
52
+ * window of cells `VirtualGrid.window` picks for the placed viewport, and
53
+ * sends the grid's Messages through `wrap`: `Moved` for a key, with the
54
+ * scroll that reveals the cell, `Focused` for the pointer, `Measured` from
55
+ * the scroll container. Attach Style and Behavior to the result.
56
+ */
57
+ define: (grid) => {
58
+ const editsAny = Object.values(grid.columns.byId).some((column) => column.edit !== void 0);
59
+ return SlotView.forMessages().define(GridSlots, (input, slots, h) => {
60
+ const { rowHeight, headerHeight, state } = input;
61
+ const projection = grid.project(input.rows, state.columns);
62
+ const width = grid.columnState.widthOf(state.columns);
63
+ const viewport = state.viewport;
64
+ const shown = grid.window(input);
65
+ const stop = GridFocus.tabStop(projection, state.focus.current);
66
+ const pageRows = Math.max(1, Math.floor((viewport.height - headerHeight) / rowHeight));
67
+ const drawn = [
68
+ ...shown.start,
69
+ ...shown.centerColumns,
70
+ ...shown.end
71
+ ];
72
+ const indexOf = new Map(projection.columns.map((id, index) => [id, index]));
73
+ const startInsets = insets(shown.start, width);
74
+ const endInsets = insets([...shown.end].reverse(), width);
75
+ const markOf = (address) => input.marks === void 0 ? Option.none() : input.marks(address);
76
+ const pinned = (id) => {
77
+ if (startInsets.has(id)) return Option.some("start");
78
+ if (endInsets.has(id)) return Option.some("end");
79
+ return Option.none();
80
+ };
81
+ const startEdge = shown.start.at(-1);
82
+ const endEdge = shown.end[0];
83
+ const pinnedEdge = (id) => {
84
+ if (id === startEdge) return [h.DataAttribute("pinned-edge", "start")];
85
+ if (id === endEdge) return [h.DataAttribute("pinned-edge", "end")];
86
+ return [];
87
+ };
88
+ const cellStyle = (id) => {
89
+ const base = {
90
+ boxSizing: "border-box",
91
+ width: px(Math.max(0, width(id))),
92
+ flex: "none"
93
+ };
94
+ return Option.match(pinned(id), {
95
+ onNone: () => base,
96
+ onSome: (edge) => edge === "start" ? {
97
+ ...base,
98
+ position: "sticky",
99
+ insetInlineStart: px(startInsets.get(id))
100
+ } : {
101
+ ...base,
102
+ position: "sticky",
103
+ insetInlineEnd: px(endInsets.get(id))
104
+ }
105
+ });
106
+ };
107
+ const spacer = (size) => size > 0 ? [h.div([h.AriaHidden(true), h.Style({
108
+ width: px(size),
109
+ flex: "none"
110
+ })], [])] : [];
111
+ const across = (drawCell) => [
112
+ ...shown.start.map(drawCell),
113
+ ...spacer(shown.center.before),
114
+ ...shown.centerColumns.map(drawCell),
115
+ ...spacer(shown.center.after),
116
+ ...shown.end.map(drawCell)
117
+ ];
118
+ const isDrawn = (address) => Option.match(projection.positionOf(address), {
119
+ onNone: () => false,
120
+ onSome: ({ row }) => row >= shown.rows.start && row < shown.rows.end && drawn.includes(address.column)
121
+ });
122
+ const isRowShown = (address) => Option.isSome(projection.positionOf(address));
123
+ const onHeader = state.focus.header;
124
+ const activeDescendant = Option.match(onHeader, {
125
+ onSome: (column) => drawn.includes(column) ? Option.some(GridFocus.headerId(grid.id, column)) : Option.none(),
126
+ onNone: () => Option.map(Option.filter(stop, isDrawn), (address) => GridFocus.cellId(grid.id, address))
127
+ });
128
+ const revealOf = (address) => Option.flatMap(projection.positionOf(address), (position) => VirtualGrid.reveal({
129
+ projection,
130
+ rowHeight,
131
+ width,
132
+ headerHeight,
133
+ viewport,
134
+ position
135
+ }));
136
+ const selection = state.selection;
137
+ const isSelectedRow = GridSelection.isSelected(selection.rows);
138
+ const range = Option.flatMap(selection.cells, (cells) => GridSelection.boxOf(projection, cells));
139
+ const rangeColumns = Option.map(range, (box) => new Set(box.columns));
140
+ const selectionKey = (key, modifiers) => {
141
+ const toggle = modifiers.ctrlKey || modifiers.metaKey;
142
+ if (key === "Escape" && Option.isSome(selection.cells)) return Option.some(grid.Message.CellsCleared());
143
+ if ((key === "a" || key === "A") && toggle && !modifiers.shiftKey && !modifiers.altKey) {
144
+ if (grid.rowSelection === "multiple") return Option.some(grid.Message.AllRowsSelected());
145
+ if (grid.cellSelection) return Option.zipWith(projection.first(), projection.last(), (anchor, far) => grid.Message.CellsSelected({
146
+ anchor,
147
+ focus: far,
148
+ reveal: Option.none()
149
+ }));
150
+ return Option.none();
151
+ }
152
+ if (key === " " && !toggle && !modifiers.altKey && grid.rowSelection !== void 0) return Option.map(stop, (current) => {
153
+ if (!modifiers.shiftKey || grid.rowSelection !== "multiple") return grid.Message.RowSelected({ row: current.row });
154
+ const from = Option.getOrElse(selection.anchor, () => current.row);
155
+ return grid.Message.RowsExtended({
156
+ rows: GridSelection.rowsBetween(projection, from, current.row),
157
+ to: current.row
158
+ });
159
+ });
160
+ if (!grid.cellSelection) return Option.none();
161
+ return Option.map(GridSelection.extend(projection, {
162
+ range: selection.cells,
163
+ current: stop,
164
+ key,
165
+ modifiers,
166
+ pageRows,
167
+ ...input.direction === void 0 ? {} : { direction: input.direction }
168
+ }), (extended) => grid.Message.CellsSelected({
169
+ anchor: extended.anchor,
170
+ focus: extended.focus,
171
+ reveal: revealOf(extended.focus)
172
+ }));
173
+ };
174
+ const focusKey = (key, modifiers) => Option.map(GridFocus.target(projection, {
175
+ current: state.focus.current,
176
+ key,
177
+ modifiers,
178
+ pageRows,
179
+ ...input.direction === void 0 ? {} : { direction: input.direction }
180
+ }), (address) => grid.Message.Moved({
181
+ address,
182
+ reveal: revealOf(address)
183
+ }));
184
+ const flip = input.direction === "rtl" ? -1 : 1;
185
+ const sideways = /* @__PURE__ */ new Map([["ArrowRight", flip], ["ArrowLeft", -flip]]);
186
+ const plainKey = (modifiers) => !modifiers.shiftKey && !modifiers.ctrlKey && !modifiers.altKey && !modifiers.metaKey;
187
+ const revealHeader = (column) => Option.flatMap(projection.columnIndex(column), (columnIndex) => Option.flatMap(VirtualGrid.reveal({
188
+ projection,
189
+ rowHeight,
190
+ width,
191
+ headerHeight,
192
+ viewport,
193
+ position: {
194
+ row: shown.rows.start,
195
+ column: columnIndex
196
+ }
197
+ }), ({ left }) => left === viewport.left ? Option.none() : Option.some({
198
+ top: viewport.top,
199
+ left
200
+ })));
201
+ const headerTo = (column) => grid.Message.HeaderFocused({
202
+ column,
203
+ reveal: revealHeader(column)
204
+ });
205
+ const headerKey = (column, key, modifiers) => {
206
+ const columns = projection.columns;
207
+ const at = columns.indexOf(column);
208
+ const step = sideways.get(key);
209
+ const opensMenu = key === "ArrowDown" && modifiers.altKey || key === "F10" && modifiers.shiftKey || key === "ContextMenu";
210
+ if (input.columnMenu === true && opensMenu) return Option.some(grid.Message.MenuOpened({ column }));
211
+ if (modifiers.altKey) return Option.none();
212
+ if (step !== void 0 && modifiers.shiftKey && (modifiers.ctrlKey || modifiers.metaKey)) {
213
+ const region = [
214
+ "start",
215
+ "center",
216
+ "end"
217
+ ].find((name) => state.columns[name].includes(column));
218
+ if (region === void 0) return Option.none();
219
+ const index = state.columns[region].indexOf(column) + step;
220
+ return Option.some(grid.Message.ColumnMoved({
221
+ column,
222
+ region,
223
+ index
224
+ }));
225
+ }
226
+ if (step !== void 0 && modifiers.shiftKey && !modifiers.ctrlKey && !modifiers.metaKey) return Option.some(grid.Message.ColumnResized({
227
+ column,
228
+ width: width(column) + resizeStep * step
229
+ }));
230
+ if (!plainKey(modifiers)) return Option.none();
231
+ if (step !== void 0) {
232
+ const next = columns[Math.min(Math.max(at + step, 0), columns.length - 1)];
233
+ return Option.fromUndefinedOr(next).pipe(Option.map(headerTo));
234
+ }
235
+ if (key === "Home") return Option.fromUndefinedOr(columns[0]).pipe(Option.map(headerTo));
236
+ if (key === "End") return Option.fromUndefinedOr(columns[columns.length - 1]).pipe(Option.map(headerTo));
237
+ if (key === "ArrowDown" || key === "Escape") {
238
+ const row = Option.orElse(Option.map(Option.filter(stop, isRowShown), (current) => current.row), () => Option.map(projection.first(), (first) => first.row));
239
+ return Option.map(row, (key) => {
240
+ const address = {
241
+ row: key,
242
+ column
243
+ };
244
+ return grid.Message.Moved({
245
+ address,
246
+ reveal: revealOf(address)
247
+ });
248
+ });
249
+ }
250
+ return Option.none();
251
+ };
252
+ const upToHeader = (key, modifiers) => key === "ArrowUp" && plainKey(modifiers) ? Option.flatMap(Option.filter(stop, (current) => Option.exists(projection.positionOf(current), ({ row }) => row === 0)), (current) => Option.some(headerTo(current.column))) : Option.none();
253
+ const editing = Option.filter(state.editing, (edit) => isDrawn(edit.address));
254
+ const draftOf = (address) => grid.draftOf(projection, address);
255
+ const editKey = (key, modifiers) => Option.flatMap(Option.filter(stop, (address) => grid.columns.byId[address.column].edit !== void 0), (address) => {
256
+ if ((key === "Enter" || key === "F2") && plainKey(modifiers)) return Option.some(grid.Message.EditStarted({
257
+ address,
258
+ draft: draftOf(address)
259
+ }));
260
+ if (key === "Escape") return Option.some(grid.Message.EditCancelled());
261
+ if (!(key.length === 1 && !modifiers.ctrlKey && !modifiers.metaKey && !modifiers.altKey)) return Option.none();
262
+ return Option.some(grid.Message.EditTyped({
263
+ address,
264
+ text: key,
265
+ from: draftOf(address)
266
+ }));
267
+ });
268
+ const editorKey = (address, key, modifiers) => {
269
+ if (key === "Escape") return Option.some(grid.Message.EditCancelled());
270
+ const back = modifiers.shiftKey ? -1 : 1;
271
+ const offsets = {
272
+ Enter: { rows: back },
273
+ Tab: { columns: back * flip }
274
+ };
275
+ const offset = Object.hasOwn(offsets, key) ? offsets[key] : void 0;
276
+ if (offset === void 0 || modifiers.ctrlKey || modifiers.metaKey || modifiers.altKey) return Option.none();
277
+ const next = projection.moveBy(address, offset);
278
+ return Option.some(grid.Message.EditCommitted({
279
+ next,
280
+ reveal: Option.flatMap(next, revealOf)
281
+ }));
282
+ };
283
+ const choiceSteps = {
284
+ ArrowDown: grid.Message.EditStepped({ by: "next" }),
285
+ ArrowUp: grid.Message.EditStepped({ by: "previous" }),
286
+ Home: grid.Message.EditStepped({ by: "first" }),
287
+ End: grid.Message.EditStepped({ by: "last" }),
288
+ PageDown: grid.Message.EditStepped({ by: "pageNext" }),
289
+ PageUp: grid.Message.EditStepped({ by: "pagePrevious" })
290
+ };
291
+ const choiceKey = (address, key, modifiers) => {
292
+ if (key === "ArrowUp" && modifiers.altKey) return Option.some(grid.Message.EditCommitted({
293
+ next: Option.none(),
294
+ reveal: Option.none()
295
+ }));
296
+ if (modifiers.ctrlKey || modifiers.metaKey || modifiers.altKey) return editorKey(address, key, modifiers);
297
+ if (Object.hasOwn(choiceSteps, key)) return Option.some(choiceSteps[key]);
298
+ if (key.length === 1) return Option.some(grid.Message.EditTyped({
299
+ address,
300
+ text: key,
301
+ from: draftOf(address)
302
+ }));
303
+ return editorKey(address, key, modifiers);
304
+ };
305
+ const choiceRows = 8;
306
+ const choiceList = (address, options, draft, shared) => {
307
+ const cellId = GridFocus.cellId(grid.id, address);
308
+ const listId = `${cellId}:options`;
309
+ const optionId = (index) => `${cellId}:option:${index}`;
310
+ const active = options.indexOf(draft);
311
+ const top = Option.match(projection.positionOf(address), {
312
+ onNone: () => 0,
313
+ onSome: ({ row }) => headerHeight + row * rowHeight - viewport.top
314
+ });
315
+ const listHeight = Math.min(options.length, choiceRows) * rowHeight;
316
+ const above = viewport.height - (top + rowHeight) < listHeight && top - headerHeight >= listHeight;
317
+ return [h.div(slots.choice.attrs([
318
+ ...shared,
319
+ h.Role("combobox"),
320
+ h.Tabindex(0),
321
+ h.AriaExpanded(true),
322
+ h.AriaControls(listId),
323
+ ...active === -1 ? [] : [h.AriaActiveDescendant(optionId(active))]
324
+ ]), [draft]), h.ul(slots.choiceList.attrs([
325
+ h.Id(listId),
326
+ h.Role("listbox"),
327
+ h.AriaLabel(grid.columns.byId[address.column].header),
328
+ h.DataAttribute("place", above ? "above" : "below"),
329
+ h.OnMount(KeepFocus()),
330
+ h.Style({
331
+ position: "absolute",
332
+ insetInlineStart: "0",
333
+ ...above ? { bottom: "100%" } : { top: "100%" }
334
+ })
335
+ ]), options.map((option, index) => h.li(slots.choiceOption.attrs([
336
+ h.Id(optionId(index)),
337
+ h.Role("option"),
338
+ h.AriaSelected(index === active),
339
+ h.OnClick(input.wrap(grid.Message.EditChosen({ draft: option })))
340
+ ]), [option])))];
341
+ };
342
+ const editorOf = (address, edit) => {
343
+ const errorId = `${GridFocus.cellId(grid.id, address)}:error`;
344
+ const sharedWith = (keys) => [
345
+ h.Key(`edit:${address.row}:${address.column}`),
346
+ h.AriaLabel(grid.columns.byId[address.column].header),
347
+ h.AriaInvalid(Option.isSome(edit.error)),
348
+ ...Option.isSome(edit.error) ? [h.AriaDescribedBy(errorId)] : [],
349
+ h.OnKeyDownPreventDefault((pressed, modifiers) => Option.map(keys(pressed, modifiers), input.wrap)),
350
+ h.OnBlur(input.wrap(grid.Message.EditCommitted({
351
+ next: Option.none(),
352
+ reveal: Option.none()
353
+ }))),
354
+ h.OnMount(HoldFocus()),
355
+ h.Style({
356
+ boxSizing: "border-box",
357
+ width: "100%",
358
+ height: "100%"
359
+ })
360
+ ];
361
+ const shared = sharedWith((pressed, modifiers) => editorKey(address, pressed, modifiers));
362
+ const changed = (draft) => input.wrap(grid.Message.EditChanged({ draft }));
363
+ const field = (mode, type = "text") => h.input(slots.editor.attrs([
364
+ ...shared,
365
+ h.Type(type),
366
+ h.Value(edit.draft),
367
+ ...Option.match(mode, {
368
+ onNone: () => [],
369
+ onSome: (decimal) => [h.InputMode(decimal)]
370
+ }),
371
+ h.OnInput(changed)
372
+ ]));
373
+ const message = Option.match(edit.error, {
374
+ onNone: () => [],
375
+ onSome: (error) => [h.div(slots.editorError.attrs([h.Id(errorId), h.Role("alert")]), [error])]
376
+ });
377
+ return [...Option.match(grid.editorFor(address.column), {
378
+ onNone: () => [field(Option.none())],
379
+ onSome: CellEditor.match({
380
+ Text: () => [field(Option.none())],
381
+ Number: () => [field(Option.some("decimal"))],
382
+ Date: () => [field(Option.none(), "date")],
383
+ Choice: ({ options }) => input.choiceEditor === "native" ? [h.select(slots.choice.attrs([...shared, h.OnChange(changed)]), options.map((option) => h.option(slots.choiceOption.attrs([h.Value(option), ...option === edit.draft ? [h.Selected(true)] : []]), [option])))] : choiceList(address, options, edit.draft, sharedWith((pressed, modifiers) => choiceKey(address, pressed, modifiers)))
384
+ })
385
+ }), ...message];
386
+ };
387
+ const clipboardBox = Option.filter(Option.orElse(range, () => Option.flatMap(stop, (address) => projection.box(address, address))), () => Option.isNone(editing));
388
+ const editable = (column) => grid.columns.byId[column].edit !== void 0;
389
+ const readOnly = (column) => editsAny && !editable(column);
390
+ const cornerOf = (box) => Option.map(projection.rows.keyAt(box.rows.start), (row) => ({
391
+ row,
392
+ column: box.columns[0]
393
+ }));
394
+ const clearedOf = (box) => Option.match(cornerOf(box), {
395
+ onNone: () => [],
396
+ onSome: (anchor) => Clipboard.pasteAt(projection, anchor, Array.from({ length: box.rows.end - box.rows.start }, () => box.columns.map(() => "")), {
397
+ editable,
398
+ from: draftOf
399
+ })
400
+ });
401
+ const clipboard = (box) => {
402
+ const text = remembered(projection, JSON.stringify(box), () => Clipboard.copy(projection, box, (address) => textOf(Option.match(Option.flatMap(projection.rowIndex(address.row), (index) => projection.rows.rowAt(index)), {
403
+ onNone: () => "",
404
+ onSome: (row) => grid.columns.byId[address.column].value(row)
405
+ }))));
406
+ const corner = cornerOf(box);
407
+ return [
408
+ h.OnCopyText(text),
409
+ h.OnCutText(text, input.wrap(grid.Message.Pasted({ cells: clearedOf(box) }))),
410
+ h.OnPastePreventDefault((pasted) => Option.flatMap(corner, (anchor) => {
411
+ const cells = Clipboard.pasteAt(projection, anchor, Clipboard.parseTsv(pasted), {
412
+ editable,
413
+ from: draftOf
414
+ });
415
+ return cells.length === 0 ? Option.none() : Option.some(input.wrap(grid.Message.Pasted({ cells })));
416
+ }))
417
+ ];
418
+ };
419
+ const sortOf = (column) => Option.fromUndefinedOr(input.sort?.[column]);
420
+ const sortKey = (column, key, modifiers) => key === "Enter" && plainKey(modifiers) ? Option.map(sortOf(column), (sorted) => sorted.message) : Option.none();
421
+ const fillKey = (key, modifiers) => {
422
+ const pressed = key.toLowerCase();
423
+ const toggle = modifiers.ctrlKey || modifiers.metaKey;
424
+ if (!editsAny || !toggle || modifiers.shiftKey || modifiers.altKey) return Option.none();
425
+ if (pressed !== "d" && pressed !== "r") return Option.none();
426
+ const box = Option.orElse(range, () => Option.flatMap(stop, (address) => projection.box(address, address)));
427
+ return Option.flatMap(box, ({ rows, columns }) => {
428
+ const first = columns[0];
429
+ const last = columns[columns.length - 1];
430
+ if (pressed === "d") {
431
+ const from = rows.end - rows.start > 1 ? rows.start : rows.start - 1;
432
+ return Option.zipWith(projection.rows.keyAt(from), projection.rows.keyAt(rows.end - 1), (sourceRow, toRow) => grid.Message.FillRequested({
433
+ source: {
434
+ anchor: {
435
+ row: sourceRow,
436
+ column: first
437
+ },
438
+ focus: {
439
+ row: sourceRow,
440
+ column: last
441
+ }
442
+ },
443
+ to: {
444
+ row: toRow,
445
+ column: first
446
+ }
447
+ }));
448
+ }
449
+ const at = projection.columns.indexOf(first);
450
+ const from = columns.length > 1 ? first : projection.columns[at - 1];
451
+ return Option.zipWith(Option.fromUndefinedOr(from), Option.zipWith(projection.rows.keyAt(rows.start), projection.rows.keyAt(rows.end - 1), (top, bottom) => ({
452
+ top,
453
+ bottom
454
+ })), (column, { top, bottom }) => grid.Message.FillRequested({
455
+ source: {
456
+ anchor: {
457
+ row: top,
458
+ column
459
+ },
460
+ focus: {
461
+ row: bottom,
462
+ column
463
+ }
464
+ },
465
+ to: {
466
+ row: top,
467
+ column: last
468
+ }
469
+ }));
470
+ });
471
+ };
472
+ const clearKey = (key, modifiers) => (key === "Delete" || key === "Backspace") && plainKey(modifiers) ? Option.flatMap(clipboardBox, (box) => {
473
+ const cells = clearedOf(box);
474
+ return cells.length === 0 ? Option.none() : Option.some(grid.Message.Pasted({ cells }));
475
+ }) : Option.none();
476
+ const onKey = (key, modifiers) => {
477
+ if (key === "Escape" && Option.isSome(state.dragging)) return Option.some(input.wrap(grid.Message.ColumnDragEnded({ completed: false })));
478
+ if (key === "Escape" && Option.isSome(state.filling)) return Option.some(input.wrap(grid.Message.FillEnded({ completed: false })));
479
+ return keyOf(key, modifiers);
480
+ };
481
+ const historyKey = (key, modifiers) => {
482
+ if (!(modifiers.ctrlKey || modifiers.metaKey) || modifiers.altKey) return Option.none();
483
+ const pressed = key.toLowerCase();
484
+ if (pressed === "z") return Option.some(modifiers.shiftKey ? grid.Message.RedoRequested() : grid.Message.UndoRequested());
485
+ return pressed === "y" && !modifiers.shiftKey ? Option.some(grid.Message.RedoRequested()) : Option.none();
486
+ };
487
+ const cellKeys = [
488
+ historyKey,
489
+ fillKey,
490
+ clearKey,
491
+ selectionKey,
492
+ editKey,
493
+ upToHeader,
494
+ focusKey
495
+ ];
496
+ const keyOf = (key, modifiers) => Option.match(onHeader, {
497
+ onSome: (column) => Option.orElse(sortKey(column, key, modifiers), () => Option.map(headerKey(column, key, modifiers), input.wrap)),
498
+ onNone: () => Option.map(cellKeys.reduce((found, next) => Option.orElse(found, () => next(key, modifiers)), Option.none()), input.wrap)
499
+ });
500
+ const pressed = Mount.mapMessage(CellPress(), (fact) => input.wrap(Match.valueTags(fact, {
501
+ CellClicked: ({ cell, shiftKey, toggleKey }) => grid.Message.CellPressed({
502
+ cell,
503
+ shiftKey,
504
+ toggleKey
505
+ }),
506
+ FillDragStarted: () => grid.Message.FillStarted(),
507
+ FillDraggedOver: ({ cell }) => grid.Message.FillDragged({ cell }),
508
+ FillDragEnded: ({ completed }) => grid.Message.FillEnded({ completed })
509
+ })));
510
+ const measured = Mount.mapMessage(GridViewport.Measure(), (reading) => input.wrap(grid.Message.Measured({
511
+ top: reading.top,
512
+ left: reading.left,
513
+ width: reading.width,
514
+ height: reading.height
515
+ })));
516
+ const rowCount = RowCount.match(projection.rowCount, {
517
+ Known: ({ total }) => total + 1,
518
+ Unknown: () => -1
519
+ });
520
+ const toward = input.direction === "rtl" ? -1 : 1;
521
+ const fillTarget = Option.map(Option.flatMap(state.filling, (filling) => Fill.plan(projection, filling)), ({ target }) => ({
522
+ rows: target.rows,
523
+ columns: new Set(target.columns)
524
+ }));
525
+ const handleAt = Option.filter(Option.orElse(Option.flatMap(range, (box) => Option.map(projection.rows.keyAt(box.rows.end - 1), (row) => ({
526
+ row,
527
+ column: box.columns[box.columns.length - 1]
528
+ }))), () => stop), () => editsAny);
529
+ const fillHandle = h.span(slots.fillHandle.attrs([
530
+ h.AriaHidden(true),
531
+ h.Attribute(fillHandleAttribute, ""),
532
+ h.Style({
533
+ position: "absolute",
534
+ insetInlineEnd: "0",
535
+ insetBlockEnd: "0"
536
+ })
537
+ ]), []);
538
+ const headerDragged = Mount.mapMessage(HeaderDrag(), (fact) => input.wrap(Match.valueTags(fact, {
539
+ HeaderDragStarted: ({ header }) => grid.Message.ColumnDragStarted({ header }),
540
+ HeaderDragged: ({ deltaX }) => grid.Message.ColumnDragged({ delta: deltaX * toward }),
541
+ HeaderDragEnded: ({ completed }) => grid.Message.ColumnDragEnded({ completed })
542
+ })));
543
+ const landing = Option.flatMap(state.dragging, ({ column, delta }) => {
544
+ const columns = grid.columnState;
545
+ const region = columns.regionOf(state.columns, column);
546
+ const moved = columns.move(state.columns, column, region, columns.dropAt(state.columns, column, delta));
547
+ if (moved === state.columns) return Option.none();
548
+ const hidden = new Set(moved.hidden);
549
+ const order = moved[region].filter((id) => !hidden.has(id));
550
+ const at = order.indexOf(column);
551
+ return at + 1 < order.length ? Option.some({
552
+ column: order[at + 1],
553
+ side: "before"
554
+ }) : Option.some({
555
+ column: order[at - 1],
556
+ side: "after"
557
+ });
558
+ });
559
+ const resizeHandle = (id) => {
560
+ const column = grid.columns.byId[id];
561
+ if (column.resizable === false) return [];
562
+ const now = width(id);
563
+ const steps = /* @__PURE__ */ new Map([["ArrowRight", resizeStep * toward], ["ArrowLeft", -16 * toward]]);
564
+ const dragged = Mount.mapMessage(Move(), (fact) => input.wrap(Match.valueTags(fact, {
565
+ MoveStarted: () => grid.Message.ResizeStarted({ column: id }),
566
+ Moved: ({ deltaX }) => grid.Message.ResizeMoved({ delta: deltaX * toward }),
567
+ MoveEnded: ({ completed }) => grid.Message.ResizeEnded({ completed })
568
+ })));
569
+ return [h.div(slots.resizeHandle.attrs([
570
+ h.Key(`resize:${id}:${toward}`),
571
+ h.Role("separator"),
572
+ h.AriaOrientation("vertical"),
573
+ h.AriaLabel(`Resize ${column.header}`),
574
+ h.AriaValuenow(now),
575
+ h.AriaValuemin(column.minWidth ?? Columns.minWidth),
576
+ ...column.maxWidth === void 0 ? [] : [h.AriaValuemax(column.maxWidth)],
577
+ h.Tabindex(-1),
578
+ h.OnKeyDownSelfPreventDefault((key) => Option.map(Option.fromUndefinedOr(steps.get(key)), (by) => input.wrap(grid.Message.ColumnResized({
579
+ column: id,
580
+ width: now + by
581
+ })))),
582
+ h.OnMount(dragged),
583
+ h.Style({
584
+ position: "absolute",
585
+ top: "0",
586
+ bottom: "0",
587
+ insetInlineEnd: "0",
588
+ width: px(resizeStep / 2),
589
+ cursor: "col-resize",
590
+ touchAction: "none"
591
+ })
592
+ ]), [])];
593
+ };
594
+ const menuPart = (column, part) => [
595
+ grid.id,
596
+ column,
597
+ "menu",
598
+ part
599
+ ].map(encodeURIComponent).join(":");
600
+ const say = (template, column) => template.replace("{column}", grid.columns.byId[column].header);
601
+ const itemLabel = (item) => grid.MenuItem.match(item, {
602
+ Pin: ({ region }) => ({
603
+ start: words.pinStart ?? "Pin to start",
604
+ end: words.pinEnd ?? "Pin to end",
605
+ center: words.unpin ?? "Unpin"
606
+ })[region],
607
+ Hide: () => words.hide ?? "Hide column",
608
+ Show: ({ column: other }) => say(words.show ?? "Show {column}", other)
609
+ });
610
+ const columnMenu = (id) => {
611
+ if (input.columnMenu !== true) return [];
612
+ const open = Option.filter(state.menu, (menu) => menu.column === id);
613
+ const button = h.button(slots.menuButton.attrs([
614
+ h.Type("button"),
615
+ h.Tabindex(-1),
616
+ h.AriaHasPopup("menu"),
617
+ h.AriaExpanded(Option.isSome(open)),
618
+ h.AriaLabel(say(words.menu ?? "{column} menu", id)),
619
+ ...grid.columns.byId[id].resizable === false ? [] : [h.Style({ marginInlineEnd: px(resizeStep / 2) })],
620
+ h.OnClick(input.wrap(Option.match(open, {
621
+ onNone: () => grid.Message.MenuOpened({ column: id }),
622
+ onSome: () => grid.Message.MenuClosed()
623
+ })))
624
+ ]), [String.fromCharCode(8942)]);
625
+ return Option.match(open, {
626
+ onNone: () => [button],
627
+ onSome: ({ active }) => {
628
+ const items = grid.menuItems(state.columns, id);
629
+ const count = items.length;
630
+ const menuKey = (key) => {
631
+ if (key === "ArrowDown") return Option.some(grid.Message.MenuMoved({ active: (active + 1) % count }));
632
+ if (key === "ArrowUp") return Option.some(grid.Message.MenuMoved({ active: (active - 1 + count) % count }));
633
+ if (key === "Home") return Option.some(grid.Message.MenuMoved({ active: 0 }));
634
+ if (key === "End") return Option.some(grid.Message.MenuMoved({ active: count - 1 }));
635
+ if (key === "Enter" || key === " ") return Option.some(grid.Message.MenuChosen({ index: active }));
636
+ if (key === "Escape") return Option.some(grid.Message.MenuClosed());
637
+ return Option.none();
638
+ };
639
+ return [button, h.div(slots.menu.attrs([
640
+ h.Key(menuPart(id, "list")),
641
+ h.Role("menu"),
642
+ h.Id(menuPart(id, "list")),
643
+ h.Tabindex(-1),
644
+ h.AriaLabel(grid.columns.byId[id].header),
645
+ ...count === 0 ? [] : [h.AriaActiveDescendant(menuPart(id, String(active)))],
646
+ h.OnKeyDownPreventDefault((key) => Option.map(menuKey(key), input.wrap)),
647
+ h.OnFocusLeave(input.wrap(grid.Message.MenuClosed())),
648
+ h.OnMount(HoldFocus()),
649
+ h.Style({
650
+ position: "absolute",
651
+ top: "100%",
652
+ insetInlineEnd: "0"
653
+ })
654
+ ]), items.map((item, index) => h.div(slots.menuItem.attrs([
655
+ h.Role("menuitem"),
656
+ h.Id(menuPart(id, String(index))),
657
+ ...index === active ? [h.DataAttribute("active", "true")] : [],
658
+ h.OnClick(input.wrap(grid.Message.MenuChosen({ index })))
659
+ ]), [itemLabel(item)])))];
660
+ }
661
+ });
662
+ };
663
+ const headerCell = (id) => {
664
+ const dragged = Option.filter(state.dragging, (dragging) => dragging.column === id);
665
+ return h.div(slots.headerCell.attrs([
666
+ h.Role("columnheader"),
667
+ h.Id(GridFocus.headerId(grid.id, id)),
668
+ ...Option.contains(onHeader, id) ? [h.DataAttribute("focused", "true")] : [],
669
+ h.AriaColindex(indexOf.get(id) + 1),
670
+ h.Style({
671
+ ...cellStyle(id),
672
+ ...Option.isNone(pinned(id)) ? { position: "relative" } : {},
673
+ ...Option.match(dragged, {
674
+ onNone: () => ({}),
675
+ onSome: ({ delta }) => ({ transform: `translateX(${px(delta * toward)})` })
676
+ })
677
+ }),
678
+ ...Option.isSome(dragged) ? [h.DataAttribute("dragging", "true")] : [],
679
+ ...Option.match(Option.filter(landing, (place) => place.column === id), {
680
+ onNone: () => [],
681
+ onSome: (place) => [h.DataAttribute("drop", place.side)]
682
+ }),
683
+ ...Option.match(pinned(id), {
684
+ onNone: () => [],
685
+ onSome: (edge) => [h.DataAttribute("pinned", edge)]
686
+ }),
687
+ ...pinnedEdge(id),
688
+ ...readOnly(id) ? [h.AriaReadonly(true), h.Title("Read-only")] : [],
689
+ ...Option.match(sortOf(id), {
690
+ onNone: () => [],
691
+ onSome: (sorted) => [h.AriaSort(ariaSort(sorted.direction))]
692
+ })
693
+ ]), [
694
+ Option.match(sortOf(id), {
695
+ onNone: () => grid.columns.byId[id].header,
696
+ onSome: (sorted) => h.button(slots.sort.attrs([
697
+ h.Type("button"),
698
+ h.Tabindex(-1),
699
+ h.OnClick(sorted.message),
700
+ ...sorted.direction === void 0 ? [] : [h.DataAttribute("sort", sorted.direction)]
701
+ ]), [grid.columns.byId[id].header])
702
+ }),
703
+ ...columnMenu(id),
704
+ ...resizeHandle(id)
705
+ ]);
706
+ };
707
+ const rowAt = (index) => Option.match(Option.zipWith(projection.rows.rowAt(index), projection.rows.keyAt(index), (row, key) => ({
708
+ row,
709
+ key
710
+ })), {
711
+ onNone: () => h.div(slots.placeholder.attrs([
712
+ h.AriaHidden(true),
713
+ h.Key(`unloaded:${index}`),
714
+ h.Style({
715
+ height: px(rowHeight),
716
+ width: px(shown.width)
717
+ })
718
+ ]), []),
719
+ onSome: ({ row, key }) => h.div(slots.row.attrs([
720
+ h.Key(key),
721
+ h.Role("row"),
722
+ h.AriaRowindex(index + 2),
723
+ ...grid.rowSelection === void 0 ? [] : [h.AriaSelected(isSelectedRow(key))],
724
+ h.Style({
725
+ display: "flex",
726
+ height: px(rowHeight),
727
+ width: px(shown.width)
728
+ })
729
+ ]), across((id) => {
730
+ const address = {
731
+ row: key,
732
+ column: id
733
+ };
734
+ const focused = Option.isNone(onHeader) && Option.exists(stop, (current) => current.row === key && current.column === id);
735
+ const inRange = Option.exists(Option.zipWith(range, rangeColumns, (box, ids) => ({
736
+ box,
737
+ ids
738
+ })), ({ box, ids }) => index >= box.rows.start && index < box.rows.end && ids.has(id));
739
+ const edited = Option.filter(editing, (edit) => edit.address.row === key && edit.address.column === id);
740
+ const handled = Option.exists(handleAt, (at) => at.row === key && at.column === id);
741
+ const filled = Option.exists(fillTarget, (box) => index >= box.rows.start && index < box.rows.end && box.columns.has(id));
742
+ return h.div(slots.cell.attrs([
743
+ h.Role("gridcell"),
744
+ h.Id(GridFocus.cellId(grid.id, address)),
745
+ h.AriaColindex(indexOf.get(id) + 1),
746
+ h.Style({
747
+ ...cellStyle(id),
748
+ ...(Option.isSome(edited) || handled) && Option.isNone(pinned(id)) ? { position: "relative" } : {}
749
+ }),
750
+ ...filled ? [h.DataAttribute("fill", "target")] : [],
751
+ ...grid.cellSelection ? [h.AriaSelected(inRange)] : [],
752
+ ...focused ? [h.DataAttribute("focused", "true")] : [],
753
+ ...Option.match(pinned(id), {
754
+ onNone: () => [],
755
+ onSome: (edge) => [h.DataAttribute("pinned", edge)]
756
+ }),
757
+ ...pinnedEdge(id),
758
+ ...editable(id) ? [h.DataAttribute("editable", "true"), h.OnDoubleClick(input.wrap(grid.Message.EditStarted({
759
+ address,
760
+ draft: draftOf(address)
761
+ })))] : [],
762
+ ...Option.isSome(edited) ? [h.DataAttribute("editing", "true")] : [],
763
+ ...readOnly(id) ? [h.AriaReadonly(true)] : [],
764
+ ...Option.match(markOf(address), {
765
+ onNone: () => [],
766
+ onSome: (mark) => [
767
+ h.DataAttribute("mark", mark.name),
768
+ h.AriaDescription(mark.description),
769
+ h.Title(mark.description)
770
+ ]
771
+ })
772
+ ]), Option.match(edited, {
773
+ onNone: () => [input.cell?.(id, row, h) ?? textOf(grid.columns.byId[id].value(row)), ...handled ? [fillHandle] : []],
774
+ onSome: (edit) => editorOf(address, edit)
775
+ }));
776
+ }))
777
+ });
778
+ const bodyRows = Array.from({ length: shown.rows.end - shown.rows.start }, (_, offset) => rowAt(shown.rows.start + offset));
779
+ const empty = addressableRows(projection.rowCount) === 0;
780
+ const status = input.status ?? RowStatus.Ready();
781
+ const busy = isBusy(status);
782
+ const words = input.words ?? {};
783
+ const failure = (message) => [h.p(slots.status.attrs([h.Role("alert")]), [(words.failed ?? "{message}").replaceAll("{message}", message)]), ...input.onRetry === void 0 ? [] : [h.button(slots.retry.attrs([h.Type("button"), h.OnClick(input.onRetry)]), [words.retry ?? "Try again"])]];
784
+ const emptyBody = RowStatus.match(status, {
785
+ Loading: () => [h.p(slots.status.attrs([h.Role("status"), h.AriaBusy(true)]), [words.loading ?? "Loading…"])],
786
+ Refreshing: () => [h.p(slots.status.attrs([h.Role("status")]), [words.empty ?? "No rows."])],
787
+ Ready: () => [h.p(slots.status.attrs([h.Role("status")]), [words.empty ?? "No rows."])],
788
+ Failed: ({ message }) => failure(message)
789
+ });
790
+ const more = Option.filter(Option.fromUndefinedOr(input.onMore), () => RowCount.match(projection.rowCount, {
791
+ Known: () => false,
792
+ Unknown: () => true
793
+ }));
794
+ const footer = [...empty ? [] : RowStatus.match(status, {
795
+ Ready: () => [],
796
+ Loading: () => [],
797
+ Refreshing: () => [],
798
+ Failed: ({ message }) => failure(message)
799
+ }), ...Option.match(more, {
800
+ onNone: () => [],
801
+ onSome: (message) => [h.button(slots.more.attrs([
802
+ h.Type("button"),
803
+ h.Disabled(busy),
804
+ h.OnClick(message),
805
+ h.Key(`more:${addressableRows(projection.rowCount)}:${busy}`)
806
+ ]), [words.more ?? "More"])]
807
+ })];
808
+ return h.div(slots.root.attrs([
809
+ h.Id(grid.id),
810
+ h.Role("grid"),
811
+ ...busy ? [h.AriaBusy(true)] : [],
812
+ h.AriaLabel(input.label),
813
+ h.Tabindex(0),
814
+ h.AriaRowcount(rowCount),
815
+ h.AriaColcount(projection.columns.length),
816
+ ...grid.rowSelection === "multiple" || grid.cellSelection ? [h.AriaMultiSelectable(true)] : [],
817
+ ...Option.match(activeDescendant, {
818
+ onNone: () => [],
819
+ onSome: (id) => [h.AriaActiveDescendant(id)]
820
+ }),
821
+ h.OnKeyDownSelfPreventDefault(onKey),
822
+ ...Option.match(clipboardBox, {
823
+ onNone: () => [],
824
+ onSome: clipboard
825
+ }),
826
+ h.OnMount(measured),
827
+ h.Style({
828
+ overflow: "auto",
829
+ position: "relative"
830
+ })
831
+ ]), [
832
+ h.div(slots.header.attrs([h.Role("rowgroup"), h.Style({
833
+ position: "sticky",
834
+ top: "0"
835
+ })]), [h.div(slots.headerRow.attrs([
836
+ h.Role("row"),
837
+ h.AriaRowindex(1),
838
+ h.Key(`header-drag:${toward}`),
839
+ h.OnMount(headerDragged),
840
+ h.Style({
841
+ display: "flex",
842
+ height: px(headerHeight),
843
+ width: px(shown.width)
844
+ })
845
+ ]), across(headerCell))]),
846
+ h.div(slots.body.attrs([
847
+ h.Role("rowgroup"),
848
+ h.OnMount(pressed),
849
+ h.Style({
850
+ height: px(shown.height - headerHeight),
851
+ width: px(shown.width)
852
+ })
853
+ ]), empty ? emptyBody : [...shown.rows.before > 0 ? [h.div([h.AriaHidden(true), h.Style({ height: px(shown.rows.before) })], [])] : [], ...bodyRows]),
854
+ ...footer.length === 0 ? [] : [h.div(slots.footer.attrs(), footer)]
855
+ ]);
856
+ }, { name: grid.id });
857
+ } });
858
+ const DataGridView = view;
859
+ export { DataGridView, isBusy };
860
+
861
+ //# sourceMappingURL=view.js.map