@rivinia/table 0.0.0-stage → 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.
@@ -0,0 +1,813 @@
1
+ import { computed, isDevMode, linkedSignal, signal } from "@angular/core";
2
+ function readCellValue(column, row) {
3
+ const accessor = column.accessor;
4
+ if (accessor === void 0) return void 0;
5
+ if (typeof accessor === "string") return row.data[accessor];
6
+ return accessor(row.data);
7
+ }
8
+ const COLLATORS = /* @__PURE__ */ new Map();
9
+ function collatorFor(locale) {
10
+ const key = locale === "" ? "en" : locale;
11
+ const cached = COLLATORS.get(key);
12
+ if (cached !== void 0) return cached;
13
+ let collator;
14
+ try {
15
+ collator = new Intl.Collator(key, {
16
+ numeric: true,
17
+ sensitivity: "base"
18
+ });
19
+ } catch {
20
+ collator = new Intl.Collator(void 0, {
21
+ numeric: true,
22
+ sensitivity: "base"
23
+ });
24
+ }
25
+ COLLATORS.set(key, collator);
26
+ return collator;
27
+ }
28
+ function normalizeSearchText(value) {
29
+ return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[\u064B-\u065F\u0670]/g, "").replace(/\u0640/g, "").replace(/[أإآٱ]/g, "ا").replace(/ى/g, "ي").replace(/ؤ/g, "و").replace(/ئ/g, "ي").replace(/ة/g, "ه").toLowerCase();
30
+ }
31
+ function isNullish$1(value) {
32
+ return value === null || value === void 0;
33
+ }
34
+ function isActiveFilter(value) {
35
+ if (value === null || value === void 0 || value === "") return false;
36
+ return !Array.isArray(value) || value.length > 0;
37
+ }
38
+ function toNumber(value) {
39
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
40
+ if (typeof value === "bigint") return Number(value);
41
+ if (typeof value === "string" && value.trim() !== "") {
42
+ const numeric = Number(value);
43
+ return Number.isFinite(numeric) ? numeric : null;
44
+ }
45
+ return null;
46
+ }
47
+ function toTime(value) {
48
+ if (value instanceof Date) {
49
+ const time = value.getTime();
50
+ return Number.isNaN(time) ? null : time;
51
+ }
52
+ if (typeof value === "number") return Number.isFinite(value) ? value : null;
53
+ if (typeof value === "bigint") return Number(value);
54
+ if (typeof value === "string") {
55
+ const trimmed = value.trim();
56
+ if (trimmed === "") return null;
57
+ const numeric = Number(trimmed);
58
+ if (Number.isFinite(numeric)) return numeric;
59
+ const parsed = Date.parse(trimmed);
60
+ return Number.isNaN(parsed) ? null : parsed;
61
+ }
62
+ return null;
63
+ }
64
+ function isRange(value) {
65
+ return typeof value === "object" && value !== null && ("min" in value || "max" in value);
66
+ }
67
+ function matchesRange(cell, range) {
68
+ const min = range.min === null || range.min === void 0 ? null : toTime(range.min);
69
+ const max = range.max === null || range.max === void 0 ? null : toTime(range.max);
70
+ if (min === null && max === null) return true;
71
+ const time = toTime(cell);
72
+ if (time === null) return false;
73
+ return (min === null || time >= min) && (max === null || time <= max);
74
+ }
75
+ function matchesFilter(cell, filter) {
76
+ if (Array.isArray(filter)) return filter.some((item) => Object.is(item, cell));
77
+ if (typeof filter === "boolean") return Boolean(cell) === filter;
78
+ if (filter instanceof Date) {
79
+ const time = toTime(cell);
80
+ return time !== null && time === filter.getTime();
81
+ }
82
+ if (typeof filter === "number") {
83
+ const numeric = toNumber(cell);
84
+ return numeric !== null && numeric === filter;
85
+ }
86
+ if (typeof filter === "bigint") {
87
+ const numeric = toNumber(cell);
88
+ return numeric !== null && numeric === Number(filter);
89
+ }
90
+ if (isRange(filter)) return matchesRange(cell, filter);
91
+ if (typeof filter === "string") return normalizeSearchText(String(cell ?? "")).includes(normalizeSearchText(filter));
92
+ return Object.is(cell, filter);
93
+ }
94
+ function applyFilters(rows, columns, filters) {
95
+ if (filters.length === 0) return rows;
96
+ const byId = new Map(columns.map((column) => [column.id, column]));
97
+ const plan = [];
98
+ for (const filter of filters) {
99
+ if (!isActiveFilter(filter.value)) continue;
100
+ const column = byId.get(filter.column);
101
+ if (column === void 0 || column.accessor === void 0) continue;
102
+ plan.push({
103
+ column,
104
+ value: filter.value
105
+ });
106
+ }
107
+ if (plan.length === 0) return rows;
108
+ return rows.filter((row) => {
109
+ for (const entry of plan) if (!matchesFilter(readCellValue(entry.column, row), entry.value)) return false;
110
+ return true;
111
+ });
112
+ }
113
+ function applySearch(rows, columns, search) {
114
+ const needle = normalizeSearchText(search.trim());
115
+ if (needle === "") return rows;
116
+ const searchable = columns.filter((column) => column.accessor !== void 0);
117
+ if (searchable.length === 0) return [];
118
+ return rows.filter((row) => {
119
+ for (const column of searchable) {
120
+ const value = readCellValue(column, row);
121
+ if (value === null || value === void 0) continue;
122
+ if (normalizeSearchText(String(value)).includes(needle)) return true;
123
+ }
124
+ return false;
125
+ });
126
+ }
127
+ function comparePresent(left, right, comparator, collator) {
128
+ if (comparator !== void 0) return comparator(left, right);
129
+ if (left instanceof Date && right instanceof Date) {
130
+ const a = left.getTime();
131
+ const b = right.getTime();
132
+ if (!Number.isNaN(a) && !Number.isNaN(b)) return a < b ? -1 : a > b ? 1 : 0;
133
+ }
134
+ if (typeof left === "number" && typeof right === "number") return left < right ? -1 : left > right ? 1 : 0;
135
+ if (typeof left === "bigint" && typeof right === "bigint") return left < right ? -1 : left > right ? 1 : 0;
136
+ if (typeof left === "boolean" && typeof right === "boolean") return left === right ? 0 : left ? 1 : -1;
137
+ return collator.compare(String(left), String(right));
138
+ }
139
+ function applySort(rows, columns, sorting, locale = "en") {
140
+ if (sorting.length === 0) return rows;
141
+ const byId = new Map(columns.map((column) => [column.id, column]));
142
+ const plan = [];
143
+ for (const state of sorting) {
144
+ const column = byId.get(state.column);
145
+ if (column === void 0 || column.accessor === void 0) continue;
146
+ plan.push({
147
+ state,
148
+ column
149
+ });
150
+ }
151
+ if (plan.length === 0) return rows;
152
+ const collator = collatorFor(locale);
153
+ const keyed = rows.map((row) => ({
154
+ row,
155
+ keys: plan.map((entry) => readCellValue(entry.column, row))
156
+ }));
157
+ keyed.sort((a, b) => {
158
+ for (let index = 0; index < plan.length; index++) {
159
+ const left = a.keys[index];
160
+ const right = b.keys[index];
161
+ const leftMissing = isNullish$1(left);
162
+ const rightMissing = isNullish$1(right);
163
+ if (leftMissing || rightMissing) {
164
+ if (leftMissing && rightMissing) continue;
165
+ return leftMissing ? 1 : -1;
166
+ }
167
+ const entry = plan[index];
168
+ const result = comparePresent(left, right, entry.column.comparator, collator);
169
+ if (result !== 0) return entry.state.direction === "desc" ? -result : result;
170
+ }
171
+ return a.row.index - b.row.index;
172
+ });
173
+ return keyed.map((entry) => entry.row);
174
+ }
175
+ function paginate(rows, page, pageSize) {
176
+ const total = rows.length;
177
+ const size = Number.isFinite(pageSize) ? Math.floor(pageSize) : 0;
178
+ const currentPage = Number.isFinite(page) ? Math.max(0, Math.floor(page)) : 0;
179
+ if (size <= 0) return {
180
+ pageRows: rows,
181
+ total,
182
+ pageCount: total > 0 ? 1 : 0,
183
+ start: total > 0 ? 1 : 0,
184
+ end: total
185
+ };
186
+ const pageCount = Math.ceil(total / size);
187
+ const offset = currentPage * size;
188
+ const pageRows = offset >= total ? [] : rows.slice(offset, offset + size);
189
+ return {
190
+ pageRows,
191
+ total,
192
+ pageCount,
193
+ start: pageRows.length === 0 ? 0 : offset + 1,
194
+ end: pageRows.length === 0 ? 0 : offset + pageRows.length
195
+ };
196
+ }
197
+ const NO_CELLS = Object.freeze([]);
198
+ function defaultWarn(message) {
199
+ if (isDevMode()) console.warn(`[rivinia/table] ${message}`);
200
+ }
201
+ function isNullish(value) {
202
+ return value === null || value === void 0;
203
+ }
204
+ function clamp(value, min, max) {
205
+ if (!Number.isFinite(value)) return min;
206
+ return value < min ? min : value > max ? max : value;
207
+ }
208
+ function checkColumns(columns, warn, strict) {
209
+ for (const column of columns) {
210
+ const conflicting = column.merge !== void 0 ? "merge" : column.colSpan !== void 0 ? "colSpan" : null;
211
+ if (column.span !== void 0 && conflicting !== null) {
212
+ const message = `column "${column.id}" declares both span and ${conflicting}; use span alone for full control, or merge/colSpan for the simple path`;
213
+ if (strict) throw new Error(`[rivinia/table] ${message}`);
214
+ warn(`${message}; span wins`);
215
+ }
216
+ if (column.merge !== void 0 && column.accessor === void 0) warn(`column "${column.id}" declares merge but has no accessor; merge is ignored`);
217
+ }
218
+ }
219
+ function assertSpanColumns(columns, options) {
220
+ checkColumns(columns, options?.warn ?? defaultWarn, true);
221
+ }
222
+ function buildSpanModel(rows, columns, options) {
223
+ const warn = options?.warn ?? defaultWarn;
224
+ checkColumns(columns, warn, options?.strict ?? true);
225
+ const rowCount = rows.length;
226
+ const columnCount = columns.length;
227
+ if (!columns.some((column) => column.span !== void 0 || column.colSpan !== void 0 || column.merge !== void 0 && column.accessor !== void 0) || rowCount === 0 || columnCount === 0) return plainSpanModel(rowCount, columnCount);
228
+ const claimed = new Uint8Array(rowCount * columnCount);
229
+ const owners = new Int32Array(rowCount * columnCount).fill(-1);
230
+ const cells = [];
231
+ const resolve = (definition, row, column) => {
232
+ const current = rows[row];
233
+ const value = readCellValue(definition, current);
234
+ const spanResolver = definition.span;
235
+ if (spanResolver !== void 0) {
236
+ const explicit = spanResolver(contextOf(current, definition, value, row, column));
237
+ if (explicit !== null && explicit !== void 0) return {
238
+ rowSpan: explicit.rowSpan ?? 1,
239
+ colSpan: explicit.colSpan ?? 1
240
+ };
241
+ }
242
+ let rowSpan = 1;
243
+ let colSpan = 1;
244
+ const merge = definition.merge;
245
+ if (merge !== void 0 && definition.accessor !== void 0) {
246
+ if (merge === "equal") {
247
+ if (!isNullish(value)) {
248
+ let end = row;
249
+ while (end + 1 < rowCount) {
250
+ const candidate = readCellValue(definition, rows[end + 1]);
251
+ if (isNullish(candidate) || !Object.is(candidate, value)) break;
252
+ end++;
253
+ }
254
+ rowSpan = end - row + 1;
255
+ }
256
+ } else {
257
+ const anchor = current.data;
258
+ let end = row;
259
+ while (end + 1 < rowCount && merge(rows[end + 1].data, anchor)) end++;
260
+ rowSpan = end - row + 1;
261
+ }
262
+ }
263
+ const colSpanDefinition = definition.colSpan;
264
+ if (colSpanDefinition !== void 0) colSpan = typeof colSpanDefinition === "function" ? colSpanDefinition(contextOf(current, definition, value, row, column)) : colSpanDefinition;
265
+ return {
266
+ rowSpan,
267
+ colSpan
268
+ };
269
+ };
270
+ for (let row = 0; row < rowCount; row++) for (let column = 0; column < columnCount; column++) {
271
+ if (claimed[row * columnCount + column] !== 0) continue;
272
+ const definition = columns[column];
273
+ const requested = resolve(definition, row, column);
274
+ let rowSpan = requested.rowSpan ?? 1;
275
+ let colSpan = requested.colSpan ?? 1;
276
+ if (colSpan === Number.POSITIVE_INFINITY) colSpan = columnCount - column;
277
+ if (!Number.isInteger(rowSpan) || rowSpan < 1) {
278
+ warn(`column "${definition.id}" returned rowSpan ${rowSpan}; clamped to 1`);
279
+ rowSpan = 1;
280
+ }
281
+ if (!Number.isInteger(colSpan) || colSpan < 1) {
282
+ warn(`column "${definition.id}" returned colSpan ${colSpan}; clamped to 1`);
283
+ colSpan = 1;
284
+ }
285
+ rowSpan = Math.min(rowSpan, rowCount - row);
286
+ colSpan = Math.min(colSpan, columnCount - column);
287
+ const pinning = definition.pinned ?? null;
288
+ for (let step = 1; step < colSpan; step++) if ((columns[column + step].pinned ?? null) !== pinning) {
289
+ colSpan = step;
290
+ break;
291
+ }
292
+ const wantedRowSpan = rowSpan;
293
+ const wantedColSpan = colSpan;
294
+ while (colSpan > 1 && !isFree(claimed, columnCount, row, column, rowSpan, colSpan)) colSpan--;
295
+ while (rowSpan > 1 && !isFree(claimed, columnCount, row, column, rowSpan, colSpan)) rowSpan--;
296
+ if (rowSpan !== wantedRowSpan || colSpan !== wantedColSpan) warn(`column "${definition.id}" at row ${row} overlaps an existing span; shrunk from ${wantedRowSpan}x${wantedColSpan} to ${rowSpan}x${colSpan}`);
297
+ const anchor = cells.length;
298
+ cells.push({
299
+ row,
300
+ column,
301
+ rowSpan,
302
+ colSpan
303
+ });
304
+ for (let r = row; r < row + rowSpan; r++) for (let c = column; c < column + colSpan; c++) {
305
+ claimed[r * columnCount + c] = 1;
306
+ owners[r * columnCount + c] = anchor;
307
+ }
308
+ }
309
+ if (cells.length === rowCount * columnCount) return plainSpanModel(rowCount, columnCount);
310
+ const ownerAt = (row, column) => {
311
+ if (row < 0 || row >= rowCount || column < 0 || column >= columnCount) return null;
312
+ const index = owners[row * columnCount + column];
313
+ return index < 0 ? null : cells[index];
314
+ };
315
+ return {
316
+ cells,
317
+ at(row, column) {
318
+ const cell = ownerAt(row, column);
319
+ return cell !== null && cell.row === row && cell.column === column ? cell : null;
320
+ },
321
+ owner: ownerAt,
322
+ move(row, column, dRow, dColumn) {
323
+ if (dRow === 0 && dColumn === 0) return {
324
+ row: clamp(row, 0, rowCount - 1),
325
+ column: clamp(column, 0, columnCount - 1)
326
+ };
327
+ let currentRow = clamp(row, 0, rowCount - 1);
328
+ let currentColumn = clamp(column, 0, columnCount - 1);
329
+ for (;;) {
330
+ const nextRow = currentRow + dRow;
331
+ const nextColumn = currentColumn + dColumn;
332
+ if (nextRow < 0 || nextRow >= rowCount || nextColumn < 0 || nextColumn >= columnCount) return {
333
+ row: currentRow,
334
+ column: currentColumn
335
+ };
336
+ const span = ownerAt(nextRow, nextColumn);
337
+ if (span === null) return {
338
+ row: nextRow,
339
+ column: nextColumn
340
+ };
341
+ if (!(currentRow >= span.row && currentRow < span.row + span.rowSpan && currentColumn >= span.column && currentColumn < span.column + span.colSpan)) return {
342
+ row: span.row,
343
+ column: span.column
344
+ };
345
+ const jumpRow = dRow === 0 ? nextRow : dRow > 0 ? span.row + span.rowSpan : span.row - 1;
346
+ const jumpColumn = dColumn === 0 ? nextColumn : dColumn > 0 ? span.column + span.colSpan : span.column - 1;
347
+ if (jumpRow < 0 || jumpRow >= rowCount || jumpColumn < 0 || jumpColumn >= columnCount) return {
348
+ row: currentRow,
349
+ column: currentColumn
350
+ };
351
+ const jumped = ownerAt(jumpRow, jumpColumn);
352
+ if (jumped === null || jumped.row === jumpRow && jumped.column === jumpColumn) return {
353
+ row: jumpRow,
354
+ column: jumpColumn
355
+ };
356
+ currentRow = jumpRow;
357
+ currentColumn = jumpColumn;
358
+ }
359
+ }
360
+ };
361
+ }
362
+ function contextOf(row, column, value, rowIndex, columnIndex) {
363
+ return {
364
+ row,
365
+ column,
366
+ value,
367
+ rowIndex,
368
+ columnIndex
369
+ };
370
+ }
371
+ function isFree(claimed, columnCount, row, column, rowSpan, colSpan) {
372
+ for (let r = row; r < row + rowSpan; r++) for (let c = column; c < column + colSpan; c++) {
373
+ if (r === row && c === column) continue;
374
+ if (claimed[r * columnCount + c] !== 0) return false;
375
+ }
376
+ return true;
377
+ }
378
+ function plainSpanModel(rowCount, columnCount) {
379
+ return {
380
+ cells: NO_CELLS,
381
+ at: () => null,
382
+ owner: () => null,
383
+ move(row, column, dRow, dColumn) {
384
+ if (rowCount === 0 || columnCount === 0) return {
385
+ row: 0,
386
+ column: 0
387
+ };
388
+ return {
389
+ row: clamp(row + dRow, 0, rowCount - 1),
390
+ column: clamp(column + dColumn, 0, columnCount - 1)
391
+ };
392
+ }
393
+ };
394
+ }
395
+ function tableColumn() {
396
+ return Object.freeze({
397
+ accessor(key, definition) {
398
+ return Object.freeze({
399
+ header: key,
400
+ ...definition,
401
+ id: key,
402
+ accessor: (row) => row[key]
403
+ });
404
+ },
405
+ display(id, definition) {
406
+ return Object.freeze({
407
+ header: id,
408
+ ...definition,
409
+ id
410
+ });
411
+ }
412
+ });
413
+ }
414
+ const DEFAULT_PAGE_SIZE = 25;
415
+ const NO_IDS = /* @__PURE__ */ new Set();
416
+ const EMPTY_SELECTION = {
417
+ mode: "include",
418
+ ids: NO_IDS
419
+ };
420
+ function pruneSelection(state, rows) {
421
+ if (state.ids.size === 0) return state;
422
+ const present = /* @__PURE__ */ new Set();
423
+ for (const row of rows) if (row.identityValid !== false) present.add(row.id);
424
+ let dropped = false;
425
+ const next = /* @__PURE__ */ new Set();
426
+ for (const id of state.ids) if (present.has(id)) next.add(id);
427
+ else dropped = true;
428
+ return dropped ? {
429
+ mode: state.mode,
430
+ ids: next
431
+ } : state;
432
+ }
433
+ function withId(ids, id) {
434
+ if (ids.has(id)) return ids;
435
+ const next = new Set(ids);
436
+ next.add(id);
437
+ return next;
438
+ }
439
+ function withoutId(ids, id) {
440
+ if (!ids.has(id)) return ids;
441
+ const next = new Set(ids);
442
+ next.delete(id);
443
+ return next;
444
+ }
445
+ var RiviniaSelection = class {
446
+ enabled;
447
+ multiple;
448
+ mode;
449
+ ids;
450
+ count;
451
+ rows;
452
+ pageRows;
453
+ defaultScope;
454
+ initial;
455
+ state;
456
+ constructor(config) {
457
+ this.rows = config.rows;
458
+ this.pageRows = config.pageRows;
459
+ this.defaultScope = config.defaultScope;
460
+ this.initial = {
461
+ mode: config.initial.mode,
462
+ ids: new Set(config.initial.ids)
463
+ };
464
+ this.enabled = config.kind !== "none";
465
+ this.multiple = config.kind === "multi";
466
+ this.state = linkedSignal({
467
+ ...ngDevMode ? { debugName: "state" } : /* istanbul ignore next */ {},
468
+ source: config.rows,
469
+ computation: (rows, previous) => pruneSelection(previous?.value ?? this.initial, rows)
470
+ });
471
+ this.mode = computed(() => this.state().mode, ...ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []);
472
+ this.ids = computed(() => this.selectedIds(), ...ngDevMode ? [{ debugName: "ids" }] : /* istanbul ignore next */ []);
473
+ this.count = computed(() => this.ids().length, ...ngDevMode ? [{ debugName: "count" }] : /* istanbul ignore next */ []);
474
+ }
475
+ select(id) {
476
+ if (!this.enabled || !this.isUnambiguous(id)) return;
477
+ const state = this.state();
478
+ if (!this.multiple) {
479
+ if (state.mode === "include" && state.ids.size === 1 && state.ids.has(id)) return;
480
+ this.state.set({
481
+ mode: "include",
482
+ ids: /* @__PURE__ */ new Set([id])
483
+ });
484
+ return;
485
+ }
486
+ if (state.mode === "include") {
487
+ const ids = withId(state.ids, id);
488
+ if (ids !== state.ids) this.state.set({
489
+ mode: "include",
490
+ ids
491
+ });
492
+ return;
493
+ }
494
+ const ids = withoutId(state.ids, id);
495
+ if (ids !== state.ids) this.state.set({
496
+ mode: "exclude",
497
+ ids
498
+ });
499
+ }
500
+ deselect(id) {
501
+ if (!this.enabled || !this.isUnambiguous(id)) return;
502
+ const state = this.state();
503
+ if (state.mode === "include") {
504
+ const ids = withoutId(state.ids, id);
505
+ if (ids !== state.ids) this.state.set({
506
+ mode: "include",
507
+ ids
508
+ });
509
+ return;
510
+ }
511
+ const ids = withId(state.ids, id);
512
+ if (ids !== state.ids) this.state.set({
513
+ mode: "exclude",
514
+ ids
515
+ });
516
+ }
517
+ toggle(id) {
518
+ if (this.isSelected(id)) this.deselect(id);
519
+ else this.select(id);
520
+ }
521
+ clear() {
522
+ if (!this.enabled) return;
523
+ this.state.set(EMPTY_SELECTION);
524
+ }
525
+ isSelected(id) {
526
+ if (!this.enabled || !this.isUnambiguous(id)) return false;
527
+ const state = this.state();
528
+ return state.mode === "include" ? state.ids.has(id) : !state.ids.has(id);
529
+ }
530
+ selectAllOnPage() {
531
+ if (!this.enabled || !this.multiple) return;
532
+ const page = this.pageRows();
533
+ if (page.length === 0) return;
534
+ const state = this.state();
535
+ if (state.mode === "include") {
536
+ let ids = state.ids;
537
+ for (const row of page) if (row.identityValid !== false) ids = withId(ids, row.id);
538
+ if (ids !== state.ids) this.state.set({
539
+ mode: "include",
540
+ ids
541
+ });
542
+ return;
543
+ }
544
+ let ids = state.ids;
545
+ for (const row of page) if (row.identityValid !== false) ids = withoutId(ids, row.id);
546
+ if (ids !== state.ids) this.state.set({
547
+ mode: "exclude",
548
+ ids
549
+ });
550
+ }
551
+ selectAll(scope) {
552
+ if (!this.enabled || !this.multiple) return;
553
+ if ((scope ?? this.defaultScope) === "page") {
554
+ this.selectAllOnPage();
555
+ return;
556
+ }
557
+ if (this.rows().length === 0) return;
558
+ this.state.set({
559
+ mode: "exclude",
560
+ ids: NO_IDS
561
+ });
562
+ }
563
+ toState() {
564
+ const state = this.state();
565
+ if (state.mode === "exclude") return {
566
+ selected: [],
567
+ excluded: [...state.ids]
568
+ };
569
+ return {
570
+ selected: [...state.ids],
571
+ excluded: []
572
+ };
573
+ }
574
+ restore(selection) {
575
+ if (!this.enabled) return;
576
+ this.state.set(pruneSelection({
577
+ mode: selection.mode,
578
+ ids: new Set(selection.ids)
579
+ }, this.rows()));
580
+ }
581
+ selectedIds() {
582
+ const state = this.state();
583
+ const ids = [];
584
+ for (const row of this.rows()) {
585
+ if (row.identityValid === false) continue;
586
+ const listed = state.ids.has(row.id);
587
+ if (state.mode === "include" ? listed : !listed) ids.push(row.id);
588
+ }
589
+ return ids;
590
+ }
591
+ isUnambiguous(id) {
592
+ let matches = 0;
593
+ for (const row of this.rows()) {
594
+ if (row.identityValid !== false && row.id === id) matches++;
595
+ if (matches > 1) return false;
596
+ }
597
+ return matches === 1;
598
+ }
599
+ };
600
+ function isValidRowId(id) {
601
+ return typeof id === "string" && id.trim().length > 0 || typeof id === "number" && Number.isFinite(id);
602
+ }
603
+ function buildRows(data, getId) {
604
+ const ids = /* @__PURE__ */ new Map();
605
+ const invalid = /* @__PURE__ */ new Set();
606
+ const rows = new Array(data.length);
607
+ for (let index = 0; index < data.length; index++) {
608
+ const item = data[index];
609
+ const id = getId(item);
610
+ if (!isValidRowId(id)) {
611
+ invalid.add(index);
612
+ if (isDevMode()) throw new Error(`[rivinia/table] Invalid row ID ${String(id)} at row index ${index}; IDs must be non-empty strings or finite numbers.`);
613
+ } else {
614
+ const positions = ids.get(id) ?? [];
615
+ positions.push(index);
616
+ ids.set(id, positions);
617
+ }
618
+ rows[index] = {
619
+ id,
620
+ data: item,
621
+ index
622
+ };
623
+ }
624
+ for (const [id, positions] of ids) {
625
+ if (positions.length < 2) continue;
626
+ for (const index of positions) invalid.add(index);
627
+ if (isDevMode()) throw new Error(`[rivinia/table] Duplicate row ID ${JSON.stringify(id)} at row indices ${positions.join(", ")}; IDs must be unique and stable within the active data set.`);
628
+ }
629
+ return rows.map((row, index) => invalid.has(index) ? {
630
+ ...row,
631
+ identityValid: false
632
+ } : row);
633
+ }
634
+ function reindex(rows) {
635
+ for (let index = 0; index < rows.length; index++) if (rows[index].index !== index) return rows.map((row, position) => ({
636
+ ...row,
637
+ index: position
638
+ }));
639
+ return rows;
640
+ }
641
+ function normalizePage(value) {
642
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
643
+ }
644
+ function normalizePageSize(value) {
645
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : DEFAULT_PAGE_SIZE;
646
+ }
647
+ var RiviniaTable = class {
648
+ columns;
649
+ visibleColumns;
650
+ rows;
651
+ pageRows;
652
+ spans;
653
+ total;
654
+ pageCount;
655
+ sorting;
656
+ filters;
657
+ globalSearch;
658
+ page;
659
+ pageSize;
660
+ selection;
661
+ request;
662
+ state;
663
+ options;
664
+ initialState;
665
+ defaultPageSize;
666
+ warn;
667
+ warned = /* @__PURE__ */ new Set();
668
+ generatedIds = /* @__PURE__ */ new WeakMap();
669
+ selectionImpl;
670
+ generatedIdSeq = 0;
671
+ constructor(options) {
672
+ this.options = options;
673
+ this.initialState = options().initialState ?? {};
674
+ const warnOption = options().warn;
675
+ this.warn = (message) => {
676
+ if (this.warned.has(message)) return;
677
+ this.warned.add(message);
678
+ if (warnOption !== void 0) warnOption(message);
679
+ else if (isDevMode()) console.warn(`[rivinia/table] ${message}`);
680
+ };
681
+ if (options().server !== void 0) this.warn("server mode is not implemented yet; nothing will be fetched. Pass `data` and let the app own the request for now");
682
+ assertSpanColumns(options().columns(), { warn: this.warn });
683
+ const initial = this.initialState;
684
+ const selectionOptions = options().selection;
685
+ this.columns = computed(() => this.options().columns(), ...ngDevMode ? [{ debugName: "columns" }] : /* istanbul ignore next */ []);
686
+ this.visibleColumns = computed(() => this.columns().filter((column) => column.hidden !== true), ...ngDevMode ? [{ debugName: "visibleColumns" }] : /* istanbul ignore next */ []);
687
+ this.sorting = signal(initial.sort ?? [], ...ngDevMode ? [{ debugName: "sorting" }] : /* istanbul ignore next */ []);
688
+ this.filters = signal(initial.filters ?? [], ...ngDevMode ? [{ debugName: "filters" }] : /* istanbul ignore next */ []);
689
+ this.globalSearch = signal(initial.search ?? "", ...ngDevMode ? [{ debugName: "globalSearch" }] : /* istanbul ignore next */ []);
690
+ this.defaultPageSize = normalizePageSize(initial.pageSize ?? options().pageSize ?? DEFAULT_PAGE_SIZE);
691
+ this.pageSize = signal(this.defaultPageSize, ...ngDevMode ? [{ debugName: "pageSize" }] : /* istanbul ignore next */ []);
692
+ this.page = linkedSignal({
693
+ ...ngDevMode ? { debugName: "page" } : /* istanbul ignore next */ {},
694
+ source: () => JSON.stringify([
695
+ this.filters(),
696
+ this.sorting(),
697
+ this.globalSearch()
698
+ ]),
699
+ computation: (_signature, previous) => previous === void 0 ? normalizePage(initial.page ?? 0) : 0
700
+ });
701
+ this.rows = computed(() => {
702
+ const current = this.options();
703
+ const data = current.data?.() ?? [];
704
+ const getId = current.getRowId ?? this.defaultRowId;
705
+ const columns = this.visibleColumns();
706
+ const all = buildRows(data, getId);
707
+ const filtered = current.manualFiltering ? all : applySearch(applyFilters(all, columns, this.filters()), columns, this.globalSearch());
708
+ return reindex(current.manualSorting ? filtered : applySort(filtered, columns, this.sorting(), current.locale?.() ?? "en"));
709
+ }, ...ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []);
710
+ this.total = computed(() => this.options().rowCount?.() ?? this.rows().length, ...ngDevMode ? [{ debugName: "total" }] : /* istanbul ignore next */ []);
711
+ this.pageRows = computed(() => {
712
+ if (this.options().manualPagination) return this.rows();
713
+ return paginate(this.rows(), this.page(), this.pageSize()).pageRows;
714
+ }, ...ngDevMode ? [{ debugName: "pageRows" }] : /* istanbul ignore next */ []);
715
+ this.pageCount = computed(() => {
716
+ const total = this.total();
717
+ const size = this.pageSize();
718
+ return size > 0 ? Math.ceil(total / size) : total > 0 ? 1 : 0;
719
+ }, ...ngDevMode ? [{ debugName: "pageCount" }] : /* istanbul ignore next */ []);
720
+ const selection = new RiviniaSelection({
721
+ rows: this.rows,
722
+ pageRows: this.pageRows,
723
+ kind: selectionOptions?.mode ?? "none",
724
+ defaultScope: selectionOptions?.selectAllScope ?? "filtered",
725
+ initial: {
726
+ mode: (initial.excluded?.length ?? 0) > 0 ? "exclude" : "include",
727
+ ids: (initial.excluded?.length ?? 0) > 0 ? initial.excluded ?? [] : initial.selected ?? []
728
+ }
729
+ });
730
+ this.selection = selection;
731
+ this.selectionImpl = selection;
732
+ this.spans = computed(() => buildSpanModel(this.pageRows(), this.visibleColumns(), {
733
+ warn: this.warn,
734
+ strict: false
735
+ }), ...ngDevMode ? [{ debugName: "spans" }] : /* istanbul ignore next */ []);
736
+ this.request = computed(() => ({
737
+ page: this.page(),
738
+ pageSize: this.pageSize(),
739
+ sort: this.sorting(),
740
+ filters: this.filters(),
741
+ search: this.globalSearch()
742
+ }), ...ngDevMode ? [{ debugName: "request" }] : /* istanbul ignore next */ []);
743
+ this.state = computed(() => {
744
+ const columns = this.columns();
745
+ const selected = selection.toState();
746
+ return {
747
+ sort: this.sorting(),
748
+ filters: this.filters(),
749
+ search: this.globalSearch(),
750
+ page: this.page(),
751
+ pageSize: this.pageSize(),
752
+ order: columns.map((column) => column.id),
753
+ hidden: columns.filter((column) => column.hidden === true).map((column) => column.id),
754
+ pinnedStart: columns.filter((column) => column.pinned === "start").map((column) => column.id),
755
+ pinnedEnd: columns.filter((column) => column.pinned === "end").map((column) => column.id),
756
+ selected: selected.selected,
757
+ excluded: selected.excluded
758
+ };
759
+ }, ...ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []);
760
+ }
761
+ setState(patch) {
762
+ if (patch.sort !== void 0) this.sorting.set(patch.sort);
763
+ if (patch.filters !== void 0) this.filters.set(patch.filters);
764
+ if (patch.search !== void 0) this.globalSearch.set(patch.search);
765
+ if (patch.pageSize !== void 0) this.pageSize.set(normalizePageSize(patch.pageSize));
766
+ if (patch.page !== void 0) this.page.set(normalizePage(patch.page));
767
+ if (patch.selected !== void 0 || patch.excluded !== void 0) {
768
+ const excluded = patch.excluded ?? [];
769
+ this.selectionImpl.restore(excluded.length > 0 ? {
770
+ mode: "exclude",
771
+ ids: excluded
772
+ } : {
773
+ mode: "include",
774
+ ids: patch.selected ?? []
775
+ });
776
+ }
777
+ }
778
+ resetState() {
779
+ const initial = this.initialState;
780
+ this.sorting.set(initial.sort ?? []);
781
+ this.filters.set(initial.filters ?? []);
782
+ this.globalSearch.set(initial.search ?? "");
783
+ this.pageSize.set(this.defaultPageSize);
784
+ this.page.set(normalizePage(initial.page ?? 0));
785
+ const excluded = initial.excluded ?? [];
786
+ this.selectionImpl.restore(excluded.length > 0 ? {
787
+ mode: "exclude",
788
+ ids: excluded
789
+ } : {
790
+ mode: "include",
791
+ ids: initial.selected ?? []
792
+ });
793
+ }
794
+ defaultRowId = (row) => {
795
+ if (typeof row === "object" && row !== null) {
796
+ const candidate = row.id;
797
+ if (typeof candidate === "string" || typeof candidate === "number") return candidate;
798
+ let generated = this.generatedIds.get(row);
799
+ if (generated === void 0) {
800
+ generated = `rv-row-${++this.generatedIdSeq}`;
801
+ this.generatedIds.set(row, generated);
802
+ }
803
+ return generated;
804
+ }
805
+ return String(row);
806
+ };
807
+ };
808
+ function createTable(options) {
809
+ return new RiviniaTable(options);
810
+ }
811
+ export { RiviniaTable, applyFilters, applySearch, applySort, assertSpanColumns, buildSpanModel, createTable, paginate, readCellValue, tableColumn };
812
+
813
+ //# sourceMappingURL=rivinia-table.mjs.map