@gridkitjs/core 0.12.0 → 0.13.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
@@ -91,6 +91,9 @@ function buildKeyShortcuts(capabilities) {
91
91
  // Down rather than reusing Up: it reads as "drill into groups", and Up is
92
92
  // already sort's own mnemonic on the same header.
93
93
  capabilities.groupable ? "Alt+ArrowDown" : "",
94
+ // Excel's own key for its filter dropdown is Alt+ArrowDown, which
95
+ // grouping already holds here; Shift sets the filter apart from it.
96
+ capabilities.filterable === true ? "Alt+Shift+ArrowDown" : "",
94
97
  // The ContextMenu key needs no mention: it says what it does on its face.
95
98
  capabilities.contextMenu === true ? "Shift+F10" : ""
96
99
  ].filter(Boolean).join(" ");
@@ -2185,807 +2188,856 @@ function createCommandContext({
2185
2188
  };
2186
2189
  }
2187
2190
 
2188
- // src/commands/builtInCommands.ts
2189
- function isOn(context, areas) {
2190
- return areas.includes(context.target.area);
2191
+ // src/util/format.ts
2192
+ var DATE_FIELDS = {
2193
+ year: "numeric",
2194
+ month: "numeric",
2195
+ day: "numeric"
2196
+ };
2197
+ var TIME_FIELDS = {
2198
+ hour: "numeric",
2199
+ minute: "numeric",
2200
+ second: "numeric"
2201
+ };
2202
+ var DATE_TIME_FIELDS = {
2203
+ ...DATE_FIELDS,
2204
+ ...TIME_FIELDS
2205
+ };
2206
+ var CURRENCY_DIGITS = {
2207
+ minimumFractionDigits: 2,
2208
+ maximumFractionDigits: 2
2209
+ };
2210
+ var NO_FORMAT = {};
2211
+ var formatters = /* @__PURE__ */ new WeakMap();
2212
+ function cached(format, key, create) {
2213
+ const owner = format ?? NO_FORMAT;
2214
+ let byKey = formatters.get(owner);
2215
+ if (byKey === void 0) {
2216
+ byKey = /* @__PURE__ */ new Map();
2217
+ formatters.set(owner, byKey);
2218
+ }
2219
+ const existing = byKey.get(key);
2220
+ if (existing !== void 0) {
2221
+ return existing;
2222
+ }
2223
+ const formatter = create();
2224
+ byKey.set(key, formatter);
2225
+ return formatter;
2191
2226
  }
2192
- function columnOn(context, areas) {
2193
- return isOn(context, areas) ? context.column : null;
2227
+ function localeKey(locale) {
2228
+ return typeof locale === "string" ? locale : locale?.join(",") ?? "";
2194
2229
  }
2195
- function isGroupedBy(context, columnId) {
2196
- return context.state.groupBy.some((entry) => entry.columnId === columnId);
2230
+ function isNumericType(type) {
2231
+ return type === "number" || type === "decimal" || type === "currency" || type === "percent";
2197
2232
  }
2198
- function sortableColumn(context) {
2199
- const column = columnOn(context, ["header"]);
2200
- return column !== null && (column.column.sortable ?? context.options.sortableColumns ?? false) ? column : null;
2233
+ function numberOptions(type, format) {
2234
+ switch (type) {
2235
+ case "currency":
2236
+ return format?.currency === void 0 ? { ...CURRENCY_DIGITS, ...format } : { style: "currency", ...format };
2237
+ case "percent":
2238
+ return { style: "percent", ...format };
2239
+ default:
2240
+ return format ?? {};
2241
+ }
2201
2242
  }
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;
2243
+ function dateOptions(type, format) {
2244
+ if (format !== void 0) {
2245
+ return format;
2246
+ }
2247
+ if (type === "date") return DATE_FIELDS;
2248
+ if (type === "time") return TIME_FIELDS;
2249
+ return DATE_TIME_FIELDS;
2205
2250
  }
2206
- function groupedColumn(context) {
2207
- const column = columnOn(context, ["header", "group"]);
2208
- return column !== null && isGroupedBy(context, column.id) ? column : null;
2251
+ function formatNumber(value, type, format, locale) {
2252
+ const formatter = cached(
2253
+ format,
2254
+ `number|${type ?? ""}|${localeKey(locale)}`,
2255
+ () => new Intl.NumberFormat(
2256
+ locale,
2257
+ numberOptions(type, format)
2258
+ )
2259
+ );
2260
+ return formatter.format(value);
2209
2261
  }
2210
- function hideableColumn(context) {
2211
- const column = columnOn(context, ["header"]);
2212
- return column !== null && column.column.hideable !== false ? column : null;
2262
+ function formatDate(value, type, format, locale) {
2263
+ if (Number.isNaN(value.getTime())) {
2264
+ return String(value);
2265
+ }
2266
+ const formatter = cached(
2267
+ format,
2268
+ `date|${type ?? ""}|${localeKey(locale)}`,
2269
+ () => new Intl.DateTimeFormat(
2270
+ locale,
2271
+ dateOptions(type, format)
2272
+ )
2273
+ );
2274
+ return formatter.format(value);
2213
2275
  }
2214
- function resizableColumn(context) {
2215
- const column = columnOn(context, ["header"]);
2216
- if (column === null) {
2217
- return null;
2276
+ function formatOther(value) {
2277
+ if (typeof value === "function") {
2278
+ return "(function)";
2218
2279
  }
2219
- return context.view.columns.find(
2220
- (entry) => entry.id === column.id && entry.resizable
2221
- ) ?? null;
2280
+ if (typeof value === "object" && value !== null) {
2281
+ try {
2282
+ return JSON.stringify(value);
2283
+ } catch {
2284
+ return Object.prototype.toString.call(value);
2285
+ }
2286
+ }
2287
+ return String(value);
2222
2288
  }
2223
- function resizableColumnIds(context) {
2224
- return context.view.columns.filter((entry) => entry.resizable).map((entry) => entry.id);
2289
+ function formatCellValue(value, column, locale, row, messages) {
2290
+ if (column.valueFormatter !== void 0) {
2291
+ return column.valueFormatter(value, row);
2292
+ }
2293
+ if (value === null || value === void 0) {
2294
+ return "";
2295
+ }
2296
+ if (typeof value === "boolean" && messages !== void 0) {
2297
+ return value ? messages.booleanTrue : messages.booleanFalse;
2298
+ }
2299
+ const { type, format } = column;
2300
+ const formatted = format !== void 0 || locale !== void 0;
2301
+ if (value instanceof Date) {
2302
+ return formatted ? formatDate(value, type, format, locale) : value.toLocaleString();
2303
+ }
2304
+ if (formatted && (typeof value === "number" || typeof value === "bigint") && isNumericType(type)) {
2305
+ return formatNumber(value, type, format, locale);
2306
+ }
2307
+ if (typeof value === "string") {
2308
+ return value;
2309
+ }
2310
+ return formatOther(value);
2225
2311
  }
2226
- function isAnythingSelectable(selectable) {
2227
- return [selectable?.rows, selectable?.columns, selectable?.cells].some(
2228
- (mode) => mode !== void 0 && mode !== false
2312
+ function aggregateFormatColumn(value, spec, column) {
2313
+ const plain = {
2314
+ field: spec.columnId,
2315
+ type: "number"
2316
+ };
2317
+ if (column === void 0 || spec.fn === "count" || spec.fn === "countDistinct") {
2318
+ return plain;
2319
+ }
2320
+ const numeric = typeof value === "number" || typeof value === "bigint";
2321
+ return column.valueFormatter === void 0 && numeric && !isNumericType(column.type) ? plain : column;
2322
+ }
2323
+ function formatAggregateValue(value, spec, column, locale) {
2324
+ const target = aggregateFormatColumn(value, spec, column);
2325
+ if (typeof value === "number" && target.valueFormatter === void 0 && target.format === void 0 && locale === void 0) {
2326
+ return value.toLocaleString();
2327
+ }
2328
+ return formatCellValue(value, target, locale);
2329
+ }
2330
+ function formatGroupValue(value, column, locale, messages) {
2331
+ return value === null || value === void 0 ? messages.blankGroupValue : formatCellValue(value, column, locale, void 0, messages);
2332
+ }
2333
+
2334
+ // src/export/buildExportTable.ts
2335
+ var EMPTY_CELL = { value: null, text: "" };
2336
+ function isDataRow(entry) {
2337
+ return !("kind" in entry);
2338
+ }
2339
+ function pickColumns(view, selection) {
2340
+ let picked;
2341
+ if (selection === "visible") {
2342
+ picked = view.columns;
2343
+ } else if (selection === "all") {
2344
+ picked = view.dataColumns;
2345
+ } else {
2346
+ const byId = new Map(view.dataColumns.map((entry) => [entry.id, entry]));
2347
+ picked = [...new Set(selection)].flatMap((id) => byId.get(id) ?? []);
2348
+ }
2349
+ return picked.filter((entry) => entry.column.exportable !== false);
2350
+ }
2351
+ function describeColumns(picked, source) {
2352
+ const shown = new Map(source.view.columns.map((entry) => [entry.id, entry]));
2353
+ const resolved = resolveColumnWidths(
2354
+ picked.map((entry) => entry.column),
2355
+ source.state.columnSizing,
2356
+ { sizes: source.options.columnSizeDefaults }
2229
2357
  );
2358
+ return resolved.map((entry) => {
2359
+ const rendered = shown.get(entry.id) ?? entry;
2360
+ return {
2361
+ id: rendered.id,
2362
+ header: resolveColumnHeaderText(rendered.column),
2363
+ type: rendered.column.type ?? "string",
2364
+ format: rendered.column.format,
2365
+ alignment: rendered.alignment,
2366
+ width: rendered.width
2367
+ };
2368
+ });
2230
2369
  }
2231
- function columnName(context) {
2232
- return context.column === null ? "" : resolveColumnHeaderText(context.column.column);
2370
+ function scopedRows(source, scope) {
2371
+ const { view, state } = source;
2372
+ switch (scope) {
2373
+ case "all":
2374
+ return view.rows;
2375
+ case "page":
2376
+ return view.displayDataRows;
2377
+ case "selection": {
2378
+ const selected = new Set(state.rowSelection);
2379
+ return view.rows.filter((entry) => selected.has(entry.rowId));
2380
+ }
2381
+ }
2233
2382
  }
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);
2383
+ function typedValue(value, text) {
2384
+ if (value === null || value === void 0) {
2385
+ return null;
2386
+ }
2387
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value instanceof Date ? value : text;
2388
+ }
2389
+ function dataCell(row, column, context) {
2390
+ const raw = accessDotted(row, column.field);
2391
+ const text = formatCellValue(
2392
+ raw,
2393
+ column,
2394
+ context.locale,
2395
+ row,
2396
+ context.messages
2397
+ );
2398
+ return {
2399
+ value: column.exportValue === void 0 ? typedValue(raw, text) : column.exportValue(raw, row),
2400
+ text
2401
+ };
2402
+ }
2403
+ function aggregateCell(results, specs, column, locale) {
2404
+ const parts = specs.flatMap((spec) => {
2405
+ const value = results.get(spec.id ?? spec.columnId);
2406
+ return value === null || value === void 0 ? [] : [{ value, text: formatAggregateValue(value, spec, column, locale) }];
2407
+ });
2408
+ const [only] = parts;
2409
+ if (only === void 0) {
2410
+ return EMPTY_CELL;
2411
+ }
2412
+ if (parts.length === 1) {
2413
+ return { value: typedValue(only.value, only.text), text: only.text };
2414
+ }
2415
+ const text = parts.map((part) => part.text).join(", ");
2416
+ return { value: text, text };
2417
+ }
2418
+ function aggregateCells(results, context) {
2419
+ return context.columns.map((entry) => {
2420
+ const specs = context.specsByColumn.get(entry.id);
2421
+ return specs === void 0 ? EMPTY_CELL : aggregateCell(results, specs, entry.column, context.locale);
2422
+ });
2423
+ }
2424
+ function groupLabel(entry, source) {
2425
+ const column = source.view.dataColumns.find(
2426
+ (candidate) => candidate.id === entry.columnId
2427
+ )?.column ?? { field: entry.columnId };
2428
+ const value = formatGroupValue(
2429
+ entry.value,
2430
+ column,
2431
+ source.options.locale,
2432
+ source.messages
2433
+ );
2434
+ return `${resolveColumnHeaderText(column)}: ${value}`;
2435
+ }
2436
+ function toExportRow(entry, source, context) {
2437
+ if (isDataRow(entry)) {
2438
+ return {
2439
+ kind: "data",
2440
+ rowId: entry.rowId,
2441
+ cells: context.columns.map(
2442
+ (column) => dataCell(entry.row, column.column, context)
2443
+ )
2444
+ };
2445
+ }
2446
+ if (entry.kind === "group") {
2447
+ return {
2448
+ kind: "group",
2449
+ level: entry.level,
2450
+ groupId: entry.groupId,
2451
+ label: groupLabel(entry, source),
2452
+ count: entry.count,
2453
+ cells: context.display === "inline" ? aggregateCells(entry.aggregates, context) : context.columns.map(() => EMPTY_CELL)
2247
2454
  };
2248
2455
  }
2249
2456
  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
- }
2457
+ kind: "groupSummary",
2458
+ level: entry.level,
2459
+ cells: aggregateCells(entry.aggregates, context)
2262
2460
  };
2263
2461
  }
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 }]);
2462
+ function specsByColumnOf(specs) {
2463
+ const byColumn = /* @__PURE__ */ new Map();
2464
+ for (const spec of specs) {
2465
+ const owned = byColumn.get(spec.columnId);
2466
+ if (owned === void 0) {
2467
+ byColumn.set(spec.columnId, [spec]);
2468
+ } else {
2469
+ owned.push(spec);
2273
2470
  }
2274
- });
2471
+ }
2472
+ return byColumn;
2275
2473
  }
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
- ];
2474
+ function buildExportTable(source, options = {}) {
2475
+ const { view, state } = source;
2476
+ const specs = options.includeAggregates ?? true ? source.options.aggregates ?? [] : [];
2477
+ const columns = pickColumns(view, options.columns ?? "visible");
2478
+ const context = {
2479
+ columns,
2480
+ specsByColumn: specsByColumnOf(specs),
2481
+ display: source.options.groupAggregateDisplay ?? "inline",
2482
+ locale: source.options.locale,
2483
+ messages: source.messages
2484
+ };
2485
+ const rows = scopedRows(source, options.scope ?? "all");
2486
+ const grouped = groupRows(rows, state.groupBy, [], view.dataColumns);
2487
+ const displayRows = options.includeGroups ?? true ? withGroupAggregates(
2488
+ grouped,
2489
+ rows,
2490
+ state.groupBy,
2491
+ specs,
2492
+ view.dataColumns,
2493
+ context.display
2494
+ ) : grouped.filter(isDataRow);
2495
+ let footer = null;
2496
+ if (specs.length > 0) {
2497
+ const totals = rows === view.rows ? view.grandTotal : computeAggregates(
2498
+ rows.map((entry) => entry.row),
2499
+ specs,
2500
+ view.dataColumns
2501
+ );
2502
+ footer = { kind: "total", cells: aggregateCells(totals, context) };
2503
+ }
2504
+ return {
2505
+ columns: describeColumns(columns, source),
2506
+ header: options.includeHeader ?? true,
2507
+ rows: displayRows.map((entry) => toExportRow(entry, source, context)),
2508
+ footer
2509
+ };
2413
2510
  }
2414
2511
 
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])
2512
+ // src/export/htmlTable.ts
2513
+ var ENTITIES = {
2514
+ "&": "&",
2515
+ "<": "&lt;",
2516
+ ">": "&gt;",
2517
+ '"': "&quot;",
2518
+ "'": "&#39;"
2519
+ };
2520
+ function escapeHtml(text) {
2521
+ return text.replace(/[&<>"']/g, (char) => ENTITIES[char] ?? char);
2522
+ }
2523
+ function cellHtml(tag, text, column) {
2524
+ const alignment = column?.alignment ?? "left";
2525
+ const style = alignment === "left" ? "" : ` style="text-align:${alignment}"`;
2526
+ return `<${tag}${style}>${escapeHtml(text)}</${tag}>`;
2527
+ }
2528
+ function cellsHtml(cells, columns) {
2529
+ return cells.map((cell, index) => cellHtml("td", cell.text, columns[index])).join("");
2530
+ }
2531
+ function groupRowHtml(row, columns) {
2532
+ const firstAggregate = row.cells.findIndex((cell) => cell.text !== "");
2533
+ const span = Math.max(
2534
+ 1,
2535
+ firstAggregate === -1 ? row.cells.length : firstAggregate
2425
2536
  );
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;
2432
- }
2433
- registry.set(command.id, command);
2537
+ const colspan = span > 1 ? ` colspan="${String(span)}"` : "";
2538
+ const label = `<td${colspan}>${escapeHtml(row.label)}</td>`;
2539
+ const aggregates = row.cells.slice(span).map((cell, index) => cellHtml("td", cell.text, columns[span + index])).join("");
2540
+ return `<tr class="export-group" data-level="${String(row.level)}">${label}${aggregates}</tr>`;
2541
+ }
2542
+ function rowHtml(row, columns) {
2543
+ switch (row.kind) {
2544
+ case "data":
2545
+ return `<tr>${cellsHtml(row.cells, columns)}</tr>`;
2546
+ case "group":
2547
+ return groupRowHtml(row, columns);
2548
+ case "groupSummary":
2549
+ return `<tr class="export-group-summary" data-level="${String(row.level)}">${cellsHtml(row.cells, columns)}</tr>`;
2434
2550
  }
2435
- return registry;
2436
2551
  }
2437
- function runCommand(command, context) {
2438
- if (!isCommandVisible(command, context) || !isCommandEnabled(command, context)) {
2439
- return false;
2440
- }
2441
- command.execute(context);
2442
- return true;
2552
+ function toHtmlTable(table, options = {}) {
2553
+ const { columns } = table;
2554
+ const caption = options.caption === void 0 ? "" : `<caption>${escapeHtml(options.caption)}</caption>`;
2555
+ const head = table.header ? `<thead><tr>${columns.map((column) => cellHtml("th", column.header, column)).join("")}</tr></thead>` : "";
2556
+ const body = `<tbody>${table.rows.map((row) => rowHtml(row, columns)).join("")}</tbody>`;
2557
+ const foot = table.footer === null ? "" : `<tfoot><tr>${cellsHtml(table.footer.cells, columns)}</tr></tfoot>`;
2558
+ return `<table class="gridkit-export-table">${caption}${head}${body}${foot}</table>`;
2443
2559
  }
2444
2560
 
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);
2561
+ // src/export/records.ts
2562
+ function rawText(value) {
2563
+ if (value === null) {
2564
+ return "";
2452
2565
  }
2453
- if (reported.has(id)) {
2454
- return;
2566
+ if (value instanceof Date) {
2567
+ return Number.isNaN(value.getTime()) ? "" : value.toISOString();
2455
2568
  }
2456
- reported.add(id);
2457
- reportError(
2458
- `the menu or toolbar entry \`${id}\` names no command, so it is left out.`
2459
- );
2460
- }
2461
- function evaluate(value, context) {
2462
- return typeof value === "function" ? value(context) : value;
2569
+ return String(value);
2463
2570
  }
2464
- function commandItem(command, context) {
2465
- if (!isCommandVisible(command, context)) {
2466
- return null;
2467
- }
2571
+ function cellField(cell, values) {
2468
2572
  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)
2573
+ text: values === "raw" ? rawText(cell.value) : cell.text,
2574
+ isNumber: typeof cell.value === "number"
2477
2575
  };
2478
2576
  }
2479
- function inlineItem(entry, context) {
2480
- if (evaluate(entry.hidden ?? false, context)) {
2577
+ function textField(text) {
2578
+ return { text, isNumber: false };
2579
+ }
2580
+ function textRecords(table, values) {
2581
+ const records = [];
2582
+ if (table.header) {
2583
+ records.push(table.columns.map((column) => textField(column.header)));
2584
+ }
2585
+ for (const row of table.rows) {
2586
+ records.push(
2587
+ row.cells.map(
2588
+ (cell, index) => row.kind === "group" && index === 0 ? textField(row.label) : cellField(cell, values)
2589
+ )
2590
+ );
2591
+ }
2592
+ if (table.footer !== null) {
2593
+ records.push(table.footer.cells.map((cell) => cellField(cell, values)));
2594
+ }
2595
+ return records;
2596
+ }
2597
+
2598
+ // src/export/tsv.ts
2599
+ var SEPARATORS = /\r\n|[\t\r\n]/g;
2600
+ function toTsv(table, options = {}) {
2601
+ return textRecords(table, options.values ?? "text").map(
2602
+ (record) => record.map((field) => field.text.replace(SEPARATORS, " ")).join(" ")
2603
+ ).join("\n");
2604
+ }
2605
+
2606
+ // src/export/copy.ts
2607
+ function isCopyable(column) {
2608
+ return column.column.exportable !== false;
2609
+ }
2610
+ function hasRow(source, rowId) {
2611
+ return source.view.rows.some((entry) => entry.rowId === rowId);
2612
+ }
2613
+ function hasColumn(source, columnId) {
2614
+ return source.view.columns.some(
2615
+ (entry) => entry.id === columnId && isCopyable(entry)
2616
+ );
2617
+ }
2618
+ function selectionCopy(source) {
2619
+ const { state, view } = source;
2620
+ const selectedRows = new Set(state.rowSelection);
2621
+ const count = view.rows.filter(
2622
+ (entry) => selectedRows.has(entry.rowId)
2623
+ ).length;
2624
+ if (count > 0) {
2625
+ return { kind: "rows", count };
2626
+ }
2627
+ const selectedColumns = new Set(state.columnSelection);
2628
+ const columnIds = view.columns.filter((entry) => selectedColumns.has(entry.id) && isCopyable(entry)).map((entry) => entry.id);
2629
+ if (columnIds.length > 0 && view.rows.length > 0) {
2630
+ return { kind: "columns", columnIds };
2631
+ }
2632
+ const cell = state.cellSelection;
2633
+ if (cell !== null && hasRow(source, cell.rowId) && hasColumn(source, cell.columnId)) {
2634
+ return { kind: "cell", rowId: cell.rowId, columnId: cell.columnId };
2635
+ }
2636
+ return null;
2637
+ }
2638
+ function canCopySelection(source) {
2639
+ return selectionCopy(source) !== null;
2640
+ }
2641
+ function withRows(source, rowIds) {
2642
+ return { ...source, state: { ...source.state, rowSelection: rowIds } };
2643
+ }
2644
+ function contentOf(source, { scope, columns, header }, unit) {
2645
+ const table = buildExportTable(source, {
2646
+ scope,
2647
+ columns: columns ?? "visible",
2648
+ includeHeader: header,
2649
+ includeGroups: false,
2650
+ includeAggregates: false
2651
+ });
2652
+ if (table.columns.length === 0 || table.rows.length === 0) {
2481
2653
  return null;
2482
2654
  }
2483
- const disabled = evaluate(entry.disabled ?? false, context);
2484
2655
  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
- }
2656
+ text: toTsv(table),
2657
+ html: toHtmlTable(table),
2658
+ count: unit === "columns" ? table.columns.length : table.rows.length,
2659
+ unit
2499
2660
  };
2500
2661
  }
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
- }
2513
- }
2514
- if (collapsed.at(-1)?.type === "separator") {
2515
- collapsed.pop();
2662
+ function cellContent(source, rowId, columnId) {
2663
+ if (!hasRow(source, rowId) || !hasColumn(source, columnId)) {
2664
+ return null;
2516
2665
  }
2517
- return collapsed;
2666
+ return contentOf(
2667
+ withRows(source, [rowId]),
2668
+ { scope: "selection", columns: [columnId], header: false },
2669
+ "cells"
2670
+ );
2518
2671
  }
2519
- function resolveMenuEntry(entry, index, options) {
2520
- const { commands, context } = options;
2521
- if (entry === "separator") {
2522
- return { type: "separator", key: `separator-${String(index)}` };
2672
+ function rowContent(source, rowId) {
2673
+ if (!hasRow(source, rowId)) {
2674
+ return null;
2523
2675
  }
2524
- if (typeof entry === "string") {
2525
- const command = commands.get(entry);
2526
- if (command === void 0) {
2527
- reportUnknownId(commands, entry);
2676
+ return contentOf(
2677
+ withRows(source, [rowId]),
2678
+ { scope: "selection", header: false },
2679
+ "rows"
2680
+ );
2681
+ }
2682
+ function selectionContent(source) {
2683
+ const selection = selectionCopy(source);
2684
+ switch (selection?.kind) {
2685
+ case void 0:
2528
2686
  return null;
2529
- }
2530
- return commandItem(command, context);
2687
+ case "rows":
2688
+ return contentOf(
2689
+ source,
2690
+ { scope: "selection", header: selection.count > 1 },
2691
+ "rows"
2692
+ );
2693
+ case "columns":
2694
+ return contentOf(
2695
+ source,
2696
+ { scope: "all", columns: selection.columnIds, header: true },
2697
+ "columns"
2698
+ );
2699
+ case "cell":
2700
+ return cellContent(source, selection.rowId, selection.columnId);
2531
2701
  }
2532
- return entry.type === "item" ? inlineItem(entry, context) : resolveSubmenu(entry, options);
2533
2702
  }
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
2542
- };
2703
+ function copyContent(source, request) {
2704
+ switch (request.kind) {
2705
+ case "cell":
2706
+ return cellContent(source, request.rowId, request.columnId);
2707
+ case "row":
2708
+ return rowContent(source, request.rowId);
2709
+ case "selection":
2710
+ return selectionContent(source);
2711
+ }
2543
2712
  }
2544
- function resolveMenu(entries, options) {
2545
- return collapseSeparators(
2546
- entries.map((entry, index) => resolveMenuEntry(entry, index, options)).filter((entry) => entry !== null)
2713
+
2714
+ // src/export/exportFile.ts
2715
+ function withExtension(fileName, extension) {
2716
+ const suffix = `.${extension}`;
2717
+ return fileName.toLowerCase().endsWith(suffix.toLowerCase()) ? fileName : fileName + suffix;
2718
+ }
2719
+ function writerFor(format, writers) {
2720
+ if (typeof format !== "string") {
2721
+ return format;
2722
+ }
2723
+ const writer = writers.get(format);
2724
+ if (writer === void 0) {
2725
+ reportError(
2726
+ `there is no export writer for \`${format}\` \u2014 register one through \`exportWriters\`.`
2727
+ );
2728
+ return null;
2729
+ }
2730
+ return writer;
2731
+ }
2732
+ function fileNameFor(options, writer) {
2733
+ const { fileName = "export" } = options;
2734
+ const name = typeof fileName === "function" ? fileName(writer.format) : fileName;
2735
+ return withExtension(name, writer.fileExtension);
2736
+ }
2737
+ function exportFile(source, format, options, writers) {
2738
+ const writer = writerFor(format, writers);
2739
+ if (writer === null) {
2740
+ return null;
2741
+ }
2742
+ const content = writer.write(buildExportTable(source, options), {
2743
+ locale: source.options.locale,
2744
+ messages: source.messages,
2745
+ options
2746
+ });
2747
+ const fileName = fileNameFor(options, writer);
2748
+ const { mimeType } = writer;
2749
+ return content instanceof Promise ? content.then((resolved) => ({ fileName, mimeType, content: resolved })) : { fileName, mimeType, content };
2750
+ }
2751
+
2752
+ // src/export/print.ts
2753
+ var PORTRAIT_COLUMN_LIMIT = 6;
2754
+ function resolveOrientation(table, options) {
2755
+ return options.orientation ?? (table.columns.length > PORTRAIT_COLUMN_LIMIT ? "landscape" : "portrait");
2756
+ }
2757
+ function timestampText(timestamp, locale) {
2758
+ const date = timestamp === true ? /* @__PURE__ */ new Date() : timestamp;
2759
+ return new Intl.DateTimeFormat(locale, {
2760
+ dateStyle: "medium",
2761
+ timeStyle: "short"
2762
+ }).format(date);
2763
+ }
2764
+ function line(tag, className, text) {
2765
+ return text === void 0 || text === "" ? "" : `<${tag} class="${className}">${escapeHtml(text)}</${tag}>`;
2766
+ }
2767
+ function headerHtml(options) {
2768
+ const { timestamp = false } = options;
2769
+ const lines = line("h1", "print-title", options.title) + line("p", "print-subtitle", options.subtitle) + (timestamp === false ? "" : line("p", "print-timestamp", timestampText(timestamp, options.locale)));
2770
+ return lines === "" ? "" : `<header class="gridkit-print-header">${lines}</header>`;
2771
+ }
2772
+ function buildPrintDocument(table, options = {}) {
2773
+ const footer = line("footer", "gridkit-print-footer", options.footer);
2774
+ return {
2775
+ title: options.title ?? "",
2776
+ bodyHtml: `${headerHtml(options)}${toHtmlTable(table)}${footer}`,
2777
+ page: {
2778
+ paperSize: options.paperSize ?? "A4",
2779
+ orientation: resolveOrientation(table, options),
2780
+ pageNumbers: options.pageNumbers ?? true,
2781
+ fitToWidth: options.fitToWidth ?? false
2782
+ }
2783
+ };
2784
+ }
2785
+ function exportPrintDocument(source, options = {}) {
2786
+ return buildPrintDocument(buildExportTable(source, options), {
2787
+ ...options,
2788
+ title: options.title ?? source.options.label,
2789
+ locale: options.locale ?? source.options.locale
2790
+ });
2791
+ }
2792
+
2793
+ // src/export/csv.ts
2794
+ var BOM = "\uFEFF";
2795
+ var FORMULA_START = /^[=+\-@\t\r]/;
2796
+ var SIGN_START = /^[+-]/;
2797
+ var NEEDS_QUOTES = /["\r\n]/;
2798
+ function autoDelimiter(locale) {
2799
+ const decimal = new Intl.NumberFormat(locale).formatToParts(1.5).find((part) => part.type === "decimal")?.value;
2800
+ return decimal === "," ? ";" : ",";
2801
+ }
2802
+ function neutralize(field) {
2803
+ const { text } = field;
2804
+ if (!FORMULA_START.test(text) || field.isNumber && SIGN_START.test(text)) {
2805
+ return text;
2806
+ }
2807
+ return `'${text}`;
2808
+ }
2809
+ function quote(text, delimiter) {
2810
+ return text.includes(delimiter) || NEEDS_QUOTES.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
2811
+ }
2812
+ function toCsv(table, options = {}) {
2813
+ const delimiter = options.delimiter === "auto" ? autoDelimiter(options.locale) : options.delimiter ?? ",";
2814
+ const guard = options.neutralizeFormulas ?? true;
2815
+ const lines = textRecords(table, options.values ?? "text").map(
2816
+ (record) => record.map((field) => quote(guard ? neutralize(field) : field.text, delimiter)).join(delimiter)
2547
2817
  );
2818
+ return (options.bom ?? true ? BOM : "") + lines.join(options.eol ?? "\r\n");
2548
2819
  }
2549
2820
 
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"
2821
+ // src/export/json.ts
2822
+ function uniqueNames(names) {
2823
+ const used = /* @__PURE__ */ new Set();
2824
+ return names.map((name) => {
2825
+ let unique = name;
2826
+ for (let n = 2; used.has(unique); n += 1) {
2827
+ unique = `${name} (${String(n)})`;
2828
+ }
2829
+ used.add(unique);
2830
+ return unique;
2831
+ });
2832
+ }
2833
+ function toJson(table, options = {}) {
2834
+ const keys = options.keys === "header" ? uniqueNames(table.columns.map((column) => column.header)) : table.columns.map((column) => column.id);
2835
+ const records = table.rows.flatMap(
2836
+ (row) => row.kind === "data" ? [
2837
+ Object.fromEntries(
2838
+ keys.map((key, index) => [key, row.cells[index]?.value ?? null])
2839
+ )
2840
+ ] : []
2841
+ );
2842
+ return JSON.stringify(records, null, options.space);
2843
+ }
2844
+
2845
+ // src/export/writer.ts
2846
+ var csvWriter = {
2847
+ format: "csv",
2848
+ fileExtension: "csv",
2849
+ mimeType: "text/csv;charset=utf-8",
2850
+ write: (table, { locale, options }) => toCsv(table, { locale, ...options.csv })
2851
+ };
2852
+ var jsonWriter = {
2853
+ format: "json",
2854
+ fileExtension: "json",
2855
+ mimeType: "application/json",
2856
+ write: (table, { options }) => toJson(table, options.json)
2857
+ };
2858
+ function resolveExportWriters(writers) {
2859
+ return new Map(
2860
+ [csvWriter, jsonWriter, ...writers ?? []].map((writer) => [
2861
+ writer.format,
2862
+ writer
2863
+ ])
2864
+ );
2865
+ }
2866
+
2867
+ // src/filterMenu/operators.ts
2868
+ function filterOperatorFamily(type) {
2869
+ return kindOf(type);
2870
+ }
2871
+ var familyOperators = {
2872
+ text: [
2873
+ "contains",
2874
+ "notContains",
2875
+ "eq",
2876
+ "ne",
2877
+ "startsWith",
2878
+ "endsWith",
2879
+ "isEmpty",
2880
+ "isNotEmpty"
2565
2881
  ],
2566
- cell: [
2567
- "selection.selectAllRows",
2568
- "selection.clear",
2569
- "separator",
2570
- "group.expandAll",
2571
- "group.collapseAll"
2882
+ number: [
2883
+ "eq",
2884
+ "ne",
2885
+ "gt",
2886
+ "gte",
2887
+ "lt",
2888
+ "lte",
2889
+ "between",
2890
+ "isEmpty",
2891
+ "isNotEmpty"
2572
2892
  ],
2573
- group: ["group.expandAll", "group.collapseAll", "separator", "group.remove"],
2574
- body: []
2893
+ date: ["eq", "ne", "lt", "gt", "between", "isEmpty", "isNotEmpty"],
2894
+ boolean: ["eq", "isEmpty", "isNotEmpty"]
2575
2895
  };
2576
- function hasContextMenu(contextMenu, area) {
2577
- if (contextMenu === void 0 || contextMenu === false) {
2578
- return false;
2579
- }
2580
- const entries = (contextMenu === true ? void 0 : contextMenu[area]) ?? defaultContextMenu[area];
2581
- return typeof entries === "function" || entries.length > 0;
2896
+ var instantOperators = [
2897
+ "lt",
2898
+ "gt",
2899
+ "between",
2900
+ "isEmpty",
2901
+ "isNotEmpty"
2902
+ ];
2903
+ function defaultFilterOperators(type) {
2904
+ return type === "dateTime" || type === "time" ? instantOperators : familyOperators[filterOperatorFamily(type)];
2582
2905
  }
2583
- function entriesFor(given, request) {
2584
- if (given === void 0) {
2585
- return request.defaults;
2906
+ function filterOperandArity(operator) {
2907
+ switch (operator) {
2908
+ case "isEmpty":
2909
+ case "isNotEmpty":
2910
+ return 0;
2911
+ case "between":
2912
+ return 2;
2913
+ case "eq":
2914
+ case "ne":
2915
+ case "contains":
2916
+ case "notContains":
2917
+ case "startsWith":
2918
+ case "endsWith":
2919
+ case "gt":
2920
+ case "gte":
2921
+ case "lt":
2922
+ case "lte":
2923
+ case "in":
2924
+ return 1;
2586
2925
  }
2587
- return typeof given === "function" ? given(request) ?? [] : given;
2588
2926
  }
2589
- function contextMenuEntries(contextMenu, context) {
2590
- if (contextMenu === void 0 || contextMenu === false) {
2591
- return [];
2927
+ var NUMBER_INPUT = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i;
2928
+ var DATE_INPUT = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,3}))?)?)?$/;
2929
+ function parseNumber(text) {
2930
+ const value = Number(text);
2931
+ return NUMBER_INPUT.test(text) && Number.isFinite(value) ? value : void 0;
2932
+ }
2933
+ function parseDate(text) {
2934
+ const match = DATE_INPUT.exec(text);
2935
+ if (match === null) {
2936
+ return void 0;
2592
2937
  }
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 [];
2938
+ const [
2939
+ ,
2940
+ year,
2941
+ month,
2942
+ day,
2943
+ hours = "0",
2944
+ minutes = "0",
2945
+ seconds = "0",
2946
+ fraction = ""
2947
+ ] = match;
2948
+ if (Number(hours) > 23 || Number(minutes) > 59 || Number(seconds) > 59) {
2949
+ return void 0;
2622
2950
  }
2951
+ const date = new Date(
2952
+ Number(year),
2953
+ Number(month) - 1,
2954
+ Number(day),
2955
+ Number(hours),
2956
+ Number(minutes),
2957
+ Number(seconds),
2958
+ Number(fraction.padEnd(3, "0"))
2959
+ );
2960
+ return date.getFullYear() === Number(year) && date.getMonth() === Number(month) - 1 && date.getDate() === Number(day) ? date : void 0;
2623
2961
  }
2624
-
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;
2962
+ function parseBoolean(text) {
2963
+ if (text === "true") {
2964
+ return true;
2637
2965
  }
2966
+ return text === "false" ? false : void 0;
2638
2967
  }
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
- }
2968
+ function parseFilterOperand(text, type) {
2969
+ if (text === "") {
2970
+ return void 0;
2648
2971
  }
2649
- return -1;
2650
- }
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;
2972
+ switch (filterOperatorFamily(type)) {
2973
+ case "text":
2974
+ return text;
2975
+ case "number":
2976
+ return parseNumber(text.trim());
2977
+ case "date":
2978
+ return parseDate(text.trim());
2979
+ case "boolean":
2980
+ return parseBoolean(text.trim());
2656
2981
  }
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
- }
2982
+ }
2983
+ function pad(value, width = 2) {
2984
+ return String(value).padStart(width, "0");
2985
+ }
2986
+ function dayInput(date) {
2987
+ return `${pad(date.getFullYear(), 4)}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
2988
+ }
2989
+ function momentInput(date) {
2990
+ const minute = `${dayInput(date)}T${pad(date.getHours())}:${pad(date.getMinutes())}`;
2991
+ const seconds = date.getSeconds();
2992
+ const milliseconds = date.getMilliseconds();
2993
+ if (seconds === 0 && milliseconds === 0) {
2994
+ return minute;
2666
2995
  }
2667
- return -1;
2996
+ const second = `${minute}:${pad(seconds)}`;
2997
+ return milliseconds === 0 ? second : `${second}.${pad(milliseconds, 3)}`;
2668
2998
  }
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 [];
2999
+ function formatFilterOperand(value, type) {
3000
+ if (value === null || value === void 0) {
3001
+ return "";
2675
3002
  }
2676
- return (toolbar === true ? void 0 : toolbar.items) ?? defaultToolbar;
3003
+ const date = filterOperatorFamily(type) === "date" ? toDate(value) : void 0;
3004
+ if (date === void 0) {
3005
+ return String(value);
3006
+ }
3007
+ return type === "date" ? dayInput(date) : momentInput(date);
2677
3008
  }
2678
3009
 
2679
- // src/toolbar/resolveToolbar.ts
2680
- function isSpacer(entry) {
2681
- return entry.type === "spacer";
3010
+ // src/filterMenu/options.ts
3011
+ var DEFAULT_FILTER_VALUE_LIMIT = 1e4;
3012
+ function conditionOperators(operators) {
3013
+ return operators.includes("in") ? operators.filter((operator) => operator !== "in") : operators;
2682
3014
  }
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 };
3015
+ function resolveColumnFilterMenu(options, column) {
3016
+ if (!(column.filterable ?? options.filterableColumns ?? false)) {
3017
+ return null;
2713
3018
  }
2714
- return collapseSeparators(
2715
- entries.map(resolveEntry).filter((entry) => entry !== null),
2716
- isSpacer
2717
- );
3019
+ const grid = options.filterMenu;
3020
+ const own = column.filterMenu;
3021
+ return {
3022
+ ui: own?.ui ?? grid?.ui ?? "simple",
3023
+ operators: conditionOperators(
3024
+ own?.operators ?? defaultFilterOperators(column.type)
3025
+ ),
3026
+ family: filterOperatorFamily(column.type),
3027
+ showCounts: grid?.showCounts ?? false,
3028
+ valueLimit: grid?.valueLimit ?? DEFAULT_FILTER_VALUE_LIMIT
3029
+ };
2718
3030
  }
2719
3031
 
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;
3032
+ // src/messages.ts
3033
+ function columnCount(count, change) {
3034
+ return count === 1 ? `1 column ${change}` : `${String(count)} columns ${change}`;
2741
3035
  }
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;
2770
- }
2771
- }
2772
- function splitToolbarItems(items, fit) {
2773
- const fits = (item, index) => index < fit || isPinnedToolbarItem(item);
2774
- return {
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
- )
2782
- };
2783
- }
2784
-
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;
2816
- }
2817
- next = stepped;
2818
- }
2819
- return next;
2820
- }
2821
- function nextFocusForKey(key, modifiers, focus, rowCount, columnCount2, pageSize, isSkippableRow = noRowsSkippable) {
2822
- if (modifiers.altKey) {
2823
- return null;
2824
- }
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;
2894
- }
2895
- }
2896
-
2897
- // src/util/paginationWindow.ts
2898
- function paginationWindow(currentPage, pageCount, options) {
2899
- if (pageCount <= 0) {
2900
- return [];
2901
- }
2902
- const boundaryCount = Math.max(0, options?.boundaryCount ?? 1);
2903
- const siblingCount = Math.max(0, options?.siblingCount ?? 1);
2904
- const current = Math.min(Math.max(currentPage, 1), pageCount);
2905
- const pages = /* @__PURE__ */ new Set();
2906
- for (let page = 1; page <= Math.min(boundaryCount, pageCount); page++) {
2907
- pages.add(page);
2908
- }
2909
- for (let page = Math.max(pageCount - boundaryCount + 1, 1); page <= pageCount; page++) {
2910
- pages.add(page);
2911
- }
2912
- for (let page = Math.max(current - siblingCount, 1); page <= Math.min(current + siblingCount, pageCount); page++) {
2913
- pages.add(page);
2914
- }
2915
- const sorted = Array.from(pages).sort((a, b) => a - b);
2916
- const entries = [];
2917
- let previous;
2918
- for (const page of sorted) {
2919
- if (previous !== void 0) {
2920
- if (page - previous === 2) {
2921
- entries.push(previous + 1);
2922
- } else if (page - previous > 2) {
2923
- entries.push("ellipsis");
2924
- }
2925
- }
2926
- entries.push(page);
2927
- previous = page;
2928
- }
2929
- return entries;
2930
- }
2931
-
2932
- // src/util/columnChooser.ts
2933
- function isHideable(column) {
2934
- return column.column.hideable !== false;
2935
- }
2936
- function matchesSearch(label, search) {
2937
- return label.toLowerCase().includes(search.toLowerCase());
2938
- }
2939
- function selectAllStateOf(states) {
2940
- if (states.length > 0 && states.every(Boolean)) {
2941
- return "checked";
2942
- }
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`;
3036
+ function searchResults(count) {
3037
+ if (count === 0) {
3038
+ return "No rows found";
3039
+ }
3040
+ return count === 1 ? "1 row found" : `${String(count)} rows found`;
2989
3041
  }
2990
3042
  function selectedCount(count, target) {
2991
3043
  const noun = target === "rows" ? "row" : "column";
@@ -2994,6 +3046,52 @@ function selectedCount(count, target) {
2994
3046
  }
2995
3047
  return count === 1 ? `1 ${noun} selected` : `${String(count)} ${noun}s selected`;
2996
3048
  }
3049
+ var operatorLabels = {
3050
+ eq: "Equals",
3051
+ ne: "Does not equal",
3052
+ contains: "Contains",
3053
+ notContains: "Does not contain",
3054
+ startsWith: "Starts with",
3055
+ endsWith: "Ends with",
3056
+ gt: "Greater than",
3057
+ gte: "Greater than or equal to",
3058
+ lt: "Less than",
3059
+ lte: "Less than or equal to",
3060
+ between: "Between",
3061
+ in: "Is one of",
3062
+ isEmpty: "Is blank",
3063
+ isNotEmpty: "Is not blank"
3064
+ };
3065
+ var dateOperatorLabels = {
3066
+ lt: "Before",
3067
+ lte: "On or before",
3068
+ gt: "After",
3069
+ gte: "On or after"
3070
+ };
3071
+ function filterOperatorLabel(operator, family) {
3072
+ return (family === "date" ? dateOperatorLabels[operator] : void 0) ?? operatorLabels[operator];
3073
+ }
3074
+ function filterSearchFirst(total) {
3075
+ return total === 1 ? "Type to search 1 value" : `Type to search ${String(total)} values`;
3076
+ }
3077
+ function filterResults(shown, total) {
3078
+ return `${String(shown)} of ${String(total)} ${total === 1 ? "row" : "rows"}`;
3079
+ }
3080
+ var filterValueRequests = {
3081
+ text: "Enter a value",
3082
+ number: "Enter a number",
3083
+ date: "Enter a date",
3084
+ boolean: "Choose a value"
3085
+ };
3086
+ var filterOperatorMenus = {
3087
+ text: "Text filters",
3088
+ number: "Number filters",
3089
+ date: "Date filters",
3090
+ boolean: "Boolean filters"
3091
+ };
3092
+ function copied(count, unit) {
3093
+ return count === 1 ? `1 ${unit.slice(0, -1)} copied` : `${String(count)} ${unit} copied`;
3094
+ }
2997
3095
  var defaultMessages = {
2998
3096
  pagerLabel: "Pagination",
2999
3097
  previousPage: "Previous",
@@ -3056,152 +3154,54 @@ var defaultMessages = {
3056
3154
  columnChooserLabel: "Columns",
3057
3155
  columnChooserSearchLabel: "Search columns",
3058
3156
  showAllColumns: "Show all",
3059
- noMatchingColumns: "No matching columns"
3157
+ noMatchingColumns: "No matching columns",
3158
+ filterOperatorLabel,
3159
+ filterBlanks: "(Blanks)",
3160
+ filterSearchFirst,
3161
+ filterMoreValues: (count) => `${String(count)} more, refine the search`,
3162
+ openFilter: "Filter\u2026",
3163
+ clearColumnFilter: "Clear filter",
3164
+ filterMenuLabel: (columnName2) => `${columnName2} filter`,
3165
+ columnFiltered: "Filtered",
3166
+ applyFilter: "OK",
3167
+ cancelFilter: "Cancel",
3168
+ customFilter: "Custom filter",
3169
+ filterResults,
3170
+ filterConditionLabel: (position) => `Condition ${String(position)}`,
3171
+ filterOperatorFieldLabel: "Operator",
3172
+ filterValueLabel: "Value",
3173
+ filterFromLabel: "From",
3174
+ filterToLabel: "To",
3175
+ filterCombinatorLabel: "Combine conditions",
3176
+ filterAnd: "And",
3177
+ filterOr: "Or",
3178
+ filterValueInvalid: (family) => filterValueRequests[family],
3179
+ filterSearchLabel: "Search values",
3180
+ filterSelectAll: "Select all",
3181
+ filterValuesLabel: "Values",
3182
+ filterByCondition: "Filtered by condition",
3183
+ filterOperatorMenu: (family) => filterOperatorMenus[family],
3184
+ filterBackToValues: "Back to values",
3185
+ exportMenu: "Export",
3186
+ exportCsv: "Export to CSV",
3187
+ exportJson: "Export to JSON",
3188
+ exportXlsx: "Export to Excel",
3189
+ exportPdf: "Export to PDF",
3190
+ exportSelectionCsv: "Export selection to CSV",
3191
+ exportSelectionJson: "Export selection to JSON",
3192
+ exportSelectionXlsx: "Export selection to Excel",
3193
+ exportFailed: "Export failed",
3194
+ copyCell: "Copy cell",
3195
+ copyRow: "Copy row",
3196
+ copySelection: "Copy selection",
3197
+ copied,
3198
+ copyFailed: "Copy failed",
3199
+ print: "Print"
3060
3200
  };
3061
3201
  function resolveMessages(messages) {
3062
3202
  return messages === void 0 ? defaultMessages : { ...defaultMessages, ...messages };
3063
3203
  }
3064
3204
 
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 };
3113
- default:
3114
- return format ?? {};
3115
- }
3116
- }
3117
- function dateOptions(type, format) {
3118
- if (format !== void 0) {
3119
- return format;
3120
- }
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);
3159
- }
3160
- }
3161
- return String(value);
3162
- }
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);
3185
- }
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;
3193
- }
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();
3201
- }
3202
- return formatCellValue(value, target, locale);
3203
- }
3204
-
3205
3205
  // src/util/filterEditor.ts
