@gridkitjs/core 0.10.0 → 0.12.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/index.js CHANGED
@@ -63,13 +63,23 @@ function getColumnId(column) {
63
63
  function resolveRowId(row, index, getRowId) {
64
64
  return getRowId?.(row, index) ?? String(index);
65
65
  }
66
+ function labelFromField(field) {
67
+ return field.split(".").join(" ");
68
+ }
66
69
  function resolveColumnLabel(column) {
67
70
  const { headerTemplate } = column;
68
71
  if (headerTemplate === void 0) {
69
- return column.field.split(".").join(" ");
72
+ return column.header ?? labelFromField(column.field);
70
73
  }
71
74
  return typeof headerTemplate === "function" ? headerTemplate() : headerTemplate;
72
75
  }
76
+ function resolveColumnHeaderText(column) {
77
+ if (column.header !== void 0) {
78
+ return column.header;
79
+ }
80
+ const label = resolveColumnLabel(column);
81
+ return typeof label === "string" ? label : labelFromField(column.field);
82
+ }
73
83
  function buildKeyShortcuts(capabilities) {
74
84
  return [
75
85
  capabilities.reorderable ? "Control+ArrowLeft Control+ArrowRight" : "",
@@ -80,855 +90,2808 @@ function buildKeyShortcuts(capabilities) {
80
90
  capabilities.sortable ? "Alt+ArrowUp" : "",
81
91
  // Down rather than reusing Up: it reads as "drill into groups", and Up is
82
92
  // already sort's own mnemonic on the same header.
83
- capabilities.groupable ? "Alt+ArrowDown" : ""
93
+ capabilities.groupable ? "Alt+ArrowDown" : "",
94
+ // The ContextMenu key needs no mention: it says what it does on its face.
95
+ capabilities.contextMenu === true ? "Shift+F10" : ""
84
96
  ].filter(Boolean).join(" ");
85
97
  }
86
98
  function accessDotted(obj, path) {
87
99
  return path.split(".").reduce((value, key) => isNested(value) ? value[key] : void 0, obj);
88
100
  }
89
101
 
90
- // src/util/navigation.ts
91
- var HEADER_ROW = -1;
92
- function clampFocus(focus, rowCount, columnCount) {
93
- return {
94
- rowIndex: Math.min(Math.max(focus.rowIndex, HEADER_ROW), rowCount - 1),
95
- columnIndex: Math.min(
96
- Math.max(focus.columnIndex, 0),
97
- Math.max(columnCount - 1, 0)
98
- )
99
- };
102
+ // src/util/sorting.ts
103
+ function isEmpty(value) {
104
+ return value === null || value === void 0;
100
105
  }
101
- function noRowsSkippable() {
102
- return false;
106
+ function toTime(value) {
107
+ return value instanceof Date ? value.getTime() : new Date(String(value)).getTime();
103
108
  }
104
- function skipSkippableRows(rowIndex, direction, rowCount, isSkippableRow, fallback) {
105
- let next = rowIndex;
106
- while (next >= 0 && next < rowCount && isSkippableRow(next)) {
107
- const stepped = next + direction;
108
- if (stepped < HEADER_ROW || stepped >= rowCount) {
109
- return fallback;
110
- }
111
- next = stepped;
109
+ function compareValues(a, b, type) {
110
+ if (isEmpty(a)) {
111
+ return isEmpty(b) ? 0 : 1;
112
112
  }
113
- return next;
114
- }
115
- function nextFocusForKey(key, modifiers, focus, rowCount, columnCount, pageSize, isSkippableRow = noRowsSkippable) {
116
- if (modifiers.altKey) {
117
- return null;
113
+ if (isEmpty(b)) {
114
+ return -1;
118
115
  }
119
- const { rowIndex, columnIndex } = focus;
120
- switch (key) {
121
- case "ArrowLeft":
122
- if (modifiers.ctrlKey) return null;
123
- return { rowIndex, columnIndex: columnIndex - 1 };
124
- case "ArrowRight":
125
- if (modifiers.ctrlKey) return null;
126
- return { rowIndex, columnIndex: columnIndex + 1 };
127
- case "ArrowUp":
128
- return {
129
- rowIndex: skipSkippableRows(
130
- rowIndex - 1,
131
- -1,
132
- rowCount,
133
- isSkippableRow,
134
- rowIndex
135
- ),
136
- columnIndex
137
- };
138
- case "ArrowDown":
139
- return {
140
- rowIndex: skipSkippableRows(
141
- rowIndex + 1,
142
- 1,
143
- rowCount,
144
- isSkippableRow,
145
- rowIndex
146
- ),
147
- columnIndex
148
- };
149
- // Ctrl takes Home and End to the grid's ends rather than the row's, which
150
- // is the one place Ctrl is navigation rather than reorder.
151
- case "Home":
152
- return modifiers.ctrlKey ? { rowIndex: HEADER_ROW, columnIndex: 0 } : { rowIndex, columnIndex: 0 };
153
- case "End":
154
- return modifiers.ctrlKey ? {
155
- rowIndex: skipSkippableRows(
156
- rowCount - 1,
157
- -1,
158
- rowCount,
159
- isSkippableRow,
160
- rowIndex
161
- ),
162
- columnIndex: columnCount - 1
163
- } : { rowIndex, columnIndex: columnCount - 1 };
164
- case "PageUp":
165
- return {
166
- rowIndex: skipSkippableRows(
167
- rowIndex - pageSize,
168
- -1,
169
- rowCount,
170
- isSkippableRow,
171
- rowIndex
172
- ),
173
- columnIndex
174
- };
175
- case "PageDown":
176
- return {
177
- rowIndex: skipSkippableRows(
178
- rowIndex + pageSize,
179
- 1,
180
- rowCount,
181
- isSkippableRow,
182
- rowIndex
183
- ),
184
- columnIndex
185
- };
116
+ switch (type) {
117
+ case "number":
118
+ case "decimal":
119
+ case "currency":
120
+ case "percent":
121
+ return Number(a) - Number(b);
122
+ case "boolean":
123
+ return Number(Boolean(a)) - Number(Boolean(b));
124
+ case "date":
125
+ case "dateTime":
126
+ case "time":
127
+ return toTime(a) - toTime(b);
186
128
  default:
187
- return null;
129
+ return String(a).localeCompare(String(b));
188
130
  }
189
131
  }
190
-
191
- // src/util/ordering.ts
192
- function applyColumnOrder(columns, order) {
193
- if (order.length === 0) {
194
- return columns;
195
- }
196
- const byId = new Map(columns.map((column) => [getColumnId(column), column]));
197
- const listed = order.map((id) => byId.get(id)).filter((column) => column !== void 0);
198
- const taken = new Set(listed.map((column) => getColumnId(column)));
199
- return [
200
- ...listed,
201
- ...columns.filter((column) => !taken.has(getColumnId(column)))
202
- ];
132
+ function sortDirectionFor(sort, columnId) {
133
+ return sort.find((entry) => entry.columnId === columnId)?.direction ?? null;
203
134
  }
204
- function moveColumnBefore(orderedIds, movedId, beforeId) {
205
- if (movedId === beforeId || !orderedIds.includes(movedId)) {
206
- return orderedIds;
135
+ function sortPriorityFor(sort, columnId) {
136
+ const index = sort.findIndex((entry) => entry.columnId === columnId);
137
+ return index === -1 ? null : index + 1;
138
+ }
139
+ function toggleColumnSort(sort, columnId, options) {
140
+ const index = sort.findIndex((entry) => entry.columnId === columnId);
141
+ const isSoleEntry = sort.length === 1 && index === 0;
142
+ if (!options.stack && !isSoleEntry) {
143
+ return [{ columnId, direction: "asc" }];
207
144
  }
208
- const without = orderedIds.filter((id) => id !== movedId);
209
- const target = beforeId === null ? without.length : without.indexOf(beforeId);
210
- if (target === -1) {
211
- return orderedIds;
145
+ const current = index === -1 ? void 0 : sort[index]?.direction;
146
+ const next = current === void 0 ? "asc" : current === "asc" ? "desc" : null;
147
+ if (next === null) {
148
+ return sort.filter((entry) => entry.columnId !== columnId);
212
149
  }
213
- const next = [...without];
214
- next.splice(target, 0, movedId);
215
- return next.every((id, index) => id === orderedIds[index]) ? orderedIds : next;
216
- }
217
- function movesColumn(orderedIds, movedId, beforeId) {
218
- return moveColumnBefore(orderedIds, movedId, beforeId) !== orderedIds;
219
- }
220
- function resolveDropBefore(orderedIds, targetId, side) {
221
- if (side === "before") {
222
- return targetId;
150
+ if (index === -1) {
151
+ return [...sort, { columnId, direction: next }];
223
152
  }
224
- const index = orderedIds.indexOf(targetId);
225
- return index === -1 ? null : orderedIds[index + 1] ?? null;
153
+ return sort.map(
154
+ (entry, i) => i === index ? { columnId, direction: next } : entry
155
+ );
226
156
  }
227
- function resolveKeyboardDropTarget(orderedIds, columnId, direction) {
228
- const index = orderedIds.indexOf(columnId);
229
- if (index === -1) {
230
- return void 0;
157
+ function sortRows(rows, sort, columns) {
158
+ if (sort.length === 0) {
159
+ return rows;
231
160
  }
232
- const targetIndex = direction === -1 ? index - 1 : index + 2;
233
- if (targetIndex < 0) {
234
- return void 0;
161
+ const byId = new Map(columns.map((entry) => [entry.id, entry.column]));
162
+ const active = sort.filter((entry) => byId.has(entry.columnId));
163
+ if (active.length === 0) {
164
+ return rows;
235
165
  }
236
- return orderedIds[targetIndex] ?? null;
166
+ const sorted = [...rows].sort((a, b) => {
167
+ for (const { columnId, direction } of active) {
168
+ const column = byId.get(columnId);
169
+ if (column === void 0) {
170
+ continue;
171
+ }
172
+ const left = accessDotted(a.row, column.field);
173
+ const right = accessDotted(b.row, column.field);
174
+ const leftEmpty = isEmpty(left);
175
+ const rightEmpty = isEmpty(right);
176
+ if (leftEmpty || rightEmpty) {
177
+ if (leftEmpty && rightEmpty) {
178
+ continue;
179
+ }
180
+ return leftEmpty ? 1 : -1;
181
+ }
182
+ const result = compareValues(left, right, column.type);
183
+ if (result !== 0) {
184
+ return direction === "asc" ? result : -result;
185
+ }
186
+ }
187
+ return 0;
188
+ });
189
+ return sorted.map(
190
+ (row, index) => row.rowIndex === index ? row : { ...row, rowIndex: index }
191
+ );
237
192
  }
238
193
 
239
- // src/util/selection.ts
240
- var NO_CHANGE = { added: [], removed: [] };
241
- function sameSelection(a, b) {
242
- return a.length === b.length && a.every((id, index) => id === b[index]);
194
+ // src/util/grouping.ts
195
+ function groupRowId(path) {
196
+ return JSON.stringify(path);
243
197
  }
244
- function toggleSelection(selection, id, mode) {
245
- if (mode === false) {
246
- return selection;
198
+ function bucketKey(value) {
199
+ if (value === null || value === void 0) {
200
+ return "\0empty";
247
201
  }
248
- if (selection.includes(id)) {
249
- return selection.filter((selectedId) => selectedId !== id);
202
+ if (value instanceof Date) {
203
+ return `\0date:${String(value.getTime())}`;
250
204
  }
251
- return mode === "single" ? [id] : [...selection, id];
252
- }
253
- function selectOnly(selection, id, mode) {
254
- if (mode === false) {
255
- return selection;
205
+ switch (typeof value) {
206
+ case "string":
207
+ case "number":
208
+ case "boolean":
209
+ case "bigint":
210
+ case "symbol":
211
+ return `\0${typeof value}:${String(value)}`;
212
+ case "function":
213
+ return "\0function";
214
+ default:
215
+ return `\0object:${JSON.stringify(value)}`;
256
216
  }
257
- return selection.length === 1 && selection[0] === id ? selection : [id];
258
217
  }
259
- function selectRange(selection, orderedIds, anchorId, focusId, mode) {
260
- if (mode !== "multiple") {
261
- return selectOnly(selection, focusId, mode);
218
+ function partitionByValue(rows, column, direction) {
219
+ const buckets = /* @__PURE__ */ new Map();
220
+ for (const row of rows) {
221
+ const value = accessDotted(row.row, column.field);
222
+ const key = bucketKey(value);
223
+ const bucket = buckets.get(key);
224
+ if (bucket === void 0) {
225
+ buckets.set(key, { value, rows: [row] });
226
+ } else {
227
+ bucket.rows.push(row);
228
+ }
262
229
  }
263
- const anchor = orderedIds.indexOf(anchorId);
264
- const focus = orderedIds.indexOf(focusId);
265
- if (anchor === -1 || focus === -1) {
266
- return selection;
230
+ const groups = [...buckets.values()];
231
+ groups.sort((a, b) => {
232
+ const result = compareValues(a.value, b.value, column.type);
233
+ return direction === "asc" ? result : -result;
234
+ });
235
+ return groups;
236
+ }
237
+ function buildLevel(rows, levels, parentPath, expansion, byId, out) {
238
+ const level = levels[0];
239
+ if (level === void 0) {
240
+ out.push(...rows);
241
+ return;
242
+ }
243
+ const column = byId.get(level.columnId);
244
+ if (column === void 0) {
245
+ out.push(...rows);
246
+ return;
247
+ }
248
+ const rest = levels.slice(1);
249
+ const groups = partitionByValue(rows, column, level.direction ?? "asc");
250
+ for (const group of groups) {
251
+ const path = [...parentPath, group.value];
252
+ const groupId = groupRowId(path);
253
+ const expanded = !expansion.includes(groupId);
254
+ const header = {
255
+ kind: "group",
256
+ groupId,
257
+ level: parentPath.length,
258
+ columnId: level.columnId,
259
+ value: group.value,
260
+ path,
261
+ expanded,
262
+ count: group.rows.length,
263
+ // Renumbered globally once `groupRows` has the whole flattened array —
264
+ // a header has no row of its own to carry a dataset position, so
265
+ // `datasetIndex` takes the same renumbered value `rowIndex` does.
266
+ rowIndex: -1,
267
+ datasetIndex: -1,
268
+ // Set by `withGroupAggregates`, downstream of grouping — empty here
269
+ // since `groupRows` itself never knows about `AggregateState`.
270
+ aggregates: /* @__PURE__ */ new Map()
271
+ };
272
+ out.push(header);
273
+ if (expanded) {
274
+ buildLevel(group.rows, rest, path, expansion, byId, out);
275
+ }
276
+ }
277
+ }
278
+ function groupRows(rows, groupBy, expansion, columns) {
279
+ if (groupBy.length === 0) {
280
+ return rows;
281
+ }
282
+ const byId = new Map(columns.map((entry) => [entry.id, entry.column]));
283
+ const active = groupBy.filter((entry) => byId.has(entry.columnId));
284
+ if (active.length === 0) {
285
+ return rows;
286
+ }
287
+ const out = [];
288
+ buildLevel(rows, active, [], expansion, byId, out);
289
+ return out.map((entry, index) => {
290
+ if ("kind" in entry) {
291
+ return entry.rowIndex === index && entry.datasetIndex === index ? entry : { ...entry, rowIndex: index, datasetIndex: index };
292
+ }
293
+ return entry.rowIndex === index ? entry : { ...entry, rowIndex: index };
294
+ });
295
+ }
296
+ function toggleGroupExpansion(expansion, groupId) {
297
+ return expansion.includes(groupId) ? expansion.filter((id) => id !== groupId) : [...expansion, groupId];
298
+ }
299
+ function expandAllGroups(expansion) {
300
+ return expansion.length === 0 ? expansion : [];
301
+ }
302
+ function collapseAllGroups(rows) {
303
+ return rows.filter(
304
+ (entry) => "kind" in entry && entry.kind === "group"
305
+ ).map((entry) => entry.groupId);
306
+ }
307
+ function moveGroupByBefore(groupBy, columnId, beforeColumnId) {
308
+ if (columnId === beforeColumnId) {
309
+ return groupBy;
310
+ }
311
+ const without = groupBy.filter((entry) => entry.columnId !== columnId);
312
+ const moved = groupBy.find((entry) => entry.columnId === columnId) ?? {
313
+ columnId
314
+ };
315
+ const target = beforeColumnId === null ? without.length : without.findIndex((entry) => entry.columnId === beforeColumnId);
316
+ if (target === -1) {
317
+ return groupBy;
318
+ }
319
+ const next = [...without];
320
+ next.splice(target, 0, moved);
321
+ return next.length === groupBy.length && next.every((entry, index) => entry === groupBy[index]) ? groupBy : next;
322
+ }
323
+ function movesGroupBy(groupBy, columnId, beforeColumnId) {
324
+ return moveGroupByBefore(groupBy, columnId, beforeColumnId) !== groupBy;
325
+ }
326
+
327
+ // src/util/memoize.ts
328
+ function memoizeLast(compute) {
329
+ let last;
330
+ return (...args) => {
331
+ const previous = last;
332
+ if (previous !== void 0 && args.every((arg, index) => Object.is(arg, previous.args[index]))) {
333
+ return previous.result;
334
+ }
335
+ const result = compute(...args);
336
+ last = { args, result };
337
+ return result;
338
+ };
339
+ }
340
+
341
+ // src/util/ordering.ts
342
+ function applyColumnOrder(columns, order) {
343
+ if (order.length === 0) {
344
+ return columns;
345
+ }
346
+ const byId = new Map(columns.map((column) => [getColumnId(column), column]));
347
+ const listed = order.map((id) => byId.get(id)).filter((column) => column !== void 0);
348
+ const taken = new Set(listed.map((column) => getColumnId(column)));
349
+ return [
350
+ ...listed,
351
+ ...columns.filter((column) => !taken.has(getColumnId(column)))
352
+ ];
353
+ }
354
+ function resolveDataColumns(columns, order) {
355
+ return applyColumnOrder(columns, order).map((column) => ({
356
+ id: getColumnId(column),
357
+ column
358
+ }));
359
+ }
360
+ function moveColumnBefore(orderedIds, movedId, beforeId) {
361
+ if (movedId === beforeId || !orderedIds.includes(movedId)) {
362
+ return orderedIds;
363
+ }
364
+ const without = orderedIds.filter((id) => id !== movedId);
365
+ const target = beforeId === null ? without.length : without.indexOf(beforeId);
366
+ if (target === -1) {
367
+ return orderedIds;
368
+ }
369
+ const next = [...without];
370
+ next.splice(target, 0, movedId);
371
+ return next.every((id, index) => id === orderedIds[index]) ? orderedIds : next;
372
+ }
373
+ function movesColumn(orderedIds, movedId, beforeId) {
374
+ return moveColumnBefore(orderedIds, movedId, beforeId) !== orderedIds;
375
+ }
376
+ function resolveDropBefore(orderedIds, targetId, side) {
377
+ if (side === "before") {
378
+ return targetId;
379
+ }
380
+ const index = orderedIds.indexOf(targetId);
381
+ return index === -1 ? null : orderedIds[index + 1] ?? null;
382
+ }
383
+ function resolveKeyboardDropTarget(orderedIds, columnId, direction) {
384
+ const index = orderedIds.indexOf(columnId);
385
+ if (index === -1) {
386
+ return void 0;
387
+ }
388
+ const targetIndex = direction === -1 ? index - 1 : index + 2;
389
+ if (targetIndex < 0) {
390
+ return void 0;
391
+ }
392
+ return orderedIds[targetIndex] ?? null;
393
+ }
394
+
395
+ // src/util/paging.ts
396
+ function unitsOf(rows) {
397
+ const units = [];
398
+ let start = 0;
399
+ let openedByGroup = false;
400
+ for (let index = 0; index < rows.length; index++) {
401
+ const entry = rows[index];
402
+ if (entry === void 0) {
403
+ continue;
404
+ }
405
+ const isTopLevelHeader = "kind" in entry && entry.kind === "group" && entry.level === 0;
406
+ const isBoundary = index !== start && (isTopLevelHeader || !openedByGroup);
407
+ if (isBoundary) {
408
+ units.push({ start, end: index });
409
+ start = index;
410
+ openedByGroup = isTopLevelHeader;
411
+ } else if (index === start) {
412
+ openedByGroup = isTopLevelHeader;
413
+ }
414
+ }
415
+ if (start < rows.length) {
416
+ units.push({ start, end: rows.length });
417
+ }
418
+ return units;
419
+ }
420
+ function paginateRows(rows, pagination) {
421
+ const pageSize = Math.max(1, pagination.pageSize);
422
+ const units = unitsOf(rows);
423
+ const pageCount = Math.ceil(units.length / pageSize);
424
+ if (pageCount === 0) {
425
+ return { rows: [], pageCount: 0, pageIndex: 0 };
426
+ }
427
+ const pageIndex = Math.min(Math.max(pagination.pageIndex, 0), pageCount - 1);
428
+ const firstUnit = units[pageIndex * pageSize];
429
+ const lastUnit = units[Math.min(pageIndex * pageSize + pageSize, units.length) - 1];
430
+ if (firstUnit === void 0 || lastUnit === void 0) {
431
+ return { rows: [], pageCount, pageIndex };
432
+ }
433
+ const page = rows.slice(firstUnit.start, lastUnit.end);
434
+ const renumbered = page.map(
435
+ (entry, index) => entry.rowIndex === index ? entry : { ...entry, rowIndex: index }
436
+ );
437
+ return { rows: renumbered, pageCount, pageIndex };
438
+ }
439
+
440
+ // src/util/report.ts
441
+ function reportError(message) {
442
+ console.error(`GridKit: ${message}`);
443
+ }
444
+
445
+ // src/util/filterOperators.ts
446
+ var matchNothing = () => false;
447
+ function kindOf(type) {
448
+ switch (type) {
449
+ case "number":
450
+ case "decimal":
451
+ case "currency":
452
+ case "percent":
453
+ return "number";
454
+ case "boolean":
455
+ return "boolean";
456
+ case "date":
457
+ case "dateTime":
458
+ case "time":
459
+ return "date";
460
+ default:
461
+ return "text";
462
+ }
463
+ }
464
+ function isDateType(type) {
465
+ return kindOf(type) === "date";
466
+ }
467
+ function isEmptyCell(value) {
468
+ return value === null || value === void 0 || value === "";
469
+ }
470
+ function toDate(value) {
471
+ const date = value instanceof Date ? value : typeof value === "string" ? new Date(value) : void 0;
472
+ return date === void 0 || Number.isNaN(date.getTime()) ? void 0 : date;
473
+ }
474
+ function comparable(value, kind) {
475
+ switch (kind) {
476
+ case "number":
477
+ return typeof value === "number" && !Number.isNaN(value) ? value : void 0;
478
+ case "boolean":
479
+ return typeof value === "boolean" ? value : void 0;
480
+ case "date":
481
+ return toDate(value);
482
+ case "text":
483
+ return String(value).toLowerCase();
484
+ }
485
+ }
486
+ function isOperandOfKind(operand, kind) {
487
+ if (kind !== "text") {
488
+ return comparable(operand, kind) !== void 0;
489
+ }
490
+ return typeof operand === "string" || typeof operand === "number" || typeof operand === "boolean";
491
+ }
492
+ function compileComparison(operand, type) {
493
+ const kind = kindOf(type);
494
+ if (!isOperandOfKind(operand, kind)) {
495
+ return void 0;
496
+ }
497
+ const target = comparable(operand, kind);
498
+ return (value) => {
499
+ if (isEmptyCell(value)) {
500
+ return void 0;
501
+ }
502
+ const cell = comparable(value, kind);
503
+ return cell === void 0 ? void 0 : compareValues(cell, target, type);
504
+ };
505
+ }
506
+ function isScalarList(operand) {
507
+ return Array.isArray(operand);
508
+ }
509
+ function compileOrdering(operator, operand, type) {
510
+ const compare = compileComparison(operand, type);
511
+ if (compare === void 0) {
512
+ return matchNothing;
513
+ }
514
+ const where = (holds) => (value) => {
515
+ const result = compare(value);
516
+ return result !== void 0 && holds(result);
517
+ };
518
+ switch (operator) {
519
+ case "eq":
520
+ return where((result) => result === 0);
521
+ case "ne": {
522
+ const equals = where((result) => result === 0);
523
+ return (value) => !equals(value);
524
+ }
525
+ case "gt":
526
+ return where((result) => result > 0);
527
+ case "gte":
528
+ return where((result) => result >= 0);
529
+ case "lt":
530
+ return where((result) => result < 0);
531
+ case "lte":
532
+ return where((result) => result <= 0);
533
+ }
534
+ }
535
+ function compileBetween(operand, type) {
536
+ if (!isScalarList(operand) || operand.length !== 2) {
537
+ return matchNothing;
538
+ }
539
+ const [low, high] = operand;
540
+ const fromLow = compileComparison(low, type);
541
+ const fromHigh = compileComparison(high, type);
542
+ if (fromLow === void 0 || fromHigh === void 0) {
543
+ return matchNothing;
544
+ }
545
+ return (value) => {
546
+ const aboveLow = fromLow(value);
547
+ const belowHigh = fromHigh(value);
548
+ return aboveLow !== void 0 && belowHigh !== void 0 && aboveLow >= 0 && belowHigh <= 0;
549
+ };
550
+ }
551
+ function filterValueKey(value, type) {
552
+ return bucketKey(isDateType(type) ? toDate(value) ?? value : value);
553
+ }
554
+ function compileIn(operand, type) {
555
+ if (!isScalarList(operand)) {
556
+ return matchNothing;
557
+ }
558
+ const keys = new Set(
559
+ operand.filter((scalar) => !isEmptyCell(scalar)).map((scalar) => filterValueKey(scalar, type))
560
+ );
561
+ return (value) => !isEmptyCell(value) && keys.has(filterValueKey(value, type));
562
+ }
563
+ function compileText(operator, operand) {
564
+ if (!isOperandOfKind(operand, "text")) {
565
+ return matchNothing;
566
+ }
567
+ const text = String(operand).toLowerCase();
568
+ const read = (value) => String(value).toLowerCase();
569
+ switch (operator) {
570
+ case "contains":
571
+ return (value) => !isEmptyCell(value) && read(value).includes(text);
572
+ case "notContains":
573
+ return (value) => isEmptyCell(value) || !read(value).includes(text);
574
+ case "startsWith":
575
+ return (value) => !isEmptyCell(value) && read(value).startsWith(text);
576
+ case "endsWith":
577
+ return (value) => !isEmptyCell(value) && read(value).endsWith(text);
578
+ }
579
+ }
580
+ function compileOperatorEntry(entry, type) {
581
+ const { operator, value } = entry;
582
+ switch (operator) {
583
+ case "isEmpty":
584
+ return isEmptyCell;
585
+ case "isNotEmpty":
586
+ return (cell) => !isEmptyCell(cell);
587
+ case "contains":
588
+ case "notContains":
589
+ case "startsWith":
590
+ case "endsWith":
591
+ return compileText(operator, value);
592
+ case "between":
593
+ return compileBetween(value, type);
594
+ case "in":
595
+ return compileIn(value, type);
596
+ case "eq":
597
+ case "ne":
598
+ case "gt":
599
+ case "gte":
600
+ case "lt":
601
+ case "lte":
602
+ return compileOrdering(operator, value, type);
603
+ }
604
+ }
605
+
606
+ // src/util/serialize.ts
607
+ function serializeScalar(scalar) {
608
+ return scalar instanceof Date ? scalar.toISOString() : scalar;
609
+ }
610
+ function serializeOperand(operand) {
611
+ return isScalarList(operand) ? operand.map(serializeScalar) : serializeScalar(operand);
612
+ }
613
+ function serializeEntry(entry) {
614
+ if ("combinator" in entry) {
615
+ const entries = serializeFilter(entry.entries);
616
+ return entries.length === 0 && entry.entries.length > 0 ? void 0 : { combinator: entry.combinator, entries };
617
+ }
618
+ if ("predicate" in entry) {
619
+ reportError(
620
+ entry.columnId === void 0 ? "a predicate filter entry across every column cannot be serialised, so it is left out of the saved state." : `a predicate filter entry on \`${entry.columnId}\` cannot be serialised, so it is left out of the saved state.`
621
+ );
622
+ return void 0;
623
+ }
624
+ if ("operator" in entry) {
625
+ const { value, ...rest } = entry;
626
+ return value === void 0 ? rest : { ...rest, value: serializeOperand(value) };
627
+ }
628
+ if ("value" in entry) {
629
+ const { value, ...rest } = entry;
630
+ return {
631
+ ...rest,
632
+ value: value instanceof Date ? value.toISOString() : value
633
+ };
634
+ }
635
+ return entry;
636
+ }
637
+ function serializeFilter(filter) {
638
+ return filter.map((entry) => serializeEntry(entry)).filter((entry) => entry !== void 0);
639
+ }
640
+ function serializeGridState(state) {
641
+ return { ...state, filter: serializeFilter(state.filter) };
642
+ }
643
+ function reviveScalar(scalar, type) {
644
+ return isDateType(type) && typeof scalar === "string" ? toDate(scalar) ?? scalar : scalar;
645
+ }
646
+ function reviveOperand(operand, type) {
647
+ return isScalarList(operand) ? operand.map((scalar) => reviveScalar(scalar, type)) : reviveScalar(operand, type);
648
+ }
649
+ function reviveEntry(entry, typeOf) {
650
+ if ("combinator" in entry) {
651
+ return {
652
+ combinator: entry.combinator,
653
+ entries: reviveEntries(entry.entries, typeOf)
654
+ };
655
+ }
656
+ if ("operator" in entry) {
657
+ const { value, ...rest } = entry;
658
+ return value === void 0 ? rest : { ...rest, value: reviveOperand(value, typeOf(entry.columnId)) };
659
+ }
660
+ if ("value" in entry) {
661
+ const { value, ...rest } = entry;
662
+ return {
663
+ ...rest,
664
+ value: typeof value === "string" ? new Date(value) : value
665
+ };
666
+ }
667
+ return entry;
668
+ }
669
+ function reviveEntries(filter, typeOf) {
670
+ return filter.map((entry) => reviveEntry(entry, typeOf));
671
+ }
672
+ function reviveFilter(filter, columns) {
673
+ const types = new Map(columns.map((entry) => [entry.id, entry.column.type]));
674
+ return reviveEntries(filter, (columnId) => types.get(columnId));
675
+ }
676
+
677
+ // src/util/sizing.ts
678
+ var DEFAULT_COLUMN_SIZES = {
679
+ width: 150,
680
+ minWidth: 40,
681
+ maxWidth: Number.POSITIVE_INFINITY
682
+ };
683
+ var KEYBOARD_STEP = 10;
684
+ function withDefaults(defaults) {
685
+ return { ...DEFAULT_COLUMN_SIZES, ...defaults };
686
+ }
687
+ function resolveColumnConstraints(column, defaults) {
688
+ const resolved = withDefaults(defaults);
689
+ return {
690
+ minWidth: column.minWidth ?? resolved.minWidth,
691
+ maxWidth: column.maxWidth ?? resolved.maxWidth
692
+ };
693
+ }
694
+ function clampColumnWidth(width, constraints) {
695
+ return Math.min(Math.max(width, constraints.minWidth), constraints.maxWidth);
696
+ }
697
+ function resolveColumnWidths(columns, sizing, options) {
698
+ const resolved = withDefaults(options?.sizes);
699
+ return columns.map((column) => {
700
+ const id = getColumnId(column);
701
+ const stored = sizing[id];
702
+ return {
703
+ column,
704
+ id,
705
+ sized: stored !== void 0,
706
+ width: clampColumnWidth(
707
+ stored ?? column.width ?? resolved.width,
708
+ resolveColumnConstraints(column, resolved)
709
+ ),
710
+ label: resolveColumnLabel(column),
711
+ resizable: column.resizable ?? options?.resizable ?? false,
712
+ reorderable: column.reorderable ?? options?.reorderable ?? false,
713
+ groupByDraggable: column.groupByDraggable ?? options?.groupByDraggable ?? false,
714
+ alignment: column.alignment ?? alignmentForType(column.type ?? "string")
715
+ };
716
+ });
717
+ }
718
+ function totalColumnWidth(resolved) {
719
+ return resolved.reduce((total, entry) => total + entry.width, 0);
720
+ }
721
+ function beginColumnResize(column, startWidth, startPosition, defaults) {
722
+ return {
723
+ columnId: getColumnId(column),
724
+ startWidth,
725
+ startPosition,
726
+ constraints: resolveColumnConstraints(column, defaults)
727
+ };
728
+ }
729
+ function applyColumnResize(session, position) {
730
+ return clampColumnWidth(
731
+ session.startWidth + (position - session.startPosition),
732
+ session.constraints
733
+ );
734
+ }
735
+ function reachedBound(width, item) {
736
+ return item.bound > item.width ? width >= item.bound : width <= item.bound;
737
+ }
738
+ function redistribute(entries, remaining) {
739
+ for (; ; ) {
740
+ const movable = entries.filter(
741
+ (item) => item.adjustable && !reachedBound(item.width, item)
742
+ );
743
+ const left = remaining(entries.reduce((t, i) => t + i.width, 0));
744
+ if (left <= 0 || movable.length === 0) {
745
+ break;
746
+ }
747
+ const base = movable.reduce((total, item) => total + item.width, 0);
748
+ let clamped = false;
749
+ for (const item of movable) {
750
+ const share = base === 0 ? left / movable.length : left * item.width / base;
751
+ const next = item.width + (item.bound > item.width ? share : -share);
752
+ if (reachedBound(next, item)) {
753
+ item.width = item.bound;
754
+ clamped = true;
755
+ } else {
756
+ item.width = next;
757
+ }
758
+ }
759
+ if (!clamped) {
760
+ break;
761
+ }
762
+ }
763
+ }
764
+ function fitColumnsToWidth(resolved, availableWidth, defaults) {
765
+ const target = Math.floor(availableWidth);
766
+ const total = totalColumnWidth(resolved);
767
+ if (resolved.length === 0 || total === target) {
768
+ return resolved;
769
+ }
770
+ const growing = total < target;
771
+ const entries = resolved.map((entry) => {
772
+ const constraints = resolveColumnConstraints(entry.column, defaults);
773
+ return {
774
+ entry,
775
+ width: entry.width,
776
+ bound: growing ? constraints.maxWidth : constraints.minWidth,
777
+ adjustable: !entry.sized
778
+ };
779
+ });
780
+ if (!growing) {
781
+ const floor = entries.reduce(
782
+ (sum, item) => sum + (item.adjustable ? item.bound : item.width),
783
+ 0
784
+ );
785
+ if (floor > target) {
786
+ return resolved;
787
+ }
788
+ }
789
+ redistribute(entries, (sum) => growing ? target - sum : sum - target);
790
+ const rounded = entries.map(
791
+ (item) => growing ? Math.floor(item.width) : Math.ceil(item.width)
792
+ );
793
+ let remainder = target - rounded.reduce((total2, width) => total2 + width, 0);
794
+ return entries.map((item, index) => {
795
+ let width = rounded[index] ?? item.width;
796
+ const hasRoom = growing ? width < item.bound : width > item.bound;
797
+ if (remainder !== 0 && item.adjustable && hasRoom) {
798
+ width += growing ? 1 : -1;
799
+ remainder += growing ? -1 : 1;
800
+ }
801
+ return { ...item.entry, width };
802
+ });
803
+ }
804
+ function sizeColumnToContent(measuredWidth, constraints, padding = 2) {
805
+ return clampColumnWidth(Math.ceil(measuredWidth + padding), constraints);
806
+ }
807
+ function revertColumnSize(current, base, columnId) {
808
+ const next = Object.fromEntries(
809
+ Object.entries(current).filter(([id]) => id !== columnId)
810
+ );
811
+ if (columnId in base) {
812
+ next[columnId] = base[columnId];
813
+ }
814
+ return next;
815
+ }
816
+
817
+ // src/util/aggregation.ts
818
+ function isEmpty2(value) {
819
+ return value === null || value === void 0;
820
+ }
821
+ function toNumber(value) {
822
+ return Number(value);
823
+ }
824
+ function distinctKey(value) {
825
+ if (isEmpty2(value)) {
826
+ return "\0empty";
827
+ }
828
+ if (value instanceof Date) {
829
+ return `\0date:${String(value.getTime())}`;
830
+ }
831
+ switch (typeof value) {
832
+ case "string":
833
+ case "number":
834
+ case "boolean":
835
+ case "bigint":
836
+ case "symbol":
837
+ return `\0${typeof value}:${String(value)}`;
838
+ case "function":
839
+ return "\0function";
840
+ default:
841
+ return `\0object:${JSON.stringify(value)}`;
842
+ }
843
+ }
844
+ function reduceBuiltIn(rows, fn, column) {
845
+ if (fn === "count") {
846
+ return rows.length;
847
+ }
848
+ const field = column?.column.field;
849
+ const values = field === void 0 ? [] : rows.map((row) => accessDotted(row, field));
850
+ switch (fn) {
851
+ case "sum": {
852
+ let total = 0;
853
+ for (const value of values) {
854
+ if (isEmpty2(value)) continue;
855
+ total += toNumber(value);
856
+ }
857
+ return total;
858
+ }
859
+ case "avg": {
860
+ const present = values.filter((value) => !isEmpty2(value));
861
+ if (present.length === 0) return void 0;
862
+ const total = present.reduce(
863
+ (acc, value) => acc + toNumber(value),
864
+ 0
865
+ );
866
+ return total / present.length;
867
+ }
868
+ case "min":
869
+ case "max": {
870
+ const type = column?.column.type;
871
+ let result;
872
+ for (const value of values) {
873
+ if (isEmpty2(value)) continue;
874
+ if (result === void 0) {
875
+ result = value;
876
+ continue;
877
+ }
878
+ const comparison = compareValues(value, result, type);
879
+ if (fn === "min" ? comparison < 0 : comparison > 0) {
880
+ result = value;
881
+ }
882
+ }
883
+ return result;
884
+ }
885
+ case "countDistinct": {
886
+ const seen = /* @__PURE__ */ new Set();
887
+ for (const value of values) {
888
+ if (isEmpty2(value)) continue;
889
+ seen.add(distinctKey(value));
890
+ }
891
+ return seen.size;
892
+ }
893
+ }
894
+ }
895
+ function keyOf(spec) {
896
+ return spec.id ?? spec.columnId;
897
+ }
898
+ function computeAggregates(rows, specs, columns) {
899
+ const results = /* @__PURE__ */ new Map();
900
+ if (specs.length === 0) {
901
+ return results;
902
+ }
903
+ const byId = new Map(columns.map((entry) => [entry.id, entry]));
904
+ for (const spec of specs) {
905
+ const value = typeof spec.fn === "function" ? spec.fn(rows) : reduceBuiltIn(rows, spec.fn, byId.get(spec.columnId));
906
+ results.set(keyOf(spec), value);
907
+ }
908
+ return results;
909
+ }
910
+ function leafDescendantsOf(flatRows, path, groupBy, byId) {
911
+ const keys = path.map((value) => bucketKey(value));
912
+ return flatRows.filter(
913
+ (row) => keys.every((key, level) => {
914
+ const columnId = groupBy[level]?.columnId;
915
+ const column = columnId === void 0 ? void 0 : byId.get(columnId);
916
+ if (column === void 0) return false;
917
+ return bucketKey(accessDotted(row.row, column.field)) === key;
918
+ })
919
+ ).map((row) => row.row);
920
+ }
921
+ function appendAggregatedSpan(displayRows, start, levelFloor, flatRows, groupBy, byId, specs, columns, display, out) {
922
+ let index = start;
923
+ while (index < displayRows.length) {
924
+ const entry = displayRows[index];
925
+ if (entry === void 0) {
926
+ index++;
927
+ continue;
928
+ }
929
+ if (levelFloor !== null && "kind" in entry && entry.kind === "group" && entry.level <= levelFloor) {
930
+ break;
931
+ }
932
+ index++;
933
+ if (!("kind" in entry) || entry.kind !== "group") {
934
+ out.push(entry);
935
+ continue;
936
+ }
937
+ const leaves = leafDescendantsOf(flatRows, entry.path, groupBy, byId);
938
+ const aggregates = computeAggregates(leaves, specs, columns);
939
+ const withAggregates = { ...entry, aggregates };
940
+ out.push(withAggregates);
941
+ index = appendAggregatedSpan(
942
+ displayRows,
943
+ index,
944
+ entry.level,
945
+ flatRows,
946
+ groupBy,
947
+ byId,
948
+ specs,
949
+ columns,
950
+ display,
951
+ out
952
+ );
953
+ if (display === "row") {
954
+ const summary = {
955
+ kind: "group-summary",
956
+ groupId: entry.groupId,
957
+ level: entry.level,
958
+ rowIndex: -1,
959
+ datasetIndex: -1,
960
+ aggregates
961
+ };
962
+ out.push(summary);
963
+ }
964
+ }
965
+ return index;
966
+ }
967
+ function withGroupAggregates(displayRows, flatRows, groupBy, specs, columns, display = "inline") {
968
+ if (specs.length === 0) {
969
+ return displayRows;
970
+ }
971
+ const byId = new Map(columns.map((entry) => [entry.id, entry.column]));
972
+ const out = [];
973
+ appendAggregatedSpan(
974
+ displayRows,
975
+ 0,
976
+ null,
977
+ flatRows,
978
+ groupBy,
979
+ byId,
980
+ specs,
981
+ columns,
982
+ display,
983
+ out
984
+ );
985
+ return out.map((entry, position) => {
986
+ if ("kind" in entry) {
987
+ return entry.rowIndex === position && entry.datasetIndex === position ? entry : { ...entry, rowIndex: position, datasetIndex: position };
988
+ }
989
+ return entry.rowIndex === position ? entry : { ...entry, rowIndex: position };
990
+ });
991
+ }
992
+
993
+ // src/util/filtering.ts
994
+ function parseFilterPattern(query) {
995
+ const start = query.startsWith("%");
996
+ const end = query.endsWith("%");
997
+ let text = query;
998
+ if (start) {
999
+ text = text.slice(1);
1000
+ }
1001
+ if (end) {
1002
+ text = text.slice(0, -1);
1003
+ }
1004
+ text = text.toLowerCase();
1005
+ if (start && end) {
1006
+ return { mode: "contains", text };
1007
+ }
1008
+ if (start) {
1009
+ return { mode: "endsWith", text };
1010
+ }
1011
+ if (end) {
1012
+ return { mode: "startsWith", text };
1013
+ }
1014
+ return { mode: "exact", text };
1015
+ }
1016
+ function isEmptyValue(value) {
1017
+ return value === null || value === void 0;
1018
+ }
1019
+ function compileFilterQuery(query) {
1020
+ const { mode, text } = parseFilterPattern(query);
1021
+ return (value) => {
1022
+ const candidate = (isEmptyValue(value) ? "" : String(value)).toLowerCase();
1023
+ switch (mode) {
1024
+ case "exact":
1025
+ return candidate === text;
1026
+ case "contains":
1027
+ return candidate.includes(text);
1028
+ case "startsWith":
1029
+ return candidate.startsWith(text);
1030
+ case "endsWith":
1031
+ return candidate.endsWith(text);
1032
+ }
1033
+ };
1034
+ }
1035
+ function matchesTypedValue(value, target, type) {
1036
+ if (typeof target === "number") {
1037
+ if (type !== "number" && type !== "decimal" && type !== "currency" && type !== "percent") {
1038
+ return false;
1039
+ }
1040
+ return typeof value === "number" && value === target;
1041
+ }
1042
+ if (typeof target === "boolean") {
1043
+ if (type !== "boolean") {
1044
+ return false;
1045
+ }
1046
+ return typeof value === "boolean" && value === target;
1047
+ }
1048
+ if (type !== "date" && type !== "dateTime" && type !== "time") {
1049
+ return false;
1050
+ }
1051
+ return value instanceof Date && value.getTime() === target.getTime();
1052
+ }
1053
+ function isEntryUsable(entry, byId) {
1054
+ if ("combinator" in entry) {
1055
+ return true;
1056
+ }
1057
+ return entry.columnId === void 0 || byId.has(entry.columnId);
1058
+ }
1059
+ function compileFilterEntry(entry, byId, columns) {
1060
+ if ("combinator" in entry) {
1061
+ const tests = entry.entries.filter((nested) => isEntryUsable(nested, byId)).map((nested) => compileFilterEntry(nested, byId, columns));
1062
+ return entry.combinator === "and" ? (row) => tests.every((test2) => test2(row)) : (row) => tests.some((test2) => test2(row));
1063
+ }
1064
+ if ("predicate" in entry) {
1065
+ const { columnId: columnId2, predicate } = entry;
1066
+ const column = columnId2 === void 0 ? void 0 : byId.get(columnId2);
1067
+ return (row) => predicate(
1068
+ column === void 0 ? void 0 : accessDotted(row, column.field),
1069
+ row
1070
+ );
267
1071
  }
268
- const next = orderedIds.slice(
269
- Math.min(anchor, focus),
270
- Math.max(anchor, focus) + 1
271
- );
272
- return sameSelection(selection, next) ? selection : next;
273
- }
274
- function selectAll(selection, orderedIds, mode) {
275
- if (mode !== "multiple") {
276
- return selection;
1072
+ if ("operator" in entry) {
1073
+ const column = byId.get(entry.columnId);
1074
+ if (column === void 0) {
1075
+ return () => false;
1076
+ }
1077
+ const test2 = compileOperatorEntry(entry, column.type);
1078
+ return (row) => test2(accessDotted(row, column.field));
277
1079
  }
278
- return sameSelection(selection, orderedIds) ? selection : orderedIds;
1080
+ if ("value" in entry) {
1081
+ const { columnId: columnId2, value } = entry;
1082
+ if (columnId2 !== void 0) {
1083
+ const column = byId.get(columnId2);
1084
+ if (column === void 0) {
1085
+ return () => false;
1086
+ }
1087
+ return (row) => matchesTypedValue(accessDotted(row, column.field), value, column.type);
1088
+ }
1089
+ return (row) => columns.some(
1090
+ (entry2) => matchesTypedValue(
1091
+ accessDotted(row, entry2.column.field),
1092
+ value,
1093
+ entry2.column.type
1094
+ )
1095
+ );
1096
+ }
1097
+ const { columnId, query } = entry;
1098
+ const test = compileFilterQuery(query);
1099
+ if (columnId !== void 0) {
1100
+ const column = byId.get(columnId);
1101
+ if (column === void 0) {
1102
+ return () => false;
1103
+ }
1104
+ return (row) => test(accessDotted(row, column.field));
1105
+ }
1106
+ return (row) => columns.some((entry2) => test(accessDotted(row, entry2.column.field)));
279
1107
  }
280
- function clearSelection(selection) {
281
- return selection.length === 0 ? selection : [];
1108
+ function compileFilter(filter, columns) {
1109
+ const byId = new Map(columns.map((entry) => [entry.id, entry.column]));
1110
+ const compiled = filter.filter((entry) => isEntryUsable(entry, byId)).map((entry) => compileFilterEntry(entry, byId, columns));
1111
+ return compiled.length === 0 ? void 0 : (row) => compiled.every((test) => test(row));
282
1112
  }
283
- function diffSelection(previous, next) {
284
- if (previous === next) {
285
- return NO_CHANGE;
286
- }
287
- const before = new Set(previous);
288
- const after = new Set(next);
289
- return {
290
- added: next.filter((id) => !before.has(id)),
291
- removed: previous.filter((id) => !after.has(id))
292
- };
1113
+ function matchesQuery(value, query) {
1114
+ return compileFilterQuery(query)(value);
293
1115
  }
294
- function isSameCell(a, b) {
295
- if (a === null || b === null) {
296
- return a === b;
297
- }
298
- return a.rowId === b.rowId && a.columnId === b.columnId;
1116
+ function filterQueryFor(filter, columnId) {
1117
+ const entry = filter.find(
1118
+ (candidate) => "query" in candidate && candidate.columnId === columnId
1119
+ );
1120
+ return entry?.query ?? null;
299
1121
  }
300
- function selectCell(selection, cell, mode) {
301
- if (mode === false || isSameCell(selection, cell)) {
302
- return selection;
1122
+ function setColumnFilter(filter, columnId, query) {
1123
+ const index = filter.findIndex(
1124
+ (entry) => !("combinator" in entry) && entry.columnId === columnId
1125
+ );
1126
+ if (query === "") {
1127
+ return index === -1 ? filter : filter.filter((_entry, i) => i !== index);
303
1128
  }
304
- return cell;
305
- }
306
- function toggleCellSelection(selection, cell, mode) {
307
- if (mode === false) {
308
- return selection;
1129
+ const next = columnId === void 0 ? { query } : { columnId, query };
1130
+ if (index === -1) {
1131
+ return [...filter, next];
309
1132
  }
310
- return isSameCell(selection, cell) ? null : cell;
311
- }
312
- function intentOf(event) {
313
- if (event.shiftKey) {
314
- return "range";
1133
+ const existing = filter[index];
1134
+ if (existing !== void 0 && "query" in existing && existing.query === query) {
1135
+ return filter;
315
1136
  }
316
- return event.ctrlKey || event.metaKey ? "toggle" : "replace";
1137
+ return filter.map((entry, i) => i === index ? next : entry);
317
1138
  }
318
- function applySelectionIntent(selection, orderedIds, anchorId, id, intent, mode) {
319
- switch (intent) {
320
- case "toggle":
321
- return toggleSelection(selection, id, mode);
322
- // A range with no anchor yet spans from the id to itself, which is the
323
- // plain click a first Shift-click may as well be.
324
- case "range":
325
- return selectRange(selection, orderedIds, anchorId ?? id, id, mode);
326
- default:
327
- return selectOnly(selection, id, mode);
1139
+ function clearAllFilters(filter) {
1140
+ return filter.length === 0 ? filter : [];
1141
+ }
1142
+ function filterRows(rows, filter, columns) {
1143
+ if (filter.length === 0) {
1144
+ return rows;
1145
+ }
1146
+ const test = compileFilter(filter, columns);
1147
+ if (test === void 0) {
1148
+ return rows;
328
1149
  }
1150
+ const filtered = rows.filter((row) => test(row.row));
1151
+ return filtered.map(
1152
+ (row, index) => row.rowIndex === index ? row : { ...row, rowIndex: index }
1153
+ );
329
1154
  }
330
1155
 
331
- // src/util/sizing.ts
332
- var DEFAULT_COLUMN_SIZES = {
333
- width: 150,
334
- minWidth: 40,
335
- maxWidth: Number.POSITIVE_INFINITY
336
- };
337
- var KEYBOARD_STEP = 10;
338
- function withDefaults(defaults) {
339
- return { ...DEFAULT_COLUMN_SIZES, ...defaults };
1156
+ // src/util/rows.ts
1157
+ function resolveShownRows(rows, filter, sort, columns) {
1158
+ return sortRows(filterRows(rows, filter, columns), sort, columns);
340
1159
  }
341
- function resolveColumnConstraints(column, defaults) {
342
- const resolved = withDefaults(defaults);
1160
+ function resolveRows(rowsById, ids) {
1161
+ return ids.map((id) => rowsById.get(id)).filter((row) => row !== void 0);
1162
+ }
1163
+ function resolveColumns(columns, ids) {
1164
+ return ids.map((id) => {
1165
+ const columnIndex = columns.findIndex((entry) => entry.id === id);
1166
+ const column = columns[columnIndex];
1167
+ return column === void 0 ? void 0 : { columnId: id, column, columnIndex };
1168
+ }).filter((column) => column !== void 0);
1169
+ }
1170
+ function resolveCell(rowsById, columns, cell) {
1171
+ if (cell === null) {
1172
+ return null;
1173
+ }
1174
+ const entry = rowsById.get(cell.rowId);
1175
+ const columnIndex = columns.findIndex(
1176
+ (candidate) => candidate.id === cell.columnId
1177
+ );
1178
+ const column = columns[columnIndex];
1179
+ if (entry === void 0 || column === void 0) {
1180
+ return null;
1181
+ }
343
1182
  return {
344
- minWidth: column.minWidth ?? resolved.minWidth,
345
- maxWidth: column.maxWidth ?? resolved.maxWidth
1183
+ rowId: cell.rowId,
1184
+ columnId: cell.columnId,
1185
+ row: entry.row,
1186
+ column,
1187
+ rowIndex: entry.rowIndex,
1188
+ columnIndex,
1189
+ value: accessDotted(entry.row, column.column.field)
346
1190
  };
347
1191
  }
348
- function clampColumnWidth(width, constraints) {
349
- return Math.min(Math.max(width, constraints.minWidth), constraints.maxWidth);
350
- }
351
- function resolveColumnWidths(columns, sizing, options) {
352
- const resolved = withDefaults(options?.sizes);
353
- return columns.map((column) => {
354
- const id = getColumnId(column);
355
- const stored = sizing[id];
356
- return {
357
- column,
358
- id,
359
- sized: stored !== void 0,
360
- width: clampColumnWidth(
361
- stored ?? column.width ?? resolved.width,
362
- resolveColumnConstraints(column, resolved)
363
- ),
364
- label: resolveColumnLabel(column),
365
- resizable: column.resizable ?? options?.resizable ?? false,
366
- reorderable: column.reorderable ?? options?.reorderable ?? false,
367
- groupByDraggable: column.groupByDraggable ?? options?.groupByDraggable ?? false,
368
- alignment: column.alignment ?? alignmentForType(column.type ?? "string")
369
- };
370
- });
1192
+
1193
+ // src/util/visibility.ts
1194
+ function isColumnVisible(column, visibility) {
1195
+ return visibility[getColumnId(column)] ?? column.hidden !== true;
371
1196
  }
372
- function totalColumnWidth(resolved) {
373
- return resolved.reduce((total, entry) => total + entry.width, 0);
1197
+ function applyColumnVisibility(columns, visibility) {
1198
+ const visible = columns.filter(
1199
+ (entry) => isColumnVisible(entry.column, visibility)
1200
+ );
1201
+ return visible.length === columns.length ? columns : visible;
374
1202
  }
375
- function beginColumnResize(column, startWidth, startPosition, defaults) {
1203
+ function setColumnVisibility(columns, visibility, columnIds, visible) {
1204
+ const requested = new Set(columnIds);
1205
+ const changed = columns.filter(
1206
+ (entry) => requested.has(entry.id) && isColumnVisible(entry.column, visibility) !== visible
1207
+ );
1208
+ if (changed.length === 0) {
1209
+ return visibility;
1210
+ }
376
1211
  return {
377
- columnId: getColumnId(column),
378
- startWidth,
379
- startPosition,
380
- constraints: resolveColumnConstraints(column, defaults)
1212
+ ...visibility,
1213
+ ...Object.fromEntries(changed.map((entry) => [entry.id, visible]))
381
1214
  };
382
1215
  }
383
- function applyColumnResize(session, position) {
384
- return clampColumnWidth(
385
- session.startWidth + (position - session.startPosition),
386
- session.constraints
1216
+
1217
+ // src/util/view.ts
1218
+ var NO_AGGREGATES = [];
1219
+ function identifyRows(rows, getRowId) {
1220
+ return rows.map((row, rowIndex) => ({
1221
+ rowId: resolveRowId(row, rowIndex, getRowId),
1222
+ row,
1223
+ rowIndex,
1224
+ datasetIndex: rowIndex
1225
+ }));
1226
+ }
1227
+ function sizeColumns(visibleColumns, sizing, sizes, resizable, reorderable, groupByDraggable, resizeMode, viewportWidth) {
1228
+ const widths = resolveColumnWidths(
1229
+ visibleColumns.map((entry) => entry.column),
1230
+ sizing,
1231
+ { sizes, resizable, reorderable, groupByDraggable }
387
1232
  );
1233
+ return resizeMode === "fit" && viewportWidth !== null ? fitColumnsToWidth(widths, viewportWidth, sizes) : widths;
388
1234
  }
389
- function reachedBound(width, item) {
390
- return item.bound > item.width ? width >= item.bound : width <= item.bound;
1235
+ function totalAggregates(rows, aggregates, dataColumns) {
1236
+ return computeAggregates(
1237
+ rows.map((entry) => entry.row),
1238
+ aggregates,
1239
+ dataColumns
1240
+ );
391
1241
  }
392
- function redistribute(entries, remaining) {
393
- for (; ; ) {
394
- const movable = entries.filter(
395
- (item) => item.adjustable && !reachedBound(item.width, item)
1242
+ function paginate(displayRows, paginated, pagination) {
1243
+ return paginated ? paginateRows(displayRows, pagination) : { rows: displayRows, pageCount: 1, pageIndex: 0 };
1244
+ }
1245
+ function narrowToDataRows(pageRows, rows, reshaped) {
1246
+ return reshaped ? pageRows.filter((entry) => !("kind" in entry)) : rows;
1247
+ }
1248
+ function createGridViewResolver() {
1249
+ const identify = memoizeLast(identifyRows);
1250
+ const orderColumns = memoizeLast(resolveDataColumns);
1251
+ const showColumns = memoizeLast(applyColumnVisibility);
1252
+ const size = memoizeLast(sizeColumns);
1253
+ const filterAndSort = memoizeLast(resolveShownRows);
1254
+ const group = memoizeLast(groupRows);
1255
+ const aggregate = memoizeLast(withGroupAggregates);
1256
+ const total = memoizeLast(totalAggregates);
1257
+ const page = memoizeLast(paginate);
1258
+ const narrow = memoizeLast(narrowToDataRows);
1259
+ const assemble = memoizeLast(
1260
+ (dataColumns, columns, rows, displayRows, pageRows, displayDataRows, pageCount, pageIndex, grandTotal) => ({
1261
+ dataColumns,
1262
+ columns,
1263
+ rows,
1264
+ displayRows,
1265
+ pageRows,
1266
+ displayDataRows,
1267
+ pageCount,
1268
+ pageIndex,
1269
+ grandTotal
1270
+ })
1271
+ );
1272
+ return ({ rows: data, getRowId, columns: definitions, state, options }) => {
1273
+ const paginated = options.paginated ?? false;
1274
+ const aggregates = options.aggregates ?? NO_AGGREGATES;
1275
+ const dataColumns = orderColumns(definitions, state.columnOrder);
1276
+ const columns = size(
1277
+ showColumns(dataColumns, state.columnVisibility),
1278
+ state.columnSizing,
1279
+ options.columnSizeDefaults,
1280
+ options.resizableColumns,
1281
+ options.reorderableColumns,
1282
+ options.groupByDraggableColumns,
1283
+ options.resizeMode ?? "fit",
1284
+ options.viewportWidth ?? null
396
1285
  );
397
- const left = remaining(entries.reduce((t, i) => t + i.width, 0));
398
- if (left <= 0 || movable.length === 0) {
399
- break;
400
- }
401
- const base = movable.reduce((total, item) => total + item.width, 0);
402
- let clamped = false;
403
- for (const item of movable) {
404
- const share = base === 0 ? left / movable.length : left * item.width / base;
405
- const next = item.width + (item.bound > item.width ? share : -share);
406
- if (reachedBound(next, item)) {
407
- item.width = item.bound;
408
- clamped = true;
409
- } else {
410
- item.width = next;
411
- }
1286
+ const rows = filterAndSort(
1287
+ identify(data, getRowId),
1288
+ state.filter,
1289
+ state.columnSort,
1290
+ dataColumns
1291
+ );
1292
+ const displayRows = aggregate(
1293
+ group(rows, state.groupBy, state.groupExpansion, dataColumns),
1294
+ rows,
1295
+ state.groupBy,
1296
+ aggregates,
1297
+ dataColumns,
1298
+ options.groupAggregateDisplay ?? "inline"
1299
+ );
1300
+ const {
1301
+ rows: pageRows,
1302
+ pageCount,
1303
+ pageIndex
1304
+ } = page(displayRows, paginated, state.pagination);
1305
+ return assemble(
1306
+ dataColumns,
1307
+ columns,
1308
+ rows,
1309
+ displayRows,
1310
+ pageRows,
1311
+ narrow(pageRows, rows, state.groupBy.length > 0 || paginated),
1312
+ pageCount,
1313
+ pageIndex,
1314
+ total(rows, aggregates, dataColumns)
1315
+ );
1316
+ };
1317
+ }
1318
+ function resolveGridView(input) {
1319
+ return createGridViewResolver()(input);
1320
+ }
1321
+
1322
+ // src/controller/changes.ts
1323
+ function changedEntryColumnId(previous, next) {
1324
+ const length = Math.max(previous.length, next.length);
1325
+ for (let index = 0; index < length; index++) {
1326
+ const before = previous[index];
1327
+ const after = next[index];
1328
+ if (before?.columnId !== after?.columnId || before?.direction !== after?.direction) {
1329
+ return (after ?? before)?.columnId;
412
1330
  }
413
- if (!clamped) {
414
- break;
1331
+ }
1332
+ return void 0;
1333
+ }
1334
+ function changedId(previous, next) {
1335
+ const length = Math.max(previous.length, next.length);
1336
+ for (let index = 0; index < length; index++) {
1337
+ if (previous[index] !== next[index]) {
1338
+ return next[index] ?? previous[index];
415
1339
  }
416
1340
  }
1341
+ return void 0;
1342
+ }
1343
+ function changedKeys(previous, next) {
1344
+ const keys = /* @__PURE__ */ new Set([...Object.keys(next), ...Object.keys(previous)]);
1345
+ return [...keys].filter((key) => previous[key] !== next[key]);
1346
+ }
1347
+ function sameItems(previous, next) {
1348
+ return previous.length === next.length && previous.every((item, index) => item === next[index]);
1349
+ }
1350
+ function samePagination(previous, next) {
1351
+ return previous.pageIndex === next.pageIndex && previous.pageSize === next.pageSize;
1352
+ }
1353
+ function sameValue(previous, next) {
1354
+ return previous instanceof Date && next instanceof Date ? previous.getTime() === next.getTime() : previous === next;
1355
+ }
1356
+ function sameOperand(previous, next) {
1357
+ if (isScalarList(previous) || isScalarList(next)) {
1358
+ return isScalarList(previous) && isScalarList(next) && previous.length === next.length && previous.every((scalar, index) => sameValue(scalar, next[index]));
1359
+ }
1360
+ return sameValue(previous, next);
1361
+ }
1362
+ function sameEntry(previous, next) {
1363
+ if ("combinator" in previous || "combinator" in next) {
1364
+ return "combinator" in previous && "combinator" in next && previous.combinator === next.combinator && sameFilter(previous.entries, next.entries);
1365
+ }
1366
+ if (previous.columnId !== next.columnId) {
1367
+ return false;
1368
+ }
1369
+ if ("predicate" in previous || "predicate" in next) {
1370
+ return "predicate" in previous && "predicate" in next && previous.predicate === next.predicate;
1371
+ }
1372
+ if ("operator" in previous || "operator" in next) {
1373
+ return "operator" in previous && "operator" in next && previous.operator === next.operator && sameOperand(previous.value, next.value);
1374
+ }
1375
+ if ("value" in previous || "value" in next) {
1376
+ return "value" in previous && "value" in next && sameValue(previous.value, next.value);
1377
+ }
1378
+ return previous.query === next.query;
1379
+ }
1380
+ function sameFilter(previous, next) {
1381
+ return previous.length === next.length && previous.every((entry, index) => {
1382
+ const other = next[index];
1383
+ return other !== void 0 && sameEntry(entry, other);
1384
+ });
1385
+ }
1386
+
1387
+ // src/util/selection.ts
1388
+ var NO_CHANGE = { added: [], removed: [] };
1389
+ function sameSelection(a, b) {
1390
+ return a.length === b.length && a.every((id, index) => id === b[index]);
1391
+ }
1392
+ function toggleSelection(selection, id, mode) {
1393
+ if (mode === false) {
1394
+ return selection;
1395
+ }
1396
+ if (selection.includes(id)) {
1397
+ return selection.filter((selectedId) => selectedId !== id);
1398
+ }
1399
+ return mode === "single" ? [id] : [...selection, id];
417
1400
  }
418
- function fitColumnsToWidth(resolved, availableWidth, defaults) {
419
- const target = Math.floor(availableWidth);
420
- const total = totalColumnWidth(resolved);
421
- if (resolved.length === 0 || total === target) {
422
- return resolved;
1401
+ function selectOnly(selection, id, mode) {
1402
+ if (mode === false) {
1403
+ return selection;
423
1404
  }
424
- const growing = total < target;
425
- const entries = resolved.map((entry) => {
426
- const constraints = resolveColumnConstraints(entry.column, defaults);
427
- return {
428
- entry,
429
- width: entry.width,
430
- bound: growing ? constraints.maxWidth : constraints.minWidth,
431
- adjustable: !entry.sized
432
- };
433
- });
434
- if (!growing) {
435
- const floor = entries.reduce(
436
- (sum, item) => sum + (item.adjustable ? item.bound : item.width),
437
- 0
438
- );
439
- if (floor > target) {
440
- return resolved;
441
- }
1405
+ return selection.length === 1 && selection[0] === id ? selection : [id];
1406
+ }
1407
+ function selectRange(selection, orderedIds, anchorId, focusId, mode) {
1408
+ if (mode !== "multiple") {
1409
+ return selectOnly(selection, focusId, mode);
442
1410
  }
443
- redistribute(entries, (sum) => growing ? target - sum : sum - target);
444
- const rounded = entries.map(
445
- (item) => growing ? Math.floor(item.width) : Math.ceil(item.width)
1411
+ const anchor = orderedIds.indexOf(anchorId);
1412
+ const focus = orderedIds.indexOf(focusId);
1413
+ if (anchor === -1 || focus === -1) {
1414
+ return selection;
1415
+ }
1416
+ const next = orderedIds.slice(
1417
+ Math.min(anchor, focus),
1418
+ Math.max(anchor, focus) + 1
446
1419
  );
447
- let remainder = target - rounded.reduce((total2, width) => total2 + width, 0);
448
- return entries.map((item, index) => {
449
- let width = rounded[index] ?? item.width;
450
- const hasRoom = growing ? width < item.bound : width > item.bound;
451
- if (remainder !== 0 && item.adjustable && hasRoom) {
452
- width += growing ? 1 : -1;
453
- remainder += growing ? -1 : 1;
454
- }
455
- return { ...item.entry, width };
456
- });
1420
+ return sameSelection(selection, next) ? selection : next;
457
1421
  }
458
- function sizeColumnToContent(measuredWidth, constraints, padding = 2) {
459
- return clampColumnWidth(Math.ceil(measuredWidth + padding), constraints);
1422
+ function selectAll(selection, orderedIds, mode) {
1423
+ if (mode !== "multiple") {
1424
+ return selection;
1425
+ }
1426
+ return sameSelection(selection, orderedIds) ? selection : orderedIds;
460
1427
  }
461
- function revertColumnSize(current, base, columnId) {
462
- const next = Object.fromEntries(
463
- Object.entries(current).filter(([id]) => id !== columnId)
464
- );
465
- if (columnId in base) {
466
- next[columnId] = base[columnId];
1428
+ function clearSelection(selection) {
1429
+ return selection.length === 0 ? selection : [];
1430
+ }
1431
+ function diffSelection(previous, next) {
1432
+ if (previous === next) {
1433
+ return NO_CHANGE;
467
1434
  }
468
- return next;
1435
+ const before = new Set(previous);
1436
+ const after = new Set(next);
1437
+ return {
1438
+ added: next.filter((id) => !before.has(id)),
1439
+ removed: previous.filter((id) => !after.has(id))
1440
+ };
469
1441
  }
470
-
471
- // src/util/sorting.ts
472
- function isEmpty(value) {
473
- return value === null || value === void 0;
1442
+ function isSameCell(a, b) {
1443
+ if (a === null || b === null) {
1444
+ return a === b;
1445
+ }
1446
+ return a.rowId === b.rowId && a.columnId === b.columnId;
474
1447
  }
475
- function toTime(value) {
476
- return value instanceof Date ? value.getTime() : new Date(String(value)).getTime();
1448
+ function selectCell(selection, cell, mode) {
1449
+ if (mode === false || isSameCell(selection, cell)) {
1450
+ return selection;
1451
+ }
1452
+ return cell;
477
1453
  }
478
- function compareValues(a, b, type) {
479
- if (isEmpty(a)) {
480
- return isEmpty(b) ? 0 : 1;
1454
+ function toggleCellSelection(selection, cell, mode) {
1455
+ if (mode === false) {
1456
+ return selection;
481
1457
  }
482
- if (isEmpty(b)) {
483
- return -1;
1458
+ return isSameCell(selection, cell) ? null : cell;
1459
+ }
1460
+ function intentOf(event, options) {
1461
+ if (event.shiftKey) {
1462
+ return "range";
484
1463
  }
485
- switch (type) {
486
- case "number":
487
- case "decimal":
488
- case "currency":
489
- case "percent":
490
- return Number(a) - Number(b);
491
- case "boolean":
492
- return Number(Boolean(a)) - Number(Boolean(b));
493
- case "date":
494
- case "dateTime":
495
- case "time":
496
- return toTime(a) - toTime(b);
497
- default:
498
- return String(a).localeCompare(String(b));
1464
+ if (event.ctrlKey || event.metaKey) {
1465
+ return "toggle";
499
1466
  }
1467
+ return options?.toggleOnClick === true ? "toggle" : "replace";
500
1468
  }
501
- function sortDirectionFor(sort, columnId) {
502
- return sort.find((entry) => entry.columnId === columnId)?.direction ?? null;
1469
+ function applySelectionIntent(selection, orderedIds, anchorId, id, intent, mode) {
1470
+ switch (intent) {
1471
+ case "toggle":
1472
+ return toggleSelection(selection, id, mode);
1473
+ // A range with no anchor yet spans from the id to itself, which is the
1474
+ // plain click a first Shift-click may as well be.
1475
+ case "range":
1476
+ return selectRange(selection, orderedIds, anchorId ?? id, id, mode);
1477
+ default:
1478
+ return selectOnly(selection, id, mode);
1479
+ }
503
1480
  }
504
- function sortPriorityFor(sort, columnId) {
505
- const index = sort.findIndex((entry) => entry.columnId === columnId);
506
- return index === -1 ? null : index + 1;
1481
+
1482
+ // src/controller/selectionEvents.ts
1483
+ function reportRowSelection(previous, next, rowsById, callbacks) {
1484
+ const diff = diffSelection(previous, next);
1485
+ const added = resolveRows(rowsById, diff.added);
1486
+ const removed = resolveRows(rowsById, diff.removed);
1487
+ for (const row of added) {
1488
+ callbacks.onRowSelect?.({ row, selection: next });
1489
+ }
1490
+ if (added.length > 0) {
1491
+ callbacks.onRowsSelect?.({ rows: added, selection: next });
1492
+ }
1493
+ for (const row of removed) {
1494
+ callbacks.onRowDeselect?.({ row, selection: next });
1495
+ }
1496
+ if (removed.length > 0) {
1497
+ callbacks.onRowsDeselect?.({ rows: removed, selection: next });
1498
+ }
1499
+ callbacks.onRowSelectionChange?.({
1500
+ added,
1501
+ removed,
1502
+ selected: resolveRows(rowsById, next),
1503
+ selection: next
1504
+ });
1505
+ return diff;
507
1506
  }
508
- function toggleColumnSort(sort, columnId, options) {
509
- const index = sort.findIndex((entry) => entry.columnId === columnId);
510
- const isSoleEntry = sort.length === 1 && index === 0;
511
- if (!options.stack && !isSoleEntry) {
512
- return [{ columnId, direction: "asc" }];
1507
+ function reportColumnSelection(previous, next, columns, callbacks) {
1508
+ const diff = diffSelection(previous, next);
1509
+ const added = resolveColumns(columns, diff.added);
1510
+ const removed = resolveColumns(columns, diff.removed);
1511
+ for (const column of added) {
1512
+ callbacks.onColumnSelect?.({ column, selection: next });
513
1513
  }
514
- const current = index === -1 ? void 0 : sort[index]?.direction;
515
- const next = current === void 0 ? "asc" : current === "asc" ? "desc" : null;
516
- if (next === null) {
517
- return sort.filter((entry) => entry.columnId !== columnId);
1514
+ if (added.length > 0) {
1515
+ callbacks.onColumnsSelect?.({ columns: added, selection: next });
518
1516
  }
519
- if (index === -1) {
520
- return [...sort, { columnId, direction: next }];
1517
+ for (const column of removed) {
1518
+ callbacks.onColumnDeselect?.({ column, selection: next });
521
1519
  }
522
- return sort.map(
523
- (entry, i) => i === index ? { columnId, direction: next } : entry
524
- );
1520
+ if (removed.length > 0) {
1521
+ callbacks.onColumnsDeselect?.({ columns: removed, selection: next });
1522
+ }
1523
+ callbacks.onColumnSelectionChange?.({
1524
+ added,
1525
+ removed,
1526
+ selected: resolveColumns(columns, next),
1527
+ selection: next
1528
+ });
1529
+ return diff;
525
1530
  }
526
- function sortRows(rows, sort, columns) {
527
- if (sort.length === 0) {
528
- return rows;
1531
+ function reportCellSelection(previous, next, rowsById, columns, callbacks) {
1532
+ const selected = resolveCell(rowsById, columns, next);
1533
+ const deselected = resolveCell(rowsById, columns, previous);
1534
+ if (deselected !== null) {
1535
+ callbacks.onCellDeselect?.({ cell: deselected, selection: next });
529
1536
  }
530
- const byId = new Map(columns.map((entry) => [entry.id, entry.column]));
531
- const active = sort.filter((entry) => byId.has(entry.columnId));
532
- if (active.length === 0) {
533
- return rows;
1537
+ if (selected !== null) {
1538
+ callbacks.onCellSelect?.({ cell: selected, selection: next });
534
1539
  }
535
- const sorted = [...rows].sort((a, b) => {
536
- for (const { columnId, direction } of active) {
537
- const column = byId.get(columnId);
538
- if (column === void 0) {
539
- continue;
540
- }
541
- const left = accessDotted(a.row, column.field);
542
- const right = accessDotted(b.row, column.field);
543
- const leftEmpty = isEmpty(left);
544
- const rightEmpty = isEmpty(right);
545
- if (leftEmpty || rightEmpty) {
546
- if (leftEmpty && rightEmpty) {
547
- continue;
548
- }
549
- return leftEmpty ? 1 : -1;
550
- }
551
- const result = compareValues(left, right, column.type);
552
- if (result !== 0) {
553
- return direction === "asc" ? result : -result;
554
- }
555
- }
556
- return 0;
1540
+ callbacks.onCellSelectionChange?.({
1541
+ selected,
1542
+ deselected,
1543
+ selection: next
557
1544
  });
558
- return sorted.map(
559
- (row, index) => row.rowIndex === index ? row : { ...row, rowIndex: index }
560
- );
561
1545
  }
562
1546
 
563
- // src/util/filtering.ts
564
- function parseFilterPattern(query) {
565
- const start = query.startsWith("%");
566
- const end = query.endsWith("%");
567
- let text = query;
568
- if (start) {
569
- text = text.slice(1);
570
- }
571
- if (end) {
572
- text = text.slice(0, -1);
1547
+ // src/controller/selectionActions.ts
1548
+ function indexRows(rows) {
1549
+ return new Map(rows.map((entry) => [entry.rowId, entry]));
1550
+ }
1551
+ function distinct(ids) {
1552
+ return [...new Set(ids)];
1553
+ }
1554
+ function createSelectionActions(grid) {
1555
+ let rowAnchor = null;
1556
+ let columnAnchor = null;
1557
+ const rowsById = memoizeLast(indexRows);
1558
+ function modes() {
1559
+ const selectable = grid.getOptions().selectable;
1560
+ return {
1561
+ rows: selectable?.rows ?? false,
1562
+ columns: selectable?.columns ?? false,
1563
+ cells: selectable?.cells ?? false
1564
+ };
573
1565
  }
574
- text = text.toLowerCase();
575
- if (start && end) {
576
- return { mode: "contains", text };
1566
+ function rowIds() {
1567
+ return grid.getView().displayDataRows.map((entry) => entry.rowId);
1568
+ }
1569
+ function columnIds() {
1570
+ return grid.getView().columns.map((entry) => entry.id);
1571
+ }
1572
+ function commitRows(next) {
1573
+ const previous = grid.getState().rowSelection;
1574
+ grid.commit("rowSelection", next, () => {
1575
+ const options = grid.getOptions();
1576
+ const diff = reportRowSelection(
1577
+ previous,
1578
+ next,
1579
+ rowsById(grid.getView().displayDataRows),
1580
+ options
1581
+ );
1582
+ options.onSelectionDiff?.({ target: "rows", ...diff, selection: next });
1583
+ });
577
1584
  }
578
- if (start) {
579
- return { mode: "endsWith", text };
1585
+ function commitColumns(next) {
1586
+ const previous = grid.getState().columnSelection;
1587
+ grid.commit("columnSelection", next, () => {
1588
+ const options = grid.getOptions();
1589
+ const diff = reportColumnSelection(
1590
+ previous,
1591
+ next,
1592
+ grid.getView().columns,
1593
+ options
1594
+ );
1595
+ options.onSelectionDiff?.({
1596
+ target: "columns",
1597
+ ...diff,
1598
+ selection: next
1599
+ });
1600
+ });
580
1601
  }
581
- if (end) {
582
- return { mode: "startsWith", text };
1602
+ function commitCell(next) {
1603
+ const previous = grid.getState().cellSelection;
1604
+ grid.commit("cellSelection", next, () => {
1605
+ const view = grid.getView();
1606
+ reportCellSelection(
1607
+ previous,
1608
+ next,
1609
+ rowsById(view.displayDataRows),
1610
+ view.columns,
1611
+ grid.getOptions()
1612
+ );
1613
+ });
583
1614
  }
584
- return { mode: "exact", text };
1615
+ return {
1616
+ setRowSelection(selection) {
1617
+ if (!sameItems(grid.getState().rowSelection, selection)) {
1618
+ commitRows(selection);
1619
+ }
1620
+ },
1621
+ selectRows(ids, intent = "replace") {
1622
+ const mode = modes().rows;
1623
+ const current = grid.getState().rowSelection;
1624
+ if (mode === false || ids.length === 0) {
1625
+ return;
1626
+ }
1627
+ const next = intent === "toggle" ? ids.reduce(
1628
+ (selection, id) => toggleSelection(selection, id, mode),
1629
+ current
1630
+ ) : mode === "single" ? ids.slice(-1) : distinct(ids);
1631
+ rowAnchor = ids.at(-1) ?? null;
1632
+ if (!sameItems(current, next)) {
1633
+ commitRows(next);
1634
+ }
1635
+ },
1636
+ selectRow(rowId, intent) {
1637
+ const next = applySelectionIntent(
1638
+ grid.getState().rowSelection,
1639
+ rowIds(),
1640
+ rowAnchor,
1641
+ rowId,
1642
+ intent,
1643
+ modes().rows
1644
+ );
1645
+ if (intent !== "range") {
1646
+ rowAnchor = rowId;
1647
+ }
1648
+ commitRows(next);
1649
+ },
1650
+ selectAllRows() {
1651
+ commitRows(
1652
+ selectAll(grid.getState().rowSelection, rowIds(), modes().rows)
1653
+ );
1654
+ },
1655
+ clearSelection() {
1656
+ commitRows(clearSelection(grid.getState().rowSelection));
1657
+ commitColumns(clearSelection(grid.getState().columnSelection));
1658
+ commitCell(null);
1659
+ },
1660
+ setColumnSelection(selection) {
1661
+ if (!sameItems(grid.getState().columnSelection, selection)) {
1662
+ commitColumns(selection);
1663
+ }
1664
+ },
1665
+ selectColumn(columnId, intent) {
1666
+ const next = applySelectionIntent(
1667
+ grid.getState().columnSelection,
1668
+ columnIds(),
1669
+ columnAnchor,
1670
+ columnId,
1671
+ intent,
1672
+ modes().columns
1673
+ );
1674
+ if (intent !== "range") {
1675
+ columnAnchor = columnId;
1676
+ }
1677
+ commitColumns(next);
1678
+ },
1679
+ setCellSelection(selection) {
1680
+ if (!isSameCell(grid.getState().cellSelection, selection)) {
1681
+ commitCell(selection);
1682
+ }
1683
+ },
1684
+ selectCell(cell, intent) {
1685
+ const current = grid.getState().cellSelection;
1686
+ const mode = modes().cells;
1687
+ commitCell(
1688
+ intent === "toggle" ? toggleCellSelection(current, cell, mode) : selectCell(current, cell, mode)
1689
+ );
1690
+ }
1691
+ };
585
1692
  }
586
- function isEmptyValue(value) {
587
- return value === null || value === void 0;
1693
+
1694
+ // src/controller/createGridController.ts
1695
+ var DEFAULT_PAGE_SIZE = 25;
1696
+ var NO_ROWS = [];
1697
+ var NO_COLUMNS = [];
1698
+ var SLICE_KEYS = [
1699
+ "columnSizing",
1700
+ "columnOrder",
1701
+ "columnVisibility",
1702
+ "columnSort",
1703
+ "filter",
1704
+ "groupBy",
1705
+ "groupExpansion",
1706
+ "pagination",
1707
+ "rowSelection",
1708
+ "columnSelection",
1709
+ "cellSelection"
1710
+ ];
1711
+ var DEFAULT_OPTION = {
1712
+ columnSizing: "defaultColumnSizing",
1713
+ columnOrder: "defaultColumnOrder",
1714
+ columnVisibility: "defaultColumnVisibility",
1715
+ columnSort: "defaultColumnSort",
1716
+ filter: "defaultFilter",
1717
+ groupBy: "defaultGroupBy",
1718
+ groupExpansion: "defaultGroupExpansion",
1719
+ pagination: "defaultPagination",
1720
+ rowSelection: "defaultRowSelection",
1721
+ columnSelection: "defaultColumnSelection",
1722
+ cellSelection: "defaultCellSelection"
1723
+ };
1724
+ function startingState(options) {
1725
+ return {
1726
+ columnSizing: options.defaultColumnSizing ?? {},
1727
+ columnOrder: options.defaultColumnOrder ?? [],
1728
+ columnVisibility: options.defaultColumnVisibility ?? {},
1729
+ columnSort: options.defaultColumnSort ?? [],
1730
+ filter: options.defaultFilter ?? [],
1731
+ groupBy: options.defaultGroupBy ?? [],
1732
+ groupExpansion: options.defaultGroupExpansion ?? [],
1733
+ pagination: options.defaultPagination ?? {
1734
+ pageIndex: 0,
1735
+ pageSize: DEFAULT_PAGE_SIZE
1736
+ },
1737
+ rowSelection: options.defaultRowSelection ?? [],
1738
+ columnSelection: options.defaultColumnSelection ?? [],
1739
+ cellSelection: options.defaultCellSelection ?? null
1740
+ };
588
1741
  }
589
- function compileFilterQuery(query) {
590
- const { mode, text } = parseFilterPattern(query);
591
- return (value) => {
592
- const candidate = (isEmptyValue(value) ? "" : String(value)).toLowerCase();
593
- switch (mode) {
594
- case "exact":
595
- return candidate === text;
596
- case "contains":
597
- return candidate.includes(text);
598
- case "startsWith":
599
- return candidate.startsWith(text);
600
- case "endsWith":
601
- return candidate.endsWith(text);
602
- }
1742
+ function definedColumns(columns, rows) {
1743
+ if (columns !== void 0 && columns.length > 0) {
1744
+ return columns;
1745
+ }
1746
+ if (rows !== void 0 && rows.length > 0) {
1747
+ return defineColumnsFromRows(rows);
1748
+ }
1749
+ return NO_COLUMNS;
1750
+ }
1751
+ function pagingInputsOf(state) {
1752
+ return {
1753
+ filter: state.filter,
1754
+ columnSort: state.columnSort,
1755
+ groupBy: state.groupBy
603
1756
  };
604
1757
  }
605
- function matchesTypedValue(value, target, type) {
606
- if (typeof target === "number") {
607
- if (type !== "number" && type !== "decimal" && type !== "currency" && type !== "percent") {
608
- return false;
1758
+ function createGridController(initial) {
1759
+ let options = initial;
1760
+ const own = startingState(initial);
1761
+ const resolveView = createGridViewResolver();
1762
+ const resolveColumns2 = memoizeLast(definedColumns);
1763
+ const listeners = /* @__PURE__ */ new Set();
1764
+ const reportedConflicts = /* @__PURE__ */ new Set();
1765
+ let heldReports;
1766
+ let state = resolveSlices();
1767
+ let pagingInputs = pagingInputsOf(state);
1768
+ reportConflicts();
1769
+ function controlledValue(key) {
1770
+ const controlled = options;
1771
+ return controlled[key];
1772
+ }
1773
+ function isControlled(key) {
1774
+ return controlledValue(key) !== void 0;
1775
+ }
1776
+ function resolveSlice(key) {
1777
+ const controlled = controlledValue(key);
1778
+ return controlled === void 0 ? own[key] : controlled;
1779
+ }
1780
+ function resolveSlices() {
1781
+ return {
1782
+ columnSizing: resolveSlice("columnSizing"),
1783
+ columnOrder: resolveSlice("columnOrder"),
1784
+ columnVisibility: resolveSlice("columnVisibility"),
1785
+ columnSort: resolveSlice("columnSort"),
1786
+ filter: resolveSlice("filter"),
1787
+ groupBy: resolveSlice("groupBy"),
1788
+ groupExpansion: resolveSlice("groupExpansion"),
1789
+ pagination: resolveSlice("pagination"),
1790
+ rowSelection: resolveSlice("rowSelection"),
1791
+ columnSelection: resolveSlice("columnSelection"),
1792
+ cellSelection: resolveSlice("cellSelection")
1793
+ };
1794
+ }
1795
+ function reportConflicts() {
1796
+ for (const key of SLICE_KEYS) {
1797
+ const defaultKey = DEFAULT_OPTION[key];
1798
+ if (reportedConflicts.has(key) || !isControlled(key) || options[defaultKey] === void 0) {
1799
+ continue;
1800
+ }
1801
+ reportedConflicts.add(key);
1802
+ reportError(
1803
+ `\`${key}\` and \`${defaultKey}\` are both set \u2014 \`${key}\` is controlled, so \`${defaultKey}\` is ignored.`
1804
+ );
609
1805
  }
610
- return typeof value === "number" && value === target;
611
1806
  }
612
- if (typeof target === "boolean") {
613
- if (type !== "boolean") {
614
- return false;
1807
+ function refresh() {
1808
+ let next = resolveSlices();
1809
+ if (next.filter !== pagingInputs.filter || next.columnSort !== pagingInputs.columnSort || next.groupBy !== pagingInputs.groupBy) {
1810
+ pagingInputs = pagingInputsOf(next);
1811
+ if (!isControlled("pagination") && own.pagination.pageIndex !== 0) {
1812
+ own.pagination = { ...own.pagination, pageIndex: 0 };
1813
+ next = { ...next, pagination: own.pagination };
1814
+ }
615
1815
  }
616
- return typeof value === "boolean" && value === target;
1816
+ state = SLICE_KEYS.every((key) => next[key] === state[key]) ? state : next;
617
1817
  }
618
- if (type !== "date" && type !== "dateTime" && type !== "time") {
619
- return false;
1818
+ function notify() {
1819
+ for (const listener of [...listeners]) {
1820
+ listener();
1821
+ }
620
1822
  }
621
- return value instanceof Date && value.getTime() === target.getTime();
622
- }
623
- function isEntryUsable(entry, byId) {
624
- if ("combinator" in entry) {
625
- return true;
1823
+ function commit(key, next, report) {
1824
+ if (next === state[key]) {
1825
+ return;
1826
+ }
1827
+ if (!isControlled(key)) {
1828
+ own[key] = next;
1829
+ }
1830
+ if (heldReports !== void 0) {
1831
+ heldReports.push(report);
1832
+ return;
1833
+ }
1834
+ refresh();
1835
+ report();
1836
+ notify();
1837
+ }
1838
+ function getView() {
1839
+ return resolveView({
1840
+ rows: options.dataSource ?? NO_ROWS,
1841
+ getRowId: options.getRowId,
1842
+ columns: resolveColumns2(options.columns, options.dataSource),
1843
+ state,
1844
+ options: {
1845
+ paginated: options.paginated,
1846
+ aggregates: options.aggregates,
1847
+ groupAggregateDisplay: options.groupAggregateDisplay,
1848
+ columnSizeDefaults: options.columnSizeDefaults,
1849
+ resizableColumns: options.resizableColumns,
1850
+ reorderableColumns: options.reorderableColumns,
1851
+ groupByDraggableColumns: options.groupByDraggableColumns,
1852
+ resizeMode: options.resizeMode,
1853
+ viewportWidth: options.viewportWidth
1854
+ }
1855
+ });
626
1856
  }
627
- return entry.columnId === void 0 || byId.has(entry.columnId);
628
- }
629
- function compileFilterEntry(entry, byId, columns) {
630
- if ("combinator" in entry) {
631
- const tests = entry.entries.filter((nested) => isEntryUsable(nested, byId)).map((nested) => compileFilterEntry(nested, byId, columns));
632
- return entry.combinator === "and" ? (row) => tests.every((test2) => test2(row)) : (row) => tests.some((test2) => test2(row));
1857
+ function commitColumnSizing(next, columnId, width, phase) {
1858
+ commit("columnSizing", next, () => {
1859
+ options.onColumnResize?.({ columnId, width, sizing: next, phase });
1860
+ });
633
1861
  }
634
- if ("predicate" in entry) {
635
- const { columnId: columnId2, predicate } = entry;
636
- const column = columnId2 === void 0 ? void 0 : byId.get(columnId2);
637
- return (row) => predicate(
638
- column === void 0 ? void 0 : accessDotted(row.row, column.field),
639
- row.row
1862
+ function definedWidth(columnId) {
1863
+ const entry = getView().dataColumns.find(
1864
+ (candidate) => candidate.id === columnId
640
1865
  );
641
- }
642
- if ("value" in entry) {
643
- const { columnId: columnId2, value } = entry;
644
- if (columnId2 !== void 0) {
645
- const column = byId.get(columnId2);
646
- if (column === void 0) {
647
- return () => false;
1866
+ const [resolved] = entry === void 0 ? [] : resolveColumnWidths(
1867
+ [entry.column],
1868
+ {},
1869
+ {
1870
+ sizes: options.columnSizeDefaults
648
1871
  }
649
- return (row) => matchesTypedValue(
650
- accessDotted(row.row, column.field),
651
- value,
652
- column.type
653
- );
654
- }
655
- return (row) => columns.some(
656
- (entry2) => matchesTypedValue(
657
- accessDotted(row.row, entry2.column.field),
658
- value,
659
- entry2.column.type
1872
+ );
1873
+ return resolved?.width ?? 0;
1874
+ }
1875
+ function commitColumnVisibility(next) {
1876
+ const previous = state.columnVisibility;
1877
+ commit("columnVisibility", next, () => {
1878
+ const changed = new Set(changedKeys(previous, next));
1879
+ const shown = getView().dataColumns.map((entry) => entry.id).filter((id) => changed.delete(id));
1880
+ options.onColumnVisibilityChange?.({
1881
+ columnIds: [...shown, ...changed],
1882
+ visibility: next
1883
+ });
1884
+ });
1885
+ }
1886
+ function changeColumnVisibility(columnIds, visible) {
1887
+ commitColumnVisibility(
1888
+ setColumnVisibility(
1889
+ getView().dataColumns,
1890
+ state.columnVisibility,
1891
+ columnIds,
1892
+ visible
660
1893
  )
661
1894
  );
662
1895
  }
663
- const { columnId, query } = entry;
664
- const test = compileFilterQuery(query);
665
- if (columnId !== void 0) {
666
- const column = byId.get(columnId);
667
- if (column === void 0) {
668
- return () => false;
669
- }
670
- return (row) => test(accessDotted(row.row, column.field));
1896
+ function commitGroupBy(next, columnId) {
1897
+ commit("groupBy", next, () => {
1898
+ options.onGroupByChange?.({ columnId, groupBy: next });
1899
+ });
671
1900
  }
672
- return (row) => columns.some((entry2) => test(accessDotted(row.row, entry2.column.field)));
673
- }
674
- function matchesQuery(value, query) {
675
- return compileFilterQuery(query)(value);
676
- }
677
- function filterQueryFor(filter, columnId) {
678
- const entry = filter.find(
679
- (candidate) => "query" in candidate && candidate.columnId === columnId
680
- );
681
- return entry?.query ?? null;
682
- }
683
- function setColumnFilter(filter, columnId, query) {
684
- const index = filter.findIndex(
685
- (entry) => !("combinator" in entry) && entry.columnId === columnId
686
- );
687
- if (query === "") {
688
- return index === -1 ? filter : filter.filter((_entry, i) => i !== index);
1901
+ function commitGroupExpansion(next, groupId) {
1902
+ commit("groupExpansion", next, () => {
1903
+ options.onGroupExpansionChange?.({ groupId, expansion: next });
1904
+ });
689
1905
  }
690
- const next = columnId === void 0 ? { query } : { columnId, query };
691
- if (index === -1) {
692
- return [...filter, next];
1906
+ function commitPagination(next) {
1907
+ if (samePagination(state.pagination, next)) {
1908
+ return;
1909
+ }
1910
+ commit("pagination", next, () => {
1911
+ options.onPaginationChange?.({
1912
+ pagination: next,
1913
+ pageCount: paginateRows(getView().displayRows, next).pageCount
1914
+ });
1915
+ });
693
1916
  }
694
- const existing = filter[index];
695
- if (existing !== void 0 && "query" in existing && existing.query === query) {
696
- return filter;
1917
+ function goToPage(pageIndex) {
1918
+ const current = state.pagination;
1919
+ const clamped = paginateRows(getView().displayRows, {
1920
+ ...current,
1921
+ pageIndex
1922
+ }).pageIndex;
1923
+ commitPagination({ ...current, pageIndex: clamped });
1924
+ }
1925
+ function shownPageIndex() {
1926
+ return paginateRows(getView().displayRows, state.pagination).pageIndex;
1927
+ }
1928
+ const selection = createSelectionActions({
1929
+ getState: () => state,
1930
+ getOptions: () => options,
1931
+ getView,
1932
+ commit
1933
+ });
1934
+ const controller = {
1935
+ ...selection,
1936
+ getOptions: () => options,
1937
+ setOptions(next) {
1938
+ options = next;
1939
+ reportConflicts();
1940
+ refresh();
1941
+ },
1942
+ getState: () => state,
1943
+ getView,
1944
+ restoreState(saved) {
1945
+ const reports = [];
1946
+ heldReports = reports;
1947
+ try {
1948
+ restoreSlices(saved);
1949
+ } finally {
1950
+ heldReports = void 0;
1951
+ }
1952
+ if (saved.pagination !== void 0) {
1953
+ pagingInputs = pagingInputsOf(resolveSlices());
1954
+ }
1955
+ refresh();
1956
+ for (const report of reports) {
1957
+ report();
1958
+ }
1959
+ if (reports.length > 0) {
1960
+ notify();
1961
+ }
1962
+ },
1963
+ subscribe(listener) {
1964
+ listeners.add(listener);
1965
+ return () => {
1966
+ listeners.delete(listener);
1967
+ };
1968
+ },
1969
+ setColumnSizing(sizing) {
1970
+ const [columnId] = changedKeys(state.columnSizing, sizing);
1971
+ if (columnId !== void 0) {
1972
+ commitColumnSizing(
1973
+ sizing,
1974
+ columnId,
1975
+ sizing[columnId] ?? definedWidth(columnId),
1976
+ "end"
1977
+ );
1978
+ }
1979
+ },
1980
+ resizeColumn(columnId, width, phase) {
1981
+ commitColumnSizing(
1982
+ { ...state.columnSizing, [columnId]: width },
1983
+ columnId,
1984
+ width,
1985
+ phase
1986
+ );
1987
+ },
1988
+ cancelColumnResize(columnId, startSizing, startWidth) {
1989
+ commitColumnSizing(
1990
+ revertColumnSize(state.columnSizing, startSizing, columnId),
1991
+ columnId,
1992
+ startWidth,
1993
+ "end"
1994
+ );
1995
+ },
1996
+ setColumnOrder(order) {
1997
+ const columnId = changedId(state.columnOrder, order);
1998
+ if (columnId === void 0) {
1999
+ return;
2000
+ }
2001
+ commit("columnOrder", order, () => {
2002
+ options.onColumnOrderChange?.({ columnId, order });
2003
+ });
2004
+ },
2005
+ /**
2006
+ * Applied to every id in display order rather than to the stored order,
2007
+ * which starts empty and may name only some columns, so would have
2008
+ * nothing to rearrange.
2009
+ */
2010
+ moveColumn(columnId, beforeId) {
2011
+ const orderedIds = getView().dataColumns.map((entry) => entry.id);
2012
+ const next = moveColumnBefore(orderedIds, columnId, beforeId);
2013
+ if (next === orderedIds) {
2014
+ return;
2015
+ }
2016
+ commit("columnOrder", next, () => {
2017
+ options.onColumnOrderChange?.({ columnId, order: next });
2018
+ });
2019
+ },
2020
+ setColumnVisibility(visibility) {
2021
+ if (changedKeys(state.columnVisibility, visibility).length > 0) {
2022
+ commitColumnVisibility(visibility);
2023
+ }
2024
+ },
2025
+ showColumns(columnIds) {
2026
+ changeColumnVisibility(columnIds, true);
2027
+ },
2028
+ hideColumns(columnIds) {
2029
+ changeColumnVisibility(columnIds, false);
2030
+ },
2031
+ setFilter(filter) {
2032
+ if (sameFilter(state.filter, filter)) {
2033
+ return;
2034
+ }
2035
+ commit("filter", filter, () => {
2036
+ options.onFilterChange?.({ filter });
2037
+ });
2038
+ },
2039
+ setColumnSort(sort) {
2040
+ const columnId = changedEntryColumnId(state.columnSort, sort);
2041
+ if (columnId === void 0) {
2042
+ return;
2043
+ }
2044
+ commit("columnSort", sort, () => {
2045
+ options.onColumnSortChange?.({ columnId, sort });
2046
+ });
2047
+ },
2048
+ toggleColumnSort(columnId, { stack = false } = {}) {
2049
+ const next = toggleColumnSort(state.columnSort, columnId, { stack });
2050
+ commit("columnSort", next, () => {
2051
+ options.onColumnSortChange?.({ columnId, sort: next });
2052
+ });
2053
+ },
2054
+ clearColumnSort(columnId) {
2055
+ const next = state.columnSort.filter(
2056
+ (entry) => entry.columnId !== columnId
2057
+ );
2058
+ if (next.length === state.columnSort.length) {
2059
+ return;
2060
+ }
2061
+ commit("columnSort", next, () => {
2062
+ options.onColumnSortChange?.({ columnId, sort: next });
2063
+ });
2064
+ },
2065
+ setGroupBy(groupBy) {
2066
+ const columnId = changedEntryColumnId(state.groupBy, groupBy);
2067
+ if (columnId !== void 0) {
2068
+ commitGroupBy(groupBy, columnId);
2069
+ }
2070
+ },
2071
+ toggleGroupBy(columnId) {
2072
+ const grouped = state.groupBy.some(
2073
+ (entry) => entry.columnId === columnId
2074
+ );
2075
+ commitGroupBy(
2076
+ grouped ? state.groupBy.filter((entry) => entry.columnId !== columnId) : [...state.groupBy, { columnId }],
2077
+ columnId
2078
+ );
2079
+ },
2080
+ moveGroupBy(columnId, beforeColumnId) {
2081
+ commitGroupBy(
2082
+ moveGroupByBefore(state.groupBy, columnId, beforeColumnId),
2083
+ columnId
2084
+ );
2085
+ },
2086
+ setGroupExpansion(expansion) {
2087
+ if (!sameItems(state.groupExpansion, expansion)) {
2088
+ commitGroupExpansion(expansion, null);
2089
+ }
2090
+ },
2091
+ toggleGroupExpansion(groupId) {
2092
+ commitGroupExpansion(
2093
+ toggleGroupExpansion(state.groupExpansion, groupId),
2094
+ groupId
2095
+ );
2096
+ },
2097
+ expandAllGroups() {
2098
+ commitGroupExpansion(expandAllGroups(state.groupExpansion), null);
2099
+ },
2100
+ collapseAllGroups() {
2101
+ commitGroupExpansion(collapseAllGroups(getView().displayRows), null);
2102
+ },
2103
+ setPagination: commitPagination,
2104
+ goToPage,
2105
+ nextPage() {
2106
+ goToPage(shownPageIndex() + 1);
2107
+ },
2108
+ previousPage() {
2109
+ goToPage(shownPageIndex() - 1);
2110
+ },
2111
+ setPageSize(pageSize) {
2112
+ const first = { pageIndex: 0, pageSize };
2113
+ commitPagination({
2114
+ ...first,
2115
+ pageIndex: paginateRows(getView().displayRows, first).pageIndex
2116
+ });
2117
+ }
2118
+ };
2119
+ function restoreSlices(saved) {
2120
+ if (saved.columnSizing !== void 0) {
2121
+ controller.setColumnSizing(saved.columnSizing);
2122
+ }
2123
+ if (saved.columnOrder !== void 0) {
2124
+ controller.setColumnOrder(saved.columnOrder);
2125
+ }
2126
+ if (saved.columnVisibility !== void 0) {
2127
+ controller.setColumnVisibility(saved.columnVisibility);
2128
+ }
2129
+ if (saved.columnSort !== void 0) {
2130
+ controller.setColumnSort(saved.columnSort);
2131
+ }
2132
+ if (saved.filter !== void 0) {
2133
+ controller.setFilter(reviveFilter(saved.filter, getView().dataColumns));
2134
+ }
2135
+ if (saved.groupBy !== void 0) {
2136
+ controller.setGroupBy(saved.groupBy);
2137
+ }
2138
+ if (saved.groupExpansion !== void 0) {
2139
+ controller.setGroupExpansion(saved.groupExpansion);
2140
+ }
2141
+ if (saved.pagination !== void 0) {
2142
+ controller.setPagination(saved.pagination);
2143
+ }
2144
+ if (saved.rowSelection !== void 0) {
2145
+ controller.setRowSelection(saved.rowSelection);
2146
+ }
2147
+ if (saved.columnSelection !== void 0) {
2148
+ controller.setColumnSelection(saved.columnSelection);
2149
+ }
2150
+ if (saved.cellSelection !== void 0) {
2151
+ controller.setCellSelection(saved.cellSelection);
2152
+ }
697
2153
  }
698
- return filter.map((entry, i) => i === index ? next : entry);
2154
+ return controller;
699
2155
  }
700
- function clearAllFilters(filter) {
701
- return filter.length === 0 ? filter : [];
702
- }
703
- function filterRows(rows, filter, columns) {
704
- if (filter.length === 0) {
705
- return rows;
706
- }
707
- const byId = new Map(columns.map((entry) => [entry.id, entry.column]));
708
- const active = filter.filter((entry) => isEntryUsable(entry, byId));
709
- if (active.length === 0) {
710
- return rows;
2156
+
2157
+ // src/commands/context.ts
2158
+ function selectedRowsOf(rows, selection) {
2159
+ if (selection.length === 0) {
2160
+ return [];
711
2161
  }
712
- const compiled = active.map(
713
- (entry) => compileFilterEntry(entry, byId, columns)
714
- );
715
- const filtered = rows.filter((row) => compiled.every((test) => test(row)));
716
- return filtered.map(
717
- (row, index) => row.rowIndex === index ? row : { ...row, rowIndex: index }
718
- );
2162
+ const selected = new Set(selection);
2163
+ return rows.filter((entry) => selected.has(entry.rowId));
2164
+ }
2165
+ function createCommandContext({
2166
+ controller,
2167
+ target,
2168
+ messages,
2169
+ host
2170
+ }) {
2171
+ const state = controller.getState();
2172
+ const view = controller.getView();
2173
+ let selectedRows;
2174
+ return {
2175
+ target,
2176
+ state,
2177
+ view,
2178
+ options: controller.getOptions(),
2179
+ messages,
2180
+ actions: controller,
2181
+ host,
2182
+ row: target.area === "cell" ? view.rows.find((entry) => entry.rowId === target.rowId) ?? null : null,
2183
+ column: "columnId" in target ? view.dataColumns.find((entry) => entry.id === target.columnId) ?? null : null,
2184
+ selectedRows: () => selectedRows ??= selectedRowsOf(view.rows, state.rowSelection)
2185
+ };
719
2186
  }
720
2187
 
721
- // src/util/grouping.ts
722
- function groupRowId(path) {
723
- return JSON.stringify(path);
2188
+ // src/commands/builtInCommands.ts
2189
+ function isOn(context, areas) {
2190
+ return areas.includes(context.target.area);
724
2191
  }
725
- function bucketKey(value) {
726
- if (value === null || value === void 0) {
727
- return "\0empty";
728
- }
729
- if (value instanceof Date) {
730
- return `\0date:${String(value.getTime())}`;
2192
+ function columnOn(context, areas) {
2193
+ return isOn(context, areas) ? context.column : null;
2194
+ }
2195
+ function isGroupedBy(context, columnId) {
2196
+ return context.state.groupBy.some((entry) => entry.columnId === columnId);
2197
+ }
2198
+ function sortableColumn(context) {
2199
+ const column = columnOn(context, ["header"]);
2200
+ return column !== null && (column.column.sortable ?? context.options.sortableColumns ?? false) ? column : null;
2201
+ }
2202
+ function groupableColumn(context) {
2203
+ const column = columnOn(context, ["header"]);
2204
+ return column !== null && (column.column.groupable ?? context.options.groupableColumns ?? false) && !isGroupedBy(context, column.id) ? column : null;
2205
+ }
2206
+ function groupedColumn(context) {
2207
+ const column = columnOn(context, ["header", "group"]);
2208
+ return column !== null && isGroupedBy(context, column.id) ? column : null;
2209
+ }
2210
+ function hideableColumn(context) {
2211
+ const column = columnOn(context, ["header"]);
2212
+ return column !== null && column.column.hideable !== false ? column : null;
2213
+ }
2214
+ function resizableColumn(context) {
2215
+ const column = columnOn(context, ["header"]);
2216
+ if (column === null) {
2217
+ return null;
731
2218
  }
732
- switch (typeof value) {
733
- case "string":
734
- case "number":
735
- case "boolean":
736
- case "bigint":
737
- case "symbol":
738
- return `\0${typeof value}:${String(value)}`;
739
- case "function":
740
- return "\0function";
741
- default:
742
- return `\0object:${JSON.stringify(value)}`;
2219
+ return context.view.columns.find(
2220
+ (entry) => entry.id === column.id && entry.resizable
2221
+ ) ?? null;
2222
+ }
2223
+ function resizableColumnIds(context) {
2224
+ return context.view.columns.filter((entry) => entry.resizable).map((entry) => entry.id);
2225
+ }
2226
+ function isAnythingSelectable(selectable) {
2227
+ return [selectable?.rows, selectable?.columns, selectable?.cells].some(
2228
+ (mode) => mode !== void 0 && mode !== false
2229
+ );
2230
+ }
2231
+ function columnName(context) {
2232
+ return context.column === null ? "" : resolveColumnHeaderText(context.column.column);
2233
+ }
2234
+ function columnCommand({
2235
+ id,
2236
+ label,
2237
+ icon,
2238
+ find,
2239
+ isEnabled,
2240
+ isChecked,
2241
+ execute
2242
+ }) {
2243
+ function test(predicate) {
2244
+ return (context) => {
2245
+ const column = find(context);
2246
+ return column !== null && predicate(column, context);
2247
+ };
743
2248
  }
2249
+ return {
2250
+ id,
2251
+ label,
2252
+ icon,
2253
+ isVisible: (context) => find(context) !== null,
2254
+ isEnabled: isEnabled === void 0 ? void 0 : test(isEnabled),
2255
+ isChecked: isChecked === void 0 ? void 0 : test(isChecked),
2256
+ execute: (context) => {
2257
+ const column = find(context);
2258
+ if (column !== null) {
2259
+ execute(column, context);
2260
+ }
2261
+ }
2262
+ };
744
2263
  }
745
- function partitionByValue(rows, column, direction) {
746
- const buckets = /* @__PURE__ */ new Map();
747
- for (const row of rows) {
748
- const value = accessDotted(row.row, column.field);
749
- const key = bucketKey(value);
750
- const bucket = buckets.get(key);
751
- if (bucket === void 0) {
752
- buckets.set(key, { value, rows: [row] });
753
- } else {
754
- bucket.rows.push(row);
2264
+ function sortCommand(direction) {
2265
+ return columnCommand({
2266
+ id: direction === "asc" ? "sort.ascending" : "sort.descending",
2267
+ label: ({ messages }) => direction === "asc" ? messages.sortAscending : messages.sortDescending,
2268
+ icon: direction === "asc" ? "sortAscending" : "sortDescending",
2269
+ find: sortableColumn,
2270
+ isChecked: (column, { state }) => sortDirectionFor(state.columnSort, column.id) === direction,
2271
+ execute: (column, { actions }) => {
2272
+ actions.setColumnSort([{ columnId: column.id, direction }]);
755
2273
  }
756
- }
757
- const groups = [...buckets.values()];
758
- groups.sort((a, b) => {
759
- const result = compareValues(a.value, b.value, column.type);
760
- return direction === "asc" ? result : -result;
761
2274
  });
762
- return groups;
763
2275
  }
764
- function buildLevel(rows, levels, parentPath, expansion, byId, out) {
765
- const level = levels[0];
766
- if (level === void 0) {
767
- out.push(...rows);
768
- return;
769
- }
770
- const column = byId.get(level.columnId);
771
- if (column === void 0) {
772
- out.push(...rows);
773
- return;
774
- }
775
- const rest = levels.slice(1);
776
- const groups = partitionByValue(rows, column, level.direction ?? "asc");
777
- for (const group of groups) {
778
- const path = [...parentPath, group.value];
779
- const groupId = groupRowId(path);
780
- const expanded = !expansion.includes(groupId);
781
- const header = {
782
- kind: "group",
783
- groupId,
784
- level: parentPath.length,
785
- columnId: level.columnId,
786
- value: group.value,
787
- path,
788
- expanded,
789
- count: group.rows.length,
790
- // Renumbered globally once `groupRows` has the whole flattened array —
791
- // a header has no row of its own to carry a dataset position, so
792
- // `datasetIndex` takes the same renumbered value `rowIndex` does.
793
- rowIndex: -1,
794
- datasetIndex: -1,
795
- // Set by `withGroupAggregates`, downstream of grouping — empty here
796
- // since `groupRows` itself never knows about `AggregateState`.
797
- aggregates: /* @__PURE__ */ new Map()
798
- };
799
- out.push(header);
800
- if (expanded) {
801
- buildLevel(group.rows, rest, path, expansion, byId, out);
2276
+ function builtInCommands() {
2277
+ return [
2278
+ sortCommand("asc"),
2279
+ sortCommand("desc"),
2280
+ // Cleared rather than set, so the change reports this column — the one
2281
+ // the announcement names — rather than a key that moved up past it.
2282
+ columnCommand({
2283
+ id: "sort.clear",
2284
+ label: ({ messages }) => messages.clearSort,
2285
+ find: sortableColumn,
2286
+ isEnabled: (column, { state }) => sortDirectionFor(state.columnSort, column.id) !== null,
2287
+ execute: (column, { actions }) => {
2288
+ actions.clearColumnSort(column.id);
2289
+ }
2290
+ }),
2291
+ {
2292
+ id: "sort.clearAll",
2293
+ label: ({ messages }) => messages.clearAllSorting,
2294
+ isVisible: (context) => isOn(context, ["grid", "toolbar", "header"]),
2295
+ isEnabled: ({ state }) => state.columnSort.length > 0,
2296
+ execute: ({ actions }) => {
2297
+ actions.setColumnSort([]);
2298
+ }
2299
+ },
2300
+ columnCommand({
2301
+ id: "group.add",
2302
+ label: (context) => context.messages.groupByColumn(columnName(context)),
2303
+ icon: "group",
2304
+ find: groupableColumn,
2305
+ execute: (column, { actions }) => {
2306
+ actions.toggleGroupBy(column.id);
2307
+ }
2308
+ }),
2309
+ // Toggled rather than set, so the change reports this column — the one
2310
+ // the announcement names — rather than the level that moved up.
2311
+ columnCommand({
2312
+ id: "group.remove",
2313
+ label: (context) => context.messages.removeGroupBy(columnName(context)),
2314
+ icon: "group",
2315
+ find: groupedColumn,
2316
+ execute: (column, { actions }) => {
2317
+ actions.toggleGroupBy(column.id);
2318
+ }
2319
+ }),
2320
+ {
2321
+ id: "group.clearAll",
2322
+ label: ({ messages }) => messages.ungroupAll,
2323
+ isVisible: (context) => isOn(context, ["grid", "toolbar"]) && context.state.groupBy.length > 0,
2324
+ execute: ({ actions }) => {
2325
+ actions.setGroupBy([]);
2326
+ }
2327
+ },
2328
+ {
2329
+ id: "group.expandAll",
2330
+ label: ({ messages }) => messages.expandAllGroups,
2331
+ isVisible: (context) => isOn(context, ["grid", "toolbar", "group", "cell"]) && context.state.groupBy.length > 0,
2332
+ execute: ({ actions }) => {
2333
+ actions.expandAllGroups();
2334
+ }
2335
+ },
2336
+ {
2337
+ id: "group.collapseAll",
2338
+ label: ({ messages }) => messages.collapseAllGroups,
2339
+ isVisible: (context) => isOn(context, ["grid", "toolbar", "group", "cell"]) && context.state.groupBy.length > 0,
2340
+ execute: ({ actions }) => {
2341
+ actions.collapseAllGroups();
2342
+ }
2343
+ },
2344
+ columnCommand({
2345
+ id: "column.hide",
2346
+ label: ({ messages }) => messages.hideColumn,
2347
+ icon: "hide",
2348
+ find: hideableColumn,
2349
+ // The last column shown stays: a grid with none has no header left to
2350
+ // bring one back from.
2351
+ isEnabled: (column, { view }) => view.columns.some((entry) => entry.id !== column.id),
2352
+ execute: (column, { actions }) => {
2353
+ actions.hideColumns([column.id]);
2354
+ }
2355
+ }),
2356
+ columnCommand({
2357
+ id: "column.autoFit",
2358
+ label: ({ messages }) => messages.autoFitColumn,
2359
+ icon: "autoFit",
2360
+ find: resizableColumn,
2361
+ execute: (column, { host }) => {
2362
+ host.autoFitColumns([column.id]);
2363
+ }
2364
+ }),
2365
+ {
2366
+ id: "column.autoFitAll",
2367
+ label: ({ messages }) => messages.autoFitAllColumns,
2368
+ icon: "autoFit",
2369
+ isVisible: (context) => isOn(context, ["grid", "toolbar", "header"]) && resizableColumnIds(context).length > 0,
2370
+ execute: (context) => {
2371
+ context.host.autoFitColumns(resizableColumnIds(context));
2372
+ }
2373
+ },
2374
+ // The target goes along, so the renderer opens the chooser where it was
2375
+ // asked for: beside the toolbar button, below the header, or at the
2376
+ // grid's corner for a call from nowhere in particular.
2377
+ {
2378
+ id: "column.choose",
2379
+ label: ({ messages }) => messages.chooseColumns,
2380
+ isVisible: (context) => isOn(context, ["grid", "toolbar", "header"]),
2381
+ execute: ({ host, target }) => {
2382
+ host.openPanel("columnChooser", target);
2383
+ }
2384
+ },
2385
+ {
2386
+ id: "selection.selectAllRows",
2387
+ label: ({ messages }) => messages.selectAllRows,
2388
+ isVisible: (context) => isOn(context, ["grid", "cell"]) && context.options.selectable?.rows === "multiple",
2389
+ isEnabled: ({ view }) => view.displayDataRows.length > 0,
2390
+ execute: ({ actions }) => {
2391
+ actions.selectAllRows();
2392
+ }
2393
+ },
2394
+ {
2395
+ id: "selection.clear",
2396
+ label: ({ messages }) => messages.clearSelection,
2397
+ isVisible: (context) => isOn(context, ["grid", "cell", "header"]) && isAnythingSelectable(context.options.selectable),
2398
+ isEnabled: ({ state }) => state.rowSelection.length > 0 || state.columnSelection.length > 0 || state.cellSelection !== null,
2399
+ execute: ({ actions }) => {
2400
+ actions.clearSelection();
2401
+ }
2402
+ },
2403
+ {
2404
+ id: "filter.clearAll",
2405
+ label: ({ messages }) => messages.clearFilters,
2406
+ isVisible: (context) => isOn(context, ["grid", "toolbar", "header"]),
2407
+ isEnabled: ({ state }) => state.filter.length > 0,
2408
+ execute: ({ actions }) => {
2409
+ actions.setFilter([]);
2410
+ }
2411
+ }
2412
+ ];
2413
+ }
2414
+
2415
+ // src/commands/registry.ts
2416
+ function isCommandVisible(command, context) {
2417
+ return command.isVisible?.(context) ?? true;
2418
+ }
2419
+ function isCommandEnabled(command, context) {
2420
+ return command.isEnabled?.(context) ?? true;
2421
+ }
2422
+ function resolveCommands(commands) {
2423
+ const registry = new Map(
2424
+ builtInCommands().map((command) => [command.id, command])
2425
+ );
2426
+ for (const command of commands ?? []) {
2427
+ if (command.id === "separator") {
2428
+ reportError(
2429
+ "a command cannot have the id `separator`, which a menu reads as a separator, so it is left out."
2430
+ );
2431
+ continue;
802
2432
  }
2433
+ registry.set(command.id, command);
803
2434
  }
2435
+ return registry;
804
2436
  }
805
- function groupRows(rows, groupBy, expansion, columns) {
806
- if (groupBy.length === 0) {
807
- return rows;
2437
+ function runCommand(command, context) {
2438
+ if (!isCommandVisible(command, context) || !isCommandEnabled(command, context)) {
2439
+ return false;
808
2440
  }
809
- const byId = new Map(columns.map((entry) => [entry.id, entry.column]));
810
- const active = groupBy.filter((entry) => byId.has(entry.columnId));
811
- if (active.length === 0) {
812
- return rows;
2441
+ command.execute(context);
2442
+ return true;
2443
+ }
2444
+
2445
+ // src/menu/resolveMenu.ts
2446
+ var reportedUnknownIds = /* @__PURE__ */ new WeakMap();
2447
+ function reportUnknownId(commands, id) {
2448
+ let reported = reportedUnknownIds.get(commands);
2449
+ if (reported === void 0) {
2450
+ reported = /* @__PURE__ */ new Set();
2451
+ reportedUnknownIds.set(commands, reported);
2452
+ }
2453
+ if (reported.has(id)) {
2454
+ return;
813
2455
  }
814
- const out = [];
815
- buildLevel(rows, active, [], expansion, byId, out);
816
- return out.map((entry, index) => {
817
- if ("kind" in entry) {
818
- return entry.rowIndex === index && entry.datasetIndex === index ? entry : { ...entry, rowIndex: index, datasetIndex: index };
819
- }
820
- return entry.rowIndex === index ? entry : { ...entry, rowIndex: index };
821
- });
2456
+ reported.add(id);
2457
+ reportError(
2458
+ `the menu or toolbar entry \`${id}\` names no command, so it is left out.`
2459
+ );
822
2460
  }
823
- function toggleGroupExpansion(expansion, groupId) {
824
- return expansion.includes(groupId) ? expansion.filter((id) => id !== groupId) : [...expansion, groupId];
2461
+ function evaluate(value, context) {
2462
+ return typeof value === "function" ? value(context) : value;
825
2463
  }
826
- function expandAllGroups(expansion) {
827
- return expansion.length === 0 ? expansion : [];
2464
+ function commandItem(command, context) {
2465
+ if (!isCommandVisible(command, context)) {
2466
+ return null;
2467
+ }
2468
+ return {
2469
+ type: "item",
2470
+ id: command.id,
2471
+ label: evaluate(command.label, context),
2472
+ icon: command.icon,
2473
+ shortcut: command.shortcut,
2474
+ disabled: !isCommandEnabled(command, context),
2475
+ checked: command.isChecked?.(context),
2476
+ run: () => runCommand(command, context)
2477
+ };
828
2478
  }
829
- function collapseAllGroups(rows) {
830
- return rows.filter((entry) => "kind" in entry).map((entry) => entry.groupId);
2479
+ function inlineItem(entry, context) {
2480
+ if (evaluate(entry.hidden ?? false, context)) {
2481
+ return null;
2482
+ }
2483
+ const disabled = evaluate(entry.disabled ?? false, context);
2484
+ return {
2485
+ type: "item",
2486
+ id: entry.id,
2487
+ label: evaluate(entry.label, context),
2488
+ icon: entry.icon,
2489
+ shortcut: entry.shortcut,
2490
+ disabled,
2491
+ checked: entry.checked === void 0 ? void 0 : evaluate(entry.checked, context),
2492
+ run: () => {
2493
+ if (disabled) {
2494
+ return false;
2495
+ }
2496
+ entry.onSelect(context);
2497
+ return true;
2498
+ }
2499
+ };
831
2500
  }
832
- function moveGroupByBefore(groupBy, columnId, beforeColumnId) {
833
- if (columnId === beforeColumnId) {
834
- return groupBy;
2501
+ function collapseSeparators(entries, isEdge = () => false) {
2502
+ const collapsed = [];
2503
+ for (const entry of entries) {
2504
+ const previous = collapsed.at(-1);
2505
+ if (entry.type !== "separator") {
2506
+ if (isEdge(entry) && previous?.type === "separator") {
2507
+ collapsed.pop();
2508
+ }
2509
+ collapsed.push(entry);
2510
+ } else if (previous !== void 0 && previous.type !== "separator" && !isEdge(previous)) {
2511
+ collapsed.push(entry);
2512
+ }
835
2513
  }
836
- const without = groupBy.filter((entry) => entry.columnId !== columnId);
837
- const moved = groupBy.find((entry) => entry.columnId === columnId) ?? {
838
- columnId
2514
+ if (collapsed.at(-1)?.type === "separator") {
2515
+ collapsed.pop();
2516
+ }
2517
+ return collapsed;
2518
+ }
2519
+ function resolveMenuEntry(entry, index, options) {
2520
+ const { commands, context } = options;
2521
+ if (entry === "separator") {
2522
+ return { type: "separator", key: `separator-${String(index)}` };
2523
+ }
2524
+ if (typeof entry === "string") {
2525
+ const command = commands.get(entry);
2526
+ if (command === void 0) {
2527
+ reportUnknownId(commands, entry);
2528
+ return null;
2529
+ }
2530
+ return commandItem(command, context);
2531
+ }
2532
+ return entry.type === "item" ? inlineItem(entry, context) : resolveSubmenu(entry, options);
2533
+ }
2534
+ function resolveSubmenu(entry, options) {
2535
+ const items = resolveMenu(entry.items, options);
2536
+ return items.length === 0 ? null : {
2537
+ type: "submenu",
2538
+ id: entry.id,
2539
+ label: evaluate(entry.label, options.context),
2540
+ icon: entry.icon,
2541
+ items
839
2542
  };
840
- const target = beforeColumnId === null ? without.length : without.findIndex((entry) => entry.columnId === beforeColumnId);
841
- if (target === -1) {
842
- return groupBy;
2543
+ }
2544
+ function resolveMenu(entries, options) {
2545
+ return collapseSeparators(
2546
+ entries.map((entry, index) => resolveMenuEntry(entry, index, options)).filter((entry) => entry !== null)
2547
+ );
2548
+ }
2549
+
2550
+ // src/menu/contextMenu.ts
2551
+ var defaultContextMenu = {
2552
+ header: [
2553
+ "sort.ascending",
2554
+ "sort.descending",
2555
+ "sort.clear",
2556
+ "separator",
2557
+ "group.add",
2558
+ "group.remove",
2559
+ "separator",
2560
+ "column.autoFit",
2561
+ "column.autoFitAll",
2562
+ "separator",
2563
+ "column.hide",
2564
+ "column.choose"
2565
+ ],
2566
+ cell: [
2567
+ "selection.selectAllRows",
2568
+ "selection.clear",
2569
+ "separator",
2570
+ "group.expandAll",
2571
+ "group.collapseAll"
2572
+ ],
2573
+ group: ["group.expandAll", "group.collapseAll", "separator", "group.remove"],
2574
+ body: []
2575
+ };
2576
+ function hasContextMenu(contextMenu, area) {
2577
+ if (contextMenu === void 0 || contextMenu === false) {
2578
+ return false;
843
2579
  }
844
- const next = [...without];
845
- next.splice(target, 0, moved);
846
- return next.length === groupBy.length && next.every((entry, index) => entry === groupBy[index]) ? groupBy : next;
2580
+ const entries = (contextMenu === true ? void 0 : contextMenu[area]) ?? defaultContextMenu[area];
2581
+ return typeof entries === "function" || entries.length > 0;
847
2582
  }
848
- function movesGroupBy(groupBy, columnId, beforeColumnId) {
849
- return moveGroupByBefore(groupBy, columnId, beforeColumnId) !== groupBy;
2583
+ function entriesFor(given, request) {
2584
+ if (given === void 0) {
2585
+ return request.defaults;
2586
+ }
2587
+ return typeof given === "function" ? given(request) ?? [] : given;
2588
+ }
2589
+ function contextMenuEntries(contextMenu, context) {
2590
+ if (contextMenu === void 0 || contextMenu === false) {
2591
+ return [];
2592
+ }
2593
+ const options = contextMenu === true ? {} : contextMenu;
2594
+ const { target } = context;
2595
+ switch (target.area) {
2596
+ case "header":
2597
+ return entriesFor(options.header, {
2598
+ target,
2599
+ defaults: defaultContextMenu.header,
2600
+ context
2601
+ });
2602
+ case "cell":
2603
+ return entriesFor(options.cell, {
2604
+ target,
2605
+ defaults: defaultContextMenu.cell,
2606
+ context
2607
+ });
2608
+ case "group":
2609
+ return entriesFor(options.group, {
2610
+ target,
2611
+ defaults: defaultContextMenu.group,
2612
+ context
2613
+ });
2614
+ case "body":
2615
+ return entriesFor(options.body, {
2616
+ target,
2617
+ defaults: defaultContextMenu.body,
2618
+ context
2619
+ });
2620
+ default:
2621
+ return [];
2622
+ }
850
2623
  }
851
2624
 
852
- // src/util/rows.ts
853
- function resolveShownRows(rows, filter, sort, columns) {
854
- return sortRows(filterRows(rows, filter, columns), sort, columns);
2625
+ // src/menu/navigation.ts
2626
+ function startIndex(count, from, move) {
2627
+ const focused = from >= 0 && from < count;
2628
+ switch (move) {
2629
+ case "first":
2630
+ return 0;
2631
+ case "last":
2632
+ return count - 1;
2633
+ case "next":
2634
+ return focused ? from + 1 : 0;
2635
+ case "previous":
2636
+ return focused ? from - 1 : count - 1;
2637
+ }
2638
+ }
2639
+ function nextMenuIndex(entries, from, move) {
2640
+ const count = entries.length;
2641
+ const step = move === "next" || move === "first" ? 1 : -1;
2642
+ const start = startIndex(count, from, move);
2643
+ for (let offset = 0; offset < count; offset++) {
2644
+ const index = ((start + step * offset) % count + count) % count;
2645
+ if (entries[index]?.type !== "separator") {
2646
+ return index;
2647
+ }
2648
+ }
2649
+ return -1;
855
2650
  }
856
- function resolveRows(rowsById, ids) {
857
- return ids.map((id) => rowsById.get(id)).filter((row) => row !== void 0);
2651
+ function typeaheadIndex(labels, from, query) {
2652
+ const typed = query.toLowerCase();
2653
+ const firstCodePoint = typed.codePointAt(0);
2654
+ if (firstCodePoint === void 0) {
2655
+ return -1;
2656
+ }
2657
+ const first = String.fromCodePoint(firstCodePoint);
2658
+ const repeated = typed.replaceAll(first, "") === "";
2659
+ const prefix = repeated ? first : typed;
2660
+ const start = repeated ? from + 1 : Math.max(from, 0);
2661
+ for (let offset = 0; offset < labels.length; offset++) {
2662
+ const index = (start + offset) % labels.length;
2663
+ if (labels[index]?.toLowerCase().startsWith(prefix) === true) {
2664
+ return index;
2665
+ }
2666
+ }
2667
+ return -1;
858
2668
  }
859
- function resolveColumns(columns, ids) {
860
- return ids.map((id) => {
861
- const columnIndex = columns.findIndex((entry) => entry.id === id);
862
- const column = columns[columnIndex];
863
- return column === void 0 ? void 0 : { columnId: id, column, columnIndex };
864
- }).filter((column) => column !== void 0);
2669
+
2670
+ // src/toolbar/options.ts
2671
+ var defaultToolbar = ["search", "spacer", "filter.clearAll", "column.choose"];
2672
+ function toolbarEntries(toolbar) {
2673
+ if (toolbar === void 0 || toolbar === false) {
2674
+ return [];
2675
+ }
2676
+ return (toolbar === true ? void 0 : toolbar.items) ?? defaultToolbar;
865
2677
  }
866
- function resolveCell(rowsById, columns, cell) {
867
- if (cell === null) {
868
- return null;
2678
+
2679
+ // src/toolbar/resolveToolbar.ts
2680
+ function isSpacer(entry) {
2681
+ return entry.type === "spacer";
2682
+ }
2683
+ function resolveToolbar(entries, options) {
2684
+ const { commands, contextFor } = options;
2685
+ function resolveEntry(entry, index) {
2686
+ if (entry === "search") {
2687
+ return { type: "search", key: "search" };
2688
+ }
2689
+ if (entry === "spacer") {
2690
+ return { type: "spacer", key: `spacer-${String(index)}` };
2691
+ }
2692
+ if (typeof entry === "object" && entry.type === "custom") {
2693
+ return { type: "custom", id: entry.id, key: entry.id };
2694
+ }
2695
+ if (entry === "separator") {
2696
+ return { type: "separator", key: `separator-${String(index)}` };
2697
+ }
2698
+ const resolved = resolveMenuEntry(entry, index, {
2699
+ commands,
2700
+ context: contextFor({
2701
+ area: "toolbar",
2702
+ itemId: typeof entry === "string" ? entry : entry.id
2703
+ })
2704
+ });
2705
+ if (resolved === null || resolved.type === "separator") {
2706
+ return resolved;
2707
+ }
2708
+ if (resolved.type === "submenu") {
2709
+ return { ...resolved, type: "menu", key: resolved.id };
2710
+ }
2711
+ const { checked, ...item } = resolved;
2712
+ return { ...item, type: "button", key: item.id, pressed: checked };
869
2713
  }
870
- const entry = rowsById.get(cell.rowId);
871
- const columnIndex = columns.findIndex(
872
- (candidate) => candidate.id === cell.columnId
2714
+ return collapseSeparators(
2715
+ entries.map(resolveEntry).filter((entry) => entry !== null),
2716
+ isSpacer
873
2717
  );
874
- const column = columns[columnIndex];
875
- if (entry === void 0 || column === void 0) {
876
- return null;
2718
+ }
2719
+
2720
+ // src/toolbar/overflow.ts
2721
+ function fitToolbarItems({
2722
+ widths,
2723
+ available,
2724
+ moreWidth,
2725
+ pinned
2726
+ }) {
2727
+ const total = widths.reduce((sum, width) => sum + width, 0);
2728
+ if (total <= available) {
2729
+ return widths.length;
2730
+ }
2731
+ let used = moreWidth + widths.reduce((sum, width, index) => sum + (pinned[index] ? width : 0), 0);
2732
+ for (const [index, width] of widths.entries()) {
2733
+ if (!pinned[index]) {
2734
+ used += width;
2735
+ if (used > available) {
2736
+ return index;
2737
+ }
2738
+ }
2739
+ }
2740
+ return widths.length;
2741
+ }
2742
+ function isPinnedToolbarItem(item) {
2743
+ return item.type === "search" || item.type === "custom";
2744
+ }
2745
+ function asMenuEntry(item) {
2746
+ switch (item.type) {
2747
+ case "button":
2748
+ return {
2749
+ type: "item",
2750
+ id: item.id,
2751
+ label: item.label,
2752
+ icon: item.icon,
2753
+ shortcut: item.shortcut,
2754
+ disabled: item.disabled,
2755
+ checked: item.pressed,
2756
+ run: item.run
2757
+ };
2758
+ case "menu":
2759
+ return {
2760
+ type: "submenu",
2761
+ id: item.id,
2762
+ label: item.label,
2763
+ icon: item.icon,
2764
+ items: item.items
2765
+ };
2766
+ case "separator":
2767
+ return item;
2768
+ default:
2769
+ return null;
877
2770
  }
2771
+ }
2772
+ function splitToolbarItems(items, fit) {
2773
+ const fits = (item, index) => index < fit || isPinnedToolbarItem(item);
878
2774
  return {
879
- rowId: cell.rowId,
880
- columnId: cell.columnId,
881
- row: entry.row,
882
- column,
883
- rowIndex: entry.rowIndex,
884
- columnIndex,
885
- value: accessDotted(entry.row, column.column.field)
2775
+ shown: collapseSeparators(
2776
+ items.filter(fits),
2777
+ (item) => item.type === "spacer"
2778
+ ),
2779
+ overflow: collapseSeparators(
2780
+ items.filter((item, index) => !fits(item, index)).map(asMenuEntry).filter((entry) => entry !== null)
2781
+ )
886
2782
  };
887
2783
  }
888
2784
 
889
- // src/util/paging.ts
890
- function unitsOf(rows) {
891
- const units = [];
892
- let start = 0;
893
- let openedByGroup = false;
894
- for (let index = 0; index < rows.length; index++) {
895
- const entry = rows[index];
896
- if (entry === void 0) {
897
- continue;
898
- }
899
- const isTopLevelHeader = "kind" in entry && entry.kind === "group" && entry.level === 0;
900
- const isBoundary = index !== start && (isTopLevelHeader || !openedByGroup);
901
- if (isBoundary) {
902
- units.push({ start, end: index });
903
- start = index;
904
- openedByGroup = isTopLevelHeader;
905
- } else if (index === start) {
906
- openedByGroup = isTopLevelHeader;
2785
+ // src/toolbar/search.ts
2786
+ function searchTextOf(filter) {
2787
+ const query = filterQueryFor(filter) ?? "";
2788
+ const start = query.startsWith("%") ? 1 : 0;
2789
+ const end = query.endsWith("%") ? -1 : void 0;
2790
+ return query.slice(start, end);
2791
+ }
2792
+ function setSearchText(filter, text) {
2793
+ return setColumnFilter(filter, void 0, text === "" ? "" : `%${text}%`);
2794
+ }
2795
+
2796
+ // src/util/navigation.ts
2797
+ var HEADER_ROW = -1;
2798
+ function clampFocus(focus, rowCount, columnCount2) {
2799
+ return {
2800
+ rowIndex: Math.min(Math.max(focus.rowIndex, HEADER_ROW), rowCount - 1),
2801
+ columnIndex: Math.min(
2802
+ Math.max(focus.columnIndex, 0),
2803
+ Math.max(columnCount2 - 1, 0)
2804
+ )
2805
+ };
2806
+ }
2807
+ function noRowsSkippable() {
2808
+ return false;
2809
+ }
2810
+ function skipSkippableRows(rowIndex, direction, rowCount, isSkippableRow, fallback) {
2811
+ let next = rowIndex;
2812
+ while (next >= 0 && next < rowCount && isSkippableRow(next)) {
2813
+ const stepped = next + direction;
2814
+ if (stepped < HEADER_ROW || stepped >= rowCount) {
2815
+ return fallback;
907
2816
  }
2817
+ next = stepped;
908
2818
  }
909
- if (start < rows.length) {
910
- units.push({ start, end: rows.length });
911
- }
912
- return units;
2819
+ return next;
913
2820
  }
914
- function paginateRows(rows, pagination) {
915
- const pageSize = Math.max(1, pagination.pageSize);
916
- const units = unitsOf(rows);
917
- const pageCount = Math.ceil(units.length / pageSize);
918
- if (pageCount === 0) {
919
- return { rows: [], pageCount: 0, pageIndex: 0 };
2821
+ function nextFocusForKey(key, modifiers, focus, rowCount, columnCount2, pageSize, isSkippableRow = noRowsSkippable) {
2822
+ if (modifiers.altKey) {
2823
+ return null;
920
2824
  }
921
- const pageIndex = Math.min(Math.max(pagination.pageIndex, 0), pageCount - 1);
922
- const firstUnit = units[pageIndex * pageSize];
923
- const lastUnit = units[Math.min(pageIndex * pageSize + pageSize, units.length) - 1];
924
- if (firstUnit === void 0 || lastUnit === void 0) {
925
- return { rows: [], pageCount, pageIndex };
2825
+ const { rowIndex, columnIndex } = focus;
2826
+ switch (key) {
2827
+ case "ArrowLeft":
2828
+ if (modifiers.ctrlKey) return null;
2829
+ return { rowIndex, columnIndex: columnIndex - 1 };
2830
+ case "ArrowRight":
2831
+ if (modifiers.ctrlKey) return null;
2832
+ return { rowIndex, columnIndex: columnIndex + 1 };
2833
+ case "ArrowUp":
2834
+ return {
2835
+ rowIndex: skipSkippableRows(
2836
+ rowIndex - 1,
2837
+ -1,
2838
+ rowCount,
2839
+ isSkippableRow,
2840
+ rowIndex
2841
+ ),
2842
+ columnIndex
2843
+ };
2844
+ case "ArrowDown":
2845
+ return {
2846
+ rowIndex: skipSkippableRows(
2847
+ rowIndex + 1,
2848
+ 1,
2849
+ rowCount,
2850
+ isSkippableRow,
2851
+ rowIndex
2852
+ ),
2853
+ columnIndex
2854
+ };
2855
+ // Ctrl takes Home and End to the grid's ends rather than the row's, which
2856
+ // is the one place Ctrl is navigation rather than reorder.
2857
+ case "Home":
2858
+ return modifiers.ctrlKey ? { rowIndex: HEADER_ROW, columnIndex: 0 } : { rowIndex, columnIndex: 0 };
2859
+ case "End":
2860
+ return modifiers.ctrlKey ? {
2861
+ rowIndex: skipSkippableRows(
2862
+ rowCount - 1,
2863
+ -1,
2864
+ rowCount,
2865
+ isSkippableRow,
2866
+ rowIndex
2867
+ ),
2868
+ columnIndex: columnCount2 - 1
2869
+ } : { rowIndex, columnIndex: columnCount2 - 1 };
2870
+ case "PageUp":
2871
+ return {
2872
+ rowIndex: skipSkippableRows(
2873
+ rowIndex - pageSize,
2874
+ -1,
2875
+ rowCount,
2876
+ isSkippableRow,
2877
+ rowIndex
2878
+ ),
2879
+ columnIndex
2880
+ };
2881
+ case "PageDown":
2882
+ return {
2883
+ rowIndex: skipSkippableRows(
2884
+ rowIndex + pageSize,
2885
+ 1,
2886
+ rowCount,
2887
+ isSkippableRow,
2888
+ rowIndex
2889
+ ),
2890
+ columnIndex
2891
+ };
2892
+ default:
2893
+ return null;
926
2894
  }
927
- const page = rows.slice(firstUnit.start, lastUnit.end);
928
- const renumbered = page.map(
929
- (entry, index) => entry.rowIndex === index ? entry : { ...entry, rowIndex: index }
930
- );
931
- return { rows: renumbered, pageCount, pageIndex };
932
2895
  }
933
2896
 
934
2897
  // src/util/paginationWindow.ts
@@ -966,180 +2929,522 @@ function paginationWindow(currentPage, pageCount, options) {
966
2929
  return entries;
967
2930
  }
968
2931
 
969
- // src/util/aggregation.ts
970
- function isEmpty2(value) {
971
- return value === null || value === void 0;
2932
+ // src/util/columnChooser.ts
2933
+ function isHideable(column) {
2934
+ return column.column.hideable !== false;
972
2935
  }
973
- function toNumber(value) {
974
- return Number(value);
2936
+ function matchesSearch(label, search) {
2937
+ return label.toLowerCase().includes(search.toLowerCase());
975
2938
  }
976
- function distinctKey(value) {
977
- if (isEmpty2(value)) {
978
- return "\0empty";
979
- }
980
- if (value instanceof Date) {
981
- return `\0date:${String(value.getTime())}`;
2939
+ function selectAllStateOf(states) {
2940
+ if (states.length > 0 && states.every(Boolean)) {
2941
+ return "checked";
982
2942
  }
983
- switch (typeof value) {
984
- case "string":
985
- case "number":
986
- case "boolean":
987
- case "bigint":
988
- case "symbol":
989
- return `\0${typeof value}:${String(value)}`;
990
- case "function":
991
- return "\0function";
2943
+ return states.some(Boolean) ? "mixed" : "unchecked";
2944
+ }
2945
+ function resolveColumnChooser({
2946
+ columns,
2947
+ visibility,
2948
+ search = ""
2949
+ }) {
2950
+ const all = columns.map((column) => ({
2951
+ column,
2952
+ label: resolveColumnHeaderText(column.column),
2953
+ visible: isColumnVisible(column.column, visibility)
2954
+ }));
2955
+ const shownCount = all.filter(({ visible }) => visible).length;
2956
+ const matched = all.filter(({ label }) => matchesSearch(label, search));
2957
+ const entries = matched.map(
2958
+ ({ column, label, visible }) => ({
2959
+ columnId: column.id,
2960
+ label,
2961
+ visible,
2962
+ disabled: visible && (!isHideable(column) || shownCount === 1)
2963
+ })
2964
+ );
2965
+ const covered = matched.filter(({ column }) => isHideable(column));
2966
+ const selectAll2 = selectAllStateOf(covered.map(({ visible }) => visible));
2967
+ return {
2968
+ entries,
2969
+ selectAll: selectAll2,
2970
+ toggleAll: toggleAllOf(covered, selectAll2, shownCount)
2971
+ };
2972
+ }
2973
+ function toggleAllOf(covered, selectAll2, shownCount) {
2974
+ const show = selectAll2 !== "checked";
2975
+ const changing = covered.filter(({ visible }) => visible !== show).map(({ column }) => column.id);
2976
+ const columnIds = !show && changing.length === shownCount ? changing.slice(1) : changing;
2977
+ return columnIds.length === 0 ? null : { visible: show, columnIds };
2978
+ }
2979
+
2980
+ // src/messages.ts
2981
+ function columnCount(count, change) {
2982
+ return count === 1 ? `1 column ${change}` : `${String(count)} columns ${change}`;
2983
+ }
2984
+ function searchResults(count) {
2985
+ if (count === 0) {
2986
+ return "No rows found";
2987
+ }
2988
+ return count === 1 ? "1 row found" : `${String(count)} rows found`;
2989
+ }
2990
+ function selectedCount(count, target) {
2991
+ const noun = target === "rows" ? "row" : "column";
2992
+ if (count === 0) {
2993
+ return `No ${noun}s selected`;
2994
+ }
2995
+ return count === 1 ? `1 ${noun} selected` : `${String(count)} ${noun}s selected`;
2996
+ }
2997
+ var defaultMessages = {
2998
+ pagerLabel: "Pagination",
2999
+ previousPage: "Previous",
3000
+ nextPage: "Next",
3001
+ pageSizeLabel: "Rows per page",
3002
+ pageButtonLabel: (page) => `Page ${String(page)}`,
3003
+ pageStatus: (page, pageCount) => `Page ${String(page)} of ${String(pageCount)}`,
3004
+ loadingMore: "Loading more\u2026",
3005
+ booleanTrue: "true",
3006
+ booleanFalse: "false",
3007
+ blankGroupValue: "(blank)",
3008
+ groupByBarLabel: "Grouped by",
3009
+ groupByBarPlaceholder: "Drag a column here to group by it",
3010
+ groupByChipLabel: (columnName2, level, levelCount) => levelCount > 1 ? `${columnName2}, level ${String(level)} of ${String(levelCount)}` : columnName2,
3011
+ removeGroupBy: (columnName2) => `Stop grouping by ${columnName2}`,
3012
+ columnResized: (columnName2, width) => `${columnName2}, ${String(width)} pixels wide`,
3013
+ columnMoved: (columnName2, position, columnCount2) => `${columnName2}, column ${String(position)} of ${String(columnCount2)}`,
3014
+ columnSorted: (columnName2, direction, key, keyCount) => {
3015
+ const sorted = `${columnName2} sorted ${direction === "asc" ? "ascending" : "descending"}`;
3016
+ return keyCount > 1 ? `${sorted}, key ${String(key)} of ${String(keyCount)}` : sorted;
3017
+ },
3018
+ sortCleared: (columnName2) => `${columnName2}, sort cleared`,
3019
+ groupedBy: (columnName2, level, levelCount) => levelCount > 1 ? `Grouped by ${columnName2}, level ${String(level)} of ${String(levelCount)}` : `Grouped by ${columnName2}`,
3020
+ groupingRemoved: (columnName2) => `${columnName2}, grouping removed`,
3021
+ groupExpanded: "Group expanded",
3022
+ groupCollapsed: "Group collapsed",
3023
+ allGroupsExpanded: "All groups expanded",
3024
+ allGroupsCollapsed: "All groups collapsed",
3025
+ selectedCount,
3026
+ cellSelected: (columnName2, rowNumber) => `${columnName2}, row ${String(rowNumber)}, selected`,
3027
+ columnHidden: (columnName2) => `${columnName2}, column hidden`,
3028
+ columnShown: (columnName2) => `${columnName2}, column shown`,
3029
+ columnsHidden: (count) => columnCount(count, "hidden"),
3030
+ columnsShown: (count) => columnCount(count, "shown"),
3031
+ columnsAutoFitted: (count) => columnCount(count, "autofitted"),
3032
+ headerMenuLabel: (columnName2) => `${columnName2} column menu`,
3033
+ cellMenuLabel: "Row menu",
3034
+ groupMenuLabel: "Group menu",
3035
+ bodyMenuLabel: "Grid menu",
3036
+ sortAscending: "Sort ascending",
3037
+ sortDescending: "Sort descending",
3038
+ clearSort: "Clear sort",
3039
+ clearAllSorting: "Clear all sorting",
3040
+ groupByColumn: (columnName2) => `Group by ${columnName2}`,
3041
+ ungroupAll: "Ungroup all",
3042
+ expandAllGroups: "Expand all groups",
3043
+ collapseAllGroups: "Collapse all groups",
3044
+ hideColumn: "Hide column",
3045
+ autoFitColumn: "Autofit column",
3046
+ autoFitAllColumns: "Autofit all columns",
3047
+ selectAllRows: "Select all rows",
3048
+ clearSelection: "Clear selection",
3049
+ clearFilters: "Clear filters",
3050
+ chooseColumns: "Choose columns",
3051
+ toolbarLabel: "Grid toolbar",
3052
+ searchLabel: "Search",
3053
+ searchPlaceholder: "Search\u2026",
3054
+ moreToolbarItems: "More",
3055
+ searchResults,
3056
+ columnChooserLabel: "Columns",
3057
+ columnChooserSearchLabel: "Search columns",
3058
+ showAllColumns: "Show all",
3059
+ noMatchingColumns: "No matching columns"
3060
+ };
3061
+ function resolveMessages(messages) {
3062
+ return messages === void 0 ? defaultMessages : { ...defaultMessages, ...messages };
3063
+ }
3064
+
3065
+ // src/util/format.ts
3066
+ var DATE_FIELDS = {
3067
+ year: "numeric",
3068
+ month: "numeric",
3069
+ day: "numeric"
3070
+ };
3071
+ var TIME_FIELDS = {
3072
+ hour: "numeric",
3073
+ minute: "numeric",
3074
+ second: "numeric"
3075
+ };
3076
+ var DATE_TIME_FIELDS = {
3077
+ ...DATE_FIELDS,
3078
+ ...TIME_FIELDS
3079
+ };
3080
+ var CURRENCY_DIGITS = {
3081
+ minimumFractionDigits: 2,
3082
+ maximumFractionDigits: 2
3083
+ };
3084
+ var NO_FORMAT = {};
3085
+ var formatters = /* @__PURE__ */ new WeakMap();
3086
+ function cached(format, key, create) {
3087
+ const owner = format ?? NO_FORMAT;
3088
+ let byKey = formatters.get(owner);
3089
+ if (byKey === void 0) {
3090
+ byKey = /* @__PURE__ */ new Map();
3091
+ formatters.set(owner, byKey);
3092
+ }
3093
+ const existing = byKey.get(key);
3094
+ if (existing !== void 0) {
3095
+ return existing;
3096
+ }
3097
+ const formatter = create();
3098
+ byKey.set(key, formatter);
3099
+ return formatter;
3100
+ }
3101
+ function localeKey(locale) {
3102
+ return typeof locale === "string" ? locale : locale?.join(",") ?? "";
3103
+ }
3104
+ function isNumericType(type) {
3105
+ return type === "number" || type === "decimal" || type === "currency" || type === "percent";
3106
+ }
3107
+ function numberOptions(type, format) {
3108
+ switch (type) {
3109
+ case "currency":
3110
+ return format?.currency === void 0 ? { ...CURRENCY_DIGITS, ...format } : { style: "currency", ...format };
3111
+ case "percent":
3112
+ return { style: "percent", ...format };
992
3113
  default:
993
- return `\0object:${JSON.stringify(value)}`;
3114
+ return format ?? {};
994
3115
  }
995
3116
  }
996
- function reduceBuiltIn(rows, fn, column) {
997
- if (fn === "count") {
998
- return rows.length;
3117
+ function dateOptions(type, format) {
3118
+ if (format !== void 0) {
3119
+ return format;
999
3120
  }
1000
- const field = column?.column.field;
1001
- const values = field === void 0 ? [] : rows.map((row) => accessDotted(row, field));
1002
- switch (fn) {
1003
- case "sum": {
1004
- let total = 0;
1005
- for (const value of values) {
1006
- if (isEmpty2(value)) continue;
1007
- total += toNumber(value);
1008
- }
1009
- return total;
1010
- }
1011
- case "avg": {
1012
- const present = values.filter((value) => !isEmpty2(value));
1013
- if (present.length === 0) return void 0;
1014
- const total = present.reduce(
1015
- (acc, value) => acc + toNumber(value),
1016
- 0
1017
- );
1018
- return total / present.length;
1019
- }
1020
- case "min":
1021
- case "max": {
1022
- const type = column?.column.type;
1023
- let result;
1024
- for (const value of values) {
1025
- if (isEmpty2(value)) continue;
1026
- if (result === void 0) {
1027
- result = value;
1028
- continue;
1029
- }
1030
- const comparison = compareValues(value, result, type);
1031
- if (fn === "min" ? comparison < 0 : comparison > 0) {
1032
- result = value;
1033
- }
1034
- }
1035
- return result;
1036
- }
1037
- case "countDistinct": {
1038
- const seen = /* @__PURE__ */ new Set();
1039
- for (const value of values) {
1040
- if (isEmpty2(value)) continue;
1041
- seen.add(distinctKey(value));
1042
- }
1043
- return seen.size;
3121
+ if (type === "date") return DATE_FIELDS;
3122
+ if (type === "time") return TIME_FIELDS;
3123
+ return DATE_TIME_FIELDS;
3124
+ }
3125
+ function formatNumber(value, type, format, locale) {
3126
+ const formatter = cached(
3127
+ format,
3128
+ `number|${type ?? ""}|${localeKey(locale)}`,
3129
+ () => new Intl.NumberFormat(
3130
+ locale,
3131
+ numberOptions(type, format)
3132
+ )
3133
+ );
3134
+ return formatter.format(value);
3135
+ }
3136
+ function formatDate(value, type, format, locale) {
3137
+ if (Number.isNaN(value.getTime())) {
3138
+ return String(value);
3139
+ }
3140
+ const formatter = cached(
3141
+ format,
3142
+ `date|${type ?? ""}|${localeKey(locale)}`,
3143
+ () => new Intl.DateTimeFormat(
3144
+ locale,
3145
+ dateOptions(type, format)
3146
+ )
3147
+ );
3148
+ return formatter.format(value);
3149
+ }
3150
+ function formatOther(value) {
3151
+ if (typeof value === "function") {
3152
+ return "(function)";
3153
+ }
3154
+ if (typeof value === "object" && value !== null) {
3155
+ try {
3156
+ return JSON.stringify(value);
3157
+ } catch {
3158
+ return Object.prototype.toString.call(value);
1044
3159
  }
1045
3160
  }
3161
+ return String(value);
1046
3162
  }
1047
- function keyOf(spec) {
1048
- return spec.id ?? spec.columnId;
3163
+ function formatCellValue(value, column, locale, row, messages) {
3164
+ if (column.valueFormatter !== void 0) {
3165
+ return column.valueFormatter(value, row);
3166
+ }
3167
+ if (value === null || value === void 0) {
3168
+ return "";
3169
+ }
3170
+ if (typeof value === "boolean" && messages !== void 0) {
3171
+ return value ? messages.booleanTrue : messages.booleanFalse;
3172
+ }
3173
+ const { type, format } = column;
3174
+ const formatted = format !== void 0 || locale !== void 0;
3175
+ if (value instanceof Date) {
3176
+ return formatted ? formatDate(value, type, format, locale) : value.toLocaleString();
3177
+ }
3178
+ if (formatted && (typeof value === "number" || typeof value === "bigint") && isNumericType(type)) {
3179
+ return formatNumber(value, type, format, locale);
3180
+ }
3181
+ if (typeof value === "string") {
3182
+ return value;
3183
+ }
3184
+ return formatOther(value);
1049
3185
  }
1050
- function computeAggregates(rows, specs, columns) {
1051
- const results = /* @__PURE__ */ new Map();
1052
- if (specs.length === 0) {
1053
- return results;
3186
+ function aggregateFormatColumn(value, spec, column) {
3187
+ const plain = {
3188
+ field: spec.columnId,
3189
+ type: "number"
3190
+ };
3191
+ if (column === void 0 || spec.fn === "count" || spec.fn === "countDistinct") {
3192
+ return plain;
1054
3193
  }
1055
- const byId = new Map(columns.map((entry) => [entry.id, entry]));
1056
- for (const spec of specs) {
1057
- const value = typeof spec.fn === "function" ? spec.fn(rows) : reduceBuiltIn(rows, spec.fn, byId.get(spec.columnId));
1058
- results.set(keyOf(spec), value);
3194
+ const numeric = typeof value === "number" || typeof value === "bigint";
3195
+ return column.valueFormatter === void 0 && numeric && !isNumericType(column.type) ? plain : column;
3196
+ }
3197
+ function formatAggregateValue(value, spec, column, locale) {
3198
+ const target = aggregateFormatColumn(value, spec, column);
3199
+ if (typeof value === "number" && target.valueFormatter === void 0 && target.format === void 0 && locale === void 0) {
3200
+ return value.toLocaleString();
1059
3201
  }
1060
- return results;
3202
+ return formatCellValue(value, target, locale);
1061
3203
  }
1062
- function leafDescendantsOf(flatRows, path, groupBy, byId) {
1063
- const keys = path.map((value) => bucketKey(value));
1064
- return flatRows.filter(
1065
- (row) => keys.every((key, level) => {
1066
- const columnId = groupBy[level]?.columnId;
1067
- const column = columnId === void 0 ? void 0 : byId.get(columnId);
1068
- if (column === void 0) return false;
1069
- return bucketKey(accessDotted(row.row, column.field)) === key;
1070
- })
1071
- ).map((row) => row.row);
3204
+
3205
+ // src/util/filterEditor.ts
3206
+ function touchesColumn(entry, columnId) {
3207
+ return "combinator" in entry ? entry.entries.some((nested) => touchesColumn(nested, columnId)) : entry.columnId === columnId;
1072
3208
  }
1073
- function appendAggregatedSpan(displayRows, start, levelFloor, flatRows, groupBy, byId, specs, columns, display, out) {
1074
- let index = start;
1075
- while (index < displayRows.length) {
1076
- const entry = displayRows[index];
1077
- if (entry === void 0) {
1078
- index++;
3209
+ function isOperatorOn(entry, columnId) {
3210
+ return "operator" in entry && entry.columnId === columnId;
3211
+ }
3212
+ function isConditionOn(entry, columnId) {
3213
+ return isOperatorOn(entry, columnId) && entry.operator !== "in";
3214
+ }
3215
+ function valuesShape(entry, columnId) {
3216
+ const values = (candidate) => candidate !== void 0 && isOperatorOn(candidate, columnId) && candidate.operator === "in" && isScalarList(candidate.value) ? candidate.value : void 0;
3217
+ const blanks = (candidate) => candidate !== void 0 && isOperatorOn(candidate, columnId) && candidate.operator === "isEmpty";
3218
+ const alone = values(entry);
3219
+ if (alone !== void 0) {
3220
+ return { kind: "values", values: alone, includeBlanks: false };
3221
+ }
3222
+ if (!("combinator" in entry) || entry.combinator !== "or" || entry.entries.length !== 2) {
3223
+ return void 0;
3224
+ }
3225
+ const [first, second] = entry.entries;
3226
+ const listed = blanks(first) ? values(second) : values(first);
3227
+ return listed !== void 0 && (blanks(first) || blanks(second)) ? { kind: "values", values: listed, includeBlanks: true } : void 0;
3228
+ }
3229
+ function conditionsShape(touching, columnId) {
3230
+ const [only] = touching;
3231
+ const grouped = touching.length === 1 && only !== void 0 && "combinator" in only;
3232
+ const entries = grouped ? only.entries : touching;
3233
+ const combinator = grouped ? only.combinator : "and";
3234
+ if (entries.length === 0 || entries.length > 2) {
3235
+ return void 0;
3236
+ }
3237
+ const conditions = entries.filter((entry) => isConditionOn(entry, columnId));
3238
+ return conditions.length === entries.length ? { kind: "conditions", conditions, combinator } : void 0;
3239
+ }
3240
+ function describeColumnFilter(filter, columnId) {
3241
+ const touching = filter.filter((entry) => touchesColumn(entry, columnId));
3242
+ const [only] = touching;
3243
+ if (only === void 0) {
3244
+ return { kind: "none" };
3245
+ }
3246
+ return (touching.length === 1 ? valuesShape(only, columnId) : void 0) ?? conditionsShape(touching, columnId) ?? { kind: "custom" };
3247
+ }
3248
+ function isOfferable(value) {
3249
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value instanceof Date;
3250
+ }
3251
+ function matchesSearch2(label, search) {
3252
+ return label.toLowerCase().includes(search.toLowerCase());
3253
+ }
3254
+ function columnValueOptions(rows, dataColumns, filter, columnId, query = {}) {
3255
+ const column = dataColumns.find((entry) => entry.id === columnId)?.column;
3256
+ if (column === void 0) {
3257
+ return { options: [], total: 0, truncated: false, blanks: 0 };
3258
+ }
3259
+ const keep = compileFilter(
3260
+ filter.filter((entry) => !touchesColumn(entry, columnId)),
3261
+ dataColumns
3262
+ );
3263
+ const buckets = /* @__PURE__ */ new Map();
3264
+ let blanks = 0;
3265
+ for (const row of rows) {
3266
+ if (keep !== void 0 && !keep(row)) {
1079
3267
  continue;
1080
3268
  }
1081
- if (levelFloor !== null && "kind" in entry && entry.kind === "group" && entry.level <= levelFloor) {
1082
- break;
1083
- }
1084
- index++;
1085
- if (!("kind" in entry) || entry.kind !== "group") {
1086
- out.push(entry);
1087
- continue;
3269
+ const value = accessDotted(row, column.field);
3270
+ if (isEmptyCell(value)) {
3271
+ blanks++;
3272
+ } else if (isOfferable(value)) {
3273
+ const key = filterValueKey(value, column.type);
3274
+ const bucket = buckets.get(key);
3275
+ if (bucket === void 0) {
3276
+ buckets.set(key, { value, row, count: 1 });
3277
+ } else {
3278
+ bucket.count++;
3279
+ }
1088
3280
  }
1089
- const leaves = leafDescendantsOf(flatRows, entry.path, groupBy, byId);
1090
- const aggregates = computeAggregates(leaves, specs, columns);
1091
- const withAggregates = { ...entry, aggregates };
1092
- out.push(withAggregates);
1093
- index = appendAggregatedSpan(
1094
- displayRows,
1095
- index,
1096
- entry.level,
1097
- flatRows,
1098
- groupBy,
1099
- byId,
1100
- specs,
1101
- columns,
1102
- display,
1103
- out
1104
- );
1105
- if (display === "row") {
1106
- const summary = {
1107
- kind: "group-summary",
1108
- groupId: entry.groupId,
1109
- level: entry.level,
1110
- rowIndex: -1,
1111
- datasetIndex: -1,
1112
- aggregates
3281
+ }
3282
+ const { search = "", limit, locale } = query;
3283
+ const messages = resolveMessages(query.messages);
3284
+ const options = [...buckets.values()].sort((a, b) => compareValues(a.value, b.value, column.type)).map(({ value, row, count }) => ({
3285
+ value,
3286
+ label: formatCellValue(value, column, locale, row, messages),
3287
+ count
3288
+ })).filter((option) => matchesSearch2(option.label, search));
3289
+ const shown = limit === void 0 ? options : options.slice(0, limit);
3290
+ return {
3291
+ options: shown,
3292
+ total: options.length,
3293
+ truncated: shown.length < options.length,
3294
+ blanks,
3295
+ columnType: column.type
3296
+ };
3297
+ }
3298
+ function keyOf2(draft, value) {
3299
+ return filterValueKey(value, draft.columnType);
3300
+ }
3301
+ function everyOption(draft) {
3302
+ return new Map(
3303
+ draft.options.map((option) => [keyOf2(draft, option.value), option.value])
3304
+ );
3305
+ }
3306
+ function initialFilterEditorDraft(shape, values) {
3307
+ const draft = {
3308
+ mode: "values",
3309
+ options: values.options,
3310
+ blanks: values.blanks,
3311
+ search: "",
3312
+ checked: everyOption(values),
3313
+ includeBlanks: values.blanks > 0,
3314
+ conditions: [],
3315
+ combinator: "and",
3316
+ columnType: values.columnType
3317
+ };
3318
+ switch (shape.kind) {
3319
+ case "values":
3320
+ return {
3321
+ ...draft,
3322
+ checked: new Map(
3323
+ shape.values.filter((value) => !isEmptyCell(value)).map((value) => [keyOf2(draft, value), value])
3324
+ ),
3325
+ includeBlanks: shape.includeBlanks
1113
3326
  };
1114
- out.push(summary);
3327
+ case "conditions":
3328
+ return {
3329
+ ...draft,
3330
+ mode: "conditions",
3331
+ conditions: shape.conditions,
3332
+ combinator: shape.combinator
3333
+ };
3334
+ case "none":
3335
+ case "custom":
3336
+ return draft;
3337
+ }
3338
+ }
3339
+ function visibleFilterOptions(draft) {
3340
+ return draft.search === "" ? draft.options : draft.options.filter(
3341
+ (option) => matchesSearch2(option.label, draft.search)
3342
+ );
3343
+ }
3344
+ function selectAllCoversBlanks(draft) {
3345
+ return draft.search === "" && draft.blanks > 0;
3346
+ }
3347
+ function selectAllState(draft) {
3348
+ const states = visibleFilterOptions(draft).map(
3349
+ (option) => draft.checked.has(keyOf2(draft, option.value))
3350
+ );
3351
+ if (selectAllCoversBlanks(draft)) {
3352
+ states.push(draft.includeBlanks);
3353
+ }
3354
+ if (states.length > 0 && states.every(Boolean)) {
3355
+ return "checked";
3356
+ }
3357
+ return states.some(Boolean) ? "mixed" : "unchecked";
3358
+ }
3359
+ function toggleAll(draft) {
3360
+ const check = selectAllState(draft) !== "checked";
3361
+ const checked = new Map(draft.checked);
3362
+ for (const option of visibleFilterOptions(draft)) {
3363
+ const key = keyOf2(draft, option.value);
3364
+ if (check) {
3365
+ checked.set(key, option.value);
3366
+ } else {
3367
+ checked.delete(key);
1115
3368
  }
1116
3369
  }
1117
- return index;
3370
+ return {
3371
+ ...draft,
3372
+ mode: "values",
3373
+ checked,
3374
+ includeBlanks: selectAllCoversBlanks(draft) ? check : draft.includeBlanks
3375
+ };
1118
3376
  }
1119
- function withGroupAggregates(displayRows, flatRows, groupBy, specs, columns, display = "inline") {
1120
- if (specs.length === 0) {
1121
- return displayRows;
3377
+ function filterEditorReducer(draft, action) {
3378
+ switch (action.type) {
3379
+ case "search":
3380
+ return { ...draft, search: action.search };
3381
+ case "toggleValue": {
3382
+ const key = keyOf2(draft, action.value);
3383
+ const checked = new Map(draft.checked);
3384
+ if (checked.has(key)) {
3385
+ checked.delete(key);
3386
+ } else {
3387
+ checked.set(key, action.value);
3388
+ }
3389
+ return { ...draft, mode: "values", checked };
3390
+ }
3391
+ case "toggleAll":
3392
+ return toggleAll(draft);
3393
+ case "setIncludeBlanks":
3394
+ return { ...draft, mode: "values", includeBlanks: action.includeBlanks };
3395
+ case "setConditions":
3396
+ return {
3397
+ ...draft,
3398
+ mode: "conditions",
3399
+ conditions: action.conditions,
3400
+ combinator: action.combinator ?? draft.combinator
3401
+ };
3402
+ case "clear":
3403
+ return {
3404
+ ...draft,
3405
+ search: "",
3406
+ checked: everyOption(draft),
3407
+ includeBlanks: draft.blanks > 0,
3408
+ conditions: []
3409
+ };
1122
3410
  }
1123
- const byId = new Map(columns.map((entry) => [entry.id, entry.column]));
1124
- const out = [];
1125
- appendAggregatedSpan(
1126
- displayRows,
1127
- 0,
1128
- null,
1129
- flatRows,
1130
- groupBy,
1131
- byId,
1132
- specs,
1133
- columns,
1134
- display,
1135
- out
1136
- );
1137
- return out.map((entry, position) => {
1138
- if ("kind" in entry) {
1139
- return entry.rowIndex === position && entry.datasetIndex === position ? entry : { ...entry, rowIndex: position, datasetIndex: position };
3411
+ }
3412
+ function checksEverything(draft) {
3413
+ return draft.options.every(
3414
+ (option) => draft.checked.has(keyOf2(draft, option.value))
3415
+ ) && (draft.includeBlanks || draft.blanks === 0);
3416
+ }
3417
+ function draftEntries(draft, columnId) {
3418
+ if (draft.mode === "conditions") {
3419
+ const conditions = draft.conditions.map((condition) => ({
3420
+ ...condition,
3421
+ columnId
3422
+ }));
3423
+ return conditions.length > 1 ? [{ combinator: draft.combinator, entries: conditions }] : conditions;
3424
+ }
3425
+ if (checksEverything(draft)) {
3426
+ return [];
3427
+ }
3428
+ const picked = {
3429
+ columnId,
3430
+ operator: "in",
3431
+ value: [...draft.checked.values()]
3432
+ };
3433
+ return draft.includeBlanks ? [
3434
+ {
3435
+ combinator: "or",
3436
+ entries: [picked, { columnId, operator: "isEmpty" }]
1140
3437
  }
1141
- return entry.rowIndex === position ? entry : { ...entry, rowIndex: position };
1142
- });
3438
+ ] : [picked];
3439
+ }
3440
+ function draftToFilter(draft, columnId, current) {
3441
+ const replacement = draftEntries(draft, columnId);
3442
+ const first = current.findIndex((entry) => touchesColumn(entry, columnId));
3443
+ if (first === -1) {
3444
+ return replacement.length === 0 ? current : [...current, ...replacement];
3445
+ }
3446
+ const kept = current.filter((entry) => !touchesColumn(entry, columnId));
3447
+ return [...kept.slice(0, first), ...replacement, ...kept.slice(first)];
1143
3448
  }
1144
3449
 
1145
3450
  // src/util/virtualization.ts
@@ -1190,26 +3495,46 @@ export {
1190
3495
  alignmentForType,
1191
3496
  applyColumnOrder,
1192
3497
  applyColumnResize,
3498
+ applyColumnVisibility,
1193
3499
  applySelectionIntent,
1194
3500
  beginColumnResize,
1195
3501
  buildKeyShortcuts,
3502
+ builtInCommands,
1196
3503
  clampColumnWidth,
1197
3504
  clampFocus,
1198
3505
  clearAllFilters,
1199
3506
  clearSelection,
1200
3507
  collapseAllGroups,
3508
+ columnValueOptions,
1201
3509
  compareValues,
1202
3510
  computeAggregates,
3511
+ contextMenuEntries,
3512
+ createCommandContext,
3513
+ createGridController,
3514
+ createGridViewResolver,
3515
+ defaultContextMenu,
3516
+ defaultMessages,
3517
+ defaultToolbar,
1203
3518
  defineColumnsFromRows,
3519
+ describeColumnFilter,
1204
3520
  diffSelection,
3521
+ draftToFilter,
1205
3522
  expandAllGroups,
3523
+ filterEditorReducer,
1206
3524
  filterQueryFor,
1207
3525
  filterRows,
1208
3526
  fitColumnsToWidth,
3527
+ fitToolbarItems,
3528
+ formatAggregateValue,
3529
+ formatCellValue,
1209
3530
  getColumnId,
1210
3531
  groupRowId,
1211
3532
  groupRows,
3533
+ hasContextMenu,
3534
+ initialFilterEditorDraft,
1212
3535
  intentOf,
3536
+ isColumnVisible,
3537
+ isPinnedToolbarItem,
1213
3538
  isSameCell,
1214
3539
  matchesQuery,
1215
3540
  moveColumnBefore,
@@ -1217,33 +3542,52 @@ export {
1217
3542
  movesColumn,
1218
3543
  movesGroupBy,
1219
3544
  nextFocusForKey,
3545
+ nextMenuIndex,
1220
3546
  paginateRows,
1221
3547
  paginationWindow,
1222
3548
  resolveCell,
3549
+ resolveColumnChooser,
1223
3550
  resolveColumnConstraints,
3551
+ resolveColumnHeaderText,
1224
3552
  resolveColumnLabel,
1225
3553
  resolveColumnWidths,
1226
3554
  resolveColumns,
3555
+ resolveCommands,
3556
+ resolveDataColumns,
1227
3557
  resolveDropBefore,
3558
+ resolveGridView,
1228
3559
  resolveKeyboardDropTarget,
3560
+ resolveMenu,
3561
+ resolveMessages,
1229
3562
  resolveRowId,
1230
3563
  resolveRows,
1231
3564
  resolveShownRows,
3565
+ resolveToolbar,
1232
3566
  revertColumnSize,
3567
+ runCommand,
3568
+ searchTextOf,
1233
3569
  selectAll,
3570
+ selectAllState,
1234
3571
  selectCell,
1235
3572
  selectOnly,
1236
3573
  selectRange,
3574
+ serializeGridState,
1237
3575
  setColumnFilter,
3576
+ setColumnVisibility,
3577
+ setSearchText,
1238
3578
  sizeColumnToContent,
1239
3579
  sortDirectionFor,
1240
3580
  sortPriorityFor,
1241
3581
  sortRows,
3582
+ splitToolbarItems,
1242
3583
  toggleCellSelection,
1243
3584
  toggleColumnSort,
1244
3585
  toggleGroupExpansion,
1245
3586
  toggleSelection,
3587
+ toolbarEntries,
1246
3588
  totalColumnWidth,
3589
+ typeaheadIndex,
3590
+ visibleFilterOptions,
1247
3591
  visibleRange,
1248
3592
  withGroupAggregates
1249
3593
  };