argus-table 0.2.2 → 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 (83) hide show
  1. package/README.md +1 -1
  2. package/dist/argus-column-group.js +66 -0
  3. package/dist/argus-column-group.js.map +1 -0
  4. package/dist/argus-column.js +28 -26
  5. package/dist/argus-column.js.map +1 -1
  6. package/dist/argus-table.js +1497 -285
  7. package/dist/argus-table.js.map +1 -1
  8. package/dist/attributes.js +21 -0
  9. package/dist/attributes.js.map +1 -0
  10. package/dist/cdn/argus-table.js +120 -61
  11. package/dist/cdn/argus-table.js.map +1 -1
  12. package/dist/column-registry.js +23 -0
  13. package/dist/column-registry.js.map +1 -0
  14. package/dist/controllers/auto-width.js +16 -4
  15. package/dist/controllers/auto-width.js.map +1 -1
  16. package/dist/controllers/column-children.js +32 -9
  17. package/dist/controllers/column-children.js.map +1 -1
  18. package/dist/controllers/column-window.js +135 -0
  19. package/dist/controllers/column-window.js.map +1 -0
  20. package/dist/controllers/data-controller.js +9 -4
  21. package/dist/controllers/data-controller.js.map +1 -1
  22. package/dist/controllers/plugins.js +121 -0
  23. package/dist/controllers/plugins.js.map +1 -0
  24. package/dist/controllers/reorder-drag.js +19 -17
  25. package/dist/controllers/reorder-drag.js.map +1 -1
  26. package/dist/controllers/row-measure.js +122 -0
  27. package/dist/controllers/row-measure.js.map +1 -0
  28. package/dist/controllers/virtual-controller.js +315 -21
  29. package/dist/controllers/virtual-controller.js.map +1 -1
  30. package/dist/core/src/columns/column-groups.js +129 -0
  31. package/dist/core/src/columns/column-groups.js.map +1 -0
  32. package/dist/core/src/columns/column-order.js +125 -27
  33. package/dist/core/src/columns/column-order.js.map +1 -1
  34. package/dist/core/src/columns/resolve-columns.js +18 -5
  35. package/dist/core/src/columns/resolve-columns.js.map +1 -1
  36. package/dist/core/src/columns/spread-width.js +43 -0
  37. package/dist/core/src/columns/spread-width.js.map +1 -0
  38. package/dist/core/src/data/page-cache.js +4 -0
  39. package/dist/core/src/data/page-cache.js.map +1 -1
  40. package/dist/core/src/filters/filter.js +183 -0
  41. package/dist/core/src/filters/filter.js.map +1 -0
  42. package/dist/core/src/grid/row-layout.js +19 -11
  43. package/dist/core/src/grid/row-layout.js.map +1 -1
  44. package/dist/core/src/state/selection.js +71 -0
  45. package/dist/core/src/state/selection.js.map +1 -0
  46. package/dist/core/src/table/create-table.js +276 -66
  47. package/dist/core/src/table/create-table.js.map +1 -1
  48. package/dist/core/src/table/filtered-row-model.js +48 -0
  49. package/dist/core/src/table/filtered-row-model.js.map +1 -0
  50. package/dist/core/src/table/sorted-row-model.js +13 -2
  51. package/dist/core/src/table/sorted-row-model.js.map +1 -1
  52. package/dist/core/src/virtual/column-window.js +72 -0
  53. package/dist/core/src/virtual/column-window.js.map +1 -0
  54. package/dist/core/src/virtual/row-pool.js +2 -1
  55. package/dist/core/src/virtual/row-pool.js.map +1 -1
  56. package/dist/core/src/virtual/row-sizes.js +152 -0
  57. package/dist/core/src/virtual/row-sizes.js.map +1 -0
  58. package/dist/core/src/virtual/scroll-scale.js +41 -3
  59. package/dist/core/src/virtual/scroll-scale.js.map +1 -1
  60. package/dist/custom-elements.json +1683 -211
  61. package/dist/events.js +16 -8
  62. package/dist/events.js.map +1 -1
  63. package/dist/index.d.ts +808 -127
  64. package/dist/index.js +4 -2
  65. package/dist/interactive.js +22 -0
  66. package/dist/interactive.js.map +1 -0
  67. package/dist/max-height.js +22 -8
  68. package/dist/max-height.js.map +1 -1
  69. package/dist/render/body.js +48 -8
  70. package/dist/render/body.js.map +1 -1
  71. package/dist/render/cells.js +22 -11
  72. package/dist/render/cells.js.map +1 -1
  73. package/dist/render/content-directive.js +115 -0
  74. package/dist/render/content-directive.js.map +1 -0
  75. package/dist/render/content.js +33 -10
  76. package/dist/render/content.js.map +1 -1
  77. package/dist/render/header.js +104 -22
  78. package/dist/render/header.js.map +1 -1
  79. package/dist/render/pinned-rows.js +1 -1
  80. package/dist/render/pinned-rows.js.map +1 -1
  81. package/dist/styles/base.js +1 -1
  82. package/dist/styles/base.js.map +1 -1
  83. package/package.json +1 -1