3206
3206
  function touchesColumn(entry, columnId) {
3207
3207
  return "combinator" in entry ? entry.entries.some((nested) => touchesColumn(nested, columnId)) : entry.columnId === columnId;
@@ -3234,217 +3234,1321 @@ function conditionsShape(touching, columnId) {
3234
3234
  if (entries.length === 0 || entries.length > 2) {
3235
3235
  return void 0;
3236
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" };
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 isColumnFiltered(filter, columnId) {
3249
+ return filter.some((entry) => touchesColumn(entry, columnId));
3250
+ }
3251
+ function isOfferable(value) {
3252
+ return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value instanceof Date;
3253
+ }
3254
+ function matchesSearch(label, search) {
3255
+ return label.toLowerCase().includes(search.toLowerCase());
3256
+ }
3257
+ function columnValueOptions(rows, dataColumns, filter, columnId, query = {}) {
3258
+ const column = dataColumns.find((entry) => entry.id === columnId)?.column;
3259
+ if (column === void 0) {
3260
+ return { options: [], total: 0, truncated: false, blanks: 0 };
3261
+ }
3262
+ const keep = compileFilter(
3263
+ filter.filter((entry) => !touchesColumn(entry, columnId)),
3264
+ dataColumns
3265
+ );
3266
+ const buckets = /* @__PURE__ */ new Map();
3267
+ let blanks = 0;
3268
+ for (const row of rows) {
3269
+ if (keep !== void 0 && !keep(row)) {
3270
+ continue;
3271
+ }
3272
+ const value = accessDotted(row, column.field);
3273
+ if (isEmptyCell(value)) {
3274
+ blanks++;
3275
+ } else if (isOfferable(value)) {
3276
+ const key = filterValueKey(value, column.type);
3277
+ const bucket = buckets.get(key);
3278
+ if (bucket === void 0) {
3279
+ buckets.set(key, { value, row, count: 1 });
3280
+ } else {
3281
+ bucket.count++;
3282
+ }
3283
+ }
3284
+ }
3285
+ const { search = "", limit, locale } = query;
3286
+ const messages = resolveMessages(query.messages);
3287
+ const options = [...buckets.values()].sort((a, b) => compareValues(a.value, b.value, column.type)).map(({ value, row, count }) => ({
3288
+ value,
3289
+ label: formatCellValue(value, column, locale, row, messages),
3290
+ count
3291
+ })).filter((option) => matchesSearch(option.label, search));
3292
+ const shown = limit === void 0 ? options : options.slice(0, limit);
3293
+ return {
3294
+ options: shown,
3295
+ total: options.length,
3296
+ truncated: shown.length < options.length,
3297
+ blanks,
3298
+ columnType: column.type
3299
+ };
3300
+ }
3301
+ function keyOf2(draft, value) {
3302
+ return filterValueKey(value, draft.columnType);
3303
+ }
3304
+ function everyOption(draft) {
3305
+ return new Map(
3306
+ draft.options.map((option) => [keyOf2(draft, option.value), option.value])
3307
+ );
3308
+ }
3309
+ function initialFilterEditorDraft(shape, values) {
3310
+ const draft = {
3311
+ mode: "values",
3312
+ options: values.options,
3313
+ blanks: values.blanks,
3314
+ search: "",
3315
+ checked: everyOption(values),
3316
+ includeBlanks: values.blanks > 0,
3317
+ conditions: [],
3318
+ combinator: "and",
3319
+ columnType: values.columnType
3320
+ };
3321
+ switch (shape.kind) {
3322
+ case "values":
3323
+ return {
3324
+ ...draft,
3325
+ checked: new Map(
3326
+ shape.values.filter((value) => !isEmptyCell(value)).map((value) => [keyOf2(draft, value), value])
3327
+ ),
3328
+ includeBlanks: shape.includeBlanks
3329
+ };
3330
+ case "conditions":
3331
+ return {
3332
+ ...draft,
3333
+ mode: "conditions",
3334
+ conditions: shape.conditions,
3335
+ combinator: shape.combinator
3336
+ };
3337
+ case "none":
3338
+ case "custom":
3339
+ return draft;
3340
+ }
3341
+ }
3342
+ function visibleFilterOptions(draft) {
3343
+ return draft.search === "" ? draft.options : draft.options.filter(
3344
+ (option) => matchesSearch(option.label, draft.search)
3345
+ );
3346
+ }
3347
+ function selectAllCoversBlanks(draft) {
3348
+ return draft.search === "" && draft.blanks > 0;
3349
+ }
3350
+ function selectAllState(draft) {
3351
+ const states = visibleFilterOptions(draft).map(
3352
+ (option) => draft.checked.has(keyOf2(draft, option.value))
3353
+ );
3354
+ if (selectAllCoversBlanks(draft)) {
3355
+ states.push(draft.includeBlanks);
3356
+ }
3357
+ if (states.length > 0 && states.every(Boolean)) {
3358
+ return "checked";
3359
+ }
3360
+ return states.some(Boolean) ? "mixed" : "unchecked";
3361
+ }
3362
+ function toggleAll(draft) {
3363
+ const check = selectAllState(draft) !== "checked";
3364
+ const checked = new Map(draft.checked);
3365
+ for (const option of visibleFilterOptions(draft)) {
3366
+ const key = keyOf2(draft, option.value);
3367
+ if (check) {
3368
+ checked.set(key, option.value);
3369
+ } else {
3370
+ checked.delete(key);
3371
+ }
3372
+ }
3373
+ return {
3374
+ ...draft,
3375
+ mode: "values",
3376
+ checked,
3377
+ includeBlanks: selectAllCoversBlanks(draft) ? check : draft.includeBlanks
3378
+ };
3379
+ }
3380
+ function filterEditorReducer(draft, action) {
3381
+ switch (action.type) {
3382
+ case "search":
3383
+ return { ...draft, search: action.search };
3384
+ case "toggleValue": {
3385
+ const key = keyOf2(draft, action.value);
3386
+ const checked = new Map(draft.checked);
3387
+ if (checked.has(key)) {
3388
+ checked.delete(key);
3389
+ } else {
3390
+ checked.set(key, action.value);
3391
+ }
3392
+ return { ...draft, mode: "values", checked };
3393
+ }
3394
+ case "toggleAll":
3395
+ return toggleAll(draft);
3396
+ case "setIncludeBlanks":
3397
+ return { ...draft, mode: "values", includeBlanks: action.includeBlanks };
3398
+ case "setConditions":
3399
+ return {
3400
+ ...draft,
3401
+ mode: "conditions",
3402
+ conditions: action.conditions,
3403
+ combinator: action.combinator ?? draft.combinator
3404
+ };
3405
+ case "clear":
3406
+ return {
3407
+ ...draft,
3408
+ search: "",
3409
+ checked: everyOption(draft),
3410
+ includeBlanks: draft.blanks > 0,
3411
+ conditions: []
3412
+ };
3413
+ }
3414
+ }
3415
+ function checksEverything(draft) {
3416
+ return draft.options.every(
3417
+ (option) => draft.checked.has(keyOf2(draft, option.value))
3418
+ ) && (draft.includeBlanks || draft.blanks === 0);
3419
+ }
3420
+ function draftEntries(draft, columnId) {
3421
+ if (draft.mode === "conditions") {
3422
+ const conditions = draft.conditions.map((condition) => ({
3423
+ ...condition,
3424
+ columnId
3425
+ }));
3426
+ return conditions.length > 1 ? [{ combinator: draft.combinator, entries: conditions }] : conditions;
3427
+ }
3428
+ if (checksEverything(draft)) {
3429
+ return [];
3430
+ }
3431
+ const picked = {
3432
+ columnId,
3433
+ operator: "in",
3434
+ value: [...draft.checked.values()]
3435
+ };
3436
+ return draft.includeBlanks ? [
3437
+ {
3438
+ combinator: "or",
3439
+ entries: [picked, { columnId, operator: "isEmpty" }]
3440
+ }
3441
+ ] : [picked];
3442
+ }
3443
+ function draftToFilter(draft, columnId, current) {
3444
+ const replacement = draftEntries(draft, columnId);
3445
+ const first = current.findIndex((entry) => touchesColumn(entry, columnId));
3446
+ if (first === -1) {
3447
+ return replacement.length === 0 ? current : [...current, ...replacement];
3448
+ }
3449
+ const kept = current.filter((entry) => !touchesColumn(entry, columnId));
3450
+ return [...kept.slice(0, first), ...replacement, ...kept.slice(first)];
3451
+ }
3452
+
3453
+ // src/commands/builtInCommands.ts
3454
+ function isOn(context, areas) {
3455
+ return areas.includes(context.target.area);
3456
+ }
3457
+ function columnOn(context, areas) {
3458
+ return isOn(context, areas) ? context.column : null;
3459
+ }
3460
+ function isGroupedBy(context, columnId) {
3461
+ return context.state.groupBy.some((entry) => entry.columnId === columnId);
3462
+ }
3463
+ function sortableColumn(context) {
3464
+ const column = columnOn(context, ["header"]);
3465
+ return column !== null && (column.column.sortable ?? context.options.sortableColumns ?? false) ? column : null;
3466
+ }
3467
+ function groupableColumn(context) {
3468
+ const column = columnOn(context, ["header"]);
3469
+ return column !== null && (column.column.groupable ?? context.options.groupableColumns ?? false) && !isGroupedBy(context, column.id) ? column : null;
3470
+ }
3471
+ function groupedColumn(context) {
3472
+ const column = columnOn(context, ["header", "group"]);
3473
+ return column !== null && isGroupedBy(context, column.id) ? column : null;
3474
+ }
3475
+ function filterableColumn(context) {
3476
+ const column = columnOn(context, ["header"]);
3477
+ return column !== null && resolveColumnFilterMenu(context.options, column.column) !== null ? column : null;
3478
+ }
3479
+ function hideableColumn(context) {
3480
+ const column = columnOn(context, ["header"]);
3481
+ return column !== null && column.column.hideable !== false ? column : null;
3482
+ }
3483
+ function resizableColumn(context) {
3484
+ const column = columnOn(context, ["header"]);
3485
+ if (column === null) {
3486
+ return null;
3487
+ }
3488
+ return context.view.columns.find(
3489
+ (entry) => entry.id === column.id && entry.resizable
3490
+ ) ?? null;
3491
+ }
3492
+ function resizableColumnIds(context) {
3493
+ return context.view.columns.filter((entry) => entry.resizable).map((entry) => entry.id);
3494
+ }
3495
+ function isAnythingSelectable(selectable) {
3496
+ return [selectable?.rows, selectable?.columns, selectable?.cells].some(
3497
+ (mode) => mode !== void 0 && mode !== false
3498
+ );
3499
+ }
3500
+ function columnName(context) {
3501
+ return context.column === null ? "" : resolveColumnHeaderText(context.column.column);
3502
+ }
3503
+ function columnCommand({
3504
+ id,
3505
+ label,
3506
+ icon,
3507
+ find,
3508
+ isEnabled,
3509
+ isChecked,
3510
+ execute
3511
+ }) {
3512
+ function test(predicate) {
3513
+ return (context) => {
3514
+ const column = find(context);
3515
+ return column !== null && predicate(column, context);
3516
+ };
3517
+ }
3518
+ return {
3519
+ id,
3520
+ label,
3521
+ icon,
3522
+ isVisible: (context) => find(context) !== null,
3523
+ isEnabled: isEnabled === void 0 ? void 0 : test(isEnabled),
3524
+ isChecked: isChecked === void 0 ? void 0 : test(isChecked),
3525
+ execute: (context) => {
3526
+ const column = find(context);
3527
+ if (column !== null) {
3528
+ execute(column, context);
3529
+ }
3530
+ }
3531
+ };
3532
+ }
3533
+ var EXPORT_AREAS = ["grid", "toolbar", "body", "cell"];
3534
+ function exportCommand({
3535
+ id,
3536
+ format,
3537
+ label,
3538
+ selection = false
3539
+ }) {
3540
+ return {
3541
+ id,
3542
+ label: ({ messages }) => label(messages),
3543
+ isVisible: (context) => isOn(context, EXPORT_AREAS) && resolveExportWriters(context.options.exportWriters).has(format) && (!selection || context.state.rowSelection.length > 0),
3544
+ isEnabled: (context) => (selection ? context.selectedRows() : context.view.rows).length > 0,
3545
+ execute: (context) => {
3546
+ const { export: defaults = {}, exportWriters } = context.options;
3547
+ const file = exportFile(
3548
+ context,
3549
+ format,
3550
+ selection ? { ...defaults, scope: "selection" } : defaults,
3551
+ resolveExportWriters(exportWriters)
3552
+ );
3553
+ if (file !== null) {
3554
+ context.host.saveFile(file);
3555
+ }
3556
+ }
3557
+ };
3558
+ }
3559
+ function cellRequest(context, kind) {
3560
+ const { target } = context;
3561
+ if (target.area !== "cell") {
3562
+ return null;
3563
+ }
3564
+ return kind === "cell" ? { kind, rowId: target.rowId, columnId: target.columnId } : { kind, rowId: target.rowId };
3565
+ }
3566
+ function copyCommand({
3567
+ id,
3568
+ label,
3569
+ request,
3570
+ isEnabled
3571
+ }) {
3572
+ function contentFor(context) {
3573
+ const asked = request(context);
3574
+ return asked === null ? null : copyContent(context, asked);
3575
+ }
3576
+ return {
3577
+ id,
3578
+ label: ({ messages }) => label(messages),
3579
+ icon: "copy",
3580
+ isVisible: (context) => request(context) !== null,
3581
+ isEnabled: isEnabled ?? ((context) => contentFor(context) !== null),
3582
+ execute: (context) => {
3583
+ const content = contentFor(context);
3584
+ if (content !== null) {
3585
+ context.host.writeClipboard(content);
3586
+ }
3587
+ }
3588
+ };
3589
+ }
3590
+ function sortCommand(direction) {
3591
+ return columnCommand({
3592
+ id: direction === "asc" ? "sort.ascending" : "sort.descending",
3593
+ label: ({ messages }) => direction === "asc" ? messages.sortAscending : messages.sortDescending,
3594
+ icon: direction === "asc" ? "sortAscending" : "sortDescending",
3595
+ find: sortableColumn,
3596
+ isChecked: (column, { state }) => sortDirectionFor(state.columnSort, column.id) === direction,
3597
+ execute: (column, { actions }) => {
3598
+ actions.setColumnSort([{ columnId: column.id, direction }]);
3599
+ }
3600
+ });
3601
+ }
3602
+ function builtInCommands() {
3603
+ return [
3604
+ sortCommand("asc"),
3605
+ sortCommand("desc"),
3606
+ // Cleared rather than set, so the change reports this column — the one
3607
+ // the announcement names — rather than a key that moved up past it.
3608
+ columnCommand({
3609
+ id: "sort.clear",
3610
+ label: ({ messages }) => messages.clearSort,
3611
+ find: sortableColumn,
3612
+ isEnabled: (column, { state }) => sortDirectionFor(state.columnSort, column.id) !== null,
3613
+ execute: (column, { actions }) => {
3614
+ actions.clearColumnSort(column.id);
3615
+ }
3616
+ }),
3617
+ {
3618
+ id: "sort.clearAll",
3619
+ label: ({ messages }) => messages.clearAllSorting,
3620
+ isVisible: (context) => isOn(context, ["grid", "toolbar", "header"]),
3621
+ isEnabled: ({ state }) => state.columnSort.length > 0,
3622
+ execute: ({ actions }) => {
3623
+ actions.setColumnSort([]);
3624
+ }
3625
+ },
3626
+ columnCommand({
3627
+ id: "group.add",
3628
+ label: (context) => context.messages.groupByColumn(columnName(context)),
3629
+ icon: "group",
3630
+ find: groupableColumn,
3631
+ execute: (column, { actions }) => {
3632
+ actions.toggleGroupBy(column.id);
3633
+ }
3634
+ }),
3635
+ // Toggled rather than set, so the change reports this column — the one
3636
+ // the announcement names — rather than the level that moved up.
3637
+ columnCommand({
3638
+ id: "group.remove",
3639
+ label: (context) => context.messages.removeGroupBy(columnName(context)),
3640
+ icon: "group",
3641
+ find: groupedColumn,
3642
+ execute: (column, { actions }) => {
3643
+ actions.toggleGroupBy(column.id);
3644
+ }
3645
+ }),
3646
+ {
3647
+ id: "group.clearAll",
3648
+ label: ({ messages }) => messages.ungroupAll,
3649
+ isVisible: (context) => isOn(context, ["grid", "toolbar"]) && context.state.groupBy.length > 0,
3650
+ execute: ({ actions }) => {
3651
+ actions.setGroupBy([]);
3652
+ }
3653
+ },
3654
+ {
3655
+ id: "group.expandAll",
3656
+ label: ({ messages }) => messages.expandAllGroups,
3657
+ isVisible: (context) => isOn(context, ["grid", "toolbar", "group", "cell"]) && context.state.groupBy.length > 0,
3658
+ execute: ({ actions }) => {
3659
+ actions.expandAllGroups();
3660
+ }
3661
+ },
3662
+ {
3663
+ id: "group.collapseAll",
3664
+ label: ({ messages }) => messages.collapseAllGroups,
3665
+ isVisible: (context) => isOn(context, ["grid", "toolbar", "group", "cell"]) && context.state.groupBy.length > 0,
3666
+ execute: ({ actions }) => {
3667
+ actions.collapseAllGroups();
3668
+ }
3669
+ },
3670
+ columnCommand({
3671
+ id: "column.hide",
3672
+ label: ({ messages }) => messages.hideColumn,
3673
+ icon: "hide",
3674
+ find: hideableColumn,
3675
+ // The last column shown stays: a grid with none has no header left to
3676
+ // bring one back from.
3677
+ isEnabled: (column, { view }) => view.columns.some((entry) => entry.id !== column.id),
3678
+ execute: (column, { actions }) => {
3679
+ actions.hideColumns([column.id]);
3680
+ }
3681
+ }),
3682
+ columnCommand({
3683
+ id: "column.autoFit",
3684
+ label: ({ messages }) => messages.autoFitColumn,
3685
+ icon: "autoFit",
3686
+ find: resizableColumn,
3687
+ execute: (column, { host }) => {
3688
+ host.autoFitColumns([column.id]);
3689
+ }
3690
+ }),
3691
+ {
3692
+ id: "column.autoFitAll",
3693
+ label: ({ messages }) => messages.autoFitAllColumns,
3694
+ icon: "autoFit",
3695
+ isVisible: (context) => isOn(context, ["grid", "toolbar", "header"]) && resizableColumnIds(context).length > 0,
3696
+ execute: (context) => {
3697
+ context.host.autoFitColumns(resizableColumnIds(context));
3698
+ }
3699
+ },
3700
+ // The target goes along, so the renderer opens the chooser where it was
3701
+ // asked for: beside the toolbar button, below the header, or at the
3702
+ // grid's corner for a call from nowhere in particular.
3703
+ {
3704
+ id: "column.choose",
3705
+ label: ({ messages }) => messages.chooseColumns,
3706
+ isVisible: (context) => isOn(context, ["grid", "toolbar", "header"]),
3707
+ execute: ({ host, target }) => {
3708
+ host.openPanel("columnChooser", target);
3709
+ }
3710
+ },
3711
+ // Opened below the header it was asked for, as the chooser is.
3712
+ columnCommand({
3713
+ id: "filter.open",
3714
+ label: ({ messages }) => messages.openFilter,
3715
+ find: filterableColumn,
3716
+ execute: (_column, { host, target }) => {
3717
+ host.openPanel("filter", target);
3718
+ }
3719
+ }),
3720
+ // Every entry about the column goes, whichever editor wrote it — or
3721
+ // none did, as with a predicate set in code — as applying an editor
3722
+ // replaces them all.
3723
+ columnCommand({
3724
+ id: "filter.clear",
3725
+ label: ({ messages }) => messages.clearColumnFilter,
3726
+ find: filterableColumn,
3727
+ isEnabled: (column, { state }) => isColumnFiltered(state.filter, column.id),
3728
+ execute: (column, { actions, state }) => {
3729
+ actions.setFilter(
3730
+ state.filter.filter((entry) => !touchesColumn(entry, column.id))
3731
+ );
3732
+ }
3733
+ }),
3734
+ {
3735
+ id: "selection.selectAllRows",
3736
+ label: ({ messages }) => messages.selectAllRows,
3737
+ isVisible: (context) => isOn(context, ["grid", "cell"]) && context.options.selectable?.rows === "multiple",
3738
+ isEnabled: ({ view }) => view.displayDataRows.length > 0,
3739
+ execute: ({ actions }) => {
3740
+ actions.selectAllRows();
3741
+ }
3742
+ },
3743
+ {
3744
+ id: "selection.clear",
3745
+ label: ({ messages }) => messages.clearSelection,
3746
+ isVisible: (context) => isOn(context, ["grid", "cell", "header"]) && isAnythingSelectable(context.options.selectable),
3747
+ isEnabled: ({ state }) => state.rowSelection.length > 0 || state.columnSelection.length > 0 || state.cellSelection !== null,
3748
+ execute: ({ actions }) => {
3749
+ actions.clearSelection();
3750
+ }
3751
+ },
3752
+ {
3753
+ id: "filter.clearAll",
3754
+ label: ({ messages }) => messages.clearFilters,
3755
+ isVisible: (context) => isOn(context, ["grid", "toolbar", "header"]),
3756
+ isEnabled: ({ state }) => state.filter.length > 0,
3757
+ execute: ({ actions }) => {
3758
+ actions.setFilter([]);
3759
+ }
3760
+ },
3761
+ exportCommand({
3762
+ id: "export.csv",
3763
+ format: "csv",
3764
+ label: ({ exportCsv }) => exportCsv
3765
+ }),
3766
+ exportCommand({
3767
+ id: "export.json",
3768
+ format: "json",
3769
+ label: ({ exportJson }) => exportJson
3770
+ }),
3771
+ exportCommand({
3772
+ id: "export.xlsx",
3773
+ format: "xlsx",
3774
+ label: ({ exportXlsx }) => exportXlsx
3775
+ }),
3776
+ exportCommand({
3777
+ id: "export.pdf",
3778
+ format: "pdf",
3779
+ label: ({ exportPdf }) => exportPdf
3780
+ }),
3781
+ exportCommand({
3782
+ id: "exportSelection.csv",
3783
+ format: "csv",
3784
+ label: ({ exportSelectionCsv }) => exportSelectionCsv,
3785
+ selection: true
3786
+ }),
3787
+ exportCommand({
3788
+ id: "exportSelection.json",
3789
+ format: "json",
3790
+ label: ({ exportSelectionJson }) => exportSelectionJson,
3791
+ selection: true
3792
+ }),
3793
+ exportCommand({
3794
+ id: "exportSelection.xlsx",
3795
+ format: "xlsx",
3796
+ label: ({ exportSelectionXlsx }) => exportSelectionXlsx,
3797
+ selection: true
3798
+ }),
3799
+ copyCommand({
3800
+ id: "copy.cell",
3801
+ label: ({ copyCell }) => copyCell,
3802
+ request: (context) => cellRequest(context, "cell")
3803
+ }),
3804
+ copyCommand({
3805
+ id: "copy.row",
3806
+ label: ({ copyRow }) => copyRow,
3807
+ request: (context) => cellRequest(context, "row")
3808
+ }),
3809
+ // Shown wherever the grid as a whole is asked for, as the exports are,
3810
+ // once anything can be selected; enabled once something is.
3811
+ copyCommand({
3812
+ id: "copy.selection",
3813
+ label: ({ copySelection }) => copySelection,
3814
+ request: (context) => isOn(context, EXPORT_AREAS) && isAnythingSelectable(context.options.selectable) ? { kind: "selection" } : null,
3815
+ isEnabled: canCopySelection
3816
+ }),
3817
+ // Shown where the exports are, under the grid's `print` defaults. A grid
3818
+ // with no rows still prints its header; one with no columns, nothing.
3819
+ {
3820
+ id: "print",
3821
+ label: ({ messages }) => messages.print,
3822
+ icon: "print",
3823
+ isVisible: (context) => isOn(context, EXPORT_AREAS),
3824
+ isEnabled: ({ view }) => view.columns.length > 0,
3825
+ execute: (context) => {
3826
+ const { print: options = {} } = context.options;
3827
+ context.host.print(exportPrintDocument(context, options), options);
3828
+ }
3829
+ }
3830
+ ];
3831
+ }
3832
+
3833
+ // src/commands/registry.ts
3834
+ function isCommandVisible(command, context) {
3835
+ return command.isVisible?.(context) ?? true;
3836
+ }
3837
+ function isCommandEnabled(command, context) {
3838
+ return command.isEnabled?.(context) ?? true;
3839
+ }
3840
+ function resolveCommands(commands) {
3841
+ const registry = new Map(
3842
+ builtInCommands().map((command) => [command.id, command])
3843
+ );
3844
+ for (const command of commands ?? []) {
3845
+ if (command.id === "separator") {
3846
+ reportError(
3847
+ "a command cannot have the id `separator`, which a menu reads as a separator, so it is left out."
3848
+ );
3849
+ continue;
3850
+ }
3851
+ registry.set(command.id, command);
3852
+ }
3853
+ return registry;
3854
+ }
3855
+ function runCommand(command, context) {
3856
+ if (!isCommandVisible(command, context) || !isCommandEnabled(command, context)) {
3857
+ return false;
3858
+ }
3859
+ command.execute(context);
3860
+ return true;
3861
+ }
3862
+
3863
+ // src/menu/resolveMenu.ts
3864
+ var reportedUnknownIds = /* @__PURE__ */ new WeakMap();
3865
+ function reportUnknownId(commands, id) {
3866
+ let reported = reportedUnknownIds.get(commands);
3867
+ if (reported === void 0) {
3868
+ reported = /* @__PURE__ */ new Set();
3869
+ reportedUnknownIds.set(commands, reported);
3870
+ }
3871
+ if (reported.has(id)) {
3872
+ return;
3873
+ }
3874
+ reported.add(id);
3875
+ reportError(
3876
+ `the menu or toolbar entry \`${id}\` names no command, so it is left out.`
3877
+ );
3878
+ }
3879
+ function evaluate(value, context) {
3880
+ return typeof value === "function" ? value(context) : value;
3881
+ }
3882
+ function commandItem(command, context) {
3883
+ if (!isCommandVisible(command, context)) {
3884
+ return null;
3885
+ }
3886
+ return {
3887
+ type: "item",
3888
+ id: command.id,
3889
+ label: evaluate(command.label, context),
3890
+ icon: command.icon,
3891
+ shortcut: command.shortcut,
3892
+ disabled: !isCommandEnabled(command, context),
3893
+ checked: command.isChecked?.(context),
3894
+ run: () => runCommand(command, context)
3895
+ };
3896
+ }
3897
+ function inlineItem(entry, context) {
3898
+ if (evaluate(entry.hidden ?? false, context)) {
3899
+ return null;
3900
+ }
3901
+ const disabled = evaluate(entry.disabled ?? false, context);
3902
+ return {
3903
+ type: "item",
3904
+ id: entry.id,
3905
+ label: evaluate(entry.label, context),
3906
+ icon: entry.icon,
3907
+ shortcut: entry.shortcut,
3908
+ disabled,
3909
+ checked: entry.checked === void 0 ? void 0 : evaluate(entry.checked, context),
3910
+ run: () => {
3911
+ if (disabled) {
3912
+ return false;
3913
+ }
3914
+ entry.onSelect(context);
3915
+ return true;
3916
+ }
3917
+ };
3918
+ }
3919
+ function collapseSeparators(entries, isEdge = () => false) {
3920
+ const collapsed = [];
3921
+ for (const entry of entries) {
3922
+ const previous = collapsed.at(-1);
3923
+ if (entry.type !== "separator") {
3924
+ if (isEdge(entry) && previous?.type === "separator") {
3925
+ collapsed.pop();
3926
+ }
3927
+ collapsed.push(entry);
3928
+ } else if (previous !== void 0 && previous.type !== "separator" && !isEdge(previous)) {
3929
+ collapsed.push(entry);
3930
+ }
3931
+ }
3932
+ if (collapsed.at(-1)?.type === "separator") {
3933
+ collapsed.pop();
3934
+ }
3935
+ return collapsed;
3936
+ }
3937
+ function resolveMenuEntry(entry, index, options) {
3938
+ const { commands, context } = options;
3939
+ if (entry === "separator") {
3940
+ return { type: "separator", key: `separator-${String(index)}` };
3941
+ }
3942
+ if (typeof entry === "string") {
3943
+ const command = commands.get(entry);
3944
+ if (command === void 0) {
3945
+ reportUnknownId(commands, entry);
3946
+ return null;
3947
+ }
3948
+ return commandItem(command, context);
3949
+ }
3950
+ return entry.type === "item" ? inlineItem(entry, context) : resolveSubmenu(entry, options);
3951
+ }
3952
+ function resolveSubmenu(entry, options) {
3953
+ const items = resolveMenu(entry.items, options);
3954
+ return items.length === 0 ? null : {
3955
+ type: "submenu",
3956
+ id: entry.id,
3957
+ label: evaluate(entry.label, options.context),
3958
+ icon: entry.icon,
3959
+ items
3960
+ };
3961
+ }
3962
+ function resolveMenu(entries, options) {
3963
+ return collapseSeparators(
3964
+ entries.map((entry, index) => resolveMenuEntry(entry, index, options)).filter((entry) => entry !== null)
3965
+ );
3966
+ }
3967
+
3968
+ // src/menu/contextMenu.ts
3969
+ var defaultContextMenu = {
3970
+ header: [
3971
+ "sort.ascending",
3972
+ "sort.descending",
3973
+ "sort.clear",
3974
+ "separator",
3975
+ "filter.open",
3976
+ "filter.clear",
3977
+ "separator",
3978
+ "group.add",
3979
+ "group.remove",
3980
+ "separator",
3981
+ "column.autoFit",
3982
+ "column.autoFitAll",
3983
+ "separator",
3984
+ "column.hide",
3985
+ "column.choose"
3986
+ ],
3987
+ cell: [
3988
+ "copy.cell",
3989
+ "copy.row",
3990
+ "copy.selection",
3991
+ "separator",
3992
+ "selection.selectAllRows",
3993
+ "selection.clear",
3994
+ "separator",
3995
+ "group.expandAll",
3996
+ "group.collapseAll"
3997
+ ],
3998
+ group: ["group.expandAll", "group.collapseAll", "separator", "group.remove"],
3999
+ body: []
4000
+ };
4001
+ function hasContextMenu(contextMenu, area) {
4002
+ if (contextMenu === void 0 || contextMenu === false) {
4003
+ return false;
4004
+ }
4005
+ const entries = (contextMenu === true ? void 0 : contextMenu[area]) ?? defaultContextMenu[area];
4006
+ return typeof entries === "function" || entries.length > 0;
4007
+ }
4008
+ function entriesFor(given, request) {
4009
+ if (given === void 0) {
4010
+ return request.defaults;
4011
+ }
4012
+ return typeof given === "function" ? given(request) ?? [] : given;
4013
+ }
4014
+ function contextMenuEntries(contextMenu, context) {
4015
+ if (contextMenu === void 0 || contextMenu === false) {
4016
+ return [];
4017
+ }
4018
+ const options = contextMenu === true ? {} : contextMenu;
4019
+ const { target } = context;
4020
+ switch (target.area) {
4021
+ case "header":
4022
+ return entriesFor(options.header, {
4023
+ target,
4024
+ defaults: defaultContextMenu.header,
4025
+ context
4026
+ });
4027
+ case "cell":
4028
+ return entriesFor(options.cell, {
4029
+ target,
4030
+ defaults: defaultContextMenu.cell,
4031
+ context
4032
+ });
4033
+ case "group":
4034
+ return entriesFor(options.group, {
4035
+ target,
4036
+ defaults: defaultContextMenu.group,
4037
+ context
4038
+ });
4039
+ case "body":
4040
+ return entriesFor(options.body, {
4041
+ target,
4042
+ defaults: defaultContextMenu.body,
4043
+ context
4044
+ });
4045
+ default:
4046
+ return [];
4047
+ }
4048
+ }
4049
+
4050
+ // src/menu/navigation.ts
4051
+ function startIndex(count, from, move) {
4052
+ const focused = from >= 0 && from < count;
4053
+ switch (move) {
4054
+ case "first":
4055
+ return 0;
4056
+ case "last":
4057
+ return count - 1;
4058
+ case "next":
4059
+ return focused ? from + 1 : 0;
4060
+ case "previous":
4061
+ return focused ? from - 1 : count - 1;
4062
+ }
4063
+ }
4064
+ function nextMenuIndex(entries, from, move) {
4065
+ const count = entries.length;
4066
+ const step = move === "next" || move === "first" ? 1 : -1;
4067
+ const start = startIndex(count, from, move);
4068
+ for (let offset = 0; offset < count; offset++) {
4069
+ const index = ((start + step * offset) % count + count) % count;
4070
+ if (entries[index]?.type !== "separator") {
4071
+ return index;
4072
+ }
4073
+ }
4074
+ return -1;
4075
+ }
4076
+ function typeaheadIndex(labels, from, query) {
4077
+ const typed = query.toLowerCase();
4078
+ const firstCodePoint = typed.codePointAt(0);
4079
+ if (firstCodePoint === void 0) {
4080
+ return -1;
4081
+ }
4082
+ const first = String.fromCodePoint(firstCodePoint);
4083
+ const repeated = typed.replaceAll(first, "") === "";
4084
+ const prefix = repeated ? first : typed;
4085
+ const start = repeated ? from + 1 : Math.max(from, 0);
4086
+ for (let offset = 0; offset < labels.length; offset++) {
4087
+ const index = (start + offset) % labels.length;
4088
+ if (labels[index]?.toLowerCase().startsWith(prefix) === true) {
4089
+ return index;
4090
+ }
4091
+ }
4092
+ return -1;
4093
+ }
4094
+
4095
+ // src/toolbar/options.ts
4096
+ var defaultToolbar = ["search", "spacer", "filter.clearAll", "column.choose"];
4097
+ function toolbarEntries(toolbar) {
4098
+ if (toolbar === void 0 || toolbar === false) {
4099
+ return [];
4100
+ }
4101
+ return (toolbar === true ? void 0 : toolbar.items) ?? defaultToolbar;
4102
+ }
4103
+
4104
+ // src/toolbar/resolveToolbar.ts
4105
+ function isSpacer(entry) {
4106
+ return entry.type === "spacer";
4107
+ }
4108
+ function exportMenu() {
4109
+ return {
4110
+ type: "submenu",
4111
+ id: "export",
4112
+ label: ({ messages }) => messages.exportMenu,
4113
+ items: [
4114
+ "export.csv",
4115
+ "export.json",
4116
+ "export.xlsx",
4117
+ "export.pdf",
4118
+ "separator",
4119
+ "exportSelection.csv",
4120
+ "exportSelection.json",
4121
+ "exportSelection.xlsx"
4122
+ ]
4123
+ };
4124
+ }
4125
+ function resolveToolbar(entries, options) {
4126
+ const { commands, contextFor } = options;
4127
+ function resolveEntry(entry, index) {
4128
+ if (entry === "search") {
4129
+ return { type: "search", key: "search" };
4130
+ }
4131
+ if (entry === "spacer") {
4132
+ return { type: "spacer", key: `spacer-${String(index)}` };
4133
+ }
4134
+ if (typeof entry === "object" && entry.type === "custom") {
4135
+ return { type: "custom", id: entry.id, key: entry.id };
4136
+ }
4137
+ if (entry === "separator") {
4138
+ return { type: "separator", key: `separator-${String(index)}` };
4139
+ }
4140
+ const menuEntry = entry === "export" ? exportMenu() : entry;
4141
+ const resolved = resolveMenuEntry(menuEntry, index, {
4142
+ commands,
4143
+ context: contextFor({
4144
+ area: "toolbar",
4145
+ itemId: typeof menuEntry === "string" ? menuEntry : menuEntry.id
4146
+ })
4147
+ });
4148
+ if (resolved === null || resolved.type === "separator") {
4149
+ return resolved;
4150
+ }
4151
+ if (resolved.type === "submenu") {
4152
+ return { ...resolved, type: "menu", key: resolved.id };
4153
+ }
4154
+ const { checked, ...item } = resolved;
4155
+ return { ...item, type: "button", key: item.id, pressed: checked };
4156
+ }
4157
+ return collapseSeparators(
4158
+ entries.map(resolveEntry).filter((entry) => entry !== null),
4159
+ isSpacer
4160
+ );
4161
+ }
4162
+
4163
+ // src/toolbar/overflow.ts
4164
+ function fitToolbarItems({
4165
+ widths,
4166
+ available,
4167
+ moreWidth,
4168
+ pinned
4169
+ }) {
4170
+ const total = widths.reduce((sum, width) => sum + width, 0);
4171
+ if (total <= available) {
4172
+ return widths.length;
4173
+ }
4174
+ let used = moreWidth + widths.reduce((sum, width, index) => sum + (pinned[index] ? width : 0), 0);
4175
+ for (const [index, width] of widths.entries()) {
4176
+ if (!pinned[index]) {
4177
+ used += width;
4178
+ if (used > available) {
4179
+ return index;
4180
+ }
4181
+ }
4182
+ }
4183
+ return widths.length;
4184
+ }
4185
+ function isPinnedToolbarItem(item) {
4186
+ return item.type === "search" || item.type === "custom";
4187
+ }
4188
+ function asMenuEntry(item) {
4189
+ switch (item.type) {
4190
+ case "button":
4191
+ return {
4192
+ type: "item",
4193
+ id: item.id,
4194
+ label: item.label,
4195
+ icon: item.icon,
4196
+ shortcut: item.shortcut,
4197
+ disabled: item.disabled,
4198
+ checked: item.pressed,
4199
+ run: item.run
4200
+ };
4201
+ case "menu":
4202
+ return {
4203
+ type: "submenu",
4204
+ id: item.id,
4205
+ label: item.label,
4206
+ icon: item.icon,
4207
+ items: item.items
4208
+ };
4209
+ case "separator":
4210
+ return item;
4211
+ default:
4212
+ return null;
4213
+ }
4214
+ }
4215
+ function splitToolbarItems(items, fit) {
4216
+ const fits = (item, index) => index < fit || isPinnedToolbarItem(item);
4217
+ return {
4218
+ shown: collapseSeparators(
4219
+ items.filter(fits),
4220
+ (item) => item.type === "spacer"
4221
+ ),
4222
+ overflow: collapseSeparators(
4223
+ items.filter((item, index) => !fits(item, index)).map(asMenuEntry).filter((entry) => entry !== null)
4224
+ )
4225
+ };
4226
+ }
4227
+
4228
+ // src/toolbar/search.ts
4229
+ function searchTextOf(filter) {
4230
+ const query = filterQueryFor(filter) ?? "";
4231
+ const start = query.startsWith("%") ? 1 : 0;
4232
+ const end = query.endsWith("%") ? -1 : void 0;
4233
+ return query.slice(start, end);
4234
+ }
4235
+ function setSearchText(filter, text) {
4236
+ return setColumnFilter(filter, void 0, text === "" ? "" : `%${text}%`);
4237
+ }
4238
+
4239
+ // src/util/navigation.ts
4240
+ var HEADER_ROW = -1;
4241
+ function clampFocus(focus, rowCount, columnCount2) {
4242
+ return {
4243
+ rowIndex: Math.min(Math.max(focus.rowIndex, HEADER_ROW), rowCount - 1),
4244
+ columnIndex: Math.min(
4245
+ Math.max(focus.columnIndex, 0),
4246
+ Math.max(columnCount2 - 1, 0)
4247
+ )
4248
+ };
4249
+ }
4250
+ function noRowsSkippable() {
4251
+ return false;
4252
+ }
4253
+ function skipSkippableRows(rowIndex, direction, rowCount, isSkippableRow, fallback) {
4254
+ let next = rowIndex;
4255
+ while (next >= 0 && next < rowCount && isSkippableRow(next)) {
4256
+ const stepped = next + direction;
4257
+ if (stepped < HEADER_ROW || stepped >= rowCount) {
4258
+ return fallback;
4259
+ }
4260
+ next = stepped;
4261
+ }
4262
+ return next;
4263
+ }
4264
+ function nextFocusForKey(key, modifiers, focus, rowCount, columnCount2, pageSize, isSkippableRow = noRowsSkippable) {
4265
+ if (modifiers.altKey) {
4266
+ return null;
4267
+ }
4268
+ const { rowIndex, columnIndex } = focus;
4269
+ switch (key) {
4270
+ case "ArrowLeft":
4271
+ if (modifiers.ctrlKey) return null;
4272
+ return { rowIndex, columnIndex: columnIndex - 1 };
4273
+ case "ArrowRight":
4274
+ if (modifiers.ctrlKey) return null;
4275
+ return { rowIndex, columnIndex: columnIndex + 1 };
4276
+ case "ArrowUp":
4277
+ return {
4278
+ rowIndex: skipSkippableRows(
4279
+ rowIndex - 1,
4280
+ -1,
4281
+ rowCount,
4282
+ isSkippableRow,
4283
+ rowIndex
4284
+ ),
4285
+ columnIndex
4286
+ };
4287
+ case "ArrowDown":
4288
+ return {
4289
+ rowIndex: skipSkippableRows(
4290
+ rowIndex + 1,
4291
+ 1,
4292
+ rowCount,
4293
+ isSkippableRow,
4294
+ rowIndex
4295
+ ),
4296
+ columnIndex
4297
+ };
4298
+ // Ctrl takes Home and End to the grid's ends rather than the row's, which
4299
+ // is the one place Ctrl is navigation rather than reorder.
4300
+ case "Home":
4301
+ return modifiers.ctrlKey ? { rowIndex: HEADER_ROW, columnIndex: 0 } : { rowIndex, columnIndex: 0 };
4302
+ case "End":
4303
+ return modifiers.ctrlKey ? {
4304
+ rowIndex: skipSkippableRows(
4305
+ rowCount - 1,
4306
+ -1,
4307
+ rowCount,
4308
+ isSkippableRow,
4309
+ rowIndex
4310
+ ),
4311
+ columnIndex: columnCount2 - 1
4312
+ } : { rowIndex, columnIndex: columnCount2 - 1 };
4313
+ case "PageUp":
4314
+ return {
4315
+ rowIndex: skipSkippableRows(
4316
+ rowIndex - pageSize,
4317
+ -1,
4318
+ rowCount,
4319
+ isSkippableRow,
4320
+ rowIndex
4321
+ ),
4322
+ columnIndex
4323
+ };
4324
+ case "PageDown":
4325
+ return {
4326
+ rowIndex: skipSkippableRows(
4327
+ rowIndex + pageSize,
4328
+ 1,
4329
+ rowCount,
4330
+ isSkippableRow,
4331
+ rowIndex
4332
+ ),
4333
+ columnIndex
4334
+ };
4335
+ default:
4336
+ return null;
4337
+ }
4338
+ }
4339
+
4340
+ // src/util/paginationWindow.ts
4341
+ function paginationWindow(currentPage, pageCount, options) {
4342
+ if (pageCount <= 0) {
4343
+ return [];
4344
+ }
4345
+ const boundaryCount = Math.max(0, options?.boundaryCount ?? 1);
4346
+ const siblingCount = Math.max(0, options?.siblingCount ?? 1);
4347
+ const current = Math.min(Math.max(currentPage, 1), pageCount);
4348
+ const pages = /* @__PURE__ */ new Set();
4349
+ for (let page = 1; page <= Math.min(boundaryCount, pageCount); page++) {
4350
+ pages.add(page);
4351
+ }
4352
+ for (let page = Math.max(pageCount - boundaryCount + 1, 1); page <= pageCount; page++) {
4353
+ pages.add(page);
4354
+ }
4355
+ for (let page = Math.max(current - siblingCount, 1); page <= Math.min(current + siblingCount, pageCount); page++) {
4356
+ pages.add(page);
4357
+ }
4358
+ const sorted = Array.from(pages).sort((a, b) => a - b);
4359
+ const entries = [];
4360
+ let previous;
4361
+ for (const page of sorted) {
4362
+ if (previous !== void 0) {
4363
+ if (page - previous === 2) {
4364
+ entries.push(previous + 1);
4365
+ } else if (page - previous > 2) {
4366
+ entries.push("ellipsis");
4367
+ }
4368
+ }
4369
+ entries.push(page);
4370
+ previous = page;
3245
4371
  }
3246
- return (touching.length === 1 ? valuesShape(only, columnId) : void 0) ?? conditionsShape(touching, columnId) ?? { kind: "custom" };
4372
+ return entries;
3247
4373
  }
3248
- function isOfferable(value) {
3249
- return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value instanceof Date;
4374
+
4375
+ // src/util/columnChooser.ts
4376
+ function isHideable(column) {
4377
+ return column.column.hideable !== false;
3250
4378
  }
3251
4379
  function matchesSearch2(label, search) {
3252
4380
  return label.toLowerCase().includes(search.toLowerCase());
3253
4381
  }
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 };
4382
+ function selectAllStateOf(states) {
4383
+ if (states.length > 0 && states.every(Boolean)) {
4384
+ return "checked";
3258
4385
  }
3259
- const keep = compileFilter(
3260
- filter.filter((entry) => !touchesColumn(entry, columnId)),
3261
- dataColumns
4386
+ return states.some(Boolean) ? "mixed" : "unchecked";
4387
+ }
4388
+ function resolveColumnChooser({
4389
+ columns,
4390
+ visibility,
4391
+ search = ""
4392
+ }) {
4393
+ const all = columns.map((column) => ({
4394
+ column,
4395
+ label: resolveColumnHeaderText(column.column),
4396
+ visible: isColumnVisible(column.column, visibility)
4397
+ }));
4398
+ const shownCount = all.filter(({ visible }) => visible).length;
4399
+ const matched = all.filter(({ label }) => matchesSearch2(label, search));
4400
+ const entries = matched.map(
4401
+ ({ column, label, visible }) => ({
4402
+ columnId: column.id,
4403
+ label,
4404
+ visible,
4405
+ disabled: visible && (!isHideable(column) || shownCount === 1)
4406
+ })
3262
4407
  );
3263
- const buckets = /* @__PURE__ */ new Map();
3264
- let blanks = 0;
3265
- for (const row of rows) {
3266
- if (keep !== void 0 && !keep(row)) {
3267
- continue;
3268
- }
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
- }
3280
- }
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);
4408
+ const covered = matched.filter(({ column }) => isHideable(column));
4409
+ const selectAll2 = selectAllStateOf(covered.map(({ visible }) => visible));
3290
4410
  return {
3291
- options: shown,
3292
- total: options.length,
3293
- truncated: shown.length < options.length,
3294
- blanks,
3295
- columnType: column.type
4411
+ entries,
4412
+ selectAll: selectAll2,
4413
+ toggleAll: toggleAllOf(covered, selectAll2, shownCount)
3296
4414
  };
3297
4415
  }
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
- );
4416
+ function toggleAllOf(covered, selectAll2, shownCount) {
4417
+ const show = selectAll2 !== "checked";
4418
+ const changing = covered.filter(({ visible }) => visible !== show).map(({ column }) => column.id);
4419
+ const columnIds = !show && changing.length === shownCount ? changing.slice(1) : changing;
4420
+ return columnIds.length === 0 ? null : { visible: show, columnIds };
3305
4421
  }
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
3326
- };
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;
4422
+
4423
+ // src/filterMenu/conditions.ts
4424
+ var ROW_COUNT = 2;
4425
+ function rowOf(condition, type) {
4426
+ const { operator, value } = condition;
4427
+ if (isScalarList(value)) {
4428
+ const [low, high] = value;
4429
+ return {
4430
+ operator,
4431
+ text: formatFilterOperand(low, type),
4432
+ textTo: formatFilterOperand(high, type)
4433
+ };
3337
4434
  }
4435
+ return { operator, text: formatFilterOperand(value, type), textTo: "" };
3338
4436
  }
