@isoftdata/svelte-table 2.10.5 → 2.11.1
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/README.md +388 -388
- package/dist/DraggableRows.svelte +285 -285
- package/dist/Pagination.svelte +303 -303
- package/dist/Table.svelte +1165 -1161
- package/dist/Table.svelte.d.ts +3 -2
- package/dist/Td.svelte +171 -171
- package/dist/TreeRow.svelte +170 -170
- package/package.json +1 -1
package/dist/Table.svelte
CHANGED
|
@@ -1,1161 +1,1165 @@
|
|
|
1
|
-
<script module>
|
|
2
|
-
import { getBootstrapCdnVersion } from '@isoftdata/utility-bootstrap'
|
|
3
|
-
let bs5 = $state(false)
|
|
4
|
-
try {
|
|
5
|
-
bs5 = getBootstrapCdnVersion() === 5
|
|
6
|
-
} catch (e) {
|
|
7
|
-
onMount(() => {
|
|
8
|
-
bs5 = getBootstrapCdnVersion() === 5
|
|
9
|
-
})
|
|
10
|
-
}
|
|
11
|
-
</script>
|
|
12
|
-
|
|
13
|
-
<script
|
|
14
|
-
lang="ts"
|
|
15
|
-
generics="R extends UuidRowProps"
|
|
16
|
-
>
|
|
17
|
-
import type { Snippet } from 'svelte'
|
|
18
|
-
|
|
19
|
-
import { ColumnInfoRunicStore } from './column-info-store.svelte'
|
|
20
|
-
|
|
21
|
-
import type { i18n } from 'i18next'
|
|
22
|
-
import type { Get } from 'type-fest'
|
|
23
|
-
import {
|
|
24
|
-
type Column as GenericColumn,
|
|
25
|
-
type SortDirection,
|
|
26
|
-
type UuidRowProps,
|
|
27
|
-
type RowProperties,
|
|
28
|
-
type FooterReducerValue,
|
|
29
|
-
ARBITRARY_PROPERTY_PREFIX,
|
|
30
|
-
type ArbitraryProperty,
|
|
31
|
-
} from './'
|
|
32
|
-
|
|
33
|
-
import { writable, type Writable } from 'svelte/store'
|
|
34
|
-
|
|
35
|
-
import Pagination from './Pagination.svelte'
|
|
36
|
-
import Td from './Td.svelte'
|
|
37
|
-
import Input from '@isoftdata/svelte-input'
|
|
38
|
-
import ContextMenu, { DropdownItem } from '@isoftdata/svelte-context-menu'
|
|
39
|
-
import { v4 as uuid } from '@lukeed/uuid'
|
|
40
|
-
import { format as formatCurrency } from '@isoftdata/utility-currency'
|
|
41
|
-
import { setContext, tick, onMount, getContext, untrack } from 'svelte'
|
|
42
|
-
import { on } from 'svelte/events'
|
|
43
|
-
import naturalCompare from 'natural-compare-lite'
|
|
44
|
-
import type { HTMLTableAttributes, ClassValue } from 'svelte/elements'
|
|
45
|
-
import { translate as defaultTranslate } from '@isoftdata/utility-string'
|
|
46
|
-
import Icon from '@isoftdata/svelte-icon'
|
|
47
|
-
|
|
48
|
-
const { t: translate } = getContext<i18n>('i18next') || { t: defaultTranslate }
|
|
49
|
-
|
|
50
|
-
type K = keyof R
|
|
51
|
-
type KeyPath = RowProperties<R>
|
|
52
|
-
type ColumnProperty = KeyPath | ArbitraryProperty
|
|
53
|
-
|
|
54
|
-
type IndexedRow = R & { originalIndex: number; uuid: string }
|
|
55
|
-
type GetRowProperty = Get<R, KeyPath>
|
|
56
|
-
type GetIndexedRowProperty = Get<IndexedRow, RowProperties<IndexedRow>>
|
|
57
|
-
type FooterValue = {
|
|
58
|
-
property: ColumnProperty
|
|
59
|
-
value: FooterReducerValue<R>
|
|
60
|
-
}
|
|
61
|
-
// Type hack so passing an array of columns without the type argument doesn't reduce the type of R to any
|
|
62
|
-
type Column = GenericColumn<R & {}>
|
|
63
|
-
|
|
64
|
-
interface Props extends Omit<HTMLTableAttributes, 'children' | `aria-${string}` | `bind:${string}` | `on:${string}`> {
|
|
65
|
-
filterEnabled?: boolean
|
|
66
|
-
columns: Array<Column>
|
|
67
|
-
tableId?: string
|
|
68
|
-
rows: Array<R>
|
|
69
|
-
sortDirection?: SortDirection
|
|
70
|
-
sortColumn?: Column | undefined
|
|
71
|
-
class?: ClassValue
|
|
72
|
-
perPageCount?: number
|
|
73
|
-
currentPageNumber?: number
|
|
74
|
-
showFooter?: boolean
|
|
75
|
-
footers?: Array<FooterValue>
|
|
76
|
-
lazySort?: boolean //when true, the table can only be resorted when the user clicks on a column header(not real time as data changes)
|
|
77
|
-
idProp?: K | 'uuid' //required for lazy sorting.
|
|
78
|
-
previousSortOrder?: Array<IndexedRow & { order: number }>
|
|
79
|
-
filterProps?: Array<KeyPath> | false
|
|
80
|
-
filter?: string
|
|
81
|
-
filterLabel?: string
|
|
82
|
-
filterPlaceholder?: string
|
|
83
|
-
filterDisabled?: boolean
|
|
84
|
-
filterReadonly?: boolean
|
|
85
|
-
filterLazy?: boolean | number
|
|
86
|
-
showFilterLabel?: boolean
|
|
87
|
-
headerRowClass?: ClassValue
|
|
88
|
-
headerColumnClass?: ClassValue
|
|
89
|
-
filterColumnClass?: ClassValue
|
|
90
|
-
columnHidingEnabled?: boolean
|
|
91
|
-
selectionEnabled?: boolean
|
|
92
|
-
// Array<number> | Array<string> | Array<R[K]> ???
|
|
93
|
-
selectedRowIds?: Array<GetIndexedRowProperty>
|
|
94
|
-
rowSelectionIdProp?: KeyPath | 'uuid'
|
|
95
|
-
rowSelectionRequiresModKey?: boolean
|
|
96
|
-
selectionMode?: 'SINGLE' | 'RANGE' | null
|
|
97
|
-
multiSelectEnabled?: boolean
|
|
98
|
-
lastClickedIdForRange?: GetIndexedRowProperty | undefined
|
|
99
|
-
totalItemsCount?: number
|
|
100
|
-
stickyHeader?: boolean
|
|
101
|
-
responsive?: boolean | 'sm' | 'md' | 'lg' | 'xl'
|
|
102
|
-
size?: 'sm' | ''
|
|
103
|
-
striped?: boolean
|
|
104
|
-
/** When true, enables tree-specific features. Required if you want to use `TreeRow`s. */
|
|
105
|
-
tree?: boolean
|
|
106
|
-
bordered?: boolean
|
|
107
|
-
hover?: boolean
|
|
108
|
-
previousSortColumn?: Column | undefined
|
|
109
|
-
previousSortDirection?: SortDirection | undefined
|
|
110
|
-
parentClass?: ClassValue
|
|
111
|
-
parentStyle?: string
|
|
112
|
-
hideButtonClass?: ClassValue
|
|
113
|
-
columnResizingEnabled?: boolean
|
|
114
|
-
columnPinningEnabled?: boolean
|
|
115
|
-
/** If specified, where to serialize the columnInfo store */
|
|
116
|
-
localStorageKey?: string | undefined
|
|
117
|
-
columnClickedMethod?: typeof defaultColumnClicked
|
|
118
|
-
filterMethod?: typeof defaultFilter
|
|
119
|
-
rowMatchesFilterMethod?: typeof defaultRowMatchesFilter
|
|
120
|
-
filteredRows?: Array<IndexedRow>
|
|
121
|
-
sortedRows?: Array<IndexedRow>
|
|
122
|
-
currentPageRows?: Array<IndexedRow>
|
|
123
|
-
// snippets
|
|
124
|
-
header?: Snippet
|
|
125
|
-
body?: Snippet<[{ rows: Array<R & { originalIndex: number; uuid: string }>; visibleColumnsCount: number }]>
|
|
126
|
-
children?: Snippet<[{ row: R & { originalIndex: number; uuid: string }; index: number }]>
|
|
127
|
-
noRows?: Snippet<[{ visibleColumnsCount: number }]>
|
|
128
|
-
footerRow?: Snippet<[{ footers: Array<FooterValue> }]>
|
|
129
|
-
// callbacks
|
|
130
|
-
pageChange?: (context: { pageNumber: number }) => void
|
|
131
|
-
columnResizeEnd?: (context: { column: Column }) => void
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const uid = $props.id()
|
|
135
|
-
|
|
136
|
-
let {
|
|
137
|
-
filterEnabled = false,
|
|
138
|
-
columns,
|
|
139
|
-
tableId = uid,
|
|
140
|
-
rows,
|
|
141
|
-
sortDirection = $bindable('ASC'),
|
|
142
|
-
sortColumn = $bindable(undefined),
|
|
143
|
-
class: tableClass = '',
|
|
144
|
-
perPageCount = 0,
|
|
145
|
-
currentPageNumber = $bindable(1),
|
|
146
|
-
showFooter = false,
|
|
147
|
-
lazySort = false,
|
|
148
|
-
idProp = 'uuid',
|
|
149
|
-
previousSortOrder = $bindable([]),
|
|
150
|
-
filterProps = false,
|
|
151
|
-
filter = $bindable(''),
|
|
152
|
-
filterLabel = translate('common:filter', 'Filter'),
|
|
153
|
-
filterPlaceholder = translate('common:filter', 'Filter'),
|
|
154
|
-
filterDisabled = false,
|
|
155
|
-
filterReadonly = false,
|
|
156
|
-
filterLazy = false,
|
|
157
|
-
showFilterLabel = false,
|
|
158
|
-
headerRowClass = bs5 ? 'row' : 'form-row',
|
|
159
|
-
headerColumnClass = 'col',
|
|
160
|
-
filterColumnClass = 'col-lg-2 col-md-4 col-sm-6 align-self-end',
|
|
161
|
-
columnHidingEnabled = false,
|
|
162
|
-
selectionEnabled = false,
|
|
163
|
-
selectedRowIds = $bindable([]),
|
|
164
|
-
rowSelectionIdProp = 'uuid',
|
|
165
|
-
rowSelectionRequiresModKey = false,
|
|
166
|
-
selectionMode = $bindable(null),
|
|
167
|
-
multiSelectEnabled = true,
|
|
168
|
-
lastClickedIdForRange = $bindable(undefined),
|
|
169
|
-
totalItemsCount = 0,
|
|
170
|
-
stickyHeader = false,
|
|
171
|
-
responsive = false,
|
|
172
|
-
size = 'sm',
|
|
173
|
-
striped = true,
|
|
174
|
-
tree = false,
|
|
175
|
-
bordered = true,
|
|
176
|
-
hover = true,
|
|
177
|
-
previousSortColumn = $bindable(undefined),
|
|
178
|
-
previousSortDirection = $bindable(undefined),
|
|
179
|
-
parentClass = '',
|
|
180
|
-
parentStyle = '',
|
|
181
|
-
hideButtonClass = '',
|
|
182
|
-
columnResizingEnabled = false,
|
|
183
|
-
columnPinningEnabled = false,
|
|
184
|
-
localStorageKey = undefined,
|
|
185
|
-
columnClickedMethod = defaultColumnClicked,
|
|
186
|
-
filterMethod = defaultFilter,
|
|
187
|
-
rowMatchesFilterMethod = defaultRowMatchesFilter,
|
|
188
|
-
filteredRows = $bindable(filterMethod('', rows, columns)),
|
|
189
|
-
sortedRows = $bindable(filteredRows),
|
|
190
|
-
currentPageRows = $bindable(sortedRows.slice(0, perPageCount)),
|
|
191
|
-
// snippets
|
|
192
|
-
header,
|
|
193
|
-
body,
|
|
194
|
-
children,
|
|
195
|
-
noRows,
|
|
196
|
-
footerRow,
|
|
197
|
-
// callbacks
|
|
198
|
-
pageChange,
|
|
199
|
-
columnResizeEnd,
|
|
200
|
-
...rest
|
|
201
|
-
}: Props = $props()
|
|
202
|
-
|
|
203
|
-
let lastPageNumber: number = $state(1)
|
|
204
|
-
let paginationComponent: Pagination<IndexedRow> | undefined = $state()
|
|
205
|
-
let contextMenu: ContextMenu | undefined = $state()
|
|
206
|
-
let contextMenuColumn: Column | undefined = $state()
|
|
207
|
-
let columnResizingIsActive: boolean = $state(false)
|
|
208
|
-
/** Set to true after getting initial table widths when resizing enabled*/
|
|
209
|
-
let useFixedLayout = $state(false)
|
|
210
|
-
let tableParent: HTMLDivElement | undefined = $state()
|
|
211
|
-
|
|
212
|
-
// Do the initial sort if they specified a default sort column
|
|
213
|
-
// svelte-ignore state_referenced_locally
|
|
214
|
-
const defaultSortColumn = columns.find(column => column.defaultSortColumn)
|
|
215
|
-
if (defaultSortColumn) {
|
|
216
|
-
sortColumn = defaultSortColumn
|
|
217
|
-
sortDirection = defaultSortColumn.defaultSortDirection || 'ASC'
|
|
218
|
-
|
|
219
|
-
doSort({
|
|
220
|
-
rows: filteredRows,
|
|
221
|
-
sortColumn,
|
|
222
|
-
sortDirection,
|
|
223
|
-
sameSortOrder: false,
|
|
224
|
-
})
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// Need to track which rows are expanded so when the rows reorder, they correctly keep that state.
|
|
228
|
-
const expandedRows = writable<Record<number | string, boolean>>({})
|
|
229
|
-
// svelte-ignore state_referenced_locally
|
|
230
|
-
if (tree) {
|
|
231
|
-
setContext('expandedRows', expandedRows)
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// TODO: bad things will happen if we update any of our apps to use Runes for the store if we don't account for that here
|
|
235
|
-
const session = getContext<Writable<{ userAccountId?: number }> | undefined>('session')
|
|
236
|
-
// This will be accessible on the component instance, ie `const columnInfo = $derived(table?.columnInfo)` if you need it
|
|
237
|
-
export const columnInfo = new ColumnInfoRunicStore<R>(() => columns, {
|
|
238
|
-
// svelte-ignore state_referenced_locally
|
|
239
|
-
key: localStorageKey,
|
|
240
|
-
userAccountId: session && $session ? $session.userAccountId : undefined,
|
|
241
|
-
})
|
|
242
|
-
// Set as context so we can retrieve it in the Td component
|
|
243
|
-
setContext('columnInfo', columnInfo)
|
|
244
|
-
// Set as context so we can retrieve it in the TreeRow component
|
|
245
|
-
const selectedRowIdsStore = writable(selectedRowIds)
|
|
246
|
-
setContext('selectedRowIds', selectedRowIdsStore)
|
|
247
|
-
// svelte-ignore state_referenced_locally
|
|
248
|
-
setContext('idProp', idProp)
|
|
249
|
-
// svelte-ignore state_referenced_locally
|
|
250
|
-
setContext('columnResizingEnabled', writable(columnResizingEnabled))
|
|
251
|
-
setContext('bs5', bs5)
|
|
252
|
-
|
|
253
|
-
// #endregion
|
|
254
|
-
// #region Functions
|
|
255
|
-
|
|
256
|
-
function isArbitraryProperty(property: string | undefined): property is ArbitraryProperty {
|
|
257
|
-
return !!property?.startsWith(ARBITRARY_PROPERTY_PREFIX)
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export async function defaultColumnClicked(clickedColumn: Column, sortDirection: SortDirection) {
|
|
261
|
-
if (currentPageNumber !== lastPageNumber) {
|
|
262
|
-
paginationComponent?.setPageNumber(1)
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
doSort({
|
|
266
|
-
rows: filteredRows,
|
|
267
|
-
sortColumn: clickedColumn,
|
|
268
|
-
sortDirection,
|
|
269
|
-
sameSortOrder: false,
|
|
270
|
-
})
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function defaultFilter(filter: string, rows: R[], columns: Column[]): IndexedRow[] {
|
|
274
|
-
const columnProps = columns
|
|
275
|
-
?.map(({ property }) => property)
|
|
276
|
-
.filter((property): property is KeyPath => !isArbitraryProperty(property))
|
|
277
|
-
|
|
278
|
-
if (filterEnabled) {
|
|
279
|
-
const props = filterProps || columnProps
|
|
280
|
-
return getFilterMatches(filter, rows, props)
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
return rows.map((row, index): IndexedRow => ({ ...row, originalIndex: index, uuid: row.uuid || uuid() }))
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/** In normal mode, returns all rows matching the filter.
|
|
287
|
-
*
|
|
288
|
-
* In tree mode, shows all children if parent matches, or shows matching children and their children
|
|
289
|
-
*/
|
|
290
|
-
function getFilterMatches(filter: string, theRows: Array<R>, props: KeyPath[]) {
|
|
291
|
-
return theRows.reduce((rows, row, index): IndexedRow[] => {
|
|
292
|
-
if (rowMatchesFilterMethod(filter, row, props)) {
|
|
293
|
-
rows.push({ ...row, originalIndex: index, uuid: 'uuid' in row ? (row.uuid as string) : uuid() })
|
|
294
|
-
} else if (tree && Array.isArray(row.children) && row.children.length) {
|
|
295
|
-
const children = getFilterMatches(filter, row.children, props)
|
|
296
|
-
if (children.length) {
|
|
297
|
-
rows.push({ ...row, children, originalIndex: index, uuid: 'uuid' in row ? (row.uuid as string) : uuid() })
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
return rows
|
|
302
|
-
}, new Array<IndexedRow>())
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
export function defaultRowMatchesFilter(filter: string, row: R, props: Array<RowProperties<R>>) {
|
|
306
|
-
return (
|
|
307
|
-
!filter ||
|
|
308
|
-
props.some(prop => {
|
|
309
|
-
const value = getNestedProperty(row, prop)
|
|
310
|
-
if (typeof value === 'string' || typeof value === 'number') {
|
|
311
|
-
return value.toString().toUpperCase().indexOf(filter.toUpperCase()) > -1
|
|
312
|
-
}
|
|
313
|
-
return false
|
|
314
|
-
})
|
|
315
|
-
)
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
export function getNestedProperty(
|
|
319
|
-
row: R & { uuid: string },
|
|
320
|
-
path: KeyPath | 'uuid',
|
|
321
|
-
defaultValue?: string | number,
|
|
322
|
-
): GetIndexedRowProperty
|
|
323
|
-
export function getNestedProperty(row: R, path: KeyPath, defaultValue?: string | number): GetRowProperty
|
|
324
|
-
export function getNestedProperty(
|
|
325
|
-
row: R | (R & { uuid: string }),
|
|
326
|
-
path: KeyPath | 'uuid',
|
|
327
|
-
defaultValue?: string | number,
|
|
328
|
-
): GetRowProperty | GetIndexedRowProperty {
|
|
329
|
-
if (!path) {
|
|
330
|
-
return defaultValue as GetRowProperty
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (row[path]) {
|
|
334
|
-
return row[path] as GetRowProperty
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
const val =
|
|
338
|
-
path
|
|
339
|
-
.split('[')
|
|
340
|
-
// TODO figure out if we can do this less evilly
|
|
341
|
-
.reduce((obj, prop) => (obj as any)?.[prop.replace(/\]/g, '')], row as any) ?? defaultValue
|
|
342
|
-
return val
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
async function columnClickHandler(clickedColumn: Column) {
|
|
346
|
-
if (clickedColumn.sortType === false || columnResizingIsActive || isArbitraryProperty(clickedColumn.property)) {
|
|
347
|
-
return
|
|
348
|
-
}
|
|
349
|
-
sortDirection = sortDirection === 'ASC' && clickedColumn.property === sortColumn?.property ? 'DESC' : 'ASC'
|
|
350
|
-
|
|
351
|
-
columnClickedMethod(clickedColumn, sortDirection)
|
|
352
|
-
|
|
353
|
-
sortColumn = clickedColumn
|
|
354
|
-
await tick()
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
export function expandRow(rowId: string | number, expanded = true) {
|
|
358
|
-
if (tree) {
|
|
359
|
-
$expandedRows[rowId] = expanded
|
|
360
|
-
} else {
|
|
361
|
-
console.warn('expandRow should only be used when the `tree` prop is set to true on the Table component.')
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
export function rowIsSelected(
|
|
366
|
-
row: IndexedRow,
|
|
367
|
-
rowSelectionIdProp: KeyPath | 'uuid',
|
|
368
|
-
selectedRowIds: Array<GetIndexedRowProperty>,
|
|
369
|
-
) {
|
|
370
|
-
const selectionId = getNestedProperty(row, rowSelectionIdProp, row.uuid)
|
|
371
|
-
return selectedRowIds.includes(selectionId)
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
export function rowClick(row: IndexedRow) {
|
|
375
|
-
if (!selectionEnabled || !rowSelectionIdProp) {
|
|
376
|
-
return
|
|
377
|
-
}
|
|
378
|
-
const clickedId = getNestedProperty(row, rowSelectionIdProp || 'uuid')
|
|
379
|
-
|
|
380
|
-
const selectedIds = selectedRowIds
|
|
381
|
-
|
|
382
|
-
let newSelectedIds: Array<GetIndexedRowProperty> = []
|
|
383
|
-
|
|
384
|
-
if (!selectionMode && !rowSelectionRequiresModKey) {
|
|
385
|
-
selectionMode = 'SINGLE'
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
if (selectionMode === 'SINGLE') {
|
|
389
|
-
newSelectedIds = selectedIds
|
|
390
|
-
const foundInExistingSelections = selectedIds.some(id => id === clickedId)
|
|
391
|
-
|
|
392
|
-
if (foundInExistingSelections) {
|
|
393
|
-
newSelectedIds = newSelectedIds.filter(id => id !== clickedId)
|
|
394
|
-
} else if (multiSelectEnabled) {
|
|
395
|
-
newSelectedIds = newSelectedIds.concat(clickedId)
|
|
396
|
-
} else {
|
|
397
|
-
newSelectedIds = [clickedId]
|
|
398
|
-
}
|
|
399
|
-
} else if (selectionMode === 'RANGE') {
|
|
400
|
-
if (selectedIds.length < 1) {
|
|
401
|
-
newSelectedIds = [clickedId]
|
|
402
|
-
} else {
|
|
403
|
-
newSelectedIds = selectedIds
|
|
404
|
-
const clickedItemIndex = sortedRows.findIndex(item => {
|
|
405
|
-
return item[rowSelectionIdProp] === clickedId
|
|
406
|
-
})
|
|
407
|
-
|
|
408
|
-
const lastSelectedIndex = sortedRows.findIndex(item => {
|
|
409
|
-
return item[rowSelectionIdProp] === lastClickedIdForRange
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
let selectionDirection: 'UP' | 'DOWN' | undefined
|
|
413
|
-
|
|
414
|
-
if (lastSelectedIndex > clickedItemIndex) {
|
|
415
|
-
selectionDirection = 'UP'
|
|
416
|
-
} else if (lastSelectedIndex < clickedItemIndex) {
|
|
417
|
-
selectionDirection = 'DOWN'
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (selectionDirection) {
|
|
421
|
-
sortedRows.forEach((item, index) => {
|
|
422
|
-
if (selectionDirection === 'UP' && index <= lastSelectedIndex && index >= clickedItemIndex) {
|
|
423
|
-
newSelectedIds = newSelectedIds.concat(getNestedProperty(item, rowSelectionIdProp))
|
|
424
|
-
} else if (selectionDirection === 'DOWN' && index >= lastSelectedIndex && index <= clickedItemIndex) {
|
|
425
|
-
newSelectedIds = newSelectedIds.concat(getNestedProperty(item, rowSelectionIdProp))
|
|
426
|
-
}
|
|
427
|
-
})
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
} else {
|
|
431
|
-
return
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const clickedIdWasAdded = newSelectedIds.find(id => id === clickedId)
|
|
435
|
-
|
|
436
|
-
let lastClickedId: GetIndexedRowProperty | undefined
|
|
437
|
-
|
|
438
|
-
if (newSelectedIds.length === 0) {
|
|
439
|
-
lastClickedId = undefined
|
|
440
|
-
} else if (selectionMode === 'SINGLE' && clickedIdWasAdded) {
|
|
441
|
-
lastClickedId = clickedId
|
|
442
|
-
} else {
|
|
443
|
-
lastClickedId = lastClickedIdForRange
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
selectedRowIds = [...new Set(newSelectedIds)]
|
|
447
|
-
lastClickedIdForRange = lastClickedId
|
|
448
|
-
}
|
|
449
|
-
export function setPageVisibleByItemIndex(index: number) {
|
|
450
|
-
if (index > -1) {
|
|
451
|
-
paginationComponent?.setPageVisibleByItemIndex(index)
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
export function setPageVisibleByItemId({ id, keyName }: { id: number | string; keyName: K }) {
|
|
455
|
-
const index = sortedRows.findIndex(item => item[keyName] == id)
|
|
456
|
-
|
|
457
|
-
if (index > -1) {
|
|
458
|
-
paginationComponent?.setPageVisibleByItemIndex(index)
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
if (
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
compareResults =
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
)
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
function
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
$effect(() => {
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
)
|
|
725
|
-
|
|
726
|
-
const
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
const
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
if (typeof val === '
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
style
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
class
|
|
850
|
-
class:table-
|
|
851
|
-
class:table-
|
|
852
|
-
{
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
'
|
|
871
|
-
'
|
|
872
|
-
'text-
|
|
873
|
-
'text-
|
|
874
|
-
'
|
|
875
|
-
'
|
|
876
|
-
'text-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
{
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
{:else}
|
|
963
|
-
{#each currentPageRows as row, index}
|
|
964
|
-
{@render tableRow(row, index)}
|
|
965
|
-
{/each}
|
|
966
|
-
{
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
</
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
{
|
|
995
|
-
bind:
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
{
|
|
1022
|
-
{isPinned
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
{
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
{
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
{
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
table.sticky
|
|
1110
|
-
position:
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
}
|
|
1148
|
-
.
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1
|
+
<script module>
|
|
2
|
+
import { getBootstrapCdnVersion } from '@isoftdata/utility-bootstrap'
|
|
3
|
+
let bs5 = $state(false)
|
|
4
|
+
try {
|
|
5
|
+
bs5 = getBootstrapCdnVersion() === 5
|
|
6
|
+
} catch (e) {
|
|
7
|
+
onMount(() => {
|
|
8
|
+
bs5 = getBootstrapCdnVersion() === 5
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<script
|
|
14
|
+
lang="ts"
|
|
15
|
+
generics="R extends UuidRowProps"
|
|
16
|
+
>
|
|
17
|
+
import type { Snippet } from 'svelte'
|
|
18
|
+
|
|
19
|
+
import { ColumnInfoRunicStore } from './column-info-store.svelte'
|
|
20
|
+
|
|
21
|
+
import type { i18n } from 'i18next'
|
|
22
|
+
import type { Get } from 'type-fest'
|
|
23
|
+
import {
|
|
24
|
+
type Column as GenericColumn,
|
|
25
|
+
type SortDirection,
|
|
26
|
+
type UuidRowProps,
|
|
27
|
+
type RowProperties,
|
|
28
|
+
type FooterReducerValue,
|
|
29
|
+
ARBITRARY_PROPERTY_PREFIX,
|
|
30
|
+
type ArbitraryProperty,
|
|
31
|
+
} from './'
|
|
32
|
+
|
|
33
|
+
import { writable, type Writable } from 'svelte/store'
|
|
34
|
+
|
|
35
|
+
import Pagination from './Pagination.svelte'
|
|
36
|
+
import Td from './Td.svelte'
|
|
37
|
+
import Input from '@isoftdata/svelte-input'
|
|
38
|
+
import ContextMenu, { DropdownItem } from '@isoftdata/svelte-context-menu'
|
|
39
|
+
import { v4 as uuid } from '@lukeed/uuid'
|
|
40
|
+
import { format as formatCurrency } from '@isoftdata/utility-currency'
|
|
41
|
+
import { setContext, tick, onMount, getContext, untrack } from 'svelte'
|
|
42
|
+
import { on } from 'svelte/events'
|
|
43
|
+
import naturalCompare from 'natural-compare-lite'
|
|
44
|
+
import type { HTMLTableAttributes, ClassValue } from 'svelte/elements'
|
|
45
|
+
import { translate as defaultTranslate } from '@isoftdata/utility-string'
|
|
46
|
+
import Icon from '@isoftdata/svelte-icon'
|
|
47
|
+
|
|
48
|
+
const { t: translate } = getContext<i18n>('i18next') || { t: defaultTranslate }
|
|
49
|
+
|
|
50
|
+
type K = keyof R
|
|
51
|
+
type KeyPath = RowProperties<R>
|
|
52
|
+
type ColumnProperty = KeyPath | ArbitraryProperty
|
|
53
|
+
|
|
54
|
+
type IndexedRow = R & { originalIndex: number; uuid: string }
|
|
55
|
+
type GetRowProperty = Get<R, KeyPath>
|
|
56
|
+
type GetIndexedRowProperty = Get<IndexedRow, RowProperties<IndexedRow>>
|
|
57
|
+
type FooterValue = {
|
|
58
|
+
property: ColumnProperty
|
|
59
|
+
value: FooterReducerValue<R>
|
|
60
|
+
}
|
|
61
|
+
// Type hack so passing an array of columns without the type argument doesn't reduce the type of R to any
|
|
62
|
+
type Column = GenericColumn<R & {}>
|
|
63
|
+
|
|
64
|
+
interface Props extends Omit<HTMLTableAttributes, 'children' | `aria-${string}` | `bind:${string}` | `on:${string}`> {
|
|
65
|
+
filterEnabled?: boolean
|
|
66
|
+
columns: Array<Column>
|
|
67
|
+
tableId?: string
|
|
68
|
+
rows: Array<R>
|
|
69
|
+
sortDirection?: SortDirection
|
|
70
|
+
sortColumn?: Column | undefined
|
|
71
|
+
class?: ClassValue
|
|
72
|
+
perPageCount?: number
|
|
73
|
+
currentPageNumber?: number
|
|
74
|
+
showFooter?: boolean
|
|
75
|
+
footers?: Array<FooterValue>
|
|
76
|
+
lazySort?: boolean //when true, the table can only be resorted when the user clicks on a column header(not real time as data changes)
|
|
77
|
+
idProp?: K | 'uuid' //required for lazy sorting.
|
|
78
|
+
previousSortOrder?: Array<IndexedRow & { order: number }>
|
|
79
|
+
filterProps?: Array<KeyPath> | false
|
|
80
|
+
filter?: string
|
|
81
|
+
filterLabel?: string
|
|
82
|
+
filterPlaceholder?: string
|
|
83
|
+
filterDisabled?: boolean
|
|
84
|
+
filterReadonly?: boolean
|
|
85
|
+
filterLazy?: boolean | number
|
|
86
|
+
showFilterLabel?: boolean
|
|
87
|
+
headerRowClass?: ClassValue
|
|
88
|
+
headerColumnClass?: ClassValue
|
|
89
|
+
filterColumnClass?: ClassValue
|
|
90
|
+
columnHidingEnabled?: boolean
|
|
91
|
+
selectionEnabled?: boolean
|
|
92
|
+
// Array<number> | Array<string> | Array<R[K]> ???
|
|
93
|
+
selectedRowIds?: Array<GetIndexedRowProperty>
|
|
94
|
+
rowSelectionIdProp?: KeyPath | 'uuid'
|
|
95
|
+
rowSelectionRequiresModKey?: boolean
|
|
96
|
+
selectionMode?: 'SINGLE' | 'RANGE' | null
|
|
97
|
+
multiSelectEnabled?: boolean
|
|
98
|
+
lastClickedIdForRange?: GetIndexedRowProperty | undefined
|
|
99
|
+
totalItemsCount?: number
|
|
100
|
+
stickyHeader?: boolean
|
|
101
|
+
responsive?: boolean | 'sm' | 'md' | 'lg' | 'xl'
|
|
102
|
+
size?: 'sm' | ''
|
|
103
|
+
striped?: boolean
|
|
104
|
+
/** When true, enables tree-specific features. Required if you want to use `TreeRow`s. */
|
|
105
|
+
tree?: boolean
|
|
106
|
+
bordered?: boolean
|
|
107
|
+
hover?: boolean
|
|
108
|
+
previousSortColumn?: Column | undefined
|
|
109
|
+
previousSortDirection?: SortDirection | undefined
|
|
110
|
+
parentClass?: ClassValue
|
|
111
|
+
parentStyle?: string
|
|
112
|
+
hideButtonClass?: ClassValue
|
|
113
|
+
columnResizingEnabled?: boolean
|
|
114
|
+
columnPinningEnabled?: boolean
|
|
115
|
+
/** If specified, where to serialize the columnInfo store */
|
|
116
|
+
localStorageKey?: string | undefined
|
|
117
|
+
columnClickedMethod?: typeof defaultColumnClicked
|
|
118
|
+
filterMethod?: typeof defaultFilter
|
|
119
|
+
rowMatchesFilterMethod?: typeof defaultRowMatchesFilter
|
|
120
|
+
filteredRows?: Array<IndexedRow>
|
|
121
|
+
sortedRows?: Array<IndexedRow>
|
|
122
|
+
currentPageRows?: Array<IndexedRow>
|
|
123
|
+
// snippets
|
|
124
|
+
header?: Snippet
|
|
125
|
+
body?: Snippet<[{ rows: Array<R & { originalIndex: number; uuid: string }>; visibleColumnsCount: number }]>
|
|
126
|
+
children?: Snippet<[{ row: R & { originalIndex: number; uuid: string }; index: number }]>
|
|
127
|
+
noRows?: Snippet<[{ visibleColumnsCount: number }]>
|
|
128
|
+
footerRow?: Snippet<[{ footers: Array<FooterValue> }]>
|
|
129
|
+
// callbacks
|
|
130
|
+
pageChange?: (context: { pageNumber: number }) => void
|
|
131
|
+
columnResizeEnd?: (context: { column: Column }) => void
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const uid = $props.id()
|
|
135
|
+
|
|
136
|
+
let {
|
|
137
|
+
filterEnabled = false,
|
|
138
|
+
columns,
|
|
139
|
+
tableId = uid,
|
|
140
|
+
rows,
|
|
141
|
+
sortDirection = $bindable('ASC'),
|
|
142
|
+
sortColumn = $bindable(undefined),
|
|
143
|
+
class: tableClass = '',
|
|
144
|
+
perPageCount = 0,
|
|
145
|
+
currentPageNumber = $bindable(1),
|
|
146
|
+
showFooter = false,
|
|
147
|
+
lazySort = false,
|
|
148
|
+
idProp = 'uuid',
|
|
149
|
+
previousSortOrder = $bindable([]),
|
|
150
|
+
filterProps = false,
|
|
151
|
+
filter = $bindable(''),
|
|
152
|
+
filterLabel = translate('common:filter', 'Filter'),
|
|
153
|
+
filterPlaceholder = translate('common:filter', 'Filter'),
|
|
154
|
+
filterDisabled = false,
|
|
155
|
+
filterReadonly = false,
|
|
156
|
+
filterLazy = false,
|
|
157
|
+
showFilterLabel = false,
|
|
158
|
+
headerRowClass = bs5 ? 'row' : 'form-row',
|
|
159
|
+
headerColumnClass = 'col',
|
|
160
|
+
filterColumnClass = 'col-lg-2 col-md-4 col-sm-6 align-self-end',
|
|
161
|
+
columnHidingEnabled = false,
|
|
162
|
+
selectionEnabled = false,
|
|
163
|
+
selectedRowIds = $bindable([]),
|
|
164
|
+
rowSelectionIdProp = 'uuid',
|
|
165
|
+
rowSelectionRequiresModKey = false,
|
|
166
|
+
selectionMode = $bindable(null),
|
|
167
|
+
multiSelectEnabled = true,
|
|
168
|
+
lastClickedIdForRange = $bindable(undefined),
|
|
169
|
+
totalItemsCount = 0,
|
|
170
|
+
stickyHeader = false,
|
|
171
|
+
responsive = false,
|
|
172
|
+
size = 'sm',
|
|
173
|
+
striped = true,
|
|
174
|
+
tree = false,
|
|
175
|
+
bordered = true,
|
|
176
|
+
hover = true,
|
|
177
|
+
previousSortColumn = $bindable(undefined),
|
|
178
|
+
previousSortDirection = $bindable(undefined),
|
|
179
|
+
parentClass = '',
|
|
180
|
+
parentStyle = '',
|
|
181
|
+
hideButtonClass = '',
|
|
182
|
+
columnResizingEnabled = false,
|
|
183
|
+
columnPinningEnabled = false,
|
|
184
|
+
localStorageKey = undefined,
|
|
185
|
+
columnClickedMethod = defaultColumnClicked,
|
|
186
|
+
filterMethod = defaultFilter,
|
|
187
|
+
rowMatchesFilterMethod = defaultRowMatchesFilter,
|
|
188
|
+
filteredRows = $bindable(filterMethod('', rows, columns)),
|
|
189
|
+
sortedRows = $bindable(filteredRows),
|
|
190
|
+
currentPageRows = $bindable(sortedRows.slice(0, perPageCount)),
|
|
191
|
+
// snippets
|
|
192
|
+
header,
|
|
193
|
+
body,
|
|
194
|
+
children,
|
|
195
|
+
noRows,
|
|
196
|
+
footerRow,
|
|
197
|
+
// callbacks
|
|
198
|
+
pageChange,
|
|
199
|
+
columnResizeEnd,
|
|
200
|
+
...rest
|
|
201
|
+
}: Props = $props()
|
|
202
|
+
|
|
203
|
+
let lastPageNumber: number = $state(1)
|
|
204
|
+
let paginationComponent: Pagination<IndexedRow> | undefined = $state()
|
|
205
|
+
let contextMenu: ContextMenu | undefined = $state()
|
|
206
|
+
let contextMenuColumn: Column | undefined = $state()
|
|
207
|
+
let columnResizingIsActive: boolean = $state(false)
|
|
208
|
+
/** Set to true after getting initial table widths when resizing enabled*/
|
|
209
|
+
let useFixedLayout = $state(false)
|
|
210
|
+
let tableParent: HTMLDivElement | undefined = $state()
|
|
211
|
+
|
|
212
|
+
// Do the initial sort if they specified a default sort column
|
|
213
|
+
// svelte-ignore state_referenced_locally
|
|
214
|
+
const defaultSortColumn = columns.find(column => column.defaultSortColumn)
|
|
215
|
+
if (defaultSortColumn) {
|
|
216
|
+
sortColumn = defaultSortColumn
|
|
217
|
+
sortDirection = defaultSortColumn.defaultSortDirection || 'ASC'
|
|
218
|
+
|
|
219
|
+
doSort({
|
|
220
|
+
rows: filteredRows,
|
|
221
|
+
sortColumn,
|
|
222
|
+
sortDirection,
|
|
223
|
+
sameSortOrder: false,
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Need to track which rows are expanded so when the rows reorder, they correctly keep that state.
|
|
228
|
+
const expandedRows = writable<Record<number | string, boolean>>({})
|
|
229
|
+
// svelte-ignore state_referenced_locally
|
|
230
|
+
if (tree) {
|
|
231
|
+
setContext('expandedRows', expandedRows)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// TODO: bad things will happen if we update any of our apps to use Runes for the store if we don't account for that here
|
|
235
|
+
const session = getContext<Writable<{ userAccountId?: number }> | undefined>('session')
|
|
236
|
+
// This will be accessible on the component instance, ie `const columnInfo = $derived(table?.columnInfo)` if you need it
|
|
237
|
+
export const columnInfo = new ColumnInfoRunicStore<R>(() => columns, {
|
|
238
|
+
// svelte-ignore state_referenced_locally
|
|
239
|
+
key: localStorageKey,
|
|
240
|
+
userAccountId: session && $session ? $session.userAccountId : undefined,
|
|
241
|
+
})
|
|
242
|
+
// Set as context so we can retrieve it in the Td component
|
|
243
|
+
setContext('columnInfo', columnInfo)
|
|
244
|
+
// Set as context so we can retrieve it in the TreeRow component
|
|
245
|
+
const selectedRowIdsStore = writable(selectedRowIds)
|
|
246
|
+
setContext('selectedRowIds', selectedRowIdsStore)
|
|
247
|
+
// svelte-ignore state_referenced_locally
|
|
248
|
+
setContext('idProp', idProp)
|
|
249
|
+
// svelte-ignore state_referenced_locally
|
|
250
|
+
setContext('columnResizingEnabled', writable(columnResizingEnabled))
|
|
251
|
+
setContext('bs5', bs5)
|
|
252
|
+
|
|
253
|
+
// #endregion
|
|
254
|
+
// #region Functions
|
|
255
|
+
|
|
256
|
+
function isArbitraryProperty(property: string | undefined): property is ArbitraryProperty {
|
|
257
|
+
return !!property?.startsWith(ARBITRARY_PROPERTY_PREFIX)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export async function defaultColumnClicked(clickedColumn: Column, sortDirection: SortDirection) {
|
|
261
|
+
if (currentPageNumber !== lastPageNumber) {
|
|
262
|
+
paginationComponent?.setPageNumber(1)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
doSort({
|
|
266
|
+
rows: filteredRows,
|
|
267
|
+
sortColumn: clickedColumn,
|
|
268
|
+
sortDirection,
|
|
269
|
+
sameSortOrder: false,
|
|
270
|
+
})
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function defaultFilter(filter: string, rows: R[], columns: Column[]): IndexedRow[] {
|
|
274
|
+
const columnProps = columns
|
|
275
|
+
?.map(({ property }) => property)
|
|
276
|
+
.filter((property): property is KeyPath => !isArbitraryProperty(property))
|
|
277
|
+
|
|
278
|
+
if (filterEnabled) {
|
|
279
|
+
const props = filterProps || columnProps
|
|
280
|
+
return getFilterMatches(filter, rows, props)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return rows.map((row, index): IndexedRow => ({ ...row, originalIndex: index, uuid: row.uuid || uuid() }))
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** In normal mode, returns all rows matching the filter.
|
|
287
|
+
*
|
|
288
|
+
* In tree mode, shows all children if parent matches, or shows matching children and their children
|
|
289
|
+
*/
|
|
290
|
+
function getFilterMatches(filter: string, theRows: Array<R>, props: KeyPath[]) {
|
|
291
|
+
return theRows.reduce((rows, row, index): IndexedRow[] => {
|
|
292
|
+
if (rowMatchesFilterMethod(filter, row, props)) {
|
|
293
|
+
rows.push({ ...row, originalIndex: index, uuid: 'uuid' in row ? (row.uuid as string) : uuid() })
|
|
294
|
+
} else if (tree && Array.isArray(row.children) && row.children.length) {
|
|
295
|
+
const children = getFilterMatches(filter, row.children, props)
|
|
296
|
+
if (children.length) {
|
|
297
|
+
rows.push({ ...row, children, originalIndex: index, uuid: 'uuid' in row ? (row.uuid as string) : uuid() })
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return rows
|
|
302
|
+
}, new Array<IndexedRow>())
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function defaultRowMatchesFilter(filter: string, row: R, props: Array<RowProperties<R>>) {
|
|
306
|
+
return (
|
|
307
|
+
!filter ||
|
|
308
|
+
props.some(prop => {
|
|
309
|
+
const value = getNestedProperty(row, prop)
|
|
310
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
311
|
+
return value.toString().toUpperCase().indexOf(filter.toUpperCase()) > -1
|
|
312
|
+
}
|
|
313
|
+
return false
|
|
314
|
+
})
|
|
315
|
+
)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function getNestedProperty(
|
|
319
|
+
row: R & { uuid: string },
|
|
320
|
+
path: KeyPath | 'uuid',
|
|
321
|
+
defaultValue?: string | number,
|
|
322
|
+
): GetIndexedRowProperty
|
|
323
|
+
export function getNestedProperty(row: R, path: KeyPath, defaultValue?: string | number): GetRowProperty
|
|
324
|
+
export function getNestedProperty(
|
|
325
|
+
row: R | (R & { uuid: string }),
|
|
326
|
+
path: KeyPath | 'uuid',
|
|
327
|
+
defaultValue?: string | number,
|
|
328
|
+
): GetRowProperty | GetIndexedRowProperty {
|
|
329
|
+
if (!path) {
|
|
330
|
+
return defaultValue as GetRowProperty
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (row[path]) {
|
|
334
|
+
return row[path] as GetRowProperty
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const val =
|
|
338
|
+
path
|
|
339
|
+
.split('[')
|
|
340
|
+
// TODO figure out if we can do this less evilly
|
|
341
|
+
.reduce((obj, prop) => (obj as any)?.[prop.replace(/\]/g, '')], row as any) ?? defaultValue
|
|
342
|
+
return val
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function columnClickHandler(clickedColumn: Column) {
|
|
346
|
+
if (clickedColumn.sortType === false || columnResizingIsActive || isArbitraryProperty(clickedColumn.property)) {
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
sortDirection = sortDirection === 'ASC' && clickedColumn.property === sortColumn?.property ? 'DESC' : 'ASC'
|
|
350
|
+
|
|
351
|
+
columnClickedMethod(clickedColumn, sortDirection)
|
|
352
|
+
|
|
353
|
+
sortColumn = clickedColumn
|
|
354
|
+
await tick()
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export function expandRow(rowId: string | number, expanded = true) {
|
|
358
|
+
if (tree) {
|
|
359
|
+
$expandedRows[rowId] = expanded
|
|
360
|
+
} else {
|
|
361
|
+
console.warn('expandRow should only be used when the `tree` prop is set to true on the Table component.')
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function rowIsSelected(
|
|
366
|
+
row: IndexedRow,
|
|
367
|
+
rowSelectionIdProp: KeyPath | 'uuid',
|
|
368
|
+
selectedRowIds: Array<GetIndexedRowProperty>,
|
|
369
|
+
) {
|
|
370
|
+
const selectionId = getNestedProperty(row, rowSelectionIdProp, row.uuid)
|
|
371
|
+
return selectedRowIds.includes(selectionId)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function rowClick(row: IndexedRow) {
|
|
375
|
+
if (!selectionEnabled || !rowSelectionIdProp) {
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
const clickedId = getNestedProperty(row, rowSelectionIdProp || 'uuid')
|
|
379
|
+
|
|
380
|
+
const selectedIds = selectedRowIds
|
|
381
|
+
|
|
382
|
+
let newSelectedIds: Array<GetIndexedRowProperty> = []
|
|
383
|
+
|
|
384
|
+
if (!selectionMode && !rowSelectionRequiresModKey) {
|
|
385
|
+
selectionMode = 'SINGLE'
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (selectionMode === 'SINGLE') {
|
|
389
|
+
newSelectedIds = selectedIds
|
|
390
|
+
const foundInExistingSelections = selectedIds.some(id => id === clickedId)
|
|
391
|
+
|
|
392
|
+
if (foundInExistingSelections) {
|
|
393
|
+
newSelectedIds = newSelectedIds.filter(id => id !== clickedId)
|
|
394
|
+
} else if (multiSelectEnabled) {
|
|
395
|
+
newSelectedIds = newSelectedIds.concat(clickedId)
|
|
396
|
+
} else {
|
|
397
|
+
newSelectedIds = [clickedId]
|
|
398
|
+
}
|
|
399
|
+
} else if (selectionMode === 'RANGE') {
|
|
400
|
+
if (selectedIds.length < 1) {
|
|
401
|
+
newSelectedIds = [clickedId]
|
|
402
|
+
} else {
|
|
403
|
+
newSelectedIds = selectedIds
|
|
404
|
+
const clickedItemIndex = sortedRows.findIndex(item => {
|
|
405
|
+
return item[rowSelectionIdProp] === clickedId
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
const lastSelectedIndex = sortedRows.findIndex(item => {
|
|
409
|
+
return item[rowSelectionIdProp] === lastClickedIdForRange
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
let selectionDirection: 'UP' | 'DOWN' | undefined
|
|
413
|
+
|
|
414
|
+
if (lastSelectedIndex > clickedItemIndex) {
|
|
415
|
+
selectionDirection = 'UP'
|
|
416
|
+
} else if (lastSelectedIndex < clickedItemIndex) {
|
|
417
|
+
selectionDirection = 'DOWN'
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (selectionDirection) {
|
|
421
|
+
sortedRows.forEach((item, index) => {
|
|
422
|
+
if (selectionDirection === 'UP' && index <= lastSelectedIndex && index >= clickedItemIndex) {
|
|
423
|
+
newSelectedIds = newSelectedIds.concat(getNestedProperty(item, rowSelectionIdProp))
|
|
424
|
+
} else if (selectionDirection === 'DOWN' && index >= lastSelectedIndex && index <= clickedItemIndex) {
|
|
425
|
+
newSelectedIds = newSelectedIds.concat(getNestedProperty(item, rowSelectionIdProp))
|
|
426
|
+
}
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
} else {
|
|
431
|
+
return
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const clickedIdWasAdded = newSelectedIds.find(id => id === clickedId)
|
|
435
|
+
|
|
436
|
+
let lastClickedId: GetIndexedRowProperty | undefined
|
|
437
|
+
|
|
438
|
+
if (newSelectedIds.length === 0) {
|
|
439
|
+
lastClickedId = undefined
|
|
440
|
+
} else if (selectionMode === 'SINGLE' && clickedIdWasAdded) {
|
|
441
|
+
lastClickedId = clickedId
|
|
442
|
+
} else {
|
|
443
|
+
lastClickedId = lastClickedIdForRange
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
selectedRowIds = [...new Set(newSelectedIds)]
|
|
447
|
+
lastClickedIdForRange = lastClickedId
|
|
448
|
+
}
|
|
449
|
+
export function setPageVisibleByItemIndex(index: number) {
|
|
450
|
+
if (index > -1) {
|
|
451
|
+
paginationComponent?.setPageVisibleByItemIndex(index)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
export function setPageVisibleByItemId({ id, keyName }: { id: number | string; keyName: K }) {
|
|
455
|
+
const index = sortedRows.findIndex(item => item[keyName] == id)
|
|
456
|
+
|
|
457
|
+
if (index > -1) {
|
|
458
|
+
paginationComponent?.setPageVisibleByItemIndex(index)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/**
|
|
462
|
+
* Sorts the rows in the table. `rows` should be pre-filtered, and defaults to `filteredRows` if omitted
|
|
463
|
+
*/
|
|
464
|
+
export function doSort({
|
|
465
|
+
rows = filteredRows,
|
|
466
|
+
sortColumn,
|
|
467
|
+
sortDirection,
|
|
468
|
+
sameSortOrder,
|
|
469
|
+
}: {
|
|
470
|
+
/** Defaults to `filteredRows` if omitted */
|
|
471
|
+
rows?: Array<IndexedRow>
|
|
472
|
+
sortColumn: Column | undefined
|
|
473
|
+
sortDirection: SortDirection
|
|
474
|
+
sameSortOrder: boolean
|
|
475
|
+
}) {
|
|
476
|
+
const keepSameSortOrder = !!(lazySort && sameSortOrder && idProp)
|
|
477
|
+
const hasPreviousSort = previousSortColumn && previousSortOrder.length > 0
|
|
478
|
+
|
|
479
|
+
let sortProp: KeyPath
|
|
480
|
+
if (
|
|
481
|
+
keepSameSortOrder &&
|
|
482
|
+
hasPreviousSort &&
|
|
483
|
+
previousSortColumn &&
|
|
484
|
+
idProp &&
|
|
485
|
+
!isArbitraryProperty(previousSortColumn.property)
|
|
486
|
+
) {
|
|
487
|
+
sortColumn = previousSortColumn
|
|
488
|
+
sortProp = previousSortColumn.property
|
|
489
|
+
sortDirection = 'ASC' //force ASC for keeping the same order because we defined the order in previousSortOrder
|
|
490
|
+
|
|
491
|
+
rows = rows.map(row => {
|
|
492
|
+
const foundItem = previousSortOrder.find(
|
|
493
|
+
previousSortOrderRow => !!idProp && previousSortOrderRow[idProp] === row[idProp],
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
return { ...row, order: foundItem ? foundItem.order : 0 }
|
|
497
|
+
})
|
|
498
|
+
} else if (sortColumn?.property && !isArbitraryProperty(sortColumn.property)) {
|
|
499
|
+
sortProp = sortColumn.property
|
|
500
|
+
} else {
|
|
501
|
+
// no sort property, can't sort
|
|
502
|
+
sortedRows = rows
|
|
503
|
+
return
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function doTheSort(theRows: Array<IndexedRow>): Array<IndexedRow> {
|
|
507
|
+
if (tree) {
|
|
508
|
+
return theRows
|
|
509
|
+
.slice()
|
|
510
|
+
.map(row => {
|
|
511
|
+
if (Array.isArray(row.children)) {
|
|
512
|
+
return {
|
|
513
|
+
...row,
|
|
514
|
+
children: doTheSort(row.children),
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
return row
|
|
518
|
+
})
|
|
519
|
+
.sort((a: IndexedRow, b: IndexedRow) =>
|
|
520
|
+
sortColumn?.sortType === 'ALPHA_NUM' ? alphaNumSort(a, b) : standardSort(a, b),
|
|
521
|
+
)
|
|
522
|
+
}
|
|
523
|
+
return theRows.slice().sort(sortColumn?.sortType === 'ALPHA_NUM' ? alphaNumSort : standardSort)
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const theSortedRows = doTheSort(rows)
|
|
527
|
+
|
|
528
|
+
previousSortOrder = theSortedRows.map((row, index) => ({
|
|
529
|
+
...row,
|
|
530
|
+
order: index,
|
|
531
|
+
}))
|
|
532
|
+
previousSortDirection = sortDirection
|
|
533
|
+
previousSortColumn = sortColumn
|
|
534
|
+
sortedRows = theSortedRows
|
|
535
|
+
return
|
|
536
|
+
|
|
537
|
+
function standardSort(a: R, b: R) {
|
|
538
|
+
let aValue: Get<R, KeyPath> | string = getNestedProperty(a, sortProp, '')
|
|
539
|
+
let bValue: Get<R, KeyPath> | string = getNestedProperty(b, sortProp, '')
|
|
540
|
+
|
|
541
|
+
if (typeof aValue === 'string') {
|
|
542
|
+
aValue = aValue.toUpperCase()
|
|
543
|
+
}
|
|
544
|
+
if (typeof bValue === 'string') {
|
|
545
|
+
bValue = bValue.toUpperCase()
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (aValue < bValue) {
|
|
549
|
+
return sortDirection === 'ASC' ? -1 : 1
|
|
550
|
+
}
|
|
551
|
+
if (aValue > bValue) {
|
|
552
|
+
return sortDirection === 'ASC' ? 1 : -1
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return 0
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function alphaNumSort(a: R, b: R) {
|
|
559
|
+
const aValue = getNestedProperty(a, sortProp, '')
|
|
560
|
+
const bValue = getNestedProperty(b, sortProp, '')
|
|
561
|
+
let compareResults = 0
|
|
562
|
+
if (
|
|
563
|
+
(typeof aValue === 'number' || typeof aValue === 'string') &&
|
|
564
|
+
(typeof bValue === 'number' || typeof bValue === 'string')
|
|
565
|
+
) {
|
|
566
|
+
compareResults = naturalCompare(aValue.toString(), bValue.toString())
|
|
567
|
+
} else if (typeof aValue === 'number' || typeof aValue === 'string') {
|
|
568
|
+
compareResults = 1
|
|
569
|
+
} else if (typeof bValue === 'number' || typeof bValue === 'string') {
|
|
570
|
+
compareResults = -1
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return sortDirection === 'ASC' ? compareResults : compareResults * -1
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
export function setColumnVisibility(columnProperties: Array<ColumnProperty> | ColumnProperty, visible: boolean) {
|
|
578
|
+
if (!Array.isArray(columnProperties)) {
|
|
579
|
+
columnProperties = [columnProperties]
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
try {
|
|
583
|
+
untrack(() => {
|
|
584
|
+
columnInfo.updateColumns(
|
|
585
|
+
columnProperties.map((property): { property: ColumnProperty; visible: boolean } => ({
|
|
586
|
+
property,
|
|
587
|
+
visible,
|
|
588
|
+
})),
|
|
589
|
+
)
|
|
590
|
+
})
|
|
591
|
+
} catch (err) {
|
|
592
|
+
console.error(`Error setting column visibility for columns ${JSON.stringify(columnProperties)}`, err)
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/** When called in `onMount` or an effect, will set the visibility of the specified columns whenever the value of `getVisible()` changes*/
|
|
597
|
+
export function setColumnVisibilityWatch(
|
|
598
|
+
columnProperties: Array<ColumnProperty> | ColumnProperty,
|
|
599
|
+
getVisible: () => boolean,
|
|
600
|
+
) {
|
|
601
|
+
$effect(() => {
|
|
602
|
+
const visible = getVisible()
|
|
603
|
+
setColumnVisibility(columnProperties, visible)
|
|
604
|
+
})
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function hideColumn(column: Column) {
|
|
608
|
+
if (column) {
|
|
609
|
+
columnInfo.updateColumn(column.property, { visible: false })
|
|
610
|
+
}
|
|
611
|
+
contextMenuColumn = undefined
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function showColumn(column: Column) {
|
|
615
|
+
if (column) {
|
|
616
|
+
columnInfo.updateColumn(column.property, { visible: true })
|
|
617
|
+
}
|
|
618
|
+
contextMenuColumn = undefined
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function pinUnpinColumn(column: Column) {
|
|
622
|
+
columnInfo.updateColumn(column.property, { pinned: !column.pinned })
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function contextMenuHandler(event: MouseEvent, column: string) {
|
|
626
|
+
if ((columnHidingEnabled || columnResizingEnabled || columnPinningEnabled) && !event.ctrlKey) {
|
|
627
|
+
event.preventDefault()
|
|
628
|
+
contextMenuColumn = columnInfo.current[column]
|
|
629
|
+
return contextMenu?.open(event, `col-${tableId}-${CSS.escape(column)}`)
|
|
630
|
+
}
|
|
631
|
+
return Promise.resolve()
|
|
632
|
+
}
|
|
633
|
+
// #endregion
|
|
634
|
+
// #region Tree-specific functions
|
|
635
|
+
|
|
636
|
+
// #endregion
|
|
637
|
+
|
|
638
|
+
function onColumnMouseDown(event: MouseEvent, column: Column) {
|
|
639
|
+
event.stopPropagation()
|
|
640
|
+
if (!event.target || !(event.target instanceof HTMLDivElement)) {
|
|
641
|
+
return
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const htmlColumn = tableParent?.querySelector<HTMLTableCellElement>(
|
|
645
|
+
`#col-${tableId}-${CSS.escape(column.property)}`,
|
|
646
|
+
)
|
|
647
|
+
const columnIndex = columns.findIndex(col => col.property === column.property)
|
|
648
|
+
if (!htmlColumn || columnIndex === -1) {
|
|
649
|
+
return
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
const mouseDownPageX = event.pageX
|
|
653
|
+
const mouseDownColumnWidth = htmlColumn.offsetWidth
|
|
654
|
+
|
|
655
|
+
event.target.classList.add('resizing')
|
|
656
|
+
columnResizingIsActive = true
|
|
657
|
+
|
|
658
|
+
const mouseMoveHandler = (mouseMoveEvent: MouseEvent) => {
|
|
659
|
+
const widthDifference = mouseMoveEvent.pageX - mouseDownPageX
|
|
660
|
+
const newWidth = `${mouseDownColumnWidth + widthDifference}px`
|
|
661
|
+
htmlColumn.style.width = column.minWidth ? `max(${newWidth}, ${column.minWidth})` : newWidth
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const mouseUpHandler = () => {
|
|
665
|
+
if (!(event.target instanceof HTMLDivElement)) {
|
|
666
|
+
return
|
|
667
|
+
}
|
|
668
|
+
event.target.classList.remove('resizing')
|
|
669
|
+
columnInfo.updateColumn(column.property, { userWidth: htmlColumn.style.width })
|
|
670
|
+
|
|
671
|
+
removeMouseMoveListener?.()
|
|
672
|
+
removeMouseUpListener?.()
|
|
673
|
+
columnResizeEnd?.({ column: columns[columnIndex] })
|
|
674
|
+
// We want to ensure the user's mouseup event doesn't trigger a click event on the column header
|
|
675
|
+
setTimeout(() => (columnResizingIsActive = false), 50)
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const removeMouseMoveListener = tableParent ? on(tableParent, 'mousemove', mouseMoveHandler) : undefined
|
|
679
|
+
const removeMouseUpListener = tableParent ? on(tableParent, 'mouseup', mouseUpHandler) : undefined
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
async function setDefaultColumnWidths() {
|
|
683
|
+
useFixedLayout = false
|
|
684
|
+
await tick()
|
|
685
|
+
if (columnResizingEnabled) {
|
|
686
|
+
columnInfo.updateColumns(
|
|
687
|
+
columns.map((column): { property: ColumnProperty; userWidth: string } => {
|
|
688
|
+
const thisColumnInfo = columnInfo.current[column.property]
|
|
689
|
+
const prevUserWidth = thisColumnInfo?.userWidth
|
|
690
|
+
// Minimum width will be inherited from the layout on reset, but this won't, so re-set it here if needed
|
|
691
|
+
const explicitWidth = thisColumnInfo?.width
|
|
692
|
+
|
|
693
|
+
return {
|
|
694
|
+
property: column.property,
|
|
695
|
+
userWidth: prevUserWidth ?? explicitWidth ?? getColumnThWidth(column.property),
|
|
696
|
+
}
|
|
697
|
+
}),
|
|
698
|
+
)
|
|
699
|
+
useFixedLayout = true
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function getColumnThWidth(property: string) {
|
|
704
|
+
const thWidth =
|
|
705
|
+
tableParent?.querySelector<HTMLTableCellElement>(`#col-${tableId}-${CSS.escape(property)}`)?.offsetWidth ?? 25
|
|
706
|
+
return `${thWidth}px`
|
|
707
|
+
}
|
|
708
|
+
// #region Computed
|
|
709
|
+
$effect(() => {
|
|
710
|
+
selectedRowIdsStore.set(selectedRowIds)
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
$effect(() => {
|
|
714
|
+
filteredRows = filterMethod(filter, rows, columns)
|
|
715
|
+
// Should only run when filter/rows/columns changes since everything else is untracked
|
|
716
|
+
untrack(() =>
|
|
717
|
+
doSort({
|
|
718
|
+
rows: filteredRows && Array.isArray(filteredRows) ? filteredRows : [],
|
|
719
|
+
sortColumn: sortColumn,
|
|
720
|
+
sortDirection: sortDirection,
|
|
721
|
+
sameSortOrder: true,
|
|
722
|
+
}),
|
|
723
|
+
)
|
|
724
|
+
})
|
|
725
|
+
|
|
726
|
+
const responsiveClass = $derived(
|
|
727
|
+
responsive === true ? 'table-responsive' : !!responsive ? `table-responsive-${responsive}` : '',
|
|
728
|
+
)
|
|
729
|
+
/** Includes information computed by the columnInfo store, including what we serialize to localstorage */
|
|
730
|
+
const computedColumns = $derived(columnInfo.columns)
|
|
731
|
+
const visibleColumns = $derived(computedColumns.filter(column => column.unhidable || column.visible))
|
|
732
|
+
const hiddenColumns = $derived(computedColumns.filter(column => !column.unhidable && !column.visible))
|
|
733
|
+
const showPagination = $derived((perPageCount && rows.length > perPageCount) || totalItemsCount > perPageCount)
|
|
734
|
+
|
|
735
|
+
const footers = $derived(
|
|
736
|
+
columns.map(column => {
|
|
737
|
+
if (column.footer && showFooter) {
|
|
738
|
+
const property = column.footer.altProperty ? column.footer.altProperty : column.property
|
|
739
|
+
const mathFunctions = ['AVG', 'SUM']
|
|
740
|
+
|
|
741
|
+
let columnValues: Array<number | GetRowProperty> = rows.map(row => {
|
|
742
|
+
const value = isArbitraryProperty(property)
|
|
743
|
+
? ('' as Get<R, RowProperties<R>>)
|
|
744
|
+
: getNestedProperty(row, property)
|
|
745
|
+
if (
|
|
746
|
+
column.footer?.fn &&
|
|
747
|
+
typeof column.footer.fn === 'string' &&
|
|
748
|
+
mathFunctions.includes(column.footer.fn) &&
|
|
749
|
+
typeof value === 'string'
|
|
750
|
+
) {
|
|
751
|
+
return parseFloat(value) || 0
|
|
752
|
+
}
|
|
753
|
+
return value
|
|
754
|
+
})
|
|
755
|
+
|
|
756
|
+
if (column.footer.requiredValue) {
|
|
757
|
+
columnValues = columnValues.filter(value => value === column.footer?.requiredValue)
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
let value: FooterReducerValue<R>
|
|
761
|
+
|
|
762
|
+
function sum(sum: number, val: unknown): number {
|
|
763
|
+
if (typeof val === 'number') {
|
|
764
|
+
return sum + val
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (typeof val === 'string') {
|
|
768
|
+
const parsedVal = parseFloat(val)
|
|
769
|
+
if (isNaN(parsedVal)) {
|
|
770
|
+
return sum
|
|
771
|
+
}
|
|
772
|
+
return sum + parsedVal
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return sum
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
switch (column.footer.fn) {
|
|
779
|
+
case 'COUNT':
|
|
780
|
+
value = columnValues.length
|
|
781
|
+
break
|
|
782
|
+
case 'AVG':
|
|
783
|
+
value = (columnValues.reduce(sum, 0) / columnValues.length).toFixed(2)
|
|
784
|
+
break
|
|
785
|
+
case 'SUM':
|
|
786
|
+
value = columnValues.reduce(sum, 0)
|
|
787
|
+
break
|
|
788
|
+
default:
|
|
789
|
+
if (typeof column.footer.fn === 'function') {
|
|
790
|
+
value = columnValues.reduce(column.footer.fn, column.footer.initialValue)
|
|
791
|
+
} else {
|
|
792
|
+
value = ''
|
|
793
|
+
}
|
|
794
|
+
break
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return {
|
|
798
|
+
property: column.property,
|
|
799
|
+
value:
|
|
800
|
+
column.footer.formatCurrency && (typeof value === 'string' || typeof value === 'number')
|
|
801
|
+
? formatCurrency(value)
|
|
802
|
+
: value,
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return {
|
|
806
|
+
property: column.property,
|
|
807
|
+
value: '',
|
|
808
|
+
}
|
|
809
|
+
}),
|
|
810
|
+
)
|
|
811
|
+
//#endregion
|
|
812
|
+
onMount(() => {
|
|
813
|
+
setDefaultColumnWidths()
|
|
814
|
+
})
|
|
815
|
+
</script>
|
|
816
|
+
|
|
817
|
+
{#if filterEnabled}
|
|
818
|
+
<div class={headerRowClass}>
|
|
819
|
+
<div class={headerColumnClass}>
|
|
820
|
+
{@render header?.()}
|
|
821
|
+
</div>
|
|
822
|
+
<div class={filterColumnClass}>
|
|
823
|
+
<Input
|
|
824
|
+
label={filterLabel}
|
|
825
|
+
showLabel={showFilterLabel}
|
|
826
|
+
bind:value={filter}
|
|
827
|
+
placeholder={filterPlaceholder}
|
|
828
|
+
disabled={filterDisabled}
|
|
829
|
+
readonly={filterReadonly || null}
|
|
830
|
+
lazy={filterLazy}
|
|
831
|
+
/>
|
|
832
|
+
<!-- || null is so svelte doesn't include readonly when it's falsy, probably a result of it being `restProps`'d on the input -->
|
|
833
|
+
</div>
|
|
834
|
+
</div>
|
|
835
|
+
{/if}
|
|
836
|
+
|
|
837
|
+
<!-- webkit-user-select: none; here fixes an issue in Safari where text selection of the column headers would happen while resizing the columns -->
|
|
838
|
+
<div
|
|
839
|
+
class={[parentClass, responsiveClass]}
|
|
840
|
+
style={parentStyle}
|
|
841
|
+
class:mb-3={showPagination}
|
|
842
|
+
style:user-select={columnResizingIsActive ? 'none' : undefined}
|
|
843
|
+
style:-webkit-user-select={columnResizingIsActive ? 'none' : undefined}
|
|
844
|
+
style:cursor={columnResizingIsActive ? 'col-resize' : undefined}
|
|
845
|
+
bind:this={tableParent}
|
|
846
|
+
>
|
|
847
|
+
<table
|
|
848
|
+
id={tableId}
|
|
849
|
+
class={['table mb-0', tableClass]}
|
|
850
|
+
class:table-sm={size === 'sm'}
|
|
851
|
+
class:table-layout-fixed={columnResizingEnabled && useFixedLayout}
|
|
852
|
+
class:sticky={stickyHeader}
|
|
853
|
+
class:table-striped={striped}
|
|
854
|
+
class:table-bordered={bordered}
|
|
855
|
+
class:table-hover={hover}
|
|
856
|
+
{...rest}
|
|
857
|
+
>
|
|
858
|
+
<thead>
|
|
859
|
+
<tr>
|
|
860
|
+
{#each visibleColumns as column, index}
|
|
861
|
+
{@const isSortColumn = sortColumn?.property === column.property}
|
|
862
|
+
{@const doEllipsis = column.ellipsis || columnResizingEnabled}
|
|
863
|
+
|
|
864
|
+
<th
|
|
865
|
+
id="col-{tableId}-{column.property}"
|
|
866
|
+
class={[
|
|
867
|
+
'unselectable',
|
|
868
|
+
column.class,
|
|
869
|
+
{
|
|
870
|
+
'cursor-pointer': column.sortType !== false && !isArbitraryProperty(column.property),
|
|
871
|
+
'd-print-none': column.hideForPrint,
|
|
872
|
+
'text-left': !bs5 && column.align === 'left',
|
|
873
|
+
'text-start': column.align === 'start' || (bs5 && column.align === 'left'),
|
|
874
|
+
'text-right': !bs5 && (column.align === 'right' || column.numeric),
|
|
875
|
+
'text-end': column.align === 'end' || (bs5 && (column.align === 'right' || column.numeric)),
|
|
876
|
+
'text-center': column.align === 'center',
|
|
877
|
+
'text-truncate': doEllipsis,
|
|
878
|
+
'bg-white': !bs5 && stickyHeader,
|
|
879
|
+
'table-secondary': column.pinned,
|
|
880
|
+
'text-nowrap': !column.wrap,
|
|
881
|
+
pinned: column.pinned,
|
|
882
|
+
},
|
|
883
|
+
]}
|
|
884
|
+
title={column.title ?? column.name}
|
|
885
|
+
style:width={column.userWidth ?? column.width}
|
|
886
|
+
style:min-width={column.minWidth}
|
|
887
|
+
style:--pinned-column-offset={column.pinned ? columnInfo.getPinnedColumnOffset(column.property) : undefined}
|
|
888
|
+
data-sort-specified={isSortColumn ? sortDirection : ''}
|
|
889
|
+
onclick={() => columnClickHandler(column)}
|
|
890
|
+
oncontextmenu={event => contextMenuHandler(event, column.property)}
|
|
891
|
+
>
|
|
892
|
+
<span
|
|
893
|
+
class="text-truncate d-inline-block"
|
|
894
|
+
style="vertical-align: bottom;"
|
|
895
|
+
style:max-width={doEllipsis && isSortColumn ? 'calc(100% - 20px)' : '100%'}
|
|
896
|
+
>
|
|
897
|
+
{@render columnNameAndIcon(column)}
|
|
898
|
+
</span>
|
|
899
|
+
<!-- I wonder how to make everything after this move to the right...? -->
|
|
900
|
+
{#if isSortColumn}
|
|
901
|
+
{sortDirection === 'ASC' ? '▲' : '▼'}
|
|
902
|
+
{/if}
|
|
903
|
+
{#if column.pinned}
|
|
904
|
+
<Icon
|
|
905
|
+
class=""
|
|
906
|
+
icon="thumbtack-angle"
|
|
907
|
+
/>
|
|
908
|
+
{/if}
|
|
909
|
+
{#if columnResizingEnabled && visibleColumns.length - 1 !== index}
|
|
910
|
+
<!-- I think setting the role and aria-orientation attributes is the best we can do here -->
|
|
911
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
912
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
913
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
914
|
+
<div
|
|
915
|
+
id="col-resizer-{tableId}-{column.property}"
|
|
916
|
+
tabindex={0}
|
|
917
|
+
role="separator"
|
|
918
|
+
aria-orientation="horizontal"
|
|
919
|
+
class="resizer"
|
|
920
|
+
style="height: 100%;"
|
|
921
|
+
onmousedown={e => onColumnMouseDown(e, column)}
|
|
922
|
+
>
|
|
923
|
+
|
|
924
|
+
</div>
|
|
925
|
+
{/if}
|
|
926
|
+
</th>
|
|
927
|
+
{/each}
|
|
928
|
+
</tr>
|
|
929
|
+
</thead>
|
|
930
|
+
<tbody class:unselectable={selectionMode === 'RANGE'}>
|
|
931
|
+
{#snippet tableRow(row: R & { originalIndex: number; uuid: string }, index: number)}
|
|
932
|
+
{#if children}
|
|
933
|
+
{@render children({ row, index })}
|
|
934
|
+
{:else}
|
|
935
|
+
<tr
|
|
936
|
+
class:table-primary={rowIsSelected(row, rowSelectionIdProp, selectedRowIds)}
|
|
937
|
+
onclick={() => rowClick(row)}
|
|
938
|
+
>
|
|
939
|
+
{#each computedColumns as column (column.property)}
|
|
940
|
+
<Td
|
|
941
|
+
align={column.align}
|
|
942
|
+
property={column.property}
|
|
943
|
+
>
|
|
944
|
+
{isArbitraryProperty(column.property)
|
|
945
|
+
? ''
|
|
946
|
+
: column.formatter
|
|
947
|
+
? column.formatter(getNestedProperty(row, column.property, ''))
|
|
948
|
+
: getNestedProperty(row, column.property, '')}
|
|
949
|
+
</Td>
|
|
950
|
+
{/each}
|
|
951
|
+
</tr>
|
|
952
|
+
{/if}
|
|
953
|
+
{/snippet}
|
|
954
|
+
{#if body}
|
|
955
|
+
{@render body?.({ rows: currentPageRows, visibleColumnsCount: visibleColumns.length })}
|
|
956
|
+
{:else if !currentPageRows.length}
|
|
957
|
+
{@render noRows?.({ visibleColumnsCount: visibleColumns.length })}
|
|
958
|
+
<!--
|
|
959
|
+
Only key the #each if the idProp is in the original row.
|
|
960
|
+
Otherwise, the table will re-render all rows if idProp is 'uuid' and your original row doesn't have one
|
|
961
|
+
-->
|
|
962
|
+
{:else if idProp in (rows[currentPageRows[0]?.originalIndex] ?? {})}
|
|
963
|
+
{#each currentPageRows as row, index (row[idProp])}
|
|
964
|
+
{@render tableRow(row, index)}
|
|
965
|
+
{/each}
|
|
966
|
+
{:else}
|
|
967
|
+
{#each currentPageRows as row, index}
|
|
968
|
+
{@render tableRow(row, index)}
|
|
969
|
+
{/each}
|
|
970
|
+
{/if}
|
|
971
|
+
</tbody>
|
|
972
|
+
{#if showFooter && footers}
|
|
973
|
+
<tfoot class="font-weight-bold fw-bold">
|
|
974
|
+
<tr class="table-secondary">
|
|
975
|
+
{#if footerRow}
|
|
976
|
+
{@render footerRow({ footers })}
|
|
977
|
+
{:else}
|
|
978
|
+
{#each footers as { property, value } (property)}
|
|
979
|
+
<Td
|
|
980
|
+
{property}
|
|
981
|
+
tagName="th">{value ?? ''}</Td
|
|
982
|
+
>
|
|
983
|
+
{/each}
|
|
984
|
+
{/if}
|
|
985
|
+
</tr>
|
|
986
|
+
</tfoot>
|
|
987
|
+
{/if}
|
|
988
|
+
</table>
|
|
989
|
+
</div>
|
|
990
|
+
|
|
991
|
+
<Pagination
|
|
992
|
+
{perPageCount}
|
|
993
|
+
{totalItemsCount}
|
|
994
|
+
items={sortedRows}
|
|
995
|
+
bind:currentPageItems={() => untrack(() => currentPageRows), items => (currentPageRows = items)}
|
|
996
|
+
bind:currentPageNumber
|
|
997
|
+
bind:lastPageNumber
|
|
998
|
+
{pageChange}
|
|
999
|
+
bind:this={paginationComponent}
|
|
1000
|
+
/>
|
|
1001
|
+
|
|
1002
|
+
<ContextMenu
|
|
1003
|
+
id="hide-column-context-menu"
|
|
1004
|
+
bind:this={contextMenu}
|
|
1005
|
+
>
|
|
1006
|
+
{#if columnPinningEnabled}
|
|
1007
|
+
{@const isPinned = !!contextMenuColumn?.pinned}
|
|
1008
|
+
<DropdownItem
|
|
1009
|
+
icon={isPinned ? 'thumbtack-angle-slash' : 'thumbtack-angle'}
|
|
1010
|
+
title={isPinned
|
|
1011
|
+
? translate('common:tableUnpinColumnTitle', 'Unpinning this column will make it scroll normally')
|
|
1012
|
+
: translate(
|
|
1013
|
+
'common:tablePinColumnTitle',
|
|
1014
|
+
'Pinning this column will keep it visible as you scroll to the right',
|
|
1015
|
+
)}
|
|
1016
|
+
onclick={() => {
|
|
1017
|
+
if (contextMenuColumn) {
|
|
1018
|
+
pinUnpinColumn(contextMenuColumn)
|
|
1019
|
+
}
|
|
1020
|
+
}}
|
|
1021
|
+
>{#if contextMenuColumn?.name}
|
|
1022
|
+
{isPinned
|
|
1023
|
+
? translate('common:tableUnpinColumnName', 'Unpin {{name}}', { name: contextMenuColumn.name })
|
|
1024
|
+
: translate('common:tablePinColumnName', 'Pin {{name}}', { name: contextMenuColumn.name })}
|
|
1025
|
+
{:else}
|
|
1026
|
+
{isPinned ? translate('common:tableUnpinColumn', 'Unpin') : translate('common:tablePinColumn', 'Pin')}
|
|
1027
|
+
{#if contextMenuColumn?.icon}
|
|
1028
|
+
<Icon icon={contextMenuColumn.icon}></Icon>
|
|
1029
|
+
{/if}
|
|
1030
|
+
{/if}
|
|
1031
|
+
</DropdownItem>
|
|
1032
|
+
|
|
1033
|
+
{#if columnHidingEnabled || columnResizingEnabled}
|
|
1034
|
+
<div class="dropdown-divider"></div>
|
|
1035
|
+
{/if}
|
|
1036
|
+
{/if}
|
|
1037
|
+
|
|
1038
|
+
{#if columnHidingEnabled}
|
|
1039
|
+
<DropdownItem
|
|
1040
|
+
class={hideButtonClass}
|
|
1041
|
+
disabled={contextMenuColumn?.unhidable}
|
|
1042
|
+
onclick={() => contextMenuColumn && hideColumn(contextMenuColumn)}
|
|
1043
|
+
>
|
|
1044
|
+
<Icon icon="eye-slash"></Icon>
|
|
1045
|
+
{#if contextMenuColumn?.name}
|
|
1046
|
+
{translate('common:tableHideColumn', 'Hide {{- columnName}}', { columnName: contextMenuColumn.name })}
|
|
1047
|
+
{:else if contextMenuColumn?.icon}
|
|
1048
|
+
{translate('common:hide', 'Hide')}
|
|
1049
|
+
|
|
1050
|
+
<Icon
|
|
1051
|
+
prefix={contextMenuColumn.iconPrefix}
|
|
1052
|
+
icon={contextMenuColumn.icon}
|
|
1053
|
+
></Icon>
|
|
1054
|
+
{/if}
|
|
1055
|
+
</DropdownItem>
|
|
1056
|
+
|
|
1057
|
+
{#if hiddenColumns.length > 0}
|
|
1058
|
+
<div class="dropdown-divider"></div>
|
|
1059
|
+
<h5 class="dropdown-header">
|
|
1060
|
+
<Icon icon="eye"></Icon>
|
|
1061
|
+
{translate('common:tableShowColumns', 'Show Column')}
|
|
1062
|
+
</h5>
|
|
1063
|
+
{/if}
|
|
1064
|
+
<div style="max-height: 300px; overflow-y: auto;">
|
|
1065
|
+
{#each hiddenColumns as hiddenColumn (hiddenColumn.property)}
|
|
1066
|
+
<DropdownItem onclick={() => showColumn(hiddenColumn)}>
|
|
1067
|
+
{@render columnNameAndIcon(hiddenColumn)}
|
|
1068
|
+
</DropdownItem>
|
|
1069
|
+
{/each}
|
|
1070
|
+
</div>
|
|
1071
|
+
|
|
1072
|
+
{#if columnResizingEnabled}
|
|
1073
|
+
<div class="dropdown-divider"></div>
|
|
1074
|
+
{/if}
|
|
1075
|
+
{/if}
|
|
1076
|
+
|
|
1077
|
+
{#if columnResizingEnabled}
|
|
1078
|
+
<DropdownItem
|
|
1079
|
+
icon="broom"
|
|
1080
|
+
onclick={() => {
|
|
1081
|
+
columnInfo.updateColumns(
|
|
1082
|
+
computedColumns.map(col => ({
|
|
1083
|
+
property: col.property,
|
|
1084
|
+
userWidth: undefined,
|
|
1085
|
+
})),
|
|
1086
|
+
)
|
|
1087
|
+
setDefaultColumnWidths()
|
|
1088
|
+
}}>{translate('common:tableResetColumnSizes', 'Reset Column Sizes')}</DropdownItem
|
|
1089
|
+
>
|
|
1090
|
+
{/if}
|
|
1091
|
+
</ContextMenu>
|
|
1092
|
+
|
|
1093
|
+
{#snippet columnNameAndIcon(column: GenericColumn)}
|
|
1094
|
+
<!-- Don't add any whitespace btwn these if blocks to keep the spaces correct -->
|
|
1095
|
+
{#if column.icon && column.iconLeft}
|
|
1096
|
+
<Icon
|
|
1097
|
+
prefix={column.iconPrefix}
|
|
1098
|
+
icon={column.icon}
|
|
1099
|
+
/>{#if column.name} {/if}
|
|
1100
|
+
{/if}{column.name}{#if column.icon && !column.iconLeft}
|
|
1101
|
+
{#if column.name} {/if}<Icon
|
|
1102
|
+
prefix={column.iconPrefix}
|
|
1103
|
+
icon={column.icon}
|
|
1104
|
+
/>
|
|
1105
|
+
{/if}
|
|
1106
|
+
{/snippet}
|
|
1107
|
+
|
|
1108
|
+
<style>
|
|
1109
|
+
table.sticky {
|
|
1110
|
+
position: relative;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
table.sticky th {
|
|
1114
|
+
position: sticky;
|
|
1115
|
+
top: 0;
|
|
1116
|
+
box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.4);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
table.sticky > thead > tr > th {
|
|
1120
|
+
z-index: 3;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/* Remove bottom margins created by form-groups in the table (BS4 only) */
|
|
1124
|
+
table > tbody :global(.form-group) {
|
|
1125
|
+
margin-bottom: 0%;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/* BS5 removed .form-group, which makes it a little trickier to target the label's parent div, but this should do it
|
|
1129
|
+
- The Label component adds mb-3 to the parent div if the label is hidden or mb-1 if it's shown (unlikely if it's in a table)
|
|
1130
|
+
- That div will have a .input-group, .form-control, or .form-select as a direct descendant
|
|
1131
|
+
*/
|
|
1132
|
+
table
|
|
1133
|
+
> tbody
|
|
1134
|
+
:global(
|
|
1135
|
+
td div:where(.mb-3, .mb-1):not(.form-group):has(> :where(.input-group, .form-control, .form-select, .btn-group))
|
|
1136
|
+
) {
|
|
1137
|
+
margin-bottom: 0% !important;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
.resizer {
|
|
1141
|
+
position: absolute;
|
|
1142
|
+
top: 0;
|
|
1143
|
+
right: 0;
|
|
1144
|
+
width: 10px;
|
|
1145
|
+
cursor: col-resize;
|
|
1146
|
+
user-select: none;
|
|
1147
|
+
}
|
|
1148
|
+
.resizer:hover,
|
|
1149
|
+
.resizing {
|
|
1150
|
+
border-right: 5px solid darkslategray;
|
|
1151
|
+
}
|
|
1152
|
+
.table-layout-fixed {
|
|
1153
|
+
table-layout: fixed;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
th.pinned {
|
|
1157
|
+
position: sticky;
|
|
1158
|
+
z-index: 11 !important;
|
|
1159
|
+
left: var(--pinned-column-offset, -1);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
.cursor-pointer {
|
|
1163
|
+
cursor: pointer;
|
|
1164
|
+
}
|
|
1165
|
+
</style>
|