argus-table 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/dist/argus-column-group.js +66 -0
  2. package/dist/argus-column-group.js.map +1 -0
  3. package/dist/argus-column.js +4 -25
  4. package/dist/argus-column.js.map +1 -1
  5. package/dist/argus-table.js +553 -200
  6. package/dist/argus-table.js.map +1 -1
  7. package/dist/attributes.js +21 -0
  8. package/dist/attributes.js.map +1 -0
  9. package/dist/cdn/argus-table.js +92 -57
  10. package/dist/cdn/argus-table.js.map +1 -1
  11. package/dist/column-registry.js +23 -0
  12. package/dist/column-registry.js.map +1 -0
  13. package/dist/controllers/auto-width.js +6 -5
  14. package/dist/controllers/auto-width.js.map +1 -1
  15. package/dist/controllers/column-children.js +32 -9
  16. package/dist/controllers/column-children.js.map +1 -1
  17. package/dist/controllers/column-window.js +0 -4
  18. package/dist/controllers/column-window.js.map +1 -1
  19. package/dist/controllers/plugins.js +25 -6
  20. package/dist/controllers/plugins.js.map +1 -1
  21. package/dist/controllers/reorder-drag.js +12 -9
  22. package/dist/controllers/reorder-drag.js.map +1 -1
  23. package/dist/controllers/row-measure.js +2 -1
  24. package/dist/controllers/row-measure.js.map +1 -1
  25. package/dist/controllers/virtual-controller.js +25 -32
  26. package/dist/controllers/virtual-controller.js.map +1 -1
  27. package/dist/core/src/columns/column-groups.js +129 -0
  28. package/dist/core/src/columns/column-groups.js.map +1 -0
  29. package/dist/core/src/columns/column-order.js +125 -31
  30. package/dist/core/src/columns/column-order.js.map +1 -1
  31. package/dist/core/src/columns/resolve-columns.js +0 -1
  32. package/dist/core/src/columns/resolve-columns.js.map +1 -1
  33. package/dist/core/src/columns/spread-width.js +43 -0
  34. package/dist/core/src/columns/spread-width.js.map +1 -0
  35. package/dist/core/src/filters/filter.js +6 -8
  36. package/dist/core/src/filters/filter.js.map +1 -1
  37. package/dist/core/src/grid/row-layout.js +19 -11
  38. package/dist/core/src/grid/row-layout.js.map +1 -1
  39. package/dist/core/src/state/selection.js +2 -1
  40. package/dist/core/src/state/selection.js.map +1 -1
  41. package/dist/core/src/table/create-table.js +57 -22
  42. package/dist/core/src/table/create-table.js.map +1 -1
  43. package/dist/core/src/table/sorted-row-model.js.map +1 -1
  44. package/dist/core/src/virtual/row-sizes.js +0 -4
  45. package/dist/core/src/virtual/row-sizes.js.map +1 -1
  46. package/dist/custom-elements.json +489 -245
  47. package/dist/events.js +16 -16
  48. package/dist/events.js.map +1 -1
  49. package/dist/index.d.ts +273 -127
  50. package/dist/index.js +3 -2
  51. package/dist/render/body.js +2 -2
  52. package/dist/render/body.js.map +1 -1
  53. package/dist/render/cells.js +11 -3
  54. package/dist/render/cells.js.map +1 -1
  55. package/dist/render/content-directive.js +1 -1
  56. package/dist/render/content-directive.js.map +1 -1
  57. package/dist/render/content.js +15 -3
  58. package/dist/render/content.js.map +1 -1
  59. package/dist/render/header.js +82 -19
  60. package/dist/render/header.js.map +1 -1
  61. package/dist/styles/base.js +1 -1
  62. package/dist/styles/base.js.map +1 -1
  63. package/package.json +1 -1
@@ -0,0 +1,43 @@
1
+ // ../core/src/columns/spread-width.ts
2
+ /**
3
+ * Spreads `total` px over columns in proportion to their widths, each within its
4
+ * bounds: a column that hits a bound keeps it, and the rest is spread over the
5
+ * others. Widths are whole px that add up to `total` (rounded) as far as the
6
+ * bounds allow. Columns of no width share equally.
7
+ */
8
+ function spreadWidth(columns, total) {
9
+ const exact = /* @__PURE__ */ new Map();
10
+ let open = [...columns];
11
+ let remaining = total;
12
+ while (open.length > 0) {
13
+ const sum = open.reduce((s, c) => s + Math.max(0, c.width), 0);
14
+ const share = (c) => sum > 0 ? Math.max(0, c.width) / sum * remaining : remaining / open.length;
15
+ const clamped = open.filter((c) => {
16
+ const w = share(c);
17
+ return w < c.minWidth || w > c.maxWidth;
18
+ });
19
+ if (clamped.length === 0) {
20
+ for (const c of open) exact.set(c.id, share(c));
21
+ break;
22
+ }
23
+ for (const c of clamped) {
24
+ const w = Math.min(c.maxWidth, Math.max(c.minWidth, share(c)));
25
+ exact.set(c.id, w);
26
+ remaining -= w;
27
+ }
28
+ open = open.filter((c) => !clamped.includes(c));
29
+ }
30
+ const widths = /* @__PURE__ */ new Map();
31
+ let acc = 0;
32
+ let rounded = 0;
33
+ for (const c of columns) {
34
+ acc += exact.get(c.id) ?? c.width;
35
+ const next = Math.round(acc);
36
+ widths.set(c.id, next - rounded);
37
+ rounded = next;
38
+ }
39
+ return widths;
40
+ }
41
+ export { spreadWidth };
42
+
43
+ //# sourceMappingURL=spread-width.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spread-width.js","names":[],"sources":["../../../../../core/src/columns/spread-width.ts"],"sourcesContent":["/** A column taking part in a group resize: its width now, and its bounds. */\nexport interface SpreadColumn {\n id: string;\n width: number;\n minWidth: number;\n maxWidth: number;\n}\n\n/**\n * Spreads `total` px over columns in proportion to their widths, each within its\n * bounds: a column that hits a bound keeps it, and the rest is spread over the\n * others. Widths are whole px that add up to `total` (rounded) as far as the\n * bounds allow. Columns of no width share equally.\n */\nexport function spreadWidth(columns: readonly SpreadColumn[], total: number): Map<string, number> {\n const exact = new Map<string, number>();\n let open = [...columns];\n let remaining = total;\n while (open.length > 0) {\n const sum = open.reduce((s, c) => s + Math.max(0, c.width), 0);\n const share = (c: SpreadColumn) =>\n sum > 0 ? (Math.max(0, c.width) / sum) * remaining : remaining / open.length;\n const clamped = open.filter((c) => {\n const w = share(c);\n return w < c.minWidth || w > c.maxWidth;\n });\n if (clamped.length === 0) {\n for (const c of open) exact.set(c.id, share(c));\n break;\n }\n for (const c of clamped) {\n const w = Math.min(c.maxWidth, Math.max(c.minWidth, share(c)));\n exact.set(c.id, w);\n remaining -= w;\n }\n open = open.filter((c) => !clamped.includes(c));\n }\n // Whole px: round the running sum, so the parts add up to the rounded total.\n const widths = new Map<string, number>();\n let acc = 0;\n let rounded = 0;\n for (const c of columns) {\n acc += exact.get(c.id) ?? c.width;\n const next = Math.round(acc);\n widths.set(c.id, next - rounded);\n rounded = next;\n }\n return widths;\n}\n"],"mappings":";;;;;;;AAcA,SAAgB,YAAY,SAAkC,OAAoC;CAChG,MAAM,wBAAQ,IAAI,IAAoB;CACtC,IAAI,OAAO,CAAC,GAAG,OAAO;CACtB,IAAI,YAAY;CAChB,OAAO,KAAK,SAAS,GAAG;EACtB,MAAM,MAAM,KAAK,QAAQ,GAAG,MAAM,IAAI,KAAK,IAAI,GAAG,EAAE,KAAK,GAAG,CAAC;EAC7D,MAAM,SAAS,MACb,MAAM,IAAK,KAAK,IAAI,GAAG,EAAE,KAAK,IAAI,MAAO,YAAY,YAAY,KAAK;EACxE,MAAM,UAAU,KAAK,QAAQ,MAAM;GACjC,MAAM,IAAI,MAAM,CAAC;GACjB,OAAO,IAAI,EAAE,YAAY,IAAI,EAAE;EACjC,CAAC;EACD,IAAI,QAAQ,WAAW,GAAG;GACxB,KAAK,MAAM,KAAK,MAAM,MAAM,IAAI,EAAE,IAAI,MAAM,CAAC,CAAC;GAC9C;EACF;EACA,KAAK,MAAM,KAAK,SAAS;GACvB,MAAM,IAAI,KAAK,IAAI,EAAE,UAAU,KAAK,IAAI,EAAE,UAAU,MAAM,CAAC,CAAC,CAAC;GAC7D,MAAM,IAAI,EAAE,IAAI,CAAC;GACjB,aAAa;EACf;EACA,OAAO,KAAK,QAAQ,MAAM,CAAC,QAAQ,SAAS,CAAC,CAAC;CAChD;CAEA,MAAM,yBAAS,IAAI,IAAoB;CACvC,IAAI,MAAM;CACV,IAAI,UAAU;CACd,KAAK,MAAM,KAAK,SAAS;EACvB,OAAO,MAAM,IAAI,EAAE,EAAE,KAAK,EAAE;EAC5B,MAAM,OAAO,KAAK,MAAM,GAAG;EAC3B,OAAO,IAAI,EAAE,IAAI,OAAO,OAAO;EAC/B,UAAU;CACZ;CACA,OAAO;AACT"}
@@ -87,11 +87,9 @@ function compileFilter(filter) {
87
87
  if (filter.op === "notEmpty") return (v) => textOf(v) !== "";
88
88
  const needle = (filter.value ?? "").toLowerCase();
89
89
  if (!needle) return void 0;
90
- const op = filter.op;
91
- return (v) => {
92
- const text = textOf(v).toLowerCase();
93
- return op === "equals" ? text === needle : op === "startsWith" ? text.startsWith(needle) : text.includes(needle);
94
- };
90
+ if (filter.op === "equals") return (v) => textOf(v).toLowerCase() === needle;
91
+ if (filter.op === "startsWith") return (v) => textOf(v).toLowerCase().startsWith(needle);
92
+ return (v) => textOf(v).toLowerCase().includes(needle);
95
93
  }
