@rimelight/ui 0.0.51 → 0.0.52
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/package.json +10 -8
- package/src/components/dropdown-menu/RLADropdownMenu.astro +258 -250
- package/src/components/dropdown-menu/RLSDropdownMenu.tsx +161 -140
- package/src/components/head/RLSHead.tsx +2 -7
- package/src/components/popover/RLAPopover.astro +1 -2
- package/src/components/select/RLASelect.astro +1 -1
- package/src/components/table/RLATable.astro +1692 -1044
- package/src/components/table/RLSTable.tsx +1239 -162
- package/src/components/table/table.theme.ts +20 -14
- package/src/components/table/table.ts +211 -43
- package/LICENSE +0 -21
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { createMemo, createSignal, For, Show, type JSX } from "solid-js"
|
|
1
|
+
import { createMemo, createSignal, For, onCleanup, onMount, Show, type JSX } from "solid-js"
|
|
2
|
+
import { isServer } from "solid-js/web"
|
|
2
3
|
import type {
|
|
3
4
|
TableProps,
|
|
4
5
|
TableColumn,
|
|
@@ -9,9 +10,15 @@ import type {
|
|
|
9
10
|
ColumnFiltersState,
|
|
10
11
|
TableColumnPinningState,
|
|
11
12
|
TableRowPinningState,
|
|
12
|
-
TablePaginationState
|
|
13
|
+
TablePaginationState,
|
|
14
|
+
TableVirtualizeOptions,
|
|
15
|
+
TableColumnSizingState,
|
|
16
|
+
TableColumnOrderState,
|
|
17
|
+
TableEditingCellState,
|
|
18
|
+
TableExportOptions
|
|
13
19
|
} from "./table"
|
|
14
20
|
import { tableTheme } from "./table.theme"
|
|
21
|
+
import { dropdownMenuTheme } from "../dropdown-menu/dropdown-menu.theme"
|
|
15
22
|
import RLSInput from "../input/RLSInput"
|
|
16
23
|
import RLSDropdownMenu from "../dropdown-menu/RLSDropdownMenu"
|
|
17
24
|
import RLSIcon from "../icon/RLSIcon"
|
|
@@ -40,6 +47,7 @@ function getNestedValue(obj: AnyData, key: string): any {
|
|
|
40
47
|
|
|
41
48
|
export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
42
49
|
let tableElementRef: HTMLTableElement | null = null
|
|
50
|
+
let containerRef: HTMLDivElement | null = null
|
|
43
51
|
|
|
44
52
|
// ─── Controlled / Uncontrolled State Store ──────────────────────────────────
|
|
45
53
|
const [internalSorting, setInternalSorting] = createSignal<SortingState>(props.sorting ?? [])
|
|
@@ -67,8 +75,31 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
67
75
|
const [internalPagination, setInternalPagination] = createSignal<
|
|
68
76
|
TablePaginationState | undefined
|
|
69
77
|
>(props.pagination)
|
|
78
|
+
const [internalColumnSizing, setInternalColumnSizing] = createSignal<TableColumnSizingState>(
|
|
79
|
+
props.columnSizing ?? {}
|
|
80
|
+
)
|
|
81
|
+
const [internalColumnOrder, setInternalColumnOrder] = createSignal<TableColumnOrderState>(
|
|
82
|
+
props.columnOrder ?? []
|
|
83
|
+
)
|
|
84
|
+
const [internalEditingCell, setInternalEditingCell] = createSignal<TableEditingCellState>(
|
|
85
|
+
props.editingCell ?? null
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
// Drag & drop indicator states
|
|
89
|
+
const [draggedColId, setDraggedColId] = createSignal<string | null>(null)
|
|
90
|
+
const [dropTargetCol, setDropTargetCol] = createSignal<{
|
|
91
|
+
id: string
|
|
92
|
+
position: "left" | "right"
|
|
93
|
+
} | null>(null)
|
|
94
|
+
|
|
95
|
+
const [draggedRowIndex, setDraggedRowIndex] = createSignal<number | null>(null)
|
|
96
|
+
const [dropTargetRow, setDropTargetRow] = createSignal<{
|
|
97
|
+
id: string
|
|
98
|
+
index: number
|
|
99
|
+
position: "top" | "bottom"
|
|
100
|
+
} | null>(null)
|
|
70
101
|
|
|
71
|
-
// Reactive state readers
|
|
102
|
+
// Reactive state readers
|
|
72
103
|
const currentSorting = () => (props.sorting !== undefined ? props.sorting : internalSorting())
|
|
73
104
|
const currentGlobalFilter = () =>
|
|
74
105
|
props.globalFilter !== undefined ? props.globalFilter : internalGlobalFilter()
|
|
@@ -85,6 +116,12 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
85
116
|
props.rowPinning !== undefined ? props.rowPinning : internalRowPinning()
|
|
86
117
|
const currentPagination = () =>
|
|
87
118
|
props.pagination !== undefined ? props.pagination : internalPagination()
|
|
119
|
+
const currentColumnSizing = () =>
|
|
120
|
+
props.columnSizing !== undefined ? props.columnSizing : internalColumnSizing()
|
|
121
|
+
const currentColumnOrder = () =>
|
|
122
|
+
props.columnOrder !== undefined ? props.columnOrder : internalColumnOrder()
|
|
123
|
+
const currentEditingCell = () =>
|
|
124
|
+
props.editingCell !== undefined ? props.editingCell : internalEditingCell()
|
|
88
125
|
|
|
89
126
|
const updateSorting = (next: SortingState) => {
|
|
90
127
|
setInternalSorting(next)
|
|
@@ -122,23 +159,210 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
122
159
|
setInternalPagination(next)
|
|
123
160
|
props.onPaginationChange?.(next)
|
|
124
161
|
}
|
|
162
|
+
const updateColumnSizing = (next: TableColumnSizingState) => {
|
|
163
|
+
setInternalColumnSizing(next)
|
|
164
|
+
props.onColumnSizingChange?.(next)
|
|
165
|
+
}
|
|
166
|
+
const updateColumnOrder = (next: TableColumnOrderState) => {
|
|
167
|
+
setInternalColumnOrder(next)
|
|
168
|
+
props.onColumnOrderChange?.(next)
|
|
169
|
+
}
|
|
170
|
+
const updateEditingCell = (next: TableEditingCellState) => {
|
|
171
|
+
setInternalEditingCell(next)
|
|
172
|
+
props.onEditingCellChange?.(next)
|
|
173
|
+
}
|
|
125
174
|
|
|
126
|
-
// ─── Column resolution
|
|
127
|
-
const
|
|
175
|
+
// ─── Column resolution & ordering ───────────────────────────────────────────
|
|
176
|
+
const rawResolvedColumns = createMemo<TableColumn<T>[]>(() => {
|
|
177
|
+
let cols: TableColumn<T>[] = []
|
|
128
178
|
if (props.columns && props.columns.length > 0) {
|
|
129
|
-
|
|
179
|
+
cols = [...props.columns]
|
|
180
|
+
} else {
|
|
181
|
+
const d = props.data
|
|
182
|
+
if (d && d.length > 0 && typeof d[0] === "object" && d[0] !== null) {
|
|
183
|
+
const firstRow = d[0] as AnyData
|
|
184
|
+
cols = Object.keys(firstRow).map((key) => ({
|
|
185
|
+
id: key,
|
|
186
|
+
accessorKey: key as any,
|
|
187
|
+
header: key.charAt(0).toUpperCase() + key.slice(1),
|
|
188
|
+
enableSorting: true
|
|
189
|
+
}))
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// If row reordering is enabled and no dedicated reorder column exists, auto-prepend a grip column
|
|
194
|
+
if (
|
|
195
|
+
props.enableRowReordering &&
|
|
196
|
+
!cols.some((c) => c.id === "reorder" || c.id === "dragHandle")
|
|
197
|
+
) {
|
|
198
|
+
const reorderCol: TableColumn<T> = {
|
|
199
|
+
id: "reorder",
|
|
200
|
+
size: 36,
|
|
201
|
+
minSize: 36,
|
|
202
|
+
maxSize: 36,
|
|
203
|
+
enableSorting: false,
|
|
204
|
+
enableHiding: false,
|
|
205
|
+
header: "",
|
|
206
|
+
cell: ({ row }) => (
|
|
207
|
+
<div class="flex items-center justify-center">
|
|
208
|
+
<span
|
|
209
|
+
class={classes().dragHandle}
|
|
210
|
+
data-slot="drag-handle"
|
|
211
|
+
draggable={true}
|
|
212
|
+
onDragStart={(e) => {
|
|
213
|
+
setDraggedRowIndex(row.index)
|
|
214
|
+
e.dataTransfer?.setData("text/plain", String(row.index))
|
|
215
|
+
if (e.dataTransfer) {
|
|
216
|
+
e.dataTransfer.effectAllowed = "move"
|
|
217
|
+
}
|
|
218
|
+
}}
|
|
219
|
+
onDragEnd={() => {
|
|
220
|
+
setDraggedRowIndex(null)
|
|
221
|
+
setDropTargetRow(null)
|
|
222
|
+
}}
|
|
223
|
+
>
|
|
224
|
+
<span class="i-lucide-grip-vertical size-4" />
|
|
225
|
+
</span>
|
|
226
|
+
</div>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
return [reorderCol, ...cols]
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Auto-inject rowActions / actions column if defined on table props and not in columns
|
|
233
|
+
const actionsDef = props.actions || props.rowActions
|
|
234
|
+
if (actionsDef && !cols.some((c) => c.id === "actions")) {
|
|
235
|
+
cols.push({
|
|
236
|
+
id: "actions",
|
|
237
|
+
header: "",
|
|
238
|
+
size: 80,
|
|
239
|
+
minSize: 60,
|
|
240
|
+
enableSorting: false,
|
|
241
|
+
enableHiding: false,
|
|
242
|
+
meta: {
|
|
243
|
+
class: {
|
|
244
|
+
th: "text-right",
|
|
245
|
+
td: "text-right"
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
actions: actionsDef
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Normalize actions column defaults
|
|
253
|
+
for (const c of cols) {
|
|
254
|
+
if (c.id === "actions" || c.actions || c.items) {
|
|
255
|
+
if (c.enableSorting === undefined) c.enableSorting = false
|
|
256
|
+
if (c.header === undefined && c.id === "actions") c.header = ""
|
|
257
|
+
if (c.size === undefined && c.id === "actions") c.size = 60
|
|
258
|
+
if (c.minSize === undefined && c.id === "actions") c.minSize = 60
|
|
259
|
+
if (!c.meta?.class?.td && c.id === "actions") {
|
|
260
|
+
c.meta = {
|
|
261
|
+
...c.meta,
|
|
262
|
+
class: {
|
|
263
|
+
...c.meta?.class,
|
|
264
|
+
th: c.meta?.class?.th || "text-right",
|
|
265
|
+
td: c.meta?.class?.td || "text-right"
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return cols
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
function getCellActions(row: TableRow<T>, col: TableColumn<T>) {
|
|
276
|
+
const actionsFn =
|
|
277
|
+
col.actions ??
|
|
278
|
+
col.items ??
|
|
279
|
+
(col.id === "actions" ? props.actions || props.rowActions : undefined)
|
|
280
|
+
if (actionsFn) {
|
|
281
|
+
return typeof actionsFn === "function" ? (actionsFn as any)(row) : actionsFn
|
|
282
|
+
}
|
|
283
|
+
return undefined
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function renderCellContent(
|
|
287
|
+
row: TableRow<T>,
|
|
288
|
+
col: TableColumn<T>,
|
|
289
|
+
cellValue: () => any,
|
|
290
|
+
isEditing: () => boolean,
|
|
291
|
+
setEditing: (e: boolean) => void
|
|
292
|
+
) {
|
|
293
|
+
const actions = getCellActions(row, col)
|
|
294
|
+
if (Array.isArray(actions) && actions.length > 0) {
|
|
295
|
+
const dmClasses = dropdownMenuTheme({ size: "xs", color: "neutral" })
|
|
296
|
+
return (
|
|
297
|
+
<div class="flex items-center justify-end">
|
|
298
|
+
<div class={dmClasses.root} data-slot="root" data-dropdown-menu>
|
|
299
|
+
<button
|
|
300
|
+
type="button"
|
|
301
|
+
class={`${dmClasses.trigger} aspect-square !px-1.5 !py-1.5 inline-flex items-center justify-center shrink-0`}
|
|
302
|
+
data-slot="trigger"
|
|
303
|
+
data-dropdown-trigger
|
|
304
|
+
>
|
|
305
|
+
<span class="i-lucide-ellipsis-vertical size-4" />
|
|
306
|
+
</button>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
if (typeof col.cell === "function") {
|
|
312
|
+
const rendered = col.cell({
|
|
313
|
+
row,
|
|
314
|
+
getValue: cellValue,
|
|
315
|
+
renderValue: cellValue,
|
|
316
|
+
isEditing,
|
|
317
|
+
setEditing,
|
|
318
|
+
column: col,
|
|
319
|
+
table: tableApi
|
|
320
|
+
})
|
|
321
|
+
if (Array.isArray(rendered) && rendered.length > 0) {
|
|
322
|
+
const dmClasses = dropdownMenuTheme({ size: "xs", color: "neutral" })
|
|
323
|
+
return (
|
|
324
|
+
<div class="flex items-center justify-end">
|
|
325
|
+
<div class={dmClasses.root} data-slot="root" data-dropdown-menu>
|
|
326
|
+
<button
|
|
327
|
+
type="button"
|
|
328
|
+
class={`${dmClasses.trigger} aspect-square !px-1.5 !py-1.5 inline-flex items-center justify-center shrink-0`}
|
|
329
|
+
data-slot="trigger"
|
|
330
|
+
data-dropdown-trigger
|
|
331
|
+
>
|
|
332
|
+
<span class="i-lucide-ellipsis-vertical size-4" />
|
|
333
|
+
</button>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
return rendered
|
|
339
|
+
}
|
|
340
|
+
return cellValue() !== undefined && cellValue() !== null ? String(cellValue()) : "—"
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const resolvedColumns = createMemo<TableColumn<T>[]>(() => {
|
|
344
|
+
const raw = rawResolvedColumns()
|
|
345
|
+
const order = currentColumnOrder()
|
|
346
|
+
if (!order || order.length === 0) return raw
|
|
347
|
+
|
|
348
|
+
const ordered: TableColumn<T>[] = []
|
|
349
|
+
const rawMap = new Map<string, TableColumn<T>>()
|
|
350
|
+
for (const col of raw) {
|
|
351
|
+
const id = String(col.id || col.accessorKey || "")
|
|
352
|
+
rawMap.set(id, col)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
for (const id of order) {
|
|
356
|
+
if (rawMap.has(id)) {
|
|
357
|
+
ordered.push(rawMap.get(id)!)
|
|
358
|
+
rawMap.delete(id)
|
|
359
|
+
}
|
|
130
360
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return Object.keys(firstRow).map((key) => ({
|
|
135
|
-
id: key,
|
|
136
|
-
accessorKey: key as any,
|
|
137
|
-
header: key.charAt(0).toUpperCase() + key.slice(1),
|
|
138
|
-
enableSorting: true
|
|
139
|
-
}))
|
|
361
|
+
// Append any columns not explicitly in order
|
|
362
|
+
for (const remaining of rawMap.values()) {
|
|
363
|
+
ordered.push(remaining)
|
|
140
364
|
}
|
|
141
|
-
return
|
|
365
|
+
return ordered
|
|
142
366
|
})
|
|
143
367
|
|
|
144
368
|
const visibleColumns = createMemo(() => {
|
|
@@ -151,7 +375,75 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
151
375
|
})
|
|
152
376
|
})
|
|
153
377
|
|
|
378
|
+
// ─── Column Resizing Handler ────────────────────────────────────────────────
|
|
379
|
+
const [resizingColId, setResizingColId] = createSignal<string | null>(null)
|
|
380
|
+
|
|
381
|
+
const startResize = (
|
|
382
|
+
e: PointerEvent,
|
|
383
|
+
colId: string,
|
|
384
|
+
minSize?: number | string,
|
|
385
|
+
maxSize?: number | string
|
|
386
|
+
) => {
|
|
387
|
+
e.preventDefault()
|
|
388
|
+
e.stopPropagation()
|
|
389
|
+
const startX = e.clientX
|
|
390
|
+
const thElement = (e.target as HTMLElement).closest("th")
|
|
391
|
+
const initialWidth = currentColumnSizing()[colId] ?? thElement?.offsetWidth ?? 150
|
|
392
|
+
const minW = typeof minSize === "number" ? minSize : 40
|
|
393
|
+
const maxW = typeof maxSize === "number" ? maxSize : 2000
|
|
394
|
+
setResizingColId(colId)
|
|
395
|
+
|
|
396
|
+
let liveWidth = initialWidth
|
|
397
|
+
|
|
398
|
+
const onPointerMove = (moveEv: PointerEvent) => {
|
|
399
|
+
const deltaX = moveEv.clientX - startX
|
|
400
|
+
liveWidth = Math.max(minW, Math.min(maxW, initialWidth + deltaX))
|
|
401
|
+
if (props.columnResizeMode !== "onEnd") {
|
|
402
|
+
updateColumnSizing({
|
|
403
|
+
...currentColumnSizing(),
|
|
404
|
+
[colId]: liveWidth
|
|
405
|
+
})
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const onPointerUp = () => {
|
|
410
|
+
if (props.columnResizeMode === "onEnd") {
|
|
411
|
+
updateColumnSizing({
|
|
412
|
+
...currentColumnSizing(),
|
|
413
|
+
[colId]: liveWidth
|
|
414
|
+
})
|
|
415
|
+
}
|
|
416
|
+
setResizingColId(null)
|
|
417
|
+
window.removeEventListener("pointermove", onPointerMove)
|
|
418
|
+
window.removeEventListener("pointerup", onPointerUp)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
window.addEventListener("pointermove", onPointerMove)
|
|
422
|
+
window.addEventListener("pointerup", onPointerUp)
|
|
423
|
+
}
|
|
424
|
+
|
|
154
425
|
// ─── Row Hierarchy Building ──────────────────────────────────────────────────
|
|
426
|
+
const getAllRowDescendants = (row: TableRow<T>): TableRow<T>[] => {
|
|
427
|
+
if (!row.subRows || row.subRows.length === 0) return []
|
|
428
|
+
const res: TableRow<T>[] = []
|
|
429
|
+
for (const child of row.subRows) {
|
|
430
|
+
res.push(child)
|
|
431
|
+
res.push(...getAllRowDescendants(child))
|
|
432
|
+
}
|
|
433
|
+
return res
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const getAllFlatRows = (rows: TableRow<T>[]): TableRow<T>[] => {
|
|
437
|
+
const flat: TableRow<T>[] = []
|
|
438
|
+
for (const r of rows) {
|
|
439
|
+
flat.push(r)
|
|
440
|
+
if (r.subRows && r.subRows.length > 0) {
|
|
441
|
+
flat.push(...getAllFlatRows(r.subRows))
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return flat
|
|
445
|
+
}
|
|
446
|
+
|
|
155
447
|
const buildRowTree = (rawItems: T[], parentDepth = 0): TableRow<T>[] => {
|
|
156
448
|
return rawItems.map((item, idx) => {
|
|
157
449
|
const rId = props.getRowId
|
|
@@ -175,11 +467,42 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
175
467
|
original: item,
|
|
176
468
|
index: idx,
|
|
177
469
|
depth: parentDepth,
|
|
178
|
-
getValue: (key: string) =>
|
|
179
|
-
|
|
470
|
+
getValue: (key: string) => {
|
|
471
|
+
const colDef = resolvedColumns().find((c) => (c.id || c.accessorKey) === key)
|
|
472
|
+
if (colDef && typeof colDef.accessorFn === "function") {
|
|
473
|
+
return colDef.accessorFn(item)
|
|
474
|
+
}
|
|
475
|
+
return getNestedValue(item as AnyData, key)
|
|
476
|
+
},
|
|
477
|
+
getIsSelected: () => {
|
|
478
|
+
const sel = currentRowSelection()
|
|
479
|
+
if (subRowsList && subRowsList.length > 0) {
|
|
480
|
+
const descendants = getAllRowDescendants(rowObj)
|
|
481
|
+
if (descendants.length > 0) {
|
|
482
|
+
return descendants.every((d) => !!sel[d.id])
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return !!sel[rId]
|
|
486
|
+
},
|
|
487
|
+
getIsSomeSelected: () => {
|
|
488
|
+
if (!subRowsList || subRowsList.length === 0) return false
|
|
489
|
+
const sel = currentRowSelection()
|
|
490
|
+
const descendants = getAllRowDescendants(rowObj)
|
|
491
|
+
if (descendants.length === 0) return false
|
|
492
|
+
const count = descendants.filter((d) => !!sel[d.id]).length
|
|
493
|
+
return count > 0 && count < descendants.length
|
|
494
|
+
},
|
|
180
495
|
toggleSelected: (val?: boolean) => {
|
|
181
|
-
const
|
|
182
|
-
|
|
496
|
+
const isCurrentlySelected = rowObj.getIsSelected()
|
|
497
|
+
const nextVal = val !== undefined ? val : !isCurrentlySelected
|
|
498
|
+
const nextMap = { ...currentRowSelection(), [rId]: nextVal }
|
|
499
|
+
if (subRowsList) {
|
|
500
|
+
const descendants = getAllRowDescendants(rowObj)
|
|
501
|
+
for (const d of descendants) {
|
|
502
|
+
nextMap[d.id] = nextVal
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
updateRowSelection(nextMap)
|
|
183
506
|
},
|
|
184
507
|
getIsExpanded: () => !!currentExpanded()[rId],
|
|
185
508
|
toggleExpanded: (val?: boolean) => {
|
|
@@ -188,13 +511,11 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
188
511
|
},
|
|
189
512
|
getToggleExpandedHandler: () => (e?: Event) => {
|
|
190
513
|
e?.stopPropagation?.()
|
|
191
|
-
|
|
192
|
-
updateExpanded({ ...currentExpanded(), [rId]: nextVal })
|
|
514
|
+
rowObj.toggleExpanded()
|
|
193
515
|
},
|
|
194
516
|
getToggleSelectedHandler: () => (e?: Event) => {
|
|
195
517
|
e?.stopPropagation?.()
|
|
196
|
-
|
|
197
|
-
updateRowSelection({ ...currentRowSelection(), [rId]: nextVal })
|
|
518
|
+
rowObj.toggleSelected()
|
|
198
519
|
},
|
|
199
520
|
getIsPinned: () => {
|
|
200
521
|
const pin = currentRowPinning()
|
|
@@ -211,7 +532,12 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
211
532
|
updateRowPinning({ top: nextTop, bottom: nextBottom })
|
|
212
533
|
},
|
|
213
534
|
getIsGrouped: () => false,
|
|
214
|
-
getCanExpand: () =>
|
|
535
|
+
getCanExpand: () => {
|
|
536
|
+
if (typeof props.getRowCanExpand === "function") {
|
|
537
|
+
return props.getRowCanExpand(rowObj)
|
|
538
|
+
}
|
|
539
|
+
return !!(subRowsList && subRowsList.length > 0)
|
|
540
|
+
},
|
|
215
541
|
...(subRowsList ? { subRows: subRowsList } : {})
|
|
216
542
|
}
|
|
217
543
|
return rowObj
|
|
@@ -227,10 +553,20 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
227
553
|
if (typeof r.original !== "object" || !r.original) {
|
|
228
554
|
return String(r.original).toLowerCase().includes(q)
|
|
229
555
|
}
|
|
230
|
-
return
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
556
|
+
return (
|
|
557
|
+
Object.values(r.original as AnyData).some((val) =>
|
|
558
|
+
String(val ?? "")
|
|
559
|
+
.toLowerCase()
|
|
560
|
+
.includes(q)
|
|
561
|
+
) ||
|
|
562
|
+
resolvedColumns().some((col) => {
|
|
563
|
+
const v = col.accessorFn
|
|
564
|
+
? col.accessorFn(r.original)
|
|
565
|
+
: r.getValue((col.id || col.accessorKey || "") as string)
|
|
566
|
+
return String(v ?? "")
|
|
567
|
+
.toLowerCase()
|
|
568
|
+
.includes(q)
|
|
569
|
+
})
|
|
234
570
|
)
|
|
235
571
|
})
|
|
236
572
|
}
|
|
@@ -271,8 +607,16 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
271
607
|
let cmp: number
|
|
272
608
|
if (typeof valA === "number" && typeof valB === "number") {
|
|
273
609
|
cmp = valA - valB
|
|
610
|
+
} else if (valA instanceof Date && valB instanceof Date) {
|
|
611
|
+
cmp = valA.getTime() - valB.getTime()
|
|
274
612
|
} else {
|
|
275
|
-
|
|
613
|
+
const numA = Number(valA)
|
|
614
|
+
const numB = Number(valB)
|
|
615
|
+
if (!isNaN(numA) && !isNaN(numB)) {
|
|
616
|
+
cmp = numA - numB
|
|
617
|
+
} else {
|
|
618
|
+
cmp = String(valA).localeCompare(String(valB))
|
|
619
|
+
}
|
|
276
620
|
}
|
|
277
621
|
return desc ? -cmp : cmp
|
|
278
622
|
}
|
|
@@ -280,44 +624,60 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
280
624
|
})
|
|
281
625
|
})
|
|
282
626
|
|
|
283
|
-
// ─── Grouping pipeline
|
|
284
|
-
type GroupedItem =
|
|
627
|
+
// ─── Multi-level Grouping pipeline ──────────────────────────────────────────
|
|
628
|
+
type GroupedItem =
|
|
629
|
+
| { type: "group"; group: TableGroup<T>; depth: number }
|
|
630
|
+
| { type: "row"; row: TableRow<T> }
|
|
285
631
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
632
|
+
function groupRowsRecursively(
|
|
633
|
+
rows: TableRow<T>[],
|
|
634
|
+
groupCols: string[],
|
|
635
|
+
depth = 0,
|
|
636
|
+
parentPath = ""
|
|
637
|
+
): GroupedItem[] {
|
|
638
|
+
if (depth >= groupCols.length || rows.length === 0) {
|
|
639
|
+
return rows.map((r) => ({ type: "row", row: r }))
|
|
291
640
|
}
|
|
292
641
|
|
|
293
|
-
const
|
|
642
|
+
const colId = groupCols[depth]!
|
|
294
643
|
const groupMap = new Map<string, TableRow<T>[]>()
|
|
295
644
|
const groupOrder: string[] = []
|
|
296
645
|
|
|
297
|
-
for (const row of
|
|
298
|
-
const val = String(row.getValue(
|
|
646
|
+
for (const row of rows) {
|
|
647
|
+
const val = String(row.getValue(colId) ?? "")
|
|
299
648
|
if (!groupMap.has(val)) {
|
|
300
649
|
groupMap.set(val, [])
|
|
301
650
|
groupOrder.push(val)
|
|
302
651
|
}
|
|
652
|
+
row.groupingColumnId = colId
|
|
653
|
+
row.groupingValue = val
|
|
303
654
|
groupMap.get(val)!.push(row)
|
|
304
655
|
}
|
|
305
656
|
|
|
306
657
|
const result: GroupedItem[] = []
|
|
307
658
|
for (const val of groupOrder) {
|
|
308
|
-
const
|
|
659
|
+
const subRows = groupMap.get(val)!
|
|
660
|
+
const groupId = `group-${parentPath}${colId}-${val}`
|
|
309
661
|
const group: TableGroup<T> = {
|
|
310
|
-
id:
|
|
311
|
-
columnId:
|
|
662
|
+
id: groupId,
|
|
663
|
+
columnId: colId,
|
|
312
664
|
value: val,
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
result.push({ type: "group", group })
|
|
316
|
-
for (const row of rows) {
|
|
317
|
-
result.push({ type: "row", row })
|
|
665
|
+
depth,
|
|
666
|
+
rows: subRows
|
|
318
667
|
}
|
|
668
|
+
result.push({ type: "group", group, depth })
|
|
669
|
+
result.push(...groupRowsRecursively(subRows, groupCols, depth + 1, `${groupId}/`))
|
|
319
670
|
}
|
|
320
671
|
return result
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const groupedItems = createMemo<GroupedItem[]>(() => {
|
|
675
|
+
const sRows = sortedRows()
|
|
676
|
+
const grp = props.grouping
|
|
677
|
+
if (!grp || grp.length === 0) {
|
|
678
|
+
return sRows.map((r) => ({ type: "row", row: r }))
|
|
679
|
+
}
|
|
680
|
+
return groupRowsRecursively(sRows, grp, 0, "")
|
|
321
681
|
})
|
|
322
682
|
|
|
323
683
|
// ─── Row Pinning ─────────────────────────────────────────────────────────────
|
|
@@ -369,7 +729,75 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
369
729
|
return pageRows
|
|
370
730
|
})
|
|
371
731
|
|
|
732
|
+
// ─── Virtualization Windowing ────────────────────────────────────────────────
|
|
733
|
+
const [scrollTop, setScrollTop] = createSignal(0)
|
|
734
|
+
const isVirtual = () => !!props.virtualize
|
|
735
|
+
const virtualOpts = (): TableVirtualizeOptions =>
|
|
736
|
+
typeof props.virtualize === "object" ? props.virtualize : {}
|
|
737
|
+
|
|
738
|
+
const estimateRowHeight = () => {
|
|
739
|
+
const opt = virtualOpts().estimateSize
|
|
740
|
+
if (typeof opt === "number") return opt
|
|
741
|
+
return 48
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const overscan = () => virtualOpts().overscan ?? 8
|
|
745
|
+
|
|
746
|
+
const virtualizedView = createMemo(() => {
|
|
747
|
+
const items = paginatedItems()
|
|
748
|
+
if (!isVirtual()) {
|
|
749
|
+
return {
|
|
750
|
+
items,
|
|
751
|
+
padTop: 0,
|
|
752
|
+
padBottom: 0,
|
|
753
|
+
startIndex: 0
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
const rowH = estimateRowHeight()
|
|
758
|
+
const scrollPos = scrollTop()
|
|
759
|
+
const containerHeight = containerRef?.clientHeight || 400
|
|
760
|
+
const totalCount = items.length
|
|
761
|
+
|
|
762
|
+
const rawStart = Math.floor(scrollPos / rowH)
|
|
763
|
+
const startIndex = Math.max(0, rawStart - overscan())
|
|
764
|
+
const visibleCount = Math.ceil(containerHeight / rowH) + overscan() * 2
|
|
765
|
+
const endIndex = Math.min(totalCount, startIndex + visibleCount)
|
|
766
|
+
|
|
767
|
+
const padTop = startIndex * rowH
|
|
768
|
+
const padBottom = Math.max(0, (totalCount - endIndex) * rowH)
|
|
769
|
+
const sliced = items.slice(startIndex, endIndex)
|
|
770
|
+
|
|
771
|
+
return {
|
|
772
|
+
items: sliced,
|
|
773
|
+
padTop,
|
|
774
|
+
padBottom,
|
|
775
|
+
startIndex
|
|
776
|
+
}
|
|
777
|
+
})
|
|
778
|
+
|
|
779
|
+
if (!isServer) {
|
|
780
|
+
onMount(() => {
|
|
781
|
+
if (isVirtual()) {
|
|
782
|
+
const scrollEl = virtualOpts().getScrollElement?.() || containerRef
|
|
783
|
+
if (scrollEl) {
|
|
784
|
+
const handleScroll = () => setScrollTop(scrollEl.scrollTop)
|
|
785
|
+
scrollEl.addEventListener("scroll", handleScroll, { passive: true })
|
|
786
|
+
onCleanup(() => scrollEl.removeEventListener("scroll", handleScroll))
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
})
|
|
790
|
+
}
|
|
791
|
+
|
|
372
792
|
// ─── Column offsets ──────────────────────────────────────────────────────────
|
|
793
|
+
const getColSizeNumber = (col: TableColumn<T>): number => {
|
|
794
|
+
const colId = String(col.id || col.accessorKey || "")
|
|
795
|
+
const sized = currentColumnSizing()[colId]
|
|
796
|
+
if (typeof sized === "number") return sized
|
|
797
|
+
if (typeof col.size === "number") return col.size
|
|
798
|
+
return 150
|
|
799
|
+
}
|
|
800
|
+
|
|
373
801
|
const columnOffsets = createMemo(() => {
|
|
374
802
|
const cols = resolvedColumns()
|
|
375
803
|
const pin = currentColumnPinning()
|
|
@@ -381,7 +809,7 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
381
809
|
for (const colId of pin.left) {
|
|
382
810
|
left[colId] = offset
|
|
383
811
|
const col = cols.find((c) => (c.id || c.accessorKey) === colId)
|
|
384
|
-
offset +=
|
|
812
|
+
offset += col ? getColSizeNumber(col) : 150
|
|
385
813
|
}
|
|
386
814
|
}
|
|
387
815
|
|
|
@@ -390,7 +818,7 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
390
818
|
for (const colId of [...pin.right].reverse()) {
|
|
391
819
|
right[colId] = offset
|
|
392
820
|
const col = cols.find((c) => (c.id || c.accessorKey) === colId)
|
|
393
|
-
offset +=
|
|
821
|
+
offset += col ? getColSizeNumber(col) : 150
|
|
394
822
|
}
|
|
395
823
|
}
|
|
396
824
|
|
|
@@ -415,15 +843,78 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
415
843
|
}
|
|
416
844
|
|
|
417
845
|
// ─── Expose TableApi ─────────────────────────────────────────────────────────
|
|
846
|
+
const getPageRows = () => {
|
|
847
|
+
return paginatedItems()
|
|
848
|
+
.filter((i) => i.type === "row")
|
|
849
|
+
.map((i) => (i as { type: "row"; row: TableRow<T> }).row)
|
|
850
|
+
}
|
|
851
|
+
|
|
418
852
|
const tableApi: TableApi<T> = {
|
|
419
853
|
get tableRef() {
|
|
420
854
|
return tableElementRef
|
|
421
855
|
},
|
|
422
856
|
getFilteredRows: () => filteredRows(),
|
|
857
|
+
getFlatRows: () => getAllFlatRows(filteredRows()),
|
|
423
858
|
getSortedRows: () => sortedRows(),
|
|
424
|
-
getSelectedRows: () => filteredRows().filter((r) => r.getIsSelected()),
|
|
859
|
+
getSelectedRows: () => getAllFlatRows(filteredRows()).filter((r) => r.getIsSelected()),
|
|
860
|
+
getFilteredSelectedRowModel: () => {
|
|
861
|
+
const flatSelected = getAllFlatRows(filteredRows()).filter((r) => r.getIsSelected())
|
|
862
|
+
return {
|
|
863
|
+
rows: filteredRows().filter((r) => r.getIsSelected()),
|
|
864
|
+
flatRows: flatSelected
|
|
865
|
+
}
|
|
866
|
+
},
|
|
867
|
+
getFilteredRowModel: () => ({
|
|
868
|
+
rows: filteredRows(),
|
|
869
|
+
flatRows: getAllFlatRows(filteredRows())
|
|
870
|
+
}),
|
|
425
871
|
getAllColumns: () => resolvedColumns(),
|
|
426
872
|
getColumn: (id: string) => resolvedColumns().find((c) => (c.id || c.accessorKey) === id),
|
|
873
|
+
getIsAllRowsSelected: () => {
|
|
874
|
+
const flat = getAllFlatRows(filteredRows())
|
|
875
|
+
if (flat.length === 0) return false
|
|
876
|
+
return flat.every((r) => r.getIsSelected())
|
|
877
|
+
},
|
|
878
|
+
getIsSomeRowsSelected: () => {
|
|
879
|
+
const flat = getAllFlatRows(filteredRows())
|
|
880
|
+
if (flat.length === 0) return false
|
|
881
|
+
const selCount = flat.filter((r) => r.getIsSelected()).length
|
|
882
|
+
return selCount > 0 && selCount < flat.length
|
|
883
|
+
},
|
|
884
|
+
toggleAllRowsSelected: (selected?: boolean) => {
|
|
885
|
+
const flat = getAllFlatRows(filteredRows())
|
|
886
|
+
const isAll = flat.length > 0 && flat.every((r) => r.getIsSelected())
|
|
887
|
+
const targetState = selected !== undefined ? selected : !isAll
|
|
888
|
+
const nextMap = { ...currentRowSelection() }
|
|
889
|
+
for (const r of flat) {
|
|
890
|
+
nextMap[r.id] = targetState
|
|
891
|
+
}
|
|
892
|
+
updateRowSelection(nextMap)
|
|
893
|
+
},
|
|
894
|
+
getIsAllPageRowsSelected: () => {
|
|
895
|
+
const rows = getPageRows()
|
|
896
|
+
const flat = getAllFlatRows(rows)
|
|
897
|
+
if (flat.length === 0) return false
|
|
898
|
+
return flat.every((r) => r.getIsSelected())
|
|
899
|
+
},
|
|
900
|
+
getIsSomePageRowsSelected: () => {
|
|
901
|
+
const rows = getPageRows()
|
|
902
|
+
const flat = getAllFlatRows(rows)
|
|
903
|
+
if (flat.length === 0) return false
|
|
904
|
+
const selCount = flat.filter((r) => r.getIsSelected()).length
|
|
905
|
+
return selCount > 0 && selCount < flat.length
|
|
906
|
+
},
|
|
907
|
+
toggleAllPageRowsSelected: (selected?: boolean) => {
|
|
908
|
+
const rows = getPageRows()
|
|
909
|
+
const flat = getAllFlatRows(rows)
|
|
910
|
+
const isAll = flat.length > 0 && flat.every((r) => r.getIsSelected())
|
|
911
|
+
const targetState = selected !== undefined ? selected : !isAll
|
|
912
|
+
const nextMap = { ...currentRowSelection() }
|
|
913
|
+
for (const r of flat) {
|
|
914
|
+
nextMap[r.id] = targetState
|
|
915
|
+
}
|
|
916
|
+
updateRowSelection(nextMap)
|
|
917
|
+
},
|
|
427
918
|
setGlobalFilter: (val: string) => updateGlobalFilter(val),
|
|
428
919
|
setColumnFilter: (id: string, val: any) => {
|
|
429
920
|
const current = currentColumnFilters().filter((f) => f.id !== id)
|
|
@@ -439,16 +930,162 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
439
930
|
setRowPinning: (pinning: TableRowPinningState) => updateRowPinning(pinning),
|
|
440
931
|
setSorting: (st: SortingState) => updateSorting(st),
|
|
441
932
|
setPagination: (pag: TablePaginationState) => updatePagination(pag),
|
|
933
|
+
setPageIndex: (pageIndex: number) => {
|
|
934
|
+
const cur = currentPagination() || { pageIndex: 0, pageSize: 10 }
|
|
935
|
+
updatePagination({ ...cur, pageIndex })
|
|
936
|
+
},
|
|
937
|
+
setPageSize: (pageSize: number) => {
|
|
938
|
+
const cur = currentPagination() || { pageIndex: 0, pageSize: 10 }
|
|
939
|
+
updatePagination({ ...cur, pageSize })
|
|
940
|
+
},
|
|
941
|
+
// Resizing & Ordering
|
|
942
|
+
setColumnSizing: (id: string, size: number) => {
|
|
943
|
+
updateColumnSizing({ ...currentColumnSizing(), [id]: size })
|
|
944
|
+
},
|
|
945
|
+
resetColumnSizing: () => updateColumnSizing({}),
|
|
946
|
+
setColumnOrder: (order: TableColumnOrderState) => updateColumnOrder(order),
|
|
947
|
+
resetColumnOrder: () => updateColumnOrder([]),
|
|
948
|
+
// Exporting
|
|
949
|
+
exportToCsv: (options?: TableExportOptions) => {
|
|
950
|
+
const targetRows = options?.selectedOnly ? tableApi.getSelectedRows() : tableApi.getFlatRows()
|
|
951
|
+
const colsToExport = resolvedColumns().filter((col) => {
|
|
952
|
+
const id = String(col.id || col.accessorKey || "")
|
|
953
|
+
if (id === "select" || id === "actions" || id === "reorder" || id === "dragHandle")
|
|
954
|
+
return false
|
|
955
|
+
if (options?.columns && options.columns.length > 0) {
|
|
956
|
+
return options.columns.includes(id)
|
|
957
|
+
}
|
|
958
|
+
return currentColumnVisibility()[id] !== false
|
|
959
|
+
})
|
|
960
|
+
|
|
961
|
+
const escapeCsv = (val: any): string => {
|
|
962
|
+
if (val === undefined || val === null) return ""
|
|
963
|
+
const str = String(val)
|
|
964
|
+
if (str.includes(",") || str.includes('"') || str.includes("\n") || str.includes("\r")) {
|
|
965
|
+
return `"${str.replace(/"/g, '""')}"`
|
|
966
|
+
}
|
|
967
|
+
return str
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const headers = colsToExport.map((c) => {
|
|
971
|
+
const label = typeof c.header === "string" ? c.header : String(c.id || c.accessorKey || "")
|
|
972
|
+
return escapeCsv(label)
|
|
973
|
+
})
|
|
974
|
+
|
|
975
|
+
const lines: string[] = [headers.join(",")]
|
|
976
|
+
|
|
977
|
+
for (const row of targetRows) {
|
|
978
|
+
const rowVals = colsToExport.map((col) => {
|
|
979
|
+
let val: any
|
|
980
|
+
if (typeof col.accessorFn === "function") {
|
|
981
|
+
val = col.accessorFn(row.original)
|
|
982
|
+
} else {
|
|
983
|
+
const key = String(col.accessorKey || col.id || "")
|
|
984
|
+
val = row.getValue(key)
|
|
985
|
+
}
|
|
986
|
+
return escapeCsv(val)
|
|
987
|
+
})
|
|
988
|
+
lines.push(rowVals.join(","))
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const csvContent = lines.join("\r\n")
|
|
992
|
+
|
|
993
|
+
if (!isServer && options?.filename) {
|
|
994
|
+
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" })
|
|
995
|
+
const url = URL.createObjectURL(blob)
|
|
996
|
+
const link = document.createElement("a")
|
|
997
|
+
link.href = url
|
|
998
|
+
link.download = options.filename.endsWith(".csv")
|
|
999
|
+
? options.filename
|
|
1000
|
+
: `${options.filename}.csv`
|
|
1001
|
+
link.click()
|
|
1002
|
+
URL.revokeObjectURL(url)
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
return csvContent
|
|
1006
|
+
},
|
|
1007
|
+
exportToJson: (options?: TableExportOptions) => {
|
|
1008
|
+
const targetRows = options?.selectedOnly ? tableApi.getSelectedRows() : tableApi.getFlatRows()
|
|
1009
|
+
const colsToExport = resolvedColumns().filter((col) => {
|
|
1010
|
+
const id = String(col.id || col.accessorKey || "")
|
|
1011
|
+
if (id === "select" || id === "actions" || id === "reorder" || id === "dragHandle")
|
|
1012
|
+
return false
|
|
1013
|
+
if (options?.columns && options.columns.length > 0) {
|
|
1014
|
+
return options.columns.includes(id)
|
|
1015
|
+
}
|
|
1016
|
+
return currentColumnVisibility()[id] !== false
|
|
1017
|
+
})
|
|
1018
|
+
|
|
1019
|
+
const jsonArray = targetRows.map((row) => {
|
|
1020
|
+
const obj: Record<string, any> = {}
|
|
1021
|
+
for (const col of colsToExport) {
|
|
1022
|
+
const key = String(col.id || col.accessorKey || "")
|
|
1023
|
+
let val: any
|
|
1024
|
+
if (typeof col.accessorFn === "function") {
|
|
1025
|
+
val = col.accessorFn(row.original)
|
|
1026
|
+
} else {
|
|
1027
|
+
val = row.getValue(key)
|
|
1028
|
+
}
|
|
1029
|
+
obj[key] = val
|
|
1030
|
+
}
|
|
1031
|
+
return obj
|
|
1032
|
+
})
|
|
1033
|
+
|
|
1034
|
+
const jsonContent = JSON.stringify(jsonArray, null, 2)
|
|
1035
|
+
|
|
1036
|
+
if (!isServer && options?.filename) {
|
|
1037
|
+
const blob = new Blob([jsonContent], { type: "application/json;charset=utf-8;" })
|
|
1038
|
+
const url = URL.createObjectURL(blob)
|
|
1039
|
+
const link = document.createElement("a")
|
|
1040
|
+
link.href = url
|
|
1041
|
+
link.download = options.filename.endsWith(".json")
|
|
1042
|
+
? options.filename
|
|
1043
|
+
: `${options.filename}.json`
|
|
1044
|
+
link.click()
|
|
1045
|
+
URL.revokeObjectURL(url)
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return jsonContent
|
|
1049
|
+
},
|
|
1050
|
+
// Faceted statistics
|
|
1051
|
+
getFacetedUniqueValues: (columnId: string) => {
|
|
1052
|
+
const map = new Map<any, number>()
|
|
1053
|
+
const rows = getAllFlatRows(filteredRows())
|
|
1054
|
+
for (const r of rows) {
|
|
1055
|
+
const val = r.getValue(columnId)
|
|
1056
|
+
map.set(val, (map.get(val) || 0) + 1)
|
|
1057
|
+
}
|
|
1058
|
+
return map
|
|
1059
|
+
},
|
|
1060
|
+
getFacetedMinMaxValues: (columnId: string) => {
|
|
1061
|
+
const rows = getAllFlatRows(filteredRows())
|
|
1062
|
+
let min: number | undefined
|
|
1063
|
+
let max: number | undefined
|
|
1064
|
+
for (const r of rows) {
|
|
1065
|
+
const val = r.getValue(columnId)
|
|
1066
|
+
if (typeof val === "number" && !isNaN(val)) {
|
|
1067
|
+
if (min === undefined || val < min) min = val
|
|
1068
|
+
if (max === undefined || val > max) max = val
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return min !== undefined && max !== undefined ? [min, max] : undefined
|
|
1072
|
+
},
|
|
1073
|
+
// Inline editing support
|
|
1074
|
+
setEditingCell: (cell: TableEditingCellState) => updateEditingCell(cell),
|
|
1075
|
+
getEditingCell: () => currentEditingCell(),
|
|
442
1076
|
getState: () => ({
|
|
443
1077
|
sorting: currentSorting(),
|
|
444
1078
|
globalFilter: currentGlobalFilter(),
|
|
445
1079
|
columnFilters: currentColumnFilters(),
|
|
446
1080
|
columnVisibility: currentColumnVisibility(),
|
|
447
1081
|
columnPinning: currentColumnPinning(),
|
|
1082
|
+
columnSizing: currentColumnSizing(),
|
|
1083
|
+
columnOrder: currentColumnOrder(),
|
|
448
1084
|
rowSelection: currentRowSelection(),
|
|
449
1085
|
expanded: currentExpanded(),
|
|
450
1086
|
rowPinning: currentRowPinning(),
|
|
451
|
-
pagination: currentPagination() || { pageIndex: 0, pageSize: 10 }
|
|
1087
|
+
pagination: currentPagination() || { pageIndex: 0, pageSize: 10 },
|
|
1088
|
+
editingCell: currentEditingCell()
|
|
452
1089
|
})
|
|
453
1090
|
}
|
|
454
1091
|
|
|
@@ -473,12 +1110,17 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
473
1110
|
)
|
|
474
1111
|
|
|
475
1112
|
const showToolbar = createMemo(
|
|
476
|
-
() =>
|
|
1113
|
+
() =>
|
|
1114
|
+
props.searchable ||
|
|
1115
|
+
props.showColumnsToggle ||
|
|
1116
|
+
props.exportable ||
|
|
1117
|
+
!!props.toolbar ||
|
|
1118
|
+
!!props.slots?.["toolbar"]
|
|
477
1119
|
)
|
|
478
1120
|
|
|
479
1121
|
const columnDropdownItems = createMemo(() =>
|
|
480
1122
|
resolvedColumns()
|
|
481
|
-
.filter((col) => col.enableHiding !== false)
|
|
1123
|
+
.filter((col) => col.enableHiding !== false && col.id !== "reorder")
|
|
482
1124
|
.map((col) => {
|
|
483
1125
|
const colId = (col.id || col.accessorKey || "") as string
|
|
484
1126
|
const rawLabel = typeof col.header === "string" ? col.header : colId
|
|
@@ -494,17 +1136,38 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
494
1136
|
})
|
|
495
1137
|
)
|
|
496
1138
|
|
|
1139
|
+
const exportDropdownItems = createMemo(() => [
|
|
1140
|
+
{
|
|
1141
|
+
id: "csv",
|
|
1142
|
+
label: "Export as CSV",
|
|
1143
|
+
icon: "i-lucide-file-spreadsheet",
|
|
1144
|
+
onSelect: () => {
|
|
1145
|
+
tableApi.exportToCsv({ filename: props.exportFilename ?? "export" })
|
|
1146
|
+
}
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
id: "json",
|
|
1150
|
+
label: "Export as JSON",
|
|
1151
|
+
icon: "i-lucide-file-json",
|
|
1152
|
+
onSelect: () => {
|
|
1153
|
+
tableApi.exportToJson({ filename: props.exportFilename ?? "export" })
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
])
|
|
1157
|
+
|
|
497
1158
|
const hasFooters = createMemo(() =>
|
|
498
|
-
visibleColumns().some(
|
|
1159
|
+
visibleColumns().some(
|
|
1160
|
+
(col) =>
|
|
1161
|
+
typeof col.footer === "function" ||
|
|
1162
|
+
col.footer ||
|
|
1163
|
+
props.slots?.[`${String(col.id || col.accessorKey || "")}-footer`]
|
|
1164
|
+
)
|
|
499
1165
|
)
|
|
500
1166
|
|
|
501
1167
|
return (
|
|
502
1168
|
<div class={classes().root} data-slot="root">
|
|
503
1169
|
<Show when={showToolbar()}>
|
|
504
|
-
<div
|
|
505
|
-
class="flex items-center gap-3 px-3.5 py-3 border-b border-default bg-muted/20 overflow-x-auto"
|
|
506
|
-
data-slot="toolbar"
|
|
507
|
-
>
|
|
1170
|
+
<div class={classes().toolbar} data-slot="toolbar">
|
|
508
1171
|
<Show when={props.searchable}>
|
|
509
1172
|
<RLSInput
|
|
510
1173
|
type="search"
|
|
@@ -516,10 +1179,23 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
516
1179
|
/>
|
|
517
1180
|
</Show>
|
|
518
1181
|
|
|
519
|
-
{props.toolbar
|
|
1182
|
+
{props.slots?.["toolbar"]
|
|
1183
|
+
? props.slots["toolbar"]({ table: tableApi })
|
|
1184
|
+
: props.toolbar?.()}
|
|
1185
|
+
|
|
1186
|
+
<div class="ml-auto flex items-center gap-2 shrink-0">
|
|
1187
|
+
<Show when={props.exportable}>
|
|
1188
|
+
<RLSDropdownMenu
|
|
1189
|
+
label="Export"
|
|
1190
|
+
icon="i-lucide-download"
|
|
1191
|
+
items={exportDropdownItems()}
|
|
1192
|
+
content={{ align: "end" }}
|
|
1193
|
+
size="sm"
|
|
1194
|
+
color="neutral"
|
|
1195
|
+
/>
|
|
1196
|
+
</Show>
|
|
520
1197
|
|
|
521
|
-
|
|
522
|
-
<div class="ml-auto shrink-0">
|
|
1198
|
+
<Show when={props.showColumnsToggle}>
|
|
523
1199
|
<RLSDropdownMenu
|
|
524
1200
|
label="Columns"
|
|
525
1201
|
icon="i-lucide-columns"
|
|
@@ -528,16 +1204,47 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
528
1204
|
size="sm"
|
|
529
1205
|
color="neutral"
|
|
530
1206
|
/>
|
|
531
|
-
</
|
|
532
|
-
</
|
|
1207
|
+
</Show>
|
|
1208
|
+
</div>
|
|
533
1209
|
</div>
|
|
534
1210
|
</Show>
|
|
535
1211
|
|
|
536
|
-
<div
|
|
1212
|
+
<div
|
|
1213
|
+
ref={(el) => (containerRef = el)}
|
|
1214
|
+
class="overflow-x-auto w-full relative"
|
|
1215
|
+
style={isVirtual() ? "max-height: 400px; overflow-y: auto;" : undefined}
|
|
1216
|
+
>
|
|
537
1217
|
<table ref={(el) => (tableElementRef = el)} class={classes().base} data-slot="base">
|
|
538
|
-
<
|
|
1218
|
+
<colgroup>
|
|
1219
|
+
<For each={visibleColumns()}>
|
|
1220
|
+
{(col) => {
|
|
1221
|
+
const colId = () => String(col.id || col.accessorKey || "")
|
|
1222
|
+
const sizeVal = () => currentColumnSizing()[colId()] ?? col.size
|
|
1223
|
+
const styles: string[] = []
|
|
1224
|
+
if (typeof sizeVal() === "number") styles.push(`width: ${sizeVal()}px;`)
|
|
1225
|
+
else if (sizeVal()) styles.push(`width: ${sizeVal()};`)
|
|
1226
|
+
if (col.minSize) {
|
|
1227
|
+
styles.push(
|
|
1228
|
+
`min-width: ${typeof col.minSize === "number" ? `${col.minSize}px` : col.minSize};`
|
|
1229
|
+
)
|
|
1230
|
+
}
|
|
1231
|
+
if (col.maxSize) {
|
|
1232
|
+
styles.push(
|
|
1233
|
+
`max-width: ${typeof col.maxSize === "number" ? `${col.maxSize}px` : col.maxSize};`
|
|
1234
|
+
)
|
|
1235
|
+
}
|
|
1236
|
+
return <col data-col-id={colId()} style={styles.join(" ") || undefined} />
|
|
1237
|
+
}}
|
|
1238
|
+
</For>
|
|
1239
|
+
</colgroup>
|
|
1240
|
+
|
|
1241
|
+
<Show when={props.caption || props.captionContent || props.slots?.["caption"]}>
|
|
539
1242
|
<caption class={classes().caption} data-slot="caption">
|
|
540
|
-
{props.
|
|
1243
|
+
{props.slots?.["caption"]
|
|
1244
|
+
? props.slots["caption"]({ table: tableApi })
|
|
1245
|
+
: props.captionContent
|
|
1246
|
+
? props.captionContent()
|
|
1247
|
+
: props.caption}
|
|
541
1248
|
</caption>
|
|
542
1249
|
</Show>
|
|
543
1250
|
|
|
@@ -547,7 +1254,7 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
547
1254
|
<tr class={classes().tr} data-slot="tr">
|
|
548
1255
|
<For each={visibleColumns()}>
|
|
549
1256
|
{(col) => {
|
|
550
|
-
const colId = () => (col.id || col.accessorKey || "")
|
|
1257
|
+
const colId = () => String(col.id || col.accessorKey || "")
|
|
551
1258
|
const pinned = () =>
|
|
552
1259
|
colId() in columnOffsets().left || colId() in columnOffsets().right
|
|
553
1260
|
const pinStyle = () => {
|
|
@@ -559,6 +1266,23 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
559
1266
|
}
|
|
560
1267
|
return ""
|
|
561
1268
|
}
|
|
1269
|
+
const sizeStyle = () => {
|
|
1270
|
+
const sizeVal = currentColumnSizing()[colId()] ?? col.size
|
|
1271
|
+
const styles: string[] = []
|
|
1272
|
+
if (typeof sizeVal === "number") styles.push(`width: ${sizeVal}px;`)
|
|
1273
|
+
else if (sizeVal) styles.push(`width: ${sizeVal};`)
|
|
1274
|
+
if (col.minSize) {
|
|
1275
|
+
styles.push(
|
|
1276
|
+
`min-width: ${typeof col.minSize === "number" ? `${col.minSize}px` : col.minSize};`
|
|
1277
|
+
)
|
|
1278
|
+
}
|
|
1279
|
+
if (col.maxSize) {
|
|
1280
|
+
styles.push(
|
|
1281
|
+
`max-width: ${typeof col.maxSize === "number" ? `${col.maxSize}px` : col.maxSize};`
|
|
1282
|
+
)
|
|
1283
|
+
}
|
|
1284
|
+
return styles.join(" ")
|
|
1285
|
+
}
|
|
562
1286
|
const metaThClass = () =>
|
|
563
1287
|
typeof col.meta?.class?.th === "function"
|
|
564
1288
|
? col.meta.class.th(col)
|
|
@@ -569,18 +1293,40 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
569
1293
|
: col.meta?.style?.th || ""
|
|
570
1294
|
const canSort = () =>
|
|
571
1295
|
col.enableSorting !== false &&
|
|
572
|
-
!!(col.accessorKey || col.id) &&
|
|
1296
|
+
!!(col.accessorKey || col.id || col.accessorFn) &&
|
|
573
1297
|
colId() !== "actions" &&
|
|
574
|
-
colId() !== "select"
|
|
1298
|
+
colId() !== "select" &&
|
|
1299
|
+
colId() !== "reorder" &&
|
|
1300
|
+
colId() !== "dragHandle"
|
|
575
1301
|
|
|
576
1302
|
const activeSort = () => currentSorting().find((s) => s.id === colId())
|
|
577
1303
|
const sortDir = () =>
|
|
578
1304
|
activeSort() ? (activeSort()!.desc ? "desc" : "asc") : "none"
|
|
579
1305
|
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
1306
|
+
const slotHeader = props.slots?.[`${colId()}-header`]
|
|
1307
|
+
|
|
1308
|
+
const isDropTarget = () =>
|
|
1309
|
+
dropTargetCol()?.id === colId() && draggedColId() !== colId()
|
|
1310
|
+
const dropPosition = () =>
|
|
1311
|
+
isDropTarget() ? dropTargetCol()?.position : undefined
|
|
1312
|
+
|
|
1313
|
+
const headerLabel = () => {
|
|
1314
|
+
if (typeof col.header === "function") {
|
|
1315
|
+
return col.header({ column: col, table: tableApi })
|
|
1316
|
+
}
|
|
1317
|
+
if (col.header !== undefined && col.header !== null) {
|
|
1318
|
+
return col.header
|
|
1319
|
+
}
|
|
1320
|
+
if (
|
|
1321
|
+
colId() === "reorder" ||
|
|
1322
|
+
colId() === "select" ||
|
|
1323
|
+
colId() === "actions" ||
|
|
1324
|
+
colId() === "dragHandle"
|
|
1325
|
+
) {
|
|
1326
|
+
return ""
|
|
1327
|
+
}
|
|
1328
|
+
return colId().charAt(0).toUpperCase() + colId().slice(1)
|
|
1329
|
+
}
|
|
584
1330
|
|
|
585
1331
|
return (
|
|
586
1332
|
<th
|
|
@@ -589,36 +1335,115 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
589
1335
|
style={`${sizeStyle()} ${pinStyle()} ${metaThStyle()}`}
|
|
590
1336
|
data-col-id={colId()}
|
|
591
1337
|
data-slot="th"
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
1338
|
+
data-drop-position={dropPosition()}
|
|
1339
|
+
onDragOver={(e) => {
|
|
1340
|
+
if (!props.enableColumnReordering || !draggedColId()) return
|
|
1341
|
+
e.preventDefault()
|
|
1342
|
+
const th = e.currentTarget as HTMLElement
|
|
1343
|
+
const rect = th.getBoundingClientRect()
|
|
1344
|
+
const pos = e.clientX < rect.left + rect.width / 2 ? "left" : "right"
|
|
1345
|
+
const cur = dropTargetCol()
|
|
1346
|
+
if (!cur || cur.id !== colId() || cur.position !== pos) {
|
|
1347
|
+
setDropTargetCol({ id: colId(), position: pos })
|
|
599
1348
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
1349
|
+
}}
|
|
1350
|
+
onDragLeave={() => {
|
|
1351
|
+
if (dropTargetCol()?.id === colId()) {
|
|
1352
|
+
setDropTargetCol(null)
|
|
1353
|
+
}
|
|
1354
|
+
}}
|
|
1355
|
+
onDrop={(e) => {
|
|
1356
|
+
if (!props.enableColumnReordering || !draggedColId()) return
|
|
1357
|
+
e.preventDefault()
|
|
1358
|
+
const sourceId = draggedColId()!
|
|
1359
|
+
const target = dropTargetCol()
|
|
1360
|
+
setDraggedColId(null)
|
|
1361
|
+
setDropTargetCol(null)
|
|
1362
|
+
if (!target || sourceId === target.id) return
|
|
1363
|
+
|
|
1364
|
+
const cols = resolvedColumns().map((c) =>
|
|
1365
|
+
String(c.id || c.accessorKey || "")
|
|
1366
|
+
)
|
|
1367
|
+
const fromIdx = cols.indexOf(sourceId)
|
|
1368
|
+
if (fromIdx === -1) return
|
|
1369
|
+
const newOrder = [...cols]
|
|
1370
|
+
newOrder.splice(fromIdx, 1)
|
|
1371
|
+
let toIdx = newOrder.indexOf(target.id)
|
|
1372
|
+
if (toIdx === -1) return
|
|
1373
|
+
if (target.position === "right") {
|
|
1374
|
+
toIdx += 1
|
|
1375
|
+
}
|
|
1376
|
+
newOrder.splice(toIdx, 0, sourceId)
|
|
1377
|
+
updateColumnOrder(newOrder)
|
|
1378
|
+
}}
|
|
1379
|
+
>
|
|
1380
|
+
<div class="flex items-center gap-1.5">
|
|
1381
|
+
{/* Column Drag Handle */}
|
|
1382
|
+
<Show
|
|
1383
|
+
when={
|
|
1384
|
+
props.enableColumnReordering &&
|
|
1385
|
+
colId() !== "reorder" &&
|
|
1386
|
+
colId() !== "select" &&
|
|
1387
|
+
colId() !== "actions" &&
|
|
1388
|
+
colId() !== "dragHandle"
|
|
1389
|
+
}
|
|
606
1390
|
>
|
|
607
|
-
<span>
|
|
608
|
-
{typeof col.header === "function"
|
|
609
|
-
? col.header({ column: col, table: tableApi })
|
|
610
|
-
: col.header || colId()}
|
|
611
|
-
</span>
|
|
612
1391
|
<span
|
|
613
|
-
class={
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
1392
|
+
class={classes().dragHandle}
|
|
1393
|
+
data-slot="drag-handle"
|
|
1394
|
+
draggable={true}
|
|
1395
|
+
onDragStart={(e) => {
|
|
1396
|
+
setDraggedColId(colId())
|
|
1397
|
+
e.dataTransfer?.setData("text/plain", colId())
|
|
1398
|
+
if (e.dataTransfer) {
|
|
1399
|
+
e.dataTransfer.effectAllowed = "move"
|
|
1400
|
+
}
|
|
1401
|
+
}}
|
|
1402
|
+
onDragEnd={() => {
|
|
1403
|
+
setDraggedColId(null)
|
|
1404
|
+
setDropTargetCol(null)
|
|
1405
|
+
}}
|
|
1406
|
+
>
|
|
1407
|
+
<span class="i-lucide-grip-vertical size-3.5 opacity-40 hover:opacity-100" />
|
|
1408
|
+
</span>
|
|
1409
|
+
</Show>
|
|
1410
|
+
|
|
1411
|
+
<div class="flex-1 min-w-0">
|
|
1412
|
+
<Show
|
|
1413
|
+
when={!slotHeader}
|
|
1414
|
+
fallback={slotHeader?.({ column: col, table: tableApi })}
|
|
1415
|
+
>
|
|
1416
|
+
<Show when={canSort()} fallback={<span>{headerLabel()}</span>}>
|
|
1417
|
+
<button
|
|
1418
|
+
type="button"
|
|
1419
|
+
class="inline-flex items-center gap-1.5 hover:text-highlighted focus:outline-none transition-colors cursor-pointer group -mx-1 px-1 py-0.5 rounded"
|
|
1420
|
+
onClick={(e) => toggleSort(colId(), e.shiftKey)}
|
|
1421
|
+
aria-label={`Sort by ${colId()}`}
|
|
1422
|
+
>
|
|
1423
|
+
<span>{headerLabel()}</span>
|
|
1424
|
+
<span
|
|
1425
|
+
class={`size-3.5 transition-all shrink-0 ${
|
|
1426
|
+
sortDir() === "asc"
|
|
1427
|
+
? "i-lucide-arrow-up-narrow-wide text-primary"
|
|
1428
|
+
: sortDir() === "desc"
|
|
1429
|
+
? "i-lucide-arrow-down-wide-narrow text-primary"
|
|
1430
|
+
: "i-lucide-arrow-up-down opacity-40 group-hover:opacity-100"
|
|
1431
|
+
}`}
|
|
1432
|
+
/>
|
|
1433
|
+
</button>
|
|
1434
|
+
</Show>
|
|
1435
|
+
</Show>
|
|
1436
|
+
</div>
|
|
1437
|
+
</div>
|
|
1438
|
+
|
|
1439
|
+
{/* Column Resizer Handle */}
|
|
1440
|
+
<Show when={props.enableColumnResizing}>
|
|
1441
|
+
<div
|
|
1442
|
+
class={classes().resizer}
|
|
1443
|
+
data-slot="resizer"
|
|
1444
|
+
data-resizing={resizingColId() === colId() ? "true" : undefined}
|
|
1445
|
+
onPointerDown={(e) => startResize(e, colId(), col.minSize, col.maxSize)}
|
|
1446
|
+
/>
|
|
622
1447
|
</Show>
|
|
623
1448
|
</th>
|
|
624
1449
|
)
|
|
@@ -630,7 +1455,9 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
630
1455
|
|
|
631
1456
|
{/* ── tbody ── */}
|
|
632
1457
|
<tbody class={classes().tbody} data-slot="tbody">
|
|
633
|
-
{props.
|
|
1458
|
+
{props.slots?.["body-top"]
|
|
1459
|
+
? props.slots["body-top"]({ table: tableApi })
|
|
1460
|
+
: props.bodyTop?.()}
|
|
634
1461
|
|
|
635
1462
|
<Show
|
|
636
1463
|
when={!props.loading}
|
|
@@ -642,7 +1469,7 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
642
1469
|
data-slot="loading"
|
|
643
1470
|
>
|
|
644
1471
|
<Show
|
|
645
|
-
when={props.loadingContent}
|
|
1472
|
+
when={props.slots?.["loading"] || props.loadingContent}
|
|
646
1473
|
fallback={
|
|
647
1474
|
<div class="flex items-center justify-center gap-2 text-muted py-4">
|
|
648
1475
|
<RLSIcon
|
|
@@ -653,7 +1480,9 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
653
1480
|
</div>
|
|
654
1481
|
}
|
|
655
1482
|
>
|
|
656
|
-
{props.
|
|
1483
|
+
{props.slots?.["loading"]
|
|
1484
|
+
? props.slots["loading"]({ table: tableApi })
|
|
1485
|
+
: props.loadingContent?.()}
|
|
657
1486
|
</Show>
|
|
658
1487
|
</td>
|
|
659
1488
|
</tr>
|
|
@@ -669,7 +1498,7 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
669
1498
|
data-slot="empty"
|
|
670
1499
|
>
|
|
671
1500
|
<Show
|
|
672
|
-
when={props.emptyContent}
|
|
1501
|
+
when={props.slots?.["empty"] || props.emptyContent}
|
|
673
1502
|
fallback={
|
|
674
1503
|
<div class="flex flex-col items-center justify-center py-6 gap-2 text-muted">
|
|
675
1504
|
<RLSIcon name="i-lucide-inbox" class="size-8 opacity-40" />
|
|
@@ -677,28 +1506,46 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
677
1506
|
</div>
|
|
678
1507
|
}
|
|
679
1508
|
>
|
|
680
|
-
{props.
|
|
1509
|
+
{props.slots?.["empty"]
|
|
1510
|
+
? props.slots["empty"]({ table: tableApi })
|
|
1511
|
+
: props.emptyContent?.()}
|
|
681
1512
|
</Show>
|
|
682
1513
|
</td>
|
|
683
1514
|
</tr>
|
|
684
1515
|
}
|
|
685
1516
|
>
|
|
686
|
-
|
|
1517
|
+
{/* Virtual Top Spacer */}
|
|
1518
|
+
<Show when={isVirtual() && virtualizedView().padTop > 0}>
|
|
1519
|
+
<tr>
|
|
1520
|
+
<td
|
|
1521
|
+
colSpan={Math.max(1, visibleColumns().length)}
|
|
1522
|
+
style={`height: ${virtualizedView().padTop}px; padding: 0; border: 0;`}
|
|
1523
|
+
/>
|
|
1524
|
+
</tr>
|
|
1525
|
+
</Show>
|
|
1526
|
+
|
|
1527
|
+
<For each={virtualizedView().items}>
|
|
687
1528
|
{(item) => {
|
|
688
1529
|
if (item.type === "group") {
|
|
689
|
-
const { group } = item
|
|
1530
|
+
const { group, depth } = item
|
|
1531
|
+
const groupHeaderSlot = props.slots?.["group-header"]
|
|
1532
|
+
|
|
690
1533
|
return (
|
|
691
|
-
<tr class={classes().trGroup} data-slot="tr-group">
|
|
1534
|
+
<tr class={classes().trGroup} data-slot="tr-group" data-depth={depth}>
|
|
692
1535
|
<td
|
|
693
1536
|
colSpan={Math.max(1, visibleColumns().length)}
|
|
694
1537
|
class={classes().tdGroup}
|
|
1538
|
+
style={{ "padding-left": `${depth * 1.5 + 1}rem` }}
|
|
695
1539
|
data-slot="td-group"
|
|
696
1540
|
>
|
|
697
1541
|
<Show
|
|
698
|
-
when={props.groupHeader}
|
|
1542
|
+
when={groupHeaderSlot || props.groupHeader}
|
|
699
1543
|
fallback={
|
|
700
1544
|
<span class="flex items-center gap-2">
|
|
701
1545
|
<span class="i-lucide-layers size-3.5 text-primary opacity-70" />
|
|
1546
|
+
<span class="text-xs uppercase text-muted font-medium">
|
|
1547
|
+
{group.columnId}:
|
|
1548
|
+
</span>
|
|
702
1549
|
<span class="text-highlighted">{String(group.value)}</span>
|
|
703
1550
|
<span class="ml-auto text-muted font-normal normal-case">
|
|
704
1551
|
{group.rows.length} {group.rows.length === 1 ? "row" : "rows"}
|
|
@@ -706,7 +1553,13 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
706
1553
|
</span>
|
|
707
1554
|
}
|
|
708
1555
|
>
|
|
709
|
-
{
|
|
1556
|
+
{groupHeaderSlot
|
|
1557
|
+
? groupHeaderSlot({
|
|
1558
|
+
group,
|
|
1559
|
+
colSpan: visibleColumns().length,
|
|
1560
|
+
table: tableApi
|
|
1561
|
+
})
|
|
1562
|
+
: props.groupHeader?.(group, visibleColumns().length)}
|
|
710
1563
|
</Show>
|
|
711
1564
|
</td>
|
|
712
1565
|
</tr>
|
|
@@ -723,6 +1576,13 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
723
1576
|
? props.meta.style.tr(row)
|
|
724
1577
|
: props.meta?.style?.tr || ""
|
|
725
1578
|
|
|
1579
|
+
const isRowDropTarget = () =>
|
|
1580
|
+
dropTargetRow()?.id === row.id &&
|
|
1581
|
+
draggedRowIndex() !== null &&
|
|
1582
|
+
draggedRowIndex() !== row.index
|
|
1583
|
+
const rowDropPos = () =>
|
|
1584
|
+
isRowDropTarget() ? dropTargetRow()?.position : undefined
|
|
1585
|
+
|
|
726
1586
|
return (
|
|
727
1587
|
<>
|
|
728
1588
|
<tr
|
|
@@ -733,6 +1593,51 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
733
1593
|
data-selected={row.getIsSelected() ? "true" : undefined}
|
|
734
1594
|
data-expanded={row.getIsExpanded() ? "true" : undefined}
|
|
735
1595
|
data-pinned={row.getIsPinned() || undefined}
|
|
1596
|
+
data-drop-position={rowDropPos()}
|
|
1597
|
+
onDragOver={(e) => {
|
|
1598
|
+
if (!props.enableRowReordering || draggedRowIndex() === null) return
|
|
1599
|
+
e.preventDefault()
|
|
1600
|
+
const tr = e.currentTarget as HTMLElement
|
|
1601
|
+
const rect = tr.getBoundingClientRect()
|
|
1602
|
+
const pos = e.clientY < rect.top + rect.height / 2 ? "top" : "bottom"
|
|
1603
|
+
const cur = dropTargetRow()
|
|
1604
|
+
if (!cur || cur.id !== row.id || cur.position !== pos) {
|
|
1605
|
+
setDropTargetRow({ id: row.id, index: row.index, position: pos })
|
|
1606
|
+
}
|
|
1607
|
+
}}
|
|
1608
|
+
onDragLeave={() => {
|
|
1609
|
+
if (dropTargetRow()?.id === row.id) {
|
|
1610
|
+
setDropTargetRow(null)
|
|
1611
|
+
}
|
|
1612
|
+
}}
|
|
1613
|
+
onDrop={(e) => {
|
|
1614
|
+
if (
|
|
1615
|
+
!props.enableRowReordering ||
|
|
1616
|
+
draggedRowIndex() === null ||
|
|
1617
|
+
!props.data
|
|
1618
|
+
)
|
|
1619
|
+
return
|
|
1620
|
+
e.preventDefault()
|
|
1621
|
+
const fromIdx = draggedRowIndex()!
|
|
1622
|
+
const target = dropTargetRow()
|
|
1623
|
+
setDraggedRowIndex(null)
|
|
1624
|
+
setDropTargetRow(null)
|
|
1625
|
+
if (!target || fromIdx === target.index) return
|
|
1626
|
+
|
|
1627
|
+
const newData = [...props.data]
|
|
1628
|
+
const [movedItem] = newData.splice(fromIdx, 1)
|
|
1629
|
+
if (movedItem !== undefined) {
|
|
1630
|
+
let toIdx = target.index
|
|
1631
|
+
if (fromIdx < target.index && target.position === "top") {
|
|
1632
|
+
toIdx = target.index - 1
|
|
1633
|
+
} else if (fromIdx > target.index && target.position === "bottom") {
|
|
1634
|
+
toIdx = target.index + 1
|
|
1635
|
+
}
|
|
1636
|
+
toIdx = Math.max(0, Math.min(newData.length, toIdx))
|
|
1637
|
+
newData.splice(toIdx, 0, movedItem)
|
|
1638
|
+
props.onRowReorder?.(newData, fromIdx, toIdx)
|
|
1639
|
+
}
|
|
1640
|
+
}}
|
|
736
1641
|
onClick={(e) => {
|
|
737
1642
|
const target = e.target as HTMLElement
|
|
738
1643
|
if (target.closest("a, button, input, select, textarea, [role=button]"))
|
|
@@ -745,10 +1650,14 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
745
1650
|
>
|
|
746
1651
|
<For each={visibleColumns()}>
|
|
747
1652
|
{(col) => {
|
|
748
|
-
const colId = () => (col.id || col.accessorKey || "")
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
1653
|
+
const colId = () => String(col.id || col.accessorKey || "")
|
|
1654
|
+
const cellValue = () => {
|
|
1655
|
+
if (typeof col.accessorFn === "function") {
|
|
1656
|
+
return col.accessorFn(row.original)
|
|
1657
|
+
}
|
|
1658
|
+
const key = (col.accessorKey || col.id || "") as string
|
|
1659
|
+
return key ? row.getValue(key) : undefined
|
|
1660
|
+
}
|
|
752
1661
|
const pinned = () =>
|
|
753
1662
|
colId() in columnOffsets().left || colId() in columnOffsets().right
|
|
754
1663
|
|
|
@@ -769,6 +1678,24 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
769
1678
|
? col.meta.rowspan.td(row)
|
|
770
1679
|
: col.meta?.rowspan?.td
|
|
771
1680
|
|
|
1681
|
+
const sizeStyle = () => {
|
|
1682
|
+
const sizeVal = currentColumnSizing()[colId()] ?? col.size
|
|
1683
|
+
const styles: string[] = []
|
|
1684
|
+
if (typeof sizeVal === "number") styles.push(`width: ${sizeVal}px;`)
|
|
1685
|
+
else if (sizeVal) styles.push(`width: ${sizeVal};`)
|
|
1686
|
+
if (col.minSize) {
|
|
1687
|
+
styles.push(
|
|
1688
|
+
`min-width: ${typeof col.minSize === "number" ? `${col.minSize}px` : col.minSize};`
|
|
1689
|
+
)
|
|
1690
|
+
}
|
|
1691
|
+
if (col.maxSize) {
|
|
1692
|
+
styles.push(
|
|
1693
|
+
`max-width: ${typeof col.maxSize === "number" ? `${col.maxSize}px` : col.maxSize};`
|
|
1694
|
+
)
|
|
1695
|
+
}
|
|
1696
|
+
return styles.join(" ")
|
|
1697
|
+
}
|
|
1698
|
+
|
|
772
1699
|
const pinStyle = () => {
|
|
773
1700
|
if (colId() in columnOffsets().left) {
|
|
774
1701
|
return `left: ${columnOffsets().left[colId()]}px;`
|
|
@@ -779,26 +1706,41 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
779
1706
|
return ""
|
|
780
1707
|
}
|
|
781
1708
|
|
|
1709
|
+
const isEditing = () =>
|
|
1710
|
+
currentEditingCell()?.rowId === row.id &&
|
|
1711
|
+
currentEditingCell()?.columnId === colId()
|
|
1712
|
+
const setEditing = (editing: boolean) => {
|
|
1713
|
+
tableApi.setEditingCell(
|
|
1714
|
+
editing ? { rowId: row.id, columnId: colId() } : null
|
|
1715
|
+
)
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
const cellSlot = props.slots?.[`${colId()}-cell`]
|
|
1719
|
+
|
|
782
1720
|
return (
|
|
783
1721
|
<td
|
|
784
1722
|
class={`${classes().td} ${pinned() ? "sticky bg-default z-10" : ""} ${metaTdClass()}`}
|
|
785
|
-
style={`${pinStyle()} ${metaTdStyle()}`}
|
|
1723
|
+
style={`${sizeStyle()} ${pinStyle()} ${metaTdStyle()}`}
|
|
786
1724
|
colSpan={colSpanVal()}
|
|
787
1725
|
rowSpan={rowSpanVal()}
|
|
788
1726
|
data-slot="td"
|
|
789
1727
|
data-col-id={colId()}
|
|
790
1728
|
>
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
:
|
|
1729
|
+
<Show
|
|
1730
|
+
when={!cellSlot}
|
|
1731
|
+
fallback={cellSlot?.({
|
|
1732
|
+
row,
|
|
1733
|
+
column: col,
|
|
1734
|
+
cell: { value: cellValue() },
|
|
1735
|
+
getValue: cellValue,
|
|
1736
|
+
renderValue: cellValue,
|
|
1737
|
+
isEditing,
|
|
1738
|
+
setEditing,
|
|
1739
|
+
table: tableApi
|
|
1740
|
+
})}
|
|
1741
|
+
>
|
|
1742
|
+
{renderCellContent(row, col, cellValue, isEditing, setEditing)}
|
|
1743
|
+
</Show>
|
|
802
1744
|
</td>
|
|
803
1745
|
)
|
|
804
1746
|
}}
|
|
@@ -806,14 +1748,22 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
806
1748
|
</tr>
|
|
807
1749
|
|
|
808
1750
|
{/* Expanded sub-row content */}
|
|
809
|
-
<Show
|
|
1751
|
+
<Show
|
|
1752
|
+
when={
|
|
1753
|
+
row.getCanExpand() &&
|
|
1754
|
+
row.getIsExpanded() &&
|
|
1755
|
+
(props.slots?.["expanded"] || props.expandedContent)
|
|
1756
|
+
}
|
|
1757
|
+
>
|
|
810
1758
|
<tr class={classes().trExpanded} data-slot="tr-expanded">
|
|
811
1759
|
<td
|
|
812
1760
|
colSpan={Math.max(1, visibleColumns().length)}
|
|
813
1761
|
class={classes().tdExpanded}
|
|
814
1762
|
data-slot="td-expanded"
|
|
815
1763
|
>
|
|
816
|
-
{props.
|
|
1764
|
+
{props.slots?.["expanded"]
|
|
1765
|
+
? props.slots["expanded"]({ row, table: tableApi })
|
|
1766
|
+
: props.expandedContent?.(row)}
|
|
817
1767
|
</td>
|
|
818
1768
|
</tr>
|
|
819
1769
|
</Show>
|
|
@@ -821,49 +1771,168 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
821
1771
|
{/* Tree / Sub-rows */}
|
|
822
1772
|
<Show when={row.getIsExpanded() && row.subRows && row.subRows.length > 0}>
|
|
823
1773
|
<For each={row.subRows}>
|
|
824
|
-
{(subRow) =>
|
|
825
|
-
|
|
826
|
-
class
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1774
|
+
{(subRow) => {
|
|
1775
|
+
const subMetaClass = () =>
|
|
1776
|
+
typeof props.meta?.class?.tr === "function"
|
|
1777
|
+
? props.meta.class.tr(subRow)
|
|
1778
|
+
: props.meta?.class?.tr || ""
|
|
1779
|
+
const subMetaStyle = () =>
|
|
1780
|
+
typeof props.meta?.style?.tr === "function"
|
|
1781
|
+
? props.meta.style.tr(subRow)
|
|
1782
|
+
: props.meta?.style?.tr || ""
|
|
1783
|
+
|
|
1784
|
+
return (
|
|
1785
|
+
<tr
|
|
1786
|
+
class={`${classes().tr} ${subMetaClass()}`}
|
|
1787
|
+
style={subMetaStyle() || undefined}
|
|
1788
|
+
data-slot="tr"
|
|
1789
|
+
data-row-id={subRow.id}
|
|
1790
|
+
data-depth={subRow.depth}
|
|
1791
|
+
data-selected={subRow.getIsSelected() ? "true" : undefined}
|
|
1792
|
+
onClick={(e) => {
|
|
1793
|
+
const target = e.target as HTMLElement
|
|
1794
|
+
if (
|
|
1795
|
+
target.closest(
|
|
1796
|
+
"a, button, input, select, textarea, [role=button]"
|
|
1797
|
+
)
|
|
1798
|
+
)
|
|
1799
|
+
return
|
|
1800
|
+
props.onSelect?.(e, subRow)
|
|
1801
|
+
}}
|
|
1802
|
+
onMouseEnter={(e) => props.onHover?.(e, subRow)}
|
|
1803
|
+
onMouseLeave={(e) => props.onHover?.(e, null)}
|
|
1804
|
+
onContextMenu={(e) => props.onContextmenu?.(e, subRow)}
|
|
1805
|
+
>
|
|
1806
|
+
<For each={visibleColumns()}>
|
|
1807
|
+
{(col) => {
|
|
1808
|
+
const colId = () => String(col.id || col.accessorKey || "")
|
|
1809
|
+
const cellValue = () => {
|
|
1810
|
+
if (typeof col.accessorFn === "function") {
|
|
1811
|
+
return col.accessorFn(subRow.original)
|
|
1812
|
+
}
|
|
1813
|
+
const key = (col.accessorKey || col.id || "") as string
|
|
1814
|
+
return key ? subRow.getValue(key) : undefined
|
|
1815
|
+
}
|
|
1816
|
+
const pinned = () =>
|
|
1817
|
+
colId() in columnOffsets().left ||
|
|
1818
|
+
colId() in columnOffsets().right
|
|
1819
|
+
|
|
1820
|
+
const metaTdClass = () =>
|
|
1821
|
+
typeof col.meta?.class?.td === "function"
|
|
1822
|
+
? col.meta.class.td(subRow)
|
|
1823
|
+
: col.meta?.class?.td || ""
|
|
1824
|
+
const metaTdStyle = () =>
|
|
1825
|
+
typeof col.meta?.style?.td === "function"
|
|
1826
|
+
? col.meta.style.td(subRow)
|
|
1827
|
+
: col.meta?.style?.td || ""
|
|
1828
|
+
const colSpanVal = () =>
|
|
1829
|
+
typeof col.meta?.colspan?.td === "function"
|
|
1830
|
+
? col.meta.colspan.td(subRow)
|
|
1831
|
+
: col.meta?.colspan?.td
|
|
1832
|
+
const rowSpanVal = () =>
|
|
1833
|
+
typeof col.meta?.rowspan?.td === "function"
|
|
1834
|
+
? col.meta.rowspan.td(subRow)
|
|
1835
|
+
: col.meta?.rowspan?.td
|
|
1836
|
+
|
|
1837
|
+
const sizeStyle = () => {
|
|
1838
|
+
const sizeVal = currentColumnSizing()[colId()] ?? col.size
|
|
1839
|
+
const styles: string[] = []
|
|
1840
|
+
if (typeof sizeVal === "number")
|
|
1841
|
+
styles.push(`width: ${sizeVal}px;`)
|
|
1842
|
+
else if (sizeVal) styles.push(`width: ${sizeVal};`)
|
|
1843
|
+
if (col.minSize) {
|
|
1844
|
+
styles.push(
|
|
1845
|
+
`min-width: ${typeof col.minSize === "number" ? `${col.minSize}px` : col.minSize};`
|
|
1846
|
+
)
|
|
1847
|
+
}
|
|
1848
|
+
if (col.maxSize) {
|
|
1849
|
+
styles.push(
|
|
1850
|
+
`max-width: ${typeof col.maxSize === "number" ? `${col.maxSize}px` : col.maxSize};`
|
|
1851
|
+
)
|
|
1852
|
+
}
|
|
1853
|
+
return styles.join(" ")
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
const pinStyle = () => {
|
|
1857
|
+
if (colId() in columnOffsets().left) {
|
|
1858
|
+
return `left: ${columnOffsets().left[colId()]}px;`
|
|
1859
|
+
}
|
|
1860
|
+
if (colId() in columnOffsets().right) {
|
|
1861
|
+
return `right: ${columnOffsets().right[colId()]}px;`
|
|
1862
|
+
}
|
|
1863
|
+
return ""
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
const isEditing = () =>
|
|
1867
|
+
currentEditingCell()?.rowId === subRow.id &&
|
|
1868
|
+
currentEditingCell()?.columnId === colId()
|
|
1869
|
+
const setEditing = (editing: boolean) => {
|
|
1870
|
+
tableApi.setEditingCell(
|
|
1871
|
+
editing ? { rowId: subRow.id, columnId: colId() } : null
|
|
1872
|
+
)
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
const cellSlot = props.slots?.[`${colId()}-cell`]
|
|
1876
|
+
|
|
1877
|
+
return (
|
|
1878
|
+
<td
|
|
1879
|
+
class={`${classes().td} ${pinned() ? "sticky bg-default z-10" : ""} ${metaTdClass()}`}
|
|
1880
|
+
style={`${sizeStyle()} ${pinStyle()} ${metaTdStyle()}`}
|
|
1881
|
+
colSpan={colSpanVal()}
|
|
1882
|
+
rowSpan={rowSpanVal()}
|
|
1883
|
+
data-slot="td"
|
|
1884
|
+
data-col-id={colId()}
|
|
1885
|
+
>
|
|
1886
|
+
<Show
|
|
1887
|
+
when={!cellSlot}
|
|
1888
|
+
fallback={cellSlot?.({
|
|
842
1889
|
row: subRow,
|
|
1890
|
+
column: col,
|
|
1891
|
+
cell: { value: cellValue() },
|
|
843
1892
|
getValue: cellValue,
|
|
844
1893
|
renderValue: cellValue,
|
|
845
|
-
|
|
1894
|
+
isEditing,
|
|
1895
|
+
setEditing,
|
|
846
1896
|
table: tableApi
|
|
847
|
-
})
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1897
|
+
})}
|
|
1898
|
+
>
|
|
1899
|
+
{renderCellContent(
|
|
1900
|
+
subRow,
|
|
1901
|
+
col,
|
|
1902
|
+
cellValue,
|
|
1903
|
+
isEditing,
|
|
1904
|
+
setEditing
|
|
1905
|
+
)}
|
|
1906
|
+
</Show>
|
|
1907
|
+
</td>
|
|
1908
|
+
)
|
|
1909
|
+
}}
|
|
1910
|
+
</For>
|
|
1911
|
+
</tr>
|
|
1912
|
+
)
|
|
1913
|
+
}}
|
|
857
1914
|
</For>
|
|
858
1915
|
</Show>
|
|
859
1916
|
</>
|
|
860
1917
|
)
|
|
861
1918
|
}}
|
|
862
1919
|
</For>
|
|
1920
|
+
|
|
1921
|
+
{/* Virtual Bottom Spacer */}
|
|
1922
|
+
<Show when={isVirtual() && virtualizedView().padBottom > 0}>
|
|
1923
|
+
<tr>
|
|
1924
|
+
<td
|
|
1925
|
+
colSpan={Math.max(1, visibleColumns().length)}
|
|
1926
|
+
style={`height: ${virtualizedView().padBottom}px; padding: 0; border: 0;`}
|
|
1927
|
+
/>
|
|
1928
|
+
</tr>
|
|
1929
|
+
</Show>
|
|
863
1930
|
</Show>
|
|
864
1931
|
</Show>
|
|
865
1932
|
|
|
866
|
-
{props.
|
|
1933
|
+
{props.slots?.["body-bottom"]
|
|
1934
|
+
? props.slots["body-bottom"]({ table: tableApi })
|
|
1935
|
+
: props.bodyBottom?.()}
|
|
867
1936
|
</tbody>
|
|
868
1937
|
|
|
869
1938
|
{/* ── tfoot ── */}
|
|
@@ -872,11 +1941,19 @@ export function RLSTable<T = any>(props: RLSTableProps<T>) {
|
|
|
872
1941
|
<tr class={classes().tr} data-slot="tr">
|
|
873
1942
|
<For each={visibleColumns()}>
|
|
874
1943
|
{(col) => {
|
|
1944
|
+
const colId = () => String(col.id || col.accessorKey || "")
|
|
1945
|
+
const footerSlot = props.slots?.[`${colId()}-footer`]
|
|
1946
|
+
|
|
875
1947
|
return (
|
|
876
1948
|
<td class={classes().td} data-slot="td">
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
: col
|
|
1949
|
+
<Show
|
|
1950
|
+
when={!footerSlot}
|
|
1951
|
+
fallback={footerSlot?.({ column: col, table: tableApi })}
|
|
1952
|
+
>
|
|
1953
|
+
{typeof col.footer === "function"
|
|
1954
|
+
? col.footer({ column: col, table: tableApi })
|
|
1955
|
+
: col.footer || ""}
|
|
1956
|
+
</Show>
|
|
880
1957
|
</td>
|
|
881
1958
|
)
|
|
882
1959
|
}}
|