@@ -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"}
@@ -42,6 +42,10 @@ var PageCache = class {
42
42
  indexOf(rowId) {
43
43
  return this.#indexById.get(rowId);
44
44
  }
45
+ /** The ids of every loaded row, in row order. */
46
+ loadedIds() {
47
+ return [...this.#pages.keys()].sort((a, b) => a - b).flatMap((page) => (this.#pages.get(page) ?? []).map((entry) => entry.id));
48
+ }
45
49
  stateAt(index) {
46
50
  if (this.entryAt(index)) return "loaded";
47
51
  return this.#failed.has(this.pageOf(index)) ? "error" : "loading";
@@ -1 +1 @@
1
- {"version":3,"file":"page-cache.js","names":[],"sources":["../../../../../core/src/data/page-cache.ts"],"sourcesContent":["/** Whether a row's data is there: loaded, still to come, or its page failed. */\nexport type RowState = 'loaded' | 'loading' | 'error';\n\n/** A loaded row and its id. */\nexport interface CachedRow<TData> {\n id: string;\n row: TData;\n}\n\nexport interface PageCacheOptions<TData> {\n /** Rows per page, at least 1. */\n pageSize: number;\n /** Row id function, given the absolute index. Default: the index as a string. */\n getRowId?: ((row: TData, index: number) => string) | undefined;\n /** Row count to start with (carried over from a previous cache). Default: one page. */\n rowCount?: number;\n /** Whether `rowCount` is the real total. Default: `false`. */\n totalKnown?: boolean;\n}\n\n/**\n * Pages kept loaded on each side of the requested range; pages further away are\n * dropped, so memory stays bounded however far the table scrolls.\n */\nexport const KEEP_PAGES = 10;\n\n/**\n * The row cache behind `dataSource`: which pages are loaded, in flight or failed,\n * and how many rows the table has. Bookkeeping only: no timers and no fetching,\n * so the caller (the element's DataController) decides when to load.\n *\n * Without a known total, the row count stays one page past the last loaded row,\n * so the next page shows as skeletons and loads when it scrolls into view. A page\n * shorter than asked for marks the end.\n */\nexport class PageCache<TData> {\n readonly pageSize: number;\n readonly #getRowId: (row: TData, index: number) => string;\n readonly #pages = new Map<number, CachedRow<TData>[]>();\n /** In-flight pages and the `end` they asked for. */\n readonly #inFlight = new Map<number, number>();\n readonly #failed = new Set<number>();\n readonly #indexById = new Map<string, number>();\n #rowCount: number;\n #totalKnown: boolean;\n\n constructor(options: PageCacheOptions<TData>) {\n this.pageSize = Math.max(1, Math.floor(options.pageSize));\n this.#getRowId = options.getRowId ?? ((_row, index) => String(index));\n this.#rowCount = Math.max(0, Math.floor(options.rowCount ?? this.pageSize));\n this.#totalKnown = options.totalKnown ?? false;\n }\n\n /** Number of rows, loaded or not. */\n get rowCount(): number {\n return this.#rowCount;\n }\n\n /** `true` once the total is known (given by the server, or found from a short page). */\n get totalKnown(): boolean {\n return this.#totalKnown;\n }\n\n pageOf(index: number): number {\n return Math.floor(index / this.pageSize);\n }\n\n /** The loaded row at an index, with its id; `undefined` until its page has loaded. */\n entryAt(index: number): CachedRow<TData> | undefined {\n if (index < 0 || index >= this.#rowCount) return undefined;\n return this.#pages.get(this.pageOf(index))?.[index % this.pageSize];\n }\n\n /** The index of a loaded row's id, or `undefined`. */\n indexOf(rowId: string): number | undefined {\n return this.#indexById.get(rowId);\n }\n\n stateAt(index: number): RowState {\n if (this.entryAt(index)) return 'loaded';\n return this.#failed.has(this.pageOf(index)) ? 'error' : 'loading';\n }\n\n /** Pages touching the rows `start` to `end` (exclusive) that still need a request. */\n missingPages(start: number, end: number): number[] {\n const last = Math.min(end, this.#rowCount) - 1;\n const pages: number[] = [];\n if (last < 0) return pages;\n for (let page = this.pageOf(Math.max(0, start)); page <= this.pageOf(last); page++) {\n if (!this.#pages.has(page) && !this.#inFlight.has(page) && !this.#failed.has(page)) {\n pages.push(page);\n }\n }\n return pages;\n }\n\n /** Marks a page as in flight and returns the rows to ask for. */\n begin(page: number): { start: number; end: number } {\n const start = page * this.pageSize;\n let end = start + this.pageSize;\n if (this.#totalKnown) end = Math.min(end, this.#rowCount);\n this.#failed.delete(page);\n this.#inFlight.set(page, end);\n return { start, end };\n }\n\n /** Stores a page's rows and updates the row count. Ignored unless the page is in flight. */\n resolve(page: number, rows: readonly TData[], total?: number): boolean {\n const end = this.#inFlight.get(page);\n if (end === undefined) return false;\n this.#inFlight.delete(page);\n const start = page * this.pageSize;\n const asked = end - start;\n const entries = rows.slice(0, asked).map((row, i) => ({\n id: this.#getRowId(row, start + i),\n row,\n }));\n this.#drop(page);\n if (entries.length > 0) this.#pages.set(page, entries);\n entries.forEach((entry, i) => this.#indexById.set(entry.id, start + i));\n\n if (total !== undefined && Number.isFinite(total) && total >= 0) {\n this.#setCount(Math.floor(total), true);\n } else if (entries.length < asked) {\n this.#setCount(start + entries.length, true);\n } else if (!this.#totalKnown) {\n this.#setCount(Math.max(this.#rowCount, end + this.pageSize), false);\n }\n return true;\n }\n\n /** Marks an in-flight page as failed. Ignored unless the page is in flight. */\n fail(page: number): boolean {\n if (!this.#inFlight.delete(page)) return false;\n this.#failed.add(page);\n return true;\n }\n\n /** Lets a failed page be requested again. */\n retry(page: number): void {\n this.#failed.delete(page);\n }\n\n /** Forgets an in-flight page (its request was aborted), so it can be requested again. */\n cancel(page: number): void {\n this.#inFlight.delete(page);\n }\n\n /** `true` if a page failed and hasn't been retried or evicted since. */\n isFailed(page: number): boolean {\n return this.#failed.has(page);\n }\n\n /** In-flight pages entirely outside rows `start` to `end` (exclusive). */\n inFlightOutside(start: number, end: number): number[] {\n const first = this.pageOf(Math.max(0, start));\n const last = this.pageOf(Math.max(0, end - 1));\n return [...this.#inFlight.keys()].filter((page) => page < first || page > last);\n }\n\n /**\n * Drops loaded and failed pages more than `KEEP_PAGES` pages away from rows\n * `start` to `end` (exclusive). Failed pages that come back into view load again.\n */\n evictOutside(start: number, end: number): void {\n const first = this.pageOf(Math.max(0, start)) - KEEP_PAGES;\n const last = this.pageOf(Math.max(0, end - 1)) + KEEP_PAGES;\n for (const page of [...this.#pages.keys()]) {\n if (page < first || page > last) this.#drop(page);\n }\n for (const page of [...this.#failed]) {\n if (page < first || page > last) this.#failed.delete(page);\n }\n }\n\n /** Number of pages holding rows: bounded by eviction. */\n get loadedPages(): number {\n return this.#pages.size;\n }\n\n #setCount(count: number, known: boolean): void {\n this.#rowCount = count;\n this.#totalKnown = known;\n // Rows past the end are gone; drop pages that start there.\n for (const page of [...this.#pages.keys()]) {\n if (page * this.pageSize >= count) this.#drop(page);\n }\n }\n\n #drop(page: number): void {\n const entries = this.#pages.get(page);\n if (!entries) return;\n const start = page * this.pageSize;\n entries.forEach((entry, i) => {\n if (this.#indexById.get(entry.id) === start + i) this.#indexById.delete(entry.id);\n });\n this.#pages.delete(page);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"page-cache.js","names":[],"sources":["../../../../../core/src/data/page-cache.ts"],"sourcesContent":["/** Whether a row's data is there: loaded, still to come, or its page failed. */\nexport type RowState = 'loaded' | 'loading' | 'error';\n\n/** A loaded row and its id. */\nexport interface CachedRow<TData> {\n id: string;\n row: TData;\n}\n\nexport interface PageCacheOptions<TData> {\n /** Rows per page, at least 1. */\n pageSize: number;\n /** Row id function, given the absolute index. Default: the index as a string. */\n getRowId?: ((row: TData, index: number) => string) | undefined;\n /** Row count to start with (carried over from a previous cache). Default: one page. */\n rowCount?: number;\n /** Whether `rowCount` is the real total. Default: `false`. */\n totalKnown?: boolean;\n}\n\n/**\n * Pages kept loaded on each side of the requested range; pages further away are\n * dropped, so memory stays bounded however far the table scrolls.\n */\nexport const KEEP_PAGES = 10;\n\n/**\n * The row cache behind `dataSource`: which pages are loaded, in flight or failed,\n * and how many rows the table has. Bookkeeping only: no timers and no fetching,\n * so the caller (the element's DataController) decides when to load.\n *\n * Without a known total, the row count stays one page past the last loaded row,\n * so the next page shows as skeletons and loads when it scrolls into view. A page\n * shorter than asked for marks the end.\n */\nexport class PageCache<TData> {\n readonly pageSize: number;\n readonly #getRowId: (row: TData, index: number) => string;\n readonly #pages = new Map<number, CachedRow<TData>[]>();\n /** In-flight pages and the `end` they asked for. */\n readonly #inFlight = new Map<number, number>();\n readonly #failed = new Set<number>();\n readonly #indexById = new Map<string, number>();\n #rowCount: number;\n #totalKnown: boolean;\n\n constructor(options: PageCacheOptions<TData>) {\n this.pageSize = Math.max(1, Math.floor(options.pageSize));\n this.#getRowId = options.getRowId ?? ((_row, index) => String(index));\n this.#rowCount = Math.max(0, Math.floor(options.rowCount ?? this.pageSize));\n this.#totalKnown = options.totalKnown ?? false;\n }\n\n /** Number of rows, loaded or not. */\n get rowCount(): number {\n return this.#rowCount;\n }\n\n /** `true` once the total is known (given by the server, or found from a short page). */\n get totalKnown(): boolean {\n return this.#totalKnown;\n }\n\n pageOf(index: number): number {\n return Math.floor(index / this.pageSize);\n }\n\n /** The loaded row at an index, with its id; `undefined` until its page has loaded. */\n entryAt(index: number): CachedRow<TData> | undefined {\n if (index < 0 || index >= this.#rowCount) return undefined;\n return this.#pages.get(this.pageOf(index))?.[index % this.pageSize];\n }\n\n /** The index of a loaded row's id, or `undefined`. */\n indexOf(rowId: string): number | undefined {\n return this.#indexById.get(rowId);\n }\n\n /** The ids of every loaded row, in row order. */\n loadedIds(): string[] {\n const pages = [...this.#pages.keys()].sort((a, b) => a - b);\n return pages.flatMap((page) => (this.#pages.get(page) ?? []).map((entry) => entry.id));\n }\n\n stateAt(index: number): RowState {\n if (this.entryAt(index)) return 'loaded';\n return this.#failed.has(this.pageOf(index)) ? 'error' : 'loading';\n }\n\n /** Pages touching the rows `start` to `end` (exclusive) that still need a request. */\n missingPages(start: number, end: number): number[] {\n const last = Math.min(end, this.#rowCount) - 1;\n const pages: number[] = [];\n if (last < 0) return pages;\n for (let page = this.pageOf(Math.max(0, start)); page <= this.pageOf(last); page++) {\n if (!this.#pages.has(page) && !this.#inFlight.has(page) && !this.#failed.has(page)) {\n pages.push(page);\n }\n }\n return pages;\n }\n\n /** Marks a page as in flight and returns the rows to ask for. */\n begin(page: number): { start: number; end: number } {\n const start = page * this.pageSize;\n let end = start + this.pageSize;\n if (this.#totalKnown) end = Math.min(end, this.#rowCount);\n this.#failed.delete(page);\n this.#inFlight.set(page, end);\n return { start, end };\n }\n\n /** Stores a page's rows and updates the row count. Ignored unless the page is in flight. */\n resolve(page: number, rows: readonly TData[], total?: number): boolean {\n const end = this.#inFlight.get(page);\n if (end === undefined) return false;\n this.#inFlight.delete(page);\n const start = page * this.pageSize;\n const asked = end - start;\n const entries = rows.slice(0, asked).map((row, i) => ({\n id: this.#getRowId(row, start + i),\n row,\n }));\n this.#drop(page);\n if (entries.length > 0) this.#pages.set(page, entries);\n entries.forEach((entry, i) => this.#indexById.set(entry.id, start + i));\n\n if (total !== undefined && Number.isFinite(total) && total >= 0) {\n this.#setCount(Math.floor(total), true);\n } else if (entries.length < asked) {\n this.#setCount(start + entries.length, true);\n } else if (!this.#totalKnown) {\n this.#setCount(Math.max(this.#rowCount, end + this.pageSize), false);\n }\n return true;\n }\n\n /** Marks an in-flight page as failed. Ignored unless the page is in flight. */\n fail(page: number): boolean {\n if (!this.#inFlight.delete(page)) return false;\n this.#failed.add(page);\n return true;\n }\n\n /** Lets a failed page be requested again. */\n retry(page: number): void {\n this.#failed.delete(page);\n }\n\n /** Forgets an in-flight page (its request was aborted), so it can be requested again. */\n cancel(page: number): void {\n this.#inFlight.delete(page);\n }\n\n /** `true` if a page failed and hasn't been retried or evicted since. */\n isFailed(page: number): boolean {\n return this.#failed.has(page);\n }\n\n /** In-flight pages entirely outside rows `start` to `end` (exclusive). */\n inFlightOutside(start: number, end: number): number[] {\n const first = this.pageOf(Math.max(0, start));\n const last = this.pageOf(Math.max(0, end - 1));\n return [...this.#inFlight.keys()].filter((page) => page < first || page > last);\n }\n\n /**\n * Drops loaded and failed pages more than `KEEP_PAGES` pages away from rows\n * `start` to `end` (exclusive). Failed pages that come back into view load again.\n */\n evictOutside(start: number, end: number): void {\n const first = this.pageOf(Math.max(0, start)) - KEEP_PAGES;\n const last = this.pageOf(Math.max(0, end - 1)) + KEEP_PAGES;\n for (const page of [...this.#pages.keys()]) {\n if (page < first || page > last) this.#drop(page);\n }\n for (const page of [...this.#failed]) {\n if (page < first || page > last) this.#failed.delete(page);\n }\n }\n\n /** Number of pages holding rows: bounded by eviction. */\n get loadedPages(): number {\n return this.#pages.size;\n }\n\n #setCount(count: number, known: boolean): void {\n this.#rowCount = count;\n this.#totalKnown = known;\n // Rows past the end are gone; drop pages that start there.\n for (const page of [...this.#pages.keys()]) {\n if (page * this.pageSize >= count) this.#drop(page);\n }\n }\n\n #drop(page: number): void {\n const entries = this.#pages.get(page);\n if (!entries) return;\n const start = page * this.pageSize;\n entries.forEach((entry, i) => {\n if (this.#indexById.get(entry.id) === start + i) this.#indexById.delete(entry.id);\n });\n this.#pages.delete(page);\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,183 @@
1
+ // ../core/src/filters/filter.ts
2
+ /** `true` for `undefined`, `null`, `''`, `NaN` and an invalid `Date`. */
3
+ function isEmptyValue(value) {
4
+ if (value === void 0 || value === null || value === "") return true;
5
+ if (typeof value === "number") return Number.isNaN(value);
6
+ return value instanceof Date && Number.isNaN(value.getTime());
7
+ }
8
+ var pad = (n, width = 2) => String(n).padStart(width, "0");
9
+ /** A `Date` as its local day, `'2026-11-10'`. */
10
+ function isoDay(date) {
11
+ return `${pad(date.getFullYear(), 4)}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
12
+ }
13
+ /**
14
+ * A value as the text filters and the quick filter read it: strings as they are,
15
+ * numbers and booleans as `String()` gives them, a `Date` as its local day
16
+ * (`'2026-11-10'`), and anything else (empty values, objects) as `''`.
17
+ */
18
+ function textOf(value) {
19
+ if (typeof value === "string") return value;
20
+ if (typeof value === "number") return Number.isNaN(value) ? "" : String(value);
21
+ if (typeof value === "boolean") return String(value);
22
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? "" : isoDay(value);
23
+ return "";
24
+ }
25
+ var DAY = /^(\d{4})-(\d{2})-(\d{2})$/;
26
+ /**
27
+ * A value's local day as a sortable number (`20261110`), or `undefined`. Takes a `Date`,
28
+ * a timestamp, an ISO date (`'2026-11-10'`, read as that day, not as UTC midnight) or
29
+ * any other string `Date` can parse.
30
+ */
31
+ function dayOf(value) {
32
+ let date;
33
+ if (value instanceof Date) date = value;
34
+ else if (typeof value === "number") date = new Date(value);
35
+ else if (typeof value === "string") {
36
+ const day = DAY.exec(value.trim());
37
+ if (day) return Number(day[1]) * 1e4 + Number(day[2]) * 100 + Number(day[3]);
38
+ if (!value.trim()) return void 0;
39
+ date = new Date(value);
40
+ } else return void 0;
41
+ const time = date.getTime();
42
+ if (Number.isNaN(time)) return void 0;
43
+ return date.getFullYear() * 1e4 + (date.getMonth() + 1) * 100 + date.getDate();
44
+ }
45
+ /** A value as a number for the number filters: numbers, and strings that are numbers. */
46
+ function numberOf(value) {
47
+ if (typeof value === "number") return Number.isNaN(value) ? void 0 : value;
48
+ if (typeof value === "string" && value.trim() !== "") {
49
+ const n = Number(value);
50
+ return Number.isNaN(n) ? void 0 : n;
51
+ }
52
+ }
53
+ /**
54
+ * The value a `'set'` filter lists and compares: strings, numbers and booleans stay,
55
+ * a `Date` becomes its local day, and empty values (and objects) become `null`.
56
+ */
57
+ function setValueOf(value) {
58
+ if (isEmptyValue(value)) return null;
59
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
60
+ return value instanceof Date ? isoDay(value) : null;
61
+ }
62
+ function compare(op, from, to) {
63
+ if (op === "empty") return (n) => n === void 0;
64
+ if (op === "notEmpty") return (n) => n !== void 0;
65
+ if (op === "between") {
66
+ if (from === void 0 && to === void 0) return void 0;
67
+ return (n) => n !== void 0 && (from === void 0 || n >= from) && (to === void 0 || n <= to);
68
+ }
69
+ if (from === void 0) return void 0;
70
+ switch (op) {
71
+ case "eq": return (n) => n === from;
72
+ case "ne": return (n) => n !== from;
73
+ case "lt": return (n) => n !== void 0 && n < from;
74
+ case "le": return (n) => n !== void 0 && n <= from;
75
+ case "gt": return (n) => n !== void 0 && n > from;
76
+ case "ge": return (n) => n !== void 0 && n >= from;
77
+ }
78
+ }
79
+ /**
80
+ * A test of one value for a filter, or `undefined` when the filter keeps every row
81
+ * (no text to match, no bound to compare with). Text matching ignores case.
82
+ */
83
+ function compileFilter(filter) {
84
+ switch (filter.kind) {
85
+ case "text": {
86
+ if (filter.op === "empty") return (v) => textOf(v) === "";
87
+ if (filter.op === "notEmpty") return (v) => textOf(v) !== "";
88
+ const needle = (filter.value ?? "").toLowerCase();
89
+ if (!needle) return void 0;
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);
93
+ }
94
+ case "number": {
95
+ const test = compare(filter.op, numberOf(filter.value), numberOf(filter.to));
96
+ return test && ((v) => test(numberOf(v)));
97
+ }
98
+ case "date": {
99
+ const test = compare(filter.op, dayOf(filter.value), dayOf(filter.to));
100
+ return test && ((v) => test(dayOf(v)));
101
+ }
102
+ case "set": {
103
+ const values = new Set(filter.values);
104
+ return (v) => values.has(setValueOf(v));
105
+ }
106
+ }
107
+ }
108
+ /** The words of a quick filter, lowercased; empty when it keeps every row. */
109
+ function quickTerms(quickFilter) {
110
+ return (quickFilter ?? "").toLowerCase().split(/\s+/).filter(Boolean);
111
+ }
112
+ /**
113
+ * One test of a row for the column filters and the quick filter, or `undefined` when
114
+ * they keep every row. A row passes every column filter, and each word of the quick
115
+ * filter is found in the text of one of `quickColumns` (ignoring case).
116
+ *
117
+ * Each value is read once per test through the column's `getValue`. A column with a
118
+ * `filterFn` uses it in place of the built-in matching. Filters for unknown columns,
119
+ * and on the `except` columns (for the values a column's own filter offers), are skipped.
120
+ */
121
+ function rowPredicate(columns, filters, quickFilter, quickColumns, except) {
122
+ const tests = [];
123
+ for (const [id, filter] of Object.entries(filters ?? {})) {
124
+ const column = columns.get(id);
125
+ if (!column || except?.has(id)) continue;
126
+ const { getValue, filterFn } = column;
127
+ if (filterFn) {
128
+ tests.push((row, index) => filterFn(getValue(row, index), filter, row));
129
+ continue;
130
+ }
131
+ const test = compileFilter(filter);
132
+ if (test) tests.push((row, index) => test(getValue(row, index)));
133
+ }
134
+ const terms = quickTerms(quickFilter);
135
+ if (terms.length && quickColumns.length) {
136
+ const textAt = (c, row, index) => textOf(c.getValue(row, index)).toLowerCase();
137
+ const [term] = terms;
138
+ tests.push(terms.length === 1 && term !== void 0 ? (row, index) => quickColumns.some((c) => textAt(c, row, index).includes(term)) : (row, index) => {
139
+ const texts = quickColumns.map((c) => textAt(c, row, index));
140
+ return terms.every((t) => texts.some((text) => text.includes(t)));
141
+ });
142
+ }
143
+ if (!tests.length) return void 0;
144
+ if (tests.length === 1) return tests[0];
145
+ return (row, index) => tests.every((test) => test(row, index));
146
+ }
147
+ var kindOrder = (v) => v === null ? 3 : typeof v === "boolean" ? 2 : typeof v === "number" ? 0 : 1;
148
+ /** Orders set values: numbers, then text (by locale, numbers inside text by value), then booleans, then empty. */
149
+ function compareSetValues(a, b) {
150
+ const kind = kindOrder(a) - kindOrder(b);
151
+ if (kind) return kind;
152
+ if (typeof a === "number" && typeof b === "number") return a - b;
153
+ if (typeof a === "string" && typeof b === "string") return a.localeCompare(b, void 0, {
154
+ numeric: true,
155
+ sensitivity: "base"
156
+ });
157
+ return Number(a) - Number(b);
158
+ }
159
+ /**
160
+ * The distinct values of `column` over the rows `keep` passes, as a `'set'` filter
161
+ * lists them (see `setValueOf`), sorted. Stops after `limit` values (`truncated`).
162
+ */
163
+ function distinctValues(rows, column, keep, limit) {
164
+ const seen = /* @__PURE__ */ new Set();
165
+ let truncated = false;
166
+ for (const { original, index } of rows) {
167
+ if (keep && !keep(original, index)) continue;
168
+ const value = setValueOf(column.getValue(original, index));
169
+ if (seen.has(value)) continue;
170
+ if (seen.size === limit) {
171
+ truncated = true;
172
+ break;
173
+ }
174
+ seen.add(value);
175
+ }
176
+ return {
177
+ values: [...seen].sort(compareSetValues),
178
+ truncated
179
+ };
180
+ }
181
+ export { compareSetValues, compileFilter, dayOf, distinctValues, isEmptyValue, rowPredicate, setValueOf, textOf };
182
+
183
+ //# sourceMappingURL=filter.js.map
@@ -0,0 +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 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"}
@@ -0,0 +1,71 @@
1
+ // ../core/src/state/selection.ts
2
+ /** Whether two selections hold the same ids, in any order. */
3
+ function sameSelection(a, b) {
4
+ if (a.length !== b.length) return false;
5
+ const ids = new Set(a);
6
+ return b.every((id) => ids.has(id));
7
+ }
8
+ /**
9
+ * Selects or deselects one row and makes it the anchor. In `'single'` mode,
10
+ * selecting a row replaces the selection.
11
+ */
12
+ function toggleRow(current, mode, id) {
13
+ if (mode === "none") return void 0;
14
+ if (current.includes(id)) return {
15
+ selection: current.filter((s) => s !== id),
16
+ anchor: id
17
+ };
18
+ return {
19
+ selection: mode === "single" ? [id] : [...current, id],
20
+ anchor: id
21
+ };
22
+ }
23
+ /**
24
+ * The rows from index `from` to `to` (either order, both inclusive), in row
25
+ * order. With `add`, they join the current selection instead of replacing it.
26
+ * The anchor stays where it was.
27
+ */
28
+ function selectRange(current, from, to, idAt, add) {
29
+ const ids = add ? [...current] : [];
30
+ const have = new Set(ids);
31
+ const first = Math.min(from, to);
32
+ const last = Math.max(from, to);
33
+ for (let index = first; index <= last; index++) {
34
+ const id = idAt(index);
35
+ if (id === void 0 || have.has(id)) continue;
36
+ have.add(id);
37
+ ids.push(id);
38
+ }
39
+ return ids;
40
+ }
41
+ /**
42
+ * Every row of `ids` selected, keeping the rows already selected (with a
43
+ * `dataSource`, `ids` are only the loaded rows: selected rows elsewhere stay).
44
+ */
45
+ function selectAll(current, ids) {
46
+ const have = new Set(current);
47
+ return [...current, ...ids.filter((id) => !have.has(id))];
48
+ }
49
+ /**
50
+ * A click on a row, desktop style. A plain click selects only that row; Ctrl or
51
+ * Cmd toggles it. In `'multi'` mode, Shift selects the range from the anchor
52
+ * (at `anchorIndex`) to the row, and Ctrl+Shift adds that range. Without an
53
+ * anchor in the current rows, Shift acts like a plain click. `undefined` in
54
+ * `'none'` mode. The selection may come back unchanged (a click on the one
55
+ * selected row): check with `sameSelection` before reporting a change.
56
+ */
57
+ function clickSelection(current, mode, target, anchor, modifiers, idAt) {
58
+ if (mode === "none") return void 0;
59
+ if (mode === "multi" && modifiers.range && anchor) return {
60
+ selection: selectRange(current, anchor.index, target.index, idAt, modifiers.toggle),
61
+ anchor: anchor.id
62
+ };
63
+ if (modifiers.toggle) return toggleRow(current, mode, target.id);
64
+ return {
65
+ selection: [target.id],
66
+ anchor: target.id
67
+ };
68
+ }
69
+ export { clickSelection, sameSelection, selectAll, selectRange, toggleRow };
70
+
71
+ //# sourceMappingURL=selection.js.map
@@ -0,0 +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 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"}