3339
- function visibleFilterOptions(draft) {
3340
- return draft.search === "" ? draft.options : draft.options.filter(
3341
- (option) => matchesSearch2(option.label, draft.search)
4437
+ function conditionRowsFrom(shape, operators, type) {
4438
+ const filled = shape.kind === "conditions" ? shape.conditions.map((condition) => rowOf(condition, type)) : [];
4439
+ const [first] = operators;
4440
+ if (first === void 0) {
4441
+ return filled;
4442
+ }
4443
+ const blank = { operator: first, text: "", textTo: "" };
4444
+ return Array.from(
4445
+ { length: ROW_COUNT },
4446
+ (_, index) => filled[index] ?? blank
3342
4447
  );
3343
4448
  }
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";
4449
+ function isEmptyRow(row) {
4450
+ switch (filterOperandArity(row.operator)) {
4451
+ case 0:
4452
+ return false;
4453
+ case 1:
4454
+ return row.text === "";
4455
+ case 2:
4456
+ return row.text === "" && row.textTo === "";
4457
+ }
4458
+ }
4459
+ function conditionOf(row, columnId, type) {
4460
+ const { operator } = row;
4461
+ switch (filterOperandArity(operator)) {
4462
+ case 0:
4463
+ return { columnId, operator };
4464
+ case 1: {
4465
+ const value = parseFilterOperand(row.text, type);
4466
+ return value === void 0 ? void 0 : { columnId, operator, value };
4467
+ }
4468
+ case 2: {
4469
+ const low = parseFilterOperand(row.text, type);
4470
+ const high = parseFilterOperand(row.textTo, type);
4471
+ return low === void 0 || high === void 0 ? void 0 : { columnId, operator, value: [low, high] };
4472
+ }
3356
4473
  }
3357
- return states.some(Boolean) ? "mixed" : "unchecked";
3358
4474
  }
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);
4475
+ function conditionsFromRows(rows, columnId, type) {
4476
+ const conditions = [];
4477
+ const invalid = [];
4478
+ for (const [index, row] of rows.entries()) {
4479
+ if (index > 0 && isEmptyRow(row)) {
4480
+ continue;
4481
+ }
4482
+ const condition = conditionOf(row, columnId, type);
4483
+ if (condition === void 0) {
4484
+ invalid.push(index);
3366
4485
  } else {
3367
- checked.delete(key);
4486
+ conditions.push(condition);
3368
4487
  }
3369
4488
  }
3370
- return {
3371
- ...draft,
3372
- mode: "values",
3373
- checked,
3374
- includeBlanks: selectAllCoversBlanks(draft) ? check : draft.includeBlanks
3375
- };
4489
+ return { conditions, invalid };
3376
4490
  }
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
- };
3410
- }
4491
+
4492
+ // src/filterMenu/valueCache.ts
4493
+ function sameSource(a, b) {
4494
+ return a.rows === b.rows && a.dataColumns === b.dataColumns && a.locale === b.locale && a.messages === b.messages;
3411
4495
  }