96
94
  case "number": {
97
95
  const test = compare(filter.op, numberOf(filter.value), numberOf(filter.to));
@@ -118,13 +116,13 @@ function quickTerms(quickFilter) {
118
116
  *
119
117
  * Each value is read once per test through the column's `getValue`. A column with a
120
118
  * `filterFn` uses it in place of the built-in matching. Filters for unknown columns,
121
- * and `except` (for the values a column's own filter offers), are skipped.
119
+ * and on the `except` columns (for the values a column's own filter offers), are skipped.
122
120
  */
123
121
  function rowPredicate(columns, filters, quickFilter, quickColumns, except) {
124
122
  const tests = [];
125
123
  for (const [id, filter] of Object.entries(filters ?? {})) {
126
124
  const column = columns.get(id);
127
- if (!column || id === except) continue;
125
+ if (!column || except?.has(id)) continue;
128
126
  const { getValue, filterFn } = column;
129
127
  if (filterFn) {
130
128
  tests.push((row, index) => filterFn(getValue(row, index), filter, row));
@@ -180,6 +178,6 @@ function distinctValues(rows, column, keep, limit) {
180
178
  truncated
181
179
  };
182
180
  }
183
- export { compareSetValues, compileFilter, dayOf, distinctValues, isEmptyValue, quickTerms, rowPredicate, setValueOf, textOf };
181
+ export { compareSetValues, compileFilter, dayOf, distinctValues, isEmptyValue, rowPredicate, setValueOf, textOf };
184
182
 
185
183
  //# sourceMappingURL=filter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"filter.js","names":[],"sources":["../../../../../core/src/filters/filter.ts"],"sourcesContent":["import type { ColumnFilter, ColumnFilterState, FilterValue, RangeFilterOp } from '../types.js';\n\n// Filtering rules, free of TanStack and the DOM: each filter compiles once into a\n// test of one value, and the tests of a filter state combine into one test of a row.\n\n/** A column as filtering sees it. */\nexport interface FilterColumn<TData> {\n id: string;\n getValue: (row: TData, index: number) => unknown;\n filterFn?: ((value: unknown, filter: ColumnFilter, row: TData) => boolean) | undefined;\n}\n\n/** Keeps a row (by its object and its index in `data`) or drops it. */\nexport type RowPredicate<TData> = (row: TData, index: number) => boolean;\n\n/** `true` for `undefined`, `null`, `''`, `NaN` and an invalid `Date`. */\nexport function isEmptyValue(value: unknown): boolean {\n if (value === undefined || value === null || value === '') return true;\n if (typeof value === 'number') return Number.isNaN(value);\n return value instanceof Date && Number.isNaN(value.getTime());\n}\n\nconst pad = (n: number, width = 2) => String(n).padStart(width, '0');\n\n/** A `Date` as its local day, `'2026-11-10'`. */\nfunction isoDay(date: Date): string {\n return `${pad(date.getFullYear(), 4)}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;\n}\n\n/**\n * A value as the text filters and the quick filter read it: strings as they are,\n * numbers and booleans as `String()` gives them, a `Date` as its local day\n * (`'2026-11-10'`), and anything else (empty values, objects) as `''`.\n */\nexport function textOf(value: unknown): string {\n if (typeof value === 'string') return value;\n if (typeof value === 'number') return Number.isNaN(value) ? '' : String(value);\n if (typeof value === 'boolean') return String(value);\n if (value instanceof Date) return Number.isNaN(value.getTime()) ? '' : isoDay(value);\n return '';\n}\n\nconst DAY = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\n\n/**\n * A value's local day as a sortable number (`20261110`), or `undefined`. Takes a `Date`,\n * a timestamp, an ISO date (`'2026-11-10'`, read as that day, not as UTC midnight) or\n * any other string `Date` can parse.\n */\nexport function dayOf(value: unknown): number | undefined {\n let date: Date;\n if (value instanceof Date) date = value;\n else if (typeof value === 'number') date = new Date(value);\n else if (typeof value === 'string') {\n const day = DAY.exec(value.trim());\n if (day) return Number(day[1]) * 10000 + Number(day[2]) * 100 + Number(day[3]);\n if (!value.trim()) return undefined;\n date = new Date(value);\n } else return undefined;\n const time = date.getTime();\n if (Number.isNaN(time)) return undefined;\n return date.getFullYear() * 10000 + (date.getMonth() + 1) * 100 + date.getDate();\n}\n\n/** A value as a number for the number filters: numbers, and strings that are numbers. */\nfunction numberOf(value: unknown): number | undefined {\n if (typeof value === 'number') return Number.isNaN(value) ? undefined : value;\n if (typeof value === 'string' && value.trim() !== '') {\n const n = Number(value);\n return Number.isNaN(n) ? undefined : n;\n }\n return undefined;\n}\n\n/**\n * The value a `'set'` filter lists and compares: strings, numbers and booleans stay,\n * a `Date` becomes its local day, and empty values (and objects) become `null`.\n */\nexport function setValueOf(value: unknown): FilterValue {\n if (isEmptyValue(value)) return null;\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return value;\n }\n return value instanceof Date ? isoDay(value) : null;\n}\n\nfunction compare(\n op: RangeFilterOp,\n from: number | undefined,\n to: number | undefined,\n): ((n: number | undefined) => boolean) | undefined {\n if (op === 'empty') return (n) => n === undefined;\n if (op === 'notEmpty') return (n) => n !== undefined;\n if (op === 'between') {\n if (from === undefined && to === undefined) return undefined;\n return (n) =>\n n !== undefined && (from === undefined || n >= from) && (to === undefined || n <= to);\n }\n if (from === undefined) return undefined;\n switch (op) {\n case 'eq':\n return (n) => n === from;\n case 'ne':\n // Like a spreadsheet: an empty cell isn't equal to the number, so it stays.\n return (n) => n !== from;\n case 'lt':\n return (n) => n !== undefined && n < from;\n case 'le':\n return (n) => n !== undefined && n <= from;\n case 'gt':\n return (n) => n !== undefined && n > from;\n case 'ge':\n return (n) => n !== undefined && n >= from;\n }\n}\n\n/**\n * A test of one value for a filter, or `undefined` when the filter keeps every row\n * (no text to match, no bound to compare with). Text matching ignores case.\n */\nexport function compileFilter(filter: ColumnFilter): ((value: unknown) => boolean) | undefined {\n switch (filter.kind) {\n case 'text': {\n if (filter.op === 'empty') return (v) => textOf(v) === '';\n if (filter.op === 'notEmpty') return (v) => textOf(v) !== '';\n const needle = (filter.value ?? '').toLowerCase();\n if (!needle) return undefined;\n const op = filter.op;\n return (v) => {\n const text = textOf(v).toLowerCase();\n return op === 'equals'\n ? text === needle\n : op === 'startsWith'\n ? text.startsWith(needle)\n : text.includes(needle);\n };\n }\n case 'number': {\n const test = compare(filter.op, numberOf(filter.value), numberOf(filter.to));\n return test && ((v) => test(numberOf(v)));\n }\n case 'date': {\n const test = compare(filter.op, dayOf(filter.value), dayOf(filter.to));\n return test && ((v) => test(dayOf(v)));\n }\n case 'set': {\n const values = new Set(filter.values);\n return (v) => values.has(setValueOf(v));\n }\n }\n}\n\n/** The words of a quick filter, lowercased; empty when it keeps every row. */\nexport function quickTerms(quickFilter: string | undefined): string[] {\n return (quickFilter ?? '').toLowerCase().split(/\\s+/).filter(Boolean);\n}\n\n/**\n * One test of a row for the column filters and the quick filter, or `undefined` when\n * they keep every row. A row passes every column filter, and each word of the quick\n * filter is found in the text of one of `quickColumns` (ignoring case).\n *\n * Each value is read once per test through the column's `getValue`. A column with a\n * `filterFn` uses it in place of the built-in matching. Filters for unknown columns,\n * and `except` (for the values a column's own filter offers), are skipped.\n */\nexport function rowPredicate<TData>(\n columns: ReadonlyMap<string, FilterColumn<TData>>,\n filters: ColumnFilterState | undefined,\n quickFilter: string | undefined,\n quickColumns: readonly FilterColumn<TData>[],\n except?: string,\n): RowPredicate<TData> | undefined {\n const tests: RowPredicate<TData>[] = [];\n for (const [id, filter] of Object.entries(filters ?? {})) {\n const column = columns.get(id);\n if (!column || id === except) continue;\n const { getValue, filterFn } = column;\n if (filterFn) {\n tests.push((row, index) => filterFn(getValue(row, index), filter, row));\n continue;\n }\n const test = compileFilter(filter);\n if (test) tests.push((row, index) => test(getValue(row, index)));\n }\n const terms = quickTerms(quickFilter);\n if (terms.length && quickColumns.length) {\n const textAt = (c: FilterColumn<TData>, row: TData, index: number) =>\n textOf(c.getValue(row, index)).toLowerCase();\n const [term] = terms;\n tests.push(\n terms.length === 1 && term !== undefined\n ? (row, index) => quickColumns.some((c) => textAt(c, row, index).includes(term))\n : (row, index) => {\n const texts = quickColumns.map((c) => textAt(c, row, index));\n return terms.every((t) => texts.some((text) => text.includes(t)));\n },\n );\n }\n if (!tests.length) return undefined;\n if (tests.length === 1) return tests[0];\n return (row, index) => tests.every((test) => test(row, index));\n}\n\nconst kindOrder = (v: FilterValue) =>\n v === null ? 3 : typeof v === 'boolean' ? 2 : typeof v === 'number' ? 0 : 1;\n\n/** Orders set values: numbers, then text (by locale, numbers inside text by value), then booleans, then empty. */\nexport function compareSetValues(a: FilterValue, b: FilterValue): number {\n const kind = kindOrder(a) - kindOrder(b);\n if (kind) return kind;\n if (typeof a === 'number' && typeof b === 'number') return a - b;\n if (typeof a === 'string' && typeof b === 'string') {\n return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });\n }\n return Number(a) - Number(b);\n}\n\n/**\n * The distinct values of `column` over the rows `keep` passes, as a `'set'` filter\n * lists them (see `setValueOf`), sorted. Stops after `limit` values (`truncated`).\n */\nexport function distinctValues<TData>(\n rows: Iterable<{ original: TData; index: number }>,\n column: FilterColumn<TData>,\n keep: RowPredicate<TData> | undefined,\n limit: number,\n): { values: FilterValue[]; truncated: boolean } {\n const seen = new Set<FilterValue>();\n let truncated = false;\n for (const { original, index } of rows) {\n if (keep && !keep(original, index)) continue;\n const value = setValueOf(column.getValue(original, index));\n if (seen.has(value)) continue;\n if (seen.size === limit) {\n truncated = true;\n break;\n }\n seen.add(value);\n }\n return { values: [...seen].sort(compareSetValues), truncated };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"filter.js","names":[],"sources":["../../../../../core/src/filters/filter.ts"],"sourcesContent":["import type { ColumnFilter, ColumnFilterState, FilterValue, RangeFilterOp } from '../types.js';\n\n// Filtering rules, free of TanStack and the DOM: each filter compiles once into a\n// test of one value, and the tests of a filter state combine into one test of a row.\n\n/** A column as filtering sees it. */\nexport interface FilterColumn<TData> {\n id: string;\n getValue: (row: TData, index: number) => unknown;\n filterFn?: ((value: unknown, filter: ColumnFilter, row: TData) => boolean) | undefined;\n}\n\n/** Keeps a row (by its object and its index in `data`) or drops it. */\nexport type RowPredicate<TData> = (row: TData, index: number) => boolean;\n\n/** `true` for `undefined`, `null`, `''`, `NaN` and an invalid `Date`. */\nexport function isEmptyValue(value: unknown): boolean {\n if (value === undefined || value === null || value === '') return true;\n if (typeof value === 'number') return Number.isNaN(value);\n return value instanceof Date && Number.isNaN(value.getTime());\n}\n\nconst pad = (n: number, width = 2) => String(n).padStart(width, '0');\n\n/** A `Date` as its local day, `'2026-11-10'`. */\nfunction isoDay(date: Date): string {\n return `${pad(date.getFullYear(), 4)}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;\n}\n\n/**\n * A value as the text filters and the quick filter read it: strings as they are,\n * numbers and booleans as `String()` gives them, a `Date` as its local day\n * (`'2026-11-10'`), and anything else (empty values, objects) as `''`.\n */\nexport function textOf(value: unknown): string {\n if (typeof value === 'string') return value;\n if (typeof value === 'number') return Number.isNaN(value) ? '' : String(value);\n if (typeof value === 'boolean') return String(value);\n if (value instanceof Date) return Number.isNaN(value.getTime()) ? '' : isoDay(value);\n return '';\n}\n\nconst DAY = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\n\n/**\n * A value's local day as a sortable number (`20261110`), or `undefined`. Takes a `Date`,\n * a timestamp, an ISO date (`'2026-11-10'`, read as that day, not as UTC midnight) or\n * any other string `Date` can parse.\n */\nexport function dayOf(value: unknown): number | undefined {\n let date: Date;\n if (value instanceof Date) date = value;\n else if (typeof value === 'number') date = new Date(value);\n else if (typeof value === 'string') {\n const day = DAY.exec(value.trim());\n if (day) return Number(day[1]) * 10000 + Number(day[2]) * 100 + Number(day[3]);\n if (!value.trim()) return undefined;\n date = new Date(value);\n } else return undefined;\n const time = date.getTime();\n if (Number.isNaN(time)) return undefined;\n return date.getFullYear() * 10000 + (date.getMonth() + 1) * 100 + date.getDate();\n}\n\n/** A value as a number for the number filters: numbers, and strings that are numbers. */\nfunction numberOf(value: unknown): number | undefined {\n if (typeof value === 'number') return Number.isNaN(value) ? undefined : value;\n if (typeof value === 'string' && value.trim() !== '') {\n const n = Number(value);\n return Number.isNaN(n) ? undefined : n;\n }\n return undefined;\n}\n\n/**\n * The value a `'set'` filter lists and compares: strings, numbers and booleans stay,\n * a `Date` becomes its local day, and empty values (and objects) become `null`.\n */\nexport function setValueOf(value: unknown): FilterValue {\n if (isEmptyValue(value)) return null;\n if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {\n return value;\n }\n return value instanceof Date ? isoDay(value) : null;\n}\n\nfunction compare(\n op: RangeFilterOp,\n from: number | undefined,\n to: number | undefined,\n): ((n: number | undefined) => boolean) | undefined {\n if (op === 'empty') return (n) => n === undefined;\n if (op === 'notEmpty') return (n) => n !== undefined;\n if (op === 'between') {\n if (from === undefined && to === undefined) return undefined;\n return (n) =>\n n !== undefined && (from === undefined || n >= from) && (to === undefined || n <= to);\n }\n if (from === undefined) return undefined;\n switch (op) {\n case 'eq':\n return (n) => n === from;\n case 'ne':\n // Like a spreadsheet: an empty cell isn't equal to the number, so it stays.\n return (n) => n !== from;\n case 'lt':\n return (n) => n !== undefined && n < from;\n case 'le':\n return (n) => n !== undefined && n <= from;\n case 'gt':\n return (n) => n !== undefined && n > from;\n case 'ge':\n return (n) => n !== undefined && n >= from;\n }\n}\n\n/**\n * A test of one value for a filter, or `undefined` when the filter keeps every row\n * (no text to match, no bound to compare with). Text matching ignores case.\n */\nexport function compileFilter(filter: ColumnFilter): ((value: unknown) => boolean) | undefined {\n switch (filter.kind) {\n case 'text': {\n if (filter.op === 'empty') return (v) => textOf(v) === '';\n if (filter.op === 'notEmpty') return (v) => textOf(v) !== '';\n const needle = (filter.value ?? '').toLowerCase();\n if (!needle) return undefined;\n if (filter.op === 'equals') return (v) => textOf(v).toLowerCase() === needle;\n if (filter.op === 'startsWith') return (v) => textOf(v).toLowerCase().startsWith(needle);\n return (v) => textOf(v).toLowerCase().includes(needle);\n }\n case 'number': {\n const test = compare(filter.op, numberOf(filter.value), numberOf(filter.to));\n return test && ((v) => test(numberOf(v)));\n }\n case 'date': {\n const test = compare(filter.op, dayOf(filter.value), dayOf(filter.to));\n return test && ((v) => test(dayOf(v)));\n }\n case 'set': {\n const values = new Set(filter.values);\n return (v) => values.has(setValueOf(v));\n }\n }\n}\n\n/** The words of a quick filter, lowercased; empty when it keeps every row. */\nfunction quickTerms(quickFilter: string | undefined): string[] {\n return (quickFilter ?? '').toLowerCase().split(/\\s+/).filter(Boolean);\n}\n\n/**\n * One test of a row for the column filters and the quick filter, or `undefined` when\n * they keep every row. A row passes every column filter, and each word of the quick\n * filter is found in the text of one of `quickColumns` (ignoring case).\n *\n * Each value is read once per test through the column's `getValue`. A column with a\n * `filterFn` uses it in place of the built-in matching. Filters for unknown columns,\n * and on the `except` columns (for the values a column's own filter offers), are skipped.\n */\nexport function rowPredicate<TData>(\n columns: ReadonlyMap<string, FilterColumn<TData>>,\n filters: ColumnFilterState | undefined,\n quickFilter: string | undefined,\n quickColumns: readonly FilterColumn<TData>[],\n except?: ReadonlySet<string>,\n): RowPredicate<TData> | undefined {\n const tests: RowPredicate<TData>[] = [];\n for (const [id, filter] of Object.entries(filters ?? {})) {\n const column = columns.get(id);\n if (!column || except?.has(id)) continue;\n const { getValue, filterFn } = column;\n if (filterFn) {\n tests.push((row, index) => filterFn(getValue(row, index), filter, row));\n continue;\n }\n const test = compileFilter(filter);\n if (test) tests.push((row, index) => test(getValue(row, index)));\n }\n const terms = quickTerms(quickFilter);\n if (terms.length && quickColumns.length) {\n const textAt = (c: FilterColumn<TData>, row: TData, index: number) =>\n textOf(c.getValue(row, index)).toLowerCase();\n const [term] = terms;\n tests.push(\n terms.length === 1 && term !== undefined\n ? (row, index) => quickColumns.some((c) => textAt(c, row, index).includes(term))\n : (row, index) => {\n const texts = quickColumns.map((c) => textAt(c, row, index));\n return terms.every((t) => texts.some((text) => text.includes(t)));\n },\n );\n }\n if (!tests.length) return undefined;\n if (tests.length === 1) return tests[0];\n return (row, index) => tests.every((test) => test(row, index));\n}\n\nconst kindOrder = (v: FilterValue) =>\n v === null ? 3 : typeof v === 'boolean' ? 2 : typeof v === 'number' ? 0 : 1;\n\n/** Orders set values: numbers, then text (by locale, numbers inside text by value), then booleans, then empty. */\nexport function compareSetValues(a: FilterValue, b: FilterValue): number {\n const kind = kindOrder(a) - kindOrder(b);\n if (kind) return kind;\n if (typeof a === 'number' && typeof b === 'number') return a - b;\n if (typeof a === 'string' && typeof b === 'string') {\n return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });\n }\n return Number(a) - Number(b);\n}\n\n/**\n * The distinct values of `column` over the rows `keep` passes, as a `'set'` filter\n * lists them (see `setValueOf`), sorted. Stops after `limit` values (`truncated`).\n */\nexport function distinctValues<TData>(\n rows: Iterable<{ original: TData; index: number }>,\n column: FilterColumn<TData>,\n keep: RowPredicate<TData> | undefined,\n limit: number,\n): { values: FilterValue[]; truncated: boolean } {\n const seen = new Set<FilterValue>();\n let truncated = false;\n for (const { original, index } of rows) {\n if (keep && !keep(original, index)) continue;\n const value = setValueOf(column.getValue(original, index));\n if (seen.has(value)) continue;\n if (seen.size === limit) {\n truncated = true;\n break;\n }\n seen.add(value);\n }\n return { values: [...seen].sort(compareSetValues), truncated };\n}\n"],"mappings":"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"}
@@ -1,37 +1,45 @@
1
1
  // ../core/src/grid/row-layout.ts
2
2
  /**
3
- * Where each row sits in the grid as a whole: the header row, then the rows
3
+ * Where each row sits in the grid as a whole: the header rows, then the rows
4
4
  * pinned to the top, the body rows, and the rows pinned to the bottom. The
5
5
  * position is 0-based and `aria-rowindex` is one more. Keyboard navigation
6
6
  * steps through positions, so it crosses sections without special cases.
7
7
  */
8
8
  var RowLayout = class {
9
- constructor(top, body, bottom) {
9
+ constructor(top, body, bottom, header = 1) {
10
10
  this.top = top;
11
11
  this.body = body;
12
12
  this.bottom = bottom;
13
+ this.header = Math.max(1, header);
13
14
  }
14
- /** Number of rows, the header included (`aria-rowcount`). */
15
+ /** Number of rows, the header rows included (`aria-rowcount`). */
15
16
  get total() {
16
- return 1 + this.top + this.body + this.bottom;
17
+ return this.header + this.top + this.body + this.bottom;
18
+ }
19
+ /** The row of column headers: the last header row. */
20
+ get leafHeader() {
21
+ return {
22
+ section: "header",
23
+ index: this.header - 1
24
+ };
17
25
  }
18
26
  /** 0-based position of a row in the grid. */
19
27
  position({ section, index }) {
20
28
  switch (section) {
21
- case "header": return 0;
22
- case "top": return 1 + index;
23
- case "body": return 1 + this.top + index;
24
- case "bottom": return 1 + this.top + this.body + index;
29
+ case "header": return index;
30
+ case "top": return this.header + index;
31
+ case "body": return this.header + this.top + index;
32
+ case "bottom": return this.header + this.top + this.body + index;
25
33
  }
26
34
  }
27
35
  /** The row at a 0-based position, or `undefined` outside the grid. */
28
36
  at(position) {
29
37
  if (!Number.isInteger(position) || position < 0 || position >= this.total) return void 0;
30
- if (position === 0) return {
38
+ if (position < this.header) return {
31
39
  section: "header",
32
- index: 0
40
+ index: position
33
41
  };
34
- let index = position - 1;
42
+ let index = position - this.header;
35
43
  if (index < this.top) return {
36
44
  section: "top",
37
45
  index
@@ -1 +1 @@
1
- {"version":3,"file":"row-layout.js","names":[],"sources":["../../../../../core/src/grid/row-layout.ts"],"sourcesContent":["/** The row groups of the grid, top to bottom. */\nexport type GridSection = 'header' | 'top' | 'body' | 'bottom';\n\n/** A row of the grid: its section and its index within it (the header is index 0). */\nexport interface GridRow {\n section: GridSection;\n index: number;\n}\n\n/**\n * Where each row sits in the grid as a whole: the header row, then the rows\n * pinned to the top, the body rows, and the rows pinned to the bottom. The\n * position is 0-based and `aria-rowindex` is one more. Keyboard navigation\n * steps through positions, so it crosses sections without special cases.\n */\nexport class RowLayout {\n readonly top: number;\n readonly body: number;\n readonly bottom: number;\n\n constructor(top: number, body: number, bottom: number) {\n this.top = top;\n this.body = body;\n this.bottom = bottom;\n }\n\n /** Number of rows, the header included (`aria-rowcount`). */\n get total(): number {\n return 1 + this.top + this.body + this.bottom;\n }\n\n /** 0-based position of a row in the grid. */\n position({ section, index }: GridRow): number {\n switch (section) {\n case 'header':\n return 0;\n case 'top':\n return 1 + index;\n case 'body':\n return 1 + this.top + index;\n case 'bottom':\n return 1 + this.top + this.body + index;\n }\n }\n\n /** The row at a 0-based position, or `undefined` outside the grid. */\n at(position: number): GridRow | undefined {\n if (!Number.isInteger(position) || position < 0 || position >= this.total) return undefined;\n if (position === 0) return { section: 'header', index: 0 };\n let index = position - 1;\n if (index < this.top) return { section: 'top', index };\n index -= this.top;\n if (index < this.body) return { section: 'body', index };\n return { section: 'bottom', index: index - this.body };\n }\n\n ariaRowIndex(row: GridRow): number {\n return this.position(row) + 1;\n }\n\n fromAriaRowIndex(ariaRowIndex: number): GridRow | undefined {\n return this.at(ariaRowIndex - 1);\n }\n}\n"],"mappings":";;;;;;;AAeA,IAAa,YAAb,MAAuB;CAKrB,YAAY,KAAa,MAAc,QAAgB;EACrD,KAAK,MAAM;EACX,KAAK,OAAO;EACZ,KAAK,SAAS;CAChB;;CAGA,IAAI,QAAgB;EAClB,OAAO,IAAI,KAAK,MAAM,KAAK,OAAO,KAAK;CACzC;;CAGA,SAAS,EAAE,SAAS,SAA0B;EAC5C,QAAQ,SAAR;GACE,KAAK,UACH,OAAO;GACT,KAAK,OACH,OAAO,IAAI;GACb,KAAK,QACH,OAAO,IAAI,KAAK,MAAM;GACxB,KAAK,UACH,OAAO,IAAI,KAAK,MAAM,KAAK,OAAO;EACtC;CACF;;CAGA,GAAG,UAAuC;EACxC,IAAI,CAAC,OAAO,UAAU,QAAQ,KAAK,WAAW,KAAK,YAAY,KAAK,OAAO,OAAO,KAAA;EAClF,IAAI,aAAa,GAAG,OAAO;GAAE,SAAS;GAAU,OAAO;EAAE;EACzD,IAAI,QAAQ,WAAW;EACvB,IAAI,QAAQ,KAAK,KAAK,OAAO;GAAE,SAAS;GAAO;EAAM;EACrD,SAAS,KAAK;EACd,IAAI,QAAQ,KAAK,MAAM,OAAO;GAAE,SAAS;GAAQ;EAAM;EACvD,OAAO;GAAE,SAAS;GAAU,OAAO,QAAQ,KAAK;EAAK;CACvD;CAEA,aAAa,KAAsB;EACjC,OAAO,KAAK,SAAS,GAAG,IAAI;CAC9B;CAEA,iBAAiB,cAA2C;EAC1D,OAAO,KAAK,GAAG,eAAe,CAAC;CACjC;AACF"}
1
+ {"version":3,"file":"row-layout.js","names":[],"sources":["../../../../../core/src/grid/row-layout.ts"],"sourcesContent":["/** The row groups of the grid, top to bottom. */\nexport type GridSection = 'header' | 'top' | 'body' | 'bottom';\n\n/**\n * A row of the grid: its section and its index within it. Header rows count from\n * the top: with column groups the column headers are the last header row.\n */\nexport interface GridRow {\n section: GridSection;\n index: number;\n}\n\n/**\n * Where each row sits in the grid as a whole: the header rows, then the rows\n * pinned to the top, the body rows, and the rows pinned to the bottom. The\n * position is 0-based and `aria-rowindex` is one more. Keyboard navigation\n * steps through positions, so it crosses sections without special cases.\n */\nexport class RowLayout {\n readonly top: number;\n readonly body: number;\n readonly bottom: number;\n /** Number of header rows: one, and one more per level of column groups. */\n readonly header: number;\n\n constructor(top: number, body: number, bottom: number, header = 1) {\n this.top = top;\n this.body = body;\n this.bottom = bottom;\n this.header = Math.max(1, header);\n }\n\n /** Number of rows, the header rows included (`aria-rowcount`). */\n get total(): number {\n return this.header + this.top + this.body + this.bottom;\n }\n\n /** The row of column headers: the last header row. */\n get leafHeader(): GridRow {\n return { section: 'header', index: this.header - 1 };\n }\n\n /** 0-based position of a row in the grid. */\n position({ section, index }: GridRow): number {\n switch (section) {\n case 'header':\n return index;\n case 'top':\n return this.header + index;\n case 'body':\n return this.header + this.top + index;\n case 'bottom':\n return this.header + this.top + this.body + index;\n }\n }\n\n /** The row at a 0-based position, or `undefined` outside the grid. */\n at(position: number): GridRow | undefined {\n if (!Number.isInteger(position) || position < 0 || position >= this.total) return undefined;\n if (position < this.header) return { section: 'header', index: position };\n let index = position - this.header;\n if (index < this.top) return { section: 'top', index };\n index -= this.top;\n if (index < this.body) return { section: 'body', index };\n return { section: 'bottom', index: index - this.body };\n }\n\n ariaRowIndex(row: GridRow): number {\n return this.position(row) + 1;\n }\n\n fromAriaRowIndex(ariaRowIndex: number): GridRow | undefined {\n return this.at(ariaRowIndex - 1);\n }\n}\n"],"mappings":";;;;;;;AAkBA,IAAa,YAAb,MAAuB;CAOrB,YAAY,KAAa,MAAc,QAAgB,SAAS,GAAG;EACjE,KAAK,MAAM;EACX,KAAK,OAAO;EACZ,KAAK,SAAS;EACd,KAAK,SAAS,KAAK,IAAI,GAAG,MAAM;CAClC;;CAGA,IAAI,QAAgB;EAClB,OAAO,KAAK,SAAS,KAAK,MAAM,KAAK,OAAO,KAAK;CACnD;;CAGA,IAAI,aAAsB;EACxB,OAAO;GAAE,SAAS;GAAU,OAAO,KAAK,SAAS;EAAE;CACrD;;CAGA,SAAS,EAAE,SAAS,SAA0B;EAC5C,QAAQ,SAAR;GACE,KAAK,UACH,OAAO;GACT,KAAK,OACH,OAAO,KAAK,SAAS;GACvB,KAAK,QACH,OAAO,KAAK,SAAS,KAAK,MAAM;GAClC,KAAK,UACH,OAAO,KAAK,SAAS,KAAK,MAAM,KAAK,OAAO;EAChD;CACF;;CAGA,GAAG,UAAuC;EACxC,IAAI,CAAC,OAAO,UAAU,QAAQ,KAAK,WAAW,KAAK,YAAY,KAAK,OAAO,OAAO,KAAA;EAClF,IAAI,WAAW,KAAK,QAAQ,OAAO;GAAE,SAAS;GAAU,OAAO;EAAS;EACxE,IAAI,QAAQ,WAAW,KAAK;EAC5B,IAAI,QAAQ,KAAK,KAAK,OAAO;GAAE,SAAS;GAAO;EAAM;EACrD,SAAS,KAAK;EACd,IAAI,QAAQ,KAAK,MAAM,OAAO;GAAE,SAAS;GAAQ;EAAM;EACvD,OAAO;GAAE,SAAS;GAAU,OAAO,QAAQ,KAAK;EAAK;CACvD;CAEA,aAAa,KAAsB;EACjC,OAAO,KAAK,SAAS,GAAG,IAAI;CAC9B;CAEA,iBAAiB,cAA2C;EAC1D,OAAO,KAAK,GAAG,eAAe,CAAC;CACjC;AACF"}
@@ -28,7 +28,8 @@ function toggleRow(current, mode, id) {
28
28
  function selectRange(current, from, to, idAt, add) {
29
29
  const ids = add ? [...current] : [];
30
30
  const have = new Set(ids);
31
- const [first, last] = (to >= from ? 1 : -1) === 1 ? [from, to] : [to, from];
31
+ const first = Math.min(from, to);
32
+ const last = Math.max(from, to);
32
33
  for (let index = first; index <= last; index++) {
33
34
  const id = idAt(index);
34
35
  if (id === void 0 || have.has(id)) continue;
@@ -1 +1 @@
1
- {"version":3,"file":"selection.js","names":[],"sources":["../../../../../core/src/state/selection.ts"],"sourcesContent":["import type { SelectionMode, SelectionState } from '../types.js';\n\n/**\n * Row selection rules, DOM-free. The element owns the state (through\n * `controllable`) and the anchor; these functions only compute the next ones.\n *\n * The anchor is the row a range starts from: the last row clicked or toggled\n * without Shift. Ranges are by index in the current row order, so they follow\n * sorting; `idAt` returns `undefined` for a row that can't be selected (a\n * `dataSource` row not loaded), and such rows are left out.\n */\n\n/** The selection after a step, and the row later ranges start from. */\nexport interface SelectionStep {\n selection: SelectionState;\n anchor: string | undefined;\n}\n\n/** The row a click or key acts on: its id and its index in the current row order. */\nexport interface SelectionTarget {\n id: string;\n index: number;\n}\n\n/** Modifier keys of a click: Ctrl or Cmd toggles, Shift selects a range. */\nexport interface SelectionModifiers {\n toggle: boolean;\n range: boolean;\n}\n\n/** Whether two selections hold the same ids, in any order. */\nexport function sameSelection(a: readonly string[], b: readonly string[]): boolean {\n if (a.length !== b.length) return false;\n const ids = new Set(a);\n return b.every((id) => ids.has(id));\n}\n\n/**\n * Selects or deselects one row and makes it the anchor. In `'single'` mode,\n * selecting a row replaces the selection.\n */\nexport function toggleRow(\n current: readonly string[],\n mode: SelectionMode,\n id: string,\n): SelectionStep | undefined {\n if (mode === 'none') return undefined;\n if (current.includes(id)) return { selection: current.filter((s) => s !== id), anchor: id };\n return { selection: mode === 'single' ? [id] : [...current, id], anchor: id };\n}\n\n/**\n * The rows from index `from` to `to` (either order, both inclusive), in row\n * order. With `add`, they join the current selection instead of replacing it.\n * The anchor stays where it was.\n */\nexport function selectRange(\n current: readonly string[],\n from: number,\n to: number,\n idAt: (index: number) => string | undefined,\n add: boolean,\n): SelectionState {\n const ids: string[] = add ? [...current] : [];\n const have = new Set(ids);\n const step = to >= from ? 1 : -1;\n const [first, last] = step === 1 ? [from, to] : [to, from];\n for (let index = first; index <= last; index++) {\n const id = idAt(index);\n if (id === undefined || have.has(id)) continue;\n have.add(id);\n ids.push(id);\n }\n return ids;\n}\n\n/**\n * Every row of `ids` selected, keeping the rows already selected (with a\n * `dataSource`, `ids` are only the loaded rows: selected rows elsewhere stay).\n */\nexport function selectAll(current: readonly string[], ids: readonly string[]): SelectionState {\n const have = new Set(current);\n return [...current, ...ids.filter((id) => !have.has(id))];\n}\n\n/**\n * A click on a row, desktop style. A plain click selects only that row; Ctrl or\n * Cmd toggles it. In `'multi'` mode, Shift selects the range from the anchor\n * (at `anchorIndex`) to the row, and Ctrl+Shift adds that range. Without an\n * anchor in the current rows, Shift acts like a plain click. `undefined` in\n * `'none'` mode. The selection may come back unchanged (a click on the one\n * selected row): check with `sameSelection` before reporting a change.\n */\nexport function clickSelection(\n current: readonly string[],\n mode: SelectionMode,\n target: SelectionTarget,\n anchor: { id: string; index: number } | undefined,\n modifiers: SelectionModifiers,\n idAt: (index: number) => string | undefined,\n): SelectionStep | undefined {\n if (mode === 'none') return undefined;\n if (mode === 'multi' && modifiers.range && anchor) {\n return {\n selection: selectRange(current, anchor.index, target.index, idAt, modifiers.toggle),\n anchor: anchor.id,\n };\n }\n if (modifiers.toggle) return toggleRow(current, mode, target.id);\n return { selection: [target.id], anchor: target.id };\n}\n"],"mappings":";;AA+BA,SAAgB,cAAc,GAAsB,GAA+B;CACjF,IAAI,EAAE,WAAW,EAAE,QAAQ,OAAO;CAClC,MAAM,MAAM,IAAI,IAAI,CAAC;CACrB,OAAO,EAAE,OAAO,OAAO,IAAI,IAAI,EAAE,CAAC;AACpC;;;;;AAMA,SAAgB,UACd,SACA,MACA,IAC2B;CAC3B,IAAI,SAAS,QAAQ,OAAO,KAAA;CAC5B,IAAI,QAAQ,SAAS,EAAE,GAAG,OAAO;EAAE,WAAW,QAAQ,QAAQ,MAAM,MAAM,EAAE;EAAG,QAAQ;CAAG;CAC1F,OAAO;EAAE,WAAW,SAAS,WAAW,CAAC,EAAE,IAAI,CAAC,GAAG,SAAS,EAAE;EAAG,QAAQ;CAAG;AAC9E;;;;;;AAOA,SAAgB,YACd,SACA,MACA,IACA,MACA,KACgB;CAChB,MAAM,MAAgB,MAAM,CAAC,GAAG,OAAO,IAAI,CAAC;CAC5C,MAAM,OAAO,IAAI,IAAI,GAAG;CAExB,MAAM,CAAC,OAAO,SADD,MAAM,OAAO,IAAI,QACC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,IAAI,IAAI;CACzD,KAAK,IAAI,QAAQ,OAAO,SAAS,MAAM,SAAS;EAC9C,MAAM,KAAK,KAAK,KAAK;EACrB,IAAI,OAAO,KAAA,KAAa,KAAK,IAAI,EAAE,GAAG;EACtC,KAAK,IAAI,EAAE;EACX,IAAI,KAAK,EAAE;CACb;CACA,OAAO;AACT;;;;;AAMA,SAAgB,UAAU,SAA4B,KAAwC;CAC5F,MAAM,OAAO,IAAI,IAAI,OAAO;CAC5B,OAAO,CAAC,GAAG,SAAS,GAAG,IAAI,QAAQ,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;AAC1D;;;;;;;;;AAUA,SAAgB,eACd,SACA,MACA,QACA,QACA,WACA,MAC2B;CAC3B,IAAI,SAAS,QAAQ,OAAO,KAAA;CAC5B,IAAI,SAAS,WAAW,UAAU,SAAS,QACzC,OAAO;EACL,WAAW,YAAY,SAAS,OAAO,OAAO,OAAO,OAAO,MAAM,UAAU,MAAM;EAClF,QAAQ,OAAO;CACjB;CAEF,IAAI,UAAU,QAAQ,OAAO,UAAU,SAAS,MAAM,OAAO,EAAE;CAC/D,OAAO;EAAE,WAAW,CAAC,OAAO,EAAE;EAAG,QAAQ,OAAO;CAAG;AACrD"}
1
+ {"version":3,"file":"selection.js","names":[],"sources":["../../../../../core/src/state/selection.ts"],"sourcesContent":["import type { SelectionMode, SelectionState } from '../types.js';\n\n/**\n * Row selection rules, DOM-free. The element owns the state (through\n * `controllable`) and the anchor; these functions only compute the next ones.\n *\n * The anchor is the row a range starts from: the last row clicked or toggled\n * without Shift. Ranges are by index in the current row order, so they follow\n * sorting; `idAt` returns `undefined` for a row that can't be selected (a\n * `dataSource` row not loaded), and such rows are left out.\n */\n\n/** The selection after a step, and the row later ranges start from. */\nexport interface SelectionStep {\n selection: SelectionState;\n anchor: string | undefined;\n}\n\n/** The row a click or key acts on: its id and its index in the current row order. */\nexport interface SelectionTarget {\n id: string;\n index: number;\n}\n\n/** Modifier keys of a click: Ctrl or Cmd toggles, Shift selects a range. */\nexport interface SelectionModifiers {\n toggle: boolean;\n range: boolean;\n}\n\n/** Whether two selections hold the same ids, in any order. */\nexport function sameSelection(a: readonly string[], b: readonly string[]): boolean {\n if (a.length !== b.length) return false;\n const ids = new Set(a);\n return b.every((id) => ids.has(id));\n}\n\n/**\n * Selects or deselects one row and makes it the anchor. In `'single'` mode,\n * selecting a row replaces the selection.\n */\nexport function toggleRow(\n current: readonly string[],\n mode: SelectionMode,\n id: string,\n): SelectionStep | undefined {\n if (mode === 'none') return undefined;\n if (current.includes(id)) return { selection: current.filter((s) => s !== id), anchor: id };\n return { selection: mode === 'single' ? [id] : [...current, id], anchor: id };\n}\n\n/**\n * The rows from index `from` to `to` (either order, both inclusive), in row\n * order. With `add`, they join the current selection instead of replacing it.\n * The anchor stays where it was.\n */\nexport function selectRange(\n current: readonly string[],\n from: number,\n to: number,\n idAt: (index: number) => string | undefined,\n add: boolean,\n): SelectionState {\n const ids: string[] = add ? [...current] : [];\n const have = new Set(ids);\n const first = Math.min(from, to);\n const last = Math.max(from, to);\n for (let index = first; index <= last; index++) {\n const id = idAt(index);\n if (id === undefined || have.has(id)) continue;\n have.add(id);\n ids.push(id);\n }\n return ids;\n}\n\n/**\n * Every row of `ids` selected, keeping the rows already selected (with a\n * `dataSource`, `ids` are only the loaded rows: selected rows elsewhere stay).\n */\nexport function selectAll(current: readonly string[], ids: readonly string[]): SelectionState {\n const have = new Set(current);\n return [...current, ...ids.filter((id) => !have.has(id))];\n}\n\n/**\n * A click on a row, desktop style. A plain click selects only that row; Ctrl or\n * Cmd toggles it. In `'multi'` mode, Shift selects the range from the anchor\n * (at `anchorIndex`) to the row, and Ctrl+Shift adds that range. Without an\n * anchor in the current rows, Shift acts like a plain click. `undefined` in\n * `'none'` mode. The selection may come back unchanged (a click on the one\n * selected row): check with `sameSelection` before reporting a change.\n */\nexport function clickSelection(\n current: readonly string[],\n mode: SelectionMode,\n target: SelectionTarget,\n anchor: SelectionTarget | undefined,\n modifiers: SelectionModifiers,\n idAt: (index: number) => string | undefined,\n): SelectionStep | undefined {\n if (mode === 'none') return undefined;\n if (mode === 'multi' && modifiers.range && anchor) {\n return {\n selection: selectRange(current, anchor.index, target.index, idAt, modifiers.toggle),\n anchor: anchor.id,\n };\n }\n if (modifiers.toggle) return toggleRow(current, mode, target.id);\n return { selection: [target.id], anchor: target.id };\n}\n"],"mappings":";;AA+BA,SAAgB,cAAc,GAAsB,GAA+B;CACjF,IAAI,EAAE,WAAW,EAAE,QAAQ,OAAO;CAClC,MAAM,MAAM,IAAI,IAAI,CAAC;CACrB,OAAO,EAAE,OAAO,OAAO,IAAI,IAAI,EAAE,CAAC;AACpC;;;;;AAMA,SAAgB,UACd,SACA,MACA,IAC2B;CAC3B,IAAI,SAAS,QAAQ,OAAO,KAAA;CAC5B,IAAI,QAAQ,SAAS,EAAE,GAAG,OAAO;EAAE,WAAW,QAAQ,QAAQ,MAAM,MAAM,EAAE;EAAG,QAAQ;CAAG;CAC1F,OAAO;EAAE,WAAW,SAAS,WAAW,CAAC,EAAE,IAAI,CAAC,GAAG,SAAS,EAAE;EAAG,QAAQ;CAAG;AAC9E;;;;;;AAOA,SAAgB,YACd,SACA,MACA,IACA,MACA,KACgB;CAChB,MAAM,MAAgB,MAAM,CAAC,GAAG,OAAO,IAAI,CAAC;CAC5C,MAAM,OAAO,IAAI,IAAI,GAAG;CACxB,MAAM,QAAQ,KAAK,IAAI,MAAM,EAAE;CAC/B,MAAM,OAAO,KAAK,IAAI,MAAM,EAAE;CAC9B,KAAK,IAAI,QAAQ,OAAO,SAAS,MAAM,SAAS;EAC9C,MAAM,KAAK,KAAK,KAAK;EACrB,IAAI,OAAO,KAAA,KAAa,KAAK,IAAI,EAAE,GAAG;EACtC,KAAK,IAAI,EAAE;EACX,IAAI,KAAK,EAAE;CACb;CACA,OAAO;AACT;;;;;AAMA,SAAgB,UAAU,SAA4B,KAAwC;CAC5F,MAAM,OAAO,IAAI,IAAI,OAAO;CAC5B,OAAO,CAAC,GAAG,SAAS,GAAG,IAAI,QAAQ,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;AAC1D;;;;;;;;;AAUA,SAAgB,eACd,SACA,MACA,QACA,QACA,WACA,MAC2B;CAC3B,IAAI,SAAS,QAAQ,OAAO,KAAA;CAC5B,IAAI,SAAS,WAAW,UAAU,SAAS,QACzC,OAAO;EACL,WAAW,YAAY,SAAS,OAAO,OAAO,OAAO,OAAO,MAAM,UAAU,MAAM;EAClF,QAAQ,OAAO;CACjB;CAEF,IAAI,UAAU,QAAQ,OAAO,UAAU,SAAS,MAAM,OAAO,EAAE;CAC/D,OAAO;EAAE,WAAW,CAAC,OAAO,EAAE;EAAG,QAAQ,OAAO;CAAG;AACrD"}
@@ -1,4 +1,5 @@
1
- import { moveColumn, normalizeOrder } from "../columns/column-order.js";
1
+ import { cellOver, headerGroupRows, resolveColumnGroups } from "../columns/column-groups.js";
2
+ import { moveUnit, normalizeOrder, siblingUnits } from "../columns/column-order.js";
2
3
  import { resolveColumns } from "../columns/resolve-columns.js";
3
4
  import { distinctValues, rowPredicate } from "../filters/filter.js";
4
5
  import { filteredRowModel, setRowPredicate } from "./filtered-row-model.js";
@@ -89,6 +90,9 @@ function createTableModel(initial) {
89
90
  let byId = new Map(resolved.map((c) => [c.id, c]));
90
91
  let tanStackById = new Map(resolved.map((c) => [c.id, toTanStack(c)]));
91
92
  let views;
93
+ const treeOf = () => resolveColumnGroups(options.columnGroups, new Set(resolved.map((c) => c.id)));
94
+ let tree = treeOf();
95
+ let groupRows;
92
96
  const pinOf = (column, pinning = options.columnPinning) => {
93
97
  const state = pinning?.[column.id];
94
98
  if (state === "start" || state === "end") return state;
@@ -129,7 +133,7 @@ function createTableModel(initial) {
129
133
  reorderable: c.reorderable
130
134
  }));
131
135
  let order;
132
- const orderState = () => order ??= normalizeOrder(options.columnOrder ?? [], orderable());
136
+ const orderState = () => order ??= normalizeOrder(options.columnOrder ?? [], orderable(), tree);
133
137
  const rowViews = /* @__PURE__ */ new WeakMap();
134
138
  let indexById;
135
139
  let ordered;
@@ -154,12 +158,12 @@ function createTableModel(initial) {
154
158
  onColumnSizingChange: () => void 0
155
159
  });
156
160
  const table = constructTable(tableOptions());
157
- const predicateFor = (except) => options.remote ? void 0 : rowPredicate(byId, options.columnFilters, options.quickFilter, resolved.filter((c) => c.hasAccessor && !hiddenOf(c)), except);
161
+ const predicateFor = (except, quick = false) => options.remote ? void 0 : rowPredicate(byId, options.columnFilters, options.quickFilter, resolved.filter((c) => c.hasAccessor && !hiddenOf(c) && !(quick && except?.has(c.id))), except);
158
162
  setRowPredicate(table, predicateFor());
159
163
  const rows = () => table.getRowModel().rows;
160
164
  const pinned = (side) => options.pinnedRows?.[side] ?? [];
161
165
  const widthOf = (id) => table.getColumn(id)?.getSize() ?? 0;
162
- const view = (def, colIndex, offset, pinEdge, width) => {
166
+ const view = (def, colIndex, offset, pinEdge) => {
163
167
  const sort = options.sorting.find((s) => s.id === def.id);
164
168
  const pin = pinOf(def);
165
169
  const resized = options.columnSizing[def.id] !== void 0;
@@ -171,7 +175,7 @@ function createTableModel(initial) {
171
175
  offset,
172
176
  pinEdge,
173
177
  hidden: colIndex === 0,
174
- width,
178
+ width: widthOf(def.id),
175
179
  flex: pin === void 0 && !resized ? def.flex : void 0,
176
180
  auto: def.auto && !resized,
177
181
  minWidth: def.minWidth,
@@ -206,23 +210,22 @@ function createTableModel(initial) {
206
210
  if (lead && byId.has("--argus-selection")) throw new Error(`argus-table: the column id '${SELECTION_COLUMN_ID}' is reserved.`);
207
211
  let x = lead * 40;
208
212
  const list = start.map((def, i) => {
209
- const width = widthOf(def.id);
210
- const v = view(def, lead + i + 1, x, i === start.length - 1, width);
211
- x += width;
213
+ const v = view(def, lead + i + 1, x, i === start.length - 1);
214
+ x += v.width;
212
215
  return v;
213
216
  });
214
217
  x = 0;
215
218
  for (const [i, def] of center.entries()) {
216
- const width = widthOf(def.id);
217
- list.push(view(def, lead + start.length + i + 1, x, false, width));
218
- x += width;
219
+ const v = view(def, lead + start.length + i + 1, x, false);
220
+ list.push(v);
221
+ x += v.width;
219
222
  }
220
223
  const ends = [];
221
224
  x = 0;
222
225
  for (const [i, def] of [...end.entries()].reverse()) {
223
- const width = widthOf(def.id);
224
- ends.unshift(view(def, lead + start.length + center.length + i + 1, x, i === 0, width));
225
- x += width;
226
+ const v = view(def, lead + start.length + center.length + i + 1, x, i === 0);
227
+ ends.unshift(v);
228
+ x += v.width;
226
229
  }
227
230
  list.push(...ends);
228
231
  if (lead) list.unshift(selectionView(start.length === 0));
@@ -235,7 +238,7 @@ function createTableModel(initial) {
235
238
  const shownView = byShownId.get(id);
236
239
  if (shownView) return [shownView];
237
240
  const def = byId.get(id);
238
- return def ? [view(def, 0, 0, false, widthOf(id))] : [];
241
+ return def ? [view(def, 0, 0, false)] : [];
239
242
  });
240
243
  };
241
244
  return {
@@ -252,11 +255,14 @@ function createTableModel(initial) {
252
255
  byId = new Map(resolved.map((c) => [c.id, c]));
253
256
  tanStackById = new Map(resolved.map((c) => [c.id, toTanStack(c)]));
254
257
  }
258
+ const columnsChanged = merged.columns !== options.columns;
255
259
  const pinningChanged = merged.columnPinning !== options.columnPinning;
256
- if (merged.columns !== options.columns || merged.columnSizing !== options.columnSizing || merged.autoWidths !== options.autoWidths || pinningChanged) sizing = void 0;
257
- if (merged.columns !== options.columns || merged.columnOrder !== options.columnOrder || pinningChanged) order = void 0;
258
- const refilter = merged.columns !== options.columns || merged.columnFilters !== options.columnFilters || merged.quickFilter !== options.quickFilter || merged.columnVisibility !== options.columnVisibility || merged.remote !== options.remote;
260
+ const groupsChanged = merged.columnGroups !== options.columnGroups;
261
+ if (columnsChanged || merged.columnSizing !== options.columnSizing || merged.autoWidths !== options.autoWidths || pinningChanged) sizing = void 0;
262
+ if (columnsChanged || groupsChanged || merged.columnOrder !== options.columnOrder || pinningChanged) order = void 0;
263
+ const refilter = columnsChanged || merged.columnFilters !== options.columnFilters || merged.quickFilter !== options.quickFilter || merged.columnVisibility !== options.columnVisibility || merged.remote !== options.remote;
259
264
  options = merged;
265
+ if (columnsChanged || groupsChanged) tree = treeOf();
260
266
  views = void 0;
261
267
  allViews = void 0;
262
268
  if (refilter) setRowPredicate(table, predicateFor());
@@ -284,6 +290,25 @@ function createTableModel(initial) {
284
290
  get totalWidth() {
285
291
  return this.columns.reduce((sum, column) => sum + column.width, 0);
286
292
  },
293
+ get headerGroups() {
294
+ const current = this.columns;
295
+ if (groupRows?.views === current && groupRows.tree === tree) return groupRows.rows;
296
+ const key = tree.groups.size ? current.map((c) => `${c.id}\u0000${String(c.pin)}\u0000${String(c.offset)}\u0000${String(c.pinEdge)}\u0000${String(c.reorderable)}\u0000${String(c.resizable)}`).join("") : "";
297
+ if (groupRows?.tree !== tree || groupRows.key !== key) groupRows = {
298
+ views: current,
299
+ tree,
300
+ key,
301
+ rows: headerGroupRows(tree, current)
302
+ };
303
+ else groupRows.views = current;
304
+ return groupRows.rows;
305
+ },
306
+ get headerRows() {
307
+ return this.headerGroups.length + 1;
308
+ },
309
+ headerCell(level, columnId) {
310
+ return cellOver(this.headerGroups, level, columnId);
311
+ },
287
312
  rowAt(index) {
288
313
  if (options.remote) {
289
314
  const entry = options.remote.entryAt(index);
@@ -371,10 +396,13 @@ function createTableModel(initial) {
371
396
  };
372
397
  },
373
398
  orderOf(base) {
374
- return base === void 0 ? orderState() : normalizeOrder(base, orderable());
399
+ return base === void 0 ? orderState() : normalizeOrder(base, orderable(), tree);
375
400
  },
376
- nextOrder(columnId, to, base) {
377
- return moveColumn(base ?? orderState(), orderable(), columnId, to, shown);
401
+ siblingUnits(unit, base) {
402
+ return siblingUnits(base ?? orderState(), orderable(), tree, unit, shown);
403
+ },
404
+ nextMove(unit, to, base) {
405
+ return moveUnit(base ?? orderState(), orderable(), tree, unit, to, shown);
378
406
  },
379
407
  nextPinning(columnId, pin, base = options.columnPinning) {
380
408
  const column = byId.get(columnId);
@@ -420,7 +448,14 @@ function createTableModel(initial) {
420
448
  };
421
449
  if (options.remote) return void 0;
422
450
  const rows = table.getCoreRowModel().rows;
423
- return distinctValues(rows, column, predicateFor(columnId), limit);
451
+ return distinctValues(rows, column, predicateFor(/* @__PURE__ */ new Set([columnId])), limit);
452
+ },
453
+ indexesPassing(except) {
454
+ if (options.remote) return void 0;
455
+ const keep = predicateFor(new Set(except), true);
456
+ const kept = [];
457
+ for (const row of table.getCoreRowModel().rows) if (!keep || keep(row.original, row.index)) kept.push(row.index);
458
+ return kept;
424
459
  },
425
460
  pinnedCount(side) {
426
461
  return pinned(side).length;