argus-table 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +1 -1
  2. package/dist/argus-column.js +24 -1
  3. package/dist/argus-column.js.map +1 -1
  4. package/dist/argus-table.js +1024 -165
  5. package/dist/argus-table.js.map +1 -1
  6. package/dist/cdn/argus-table.js +74 -50
  7. package/dist/cdn/argus-table.js.map +1 -1
  8. package/dist/controllers/auto-width.js +11 -0
  9. package/dist/controllers/auto-width.js.map +1 -1
  10. package/dist/controllers/column-window.js +139 -0
  11. package/dist/controllers/column-window.js.map +1 -0
  12. package/dist/controllers/data-controller.js +9 -4
  13. package/dist/controllers/data-controller.js.map +1 -1
  14. package/dist/controllers/plugins.js +102 -0
  15. package/dist/controllers/plugins.js.map +1 -0
  16. package/dist/controllers/reorder-drag.js +9 -10
  17. package/dist/controllers/reorder-drag.js.map +1 -1
  18. package/dist/controllers/row-measure.js +121 -0
  19. package/dist/controllers/row-measure.js.map +1 -0
  20. package/dist/controllers/virtual-controller.js +311 -10
  21. package/dist/controllers/virtual-controller.js.map +1 -1
  22. package/dist/core/src/columns/column-order.js +14 -10
  23. package/dist/core/src/columns/column-order.js.map +1 -1
  24. package/dist/core/src/columns/resolve-columns.js +18 -4
  25. package/dist/core/src/columns/resolve-columns.js.map +1 -1
  26. package/dist/core/src/data/page-cache.js +4 -0
  27. package/dist/core/src/data/page-cache.js.map +1 -1
  28. package/dist/core/src/filters/filter.js +185 -0
  29. package/dist/core/src/filters/filter.js.map +1 -0
  30. package/dist/core/src/state/selection.js +70 -0
  31. package/dist/core/src/state/selection.js.map +1 -0
  32. package/dist/core/src/table/create-table.js +239 -64
  33. package/dist/core/src/table/create-table.js.map +1 -1
  34. package/dist/core/src/table/filtered-row-model.js +48 -0
  35. package/dist/core/src/table/filtered-row-model.js.map +1 -0
  36. package/dist/core/src/table/sorted-row-model.js +13 -2
  37. package/dist/core/src/table/sorted-row-model.js.map +1 -1
  38. package/dist/core/src/virtual/column-window.js +72 -0
  39. package/dist/core/src/virtual/column-window.js.map +1 -0
  40. package/dist/core/src/virtual/row-pool.js +2 -1
  41. package/dist/core/src/virtual/row-pool.js.map +1 -1
  42. package/dist/core/src/virtual/row-sizes.js +156 -0
  43. package/dist/core/src/virtual/row-sizes.js.map +1 -0
  44. package/dist/core/src/virtual/scroll-scale.js +41 -3
  45. package/dist/core/src/virtual/scroll-scale.js.map +1 -1
  46. package/dist/custom-elements.json +1273 -45
  47. package/dist/events.js +9 -1
  48. package/dist/events.js.map +1 -1
  49. package/dist/index.d.ts +609 -74
  50. package/dist/index.js +3 -2
  51. package/dist/interactive.js +22 -0
  52. package/dist/interactive.js.map +1 -0
  53. package/dist/max-height.js +22 -8
  54. package/dist/max-height.js.map +1 -1
  55. package/dist/render/body.js +47 -7
  56. package/dist/render/body.js.map +1 -1
  57. package/dist/render/cells.js +14 -11
  58. package/dist/render/cells.js.map +1 -1
  59. package/dist/render/content-directive.js +115 -0
  60. package/dist/render/content-directive.js.map +1 -0
  61. package/dist/render/content.js +20 -9
  62. package/dist/render/content.js.map +1 -1
  63. package/dist/render/header.js +24 -5
  64. package/dist/render/header.js.map +1 -1
  65. package/dist/render/pinned-rows.js +1 -1
  66. package/dist/render/pinned-rows.js.map +1 -1
  67. package/dist/styles/base.js +1 -1
  68. package/dist/styles/base.js.map +1 -1
  69. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"virtual-controller.js","names":[],"sources":["../../src/controllers/virtual-controller.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit';\nimport { VirtualizerController } from '@tanstack/lit-virtual';\nimport { RowPool, ScrollScale, maxVisibleRows, steadyWindow } from '@argus-table/core';\n\n/** A rendered body row: its pool slot, its index in the row model and its offset. */\nexport interface WindowRow {\n slot: number;\n index: number;\n /** Offset in the rendered body, in px (scaled bodies: relative to the scroll position). */\n start: number;\n}\n\nexport interface VirtualWindow {\n /** Rendered rows, in pool order (ADR 0001 addendum). */\n rows: WindowRow[];\n /** Rendered height of the body in px: all rows, or the cap. */\n totalSize: number;\n /** Visible rows, without overscan; `null` before the first layout. */\n range: { start: number; end: number } | null;\n}\n\nexport interface VirtualConfig {\n count: number;\n rowHeight: number;\n overscan: number;\n /** Row id at an index of the row model; pool slots follow row ids. */\n rowIdAt: (index: number) => string;\n /**\n * The row to keep rendered outside the range (the one holding the grid's tab\n * stop), so its DOM and focus survive scrolling. `undefined`: none.\n */\n keep?: { id: string; index: number } | undefined;\n /** The tallest the body may render (`bodyCap()`); a taller one scrolls scaled. */\n cap: number;\n}\n\n/**\n * Row virtualization (TanStack Virtual) plus the row pool: which rows render,\n * where, and which pool slot's DOM each one uses. The virtualizer registers itself\n * with the host and requests updates on scroll.\n *\n * Scroll scaling: engines limit an element's height, so a body taller than the\n * cap renders at the cap and its scroll position maps linearly onto the rows\n * (`ScrollScale`). The virtualizer works in logical offsets: it gets a stand-in\n * for the scroller whose `scrollTop`, `scrollHeight` and `scrollTo()` are\n * logical, and everything else passes through. Rows render relative to the\n * scroll position (`start` minus the difference between the logical and the\n * physical offset). Scroll through this controller (`scrollBy`, `reveal`,\n * `scrollToIndex`): physical px no longer match rows once the body is scaled.\n */\nexport class VirtualController implements ReactiveController {\n readonly #virtual: VirtualizerController<HTMLElement, Element>;\n readonly #pool = new RowPool();\n readonly #scale = new ScrollScale();\n readonly #host: ReactiveControllerHost;\n readonly #getScrollElement: () => HTMLElement | null;\n #element: HTMLElement | undefined;\n #standIn: HTMLElement | undefined;\n #config: VirtualConfig = {\n count: 0,\n rowHeight: 40,\n overscan: 6,\n rowIdAt: String,\n cap: Infinity,\n };\n /** A logical offset to restore after the update, once the body has its new height. */\n #anchor: number | undefined;\n\n constructor(host: ReactiveControllerHost, getScrollElement: () => HTMLElement | null) {\n this.#host = host;\n this.#getScrollElement = getScrollElement;\n this.#virtual = new VirtualizerController<HTMLElement, Element>(host, {\n count: 0,\n getScrollElement: () => this.#scroller(),\n // The real element: a ResizeObserver can't observe the stand-in.\n observeElementRect: (_instance, cb) => {\n const element = this.#element;\n if (!element) return undefined;\n const report = () => {\n cb({ width: element.clientWidth, height: element.clientHeight });\n };\n report();\n const observer = new ResizeObserver(report);\n observer.observe(element);\n return () => {\n observer.disconnect();\n };\n },\n estimateSize: () => this.#config.rowHeight,\n overscan: this.#config.overscan,\n // One window size from the first render: the pool never grows while scrolling\n // (in WebKit, new rows' slots would cut a wheel scroll short; see steadyWindow).\n rangeExtractor: (range) =>\n steadyWindow(\n range,\n maxVisibleRows(this.#element?.clientHeight ?? 0, this.#config.rowHeight),\n ),\n });\n host.addController(this);\n }\n\n /** The scroller's stand-in for the virtualizer: vertical offsets and height in logical px. */\n #scroller(): HTMLElement | null {\n const element = this.#getScrollElement();\n if (!element) return null;\n if (this.#element !== element || !this.#standIn) {\n this.#element = element;\n this.#standIn = new Proxy(element, {\n get: (target, key) => {\n if (key === 'scrollTop') return this.#top(target);\n if (key === 'scrollHeight') return target.scrollHeight + this.#scale.excess;\n if (key === 'scrollTo') {\n return (options: ScrollToOptions) => {\n if (options.top === undefined) target.scrollTo(options);\n else this.#write(target, options.top, options.behavior);\n };\n }\n const value: unknown = Reflect.get(target, key);\n // Methods run on the element itself (DOM methods reject the proxy).\n return typeof value === 'function'\n ? (value as (...args: unknown[]) => unknown).bind(target)\n : value;\n },\n });\n }\n return this.#standIn;\n }\n\n /** The scroller's logical offset. */\n #top(element: HTMLElement): number {\n const top = element.scrollTop;\n const scale = this.#scale;\n return scale.scaled ? scale.toLogical(top, element.scrollHeight - element.clientHeight) : top;\n }\n\n /**\n * Scrolls to a logical offset. `round` rounds the physical one, so that a\n * scaled offset moves the scroller at all.\n */\n #write(\n element: HTMLElement,\n logical: number,\n behavior?: ScrollBehavior,\n round?: (px: number) => number,\n ): void {\n const scale = this.#scale;\n if (!scale.scaled) {\n element.scrollTo({ top: logical, behavior });\n return;\n }\n const range = element.scrollHeight - element.clientHeight;\n const target = Math.min(Math.max(logical, 0), range + scale.excess);\n const physical = scale.toPhysical(target, range);\n element.scrollTo({ top: round ? round(physical) : physical, behavior });\n if (behavior !== 'smooth') scale.land(target, element.scrollTop);\n }\n\n configure(next: VirtualConfig): void {\n const prev = this.#config;\n this.#config = next;\n if (\n next.count !== prev.count ||\n next.overscan !== prev.overscan ||\n next.rowHeight !== prev.rowHeight\n ) {\n this.#virtual.setOptions({ count: next.count, overscan: next.overscan });\n if (next.rowHeight !== prev.rowHeight) this.#virtual.getVirtualizer().measure();\n }\n // Not `scaled`: TypeScript would take it for unchanged after `configure()`.\n const wasScaled = this.#scale.excess > 0;\n const offset = this.#virtual.getVirtualizer().scrollOffset;\n if (this.#scale.configure(next.count * next.rowHeight, next.cap)) {\n // A new mapping would move the rows under a scaled scroll position (a data\n // source growing, a zoom change): keep the logical offset once the body\n // has its new height.\n if ((wasScaled || this.#scale.scaled) && offset !== null && offset > 0) {\n this.#anchor = offset;\n }\n }\n }\n\n hostUpdated(): void {\n const anchor = this.#anchor;\n if (anchor === undefined) return;\n this.#anchor = undefined;\n if (this.#element) this.#write(this.#element, anchor);\n // The logical offset is the same, so the virtualizer sees no change; the\n // rows still have to move to the new physical one.\n this.#host.requestUpdate();\n }\n\n /**\n * Computes the rendered window and updates the pool. Call once per render.\n */\n computeWindow(): VirtualWindow {\n const v = this.#virtual.getVirtualizer();\n const items = v.getVirtualItems();\n // Scaled: rows render relative to the scroll position. The physical offset is\n // read now, so rows for the virtualizer's offset fill the view even if the\n // scroller has moved on since its last scroll event.\n const scaled = this.#scale.scaled;\n const size = this.#scale.size;\n const shift = scaled ? (v.scrollOffset ?? 0) - (this.#element?.scrollTop ?? 0) : 0;\n const byId = new Map<string, { index: number; start: number }>();\n for (const item of items) {\n byId.set(this.#config.rowIdAt(item.index), { index: item.index, start: item.start - shift });\n }\n const { keep, rowHeight } = this.#config;\n this.#pool.pin(keep?.id);\n if (keep && !byId.has(keep.id)) {\n // Fixed row heights: the virtualizer would place it here too. Scaled, it is\n // out of view: keep it inside the body (or just above it, under the header),\n // so it doesn't stretch the scroll range past the cap.\n const start = keep.index * rowHeight - shift;\n byId.set(keep.id, {\n index: keep.index,\n start: scaled ? Math.min(Math.max(start, -rowHeight), size) : start,\n });\n }\n const rows: WindowRow[] = [];\n for (const { id, slot } of this.#pool.update(byId.keys())) {\n const item = byId.get(id);\n if (item) rows.push({ slot, ...item });\n }\n const range = v.range;\n return {\n rows,\n totalSize: scaled ? size : v.getTotalSize(),\n range: range ? { start: range.startIndex, end: range.endIndex } : null,\n };\n }\n\n /** Number of pool slots created so far: bounded by the most rows rendered at once. */\n get poolSize(): number {\n return this.#pool.size;\n }\n\n /** Whether the body is taller than the cap and scrolls scaled. */\n get scaled(): boolean {\n return this.#scale.scaled;\n }\n\n /** `'start'` puts the row at the top; `'auto'` scrolls only as far as needed to show it. */\n scrollToIndex(index: number, align: 'start' | 'auto' = 'start'): void {\n this.#virtual.getVirtualizer().scrollToIndex(index, { align });\n }\n\n /** Scrolls by `delta` px of rows (logical px). */\n scrollBy(delta: number): void {\n const element = this.#element;\n if (element) this.#write(element, this.#top(element) + delta);\n }\n\n /** Scrolls to the start or the end of the body. */\n scrollToEdge(edge: 'start' | 'end'): void {\n const element = this.#element;\n // The end: #write clamps to the last logical offset.\n if (element) this.#write(element, edge === 'start' ? 0 : Number.MAX_SAFE_INTEGER);\n }\n\n /**\n * Scrolls the least distance that shows row `index` in full, between the\n * content above the body (`before` px: the header and top pinned rows) and\n * below it (`after` px). Returns whether the rows are yet to move there (with\n * the update after the scroll event): until then the row's DOM isn't where it\n * will be.\n */\n reveal(index: number, before: number, after: number): boolean {\n const element = this.#element;\n if (!element) return false;\n const { rowHeight } = this.#config;\n const offset = this.#top(element);\n const view = element.clientHeight - before - after;\n const start = index * rowHeight;\n let target: number;\n let round: ((px: number) => number) | undefined;\n if (start < offset) {\n target = start;\n round = Math.floor;\n } else if (start + rowHeight > offset + view) {\n target = start + rowHeight - view;\n round = Math.ceil;\n } else {\n target = offset;\n }\n if (target !== offset) this.#write(element, target, undefined, round);\n // Rendered for an offset the scroller has since left: they move with the\n // update after the scroll event.\n return this.#virtual.getVirtualizer().scrollOffset !== this.#top(element);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"virtual-controller.js","names":[],"sources":["../../src/controllers/virtual-controller.ts"],"sourcesContent":["import type { ReactiveController, ReactiveControllerHost } from 'lit';\nimport { VirtualizerController } from '@tanstack/lit-virtual';\nimport { RowPool, RowSizes, ScrollScale, maxVisibleRows, steadyWindow } from '@argus-table/core';\n\n/** A rendered body row: its pool slot, its index in the row model and its offset. */\nexport interface WindowRow {\n slot: number;\n index: number;\n /** Offset in the rendered body, in px (scaled bodies: relative to the scroll position). */\n start: number;\n /** Measured row heights (`auto`): the row's height, the least it renders at. */\n size?: number;\n}\n\n/**\n * Engines place elements in a body taller than this (2^24 px) only to 2 px:\n * positions are 32-bit floats. Measured rows are whole px below it and an even\n * number of px above it, so every row starts on the grid and they stay flush.\n */\nconst COARSE_BODY = 16_000_000;\n\n/** How long scrolling has to be quiet before a held correction is written (see `#hold`). */\nconst HOLD_MS = 150;\n\nexport interface VirtualWindow {\n /** Rendered rows, in pool order (ADR 0001 addendum). */\n rows: WindowRow[];\n /** Rendered height of the body in px: all rows, or the cap. */\n totalSize: number;\n /** Visible rows, without overscan; `null` before the first layout. */\n range: { start: number; end: number } | null;\n}\n\n/** Measured row heights (`rowHeight: 'auto'`). */\nexport interface AutoRows {\n /** Changes identity whenever the row at an index may have changed (the model's `rowOrder`). */\n order: object;\n /** The index of a row id in the current order. */\n indexOf: (id: string) => number | undefined;\n}\n\nexport interface VirtualConfig {\n count: number;\n /** The row height; with `auto`, the least a row is and what an unmeasured row counts as. */\n rowHeight: number;\n overscan: number;\n /** Row id at an index of the row model; pool slots follow row ids. */\n rowIdAt: (index: number) => string;\n /**\n * The row to keep rendered outside the range (the one holding the grid's tab\n * stop), so its DOM and focus survive scrolling. `undefined`: none.\n */\n keep?: { id: string; index: number } | undefined;\n /** The tallest the body may render when it is `logical` px tall (`bodyCap()`); a taller one scrolls scaled. */\n cap: (logical: number) => number;\n /** Rows of different heights, measured as they render (`measureRows`). */\n auto?: AutoRows | undefined;\n}\n\n/**\n * Row virtualization (TanStack Virtual) plus the row pool: which rows render,\n * where, and which pool slot's DOM each one uses. The virtualizer registers itself\n * with the host and requests updates on scroll.\n *\n * Scroll scaling: engines limit an element's height, so a body taller than the\n * cap renders at the cap and its scroll position maps linearly onto the rows\n * (`ScrollScale`). The virtualizer works in logical offsets: it gets a stand-in\n * for the scroller whose `scrollTop`, `scrollHeight` and `scrollTo()` are\n * logical, and everything else passes through. Rows render relative to the\n * scroll position (`start` minus the difference between the logical and the\n * physical offset). Scroll through this controller (`scrollBy`, `reveal`,\n * `scrollToIndex`): physical px no longer match rows once the body is scaled.\n *\n * Rows of different heights (`auto`): TanStack's own measuring recomputes every\n * row below a measured one, too slow for a million rows, so in this mode the\n * virtualizer sits idle (no rows) and the controller does its part: the window\n * from `RowSizes` (heights by index, O(log n) offsets), a scroll listener that\n * re-renders when the window moves, and anchoring. Heights follow row ids, so\n * a sort or filter keeps them. A measured row above the top row would move\n * the rows in view: unscaled, the rows render shifted by the difference until\n * scrolling stops (`#hold`); scaled, the mapping pivots at the current offset.\n * Neither writes the scroll position while the user scrolls.\n */\nexport class VirtualController implements ReactiveController {\n readonly #virtual: VirtualizerController<HTMLElement, Element>;\n readonly #pool = new RowPool();\n readonly #scale = new ScrollScale();\n readonly #host: ReactiveControllerHost;\n readonly #getScrollElement: () => HTMLElement | null;\n #element: HTMLElement | undefined;\n #standIn: HTMLElement | undefined;\n #config: VirtualConfig = {\n count: 0,\n rowHeight: 40,\n overscan: 6,\n rowIdAt: String,\n cap: () => Infinity,\n };\n /** A logical offset to restore after the update, once the body has its new height. */\n #anchor: number | undefined;\n // Auto mode only.\n readonly #sizes = new RowSizes();\n /** Measured heights by row id (as read), so they follow rows through a sort or filter. */\n readonly #heights = new Map<string, number>();\n /** Rows are whole multiples of this many px (see `COARSE_BODY`). */\n #quantum = 1;\n /**\n * Unscaled: the logical offset less the scroller's. Rows measured above the\n * top row would move the rows in view; instead of writing the scroll position\n * (which cancels a wheel or touch scroll in progress, in WebKit at once) the\n * rows render shifted by this much, and it is written once scrolling has been\n * quiet for `HOLD_MS`.\n */\n #hold = 0;\n #holdTimer: ReturnType<typeof setTimeout> | undefined;\n /** The `order` the sizes are indexed for. */\n #order: object | undefined;\n /** The visible rows last rendered, and the logical offset they were rendered for. */\n #rendered: { start: number; end: number; top: number } | undefined;\n /** The scroller this controller listens to (auto mode). */\n #listening: HTMLElement | undefined;\n #resizeObserver: ResizeObserver | undefined;\n\n constructor(host: ReactiveControllerHost, getScrollElement: () => HTMLElement | null) {\n this.#host = host;\n this.#getScrollElement = getScrollElement;\n this.#virtual = new VirtualizerController<HTMLElement, Element>(host, {\n count: 0,\n getScrollElement: () => this.#scroller(),\n // The real element: a ResizeObserver can't observe the stand-in.\n observeElementRect: (_instance, cb) => {\n const element = this.#element;\n if (!element) return undefined;\n const report = () => {\n cb({ width: element.clientWidth, height: element.clientHeight });\n };\n report();\n const observer = new ResizeObserver(report);\n observer.observe(element);\n return () => {\n observer.disconnect();\n };\n },\n estimateSize: () => this.#config.rowHeight,\n overscan: this.#config.overscan,\n // One window size from the first render: the pool never grows while scrolling\n // (in WebKit, new rows' slots would cut a wheel scroll short; see steadyWindow).\n rangeExtractor: (range) =>\n steadyWindow(\n range,\n maxVisibleRows(this.#element?.clientHeight ?? 0, this.#config.rowHeight),\n ),\n });\n host.addController(this);\n }\n\n /** The scroller's stand-in for the virtualizer: vertical offsets and height in logical px. */\n #scroller(): HTMLElement | null {\n const element = this.#getScrollElement();\n if (!element) return null;\n if (this.#element !== element || !this.#standIn) {\n this.#element = element;\n this.#standIn = new Proxy(element, {\n get: (target, key) => {\n if (key === 'scrollTop') return this.#top(target);\n if (key === 'scrollHeight') return target.scrollHeight + this.#scale.excess;\n if (key === 'scrollTo') {\n return (options: ScrollToOptions) => {\n if (options.top === undefined) target.scrollTo(options);\n else this.#write(target, options.top, options.behavior);\n };\n }\n const value: unknown = Reflect.get(target, key);\n // Methods run on the element itself (DOM methods reject the proxy).\n return typeof value === 'function'\n ? (value as (...args: unknown[]) => unknown).bind(target)\n : value;\n },\n });\n }\n return this.#standIn;\n }\n\n /** The scroller's logical offset. */\n #top(element: HTMLElement): number {\n const top = element.scrollTop;\n const scale = this.#scale;\n return scale.scaled\n ? scale.toLogical(top, element.scrollHeight - element.clientHeight)\n : top + this.#hold;\n }\n\n /**\n * Scrolls to a logical offset. `round` rounds the physical one, so that a\n * scaled offset moves the scroller at all.\n */\n #write(\n element: HTMLElement,\n logical: number,\n behavior?: ScrollBehavior,\n round?: (px: number) => number,\n ): void {\n const scale = this.#scale;\n if (!scale.scaled) {\n this.#dropHold();\n element.scrollTo({ top: logical, behavior });\n return;\n }\n const range = element.scrollHeight - element.clientHeight;\n const target = Math.min(Math.max(logical, 0), range + scale.excess);\n const physical = scale.toPhysical(target, range);\n element.scrollTo({ top: round ? round(physical) : physical, behavior });\n if (behavior !== 'smooth') scale.land(target, element.scrollTop);\n }\n\n configure(next: VirtualConfig): void {\n const prev = this.#config;\n this.#config = next;\n if (next.auto) {\n this.#configureAuto(next.auto, prev);\n return;\n }\n if (prev.auto) {\n // Back to fixed heights: the virtualizer takes over again.\n this.#heights.clear();\n this.#sizes.reset(0, next.rowHeight);\n this.#order = undefined;\n this.#rendered = undefined;\n this.#dropHold();\n this.#virtual.setOptions({ count: next.count, overscan: next.overscan });\n this.#virtual.getVirtualizer().measure();\n } else if (\n next.count !== prev.count ||\n next.overscan !== prev.overscan ||\n next.rowHeight !== prev.rowHeight\n ) {\n this.#virtual.setOptions({ count: next.count, overscan: next.overscan });\n if (next.rowHeight !== prev.rowHeight) this.#virtual.getVirtualizer().measure();\n }\n // Not `scaled`: TypeScript would take it for unchanged after `configure()`.\n const wasScaled = this.#scale.excess > 0;\n const offset = this.#virtual.getVirtualizer().scrollOffset;\n const logical = next.count * next.rowHeight;\n if (this.#scale.configure(logical, next.cap(logical))) {\n // A new mapping would move the rows under a scaled scroll position (a data\n // source growing, a zoom change): keep the logical offset once the body\n // has its new height.\n if ((wasScaled || this.#scale.scaled) && offset !== null && offset > 0) {\n this.#anchor = offset;\n }\n }\n }\n\n #configureAuto(auto: AutoRows, prev: VirtualConfig): void {\n const next = this.#config;\n if (!prev.auto) this.#virtual.setOptions({ count: 0 });\n const sizes = this.#sizes;\n if (!prev.auto || next.rowHeight !== prev.rowHeight) {\n // A new minimum height (density, a theme): every row measures again.\n this.#heights.clear();\n this.#order = undefined;\n }\n const reset = () => {\n sizes.reset(next.count, this.#fit(0), this.#known(auto, next.count));\n };\n if (auto.order !== this.#order) {\n this.#order = auto.order;\n reset();\n } else if (next.count !== sizes.count) {\n // A data source counting more rows: they keep their indexes.\n sizes.resize(next.count);\n }\n // A body past 2^24 px needs even heights: fit the rows to the new grid.\n const quantum = this.#quantumFor(sizes.total);\n if (quantum !== this.#quantum) {\n this.#quantum = quantum;\n reset();\n }\n const element = this.#element;\n const wasScaled = this.#scale.excess > 0;\n const offset = element ? this.#top(element) : 0;\n const logical = sizes.total;\n if (this.#scale.configure(logical, next.cap(logical))) {\n if ((wasScaled || this.#scale.scaled) && offset > 0) this.#anchor = offset;\n }\n }\n\n /** The measured heights by index in the current order. Forgets rows long gone. */\n *#known(auto: AutoRows, count: number): Generator<[number, number]> {\n const heights = this.#heights;\n const prune = heights.size > 2 * count + 1000;\n for (const [id, px] of heights) {\n const index = auto.indexOf(id);\n if (index !== undefined && index < count) yield [index, this.#fit(px)];\n else if (prune) heights.delete(id);\n }\n }\n\n /** The px grid rows are on for a body `logical` px tall (see `COARSE_BODY`). */\n #quantumFor(logical: number): number {\n return Math.min(logical, this.#config.cap(logical)) > COARSE_BODY ? 2 : 1;\n }\n\n /** A measured height as a row renders it: on the grid, at least the row height. */\n #fit(px: number): number {\n const q = this.#quantum;\n const up = (value: number) => Math.ceil(value / q - 0.01) * q;\n return Math.max(up(px), up(this.#config.rowHeight));\n }\n\n hostUpdated(): void {\n this.#listen();\n const anchor = this.#anchor;\n if (anchor === undefined) return;\n this.#anchor = undefined;\n if (this.#element) this.#write(this.#element, anchor);\n // The logical offset is the same, so the virtualizer sees no change; the\n // rows still have to move to the new physical one.\n this.#host.requestUpdate();\n }\n\n hostDisconnected(): void {\n this.#unlisten();\n }\n\n /**\n * Auto mode: the virtualizer has no rows, so it reports neither scrolling nor\n * a new viewport size. Listen to the scroller here.\n */\n #listen(): void {\n const element = this.#config.auto ? this.#getScrollElement() : null;\n if (element === (this.#listening ?? null)) return;\n this.#unlisten();\n if (!element) return;\n this.#scroller();\n this.#listening = element;\n element.addEventListener('scroll', this.#onScroll, { passive: true });\n this.#resizeObserver ??= new ResizeObserver(this.#onScroll);\n // Reports the size once right away, which renders the first rows.\n this.#resizeObserver.observe(element);\n }\n\n #unlisten(): void {\n this.#listening?.removeEventListener('scroll', this.#onScroll);\n this.#resizeObserver?.disconnect();\n this.#listening = undefined;\n this.#dropHold();\n }\n\n #dropHold(): void {\n clearTimeout(this.#holdTimer);\n this.#holdTimer = undefined;\n this.#hold = 0;\n }\n\n /** Writes the held correction: the rows stay where they are on screen. */\n readonly #flushHold = () => {\n this.#holdTimer = undefined;\n const element = this.#element;\n if (!element || this.#hold === 0) return;\n const logical = this.#top(element);\n this.#hold = 0;\n element.scrollTo({ top: logical });\n this.#host.requestUpdate();\n };\n\n /** Re-renders when the visible rows have changed. */\n readonly #onScroll = () => {\n if (this.#holdTimer !== undefined) {\n clearTimeout(this.#holdTimer);\n this.#holdTimer = setTimeout(this.#flushHold, HOLD_MS);\n }\n const visible = this.#visible();\n const rendered = this.#rendered;\n if (visible?.start !== rendered?.start || visible?.end !== rendered?.end) {\n this.#host.requestUpdate();\n }\n };\n\n /** Auto mode: the visible rows (without overscan) and the logical offset, or `undefined` before layout. */\n #visible(): { start: number; end: number; top: number } | undefined {\n const element = this.#element;\n const sizes = this.#sizes;\n const view = element?.clientHeight ?? 0;\n if (!element || sizes.count === 0 || view <= 0) return undefined;\n const top = this.#top(element);\n const start = sizes.indexAt(top);\n // The last row that starts above the bottom edge.\n const end = Math.max(start, sizes.indexAt(top + view - 0.5));\n return { start, end, top };\n }\n\n /**\n * Computes the rendered window and updates the pool. Call once per render.\n */\n computeWindow(): VirtualWindow {\n if (this.#config.auto) return this.#autoWindow();\n const v = this.#virtual.getVirtualizer();\n const items = v.getVirtualItems();\n // Scaled: rows render relative to the scroll position. The physical offset is\n // read now, so rows for the virtualizer's offset fill the view even if the\n // scroller has moved on since its last scroll event.\n const scaled = this.#scale.scaled;\n const size = this.#scale.size;\n const shift = scaled ? (v.scrollOffset ?? 0) - (this.#element?.scrollTop ?? 0) : 0;\n const byId = new Map<string, { index: number; start: number }>();\n for (const item of items) {\n byId.set(this.#config.rowIdAt(item.index), { index: item.index, start: item.start - shift });\n }\n const { keep, rowHeight } = this.#config;\n this.#pool.pin(keep?.id);\n if (keep && !byId.has(keep.id)) {\n // Fixed row heights: the virtualizer would place it here too. Scaled, it is\n // out of view: keep it inside the body (or just above it, under the header),\n // so it doesn't stretch the scroll range past the cap.\n const start = keep.index * rowHeight - shift;\n byId.set(keep.id, {\n index: keep.index,\n start: scaled ? Math.min(Math.max(start, -rowHeight), size) : start,\n });\n }\n const rows: WindowRow[] = [];\n for (const { id, slot } of this.#pool.update(byId.keys())) {\n const item = byId.get(id);\n if (item) rows.push({ slot, ...item });\n }\n const range = v.range;\n return {\n rows,\n totalSize: scaled ? size : v.getTotalSize(),\n range: range ? { start: range.startIndex, end: range.endIndex } : null,\n };\n }\n\n /** `computeWindow()` for rows of different heights. */\n #autoWindow(): VirtualWindow {\n const { count, overscan, rowHeight, keep, rowIdAt } = this.#config;\n const sizes = this.#sizes;\n const scale = this.#scale;\n const element = this.#element;\n const visible = this.#visible();\n this.#rendered = visible;\n // Scaled: rows render relative to the scroll position. Unscaled: shifted by the held correction.\n const shift = visible && element ? visible.top - element.scrollTop : 0;\n const byId = new Map<string, { index: number; start: number; size?: number }>();\n if (visible && element) {\n const indexes = steadyWindow(\n { startIndex: visible.start, endIndex: visible.end, overscan, count },\n maxVisibleRows(element.clientHeight, rowHeight),\n );\n const first = indexes[0];\n let start = first === undefined ? 0 : sizes.startOf(first);\n for (const index of indexes) {\n const size = sizes.sizeOf(index);\n byId.set(rowIdAt(index), { index, start: start - shift, size });\n start += size;\n }\n }\n this.#pool.pin(keep?.id);\n if (keep && !byId.has(keep.id) && keep.index < count) {\n // Scaled, it is out of view: keep it inside the body (or just above it).\n const size = sizes.sizeOf(keep.index);\n const start = sizes.startOf(keep.index) - shift;\n byId.set(keep.id, {\n index: keep.index,\n start: scale.scaled ? Math.min(Math.max(start, -size), scale.size) : start,\n size,\n });\n }\n const rows: WindowRow[] = [];\n for (const { id, slot } of this.#pool.update(byId.keys())) {\n const item = byId.get(id);\n if (item) rows.push({ slot, ...item });\n }\n return {\n rows,\n totalSize: scale.scaled ? scale.size : sizes.total,\n range: visible ? { start: visible.start, end: visible.end } : null,\n };\n }\n\n /**\n * Auto mode: records the heights of rendered body rows (`[index, px]`), read\n * once their content has rendered. Heights under the row height count as it.\n * Returns whether any changed; the controller then requests the update that\n * places the rows. Rows above the top row keep the rows in view in place.\n */\n measureRows(measured: Iterable<readonly [number, number]>): boolean {\n const { auto, rowIdAt, cap } = this.#config;\n if (!auto) return false;\n const sizes = this.#sizes;\n const element = this.#element;\n const top = element ? this.#top(element) : 0;\n const anchor = sizes.indexAt(top);\n const within = top - sizes.startOf(anchor);\n let changed = false;\n for (const [index, px] of measured) {\n if (index < 0 || index >= sizes.count || !(px > 0)) continue;\n this.#heights.set(rowIdAt(index), px);\n if (sizes.set(index, this.#fit(px)) !== 0) changed = true;\n }\n if (!changed) return false;\n const target = sizes.startOf(anchor) + within;\n const scale = this.#scale;\n const wasScaled = scale.excess > 0;\n const logical = sizes.total;\n if (scale.configure(logical, cap(logical)) && element) {\n if (wasScaled && scale.scaled) {\n // Keep the rows in view where they are without moving the scroller.\n scale.pivot(element.scrollTop, target, element.scrollHeight - element.clientHeight);\n } else if (wasScaled !== scale.scaled) {\n // Once the body has its new height (the write would clamp to the old one).\n this.#anchor = target;\n } else if (Math.abs(target - top) > 0.01) {\n this.#hold += target - top;\n clearTimeout(this.#holdTimer);\n this.#holdTimer = setTimeout(this.#flushHold, HOLD_MS);\n }\n }\n this.#host.requestUpdate();\n return true;\n }\n\n /** The height of the row at `index` (auto mode: measured, or the row height). */\n sizeOf(index: number): number {\n return this.#config.auto ? this.#sizes.sizeOf(index) : this.#config.rowHeight;\n }\n\n /** The logical offset of the row at `index` in the body. */\n startOf(index: number): number {\n return this.#config.auto ? this.#sizes.startOf(index) : index * this.#config.rowHeight;\n }\n\n /**\n * Auto mode: how many rows a page moves from row `from`, the rows that fit in\n * `view` px below it (`down`) or above it. At least one.\n */\n pageRows(from: number, down: boolean, view: number): number {\n const sizes = this.#sizes;\n const start = sizes.startOf(from);\n if (down) return Math.max(1, sizes.indexAt(start + view + 0.01) - from);\n // The first row that starts at most `view` px above this one.\n let to = sizes.indexAt(start - view);\n if (sizes.startOf(to) < start - view - 0.01) to += 1;\n return Math.max(1, from - to);\n }\n\n /** Number of pool slots created so far: bounded by the most rows rendered at once. */\n get poolSize(): number {\n return this.#pool.size;\n }\n\n /** Whether the body is taller than the cap and scrolls scaled. */\n get scaled(): boolean {\n return this.#scale.scaled;\n }\n\n /** The index of the row at the top of the body (0 before the first layout). */\n get topRow(): number {\n const element = this.#element;\n const { count, rowHeight } = this.#config;\n if (!element || count === 0 || rowHeight <= 0) return 0;\n if (this.#config.auto) return this.#sizes.indexAt(this.#top(element) + 0.5);\n // Half a px of slack: engines round scroll offsets to device pixels.\n const row = Math.floor((this.#top(element) + 0.5) / rowHeight);\n return Math.min(Math.max(row, 0), count - 1);\n }\n\n /** `'start'` puts the row at the top; `'auto'` scrolls only as far as needed to show it. */\n scrollToIndex(index: number, align: 'start' | 'auto' = 'start'): void {\n if (this.#config.auto) {\n const element = this.#element;\n if (!element) return;\n if (align === 'auto') {\n this.reveal(index, 0, 0);\n } else {\n this.#write(element, this.#sizes.startOf(Math.min(Math.max(index, 0), this.#sizes.count)));\n }\n this.#host.requestUpdate();\n return;\n }\n this.#virtual.getVirtualizer().scrollToIndex(index, { align });\n }\n\n /** Scrolls by `delta` px of rows (logical px). */\n scrollBy(delta: number): void {\n const element = this.#element;\n if (element) this.#write(element, this.#top(element) + delta);\n }\n\n /** Scrolls to the start or the end of the body. */\n scrollToEdge(edge: 'start' | 'end'): void {\n const element = this.#element;\n // The end: #write clamps to the last logical offset.\n if (element) this.#write(element, edge === 'start' ? 0 : Number.MAX_SAFE_INTEGER);\n }\n\n /**\n * Scrolls the least distance that shows row `index` in full, between the\n * content above the body (`before` px: the header and top pinned rows) and\n * below it (`after` px). Returns whether the rows are yet to move there (with\n * the update after the scroll event): until then the row's DOM isn't where it\n * will be.\n */\n reveal(index: number, before: number, after: number): boolean {\n const element = this.#element;\n if (!element) return false;\n const auto = this.#config.auto !== undefined;\n const offset = this.#top(element);\n const view = element.clientHeight - before - after;\n const start = this.startOf(index);\n const size = this.sizeOf(index);\n let target: number;\n let round: ((px: number) => number) | undefined;\n // A row taller than the view shows its start.\n if (start < offset || (auto && size > view)) {\n target = start;\n round = Math.floor;\n } else if (start + size > offset + view) {\n target = start + size - view;\n round = Math.ceil;\n } else {\n target = offset;\n }\n if (target !== offset) this.#write(element, target, undefined, round);\n // Rendered for an offset the scroller has since left: they move with the\n // update after the scroll event.\n const rendered = auto ? this.#rendered?.top : this.#virtual.getVirtualizer().scrollOffset;\n return rendered !== this.#top(element);\n }\n}\n"],"mappings":"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"}
@@ -27,23 +27,27 @@ function normalizeOrder(order, columns) {
27
27
  ];
28
28
  }
29
29
  /**
30
- * The order after moving a column to position `to` (0-based, in the whole order
31
- * as the table shows it, counted after the move). `undefined` if the column can't
32
- * move, `to` is outside the column's group, or nothing changes.
30
+ * The order after moving a column to position `to`: 0-based among the columns
31
+ * `shown` accepts (hidden columns don't count), in the order the table shows them,
32
+ * counted after the move. The column lands just before the shown column now at `to`,
33
+ * hidden columns keep their place among the others. `undefined` if the column can't
34
+ * move or isn't shown, `to` is outside the column's group, or nothing changes.
33
35
  */
34
- function moveColumn(order, columns, columnId, to) {
36
+ function moveColumn(order, columns, columnId, to, shown = () => true) {
35
37
  const column = columns.find((c) => c.id === columnId);
36
- if (!column?.reorderable || !Number.isInteger(to)) return void 0;
38
+ if (!column?.reorderable || !Number.isInteger(to) || !shown(columnId)) return void 0;
37
39
  const current = normalizeOrder(order, columns);
38
- if (to === current.indexOf(columnId)) return void 0;
40
+ const visible = current.filter(shown);
41
+ if (to === visible.indexOf(columnId)) return void 0;
39
42
  const pinOf = new Map(columns.map((c) => [c.id, groupOf(c)]));
40
43
  const group = groupOf(column);
41
- const first = current.findIndex((id) => pinOf.get(id) === group);
42
- const last = current.findLastIndex((id) => pinOf.get(id) === group);
44
+ const first = visible.findIndex((id) => pinOf.get(id) === group);
45
+ const last = visible.findLastIndex((id) => pinOf.get(id) === group);
43
46
  if (to < first || to > last) return void 0;
44
47
  const next = current.filter((id) => id !== columnId);
45
- next.splice(to, 0, columnId);
46
- return next;
48
+ const before = visible.filter((id) => id !== columnId)[to];
49
+ next.splice(before === void 0 ? next.length : next.indexOf(before), 0, columnId);
50
+ return normalizeOrder(next, columns);
47
51
  }
48
52
  export { moveColumn, normalizeOrder };
49
53
 
@@ -1 +1 @@
1
- {"version":3,"file":"column-order.js","names":[],"sources":["../../../../../core/src/columns/column-order.ts"],"sourcesContent":["import type { ColumnOrderState, ColumnPin } from '../types.js';\n\n/** What ordering needs to know about a column. */\nexport interface OrderableColumn {\n id: string;\n pin: ColumnPin | undefined;\n reorderable: boolean;\n}\n\ntype Group = ColumnPin | 'center';\n\nconst groupOf = (column: OrderableColumn): Group => column.pin ?? 'center';\n\n/**\n * The order the table shows: start-pinned, scrolling, end-pinned columns, each\n * group in the sequence of `order`. Unknown and repeated ids are dropped; columns\n * the order doesn't list follow the listed ones of their group, in `columns` order.\n */\nexport function normalizeOrder(\n order: readonly string[],\n columns: readonly OrderableColumn[],\n): ColumnOrderState {\n const byId = new Map(columns.map((c) => [c.id, c]));\n const listed = new Set<string>();\n const groups: Record<Group, string[]> = { start: [], center: [], end: [] };\n for (const id of order) {\n const column = byId.get(id);\n if (!column || listed.has(id)) continue;\n listed.add(id);\n groups[groupOf(column)].push(id);\n }\n for (const column of columns) {\n if (!listed.has(column.id)) groups[groupOf(column)].push(column.id);\n }\n return [...groups.start, ...groups.center, ...groups.end];\n}\n\n/**\n * The order after moving a column to position `to` (0-based, in the whole order\n * as the table shows it, counted after the move). `undefined` if the column can't\n * move, `to` is outside the column's group, or nothing changes.\n */\nexport function moveColumn(\n order: readonly string[],\n columns: readonly OrderableColumn[],\n columnId: string,\n to: number,\n): ColumnOrderState | undefined {\n const column = columns.find((c) => c.id === columnId);\n if (!column?.reorderable || !Number.isInteger(to)) return undefined;\n const current = normalizeOrder(order, columns);\n const from = current.indexOf(columnId);\n if (to === from) return undefined;\n const pinOf = new Map(columns.map((c) => [c.id, groupOf(c)]));\n const group = groupOf(column);\n const first = current.findIndex((id) => pinOf.get(id) === group);\n const last = current.findLastIndex((id) => pinOf.get(id) === group);\n if (to < first || to > last) return undefined;\n const next = current.filter((id) => id !== columnId);\n next.splice(to, 0, columnId);\n return next;\n}\n"],"mappings":";AAWA,IAAM,WAAW,WAAmC,OAAO,OAAO;;;;;;AAOlE,SAAgB,eACd,OACA,SACkB;CAClB,MAAM,OAAO,IAAI,IAAI,QAAQ,KAAK,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;CAClD,MAAM,yBAAS,IAAI,IAAY;CAC/B,MAAM,SAAkC;EAAE,OAAO,CAAC;EAAG,QAAQ,CAAC;EAAG,KAAK,CAAC;CAAE;CACzE,KAAK,MAAM,MAAM,OAAO;EACtB,MAAM,SAAS,KAAK,IAAI,EAAE;EAC1B,IAAI,CAAC,UAAU,OAAO,IAAI,EAAE,GAAG;EAC/B,OAAO,IAAI,EAAE;EACb,OAAO,QAAQ,MAAM,EAAE,CAAC,KAAK,EAAE;CACjC;CACA,KAAK,MAAM,UAAU,SACnB,IAAI,CAAC,OAAO,IAAI,OAAO,EAAE,GAAG,OAAO,QAAQ,MAAM,EAAE,CAAC,KAAK,OAAO,EAAE;CAEpE,OAAO;EAAC,GAAG,OAAO;EAAO,GAAG,OAAO;EAAQ,GAAG,OAAO;CAAG;AAC1D;;;;;;AAOA,SAAgB,WACd,OACA,SACA,UACA,IAC8B;CAC9B,MAAM,SAAS,QAAQ,MAAM,MAAM,EAAE,OAAO,QAAQ;CACpD,IAAI,CAAC,QAAQ,eAAe,CAAC,OAAO,UAAU,EAAE,GAAG,OAAO,KAAA;CAC1D,MAAM,UAAU,eAAe,OAAO,OAAO;CAE7C,IAAI,OADS,QAAQ,QAAQ,QAClB,GAAM,OAAO,KAAA;CACxB,MAAM,QAAQ,IAAI,IAAI,QAAQ,KAAK,MAAM,CAAC,EAAE,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC;CAC5D,MAAM,QAAQ,QAAQ,MAAM;CAC5B,MAAM,QAAQ,QAAQ,WAAW,OAAO,MAAM,IAAI,EAAE,MAAM,KAAK;CAC/D,MAAM,OAAO,QAAQ,eAAe,OAAO,MAAM,IAAI,EAAE,MAAM,KAAK;CAClE,IAAI,KAAK,SAAS,KAAK,MAAM,OAAO,KAAA;CACpC,MAAM,OAAO,QAAQ,QAAQ,OAAO,OAAO,QAAQ;CACnD,KAAK,OAAO,IAAI,GAAG,QAAQ;CAC3B,OAAO;AACT"}
1
+ {"version":3,"file":"column-order.js","names":[],"sources":["../../../../../core/src/columns/column-order.ts"],"sourcesContent":["import type { ColumnOrderState, ColumnPin } from '../types.js';\n\n/** What ordering needs to know about a column. */\nexport interface OrderableColumn {\n id: string;\n pin: ColumnPin | undefined;\n reorderable: boolean;\n}\n\ntype Group = ColumnPin | 'center';\n\nconst groupOf = (column: OrderableColumn): Group => column.pin ?? 'center';\n\n/**\n * The order the table shows: start-pinned, scrolling, end-pinned columns, each\n * group in the sequence of `order`. Unknown and repeated ids are dropped; columns\n * the order doesn't list follow the listed ones of their group, in `columns` order.\n */\nexport function normalizeOrder(\n order: readonly string[],\n columns: readonly OrderableColumn[],\n): ColumnOrderState {\n const byId = new Map(columns.map((c) => [c.id, c]));\n const listed = new Set<string>();\n const groups: Record<Group, string[]> = { start: [], center: [], end: [] };\n for (const id of order) {\n const column = byId.get(id);\n if (!column || listed.has(id)) continue;\n listed.add(id);\n groups[groupOf(column)].push(id);\n }\n for (const column of columns) {\n if (!listed.has(column.id)) groups[groupOf(column)].push(column.id);\n }\n return [...groups.start, ...groups.center, ...groups.end];\n}\n\n/**\n * The order after moving a column to position `to`: 0-based among the columns\n * `shown` accepts (hidden columns don't count), in the order the table shows them,\n * counted after the move. The column lands just before the shown column now at `to`,\n * hidden columns keep their place among the others. `undefined` if the column can't\n * move or isn't shown, `to` is outside the column's group, or nothing changes.\n */\nexport function moveColumn(\n order: readonly string[],\n columns: readonly OrderableColumn[],\n columnId: string,\n to: number,\n shown: (id: string) => boolean = () => true,\n): ColumnOrderState | undefined {\n const column = columns.find((c) => c.id === columnId);\n if (!column?.reorderable || !Number.isInteger(to) || !shown(columnId)) return undefined;\n const current = normalizeOrder(order, columns);\n const visible = current.filter(shown);\n const from = visible.indexOf(columnId);\n if (to === from) return undefined;\n const pinOf = new Map(columns.map((c) => [c.id, groupOf(c)]));\n const group = groupOf(column);\n const first = visible.findIndex((id) => pinOf.get(id) === group);\n const last = visible.findLastIndex((id) => pinOf.get(id) === group);\n if (to < first || to > last) return undefined;\n const next = current.filter((id) => id !== columnId);\n const before = visible.filter((id) => id !== columnId)[to];\n // Before the shown column now at `to`: one of the next group's, at the end of a\n // group, and normalizing puts the column back at the end of its own.\n next.splice(before === undefined ? next.length : next.indexOf(before), 0, columnId);\n return normalizeOrder(next, columns);\n}\n"],"mappings":";AAWA,IAAM,WAAW,WAAmC,OAAO,OAAO;;;;;;AAOlE,SAAgB,eACd,OACA,SACkB;CAClB,MAAM,OAAO,IAAI,IAAI,QAAQ,KAAK,MAAM,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC;CAClD,MAAM,yBAAS,IAAI,IAAY;CAC/B,MAAM,SAAkC;EAAE,OAAO,CAAC;EAAG,QAAQ,CAAC;EAAG,KAAK,CAAC;CAAE;CACzE,KAAK,MAAM,MAAM,OAAO;EACtB,MAAM,SAAS,KAAK,IAAI,EAAE;EAC1B,IAAI,CAAC,UAAU,OAAO,IAAI,EAAE,GAAG;EAC/B,OAAO,IAAI,EAAE;EACb,OAAO,QAAQ,MAAM,EAAE,CAAC,KAAK,EAAE;CACjC;CACA,KAAK,MAAM,UAAU,SACnB,IAAI,CAAC,OAAO,IAAI,OAAO,EAAE,GAAG,OAAO,QAAQ,MAAM,EAAE,CAAC,KAAK,OAAO,EAAE;CAEpE,OAAO;EAAC,GAAG,OAAO;EAAO,GAAG,OAAO;EAAQ,GAAG,OAAO;CAAG;AAC1D;;;;;;;;AASA,SAAgB,WACd,OACA,SACA,UACA,IACA,cAAuC,MACT;CAC9B,MAAM,SAAS,QAAQ,MAAM,MAAM,EAAE,OAAO,QAAQ;CACpD,IAAI,CAAC,QAAQ,eAAe,CAAC,OAAO,UAAU,EAAE,KAAK,CAAC,MAAM,QAAQ,GAAG,OAAO,KAAA;CAC9E,MAAM,UAAU,eAAe,OAAO,OAAO;CAC7C,MAAM,UAAU,QAAQ,OAAO,KAAK;CAEpC,IAAI,OADS,QAAQ,QAAQ,QAClB,GAAM,OAAO,KAAA;CACxB,MAAM,QAAQ,IAAI,IAAI,QAAQ,KAAK,MAAM,CAAC,EAAE,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC;CAC5D,MAAM,QAAQ,QAAQ,MAAM;CAC5B,MAAM,QAAQ,QAAQ,WAAW,OAAO,MAAM,IAAI,EAAE,MAAM,KAAK;CAC/D,MAAM,OAAO,QAAQ,eAAe,OAAO,MAAM,IAAI,EAAE,MAAM,KAAK;CAClE,IAAI,KAAK,SAAS,KAAK,MAAM,OAAO,KAAA;CACpC,MAAM,OAAO,QAAQ,QAAQ,OAAO,OAAO,QAAQ;CACnD,MAAM,SAAS,QAAQ,QAAQ,OAAO,OAAO,QAAQ,CAAC,CAAC;CAGvD,KAAK,OAAO,WAAW,KAAA,IAAY,KAAK,SAAS,KAAK,QAAQ,MAAM,GAAG,GAAG,QAAQ;CAClF,OAAO,eAAe,MAAM,OAAO;AACrC"}
@@ -1,3 +1,9 @@
1
+ var FILTER_KINDS = [
2
+ "text",
3
+ "number",
4
+ "date",
5
+ "set"
6
+ ];
1
7
  var pathCache = /* @__PURE__ */ new Map();
2
8
  /**
3
9
  * Reads a dotted property path such as `'customer.name'`. Returns `undefined`
@@ -35,10 +41,10 @@ function resolveColumns(defs) {
35
41
  const clamp = (px) => Math.min(maxWidth, Math.max(minWidth, px));
36
42
  const pin = def.pin === "start" || def.pin === "end" ? def.pin : void 0;
37
43
  const { width } = def;
38
- const flex = pin === void 0 && typeof width === "object" && Number.isFinite(width.flex) && width.flex > 0 ? width.flex : void 0;
44
+ const flex = typeof width === "object" && Number.isFinite(width.flex) && width.flex > 0 ? width.flex : void 0;
39
45
  const auto = width === "auto";
40
- const fixed = typeof width === "number" ? finite(width) : void 0;
41
- const base = flex !== void 0 ? minWidth : clamp(fixed ?? 150);
46
+ const pinnedWidth = clamp((typeof width === "number" ? finite(width) : void 0) ?? 150);
47
+ const base = flex !== void 0 ? minWidth : pinnedWidth;
42
48
  const { accessor } = def;
43
49
  const getValue = typeof accessor === "function" ? accessor : typeof accessor === "string" ? (row) => readPath(row, accessor) : () => void 0;
44
50
  return {
@@ -50,14 +56,22 @@ function resolveColumns(defs) {
50
56
  hasAccessor: accessor !== void 0,
51
57
  width: base,
52
58
  flex,
59
+ pinnedWidth,
53
60
  auto,
54
61
  minWidth,
55
62
  maxWidth,
56
63
  pin,
64
+ hidden: def.hidden === true,
57
65
  sortable: def.sortable === true,
58
66
  resizable: def.resizable !== false,
59
67
  reorderable: def.reorderable !== false,
60
- align: def.align ?? "start"
68
+ pinnable: def.pinnable !== false,
69
+ hideable: def.hideable !== false,
70
+ filter: FILTER_KINDS.includes(def.filter) ? def.filter : void 0,
71
+ filterFn: def.filterFn,
72
+ filterOptions: def.filterOptions,
73
+ align: def.align ?? "start",
74
+ wrap: def.wrap === true
61
75
  };
62
76
  });
63
77
  }
@@ -1 +1 @@
1
- {"version":3,"file":"resolve-columns.js","names":[],"sources":["../../../../../core/src/columns/resolve-columns.ts"],"sourcesContent":["import type { ColumnAlign, ColumnDef, ColumnPin } from '../types.js';\n\n/** Width used when a column sets none. */\nexport const DEFAULT_WIDTH = 150;\n/** Lower bound used when a column sets no `minWidth`. */\nexport const DEFAULT_MIN_WIDTH = 40;\n\n/** A column after defaults, bounds and the accessor have been resolved. */\nexport interface ResolvedColumn<TData, TContent> {\n id: string;\n /** Position in the `columns` array. */\n index: number;\n header: ColumnDef<TData, TContent>['header'];\n cell: ColumnDef<TData, TContent>['cell'];\n /** Reads the cell value; returns `undefined` when the column has no accessor. */\n getValue: (row: TData, index: number) => unknown;\n hasAccessor: boolean;\n /** Base width in px. For a flex column this is its minimum track size. */\n width: number;\n /** Share of free space for `{ flex }` columns, otherwise `undefined`. Pinned columns never flex. */\n flex: number | undefined;\n /** `true` for `width: 'auto'`: the table measures the content and passes the width in. */\n auto: boolean;\n minWidth: number;\n maxWidth: number;\n pin: ColumnPin | undefined;\n sortable: boolean;\n resizable: boolean;\n reorderable: boolean;\n align: ColumnAlign;\n}\n\nconst pathCache = new Map<string, readonly string[]>();\n\n/**\n * Reads a dotted property path such as `'customer.name'`. Returns `undefined`\n * as soon as a step is `null` or `undefined`, like optional chaining.\n */\nexport function readPath(source: unknown, path: string): unknown {\n let keys = pathCache.get(path);\n if (!keys) {\n keys = path.split('.');\n pathCache.set(path, keys);\n }\n let value: unknown = source;\n for (const key of keys) {\n if (value === null || value === undefined) return undefined;\n value = (value as Record<string, unknown>)[key];\n }\n return value;\n}\n\nfunction finite(value: number | undefined): number | undefined {\n return value !== undefined && Number.isFinite(value) ? value : undefined;\n}\n\n/**\n * Normalizes `ColumnDef`s: checks ids, applies defaults and clamps widths.\n *\n * @throws if an id is empty or used twice, because state and events are keyed by id.\n */\nexport function resolveColumns<TData, TContent>(\n defs: readonly ColumnDef<TData, TContent>[],\n): ResolvedColumn<TData, TContent>[] {\n const seen = new Set<string>();\n return defs.map((def, index) => {\n if (typeof def.id !== 'string' || def.id === '') {\n throw new Error(`argus-table: column at index ${String(index)} needs a non-empty string id.`);\n }\n if (seen.has(def.id)) {\n throw new Error(`argus-table: column id '${def.id}' is used more than once.`);\n }\n seen.add(def.id);\n\n const minWidth = Math.max(0, finite(def.minWidth) ?? DEFAULT_MIN_WIDTH);\n const maxWidth = Math.max(minWidth, finite(def.maxWidth) ?? Number.POSITIVE_INFINITY);\n const clamp = (px: number) => Math.min(maxWidth, Math.max(minWidth, px));\n\n const pin = def.pin === 'start' || def.pin === 'end' ? def.pin : undefined;\n const { width } = def;\n // Pinned columns need a known width for their sticky offsets, so they never flex.\n const flex =\n pin === undefined &&\n typeof width === 'object' &&\n Number.isFinite(width.flex) &&\n width.flex > 0\n ? width.flex\n : undefined;\n const auto = width === 'auto';\n // An auto column shows the default width until it is measured.\n const fixed = typeof width === 'number' ? finite(width) : undefined;\n const base = flex !== undefined ? minWidth : clamp(fixed ?? DEFAULT_WIDTH);\n\n const { accessor } = def;\n const getValue: ResolvedColumn<TData, TContent>['getValue'] =\n typeof accessor === 'function'\n ? accessor\n : typeof accessor === 'string'\n ? (row) => readPath(row, accessor)\n : () => undefined;\n\n return {\n id: def.id,\n index,\n header: def.header,\n cell: def.cell,\n getValue,\n hasAccessor: accessor !== undefined,\n width: base,\n flex,\n auto,\n minWidth,\n maxWidth,\n pin,\n sortable: def.sortable === true,\n resizable: def.resizable !== false,\n reorderable: def.reorderable !== false,\n align: def.align ?? 'start',\n };\n });\n}\n"],"mappings":"AAgCA,IAAM,4BAAY,IAAI,IAA+B;;;;;AAMrD,SAAgB,SAAS,QAAiB,MAAuB;CAC/D,IAAI,OAAO,UAAU,IAAI,IAAI;CAC7B,IAAI,CAAC,MAAM;EACT,OAAO,KAAK,MAAM,GAAG;EACrB,UAAU,IAAI,MAAM,IAAI;CAC1B;CACA,IAAI,QAAiB;CACrB,KAAK,MAAM,OAAO,MAAM;EACtB,IAAI,UAAU,QAAQ,UAAU,KAAA,GAAW,OAAO,KAAA;EAClD,QAAS,MAAkC;CAC7C;CACA,OAAO;AACT;AAEA,SAAS,OAAO,OAA+C;CAC7D,OAAO,UAAU,KAAA,KAAa,OAAO,SAAS,KAAK,IAAI,QAAQ,KAAA;AACjE;;;;;;AAOA,SAAgB,eACd,MACmC;CACnC,MAAM,uBAAO,IAAI,IAAY;CAC7B,OAAO,KAAK,KAAK,KAAK,UAAU;EAC9B,IAAI,OAAO,IAAI,OAAO,YAAY,IAAI,OAAO,IAC3C,MAAM,IAAI,MAAM,gCAAgC,OAAO,KAAK,EAAE,8BAA8B;EAE9F,IAAI,KAAK,IAAI,IAAI,EAAE,GACjB,MAAM,IAAI,MAAM,2BAA2B,IAAI,GAAG,0BAA0B;EAE9E,KAAK,IAAI,IAAI,EAAE;EAEf,MAAM,WAAW,KAAK,IAAI,GAAG,OAAO,IAAI,QAAQ,KAAA,EAAsB;EACtE,MAAM,WAAW,KAAK,IAAI,UAAU,OAAO,IAAI,QAAQ,KAAK,OAAO,iBAAiB;EACpF,MAAM,SAAS,OAAe,KAAK,IAAI,UAAU,KAAK,IAAI,UAAU,EAAE,CAAC;EAEvE,MAAM,MAAM,IAAI,QAAQ,WAAW,IAAI,QAAQ,QAAQ,IAAI,MAAM,KAAA;EACjE,MAAM,EAAE,UAAU;EAElB,MAAM,OACJ,QAAQ,KAAA,KACR,OAAO,UAAU,YACjB,OAAO,SAAS,MAAM,IAAI,KAC1B,MAAM,OAAO,IACT,MAAM,OACN,KAAA;EACN,MAAM,OAAO,UAAU;EAEvB,MAAM,QAAQ,OAAO,UAAU,WAAW,OAAO,KAAK,IAAI,KAAA;EAC1D,MAAM,OAAO,SAAS,KAAA,IAAY,WAAW,MAAM,SAAA,GAAsB;EAEzE,MAAM,EAAE,aAAa;EACrB,MAAM,WACJ,OAAO,aAAa,aAChB,WACA,OAAO,aAAa,YACjB,QAAQ,SAAS,KAAK,QAAQ,UACzB,KAAA;EAEd,OAAO;GACL,IAAI,IAAI;GACR;GACA,QAAQ,IAAI;GACZ,MAAM,IAAI;GACV;GACA,aAAa,aAAa,KAAA;GAC1B,OAAO;GACP;GACA;GACA;GACA;GACA;GACA,UAAU,IAAI,aAAa;GAC3B,WAAW,IAAI,cAAc;GAC7B,aAAa,IAAI,gBAAgB;GACjC,OAAO,IAAI,SAAS;EACtB;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"resolve-columns.js","names":[],"sources":["../../../../../core/src/columns/resolve-columns.ts"],"sourcesContent":["import type { ColumnAlign, ColumnDef, ColumnPin, FilterKind } from '../types.js';\n\n/** Width used when a column sets none. */\nexport const DEFAULT_WIDTH = 150;\n/** Lower bound used when a column sets no `minWidth`. */\nexport const DEFAULT_MIN_WIDTH = 40;\n\n/** A column after defaults, bounds and the accessor have been resolved. */\nexport interface ResolvedColumn<TData, TContent> {\n id: string;\n /** Position in the `columns` array. */\n index: number;\n header: ColumnDef<TData, TContent>['header'];\n cell: ColumnDef<TData, TContent>['cell'];\n /** Reads the cell value; returns `undefined` when the column has no accessor. */\n getValue: (row: TData, index: number) => unknown;\n hasAccessor: boolean;\n /** Base width in px. For a flex column this is its minimum track size. */\n width: number;\n /**\n * Share of free space for `{ flex }` columns, otherwise `undefined`. Kept for pinned\n * columns too: the model drops it while the column is pinned (see `pinnedWidth`).\n */\n flex: number | undefined;\n /**\n * Width in px while the column is pinned. Pinned columns need a known width for their\n * sticky offsets, so a flex column pinned (by its definition or by state) gets the\n * default width, within its bounds. Equal to `width` for every other column.\n */\n pinnedWidth: number;\n /** `true` for `width: 'auto'`: the table measures the content and passes the width in. */\n auto: boolean;\n minWidth: number;\n maxWidth: number;\n /** The pin of the definition; `columnPinning` state can override it. */\n pin: ColumnPin | undefined;\n /** Hidden by the definition; `columnVisibility` state can override it. */\n hidden: boolean;\n sortable: boolean;\n resizable: boolean;\n reorderable: boolean;\n pinnable: boolean;\n hideable: boolean;\n /** The kind of filter users can set, or `undefined` when they can't filter the column. */\n filter: FilterKind | undefined;\n filterFn: ColumnDef<TData, TContent>['filterFn'];\n filterOptions: ColumnDef<TData, TContent>['filterOptions'];\n align: ColumnAlign;\n wrap: boolean;\n}\n\nconst FILTER_KINDS: readonly unknown[] = ['text', 'number', 'date', 'set'] satisfies FilterKind[];\n\nconst pathCache = new Map<string, readonly string[]>();\n\n/**\n * Reads a dotted property path such as `'customer.name'`. Returns `undefined`\n * as soon as a step is `null` or `undefined`, like optional chaining.\n */\nexport function readPath(source: unknown, path: string): unknown {\n let keys = pathCache.get(path);\n if (!keys) {\n keys = path.split('.');\n pathCache.set(path, keys);\n }\n let value: unknown = source;\n for (const key of keys) {\n if (value === null || value === undefined) return undefined;\n value = (value as Record<string, unknown>)[key];\n }\n return value;\n}\n\nfunction finite(value: number | undefined): number | undefined {\n return value !== undefined && Number.isFinite(value) ? value : undefined;\n}\n\n/**\n * Normalizes `ColumnDef`s: checks ids, applies defaults and clamps widths.\n *\n * @throws if an id is empty or used twice, because state and events are keyed by id.\n */\nexport function resolveColumns<TData, TContent>(\n defs: readonly ColumnDef<TData, TContent>[],\n): ResolvedColumn<TData, TContent>[] {\n const seen = new Set<string>();\n return defs.map((def, index) => {\n if (typeof def.id !== 'string' || def.id === '') {\n throw new Error(`argus-table: column at index ${String(index)} needs a non-empty string id.`);\n }\n if (seen.has(def.id)) {\n throw new Error(`argus-table: column id '${def.id}' is used more than once.`);\n }\n seen.add(def.id);\n\n const minWidth = Math.max(0, finite(def.minWidth) ?? DEFAULT_MIN_WIDTH);\n const maxWidth = Math.max(minWidth, finite(def.maxWidth) ?? Number.POSITIVE_INFINITY);\n const clamp = (px: number) => Math.min(maxWidth, Math.max(minWidth, px));\n\n const pin = def.pin === 'start' || def.pin === 'end' ? def.pin : undefined;\n const { width } = def;\n const flex =\n typeof width === 'object' && Number.isFinite(width.flex) && width.flex > 0\n ? width.flex\n : undefined;\n const auto = width === 'auto';\n // An auto column shows the default width until it is measured.\n const fixed = typeof width === 'number' ? finite(width) : undefined;\n const pinnedWidth = clamp(fixed ?? DEFAULT_WIDTH);\n const base = flex !== undefined ? minWidth : pinnedWidth;\n\n const { accessor } = def;\n const getValue: ResolvedColumn<TData, TContent>['getValue'] =\n typeof accessor === 'function'\n ? accessor\n : typeof accessor === 'string'\n ? (row) => readPath(row, accessor)\n : () => undefined;\n\n return {\n id: def.id,\n index,\n header: def.header,\n cell: def.cell,\n getValue,\n hasAccessor: accessor !== undefined,\n width: base,\n flex,\n pinnedWidth,\n auto,\n minWidth,\n maxWidth,\n pin,\n hidden: def.hidden === true,\n sortable: def.sortable === true,\n resizable: def.resizable !== false,\n reorderable: def.reorderable !== false,\n pinnable: def.pinnable !== false,\n hideable: def.hideable !== false,\n filter: FILTER_KINDS.includes(def.filter) ? def.filter : undefined,\n filterFn: def.filterFn,\n filterOptions: def.filterOptions,\n align: def.align ?? 'start',\n wrap: def.wrap === true,\n };\n });\n}\n"],"mappings":"AAmDA,IAAM,eAAmC;CAAC;CAAQ;CAAU;CAAQ;AAAK;AAEzE,IAAM,4BAAY,IAAI,IAA+B;;;;;AAMrD,SAAgB,SAAS,QAAiB,MAAuB;CAC/D,IAAI,OAAO,UAAU,IAAI,IAAI;CAC7B,IAAI,CAAC,MAAM;EACT,OAAO,KAAK,MAAM,GAAG;EACrB,UAAU,IAAI,MAAM,IAAI;CAC1B;CACA,IAAI,QAAiB;CACrB,KAAK,MAAM,OAAO,MAAM;EACtB,IAAI,UAAU,QAAQ,UAAU,KAAA,GAAW,OAAO,KAAA;EAClD,QAAS,MAAkC;CAC7C;CACA,OAAO;AACT;AAEA,SAAS,OAAO,OAA+C;CAC7D,OAAO,UAAU,KAAA,KAAa,OAAO,SAAS,KAAK,IAAI,QAAQ,KAAA;AACjE;;;;;;AAOA,SAAgB,eACd,MACmC;CACnC,MAAM,uBAAO,IAAI,IAAY;CAC7B,OAAO,KAAK,KAAK,KAAK,UAAU;EAC9B,IAAI,OAAO,IAAI,OAAO,YAAY,IAAI,OAAO,IAC3C,MAAM,IAAI,MAAM,gCAAgC,OAAO,KAAK,EAAE,8BAA8B;EAE9F,IAAI,KAAK,IAAI,IAAI,EAAE,GACjB,MAAM,IAAI,MAAM,2BAA2B,IAAI,GAAG,0BAA0B;EAE9E,KAAK,IAAI,IAAI,EAAE;EAEf,MAAM,WAAW,KAAK,IAAI,GAAG,OAAO,IAAI,QAAQ,KAAA,EAAsB;EACtE,MAAM,WAAW,KAAK,IAAI,UAAU,OAAO,IAAI,QAAQ,KAAK,OAAO,iBAAiB;EACpF,MAAM,SAAS,OAAe,KAAK,IAAI,UAAU,KAAK,IAAI,UAAU,EAAE,CAAC;EAEvE,MAAM,MAAM,IAAI,QAAQ,WAAW,IAAI,QAAQ,QAAQ,IAAI,MAAM,KAAA;EACjE,MAAM,EAAE,UAAU;EAClB,MAAM,OACJ,OAAO,UAAU,YAAY,OAAO,SAAS,MAAM,IAAI,KAAK,MAAM,OAAO,IACrE,MAAM,OACN,KAAA;EACN,MAAM,OAAO,UAAU;EAGvB,MAAM,cAAc,OADN,OAAO,UAAU,WAAW,OAAO,KAAK,IAAI,KAAA,MAAA,GACV;EAChD,MAAM,OAAO,SAAS,KAAA,IAAY,WAAW;EAE7C,MAAM,EAAE,aAAa;EACrB,MAAM,WACJ,OAAO,aAAa,aAChB,WACA,OAAO,aAAa,YACjB,QAAQ,SAAS,KAAK,QAAQ,UACzB,KAAA;EAEd,OAAO;GACL,IAAI,IAAI;GACR;GACA,QAAQ,IAAI;GACZ,MAAM,IAAI;GACV;GACA,aAAa,aAAa,KAAA;GAC1B,OAAO;GACP;GACA;GACA;GACA;GACA;GACA;GACA,QAAQ,IAAI,WAAW;GACvB,UAAU,IAAI,aAAa;GAC3B,WAAW,IAAI,cAAc;GAC7B,aAAa,IAAI,gBAAgB;GACjC,UAAU,IAAI,aAAa;GAC3B,UAAU,IAAI,aAAa;GAC3B,QAAQ,aAAa,SAAS,IAAI,MAAM,IAAI,IAAI,SAAS,KAAA;GACzD,UAAU,IAAI;GACd,eAAe,IAAI;GACnB,OAAO,IAAI,SAAS;GACpB,MAAM,IAAI,SAAS;EACrB;CACF,CAAC;AACH"}
@@ -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,185 @@
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
+ 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
+ };
95
+ }
96
+ case "number": {
97
+ const test = compare(filter.op, numberOf(filter.value), numberOf(filter.to));
98
+ return test && ((v) => test(numberOf(v)));
99
+ }
100
+ case "date": {
101
+ const test = compare(filter.op, dayOf(filter.value), dayOf(filter.to));
102
+ return test && ((v) => test(dayOf(v)));
103
+ }
104
+ case "set": {
105
+ const values = new Set(filter.values);
106
+ return (v) => values.has(setValueOf(v));
107
+ }
108
+ }
109
+ }
110
+ /** The words of a quick filter, lowercased; empty when it keeps every row. */
111
+ function quickTerms(quickFilter) {
112
+ return (quickFilter ?? "").toLowerCase().split(/\s+/).filter(Boolean);
113
+ }
114
+ /**
115
+ * One test of a row for the column filters and the quick filter, or `undefined` when
116
+ * they keep every row. A row passes every column filter, and each word of the quick
117
+ * filter is found in the text of one of `quickColumns` (ignoring case).
118
+ *
119
+ * Each value is read once per test through the column's `getValue`. A column with a
120
+ * `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.
122
+ */
123
+ function rowPredicate(columns, filters, quickFilter, quickColumns, except) {
124
+ const tests = [];
125
+ for (const [id, filter] of Object.entries(filters ?? {})) {
126
+ const column = columns.get(id);
127
+ if (!column || id === except) continue;
128
+ const { getValue, filterFn } = column;
129
+ if (filterFn) {
130
+ tests.push((row, index) => filterFn(getValue(row, index), filter, row));
131
+ continue;
132
+ }
133
+ const test = compileFilter(filter);
134
+ if (test) tests.push((row, index) => test(getValue(row, index)));
135
+ }
136
+ const terms = quickTerms(quickFilter);
137
+ if (terms.length && quickColumns.length) {
138
+ const textAt = (c, row, index) => textOf(c.getValue(row, index)).toLowerCase();
139
+ const [term] = terms;
140
+ tests.push(terms.length === 1 && term !== void 0 ? (row, index) => quickColumns.some((c) => textAt(c, row, index).includes(term)) : (row, index) => {
141
+ const texts = quickColumns.map((c) => textAt(c, row, index));
142
+ return terms.every((t) => texts.some((text) => text.includes(t)));
143
+ });
144
+ }
145
+ if (!tests.length) return void 0;
146
+ if (tests.length === 1) return tests[0];
147
+ return (row, index) => tests.every((test) => test(row, index));
148
+ }
149
+ var kindOrder = (v) => v === null ? 3 : typeof v === "boolean" ? 2 : typeof v === "number" ? 0 : 1;
150
+ /** Orders set values: numbers, then text (by locale, numbers inside text by value), then booleans, then empty. */
151
+ function compareSetValues(a, b) {
152
+ const kind = kindOrder(a) - kindOrder(b);
153
+ if (kind) return kind;
154
+ if (typeof a === "number" && typeof b === "number") return a - b;
155
+ if (typeof a === "string" && typeof b === "string") return a.localeCompare(b, void 0, {
156
+ numeric: true,
157
+ sensitivity: "base"
158
+ });
159
+ return Number(a) - Number(b);
160
+ }
161
+ /**
162
+ * The distinct values of `column` over the rows `keep` passes, as a `'set'` filter
163
+ * lists them (see `setValueOf`), sorted. Stops after `limit` values (`truncated`).
164
+ */
165
+ function distinctValues(rows, column, keep, limit) {
166
+ const seen = /* @__PURE__ */ new Set();
167
+ let truncated = false;
168
+ for (const { original, index } of rows) {
169
+ if (keep && !keep(original, index)) continue;
170
+ const value = setValueOf(column.getValue(original, index));
171
+ if (seen.has(value)) continue;
172
+ if (seen.size === limit) {
173
+ truncated = true;
174
+ break;
175
+ }
176
+ seen.add(value);
177
+ }
178
+ return {
179
+ values: [...seen].sort(compareSetValues),
180
+ truncated
181
+ };
182
+ }
183
+ export { compareSetValues, compileFilter, dayOf, distinctValues, isEmptyValue, quickTerms, rowPredicate, setValueOf, textOf };
184
+
185
+ //# 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 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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"}