3412
- function checksEverything(draft) {
3413
- return draft.options.every(
3414
- (option) => draft.checked.has(keyOf2(draft, option.value))
3415
- ) && (draft.includeBlanks || draft.blanks === 0);
4496
+ function sameEntries(a, b) {
4497
+ return a.length === b.length && a.every((entry, index) => entry === b[index]);
3416
4498
  }
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" }]
4499
+ function createColumnValueOptionsCache() {
4500
+ let source;
4501
+ const lists = /* @__PURE__ */ new Map();
4502
+ return {
4503
+ get: (rows, dataColumns, filter, columnId, query = {}) => {
4504
+ const { locale, messages } = query;
4505
+ const current = { rows, dataColumns, locale, messages };
4506
+ if (source === void 0 || !sameSource(source, current)) {
4507
+ lists.clear();
4508
+ source = current;
4509
+ }
4510
+ const others = filter.filter((entry) => !touchesColumn(entry, columnId));
4511
+ const cached2 = lists.get(columnId);
4512
+ if (cached2 !== void 0 && sameEntries(cached2.others, others)) {
4513
+ return cached2.values;
4514
+ }
4515
+ const values = columnValueOptions(
4516
+ rows,
4517
+ dataColumns,
4518
+ filter,
4519
+ columnId,
4520
+ messages === void 0 ? { locale } : { locale, messages }
4521
+ );
4522
+ lists.set(columnId, { others, values });
4523
+ return values;
3437
4524
  }
3438
- ] : [picked];
4525
+ };
3439
4526
  }
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];
4527
+
4528
+ // src/filterMenu/checklist.ts
4529
+ function filterChecklist(draft, { valueLimit, blanksLabel }) {
4530
+ const visible = visibleFilterOptions(draft);
4531
+ const total = visible.length;
4532
+ if (draft.search === "" && total > valueLimit) {
4533
+ return { items: [], needsSearch: true, hiddenCount: 0, total };
4534
+ }
4535
+ const items = visible.slice(0, valueLimit).map(({ value, label, count }) => {
4536
+ const key = keyOf2(draft, value);
4537
+ const checked = draft.checked.has(key);
4538
+ return { kind: "value", key, value, label, count, checked };
4539
+ });
4540
+ const hiddenCount = total - items.length;
4541
+ if (selectAllCoversBlanks(draft)) {
4542
+ items.push({
4543
+ kind: "blanks",
4544
+ // Blank cells' own key, which no offered value has.
4545
+ key: keyOf2(draft, null),
4546
+ label: blanksLabel,
4547
+ count: draft.blanks,
4548
+ checked: draft.includeBlanks
4549
+ });
3445
4550
  }
3446
- const kept = current.filter((entry) => !touchesColumn(entry, columnId));
3447
- return [...kept.slice(0, first), ...replacement, ...kept.slice(first)];
4551
+ return { items, needsSearch: false, hiddenCount, total };
3448
4552
  }
