@joewinke/jatui 0.26.0 → 0.27.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/CLAUDE.md +106 -1
- package/dist/actions/keyboardScope.d.ts +58 -0
- package/dist/actions/keyboardScope.js +110 -0
- package/dist/actions/listNav.d.ts +48 -3
- package/dist/actions/listNav.js +267 -77
- package/dist/actions/longpress.d.ts +37 -0
- package/dist/actions/longpress.js +105 -0
- package/dist/actions/stickToBottom.d.ts +66 -0
- package/dist/actions/stickToBottom.js +155 -0
- package/dist/actions/swipe.d.ts +64 -0
- package/dist/actions/swipe.js +179 -0
- package/dist/components/BottomSheet.svelte +280 -0
- package/dist/components/BottomSheet.svelte.d.ts +27 -0
- package/dist/components/ConfirmDialog.svelte +21 -0
- package/dist/components/ConfirmDialog.svelte.d.ts +11 -1
- package/dist/components/ConfirmModal.svelte +15 -0
- package/dist/components/ConfirmModal.svelte.d.ts +5 -0
- package/dist/components/Drawer.svelte +18 -1
- package/dist/components/Drawer.svelte.d.ts +9 -1
- package/dist/components/InputDialog.svelte +21 -0
- package/dist/components/InputDialog.svelte.d.ts +11 -1
- package/dist/components/Modal.svelte +18 -1
- package/dist/components/Modal.svelte.d.ts +8 -0
- package/dist/components/basic/Empty.svelte +173 -9
- package/dist/components/basic/Empty.svelte.d.ts +50 -1
- package/dist/components/basic/ErrorState.svelte +128 -0
- package/dist/components/basic/ErrorState.svelte.d.ts +46 -0
- package/dist/components/basic/JumpToLatest.svelte +60 -0
- package/dist/components/basic/JumpToLatest.svelte.d.ts +9 -0
- package/dist/components/basic/PageHeader.svelte +216 -0
- package/dist/components/basic/PageHeader.svelte.d.ts +52 -0
- package/dist/components/basic/ProgressButton.svelte +221 -0
- package/dist/components/basic/ProgressButton.svelte.d.ts +63 -0
- package/dist/components/graph/GraphApiBridge.svelte +36 -0
- package/dist/components/graph/GraphApiBridge.svelte.d.ts +9 -0
- package/dist/components/graph/GraphCanvas.svelte +212 -27
- package/dist/components/graph/GraphCanvas.svelte.d.ts +113 -19
- package/dist/components/graph/api.d.ts +52 -0
- package/dist/components/graph/api.js +66 -0
- package/dist/components/graph/index.d.ts +2 -0
- package/dist/components/graph/index.js +1 -0
- package/dist/components/graph/theme.js +13 -0
- package/dist/components/messaging/MessageItem.svelte +5 -0
- package/dist/components/messaging/MessageItem.svelte.d.ts +7 -0
- package/dist/components/table/DataTable.svelte +452 -78
- package/dist/components/table/DataTable.svelte.d.ts +2 -2
- package/dist/components/table/ManageColumnsDropdown.svelte +466 -0
- package/dist/components/table/ManageColumnsDropdown.svelte.d.ts +26 -0
- package/dist/components/table/columnStorage.d.ts +29 -0
- package/dist/components/table/columnStorage.js +61 -0
- package/dist/components/table/groupRows.d.ts +72 -0
- package/dist/components/table/groupRows.js +118 -0
- package/dist/components/table/index.d.ts +7 -1
- package/dist/components/table/index.js +3 -0
- package/dist/components/table/types.d.ts +121 -20
- package/dist/index.d.ts +25 -2
- package/dist/index.js +32 -1
- package/dist/utils/ansiToHtml.d.ts +83 -0
- package/dist/utils/ansiToHtml.js +543 -0
- package/dist/utils/keyboardShortcutRegistry.d.ts +332 -11
- package/dist/utils/keyboardShortcutRegistry.js +1070 -76
- package/dist/utils/pollBackoff.d.ts +83 -0
- package/dist/utils/pollBackoff.js +151 -0
- package/dist/utils/snapFlick.d.ts +59 -0
- package/dist/utils/snapFlick.js +66 -0
- package/dist/video/advisors/qr.d.ts +1 -1
- package/dist/virtual/createRowWindow.d.ts +75 -0
- package/dist/virtual/createRowWindow.js +129 -0
- package/dist/virtual/index.d.ts +4 -0
- package/dist/virtual/index.js +5 -0
- package/dist/virtual/rowWindowCore.d.ts +202 -0
- package/dist/virtual/rowWindowCore.js +510 -0
- package/package.json +11 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts" generics="TData">
|
|
2
|
-
import { tick } from 'svelte';
|
|
2
|
+
import { tick, untrack } from 'svelte';
|
|
3
3
|
import {
|
|
4
4
|
createTable,
|
|
5
5
|
getCoreRowModel,
|
|
@@ -13,7 +13,15 @@
|
|
|
13
13
|
type Row,
|
|
14
14
|
} from '@tanstack/table-core';
|
|
15
15
|
import { flexRender } from './flexRender.js';
|
|
16
|
-
import
|
|
16
|
+
import { groupRows, groupPathKey, type GroupedGroupItem, type GroupedRowItem, type GroupLevel } from './groupRows.js';
|
|
17
|
+
import type {
|
|
18
|
+
DataTableProps,
|
|
19
|
+
DataTableRowProps,
|
|
20
|
+
DataTableSortEvent,
|
|
21
|
+
DataTableRenderItem,
|
|
22
|
+
DataTableRowWindow,
|
|
23
|
+
GroupedItem,
|
|
24
|
+
} from './types.js';
|
|
17
25
|
|
|
18
26
|
let {
|
|
19
27
|
data,
|
|
@@ -40,6 +48,18 @@
|
|
|
40
48
|
variant = 'zebra',
|
|
41
49
|
tableClass = '',
|
|
42
50
|
api = $bindable(),
|
|
51
|
+
groupBy,
|
|
52
|
+
collapsed = $bindable(),
|
|
53
|
+
onCollapsedChange,
|
|
54
|
+
groupLabel,
|
|
55
|
+
groupHeader,
|
|
56
|
+
stickyGroupHeaders = false,
|
|
57
|
+
stickyHeader = false,
|
|
58
|
+
maxHeight,
|
|
59
|
+
virtual,
|
|
60
|
+
pinnedRowIds,
|
|
61
|
+
ariaGrid = false,
|
|
62
|
+
ariaLabel,
|
|
43
63
|
}: DataTableProps<TData> = $props();
|
|
44
64
|
|
|
45
65
|
// ── State ──────────────────────────────────────────────────────────────
|
|
@@ -47,6 +67,9 @@
|
|
|
47
67
|
// state seeded from `initialSorting`.
|
|
48
68
|
// svelte-ignore state_referenced_locally
|
|
49
69
|
if (sorting === undefined) sorting = initialSorting ?? [];
|
|
70
|
+
// Same contract for the collapsed-group set.
|
|
71
|
+
// svelte-ignore state_referenced_locally
|
|
72
|
+
if (collapsed === undefined) collapsed = new Set();
|
|
50
73
|
const sortState = $derived<SortingState>(sorting ?? []);
|
|
51
74
|
let rowSelection = $state<RowSelectionState>({});
|
|
52
75
|
// svelte-ignore state_referenced_locally
|
|
@@ -83,8 +106,19 @@
|
|
|
83
106
|
const allColumns = $derived<ColumnDef<TData, unknown>[]>(selectable ? [checkboxCol, ...columns] : columns);
|
|
84
107
|
|
|
85
108
|
// ── Table instance ─────────────────────────────────────────────────────
|
|
86
|
-
|
|
87
|
-
|
|
109
|
+
// Created ONCE; every reactive change is pushed in with `setOptions` (R6).
|
|
110
|
+
// It used to be rebuilt in a `$derived.by` on every sort / selection / data
|
|
111
|
+
// change, which threw away TanStack's memoised row models and handed the
|
|
112
|
+
// template brand-new Row objects, so every row re-rendered on every poll.
|
|
113
|
+
//
|
|
114
|
+
// Row-model factories are created once too: TanStack caches the model a
|
|
115
|
+
// factory builds on first use, so a fresh factory per update would be ignored.
|
|
116
|
+
const coreRowModel = getCoreRowModel<TData>();
|
|
117
|
+
const sortedRowModel = getSortedRowModel<TData>();
|
|
118
|
+
const paginationRowModel = getPaginationRowModel<TData>();
|
|
119
|
+
|
|
120
|
+
function buildOptions(): TableOptionsResolved<TData> {
|
|
121
|
+
return {
|
|
88
122
|
data,
|
|
89
123
|
columns: allColumns,
|
|
90
124
|
state: {
|
|
@@ -110,28 +144,55 @@
|
|
|
110
144
|
}
|
|
111
145
|
: () => {},
|
|
112
146
|
onStateChange: () => {},
|
|
113
|
-
getCoreRowModel:
|
|
114
|
-
getSortedRowModel: sortable && !manualSorting ?
|
|
115
|
-
getPaginationRowModel: paginated ?
|
|
147
|
+
getCoreRowModel: coreRowModel,
|
|
148
|
+
getSortedRowModel: sortable && !manualSorting ? sortedRowModel : undefined,
|
|
149
|
+
getPaginationRowModel: paginated ? paginationRowModel : undefined,
|
|
116
150
|
renderFallbackValue: '',
|
|
117
151
|
};
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const table = createTable(untrack(buildOptions));
|
|
155
|
+
/** The row-model mode the cached `_get*RowModel`s were built for. */
|
|
156
|
+
let modelMode = '';
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Pushes the current props/state into the one instance. Its value is a fresh
|
|
160
|
+
* token per change: everything that reads the table goes through `t()` (or
|
|
161
|
+
* reads this), so it re-runs exactly when the table's inputs moved.
|
|
162
|
+
*/
|
|
163
|
+
const tableRev = $derived.by(() => {
|
|
164
|
+
const opts = buildOptions();
|
|
165
|
+
const mode = `${sortable && !manualSorting}|${paginated}`;
|
|
166
|
+
if (mode !== modelMode) {
|
|
167
|
+
// TanStack caches the sorted / paginated model on first use and never
|
|
168
|
+
// re-reads the option, so a flag flip must drop the cache.
|
|
169
|
+
table._getSortedRowModel = undefined;
|
|
170
|
+
table._getPaginationRowModel = undefined;
|
|
171
|
+
modelMode = mode;
|
|
172
|
+
}
|
|
173
|
+
// createTable takes a fully-resolved state — unlike the framework adapters it
|
|
174
|
+
// does not fill in defaults, so feature state we never touch (columnPinning,
|
|
175
|
+
// columnVisibility, …) would land undefined and getHeaderGroups() dies reading
|
|
176
|
+
// columnPinning.left. Merge the instance defaults under our reactive slices.
|
|
177
|
+
table.setOptions((prev) => ({
|
|
124
178
|
...prev,
|
|
125
|
-
|
|
179
|
+
...opts,
|
|
180
|
+
state: { ...table.initialState, ...opts.state },
|
|
126
181
|
}));
|
|
127
|
-
return
|
|
182
|
+
return {};
|
|
128
183
|
});
|
|
129
184
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
185
|
+
/** The table, read through `tableRev` so the caller re-runs when it changes. */
|
|
186
|
+
function t() {
|
|
187
|
+
void tableRev;
|
|
188
|
+
return table;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const headerGroups = $derived(t().getHeaderGroups());
|
|
192
|
+
const rows = $derived(t().getRowModel().rows);
|
|
193
|
+
const pageCount = $derived(t().getPageCount());
|
|
194
|
+
const canPrevPage = $derived(t().getCanPreviousPage());
|
|
195
|
+
const canNextPage = $derived(t().getCanNextPage());
|
|
135
196
|
|
|
136
197
|
const tableClasses = $derived(
|
|
137
198
|
[variant === 'unstyled' ? '' : variant === 'plain' ? 'table' : 'table table-zebra', 'w-full', tableClass]
|
|
@@ -144,16 +205,168 @@
|
|
|
144
205
|
|
|
145
206
|
$effect(() => {
|
|
146
207
|
if (!selectable || !onSelectionChange) return;
|
|
147
|
-
const selected =
|
|
208
|
+
const selected = t().getSelectedRowModel().rows.map((r) => r.original);
|
|
148
209
|
onSelectionChange(selected);
|
|
149
210
|
});
|
|
150
211
|
|
|
212
|
+
// ── Render list ────────────────────────────────────────────────────────
|
|
213
|
+
// Everything the body draws, in order: group headers, data rows and the
|
|
214
|
+
// detail row under each expanded row. One entry = one `<tr>`, which is what
|
|
215
|
+
// the row window counts and measures.
|
|
216
|
+
const GROUP_KEY = '\u0000group:';
|
|
217
|
+
const DETAIL_KEY = '\u0000detail:';
|
|
218
|
+
|
|
219
|
+
type Entry =
|
|
220
|
+
| { kind: 'group'; key: string; item: GroupedGroupItem<TData>; pathKey: string }
|
|
221
|
+
| { kind: 'row'; key: string; item: GroupedRowItem<TData>; row: Row<TData> }
|
|
222
|
+
| { kind: 'detail'; key: string; row: Row<TData> };
|
|
223
|
+
|
|
224
|
+
const grouped = $derived(!!groupBy && groupBy.length > 0);
|
|
225
|
+
|
|
226
|
+
const entries = $derived.by<Entry[]>(() => {
|
|
227
|
+
const list = rows;
|
|
228
|
+
const out: Entry[] = [];
|
|
229
|
+
const pushRow = (row: Row<TData>, item: GroupedRowItem<TData>) => {
|
|
230
|
+
out.push({ kind: 'row', key: row.id, item, row });
|
|
231
|
+
if (rowIsExpanded(row)) out.push({ kind: 'detail', key: DETAIL_KEY + row.id, row });
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
if (!grouped) {
|
|
235
|
+
const n = list.length;
|
|
236
|
+
list.forEach((row, i) => {
|
|
237
|
+
const position = n === 1 ? 'only' : i === 0 ? 'first' : i === n - 1 ? 'last' : 'middle';
|
|
238
|
+
pushRow(row, { kind: 'row', row: row.original, id: row.id, depth: 0, groupPath: [], position });
|
|
239
|
+
});
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Group the TanStack rows themselves (not their originals) so duplicate
|
|
244
|
+
// originals stay distinct, then hand callers originals in the items.
|
|
245
|
+
const levels: GroupLevel<Row<TData>>[] = groupBy!.map((level) => ({
|
|
246
|
+
key: (r) => level.key(r.original),
|
|
247
|
+
compare: level.compare,
|
|
248
|
+
showHeader: level.showHeader
|
|
249
|
+
? (k, rs) => level.showHeader!(k, rs.map((r) => r.original))
|
|
250
|
+
: undefined,
|
|
251
|
+
}));
|
|
252
|
+
const items = groupRows(list, levels, {
|
|
253
|
+
getRowId: (r) => r.id,
|
|
254
|
+
collapsed: collapsed ?? new Set(),
|
|
255
|
+
label: groupLabel,
|
|
256
|
+
});
|
|
257
|
+
for (const it of items) {
|
|
258
|
+
if (it.kind === 'group') {
|
|
259
|
+
const pathKey = groupPathKey(it.path);
|
|
260
|
+
out.push({
|
|
261
|
+
kind: 'group',
|
|
262
|
+
key: GROUP_KEY + pathKey,
|
|
263
|
+
pathKey,
|
|
264
|
+
item: { ...it, rows: it.rows.map((r) => r.original) },
|
|
265
|
+
});
|
|
266
|
+
} else {
|
|
267
|
+
pushRow(it.row, { ...it, row: it.row.original });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return out;
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
/** Group-header indices per depth, outermost first — the window's sticky levels. */
|
|
274
|
+
const stickyLevels = $derived.by<number[][]>(() => {
|
|
275
|
+
const levels: number[][] = [];
|
|
276
|
+
entries.forEach((e, i) => {
|
|
277
|
+
if (e.kind !== 'group') return;
|
|
278
|
+
(levels[e.item.depth] ??= []).push(i);
|
|
279
|
+
});
|
|
280
|
+
for (let d = 0; d < levels.length; d++) levels[d] ??= [];
|
|
281
|
+
return levels;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// ── Windowing (grid mode) ──────────────────────────────────────────────
|
|
285
|
+
function renderItem(e: Entry): DataTableRenderItem<TData> {
|
|
286
|
+
if (e.kind === 'detail') return { kind: 'detail', row: e.row.original, id: e.row.id };
|
|
287
|
+
return e.item as GroupedItem<TData>;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function estimate(e: Entry | undefined): number {
|
|
291
|
+
if (!e) return 40;
|
|
292
|
+
const est = virtual?.estimateRowSize;
|
|
293
|
+
if (typeof est === 'function') return est(renderItem(e));
|
|
294
|
+
if (e.kind === 'group') return 36;
|
|
295
|
+
if (e.kind === 'detail') return 120;
|
|
296
|
+
return typeof est === 'number' ? est : 40;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Read once: the window must be created during component init.
|
|
300
|
+
const virtualAtMount = untrack(() => virtual);
|
|
301
|
+
const win: DataTableRowWindow | null = virtualAtMount
|
|
302
|
+
? virtualAtMount.createRowWindow({
|
|
303
|
+
count: () => entries.length,
|
|
304
|
+
getKey: (i) => entries[i]?.key ?? String(i),
|
|
305
|
+
estimateSize: (i) => estimate(entries[i]),
|
|
306
|
+
overscan: virtualAtMount.overscan,
|
|
307
|
+
get threshold() {
|
|
308
|
+
return virtual?.threshold ?? 0;
|
|
309
|
+
},
|
|
310
|
+
scrollRoot: virtualAtMount.scrollRoot ?? 'auto',
|
|
311
|
+
scrollPaddingStart: virtualAtMount.scrollPaddingStart,
|
|
312
|
+
pinned: () => {
|
|
313
|
+
const keys: string[] = [];
|
|
314
|
+
for (const id of pinnedRowIds ?? []) keys.push(id, DETAIL_KEY + id);
|
|
315
|
+
return keys;
|
|
316
|
+
},
|
|
317
|
+
stickyIndices: () => (stickyGroupHeaders ? stickyLevels : []),
|
|
318
|
+
})
|
|
319
|
+
: null;
|
|
320
|
+
|
|
321
|
+
const noopAction = (_node: HTMLElement, _key?: string) => ({ update() {}, destroy() {} });
|
|
322
|
+
const measureAction = win ? win.measure : noopAction;
|
|
323
|
+
const anchorAction = win ? win.anchor : noopAction;
|
|
324
|
+
|
|
325
|
+
type Slot = { entry: Entry; index: number; spacers: number[] };
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Spacer rows that stand in for `skipped` undrawn entries totalling `height`
|
|
329
|
+
* px. The COUNT keeps `nth-child` parity with the unwindowed table, so zebra
|
|
330
|
+
* striping does not flip as the slice scrolls: an odd gap gets one spacer, an
|
|
331
|
+
* even one gets two.
|
|
332
|
+
*/
|
|
333
|
+
function spacersFor(skipped: number, height: number): number[] {
|
|
334
|
+
if (skipped <= 0) return height > 0 ? [height, 0] : [];
|
|
335
|
+
return skipped % 2 === 1 ? [height] : [height, 0];
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const slots = $derived.by<Slot[]>(() => {
|
|
339
|
+
if (!win) return entries.map((entry, index) => ({ entry, index, spacers: [] }));
|
|
340
|
+
const out: Slot[] = [];
|
|
341
|
+
let prev = -1;
|
|
342
|
+
for (const it of win.items) {
|
|
343
|
+
const entry = entries[it.index];
|
|
344
|
+
// The window can trail the entry list by one flush; skip strays.
|
|
345
|
+
if (!entry || entry.key !== it.key) continue;
|
|
346
|
+
const height = prev < 0 ? win.paddingTop : it.gapBefore;
|
|
347
|
+
out.push({ entry, index: it.index, spacers: spacersFor(it.index - prev - 1, height) });
|
|
348
|
+
prev = it.index;
|
|
349
|
+
}
|
|
350
|
+
return out;
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// ── ARIA ───────────────────────────────────────────────────────────────
|
|
354
|
+
const ariaRows = $derived(ariaGrid || !!win);
|
|
355
|
+
const tableRole = $derived(ariaGrid ? (grouped ? 'treegrid' : 'grid') : undefined);
|
|
356
|
+
/** 1-based row index, counting the header rows. */
|
|
357
|
+
function ariaRowIndex(index: number): number | undefined {
|
|
358
|
+
return ariaRows ? index + 1 + headerGroups.length : undefined;
|
|
359
|
+
}
|
|
360
|
+
|
|
151
361
|
// ── Imperative api ─────────────────────────────────────────────────────
|
|
152
362
|
api = {
|
|
153
|
-
orderedRowIds: () =>
|
|
363
|
+
orderedRowIds: () =>
|
|
364
|
+
grouped && !paginated
|
|
365
|
+
? entries.filter((e) => e.kind === 'row').map((e) => e.key)
|
|
366
|
+
: t().getPrePaginationRowModel().rows.map((r) => r.id),
|
|
154
367
|
scrollToRow: async (id, align = 'auto') => {
|
|
155
368
|
if (paginated) {
|
|
156
|
-
const index =
|
|
369
|
+
const index = t().getPrePaginationRowModel().rows.findIndex((r) => r.id === id);
|
|
157
370
|
if (index < 0) return;
|
|
158
371
|
const pageIndex = Math.floor(index / pagination.pageSize);
|
|
159
372
|
if (pageIndex !== pagination.pageIndex) {
|
|
@@ -161,6 +374,10 @@
|
|
|
161
374
|
await tick();
|
|
162
375
|
}
|
|
163
376
|
}
|
|
377
|
+
if (win) {
|
|
378
|
+
win.scrollToKey(id, { align });
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
164
381
|
const el = rootEl?.querySelector<HTMLElement>(`tr[data-row-id="${CSS.escape(id)}"]`);
|
|
165
382
|
el?.scrollIntoView({ block: align === 'auto' ? 'nearest' : align });
|
|
166
383
|
},
|
|
@@ -197,9 +414,9 @@
|
|
|
197
414
|
}
|
|
198
415
|
|
|
199
416
|
// ── Row helpers ────────────────────────────────────────────────────────
|
|
200
|
-
function
|
|
201
|
-
|
|
202
|
-
return
|
|
417
|
+
function isSelected(row: Row<TData>): boolean {
|
|
418
|
+
void tableRev;
|
|
419
|
+
return row.getIsSelected();
|
|
203
420
|
}
|
|
204
421
|
|
|
205
422
|
function rowIsExpanded(row: Row<TData>): boolean {
|
|
@@ -214,6 +431,14 @@
|
|
|
214
431
|
expandedIds = next;
|
|
215
432
|
}
|
|
216
433
|
|
|
434
|
+
function toggleGroup(pathKey: string) {
|
|
435
|
+
const next = new Set(collapsed ?? []);
|
|
436
|
+
if (next.has(pathKey)) next.delete(pathKey);
|
|
437
|
+
else next.add(pathKey);
|
|
438
|
+
collapsed = next;
|
|
439
|
+
onCollapsedChange?.(next);
|
|
440
|
+
}
|
|
441
|
+
|
|
217
442
|
/** Clicks on these inside a row belong to the control, not to the row. */
|
|
218
443
|
const INTERACTIVE = 'input, button, a, select, textarea, label, [contenteditable]:not([contenteditable="false"])';
|
|
219
444
|
|
|
@@ -249,6 +474,15 @@
|
|
|
249
474
|
}
|
|
250
475
|
|
|
251
476
|
const hasRowHandlers = $derived(!!rowProps || !!onRowClick || internalExpand);
|
|
477
|
+
|
|
478
|
+
// ── Scroll container ───────────────────────────────────────────────────
|
|
479
|
+
// The default wrapper scrolls horizontally, which also makes it the sticky
|
|
480
|
+
// containing block — a sticky header would stick to a box that never scrolls
|
|
481
|
+
// vertically. With sticky on and no `maxHeight`, clip instead (no scroll
|
|
482
|
+
// container); with `maxHeight`, the wrapper becomes the real scroller.
|
|
483
|
+
const sticky = $derived(stickyHeader || stickyGroupHeaders);
|
|
484
|
+
const scrollerClass = $derived(maxHeight ? 'dt-scroller' : sticky ? 'dt-clip' : 'overflow-x-auto');
|
|
485
|
+
let theadHeight = $state(0);
|
|
252
486
|
</script>
|
|
253
487
|
|
|
254
488
|
<div class="datatable-root {className}" bind:this={rootEl}>
|
|
@@ -276,19 +510,30 @@
|
|
|
276
510
|
</table>
|
|
277
511
|
</div>
|
|
278
512
|
{:else}
|
|
279
|
-
<div
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
513
|
+
<div
|
|
514
|
+
class={scrollerClass}
|
|
515
|
+
style:max-height={maxHeight}
|
|
516
|
+
style:--dt-thead-h={sticky ? `${stickyHeader ? theadHeight : 0}px` : undefined}
|
|
517
|
+
>
|
|
518
|
+
<table
|
|
519
|
+
class={tableClasses}
|
|
520
|
+
class:dt-sticky-head={stickyHeader}
|
|
521
|
+
role={tableRole}
|
|
522
|
+
aria-label={ariaLabel}
|
|
523
|
+
aria-multiselectable={ariaGrid && selectable ? 'true' : undefined}
|
|
524
|
+
aria-rowcount={ariaRows ? entries.length + headerGroups.length : undefined}
|
|
525
|
+
>
|
|
526
|
+
<thead bind:offsetHeight={theadHeight}>
|
|
527
|
+
{#each headerGroups as headerGroup, hi}
|
|
528
|
+
<tr aria-rowindex={ariaRows ? hi + 1 : undefined}>
|
|
284
529
|
{#each headerGroup.headers as header}
|
|
285
530
|
{#if header.column.id === '__select'}
|
|
286
531
|
<th class="dt-select-cell">
|
|
287
532
|
<input
|
|
288
533
|
type="checkbox"
|
|
289
534
|
class="checkbox checkbox-sm"
|
|
290
|
-
checked={
|
|
291
|
-
indeterminate={
|
|
535
|
+
checked={t().getIsAllRowsSelected()}
|
|
536
|
+
indeterminate={t().getIsSomeRowsSelected()}
|
|
292
537
|
aria-label="Select all rows"
|
|
293
538
|
onchange={() => table.toggleAllRowsSelected()}
|
|
294
539
|
/>
|
|
@@ -336,7 +581,7 @@
|
|
|
336
581
|
{/each}
|
|
337
582
|
</thead>
|
|
338
583
|
|
|
339
|
-
<tbody>
|
|
584
|
+
<tbody use:anchorAction>
|
|
340
585
|
{#if rows.length === 0}
|
|
341
586
|
<tr>
|
|
342
587
|
{#if empty}
|
|
@@ -346,56 +591,113 @@
|
|
|
346
591
|
{/if}
|
|
347
592
|
</tr>
|
|
348
593
|
{:else}
|
|
349
|
-
{#each
|
|
350
|
-
{
|
|
351
|
-
|
|
352
|
-
{
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
{
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
class="checkbox checkbox-sm"
|
|
375
|
-
checked={row.getIsSelected()}
|
|
376
|
-
disabled={!row.getCanSelect()}
|
|
377
|
-
aria-label="Select row"
|
|
378
|
-
onchange={() => row.toggleSelected()}
|
|
379
|
-
/>
|
|
380
|
-
</td>
|
|
594
|
+
{#each slots as slot (slot.entry.key)}
|
|
595
|
+
{#each slot.spacers as h}
|
|
596
|
+
<tr class="dt-spacer" aria-hidden="true" style:height="{h}px"></tr>
|
|
597
|
+
{/each}
|
|
598
|
+
{@const entry = slot.entry}
|
|
599
|
+
{#if entry.kind === 'group'}
|
|
600
|
+
{@const g = entry.item}
|
|
601
|
+
{@const pathKey = entry.pathKey}
|
|
602
|
+
<tr
|
|
603
|
+
class="dt-group-row"
|
|
604
|
+
class:dt-group-sticky={stickyGroupHeaders}
|
|
605
|
+
data-group-key={pathKey}
|
|
606
|
+
data-group-depth={g.depth}
|
|
607
|
+
style:--dt-group-depth={g.depth}
|
|
608
|
+
aria-rowindex={ariaRowIndex(slot.index)}
|
|
609
|
+
aria-level={ariaGrid ? g.depth + 1 : undefined}
|
|
610
|
+
aria-expanded={ariaGrid ? !g.collapsed : undefined}
|
|
611
|
+
use:measureAction={entry.key}
|
|
612
|
+
>
|
|
613
|
+
{#if groupHeader}
|
|
614
|
+
{@render groupHeader(g, {
|
|
615
|
+
toggle: () => toggleGroup(pathKey),
|
|
616
|
+
colspan: allColumns.length,
|
|
617
|
+
collapsed: g.collapsed,
|
|
618
|
+
})}
|
|
381
619
|
{:else}
|
|
382
|
-
{
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
{
|
|
388
|
-
|
|
389
|
-
|
|
620
|
+
<th colspan={allColumns.length} scope="rowgroup" class="dt-group-cell">
|
|
621
|
+
<button
|
|
622
|
+
type="button"
|
|
623
|
+
class="dt-group-toggle"
|
|
624
|
+
aria-expanded={!g.collapsed}
|
|
625
|
+
onclick={() => toggleGroup(pathKey)}
|
|
626
|
+
>
|
|
627
|
+
<svg class="dt-group-chevron" class:dt-group-chevron-open={!g.collapsed} width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
628
|
+
<path d="M4.5 2.5L8 6L4.5 9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
629
|
+
</svg>
|
|
630
|
+
<span class="dt-group-label">{g.label}</span>
|
|
631
|
+
<span class="dt-group-count">{g.count}</span>
|
|
632
|
+
</button>
|
|
633
|
+
</th>
|
|
390
634
|
{/if}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
635
|
+
</tr>
|
|
636
|
+
{:else if entry.kind === 'row'}
|
|
637
|
+
{@const row = entry.row}
|
|
638
|
+
{@const rp = rowProps?.(row.original, entry.item) ?? {}}
|
|
639
|
+
{@const attrs = rowAttrs(rp)}
|
|
640
|
+
{@const expanded = rowIsExpanded(row)}
|
|
641
|
+
{@const selected = isSelected(row)}
|
|
642
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
643
|
+
<tr
|
|
644
|
+
{...attrs}
|
|
645
|
+
class={rp.class}
|
|
646
|
+
class:dt-row-selected={selected}
|
|
647
|
+
class:dt-row-clickable={!!rp.onclick || !!onRowClick || internalExpand}
|
|
648
|
+
class:dt-row-expanded={expanded}
|
|
649
|
+
data-row-id={getRowId ? row.id : undefined}
|
|
650
|
+
aria-expanded={expandedRow ? expanded : undefined}
|
|
651
|
+
aria-selected={ariaGrid && selectable ? selected : undefined}
|
|
652
|
+
aria-level={ariaGrid && grouped ? entry.item.depth + 1 : undefined}
|
|
653
|
+
aria-rowindex={ariaRowIndex(slot.index)}
|
|
654
|
+
tabindex={internalExpand ? (attrs.tabindex ?? 0) : attrs.tabindex}
|
|
655
|
+
onclick={hasRowHandlers ? (e) => onRowClickInternal(row, rp, e) : undefined}
|
|
656
|
+
onkeydown={internalExpand ? (e) => onRowKeydown(row, e) : undefined}
|
|
657
|
+
oncontextmenu={rp.oncontextmenu}
|
|
658
|
+
ondragover={rp.ondragover}
|
|
659
|
+
ondrop={rp.ondrop}
|
|
660
|
+
use:measureAction={entry.key}
|
|
661
|
+
>
|
|
662
|
+
{#each row.getVisibleCells() as cell}
|
|
663
|
+
{#if cell.column.id === '__select'}
|
|
664
|
+
<td class="dt-select-cell">
|
|
665
|
+
<input
|
|
666
|
+
type="checkbox"
|
|
667
|
+
class="checkbox checkbox-sm"
|
|
668
|
+
checked={selected}
|
|
669
|
+
disabled={!row.getCanSelect()}
|
|
670
|
+
aria-label="Select row"
|
|
671
|
+
onchange={() => row.toggleSelected()}
|
|
672
|
+
/>
|
|
673
|
+
</td>
|
|
674
|
+
{:else}
|
|
675
|
+
{@const rendered = renderCell(cell)}
|
|
676
|
+
<td>
|
|
677
|
+
{#if rendered.type === 'snippet'}
|
|
678
|
+
{@render rendered.snippet(rendered.props)}
|
|
679
|
+
{:else}
|
|
680
|
+
{rendered.value}
|
|
681
|
+
{/if}
|
|
682
|
+
</td>
|
|
683
|
+
{/if}
|
|
684
|
+
{/each}
|
|
685
|
+
</tr>
|
|
686
|
+
{:else if expandedRow}
|
|
687
|
+
{@const row = entry.row}
|
|
688
|
+
<tr
|
|
689
|
+
class="dt-expanded-detail"
|
|
690
|
+
data-detail-for={getRowId ? row.id : undefined}
|
|
691
|
+
aria-rowindex={ariaRowIndex(slot.index)}
|
|
692
|
+
use:measureAction={entry.key}
|
|
693
|
+
>
|
|
395
694
|
<td colspan={allColumns.length}>{@render expandedRow(row.original, { colspan: allColumns.length })}</td>
|
|
396
695
|
</tr>
|
|
397
696
|
{/if}
|
|
398
697
|
{/each}
|
|
698
|
+
{#if win && win.paddingBottom > 0}
|
|
699
|
+
<tr class="dt-spacer" aria-hidden="true" style:height="{win.paddingBottom}px"></tr>
|
|
700
|
+
{/if}
|
|
399
701
|
{/if}
|
|
400
702
|
</tbody>
|
|
401
703
|
</table>
|
|
@@ -542,4 +844,76 @@
|
|
|
542
844
|
@media (prefers-reduced-motion: reduce) {
|
|
543
845
|
.skel-cell { animation: none; }
|
|
544
846
|
}
|
|
847
|
+
|
|
848
|
+
/* ── Grid mode ─────────────────────────────────────────────────────── */
|
|
849
|
+
.dt-scroller {
|
|
850
|
+
overflow: auto;
|
|
851
|
+
}
|
|
852
|
+
/* Sticky without maxHeight: no scroll container, so sticky cells stick to
|
|
853
|
+
the page's scroller. Horizontal overflow is clipped instead of scrolled. */
|
|
854
|
+
.dt-clip {
|
|
855
|
+
overflow-x: clip;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
.dt-sticky-head thead th {
|
|
859
|
+
position: sticky;
|
|
860
|
+
top: 0;
|
|
861
|
+
z-index: 3;
|
|
862
|
+
background-color: var(--color-base-100);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
.dt-spacer {
|
|
866
|
+
border: 0;
|
|
867
|
+
background: transparent !important;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
.dt-group-sticky > :global(th),
|
|
871
|
+
.dt-group-sticky > :global(td) {
|
|
872
|
+
position: sticky;
|
|
873
|
+
top: calc(var(--dt-thead-h, 0px) + var(--dt-group-depth, 0) * var(--dt-group-row-h, 2.25rem));
|
|
874
|
+
z-index: 2;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
.dt-group-cell {
|
|
878
|
+
text-align: left;
|
|
879
|
+
padding-left: calc(var(--jatui-space-base, 8px) + var(--dt-group-depth, 0) * var(--dt-group-indent, 1rem));
|
|
880
|
+
background-color: var(--color-base-200);
|
|
881
|
+
font-weight: 600;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
.dt-group-toggle {
|
|
885
|
+
display: inline-flex;
|
|
886
|
+
align-items: center;
|
|
887
|
+
gap: var(--jatui-space-sm, 4px);
|
|
888
|
+
padding: 0;
|
|
889
|
+
border: 0;
|
|
890
|
+
background: none;
|
|
891
|
+
color: inherit;
|
|
892
|
+
font: inherit;
|
|
893
|
+
cursor: pointer;
|
|
894
|
+
border-radius: var(--jatui-radius-selector, 0.375rem);
|
|
895
|
+
}
|
|
896
|
+
.dt-group-toggle:focus-visible {
|
|
897
|
+
outline: 2px solid var(--color-primary);
|
|
898
|
+
outline-offset: 2px;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
.dt-group-chevron {
|
|
902
|
+
flex-shrink: 0;
|
|
903
|
+
color: color-mix(in oklch, var(--color-base-content) 55%, transparent);
|
|
904
|
+
transition: transform var(--motion-fast, 150ms) var(--motion-glide, ease-out);
|
|
905
|
+
}
|
|
906
|
+
.dt-group-chevron-open {
|
|
907
|
+
transform: rotate(90deg);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
.dt-group-count {
|
|
911
|
+
font-weight: 400;
|
|
912
|
+
font-size: 0.75rem;
|
|
913
|
+
color: color-mix(in oklch, var(--color-base-content) 50%, transparent);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
@media (prefers-reduced-motion: reduce) {
|
|
917
|
+
.dt-group-chevron { transition: none; }
|
|
918
|
+
}
|
|
545
919
|
</style>
|
|
@@ -2,7 +2,7 @@ import type { DataTableProps } from './types.js';
|
|
|
2
2
|
declare function $$render<TData>(): {
|
|
3
3
|
props: DataTableProps<TData>;
|
|
4
4
|
exports: {};
|
|
5
|
-
bindings: "sorting" | "api";
|
|
5
|
+
bindings: "collapsed" | "sorting" | "api";
|
|
6
6
|
slots: {};
|
|
7
7
|
events: {};
|
|
8
8
|
};
|
|
@@ -10,7 +10,7 @@ declare class __sveltets_Render<TData> {
|
|
|
10
10
|
props(): ReturnType<typeof $$render<TData>>['props'];
|
|
11
11
|
events(): ReturnType<typeof $$render<TData>>['events'];
|
|
12
12
|
slots(): ReturnType<typeof $$render<TData>>['slots'];
|
|
13
|
-
bindings(): "sorting" | "api";
|
|
13
|
+
bindings(): "collapsed" | "sorting" | "api";
|
|
14
14
|
exports(): {};
|
|
15
15
|
}
|
|
16
16
|
interface $$IsomorphicComponent {
|