@rivinia/table 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/README.md +38 -19
- package/fesm2022/rivinia-table-ui.mjs +31 -17
- package/fesm2022/rivinia-table-ui.mjs.map +1 -1
- package/fesm2022/rivinia-table.mjs +40 -10
- package/fesm2022/rivinia-table.mjs.map +1 -1
- package/package.json +13 -1
- package/types/rivinia-table-ui.d.ts +2 -8
- package/types/rivinia-table.d.ts +12 -16
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rivinia-table.mjs","names":["isNullish"],"sources":["../../../projects/table/src/lib/utils/table-pipeline.ts","../../../projects/table/src/lib/utils/spans.ts","../../../projects/table/src/lib/utils/table-column.ts","../../../projects/table/src/lib/services/rivinia-table.ts"],"sourcesContent":["import type { FilterState, SortState, TableColumn, TableRow } from '../models/table.models';\n\n/*\n * The row pipeline: filter -> search -> sort -> page, as pure functions.\n *\n * Nothing here imports Angular and nothing mutates its input. The controller\n * composes these into `computed`s, which is what gives incremental\n * recomputation: changing the page does not re-sort, changing the sort does\n * not re-filter. Keeping them free of signals also makes them usable by a\n * headless consumer and testable without a TestBed.\n */\n\n/**\n * The value of one cell, resolved through the column's accessor.\n *\n * The accessor is either a key or a function (the shared contract allows\n * both), so every consumer — the pipeline, the span engine, a custom renderer\n * — reads a cell the same way. A display/action column has no accessor and\n * therefore no value; `undefined` is the honest answer, not an error.\n */\nexport function readCellValue<T, V = unknown>(\n column: TableColumn<T, V>,\n row: TableRow<T>,\n): V | undefined {\n const accessor = column.accessor;\n if (accessor === undefined) return undefined;\n if (typeof accessor === 'string') {\n // The key form is a runtime index; TS cannot prove it exists on a generic\n // `T`, and the column author already asserted it by choosing the key.\n const keyed = row.data as unknown as Record<string, V | undefined>;\n return keyed[accessor];\n }\n return accessor(row.data);\n}\n\n/**\n * A range filter value. Bounds are inclusive and either side may be open, so\n * one shape covers \"from\", \"to\" and \"between\" for both numbers and dates.\n *\n * Numbers and dates share it on purpose: a date bound is compared as a\n * timestamp and a numeric bound as a number, which is exactly what a date\n * input and a number input produce in the UI.\n */\nexport interface TableFilterRange {\n readonly min?: number | Date | null;\n readonly max?: number | Date | null;\n}\n\n/** The result of `paginate`, with the display range the footer needs. */\nexport interface TablePage<T> {\n readonly pageRows: readonly TableRow<T>[];\n /** Rows before pagination. */\n readonly total: number;\n /** `ceil(total / pageSize)`; `0` when there is no row. */\n readonly pageCount: number;\n /** 1-based index of the first row on this page; `0` when the page is empty. */\n readonly start: number;\n /** 1-based index of the last row on this page; `0` when the page is empty. */\n readonly end: number;\n}\n\nconst COLLATORS = new Map<string, Intl.Collator>();\n\n/**\n * One collator per locale, cached. `Intl.Collator` construction is expensive\n * and a sort asks it once per comparison; `numeric` makes \"item 10\" sort after\n * \"item 9\", and `base` ignores case and accents so `Ahmed` and `أحمد` behave\n * like a human expects in a search-and-sort list.\n */\nfunction collatorFor(locale: string): Intl.Collator {\n const key = locale === '' ? 'en' : locale;\n const cached = COLLATORS.get(key);\n if (cached !== undefined) return cached;\n let collator: Intl.Collator;\n try {\n collator = new Intl.Collator(key, { numeric: true, sensitivity: 'base' });\n } catch {\n // An invalid locale tag must degrade to the runtime default, never break\n // a render: a table that throws on `locale = ''` is worse than an\n // approximate order.\n collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });\n }\n COLLATORS.set(key, collator);\n return collator;\n}\n\n/**\n * Case-, accent- and Arabic-aware search key: NFD decomposition, combining\n * marks, tashkeel, tatweel, the alef forms, alef maqsura, hamza carriers and\n * ta marbuta. The same rules the select's search documents — a user typing\n * `احمد` must find `أحمد`, and `محمود` must find `محمّد`.\n */\nfunction normalizeSearchText(value: string): string {\n return value\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/[\\u064B-\\u065F\\u0670]/g, '')\n .replace(/\\u0640/g, '')\n .replace(/[أإآٱ]/g, 'ا')\n .replace(/ى/g, 'ي')\n .replace(/ؤ/g, 'و')\n .replace(/ئ/g, 'ي')\n .replace(/ة/g, 'ه')\n .toLowerCase();\n}\n\nfunction isNullish(value: unknown): value is null | undefined {\n return value === null || value === undefined;\n}\n\n/** An empty string, `null`, `undefined` or an empty array means \"no filter\". */\nfunction isActiveFilter(value: unknown): boolean {\n if (value === null || value === undefined || value === '') return false;\n return !Array.isArray(value) || value.length > 0;\n}\n\nfunction toNumber(value: unknown): number | null {\n if (typeof value === 'number') return Number.isFinite(value) ? value : null;\n if (typeof value === 'bigint') return Number(value);\n if (typeof value === 'string' && value.trim() !== '') {\n const numeric = Number(value);\n return Number.isFinite(numeric) ? numeric : null;\n }\n return null;\n}\n\n/**\n * A comparable timestamp for a date-ish or numeric cell/ bound.\n *\n * Numeric strings are read as numbers BEFORE `Date.parse`, because\n * `Date.parse('5')` happily returns a 2001 date and would make a numeric range\n * silently match nothing.\n */\nfunction toTime(value: unknown): number | null {\n if (value instanceof Date) {\n const time = value.getTime();\n return Number.isNaN(time) ? null : time;\n }\n if (typeof value === 'number') return Number.isFinite(value) ? value : null;\n if (typeof value === 'bigint') return Number(value);\n if (typeof value === 'string') {\n const trimmed = value.trim();\n if (trimmed === '') return null;\n const numeric = Number(trimmed);\n if (Number.isFinite(numeric)) return numeric;\n const parsed = Date.parse(trimmed);\n return Number.isNaN(parsed) ? null : parsed;\n }\n return null;\n}\n\nfunction isRange(value: unknown): value is TableFilterRange {\n return typeof value === 'object' && value !== null && ('min' in value || 'max' in value);\n}\n\nfunction matchesRange(cell: unknown, range: TableFilterRange): boolean {\n const min = range.min === null || range.min === undefined ? null : toTime(range.min);\n const max = range.max === null || range.max === undefined ? null : toTime(range.max);\n if (min === null && max === null) return true;\n const time = toTime(cell);\n if (time === null) return false;\n return (min === null || time >= min) && (max === null || time <= max);\n}\n\n/**\n * What a filter value means is decided by its SHAPE, because the shared\n * contract stores it as `unknown` and a column carries no filter kind.\n * `'text'` -> substring, `true` -> boolean, `5` -> number, `Date` -> day,\n * `[...]` -> one of, `{min,max}` -> range. Everything else falls back to\n * identity, which is safe: an unrecognised filter can only be too strict,\n * never silently match everything.\n */\nfunction matchesFilter(cell: unknown, filter: unknown): boolean {\n if (Array.isArray(filter)) return filter.some((item) => Object.is(item, cell));\n if (typeof filter === 'boolean') return Boolean(cell) === filter;\n if (filter instanceof Date) {\n const time = toTime(cell);\n return time !== null && time === filter.getTime();\n }\n if (typeof filter === 'number') {\n const numeric = toNumber(cell);\n return numeric !== null && numeric === filter;\n }\n if (typeof filter === 'bigint') {\n const numeric = toNumber(cell);\n return numeric !== null && numeric === Number(filter);\n }\n if (isRange(filter)) return matchesRange(cell, filter);\n if (typeof filter === 'string') {\n return normalizeSearchText(String(cell ?? '')).includes(normalizeSearchText(filter));\n }\n return Object.is(cell, filter);\n}\n\n/**\n * Keeps the rows that satisfy EVERY active filter.\n *\n * A filter on a column that is hidden, unknown, or has no accessor is ignored\n * rather than treated as \"match nothing\": a saved view that references a\n * removed column should lose that constraint, not empty the table.\n */\nexport function applyFilters<T>(\n rows: readonly TableRow<T>[],\n columns: readonly TableColumn<T>[],\n filters: readonly FilterState[],\n): readonly TableRow<T>[] {\n if (filters.length === 0) return rows;\n\n const byId = new Map(columns.map((column) => [column.id, column]));\n const plan: Array<{ readonly column: TableColumn<T>; readonly value: unknown }> = [];\n for (const filter of filters) {\n if (!isActiveFilter(filter.value)) continue;\n const column = byId.get(filter.column);\n if (column === undefined || column.accessor === undefined) continue;\n plan.push({ column, value: filter.value });\n }\n if (plan.length === 0) return rows;\n\n return rows.filter((row) => {\n for (const entry of plan) {\n if (!matchesFilter(readCellValue(entry.column, row), entry.value)) return false;\n }\n return true;\n });\n}\n\n/**\n * Global search across every column that has a value.\n *\n * Display columns are skipped (they hold no data), and the needle is\n * normalized with the same Arabic rules as the filters, so search finds a name\n * written with or without diacritics. An empty search returns the input array\n * unchanged — identity matters, because the controller's `computed` chain\n * uses it to avoid downstream work.\n */\nexport function applySearch<T>(\n rows: readonly TableRow<T>[],\n columns: readonly TableColumn<T>[],\n search: string,\n): readonly TableRow<T>[] {\n const needle = normalizeSearchText(search.trim());\n if (needle === '') return rows;\n const searchable = columns.filter((column) => column.accessor !== undefined);\n if (searchable.length === 0) return [];\n\n return rows.filter((row) => {\n for (const column of searchable) {\n const value = readCellValue(column, row);\n if (value === null || value === undefined) continue;\n if (normalizeSearchText(String(value)).includes(needle)) return true;\n }\n return false;\n });\n}\n\n/**\n * Compares two present (non-nullish) values. `column.comparator` wins when the\n * consumer declared one, because only the consumer knows what the value means.\n */\nfunction comparePresent(\n left: unknown,\n right: unknown,\n comparator: ((left: unknown, right: unknown) => number) | undefined,\n collator: Intl.Collator,\n): number {\n if (comparator !== undefined) return comparator(left, right);\n if (left instanceof Date && right instanceof Date) {\n const a = left.getTime();\n const b = right.getTime();\n if (!Number.isNaN(a) && !Number.isNaN(b)) return a < b ? -1 : a > b ? 1 : 0;\n }\n if (typeof left === 'number' && typeof right === 'number') {\n return left < right ? -1 : left > right ? 1 : 0;\n }\n if (typeof left === 'bigint' && typeof right === 'bigint') {\n return left < right ? -1 : left > right ? 1 : 0;\n }\n if (typeof left === 'boolean' && typeof right === 'boolean') {\n return left === right ? 0 : left ? 1 : -1;\n }\n return collator.compare(String(left), String(right));\n}\n\n/**\n * Multi-level, stable sort with nulls LAST in both directions.\n *\n * Two decisions worth stating: a missing value always sorts to the end (a\n * blank salary at the top of an ascending list is noise, and flipping it with\n * the direction would put it at the top of a descending one too), and the\n * pre-sort index is the final tie-break, so equal keys keep their order even\n * on an engine whose sort is not stable.\n *\n * Sort levels that reference a hidden, unknown or accessor-less column are\n * skipped rather than applied as a no-op, so `sorting` never lies about what\n * is active.\n */\nexport function applySort<T>(\n rows: readonly TableRow<T>[],\n columns: readonly TableColumn<T>[],\n sorting: readonly SortState[],\n locale = 'en',\n): readonly TableRow<T>[] {\n if (sorting.length === 0) return rows;\n\n const byId = new Map(columns.map((column) => [column.id, column]));\n const plan: Array<{\n readonly state: SortState;\n readonly column: TableColumn<T>;\n }> = [];\n for (const state of sorting) {\n const column = byId.get(state.column);\n if (column === undefined || column.accessor === undefined) continue;\n plan.push({ state, column });\n }\n if (plan.length === 0) return rows;\n\n const collator = collatorFor(locale);\n // Keys are read once per row, not once per comparison: at 10k rows the\n // accessor would otherwise run ~130k times instead of 10k.\n const keyed = rows.map((row) => ({\n row,\n keys: plan.map((entry) => readCellValue(entry.column, row)),\n }));\n\n keyed.sort((a, b) => {\n for (let index = 0; index < plan.length; index++) {\n const left = a.keys[index];\n const right = b.keys[index];\n const leftMissing = isNullish(left);\n const rightMissing = isNullish(right);\n if (leftMissing || rightMissing) {\n if (leftMissing && rightMissing) continue;\n return leftMissing ? 1 : -1;\n }\n const entry = plan[index];\n const result = comparePresent(left, right, entry.column.comparator, collator);\n if (result !== 0) return entry.state.direction === 'desc' ? -result : result;\n }\n return a.row.index - b.row.index;\n });\n\n return keyed.map((entry) => entry.row);\n}\n\n/**\n * Slices one page and reports the footer's numbers.\n *\n * `start` / `end` are 1-based and inclusive (\"1-25 of 12,345\"), and both are 0\n * when the page holds nothing — a page past the end is legal and returns an\n * empty page, it is not silently clamped to the last one (clamping would hide\n * a controller bug and lie about the requested state).\n *\n * A non-positive / non-finite `pageSize` means \"no pagination\": every row on\n * one page. That is a deliberate degradation rather than a crash.\n */\nexport function paginate<T>(\n rows: readonly TableRow<T>[],\n page: number,\n pageSize: number,\n): TablePage<T> {\n const total = rows.length;\n const size = Number.isFinite(pageSize) ? Math.floor(pageSize) : 0;\n const currentPage = Number.isFinite(page) ? Math.max(0, Math.floor(page)) : 0;\n\n if (size <= 0) {\n return {\n pageRows: rows,\n total,\n pageCount: total > 0 ? 1 : 0,\n start: total > 0 ? 1 : 0,\n end: total,\n };\n }\n\n const pageCount = Math.ceil(total / size);\n const offset = currentPage * size;\n const pageRows = offset >= total ? [] : rows.slice(offset, offset + size);\n\n return {\n pageRows,\n total,\n pageCount,\n start: pageRows.length === 0 ? 0 : offset + 1,\n end: pageRows.length === 0 ? 0 : offset + pageRows.length,\n };\n}\n","import { isDevMode } from '@angular/core';\nimport type {\n CellSpan,\n SpanCell,\n SpanContext,\n SpanModel,\n TableColumn,\n TableRow,\n} from '../models/table.models';\nimport { readCellValue } from './table-pipeline';\n\n/**\n * Where span diagnostics go. A `warn` sink is injected rather than hard-coded\n * so a consumer can route the messages through its own logger, and a test can\n * assert them without touching the console.\n */\nexport interface SpanWarnOptions {\n readonly warn?: (message: string) => void;\n}\n\n/**\n * `buildSpanModel` options.\n *\n * `strict` exists because the same builder serves two callers with opposite\n * needs: `createTable()` validates its column list ONCE and may throw (a\n * conflicting declaration is a bug in code, not in data), while a render-time\n * rebuild must never throw — an app that swaps its columns at runtime would\n * otherwise blank the grid. Strict mode is the documented default; the\n * controller passes `strict: false` and lets `span` win, as the design says.\n */\nexport interface SpanBuildOptions extends SpanWarnOptions {\n /**\n * Throw when a column declares `span` together with `merge`/`colSpan`.\n * @default true\n */\n readonly strict?: boolean;\n}\n\nconst NO_CELLS: readonly SpanCell[] = Object.freeze([]);\n\n/**\n * A headless core may run under SSR, in a worker, or inside a test runner, so\n * the default sink is dev-mode only: it costs nothing in production and never\n * writes to a console the library does not own.\n */\nfunction defaultWarn(message: string): void {\n if (isDevMode()) {\n console.warn(`[rivinia/table] ${message}`);\n }\n}\n\nfunction isNullish(value: unknown): value is null | undefined {\n return value === null || value === undefined;\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n if (!Number.isFinite(value)) return min;\n return value < min ? min : value > max ? max : value;\n}\n\n/**\n * Shared declaration checks.\n *\n * The span rules are mutually exclusive by design: `merge` and `colSpan` are\n * the simple path and compose into one rectangle, `span` is full manual\n * control. Accepting both would make the outcome depend on evaluation order,\n * which is exactly the kind of silent surprise a table cannot afford.\n */\nfunction checkColumns<T>(\n columns: readonly TableColumn<T>[],\n warn: (message: string) => void,\n strict: boolean,\n): void {\n for (const column of columns) {\n const conflicting =\n column.merge !== undefined ? 'merge' : column.colSpan !== undefined ? 'colSpan' : null;\n if (column.span !== undefined && conflicting !== null) {\n const message =\n `column \"${column.id}\" declares both span and ${conflicting}; ` +\n 'use span alone for full control, or merge/colSpan for the simple path';\n if (strict) {\n throw new Error(`[rivinia/table] ${message}`);\n }\n warn(`${message}; span wins`);\n }\n if (column.merge !== undefined && column.accessor === undefined) {\n // A display column has no value to compare, so merging it would merge\n // every row \"by accident\". Warn once and render plain cells instead.\n warn(`column \"${column.id}\" declares merge but has no accessor; merge is ignored`);\n }\n }\n}\n\n/**\n * Validates a column list without touching any data. `createTable()` calls\n * this at construction so a conflicting declaration fails immediately — with\n * an actionable message — instead of at the first paint, and in production\n * too, because it is a defect in the column list rather than in the data.\n */\nexport function assertSpanColumns<T>(\n columns: readonly TableColumn<T>[],\n options?: SpanWarnOptions,\n): void {\n checkColumns(columns, options?.warn ?? defaultWarn, true);\n}\n\n/**\n * Resolves `span` / `merge` / `colSpan` into the concrete span grid of the\n * given rows and columns.\n *\n * Semantics (ported from the validated prototype, 35/35 contract checks):\n * - only anchors exist; a covered position has no cell and, in the UI, no DOM;\n * - first claim wins: an overlapping later span is shrunk (width, then height)\n * and reported, so a configuration hazard is never silent;\n * - row spans stop at the page (a page is a DOM boundary) and column spans at\n * the visible range and at the pinned-region boundary;\n * - `Infinity` means \"the rest of my region\", resolved before integer checks;\n * - `0`, negatives, `NaN` and fractions are clamped to 1 with a warning, so a\n * bad return value can never delete a cell;\n * - rows must already be the rendered order: a merge follows adjacency, so\n * sorting legitimately starts a new run.\n *\n * The model is a dense matrix of owner indices, so `at()` / `owner()` are O(1)\n * and `move()` walks only the cells it skips.\n */\nexport function buildSpanModel<T>(\n rows: readonly TableRow<T>[],\n columns: readonly TableColumn<T>[],\n options?: SpanBuildOptions,\n): SpanModel {\n const warn = options?.warn ?? defaultWarn;\n checkColumns(columns, warn, options?.strict ?? true);\n\n const rowCount = rows.length;\n const columnCount = columns.length;\n const hasRules = columns.some(\n (column) =>\n column.span !== undefined ||\n column.colSpan !== undefined ||\n (column.merge !== undefined && column.accessor !== undefined),\n );\n if (!hasRules || rowCount === 0 || columnCount === 0) {\n return plainSpanModel(rowCount, columnCount);\n }\n\n const claimed = new Uint8Array(rowCount * columnCount);\n const owners = new Int32Array(rowCount * columnCount).fill(-1);\n const cells: SpanCell[] = [];\n\n /** Resolves one cell's request. `span` wins over `merge` / `colSpan`. */\n const resolve = (definition: TableColumn<T>, row: number, column: number): CellSpan => {\n const current = rows[row];\n const value = readCellValue(definition, current);\n\n const spanResolver = definition.span;\n if (spanResolver !== undefined) {\n const explicit = spanResolver(contextOf(current, definition, value, row, column));\n if (explicit !== null && explicit !== undefined) {\n return { rowSpan: explicit.rowSpan ?? 1, colSpan: explicit.colSpan ?? 1 };\n }\n }\n\n let rowSpan = 1;\n let colSpan = 1;\n\n /*\n * Row run. The anchor is the run's FIRST row and every candidate is\n * compared with it, never with its neighbour: that makes runs transitive\n * by construction (A = B and B = C cannot produce a run that excludes A).\n */\n const merge = definition.merge;\n if (merge !== undefined && definition.accessor !== undefined) {\n if (merge === 'equal') {\n // Nullish values never merge: an empty cell must not swallow the rows\n // below it, which is what an `undefined === undefined` run would do.\n if (!isNullish(value)) {\n let end = row;\n while (end + 1 < rowCount) {\n const candidate = readCellValue(definition, rows[end + 1]);\n if (isNullish(candidate) || !Object.is(candidate, value)) break;\n end++;\n }\n rowSpan = end - row + 1;\n }\n } else {\n const anchor = current.data;\n let end = row;\n while (end + 1 < rowCount && merge(rows[end + 1].data, anchor)) end++;\n rowSpan = end - row + 1;\n }\n }\n\n // The column half composes with the row half, so merge + colSpan is ONE\n // rectangle (a department cell covering its rows and its neighbour).\n const colSpanDefinition = definition.colSpan;\n if (colSpanDefinition !== undefined) {\n colSpan =\n typeof colSpanDefinition === 'function'\n ? colSpanDefinition(contextOf(current, definition, value, row, column))\n : colSpanDefinition;\n }\n\n return { rowSpan, colSpan };\n };\n\n for (let row = 0; row < rowCount; row++) {\n for (let column = 0; column < columnCount; column++) {\n if (claimed[row * columnCount + column] !== 0) continue;\n const definition = columns[column];\n const requested = resolve(definition, row, column);\n\n let rowSpan = requested.rowSpan ?? 1;\n let colSpan = requested.colSpan ?? 1;\n // `Infinity` is resolved before the integer checks, so \"the rest of the\n // region\" is not a special case for every later stage.\n if (colSpan === Number.POSITIVE_INFINITY) colSpan = columnCount - column;\n\n if (!Number.isInteger(rowSpan) || rowSpan < 1) {\n warn(`column \"${definition.id}\" returned rowSpan ${rowSpan}; clamped to 1`);\n rowSpan = 1;\n }\n if (!Number.isInteger(colSpan) || colSpan < 1) {\n warn(`column \"${definition.id}\" returned colSpan ${colSpan}; clamped to 1`);\n colSpan = 1;\n }\n\n // A page is a DOM boundary and a pinned region is a sticky boundary;\n // a span may not cross either, so both are hard clamps (and silent:\n // they are the documented meaning of a request, not a hazard).\n rowSpan = Math.min(rowSpan, rowCount - row);\n colSpan = Math.min(colSpan, columnCount - column);\n\n const pinning = definition.pinned ?? null;\n for (let step = 1; step < colSpan; step++) {\n if ((columns[column + step].pinned ?? null) !== pinning) {\n colSpan = step;\n break;\n }\n }\n\n const wantedRowSpan = rowSpan;\n const wantedColSpan = colSpan;\n while (colSpan > 1 && !isFree(claimed, columnCount, row, column, rowSpan, colSpan)) {\n colSpan--;\n }\n while (rowSpan > 1 && !isFree(claimed, columnCount, row, column, rowSpan, colSpan)) {\n rowSpan--;\n }\n if (rowSpan !== wantedRowSpan || colSpan !== wantedColSpan) {\n warn(\n `column \"${definition.id}\" at row ${row} overlaps an existing span; ` +\n `shrunk from ${wantedRowSpan}x${wantedColSpan} to ${rowSpan}x${colSpan}`,\n );\n }\n\n const anchor = cells.length;\n cells.push({ row, column, rowSpan, colSpan });\n for (let r = row; r < row + rowSpan; r++) {\n for (let c = column; c < column + colSpan; c++) {\n claimed[r * columnCount + c] = 1;\n owners[r * columnCount + c] = anchor;\n }\n }\n }\n }\n\n // No column actually spanned: hand back the cheap model, not a matrix of\n // 1x1 anchors that every renderer would then have to walk.\n if (cells.length === rowCount * columnCount) {\n return plainSpanModel(rowCount, columnCount);\n }\n\n const ownerAt = (row: number, column: number): SpanCell | null => {\n if (row < 0 || row >= rowCount || column < 0 || column >= columnCount) return null;\n const index = owners[row * columnCount + column];\n return index < 0 ? null : cells[index];\n };\n\n return {\n cells,\n at(row, column) {\n const cell = ownerAt(row, column);\n return cell !== null && cell.row === row && cell.column === column ? cell : null;\n },\n owner: ownerAt,\n move(row, column, dRow, dColumn) {\n // A stale position (the page shrank under the UI) is clamped rather\n // than trusted: focus must never land outside the grid.\n if (dRow === 0 && dColumn === 0) {\n return { row: clamp(row, 0, rowCount - 1), column: clamp(column, 0, columnCount - 1) };\n }\n let currentRow = clamp(row, 0, rowCount - 1);\n let currentColumn = clamp(column, 0, columnCount - 1);\n for (;;) {\n const nextRow = currentRow + dRow;\n const nextColumn = currentColumn + dColumn;\n if (nextRow < 0 || nextRow >= rowCount || nextColumn < 0 || nextColumn >= columnCount) {\n return { row: currentRow, column: currentColumn };\n }\n const span = ownerAt(nextRow, nextColumn);\n if (span === null) return { row: nextRow, column: nextColumn };\n\n /*\n * Moving INSIDE a merged cell leaves it; moving INTO one lands on its\n * anchor. Without the first half, ArrowDown inside a 7-row merge would\n * walk the covered cells one at a time and focus could sit on a\n * position that has no rendered cell.\n */\n const inside =\n currentRow >= span.row &&\n currentRow < span.row + span.rowSpan &&\n currentColumn >= span.column &&\n currentColumn < span.column + span.colSpan;\n if (!inside) return { row: span.row, column: span.column };\n\n const jumpRow = dRow === 0 ? nextRow : dRow > 0 ? span.row + span.rowSpan : span.row - 1;\n const jumpColumn =\n dColumn === 0 ? nextColumn : dColumn > 0 ? span.column + span.colSpan : span.column - 1;\n if (jumpRow < 0 || jumpRow >= rowCount || jumpColumn < 0 || jumpColumn >= columnCount) {\n return { row: currentRow, column: currentColumn };\n }\n const jumped = ownerAt(jumpRow, jumpColumn);\n /*\n * Landing on a RENDERED cell ends the step, and an anchor is rendered:\n * one arrow press moves one logical row, so the anchor of the next run\n * is the destination. Re-entering the loop there would treat the anchor\n * as \"inside a span\" and walk through every following run — a merged\n * column would jump from run 1 to the last run in a single ArrowDown.\n * Only a COVERED cell (possible on a diagonal step into another span)\n * keeps walking, and then it lands on that span's anchor.\n */\n if (jumped === null || (jumped.row === jumpRow && jumped.column === jumpColumn)) {\n return { row: jumpRow, column: jumpColumn };\n }\n currentRow = jumpRow;\n currentColumn = jumpColumn;\n }\n },\n };\n}\n\nfunction contextOf<T>(\n row: TableRow<T>,\n column: TableColumn<T>,\n value: unknown,\n rowIndex: number,\n columnIndex: number,\n): SpanContext<T> {\n return { row, column, value, rowIndex, columnIndex };\n}\n\n/** Rectangle test that ignores the anchor cell itself (always free by then). */\nfunction isFree(\n claimed: Uint8Array,\n columnCount: number,\n row: number,\n column: number,\n rowSpan: number,\n colSpan: number,\n): boolean {\n for (let r = row; r < row + rowSpan; r++) {\n for (let c = column; c < column + colSpan; c++) {\n if (r === row && c === column) continue;\n if (claimed[r * columnCount + c] !== 0) return false;\n }\n }\n return true;\n}\n\n/**\n * The model for a grid where nothing (or nothing that matters) spans.\n *\n * It is intentionally NOT a single shared constant: `move()` must clamp at the\n * edges, which needs the grid size. The expensive part — the two typed arrays\n * — is what the fast path skips; these three closures cost nothing per frame.\n */\nfunction plainSpanModel(rowCount: number, columnCount: number): SpanModel {\n return {\n cells: NO_CELLS,\n at: () => null,\n owner: () => null,\n move(row, column, dRow, dColumn) {\n if (rowCount === 0 || columnCount === 0) return { row: 0, column: 0 };\n return {\n row: clamp(row + dRow, 0, rowCount - 1),\n column: clamp(column + dColumn, 0, columnCount - 1),\n };\n },\n };\n}\n","import type { ColumnDef, TableColumn } from '../models/table.models';\n\n/**\n * The two ways a column is declared. `accessor` is a data column (it has a\n * value to sort, filter and span by); `display` is a rendering-only column\n * such as an action or a status chip.\n */\nexport interface TableColumnFactory<T> {\n /**\n * A data column bound to a key of the row.\n *\n * The id defaults to the key and the header to the id, so a column is a\n * one-liner until it needs to say something else — and the id stays the key\n * unless the definition overrides it... it cannot: `ColumnDef` omits `id`,\n * which keeps `state`, saved views and template slots referring to the same\n * string the data is bound to.\n */\n accessor<K extends keyof T & string>(\n key: K,\n definition?: ColumnDef<T, T[K]>,\n ): TableColumn<T, T[K]>;\n\n /**\n * A column with no accessor: a template, an action, a computed badge.\n *\n * It still has an id (that is what `rvTableCell=\"status\"` and the state\n * round-trip refer to), but no value, so sorting and filtering skip it.\n */\n display(id: string, definition?: ColumnDef<T, unknown>): TableColumn<T>;\n}\n\n/**\n * Typed column factory: `tableColumn<Employee>()`.\n *\n * The factory exists instead of an object literal so `accessor('salary')` is\n * checked against the row type (`keyof T`) and the value type (`T[K]`) at the\n * call site — a misspelled key is a compile error, not an empty column. The\n * returned columns are frozen: a column list is configuration, and a mutation\n * after `createTable()` would silently disagree with the cached signals.\n */\nexport function tableColumn<T>(): TableColumnFactory<T> {\n const factory: TableColumnFactory<T> = {\n accessor<K extends keyof T & string>(\n key: K,\n definition?: ColumnDef<T, T[K]>,\n ): TableColumn<T, T[K]> {\n return Object.freeze({\n header: key,\n ...definition,\n id: key,\n accessor: (row: T): T[K] => row[key],\n });\n },\n\n display(id: string, definition?: ColumnDef<T, unknown>): TableColumn<T> {\n return Object.freeze({\n header: id,\n ...definition,\n id,\n });\n },\n };\n return Object.freeze(factory);\n}\n","import { computed, isDevMode, linkedSignal, signal } from '@angular/core';\nimport type { Signal, WritableSignal } from '@angular/core';\nimport type {\n FilterState,\n SortState,\n TableColumn,\n TableRequest,\n TableRow,\n TableRowId,\n TableState,\n} from '../models/table.models';\nimport { assertSpanColumns, buildSpanModel } from '../utils/spans';\nimport type { SpanModel } from '../models/table.models';\nimport { applyFilters, applySearch, applySort, paginate } from '../utils/table-pipeline';\n\n/** How many rows a table can select. */\nexport type TableSelectionKind = 'none' | 'single' | 'multi';\n\n/**\n * The shape of the selection set (not how many rows may be selected).\n *\n * `include` lists the selected ids; `exclude` means \"everything the current\n * rows contain, minus these\". The second form is what makes \"select all 12,345\n * matching rows\" representable without materializing 12,345 ids, and it is why\n * a server can be sent `{ all: true, except: [...] }` instead.\n */\nexport type TableSelectionMode = 'include' | 'exclude';\n\n/** Which rows \"select all\" covers. */\nexport type TableSelectionScope = 'page' | 'filtered' | 'all';\n\n/** Selection configuration. Absent means selection is off. */\nexport interface TableSelectionOptions {\n /**\n * `'none'` renders no selection affordance and makes every method a no-op.\n * @default 'none'\n */\n readonly mode?: TableSelectionKind;\n /**\n * Scope used by `selectAll()` when the caller does not pass one.\n * @default 'filtered'\n */\n readonly selectAllScope?: TableSelectionScope;\n}\n\n/**\n * Server-mode loader shape. Declared so the public contract is stable, but\n * NOT wired in 0.1.0: the client-only build never fetches.\n */\nexport interface TableServerOptions<T> {\n readonly load: (\n request: TableRequest,\n context: { readonly abortSignal: AbortSignal },\n ) => Promise<{ readonly items: readonly T[]; readonly total: number }>;\n}\n\n/**\n * Everything `createTable()` needs.\n *\n * It is a FUNCTION, not an object, because every read must happen inside the\n * controller's `computed`s: passing `data: () => this.api.rows()` (a signal\n * read) is what makes the row model react to new data without a subscription\n * or a `reload()` call.\n */\nexport interface TableOptions<T> {\n /** The declared columns, in display order. */\n readonly columns: () => readonly TableColumn<T>[];\n /** Client mode: the whole data set. */\n readonly data?: () => readonly T[];\n /** Server mode: declared only, not wired in 0.1.0 (the table warns). */\n readonly server?: TableServerOptions<T>;\n /**\n * Row identity. Defaults to `row.id`, then a per-object generated id kept in\n * a `WeakMap`. Never the array index: an index changes under sorting and\n * filtering, and would silently break selection and focus.\n */\n readonly getRowId?: (row: T) => TableRowId;\n /** Locale for collation and formatting; read lazily so it can be a signal. */\n readonly locale?: () => string;\n /** Selection configuration; absent = selection off. */\n readonly selection?: TableSelectionOptions;\n /**\n * Rows per page. `initialState.pageSize` — a restored view — wins over it, so\n * a saved state is never silently replaced by the default.\n * @default 25\n */\n readonly pageSize?: number;\n /** The saved view the table starts from. */\n readonly initialState?: Partial<TableState>;\n /** Skip the client filter/search stage (the caller filtered the data). */\n readonly manualFiltering?: boolean;\n /** Skip the client sort stage (the caller sorted the data). */\n readonly manualSorting?: boolean;\n /** Skip the client page stage (the caller paged the data). */\n readonly manualPagination?: boolean;\n /** Total row count when the data comes from outside. */\n readonly rowCount?: () => number;\n /** Diagnostic sink for span/clamp messages. Defaults to dev-mode console. */\n readonly warn?: (message: string) => void;\n}\n\n/**\n * Row selection as the UI sees it.\n *\n * Reads are signals so a header checkbox and every row cell update together;\n * writes are methods because they must normalize the include/exclude mode and\n * respect `mode: 'none' | 'single'`.\n */\nexport interface RiviniaTableSelection {\n /** True when selection is enabled at all (the UI renders no box otherwise). */\n readonly enabled: boolean;\n /** True for multi-select; false for single-select (checkbox vs radio). */\n readonly multiple: boolean;\n /** Include or exclude set. */\n readonly mode: Signal<TableSelectionMode>;\n /** Selected ids, in row order, limited to the rows the table currently holds. */\n readonly ids: Signal<readonly TableRowId[]>;\n /** `ids().length`. */\n readonly count: Signal<number>;\n select(id: TableRowId): void;\n deselect(id: TableRowId): void;\n toggle(id: TableRowId): void;\n /** Clears the selection (including \"select all\", in every mode). */\n clear(): void;\n isSelected(id: TableRowId): boolean;\n /** Adds every row of the current page; in exclude mode it re-includes them. */\n selectAllOnPage(): void;\n /**\n * `'page'` selects the page; `'filtered'`/`'all'` switch to exclude mode\n * with nothing excluded, i.e. every matching row (and any future one).\n */\n selectAll(scope?: TableSelectionScope): void;\n}\n\nconst DEFAULT_PAGE_SIZE = 25;\n\ninterface SelectionState {\n readonly mode: TableSelectionMode;\n readonly ids: ReadonlySet<TableRowId>;\n}\n\ninterface SelectionConfig<T> {\n readonly rows: Signal<readonly TableRow<T>[]>;\n readonly pageRows: Signal<readonly TableRow<T>[]>;\n readonly kind: TableSelectionKind;\n readonly defaultScope: TableSelectionScope;\n readonly initial: { readonly mode: TableSelectionMode; readonly ids: readonly TableRowId[] };\n}\n\nconst NO_IDS: ReadonlySet<TableRowId> = new Set<TableRowId>();\nconst EMPTY_SELECTION: SelectionState = { mode: 'include', ids: NO_IDS };\n\n/**\n * Drops ids that no longer exist in `rows`.\n *\n * This is the rule that keeps a filtered-out row from staying \"selected\" and\n * reappearing as part of a batch action the user cannot see. It returns the\n * SAME object when nothing was dropped, so a sort or a page change (which\n * produce a new rows array with the same ids) does not churn the selection.\n */\nfunction pruneSelection(\n state: SelectionState,\n rows: readonly { readonly id: TableRowId }[],\n): SelectionState {\n if (state.ids.size === 0) return state;\n const present = new Set<TableRowId>();\n for (const row of rows) present.add(row.id);\n let dropped = false;\n const next = new Set<TableRowId>();\n for (const id of state.ids) {\n if (present.has(id)) next.add(id);\n else dropped = true;\n }\n return dropped ? { mode: state.mode, ids: next } : state;\n}\n\n/** Immutable set helpers: every write produces a new state object. */\nfunction withId(ids: ReadonlySet<TableRowId>, id: TableRowId): ReadonlySet<TableRowId> {\n if (ids.has(id)) return ids;\n const next = new Set(ids);\n next.add(id);\n return next;\n}\n\nfunction withoutId(ids: ReadonlySet<TableRowId>, id: TableRowId): ReadonlySet<TableRowId> {\n if (!ids.has(id)) return ids;\n const next = new Set(ids);\n next.delete(id);\n return next;\n}\n\n/**\n * The selection implementation. Deliberately NOT exported: consumers receive\n * it through `table.selection` typed as `RiviniaTableSelection`, so there is\n * one contract and no second way to construct selection state.\n */\nclass RiviniaSelection<T> implements RiviniaTableSelection {\n readonly enabled: boolean;\n readonly multiple: boolean;\n readonly mode: Signal<TableSelectionMode>;\n readonly ids: Signal<readonly TableRowId[]>;\n readonly count: Signal<number>;\n\n private readonly rows: Signal<readonly TableRow<T>[]>;\n private readonly pageRows: Signal<readonly TableRow<T>[]>;\n private readonly defaultScope: TableSelectionScope;\n private readonly initial: SelectionState;\n /**\n * A linked signal, not a plain one: when the row set changes the ids are\n * pruned IN THE SAME READ, so no stale id can survive a filter change and\n * no effect (or injection context) is needed to do it.\n */\n private readonly state: WritableSignal<SelectionState>;\n\n constructor(config: SelectionConfig<T>) {\n this.rows = config.rows;\n this.pageRows = config.pageRows;\n this.defaultScope = config.defaultScope;\n this.initial = { mode: config.initial.mode, ids: new Set(config.initial.ids) };\n this.enabled = config.kind !== 'none';\n this.multiple = config.kind === 'multi';\n this.state = linkedSignal<readonly TableRow<T>[], SelectionState>({\n source: config.rows,\n computation: (rows, previous) => pruneSelection(previous?.value ?? this.initial, rows),\n });\n this.mode = computed(() => this.state().mode);\n this.ids = computed(() => this.selectedIds());\n this.count = computed(() => this.ids().length);\n }\n\n select(id: TableRowId): void {\n if (!this.enabled) return;\n const state = this.state();\n if (!this.multiple) {\n if (state.mode === 'include' && state.ids.size === 1 && state.ids.has(id)) return;\n this.state.set({ mode: 'include', ids: new Set([id]) });\n return;\n }\n if (state.mode === 'include') {\n const ids = withId(state.ids, id);\n if (ids !== state.ids) this.state.set({ mode: 'include', ids });\n return;\n }\n const ids = withoutId(state.ids, id);\n if (ids !== state.ids) this.state.set({ mode: 'exclude', ids });\n }\n\n deselect(id: TableRowId): void {\n if (!this.enabled) return;\n const state = this.state();\n if (state.mode === 'include') {\n const ids = withoutId(state.ids, id);\n if (ids !== state.ids) this.state.set({ mode: 'include', ids });\n return;\n }\n const ids = withId(state.ids, id);\n if (ids !== state.ids) this.state.set({ mode: 'exclude', ids });\n }\n\n toggle(id: TableRowId): void {\n if (this.isSelected(id)) this.deselect(id);\n else this.select(id);\n }\n\n clear(): void {\n if (!this.enabled) return;\n this.state.set(EMPTY_SELECTION);\n }\n\n isSelected(id: TableRowId): boolean {\n if (!this.enabled) return false;\n const state = this.state();\n return state.mode === 'include' ? state.ids.has(id) : !state.ids.has(id);\n }\n\n selectAllOnPage(): void {\n if (!this.enabled || !this.multiple) return;\n const page = this.pageRows();\n if (page.length === 0) return;\n const state = this.state();\n if (state.mode === 'include') {\n let ids = state.ids;\n for (const row of page) ids = withId(ids, row.id);\n if (ids !== state.ids) this.state.set({ mode: 'include', ids });\n return;\n }\n let ids = state.ids;\n for (const row of page) ids = withoutId(ids, row.id);\n if (ids !== state.ids) this.state.set({ mode: 'exclude', ids });\n }\n\n selectAll(scope?: TableSelectionScope): void {\n if (!this.enabled || !this.multiple) return;\n const resolved = scope ?? this.defaultScope;\n if (resolved === 'page') {\n this.selectAllOnPage();\n return;\n }\n // Nothing matches, so there is nothing to select — entering exclude mode\n // here would select rows that appear later, which is not what was asked.\n if (this.rows().length === 0) return;\n this.state.set({ mode: 'exclude', ids: NO_IDS });\n }\n\n /** Serializable selection, in the shape `TableState` expects. */\n toState(): {\n readonly selected: readonly TableRowId[];\n readonly excluded: readonly TableRowId[];\n } {\n const state = this.state();\n if (state.mode === 'exclude') {\n return { selected: [], excluded: [...state.ids] };\n }\n return { selected: [...state.ids], excluded: [] };\n }\n\n /** Restores a saved view. Pruned immediately so a stale id never leaks. */\n restore(selection: {\n readonly mode: TableSelectionMode;\n readonly ids: readonly TableRowId[];\n }): void {\n if (!this.enabled) return;\n this.state.set(\n pruneSelection({ mode: selection.mode, ids: new Set(selection.ids) }, this.rows()),\n );\n }\n\n private selectedIds(): readonly TableRowId[] {\n const state = this.state();\n const ids: TableRowId[] = [];\n for (const row of this.rows()) {\n const listed = state.ids.has(row.id);\n if (state.mode === 'include' ? listed : !listed) ids.push(row.id);\n }\n return ids;\n }\n}\n\nfunction buildRows<T>(data: readonly T[], getId: (row: T) => TableRowId): readonly TableRow<T>[] {\n const rows = new Array<TableRow<T>>(data.length);\n for (let index = 0; index < data.length; index++) {\n const item = data[index];\n rows[index] = { id: getId(item), data: item, index };\n }\n return rows;\n}\n\n/**\n * Re-numbers `index` after filtering and sorting.\n *\n * `TableRow.index` is documented as the position in `rows`, and the UI uses it\n * for `aria-rowindex`; leaving the source position there would make the grid\n * announce a row number that does not exist on screen.\n */\nfunction reindex<T>(rows: readonly TableRow<T>[]): readonly TableRow<T>[] {\n for (let index = 0; index < rows.length; index++) {\n if (rows[index].index !== index) {\n return rows.map((row, position) => ({ id: row.id, data: row.data, index: position }));\n }\n }\n return rows;\n}\n\nfunction normalizePage(value: number): number {\n return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;\n}\n\nfunction normalizePageSize(value: number): number {\n return Number.isFinite(value) && value > 0 ? Math.floor(value) : DEFAULT_PAGE_SIZE;\n}\n\n/**\n * The table, as a chain of `computed`s — one per pipeline stage.\n *\n * `data -> rows (filter, search, sort) -> pageRows -> spans` means Angular's\n * own caching gives incremental recomputation for free: a page change does not\n * re-sort, a sort does not re-filter, and selection/scroll never rebuild the\n * span matrix because nothing in that chain reads them.\n *\n * The controller owns no fetching and no DOM. It is constructed outside the\n * injector graph (`createTable` needs no provider), which is what lets a\n * consumer build one inside a component field initializer or inside a test.\n */\nexport class RiviniaTable<T> {\n /** The declared columns, hidden ones included, in display order. */\n readonly columns: Signal<readonly TableColumn<T>[]>;\n /** The columns that render: `columns` without `hidden: true`. */\n readonly visibleColumns: Signal<readonly TableColumn<T>[]>;\n /** Filtered and sorted rows, numbered from 0. */\n readonly rows: Signal<readonly TableRow<T>[]>;\n /** The current page of `rows`. */\n readonly pageRows: Signal<readonly TableRow<T>[]>;\n /** The resolved span grid over `pageRows` x `visibleColumns`. */\n readonly spans: Signal<SpanModel>;\n /** Client row count, or `rowCount()` when the data comes from outside. */\n readonly total: Signal<number>;\n /** `ceil(total / pageSize)`; `0` for an empty table. */\n readonly pageCount: Signal<number>;\n\n /** Ordered multi-sort; index 0 is the primary level. */\n readonly sorting: WritableSignal<readonly SortState[]>;\n /** Active column filters, ANDed together. */\n readonly filters: WritableSignal<readonly FilterState[]>;\n /** Global search across every data column. */\n readonly globalSearch: WritableSignal<string>;\n /**\n * 0-based page. A linked signal: it returns to 0 when the filter, sort or\n * search changes (the result set changed under the user), while an explicit\n * `set()` still wins until the next such change.\n */\n readonly page: WritableSignal<number>;\n readonly pageSize: WritableSignal<number>;\n readonly selection: RiviniaTableSelection;\n\n /** What a server-mode fetch would need; `data`-driven tables ignore it. */\n readonly request: Signal<TableRequest>;\n /** The serializable slice: a saved view, a URL, a test assertion. */\n readonly state: Signal<TableState>;\n\n private readonly options: () => TableOptions<T>;\n private readonly initialState: Partial<TableState>;\n /** What `resetState()` returns to: the restored size, else the option. */\n private readonly defaultPageSize: number;\n private readonly warn: (message: string) => void;\n private readonly warned = new Set<string>();\n private readonly generatedIds = new WeakMap<object, string>();\n private readonly selectionImpl: {\n toState(): {\n readonly selected: readonly TableRowId[];\n readonly excluded: readonly TableRowId[];\n };\n restore(selection: {\n readonly mode: TableSelectionMode;\n readonly ids: readonly TableRowId[];\n }): void;\n };\n private generatedIdSeq = 0;\n\n constructor(options: () => TableOptions<T>) {\n this.options = options;\n this.initialState = options().initialState ?? {};\n\n const warnOption = options().warn;\n this.warn = (message: string): void => {\n // One message per defect per table: a span diagnostic repeats on every\n // page rebuild, and a console flooded 20 times is a console ignored.\n if (this.warned.has(message)) return;\n this.warned.add(message);\n if (warnOption !== undefined) warnOption(message);\n else if (isDevMode()) console.warn(`[rivinia/table] ${message}`);\n };\n\n if (options().server !== undefined) {\n this.warn(\n 'server mode is not implemented yet; nothing will be fetched. ' +\n 'Pass `data` and let the app own the request for now',\n );\n }\n\n assertSpanColumns(options().columns(), { warn: this.warn });\n\n const initial = this.initialState;\n const selectionOptions = options().selection;\n\n this.columns = computed(() => this.options().columns());\n this.visibleColumns = computed(() => this.columns().filter((column) => column.hidden !== true));\n\n this.sorting = signal<readonly SortState[]>(initial.sort ?? []);\n this.filters = signal<readonly FilterState[]>(initial.filters ?? []);\n this.globalSearch = signal(initial.search ?? '');\n this.defaultPageSize = normalizePageSize(\n initial.pageSize ?? options().pageSize ?? DEFAULT_PAGE_SIZE,\n );\n this.pageSize = signal(this.defaultPageSize);\n this.page = linkedSignal<string, number>({\n source: () => JSON.stringify([this.filters(), this.sorting(), this.globalSearch()]),\n computation: (_signature, previous) =>\n previous === undefined ? normalizePage(initial.page ?? 0) : 0,\n });\n\n this.rows = computed<readonly TableRow<T>[]>(() => {\n const current = this.options();\n const data = current.data?.() ?? [];\n const getId = current.getRowId ?? this.defaultRowId;\n const columns = this.visibleColumns();\n const all = buildRows(data, getId);\n const filtered = current.manualFiltering\n ? all\n : applySearch(applyFilters(all, columns, this.filters()), columns, this.globalSearch());\n const sorted = current.manualSorting\n ? filtered\n : applySort(filtered, columns, this.sorting(), current.locale?.() ?? 'en');\n return reindex(sorted);\n });\n\n this.total = computed(() => this.options().rowCount?.() ?? this.rows().length);\n this.pageRows = computed<readonly TableRow<T>[]>(() => {\n if (this.options().manualPagination) return this.rows();\n return paginate(this.rows(), this.page(), this.pageSize()).pageRows;\n });\n this.pageCount = computed(() => {\n const total = this.total();\n const size = this.pageSize();\n return size > 0 ? Math.ceil(total / size) : total > 0 ? 1 : 0;\n });\n\n const selection = new RiviniaSelection<T>({\n rows: this.rows,\n pageRows: this.pageRows,\n kind: selectionOptions?.mode ?? 'none',\n defaultScope: selectionOptions?.selectAllScope ?? 'filtered',\n initial: {\n // `excluded` wins when both are present: it is the only field that can\n // mean \"everything except\", and it is the narrower statement.\n mode: (initial.excluded?.length ?? 0) > 0 ? 'exclude' : 'include',\n ids:\n (initial.excluded?.length ?? 0) > 0 ? (initial.excluded ?? []) : (initial.selected ?? []),\n },\n });\n this.selection = selection;\n this.selectionImpl = selection;\n\n this.spans = computed(() =>\n buildSpanModel(this.pageRows(), this.visibleColumns(), {\n warn: this.warn,\n // A runtime column swap must never throw during render; `span` wins.\n strict: false,\n }),\n );\n\n this.request = computed<TableRequest>(() => ({\n page: this.page(),\n pageSize: this.pageSize(),\n sort: this.sorting(),\n filters: this.filters(),\n search: this.globalSearch(),\n }));\n\n this.state = computed<TableState>(() => {\n const columns = this.columns();\n const selected = selection.toState();\n return {\n sort: this.sorting(),\n filters: this.filters(),\n search: this.globalSearch(),\n page: this.page(),\n pageSize: this.pageSize(),\n order: columns.map((column) => column.id),\n hidden: columns.filter((column) => column.hidden === true).map((column) => column.id),\n pinnedStart: columns\n .filter((column) => column.pinned === 'start')\n .map((column) => column.id),\n pinnedEnd: columns.filter((column) => column.pinned === 'end').map((column) => column.id),\n selected: selected.selected,\n excluded: selected.excluded,\n };\n });\n }\n\n /**\n * Applies a saved view. Unknown fields are ignored on purpose: column order,\n * visibility and pinning are owned by the column list in 0.1.0, so a state\n * that carries them round-trips unchanged without pretending the controller\n * can reorder the consumer's columns.\n */\n setState(patch: Partial<TableState>): void {\n if (patch.sort !== undefined) this.sorting.set(patch.sort);\n if (patch.filters !== undefined) this.filters.set(patch.filters);\n if (patch.search !== undefined) this.globalSearch.set(patch.search);\n if (patch.pageSize !== undefined) this.pageSize.set(normalizePageSize(patch.pageSize));\n // `page.set` flushes the linked computation first, so the explicit page\n // survives the reset that the lines above just triggered.\n if (patch.page !== undefined) this.page.set(normalizePage(patch.page));\n if (patch.selected !== undefined || patch.excluded !== undefined) {\n const excluded = patch.excluded ?? [];\n this.selectionImpl.restore(\n excluded.length > 0\n ? { mode: 'exclude', ids: excluded }\n : { mode: 'include', ids: patch.selected ?? [] },\n );\n }\n }\n\n /** Returns to `initialState`, including the initial page and selection. */\n resetState(): void {\n const initial = this.initialState;\n this.sorting.set(initial.sort ?? []);\n this.filters.set(initial.filters ?? []);\n this.globalSearch.set(initial.search ?? '');\n this.pageSize.set(this.defaultPageSize);\n this.page.set(normalizePage(initial.page ?? 0));\n const excluded = initial.excluded ?? [];\n this.selectionImpl.restore(\n excluded.length > 0\n ? { mode: 'exclude', ids: excluded }\n : { mode: 'include', ids: initial.selected ?? [] },\n );\n }\n\n /**\n * `getRowId` -> `row.id` -> a per-object generated id.\n *\n * A primitive row has no identity of its own, so equal primitives collapse\n * into one id; that is documented, and `getRowId` is the answer.\n */\n private readonly defaultRowId = (row: T): TableRowId => {\n if (typeof row === 'object' && row !== null) {\n const candidate = (row as { readonly id?: unknown }).id;\n if (typeof candidate === 'string' || typeof candidate === 'number') return candidate;\n let generated = this.generatedIds.get(row);\n if (generated === undefined) {\n generated = `rv-row-${++this.generatedIdSeq}`;\n this.generatedIds.set(row, generated);\n }\n return generated;\n }\n return String(row);\n };\n}\n\n/**\n * Creates a table from a reactive options function.\n *\n * No DI, no injection context, no HTTP: the caller passes `data` as a signal\n * read and gets derived signals back, so the table is usable in a component\n * field initializer, in a test, or in a headless script.\n */\nexport function createTable<T>(options: () => TableOptions<T>): RiviniaTable<T> {\n return new RiviniaTable<T>(options);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rivinia-table.mjs","names":["isNullish"],"sources":["../../../projects/table/src/lib/utils/table-pipeline.ts","../../../projects/table/src/lib/utils/spans.ts","../../../projects/table/src/lib/utils/table-column.ts","../../../projects/table/src/lib/services/rivinia-table.ts"],"sourcesContent":["import type { FilterState, SortState, TableColumn, TableRow } from '../models/table.models';\n\n/*\n * The row pipeline: filter -> search -> sort -> page, as pure functions.\n *\n * Nothing here imports Angular and nothing mutates its input. The controller\n * composes these into `computed`s, which is what gives incremental\n * recomputation: changing the page does not re-sort, changing the sort does\n * not re-filter. Keeping them free of signals also makes them usable by a\n * headless consumer and testable without a TestBed.\n */\n\n/**\n * The value of one cell, resolved through the column's accessor.\n *\n * The accessor is either a key or a function (the shared contract allows\n * both), so every consumer — the pipeline, the span engine, a custom renderer\n * — reads a cell the same way. A display/action column has no accessor and\n * therefore no value; `undefined` is the honest answer, not an error.\n */\nexport function readCellValue<T, V = unknown>(\n column: TableColumn<T, V>,\n row: TableRow<T>,\n): V | undefined {\n const accessor = column.accessor;\n if (accessor === undefined) return undefined;\n if (typeof accessor === 'string') {\n // The key form is a runtime index; TS cannot prove it exists on a generic\n // `T`, and the column author already asserted it by choosing the key.\n const keyed = row.data as unknown as Record<string, V | undefined>;\n return keyed[accessor];\n }\n return accessor(row.data);\n}\n\n/**\n * A range filter value. Bounds are inclusive and either side may be open, so\n * one shape covers \"from\", \"to\" and \"between\" for both numbers and dates.\n *\n * Numbers and dates share it on purpose: a date bound is compared as a\n * timestamp and a numeric bound as a number, which is exactly what a date\n * input and a number input produce in the UI.\n */\nexport interface TableFilterRange {\n readonly min?: number | Date | null;\n readonly max?: number | Date | null;\n}\n\n/** The result of `paginate`, with the display range the footer needs. */\nexport interface TablePage<T> {\n readonly pageRows: readonly TableRow<T>[];\n /** Rows before pagination. */\n readonly total: number;\n /** `ceil(total / pageSize)`; `0` when there is no row. */\n readonly pageCount: number;\n /** 1-based index of the first row on this page; `0` when the page is empty. */\n readonly start: number;\n /** 1-based index of the last row on this page; `0` when the page is empty. */\n readonly end: number;\n}\n\nconst COLLATORS = new Map<string, Intl.Collator>();\n\n/**\n * One collator per locale, cached. `Intl.Collator` construction is expensive\n * and a sort asks it once per comparison; `numeric` makes \"item 10\" sort after\n * \"item 9\", and `base` ignores case and accents so `Ahmed` and `أحمد` behave\n * like a human expects in a search-and-sort list.\n */\nfunction collatorFor(locale: string): Intl.Collator {\n const key = locale === '' ? 'en' : locale;\n const cached = COLLATORS.get(key);\n if (cached !== undefined) return cached;\n let collator: Intl.Collator;\n try {\n collator = new Intl.Collator(key, { numeric: true, sensitivity: 'base' });\n } catch {\n // An invalid locale tag must degrade to the runtime default, never break\n // a render: a table that throws on `locale = ''` is worse than an\n // approximate order.\n collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });\n }\n COLLATORS.set(key, collator);\n return collator;\n}\n\n/**\n * Case-, accent- and Arabic-aware search key: NFD decomposition, combining\n * marks, tashkeel, tatweel, the alef forms, alef maqsura, hamza carriers and\n * ta marbuta. The same rules the select's search documents — a user typing\n * `احمد` must find `أحمد`, and `محمود` must find `محمّد`.\n */\nfunction normalizeSearchText(value: string): string {\n return value\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/[\\u064B-\\u065F\\u0670]/g, '')\n .replace(/\\u0640/g, '')\n .replace(/[أإآٱ]/g, 'ا')\n .replace(/ى/g, 'ي')\n .replace(/ؤ/g, 'و')\n .replace(/ئ/g, 'ي')\n .replace(/ة/g, 'ه')\n .toLowerCase();\n}\n\nfunction isNullish(value: unknown): value is null | undefined {\n return value === null || value === undefined;\n}\n\n/** An empty string, `null`, `undefined` or an empty array means \"no filter\". */\nfunction isActiveFilter(value: unknown): boolean {\n if (value === null || value === undefined || value === '') return false;\n return !Array.isArray(value) || value.length > 0;\n}\n\nfunction toNumber(value: unknown): number | null {\n if (typeof value === 'number') return Number.isFinite(value) ? value : null;\n if (typeof value === 'bigint') return Number(value);\n if (typeof value === 'string' && value.trim() !== '') {\n const numeric = Number(value);\n return Number.isFinite(numeric) ? numeric : null;\n }\n return null;\n}\n\n/**\n * A comparable timestamp for a date-ish or numeric cell/ bound.\n *\n * Numeric strings are read as numbers BEFORE `Date.parse`, because\n * `Date.parse('5')` happily returns a 2001 date and would make a numeric range\n * silently match nothing.\n */\nfunction toTime(value: unknown): number | null {\n if (value instanceof Date) {\n const time = value.getTime();\n return Number.isNaN(time) ? null : time;\n }\n if (typeof value === 'number') return Number.isFinite(value) ? value : null;\n if (typeof value === 'bigint') return Number(value);\n if (typeof value === 'string') {\n const trimmed = value.trim();\n if (trimmed === '') return null;\n const numeric = Number(trimmed);\n if (Number.isFinite(numeric)) return numeric;\n const parsed = Date.parse(trimmed);\n return Number.isNaN(parsed) ? null : parsed;\n }\n return null;\n}\n\nfunction isRange(value: unknown): value is TableFilterRange {\n return typeof value === 'object' && value !== null && ('min' in value || 'max' in value);\n}\n\nfunction matchesRange(cell: unknown, range: TableFilterRange): boolean {\n const min = range.min === null || range.min === undefined ? null : toTime(range.min);\n const max = range.max === null || range.max === undefined ? null : toTime(range.max);\n if (min === null && max === null) return true;\n const time = toTime(cell);\n if (time === null) return false;\n return (min === null || time >= min) && (max === null || time <= max);\n}\n\n/**\n * What a filter value means is decided by its SHAPE, because the shared\n * contract stores it as `unknown` and a column carries no filter kind.\n * `'text'` -> substring, `true` -> boolean, `5` -> number, `Date` -> day,\n * `[...]` -> one of, `{min,max}` -> range. Everything else falls back to\n * identity, which is safe: an unrecognised filter can only be too strict,\n * never silently match everything.\n */\nfunction matchesFilter(cell: unknown, filter: unknown): boolean {\n if (Array.isArray(filter)) return filter.some((item) => Object.is(item, cell));\n if (typeof filter === 'boolean') return Boolean(cell) === filter;\n if (filter instanceof Date) {\n const time = toTime(cell);\n return time !== null && time === filter.getTime();\n }\n if (typeof filter === 'number') {\n const numeric = toNumber(cell);\n return numeric !== null && numeric === filter;\n }\n if (typeof filter === 'bigint') {\n const numeric = toNumber(cell);\n return numeric !== null && numeric === Number(filter);\n }\n if (isRange(filter)) return matchesRange(cell, filter);\n if (typeof filter === 'string') {\n return normalizeSearchText(String(cell ?? '')).includes(normalizeSearchText(filter));\n }\n return Object.is(cell, filter);\n}\n\n/**\n * Keeps the rows that satisfy EVERY active filter.\n *\n * A filter on a column that is hidden, unknown, or has no accessor is ignored\n * rather than treated as \"match nothing\": a saved view that references a\n * removed column should lose that constraint, not empty the table.\n */\nexport function applyFilters<T>(\n rows: readonly TableRow<T>[],\n columns: readonly TableColumn<T>[],\n filters: readonly FilterState[],\n): readonly TableRow<T>[] {\n if (filters.length === 0) return rows;\n\n const byId = new Map(columns.map((column) => [column.id, column]));\n const plan: Array<{ readonly column: TableColumn<T>; readonly value: unknown }> = [];\n for (const filter of filters) {\n if (!isActiveFilter(filter.value)) continue;\n const column = byId.get(filter.column);\n if (column === undefined || column.accessor === undefined) continue;\n plan.push({ column, value: filter.value });\n }\n if (plan.length === 0) return rows;\n\n return rows.filter((row) => {\n for (const entry of plan) {\n if (!matchesFilter(readCellValue(entry.column, row), entry.value)) return false;\n }\n return true;\n });\n}\n\n/**\n * Global search across every column that has a value.\n *\n * Display columns are skipped (they hold no data), and the needle is\n * normalized with the same Arabic rules as the filters, so search finds a name\n * written with or without diacritics. An empty search returns the input array\n * unchanged — identity matters, because the controller's `computed` chain\n * uses it to avoid downstream work.\n */\nexport function applySearch<T>(\n rows: readonly TableRow<T>[],\n columns: readonly TableColumn<T>[],\n search: string,\n): readonly TableRow<T>[] {\n const needle = normalizeSearchText(search.trim());\n if (needle === '') return rows;\n const searchable = columns.filter((column) => column.accessor !== undefined);\n if (searchable.length === 0) return [];\n\n return rows.filter((row) => {\n for (const column of searchable) {\n const value = readCellValue(column, row);\n if (value === null || value === undefined) continue;\n if (normalizeSearchText(String(value)).includes(needle)) return true;\n }\n return false;\n });\n}\n\n/**\n * Compares two present (non-nullish) values. `column.comparator` wins when the\n * consumer declared one, because only the consumer knows what the value means.\n */\nfunction comparePresent(\n left: unknown,\n right: unknown,\n comparator: ((left: unknown, right: unknown) => number) | undefined,\n collator: Intl.Collator,\n): number {\n if (comparator !== undefined) return comparator(left, right);\n if (left instanceof Date && right instanceof Date) {\n const a = left.getTime();\n const b = right.getTime();\n if (!Number.isNaN(a) && !Number.isNaN(b)) return a < b ? -1 : a > b ? 1 : 0;\n }\n if (typeof left === 'number' && typeof right === 'number') {\n return left < right ? -1 : left > right ? 1 : 0;\n }\n if (typeof left === 'bigint' && typeof right === 'bigint') {\n return left < right ? -1 : left > right ? 1 : 0;\n }\n if (typeof left === 'boolean' && typeof right === 'boolean') {\n return left === right ? 0 : left ? 1 : -1;\n }\n return collator.compare(String(left), String(right));\n}\n\n/**\n * Multi-level, stable sort with nulls LAST in both directions.\n *\n * Two decisions worth stating: a missing value always sorts to the end (a\n * blank salary at the top of an ascending list is noise, and flipping it with\n * the direction would put it at the top of a descending one too), and the\n * pre-sort index is the final tie-break, so equal keys keep their order even\n * on an engine whose sort is not stable.\n *\n * Sort levels that reference a hidden, unknown or accessor-less column are\n * skipped rather than applied as a no-op, so `sorting` never lies about what\n * is active.\n */\nexport function applySort<T>(\n rows: readonly TableRow<T>[],\n columns: readonly TableColumn<T>[],\n sorting: readonly SortState[],\n locale = 'en',\n): readonly TableRow<T>[] {\n if (sorting.length === 0) return rows;\n\n const byId = new Map(columns.map((column) => [column.id, column]));\n const plan: Array<{\n readonly state: SortState;\n readonly column: TableColumn<T>;\n }> = [];\n for (const state of sorting) {\n const column = byId.get(state.column);\n if (column === undefined || column.accessor === undefined) continue;\n plan.push({ state, column });\n }\n if (plan.length === 0) return rows;\n\n const collator = collatorFor(locale);\n // Keys are read once per row, not once per comparison: at 10k rows the\n // accessor would otherwise run ~130k times instead of 10k.\n const keyed = rows.map((row) => ({\n row,\n keys: plan.map((entry) => readCellValue(entry.column, row)),\n }));\n\n keyed.sort((a, b) => {\n for (let index = 0; index < plan.length; index++) {\n const left = a.keys[index];\n const right = b.keys[index];\n const leftMissing = isNullish(left);\n const rightMissing = isNullish(right);\n if (leftMissing || rightMissing) {\n if (leftMissing && rightMissing) continue;\n return leftMissing ? 1 : -1;\n }\n const entry = plan[index];\n const result = comparePresent(left, right, entry.column.comparator, collator);\n if (result !== 0) return entry.state.direction === 'desc' ? -result : result;\n }\n return a.row.index - b.row.index;\n });\n\n return keyed.map((entry) => entry.row);\n}\n\n/**\n * Slices one page and reports the footer's numbers.\n *\n * `start` / `end` are 1-based and inclusive (\"1-25 of 12,345\"), and both are 0\n * when the page holds nothing — a page past the end is legal and returns an\n * empty page, it is not silently clamped to the last one (clamping would hide\n * a controller bug and lie about the requested state).\n *\n * A non-positive / non-finite `pageSize` means \"no pagination\": every row on\n * one page. That is a deliberate degradation rather than a crash.\n */\nexport function paginate<T>(\n rows: readonly TableRow<T>[],\n page: number,\n pageSize: number,\n): TablePage<T> {\n const total = rows.length;\n const size = Number.isFinite(pageSize) ? Math.floor(pageSize) : 0;\n const currentPage = Number.isFinite(page) ? Math.max(0, Math.floor(page)) : 0;\n\n if (size <= 0) {\n return {\n pageRows: rows,\n total,\n pageCount: total > 0 ? 1 : 0,\n start: total > 0 ? 1 : 0,\n end: total,\n };\n }\n\n const pageCount = Math.ceil(total / size);\n const offset = currentPage * size;\n const pageRows = offset >= total ? [] : rows.slice(offset, offset + size);\n\n return {\n pageRows,\n total,\n pageCount,\n start: pageRows.length === 0 ? 0 : offset + 1,\n end: pageRows.length === 0 ? 0 : offset + pageRows.length,\n };\n}\n","import { isDevMode } from '@angular/core';\nimport type {\n CellSpan,\n SpanCell,\n SpanContext,\n SpanModel,\n TableColumn,\n TableRow,\n} from '../models/table.models';\nimport { readCellValue } from './table-pipeline';\n\n/**\n * Where span diagnostics go. A `warn` sink is injected rather than hard-coded\n * so a consumer can route the messages through its own logger, and a test can\n * assert them without touching the console.\n */\nexport interface SpanWarnOptions {\n readonly warn?: (message: string) => void;\n}\n\n/**\n * `buildSpanModel` options.\n *\n * `strict` exists because the same builder serves two callers with opposite\n * needs: `createTable()` validates its column list ONCE and may throw (a\n * conflicting declaration is a bug in code, not in data), while a render-time\n * rebuild must never throw — an app that swaps its columns at runtime would\n * otherwise blank the grid. Strict mode is the documented default; the\n * controller passes `strict: false` and lets `span` win, as the design says.\n */\nexport interface SpanBuildOptions extends SpanWarnOptions {\n /**\n * Throw when a column declares `span` together with `merge`/`colSpan`.\n * @default true\n */\n readonly strict?: boolean;\n}\n\nconst NO_CELLS: readonly SpanCell[] = Object.freeze([]);\n\n/**\n * A headless core may run under SSR, in a worker, or inside a test runner, so\n * the default sink is dev-mode only: it costs nothing in production and never\n * writes to a console the library does not own.\n */\nfunction defaultWarn(message: string): void {\n if (isDevMode()) {\n console.warn(`[rivinia/table] ${message}`);\n }\n}\n\nfunction isNullish(value: unknown): value is null | undefined {\n return value === null || value === undefined;\n}\n\nfunction clamp(value: number, min: number, max: number): number {\n if (!Number.isFinite(value)) return min;\n return value < min ? min : value > max ? max : value;\n}\n\n/**\n * Shared declaration checks.\n *\n * The span rules are mutually exclusive by design: `merge` and `colSpan` are\n * the simple path and compose into one rectangle, `span` is full manual\n * control. Accepting both would make the outcome depend on evaluation order,\n * which is exactly the kind of silent surprise a table cannot afford.\n */\nfunction checkColumns<T>(\n columns: readonly TableColumn<T>[],\n warn: (message: string) => void,\n strict: boolean,\n): void {\n for (const column of columns) {\n const conflicting =\n column.merge !== undefined ? 'merge' : column.colSpan !== undefined ? 'colSpan' : null;\n if (column.span !== undefined && conflicting !== null) {\n const message =\n `column \"${column.id}\" declares both span and ${conflicting}; ` +\n 'use span alone for full control, or merge/colSpan for the simple path';\n if (strict) {\n throw new Error(`[rivinia/table] ${message}`);\n }\n warn(`${message}; span wins`);\n }\n if (column.merge !== undefined && column.accessor === undefined) {\n // A display column has no value to compare, so merging it would merge\n // every row \"by accident\". Warn once and render plain cells instead.\n warn(`column \"${column.id}\" declares merge but has no accessor; merge is ignored`);\n }\n }\n}\n\n/**\n * Validates a column list without touching any data. `createTable()` calls\n * this at construction so a conflicting declaration fails immediately — with\n * an actionable message — instead of at the first paint, and in production\n * too, because it is a defect in the column list rather than in the data.\n */\nexport function assertSpanColumns<T>(\n columns: readonly TableColumn<T>[],\n options?: SpanWarnOptions,\n): void {\n checkColumns(columns, options?.warn ?? defaultWarn, true);\n}\n\n/**\n * Resolves `span` / `merge` / `colSpan` into the concrete span grid of the\n * given rows and columns.\n *\n * Semantics (ported from the validated prototype, 35/35 contract checks):\n * - only anchors exist; a covered position has no cell and, in the UI, no DOM;\n * - first claim wins: an overlapping later span is shrunk (width, then height)\n * and reported, so a configuration hazard is never silent;\n * - row spans stop at the page (a page is a DOM boundary) and column spans at\n * the visible range and at the pinned-region boundary;\n * - `Infinity` means \"the rest of my region\", resolved before integer checks;\n * - `0`, negatives, `NaN` and fractions are clamped to 1 with a warning, so a\n * bad return value can never delete a cell;\n * - rows must already be the rendered order: a merge follows adjacency, so\n * sorting legitimately starts a new run.\n *\n * The model is a dense matrix of owner indices, so `at()` / `owner()` are O(1)\n * and `move()` walks only the cells it skips.\n */\nexport function buildSpanModel<T>(\n rows: readonly TableRow<T>[],\n columns: readonly TableColumn<T>[],\n options?: SpanBuildOptions,\n): SpanModel {\n const warn = options?.warn ?? defaultWarn;\n checkColumns(columns, warn, options?.strict ?? true);\n\n const rowCount = rows.length;\n const columnCount = columns.length;\n const hasRules = columns.some(\n (column) =>\n column.span !== undefined ||\n column.colSpan !== undefined ||\n (column.merge !== undefined && column.accessor !== undefined),\n );\n if (!hasRules || rowCount === 0 || columnCount === 0) {\n return plainSpanModel(rowCount, columnCount);\n }\n\n const claimed = new Uint8Array(rowCount * columnCount);\n const owners = new Int32Array(rowCount * columnCount).fill(-1);\n const cells: SpanCell[] = [];\n\n /** Resolves one cell's request. `span` wins over `merge` / `colSpan`. */\n const resolve = (definition: TableColumn<T>, row: number, column: number): CellSpan => {\n const current = rows[row];\n const value = readCellValue(definition, current);\n\n const spanResolver = definition.span;\n if (spanResolver !== undefined) {\n const explicit = spanResolver(contextOf(current, definition, value, row, column));\n if (explicit !== null && explicit !== undefined) {\n return { rowSpan: explicit.rowSpan ?? 1, colSpan: explicit.colSpan ?? 1 };\n }\n }\n\n let rowSpan = 1;\n let colSpan = 1;\n\n /*\n * Row run. The anchor is the run's FIRST row and every candidate is\n * compared with it, never with its neighbour: that makes runs transitive\n * by construction (A = B and B = C cannot produce a run that excludes A).\n */\n const merge = definition.merge;\n if (merge !== undefined && definition.accessor !== undefined) {\n if (merge === 'equal') {\n // Nullish values never merge: an empty cell must not swallow the rows\n // below it, which is what an `undefined === undefined` run would do.\n if (!isNullish(value)) {\n let end = row;\n while (end + 1 < rowCount) {\n const candidate = readCellValue(definition, rows[end + 1]);\n if (isNullish(candidate) || !Object.is(candidate, value)) break;\n end++;\n }\n rowSpan = end - row + 1;\n }\n } else {\n const anchor = current.data;\n let end = row;\n while (end + 1 < rowCount && merge(rows[end + 1].data, anchor)) end++;\n rowSpan = end - row + 1;\n }\n }\n\n // The column half composes with the row half, so merge + colSpan is ONE\n // rectangle (a department cell covering its rows and its neighbour).\n const colSpanDefinition = definition.colSpan;\n if (colSpanDefinition !== undefined) {\n colSpan =\n typeof colSpanDefinition === 'function'\n ? colSpanDefinition(contextOf(current, definition, value, row, column))\n : colSpanDefinition;\n }\n\n return { rowSpan, colSpan };\n };\n\n for (let row = 0; row < rowCount; row++) {\n for (let column = 0; column < columnCount; column++) {\n if (claimed[row * columnCount + column] !== 0) continue;\n const definition = columns[column];\n const requested = resolve(definition, row, column);\n\n let rowSpan = requested.rowSpan ?? 1;\n let colSpan = requested.colSpan ?? 1;\n // `Infinity` is resolved before the integer checks, so \"the rest of the\n // region\" is not a special case for every later stage.\n if (colSpan === Number.POSITIVE_INFINITY) colSpan = columnCount - column;\n\n if (!Number.isInteger(rowSpan) || rowSpan < 1) {\n warn(`column \"${definition.id}\" returned rowSpan ${rowSpan}; clamped to 1`);\n rowSpan = 1;\n }\n if (!Number.isInteger(colSpan) || colSpan < 1) {\n warn(`column \"${definition.id}\" returned colSpan ${colSpan}; clamped to 1`);\n colSpan = 1;\n }\n\n // A page is a DOM boundary and a pinned region is a sticky boundary;\n // a span may not cross either, so both are hard clamps (and silent:\n // they are the documented meaning of a request, not a hazard).\n rowSpan = Math.min(rowSpan, rowCount - row);\n colSpan = Math.min(colSpan, columnCount - column);\n\n const pinning = definition.pinned ?? null;\n for (let step = 1; step < colSpan; step++) {\n if ((columns[column + step].pinned ?? null) !== pinning) {\n colSpan = step;\n break;\n }\n }\n\n const wantedRowSpan = rowSpan;\n const wantedColSpan = colSpan;\n while (colSpan > 1 && !isFree(claimed, columnCount, row, column, rowSpan, colSpan)) {\n colSpan--;\n }\n while (rowSpan > 1 && !isFree(claimed, columnCount, row, column, rowSpan, colSpan)) {\n rowSpan--;\n }\n if (rowSpan !== wantedRowSpan || colSpan !== wantedColSpan) {\n warn(\n `column \"${definition.id}\" at row ${row} overlaps an existing span; ` +\n `shrunk from ${wantedRowSpan}x${wantedColSpan} to ${rowSpan}x${colSpan}`,\n );\n }\n\n const anchor = cells.length;\n cells.push({ row, column, rowSpan, colSpan });\n for (let r = row; r < row + rowSpan; r++) {\n for (let c = column; c < column + colSpan; c++) {\n claimed[r * columnCount + c] = 1;\n owners[r * columnCount + c] = anchor;\n }\n }\n }\n }\n\n // No column actually spanned: hand back the cheap model, not a matrix of\n // 1x1 anchors that every renderer would then have to walk.\n if (cells.length === rowCount * columnCount) {\n return plainSpanModel(rowCount, columnCount);\n }\n\n const ownerAt = (row: number, column: number): SpanCell | null => {\n if (row < 0 || row >= rowCount || column < 0 || column >= columnCount) return null;\n const index = owners[row * columnCount + column];\n return index < 0 ? null : cells[index];\n };\n\n return {\n cells,\n at(row, column) {\n const cell = ownerAt(row, column);\n return cell !== null && cell.row === row && cell.column === column ? cell : null;\n },\n owner: ownerAt,\n move(row, column, dRow, dColumn) {\n // A stale position (the page shrank under the UI) is clamped rather\n // than trusted: focus must never land outside the grid.\n if (dRow === 0 && dColumn === 0) {\n return { row: clamp(row, 0, rowCount - 1), column: clamp(column, 0, columnCount - 1) };\n }\n let currentRow = clamp(row, 0, rowCount - 1);\n let currentColumn = clamp(column, 0, columnCount - 1);\n for (;;) {\n const nextRow = currentRow + dRow;\n const nextColumn = currentColumn + dColumn;\n if (nextRow < 0 || nextRow >= rowCount || nextColumn < 0 || nextColumn >= columnCount) {\n return { row: currentRow, column: currentColumn };\n }\n const span = ownerAt(nextRow, nextColumn);\n if (span === null) return { row: nextRow, column: nextColumn };\n\n /*\n * Moving INSIDE a merged cell leaves it; moving INTO one lands on its\n * anchor. Without the first half, ArrowDown inside a 7-row merge would\n * walk the covered cells one at a time and focus could sit on a\n * position that has no rendered cell.\n */\n const inside =\n currentRow >= span.row &&\n currentRow < span.row + span.rowSpan &&\n currentColumn >= span.column &&\n currentColumn < span.column + span.colSpan;\n if (!inside) return { row: span.row, column: span.column };\n\n const jumpRow = dRow === 0 ? nextRow : dRow > 0 ? span.row + span.rowSpan : span.row - 1;\n const jumpColumn =\n dColumn === 0 ? nextColumn : dColumn > 0 ? span.column + span.colSpan : span.column - 1;\n if (jumpRow < 0 || jumpRow >= rowCount || jumpColumn < 0 || jumpColumn >= columnCount) {\n return { row: currentRow, column: currentColumn };\n }\n const jumped = ownerAt(jumpRow, jumpColumn);\n /*\n * Landing on a RENDERED cell ends the step, and an anchor is rendered:\n * one arrow press moves one logical row, so the anchor of the next run\n * is the destination. Re-entering the loop there would treat the anchor\n * as \"inside a span\" and walk through every following run — a merged\n * column would jump from run 1 to the last run in a single ArrowDown.\n * Only a COVERED cell (possible on a diagonal step into another span)\n * keeps walking, and then it lands on that span's anchor.\n */\n if (jumped === null || (jumped.row === jumpRow && jumped.column === jumpColumn)) {\n return { row: jumpRow, column: jumpColumn };\n }\n currentRow = jumpRow;\n currentColumn = jumpColumn;\n }\n },\n };\n}\n\nfunction contextOf<T>(\n row: TableRow<T>,\n column: TableColumn<T>,\n value: unknown,\n rowIndex: number,\n columnIndex: number,\n): SpanContext<T> {\n return { row, column, value, rowIndex, columnIndex };\n}\n\n/** Rectangle test that ignores the anchor cell itself (always free by then). */\nfunction isFree(\n claimed: Uint8Array,\n columnCount: number,\n row: number,\n column: number,\n rowSpan: number,\n colSpan: number,\n): boolean {\n for (let r = row; r < row + rowSpan; r++) {\n for (let c = column; c < column + colSpan; c++) {\n if (r === row && c === column) continue;\n if (claimed[r * columnCount + c] !== 0) return false;\n }\n }\n return true;\n}\n\n/**\n * The model for a grid where nothing (or nothing that matters) spans.\n *\n * It is intentionally NOT a single shared constant: `move()` must clamp at the\n * edges, which needs the grid size. The expensive part — the two typed arrays\n * — is what the fast path skips; these three closures cost nothing per frame.\n */\nfunction plainSpanModel(rowCount: number, columnCount: number): SpanModel {\n return {\n cells: NO_CELLS,\n at: () => null,\n owner: () => null,\n move(row, column, dRow, dColumn) {\n if (rowCount === 0 || columnCount === 0) return { row: 0, column: 0 };\n return {\n row: clamp(row + dRow, 0, rowCount - 1),\n column: clamp(column + dColumn, 0, columnCount - 1),\n };\n },\n };\n}\n","import type { ColumnDef, TableColumn } from '../models/table.models';\n\n/**\n * The two ways a column is declared. `accessor` is a data column (it has a\n * value to sort, filter and span by); `display` is a rendering-only column\n * such as an action or a status chip.\n */\nexport interface TableColumnFactory<T> {\n /**\n * A data column bound to a key of the row.\n *\n * The id defaults to the key and the header to the id, so a column is a\n * one-liner until it needs to say something else — and the id stays the key\n * unless the definition overrides it... it cannot: `ColumnDef` omits `id`,\n * which keeps `state`, saved views and template slots referring to the same\n * string the data is bound to.\n */\n accessor<K extends keyof T & string>(\n key: K,\n definition?: ColumnDef<T, T[K]>,\n ): TableColumn<T, T[K]>;\n\n /**\n * A column with no accessor: a template, an action, a computed badge.\n *\n * It still has an id (that is what `rvTableCell=\"status\"` and the state\n * round-trip refer to), but no value, so sorting and filtering skip it.\n */\n display(id: string, definition?: ColumnDef<T, unknown>): TableColumn<T>;\n}\n\n/**\n * Typed column factory: `tableColumn<Employee>()`.\n *\n * The factory exists instead of an object literal so `accessor('salary')` is\n * checked against the row type (`keyof T`) and the value type (`T[K]`) at the\n * call site — a misspelled key is a compile error, not an empty column. The\n * returned columns are frozen: a column list is configuration, and a mutation\n * after `createTable()` would silently disagree with the cached signals.\n */\nexport function tableColumn<T>(): TableColumnFactory<T> {\n const factory: TableColumnFactory<T> = {\n accessor<K extends keyof T & string>(\n key: K,\n definition?: ColumnDef<T, T[K]>,\n ): TableColumn<T, T[K]> {\n return Object.freeze({\n header: key,\n ...definition,\n id: key,\n accessor: (row: T): T[K] => row[key],\n });\n },\n\n display(id: string, definition?: ColumnDef<T, unknown>): TableColumn<T> {\n return Object.freeze({\n header: id,\n ...definition,\n id,\n });\n },\n };\n return Object.freeze(factory);\n}\n","import { computed, isDevMode, linkedSignal, signal } from '@angular/core';\nimport type { Signal, WritableSignal } from '@angular/core';\nimport type {\n FilterState,\n SortState,\n TableColumn,\n TableRequest,\n TableRow,\n TableRowId,\n TableState,\n} from '../models/table.models';\nimport { assertSpanColumns, buildSpanModel } from '../utils/spans';\nimport type { SpanModel } from '../models/table.models';\nimport { applyFilters, applySearch, applySort, paginate } from '../utils/table-pipeline';\n\n/** How many rows a table can select. */\nexport type TableSelectionKind = 'none' | 'single' | 'multi';\n\n/**\n * The shape of the selection set (not how many rows may be selected).\n *\n * `include` lists the selected ids; `exclude` means \"everything the current\n * rows contain, minus these\". The second form is what makes \"select all 12,345\n * matching rows\" representable without materializing 12,345 ids, and it is why\n * a server can be sent `{ all: true, except: [...] }` instead.\n */\nexport type TableSelectionMode = 'include' | 'exclude';\n\n/** Which rows \"select all\" covers. */\nexport type TableSelectionScope = 'page' | 'filtered' | 'all';\n\n/** Selection configuration. Absent means selection is off. */\nexport interface TableSelectionOptions {\n /**\n * `'none'` renders no selection affordance and makes every method a no-op.\n * @default 'none'\n */\n readonly mode?: TableSelectionKind;\n /**\n * Scope used by `selectAll()` when the caller does not pass one.\n * @default 'filtered'\n */\n readonly selectAllScope?: TableSelectionScope;\n}\n\n/**\n * Server-mode loader shape. Declared so the public contract is stable, but\n * NOT wired in 0.1.0: the client-only build never fetches.\n */\nexport interface TableServerOptions<T> {\n readonly load: (\n request: TableRequest,\n context: { readonly abortSignal: AbortSignal },\n ) => Promise<{ readonly items: readonly T[]; readonly total: number }>;\n}\n\n/**\n * Everything `createTable()` needs.\n *\n * It is a FUNCTION, not an object, because every read must happen inside the\n * controller's `computed`s: passing `data: () => this.api.rows()` (a signal\n * read) is what makes the row model react to new data without a subscription\n * or a `reload()` call.\n */\nexport interface TableOptions<T> {\n /** The declared columns, in display order. */\n readonly columns: () => readonly TableColumn<T>[];\n /** Client mode: the whole data set. */\n readonly data?: () => readonly T[];\n /** Server mode: declared only, not wired in 0.1.0 (the table warns). */\n readonly server?: TableServerOptions<T>;\n /**\n * Row identity. IDs must be unique, stable, non-empty strings or finite\n * numbers within the full data set. In development invalid or duplicate\n * values throw with their source row positions. In production ambiguous\n * rows remain visible but identity-dependent actions (such as selection) are\n * disabled for them. Defaults to `row.id`, then a per-object generated id\n * kept in a `WeakMap`; never use the array index.\n */\n readonly getRowId?: (row: T) => TableRowId;\n /** Locale for collation and formatting; read lazily so it can be a signal. */\n readonly locale?: () => string;\n /** Selection configuration; absent = selection off. */\n readonly selection?: TableSelectionOptions;\n /**\n * Rows per page. `initialState.pageSize` — a restored view — wins over it, so\n * a saved state is never silently replaced by the default.\n * @default 25\n */\n readonly pageSize?: number;\n /** The saved view the table starts from. */\n readonly initialState?: Partial<TableState>;\n /** Skip the client filter/search stage (the caller filtered the data). */\n readonly manualFiltering?: boolean;\n /** Skip the client sort stage (the caller sorted the data). */\n readonly manualSorting?: boolean;\n /** Skip the client page stage (the caller paged the data). */\n readonly manualPagination?: boolean;\n /** Total row count when the data comes from outside. */\n readonly rowCount?: () => number;\n /** Diagnostic sink for span/clamp messages. Defaults to dev-mode console. */\n readonly warn?: (message: string) => void;\n}\n\n/**\n * Row selection as the UI sees it.\n *\n * Reads are signals so a header checkbox and every row cell update together;\n * writes are methods because they must normalize the include/exclude mode and\n * respect `mode: 'none' | 'single'`.\n */\nexport interface RiviniaTableSelection {\n /** True when selection is enabled at all (the UI renders no box otherwise). */\n readonly enabled: boolean;\n /** True for multi-select; false for single-select (checkbox vs radio). */\n readonly multiple: boolean;\n /** Include or exclude set. */\n readonly mode: Signal<TableSelectionMode>;\n /** Selected ids, in row order, limited to the rows the table currently holds. */\n readonly ids: Signal<readonly TableRowId[]>;\n /** `ids().length`. */\n readonly count: Signal<number>;\n select(id: TableRowId): void;\n deselect(id: TableRowId): void;\n toggle(id: TableRowId): void;\n /** Clears the selection (including \"select all\", in every mode). */\n clear(): void;\n isSelected(id: TableRowId): boolean;\n /** Adds every row of the current page; in exclude mode it re-includes them. */\n selectAllOnPage(): void;\n /**\n * `'page'` selects the page; `'filtered'`/`'all'` switch to exclude mode\n * with nothing excluded, i.e. every matching row (and any future one).\n */\n selectAll(scope?: TableSelectionScope): void;\n}\n\nconst DEFAULT_PAGE_SIZE = 25;\n\ninterface SelectionState {\n readonly mode: TableSelectionMode;\n readonly ids: ReadonlySet<TableRowId>;\n}\n\ninterface SelectionConfig<T> {\n readonly rows: Signal<readonly TableRow<T>[]>;\n readonly pageRows: Signal<readonly TableRow<T>[]>;\n readonly kind: TableSelectionKind;\n readonly defaultScope: TableSelectionScope;\n readonly initial: { readonly mode: TableSelectionMode; readonly ids: readonly TableRowId[] };\n}\n\nconst NO_IDS: ReadonlySet<TableRowId> = new Set<TableRowId>();\nconst EMPTY_SELECTION: SelectionState = { mode: 'include', ids: NO_IDS };\n\n/**\n * Drops ids that no longer exist in `rows`.\n *\n * This is the rule that keeps a filtered-out row from staying \"selected\" and\n * reappearing as part of a batch action the user cannot see. It returns the\n * SAME object when nothing was dropped, so a sort or a page change (which\n * produce a new rows array with the same ids) does not churn the selection.\n */\nfunction pruneSelection(\n state: SelectionState,\n rows: readonly { readonly id: TableRowId; readonly identityValid?: boolean }[],\n): SelectionState {\n if (state.ids.size === 0) return state;\n const present = new Set<TableRowId>();\n for (const row of rows) if (row.identityValid !== false) present.add(row.id);\n let dropped = false;\n const next = new Set<TableRowId>();\n for (const id of state.ids) {\n if (present.has(id)) next.add(id);\n else dropped = true;\n }\n return dropped ? { mode: state.mode, ids: next } : state;\n}\n\n/** Immutable set helpers: every write produces a new state object. */\nfunction withId(ids: ReadonlySet<TableRowId>, id: TableRowId): ReadonlySet<TableRowId> {\n if (ids.has(id)) return ids;\n const next = new Set(ids);\n next.add(id);\n return next;\n}\n\nfunction withoutId(ids: ReadonlySet<TableRowId>, id: TableRowId): ReadonlySet<TableRowId> {\n if (!ids.has(id)) return ids;\n const next = new Set(ids);\n next.delete(id);\n return next;\n}\n\n/**\n * The selection implementation. Deliberately NOT exported: consumers receive\n * it through `table.selection` typed as `RiviniaTableSelection`, so there is\n * one contract and no second way to construct selection state.\n */\nclass RiviniaSelection<T> implements RiviniaTableSelection {\n readonly enabled: boolean;\n readonly multiple: boolean;\n readonly mode: Signal<TableSelectionMode>;\n readonly ids: Signal<readonly TableRowId[]>;\n readonly count: Signal<number>;\n\n private readonly rows: Signal<readonly TableRow<T>[]>;\n private readonly pageRows: Signal<readonly TableRow<T>[]>;\n private readonly defaultScope: TableSelectionScope;\n private readonly initial: SelectionState;\n /**\n * A linked signal, not a plain one: when the row set changes the ids are\n * pruned IN THE SAME READ, so no stale id can survive a filter change and\n * no effect (or injection context) is needed to do it.\n */\n private readonly state: WritableSignal<SelectionState>;\n\n constructor(config: SelectionConfig<T>) {\n this.rows = config.rows;\n this.pageRows = config.pageRows;\n this.defaultScope = config.defaultScope;\n this.initial = { mode: config.initial.mode, ids: new Set(config.initial.ids) };\n this.enabled = config.kind !== 'none';\n this.multiple = config.kind === 'multi';\n this.state = linkedSignal<readonly TableRow<T>[], SelectionState>({\n source: config.rows,\n computation: (rows, previous) => pruneSelection(previous?.value ?? this.initial, rows),\n });\n this.mode = computed(() => this.state().mode);\n this.ids = computed(() => this.selectedIds());\n this.count = computed(() => this.ids().length);\n }\n\n select(id: TableRowId): void {\n if (!this.enabled || !this.isUnambiguous(id)) return;\n const state = this.state();\n if (!this.multiple) {\n if (state.mode === 'include' && state.ids.size === 1 && state.ids.has(id)) return;\n this.state.set({ mode: 'include', ids: new Set([id]) });\n return;\n }\n if (state.mode === 'include') {\n const ids = withId(state.ids, id);\n if (ids !== state.ids) this.state.set({ mode: 'include', ids });\n return;\n }\n const ids = withoutId(state.ids, id);\n if (ids !== state.ids) this.state.set({ mode: 'exclude', ids });\n }\n\n deselect(id: TableRowId): void {\n if (!this.enabled || !this.isUnambiguous(id)) return;\n const state = this.state();\n if (state.mode === 'include') {\n const ids = withoutId(state.ids, id);\n if (ids !== state.ids) this.state.set({ mode: 'include', ids });\n return;\n }\n const ids = withId(state.ids, id);\n if (ids !== state.ids) this.state.set({ mode: 'exclude', ids });\n }\n\n toggle(id: TableRowId): void {\n if (this.isSelected(id)) this.deselect(id);\n else this.select(id);\n }\n\n clear(): void {\n if (!this.enabled) return;\n this.state.set(EMPTY_SELECTION);\n }\n\n isSelected(id: TableRowId): boolean {\n if (!this.enabled || !this.isUnambiguous(id)) return false;\n const state = this.state();\n return state.mode === 'include' ? state.ids.has(id) : !state.ids.has(id);\n }\n\n selectAllOnPage(): void {\n if (!this.enabled || !this.multiple) return;\n const page = this.pageRows();\n if (page.length === 0) return;\n const state = this.state();\n if (state.mode === 'include') {\n let ids = state.ids;\n for (const row of page) if (row.identityValid !== false) ids = withId(ids, row.id);\n if (ids !== state.ids) this.state.set({ mode: 'include', ids });\n return;\n }\n let ids = state.ids;\n for (const row of page) if (row.identityValid !== false) ids = withoutId(ids, row.id);\n if (ids !== state.ids) this.state.set({ mode: 'exclude', ids });\n }\n\n selectAll(scope?: TableSelectionScope): void {\n if (!this.enabled || !this.multiple) return;\n const resolved = scope ?? this.defaultScope;\n if (resolved === 'page') {\n this.selectAllOnPage();\n return;\n }\n // Nothing matches, so there is nothing to select — entering exclude mode\n // here would select rows that appear later, which is not what was asked.\n if (this.rows().length === 0) return;\n this.state.set({ mode: 'exclude', ids: NO_IDS });\n }\n\n /** Serializable selection, in the shape `TableState` expects. */\n toState(): {\n readonly selected: readonly TableRowId[];\n readonly excluded: readonly TableRowId[];\n } {\n const state = this.state();\n if (state.mode === 'exclude') {\n return { selected: [], excluded: [...state.ids] };\n }\n return { selected: [...state.ids], excluded: [] };\n }\n\n /** Restores a saved view. Pruned immediately so a stale id never leaks. */\n restore(selection: {\n readonly mode: TableSelectionMode;\n readonly ids: readonly TableRowId[];\n }): void {\n if (!this.enabled) return;\n this.state.set(\n pruneSelection({ mode: selection.mode, ids: new Set(selection.ids) }, this.rows()),\n );\n }\n\n private selectedIds(): readonly TableRowId[] {\n const state = this.state();\n const ids: TableRowId[] = [];\n for (const row of this.rows()) {\n if (row.identityValid === false) continue;\n const listed = state.ids.has(row.id);\n if (state.mode === 'include' ? listed : !listed) ids.push(row.id);\n }\n return ids;\n }\n\n private isUnambiguous(id: TableRowId): boolean {\n let matches = 0;\n for (const row of this.rows()) {\n if (row.identityValid !== false && row.id === id) matches++;\n if (matches > 1) return false;\n }\n return matches === 1;\n }\n}\n\nfunction isValidRowId(id: unknown): id is TableRowId {\n return (typeof id === 'string' && id.trim().length > 0) ||\n (typeof id === 'number' && Number.isFinite(id));\n}\n\nfunction buildRows<T>(data: readonly T[], getId: (row: T) => TableRowId): readonly TableRow<T>[] {\n const ids = new Map<TableRowId, number[]>();\n const invalid = new Set<number>();\n const rows = new Array<TableRow<T>>(data.length);\n for (let index = 0; index < data.length; index++) {\n const item = data[index];\n const id: unknown = getId(item);\n if (!isValidRowId(id)) {\n invalid.add(index);\n if (isDevMode()) {\n throw new Error(\n `[rivinia/table] Invalid row ID ${String(id)} at row index ${index}; ` +\n 'IDs must be non-empty strings or finite numbers.',\n );\n }\n } else {\n const positions = ids.get(id) ?? [];\n positions.push(index);\n ids.set(id, positions);\n }\n // Preserve the caller's value for diagnosis; identityValid gates all\n // identity-dependent behavior. Never synthesize an index-based ID.\n rows[index] = { id: id as TableRowId, data: item, index };\n }\n for (const [id, positions] of ids) {\n if (positions.length < 2) continue;\n for (const index of positions) invalid.add(index);\n if (isDevMode()) {\n throw new Error(\n `[rivinia/table] Duplicate row ID ${JSON.stringify(id)} at row indices ${positions.join(', ')}; ` +\n 'IDs must be unique and stable within the active data set.',\n );\n }\n }\n return rows.map((row, index) => invalid.has(index) ? { ...row, identityValid: false } : row);\n}\n\n/**\n * Re-numbers `index` after filtering and sorting.\n *\n * `TableRow.index` is documented as the position in `rows`, and the UI uses it\n * for `aria-rowindex`; leaving the source position there would make the grid\n * announce a row number that does not exist on screen.\n */\nfunction reindex<T>(rows: readonly TableRow<T>[]): readonly TableRow<T>[] {\n for (let index = 0; index < rows.length; index++) {\n if (rows[index].index !== index) {\n return rows.map((row, position) => ({ ...row, index: position }));\n }\n }\n return rows;\n}\n\nfunction normalizePage(value: number): number {\n return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;\n}\n\nfunction normalizePageSize(value: number): number {\n return Number.isFinite(value) && value > 0 ? Math.floor(value) : DEFAULT_PAGE_SIZE;\n}\n\n/**\n * The table, as a chain of `computed`s — one per pipeline stage.\n *\n * `data -> rows (filter, search, sort) -> pageRows -> spans` means Angular's\n * own caching gives incremental recomputation for free: a page change does not\n * re-sort, a sort does not re-filter, and selection/scroll never rebuild the\n * span matrix because nothing in that chain reads them.\n *\n * The controller owns no fetching and no DOM. It is constructed outside the\n * injector graph (`createTable` needs no provider), which is what lets a\n * consumer build one inside a component field initializer or inside a test.\n */\nexport class RiviniaTable<T> {\n /** The declared columns, hidden ones included, in display order. */\n readonly columns: Signal<readonly TableColumn<T>[]>;\n /** The columns that render: `columns` without `hidden: true`. */\n readonly visibleColumns: Signal<readonly TableColumn<T>[]>;\n /** Filtered and sorted rows, numbered from 0. */\n readonly rows: Signal<readonly TableRow<T>[]>;\n /** The current page of `rows`. */\n readonly pageRows: Signal<readonly TableRow<T>[]>;\n /** The resolved span grid over `pageRows` x `visibleColumns`. */\n readonly spans: Signal<SpanModel>;\n /** Client row count, or `rowCount()` when the data comes from outside. */\n readonly total: Signal<number>;\n /** `ceil(total / pageSize)`; `0` for an empty table. */\n readonly pageCount: Signal<number>;\n\n /** Ordered multi-sort; index 0 is the primary level. */\n readonly sorting: WritableSignal<readonly SortState[]>;\n /** Active column filters, ANDed together. */\n readonly filters: WritableSignal<readonly FilterState[]>;\n /** Global search across every data column. */\n readonly globalSearch: WritableSignal<string>;\n /**\n * 0-based page. A linked signal: it returns to 0 when the filter, sort or\n * search changes (the result set changed under the user), while an explicit\n * `set()` still wins until the next such change.\n */\n readonly page: WritableSignal<number>;\n readonly pageSize: WritableSignal<number>;\n readonly selection: RiviniaTableSelection;\n\n /** What a server-mode fetch would need; `data`-driven tables ignore it. */\n readonly request: Signal<TableRequest>;\n /** The serializable slice: a saved view, a URL, a test assertion. */\n readonly state: Signal<TableState>;\n\n private readonly options: () => TableOptions<T>;\n private readonly initialState: Partial<TableState>;\n /** What `resetState()` returns to: the restored size, else the option. */\n private readonly defaultPageSize: number;\n private readonly warn: (message: string) => void;\n private readonly warned = new Set<string>();\n private readonly generatedIds = new WeakMap<object, string>();\n private readonly selectionImpl: {\n toState(): {\n readonly selected: readonly TableRowId[];\n readonly excluded: readonly TableRowId[];\n };\n restore(selection: {\n readonly mode: TableSelectionMode;\n readonly ids: readonly TableRowId[];\n }): void;\n };\n private generatedIdSeq = 0;\n\n constructor(options: () => TableOptions<T>) {\n this.options = options;\n this.initialState = options().initialState ?? {};\n\n const warnOption = options().warn;\n this.warn = (message: string): void => {\n // One message per defect per table: a span diagnostic repeats on every\n // page rebuild, and a console flooded 20 times is a console ignored.\n if (this.warned.has(message)) return;\n this.warned.add(message);\n if (warnOption !== undefined) warnOption(message);\n else if (isDevMode()) console.warn(`[rivinia/table] ${message}`);\n };\n\n if (options().server !== undefined) {\n this.warn(\n 'server mode is not implemented yet; nothing will be fetched. ' +\n 'Pass `data` and let the app own the request for now',\n );\n }\n\n assertSpanColumns(options().columns(), { warn: this.warn });\n\n const initial = this.initialState;\n const selectionOptions = options().selection;\n\n this.columns = computed(() => this.options().columns());\n this.visibleColumns = computed(() => this.columns().filter((column) => column.hidden !== true));\n\n this.sorting = signal<readonly SortState[]>(initial.sort ?? []);\n this.filters = signal<readonly FilterState[]>(initial.filters ?? []);\n this.globalSearch = signal(initial.search ?? '');\n this.defaultPageSize = normalizePageSize(\n initial.pageSize ?? options().pageSize ?? DEFAULT_PAGE_SIZE,\n );\n this.pageSize = signal(this.defaultPageSize);\n this.page = linkedSignal<string, number>({\n source: () => JSON.stringify([this.filters(), this.sorting(), this.globalSearch()]),\n computation: (_signature, previous) =>\n previous === undefined ? normalizePage(initial.page ?? 0) : 0,\n });\n\n this.rows = computed<readonly TableRow<T>[]>(() => {\n const current = this.options();\n const data = current.data?.() ?? [];\n const getId = current.getRowId ?? this.defaultRowId;\n const columns = this.visibleColumns();\n const all = buildRows(data, getId);\n const filtered = current.manualFiltering\n ? all\n : applySearch(applyFilters(all, columns, this.filters()), columns, this.globalSearch());\n const sorted = current.manualSorting\n ? filtered\n : applySort(filtered, columns, this.sorting(), current.locale?.() ?? 'en');\n return reindex(sorted);\n });\n\n this.total = computed(() => this.options().rowCount?.() ?? this.rows().length);\n this.pageRows = computed<readonly TableRow<T>[]>(() => {\n if (this.options().manualPagination) return this.rows();\n return paginate(this.rows(), this.page(), this.pageSize()).pageRows;\n });\n this.pageCount = computed(() => {\n const total = this.total();\n const size = this.pageSize();\n return size > 0 ? Math.ceil(total / size) : total > 0 ? 1 : 0;\n });\n\n const selection = new RiviniaSelection<T>({\n rows: this.rows,\n pageRows: this.pageRows,\n kind: selectionOptions?.mode ?? 'none',\n defaultScope: selectionOptions?.selectAllScope ?? 'filtered',\n initial: {\n // `excluded` wins when both are present: it is the only field that can\n // mean \"everything except\", and it is the narrower statement.\n mode: (initial.excluded?.length ?? 0) > 0 ? 'exclude' : 'include',\n ids:\n (initial.excluded?.length ?? 0) > 0 ? (initial.excluded ?? []) : (initial.selected ?? []),\n },\n });\n this.selection = selection;\n this.selectionImpl = selection;\n\n this.spans = computed(() =>\n buildSpanModel(this.pageRows(), this.visibleColumns(), {\n warn: this.warn,\n // A runtime column swap must never throw during render; `span` wins.\n strict: false,\n }),\n );\n\n this.request = computed<TableRequest>(() => ({\n page: this.page(),\n pageSize: this.pageSize(),\n sort: this.sorting(),\n filters: this.filters(),\n search: this.globalSearch(),\n }));\n\n this.state = computed<TableState>(() => {\n const columns = this.columns();\n const selected = selection.toState();\n return {\n sort: this.sorting(),\n filters: this.filters(),\n search: this.globalSearch(),\n page: this.page(),\n pageSize: this.pageSize(),\n order: columns.map((column) => column.id),\n hidden: columns.filter((column) => column.hidden === true).map((column) => column.id),\n pinnedStart: columns\n .filter((column) => column.pinned === 'start')\n .map((column) => column.id),\n pinnedEnd: columns.filter((column) => column.pinned === 'end').map((column) => column.id),\n selected: selected.selected,\n excluded: selected.excluded,\n };\n });\n }\n\n /**\n * Applies a saved view. Unknown fields are ignored on purpose: column order,\n * visibility and pinning are owned by the column list in 0.1.0, so a state\n * that carries them round-trips unchanged without pretending the controller\n * can reorder the consumer's columns.\n */\n setState(patch: Partial<TableState>): void {\n if (patch.sort !== undefined) this.sorting.set(patch.sort);\n if (patch.filters !== undefined) this.filters.set(patch.filters);\n if (patch.search !== undefined) this.globalSearch.set(patch.search);\n if (patch.pageSize !== undefined) this.pageSize.set(normalizePageSize(patch.pageSize));\n // `page.set` flushes the linked computation first, so the explicit page\n // survives the reset that the lines above just triggered.\n if (patch.page !== undefined) this.page.set(normalizePage(patch.page));\n if (patch.selected !== undefined || patch.excluded !== undefined) {\n const excluded = patch.excluded ?? [];\n this.selectionImpl.restore(\n excluded.length > 0\n ? { mode: 'exclude', ids: excluded }\n : { mode: 'include', ids: patch.selected ?? [] },\n );\n }\n }\n\n /** Returns to `initialState`, including the initial page and selection. */\n resetState(): void {\n const initial = this.initialState;\n this.sorting.set(initial.sort ?? []);\n this.filters.set(initial.filters ?? []);\n this.globalSearch.set(initial.search ?? '');\n this.pageSize.set(this.defaultPageSize);\n this.page.set(normalizePage(initial.page ?? 0));\n const excluded = initial.excluded ?? [];\n this.selectionImpl.restore(\n excluded.length > 0\n ? { mode: 'exclude', ids: excluded }\n : { mode: 'include', ids: initial.selected ?? [] },\n );\n }\n\n /**\n * `getRowId` -> `row.id` -> a per-object generated id. Custom IDs must be\n * unique, stable, non-empty strings or finite numbers; ambiguous rows are\n * rejected in development and excluded from identity-dependent operations\n * in production. Primitive rows require a unique `getRowId`.\n *\n * A primitive row has no identity of its own, so equal primitives collapse\n * into one id; that is documented, and `getRowId` is the answer.\n */\n private readonly defaultRowId = (row: T): TableRowId => {\n if (typeof row === 'object' && row !== null) {\n const candidate = (row as { readonly id?: unknown }).id;\n if (typeof candidate === 'string' || typeof candidate === 'number') return candidate;\n let generated = this.generatedIds.get(row);\n if (generated === undefined) {\n generated = `rv-row-${++this.generatedIdSeq}`;\n this.generatedIds.set(row, generated);\n }\n return generated;\n }\n return String(row);\n };\n}\n\n/**\n * Creates a table from a reactive options function.\n *\n * No DI, no injection context, no HTTP: the caller passes `data` as a signal\n * read and gets derived signals back, so the table is usable in a component\n * field initializer, in a test, or in a headless script.\n */\nexport function createTable<T>(options: () => TableOptions<T>): RiviniaTable<T> {\n return new RiviniaTable<T>(options);\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rivinia/table",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Headless Angular table: typed columns, sorting, filtering, pagination, selection and real row/column spans.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"angular",
|
|
@@ -16,6 +16,18 @@
|
|
|
16
16
|
"@angular/core": "^22.0.0"
|
|
17
17
|
},
|
|
18
18
|
"sideEffects": false,
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/abdelfattahqandil21-oss/Revinia.git",
|
|
22
|
+
"directory": "projects/table"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/abdelfattahqandil21-oss/Revinia/tree/table/v1.0.0/projects/table#readme",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/abdelfattahqandil21-oss/Revinia/issues"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": "^22.22.3 || ^24.15.0 || >=26.0.0"
|
|
30
|
+
},
|
|
19
31
|
"module": "fesm2022/rivinia-table.mjs",
|
|
20
32
|
"typings": "types/rivinia-table.d.ts",
|
|
21
33
|
"exports": {
|
|
@@ -64,13 +64,6 @@ interface TableUiLabels {
|
|
|
64
64
|
}
|
|
65
65
|
export declare const TABLE_UI_LABELS_EN: TableUiLabels;
|
|
66
66
|
export declare const TABLE_UI_LABELS_AR: TableUiLabels;
|
|
67
|
-
/**
|
|
68
|
-
* The label set for a locale, defaulting to English.
|
|
69
|
-
*
|
|
70
|
-
* It is a plain function (not a DI token) because it is pure and the UI entry
|
|
71
|
-
* must stay usable without a provider — the same rule the core follows.
|
|
72
|
-
*/
|
|
73
|
-
declare function tableUiLabels(locale: string | undefined): TableUiLabels;
|
|
74
67
|
/**
|
|
75
68
|
* What a projected `[rvTableCell]` template receives.
|
|
76
69
|
*
|
|
@@ -125,8 +118,9 @@ interface RenderCell {
|
|
|
125
118
|
readonly gridColumn: string;
|
|
126
119
|
}
|
|
127
120
|
interface RenderRow {
|
|
128
|
-
/** The effective row id (`[rowKey]` applied): what selection
|
|
121
|
+
/** The effective row id (`[rowKey]` applied): what selection refers to. */
|
|
129
122
|
readonly id: TableRowId;
|
|
123
|
+
identityValid: boolean;
|
|
130
124
|
readonly row: TableUiRow;
|
|
131
125
|
/** Position inside the rendered page. */
|
|
132
126
|
readonly rowIndex: number;
|
package/types/rivinia-table.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ interface FilterState {
|
|
|
23
23
|
interface TableRow<T> {
|
|
24
24
|
/** Stable across sorting and paging: what selection and actions refer to. */
|
|
25
25
|
readonly id: TableRowId;
|
|
26
|
+
/** False when this row's configured identity is invalid or ambiguous. */
|
|
27
|
+
readonly identityValid?: boolean;
|
|
26
28
|
readonly data: T;
|
|
27
29
|
/** Position in `rows`, i.e. after filtering and sorting. */
|
|
28
30
|
readonly index: number;
|
|
@@ -195,18 +197,6 @@ export declare function buildSpanModel<T>(rows: readonly TableRow<T>[], columns:
|
|
|
195
197
|
* therefore no value; `undefined` is the honest answer, not an error.
|
|
196
198
|
*/
|
|
197
199
|
export declare function readCellValue<T, V = unknown>(column: TableColumn<T, V>, row: TableRow<T>): V | undefined;
|
|
198
|
-
/**
|
|
199
|
-
* A range filter value. Bounds are inclusive and either side may be open, so
|
|
200
|
-
* one shape covers "from", "to" and "between" for both numbers and dates.
|
|
201
|
-
*
|
|
202
|
-
* Numbers and dates share it on purpose: a date bound is compared as a
|
|
203
|
-
* timestamp and a numeric bound as a number, which is exactly what a date
|
|
204
|
-
* input and a number input produce in the UI.
|
|
205
|
-
*/
|
|
206
|
-
interface TableFilterRange {
|
|
207
|
-
readonly min?: number | Date | null;
|
|
208
|
-
readonly max?: number | Date | null;
|
|
209
|
-
}
|
|
210
200
|
/** The result of `paginate`, with the display range the footer needs. */
|
|
211
201
|
interface TablePage<T> {
|
|
212
202
|
readonly pageRows: readonly TableRow<T>[];
|
|
@@ -351,9 +341,12 @@ interface TableOptions<T> {
|
|
|
351
341
|
/** Server mode: declared only, not wired in 0.1.0 (the table warns). */
|
|
352
342
|
readonly server?: TableServerOptions<T>;
|
|
353
343
|
/**
|
|
354
|
-
* Row identity.
|
|
355
|
-
*
|
|
356
|
-
*
|
|
344
|
+
* Row identity. IDs must be unique, stable, non-empty strings or finite
|
|
345
|
+
* numbers within the full data set. In development invalid or duplicate
|
|
346
|
+
* values throw with their source row positions. In production ambiguous
|
|
347
|
+
* rows remain visible but identity-dependent actions (such as selection) are
|
|
348
|
+
* disabled for them. Defaults to `row.id`, then a per-object generated id
|
|
349
|
+
* kept in a `WeakMap`; never use the array index.
|
|
357
350
|
*/
|
|
358
351
|
readonly getRowId?: (row: T) => TableRowId;
|
|
359
352
|
/** Locale for collation and formatting; read lazily so it can be a signal. */
|
|
@@ -476,7 +469,10 @@ export declare class RiviniaTable<T> {
|
|
|
476
469
|
/** Returns to `initialState`, including the initial page and selection. */
|
|
477
470
|
resetState(): void;
|
|
478
471
|
/**
|
|
479
|
-
* `getRowId` -> `row.id` -> a per-object generated id.
|
|
472
|
+
* `getRowId` -> `row.id` -> a per-object generated id. Custom IDs must be
|
|
473
|
+
* unique, stable, non-empty strings or finite numbers; ambiguous rows are
|
|
474
|
+
* rejected in development and excluded from identity-dependent operations
|
|
475
|
+
* in production. Primitive rows require a unique `getRowId`.
|
|
480
476
|
*
|
|
481
477
|
* A primitive row has no identity of its own, so equal primitives collapse
|
|
482
478
|
* into one id; that is documented, and `getRowId` is the answer.
|