3449
4553
 
3450
4554
  // src/util/virtualization.ts
@@ -3487,8 +4591,15 @@ function visibleRange(heights, estimatedHeight, scrollTop, viewportHeight, overs
3487
4591
  offsetAfter
3488
4592
  };
3489
4593
  }
4594
+
4595
+ // src/presets.ts
4596
+ var excelFilter = {
4597
+ filterableColumns: true,
4598
+ filterMenu: { ui: "excel", showCounts: true }
4599
+ };
3490
4600
  export {
3491
4601
  DEFAULT_COLUMN_SIZES,
4602
+ DEFAULT_FILTER_VALUE_LIMIT,
3492
4603
  HEADER_ROW,
3493
4604
  KEYBOARD_STEP,
3494
4605
  accessDotted,
@@ -3498,7 +4609,9 @@ export {
3498
4609
  applyColumnVisibility,
3499
4610
  applySelectionIntent,
3500
4611
  beginColumnResize,
4612
+ buildExportTable,
3501
4613
  buildKeyShortcuts,
4614
+ buildPrintDocument,
3502
4615
  builtInCommands,
3503
4616
  clampColumnWidth,
3504
4617
  clampFocus,
@@ -3508,34 +4621,50 @@ export {
3508
4621
  columnValueOptions,
3509
4622
  compareValues,
3510
4623
  computeAggregates,
4624
+ conditionRowsFrom,
4625
+ conditionsFromRows,
3511
4626
  contextMenuEntries,
4627
+ copyContent,
4628
+ createColumnValueOptionsCache,
3512
4629
  createCommandContext,
3513
4630
  createGridController,
3514
4631
  createGridViewResolver,
4632
+ csvWriter,
3515
4633
  defaultContextMenu,
4634
+ defaultFilterOperators,
3516
4635
  defaultMessages,
3517
4636
  defaultToolbar,
3518
4637
  defineColumnsFromRows,
3519
4638
  describeColumnFilter,
3520
4639
  diffSelection,
3521
4640
  draftToFilter,
4641
+ excelFilter,
3522
4642
  expandAllGroups,
4643
+ exportFile,
4644
+ exportPrintDocument,
4645
+ filterChecklist,
3523
4646
  filterEditorReducer,
4647
+ filterOperandArity,
4648
+ filterOperatorFamily,
3524
4649
  filterQueryFor,
3525
4650
  filterRows,
3526
4651
  fitColumnsToWidth,
3527
4652
  fitToolbarItems,
3528
4653
  formatAggregateValue,
3529
4654
  formatCellValue,
4655
+ formatFilterOperand,
4656
+ formatGroupValue,
3530
4657
  getColumnId,
3531
4658
  groupRowId,
3532
4659
  groupRows,
3533
4660
  hasContextMenu,
3534
4661
  initialFilterEditorDraft,
3535
4662
  intentOf,
4663
+ isColumnFiltered,
3536
4664
  isColumnVisible,
3537
4665
  isPinnedToolbarItem,
3538
4666
  isSameCell,
4667
+ jsonWriter,
3539
4668
  matchesQuery,
3540
4669
  moveColumnBefore,
3541
4670
  moveGroupByBefore,
@@ -3545,9 +4674,11 @@ export {
3545
4674
  nextMenuIndex,
3546
4675
  paginateRows,
3547
4676
  paginationWindow,
4677
+ parseFilterOperand,
3548
4678
  resolveCell,
3549
4679
  resolveColumnChooser,
3550
4680
  resolveColumnConstraints,
4681
+ resolveColumnFilterMenu,
3551
4682
  resolveColumnHeaderText,
3552
4683
  resolveColumnLabel,
3553
4684
  resolveColumnWidths,
@@ -3555,6 +4686,7 @@ export {
3555
4686
  resolveCommands,
3556
4687
  resolveDataColumns,
3557
4688
  resolveDropBefore,
4689
+ resolveExportWriters,
3558
4690
  resolveGridView,
3559
4691
  resolveKeyboardDropTarget,
3560
4692
  resolveMenu,
@@ -3580,6 +4712,10 @@ export {
3580
4712
  sortPriorityFor,
3581
4713
  sortRows,
3582
4714
  splitToolbarItems,
4715
+ toCsv,
4716
+ toHtmlTable,
4717
+ toJson,
4718
+ toTsv,
3583
4719
  toggleCellSelection,
3584
4720
  toggleColumnSort,
3585
4721
  toggleGroupExpansion,