svelte-tably 1.0.0-next.8 → 1.0.0-next.9
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 +2 -1
- package/dist/Column.svelte +94 -49
- package/dist/Column.svelte.d.ts +66 -26
- package/dist/Table.svelte +390 -247
- package/dist/Table.svelte.d.ts +11 -53
- package/package.json +1 -1
package/dist/Table.svelte
CHANGED
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
|
|
9
9
|
-->
|
|
10
10
|
|
|
11
|
-
<script module lang=
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
<script module lang="ts">
|
|
12
|
+
export interface TableState<
|
|
13
|
+
T extends Record<PropertyKey, any> = Record<PropertyKey, any>
|
|
14
|
+
> {
|
|
15
|
+
columns: Record<string, ColumnState<T>>
|
|
15
16
|
panels: Record<string, TPanel<T>>
|
|
16
17
|
selected: T[] | null
|
|
17
18
|
sortby?: string
|
|
19
|
+
sortReverse: boolean
|
|
18
20
|
positions: {
|
|
19
21
|
sticky: string[]
|
|
20
22
|
scroll: string[]
|
|
@@ -25,7 +27,7 @@
|
|
|
25
27
|
readonly data: T[]
|
|
26
28
|
/** Rows become anchors */
|
|
27
29
|
readonly href?: (item: T) => string
|
|
28
|
-
addColumn(key: string, options:
|
|
30
|
+
addColumn(key: string, options: ColumnState<T>): void
|
|
29
31
|
removeColumn(key: string): void
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -33,13 +35,13 @@
|
|
|
33
35
|
return getContext<TableState<T>>('svelte5-table')
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
export type HeaderSelectCtx<T = any> = {
|
|
37
|
-
isSelected: boolean
|
|
38
|
+
export type HeaderSelectCtx<T = any> = {
|
|
39
|
+
isSelected: boolean
|
|
38
40
|
/** The list of selected items */
|
|
39
41
|
readonly selected: T[]
|
|
40
42
|
/**
|
|
41
43
|
* See [MDN :indeterminate](https://developer.mozilla.org/en-US/docs/Web/CSS/:indeterminate)
|
|
42
|
-
|
|
44
|
+
*/
|
|
43
45
|
readonly indeterminate: boolean
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -49,22 +51,33 @@
|
|
|
49
51
|
data: T[]
|
|
50
52
|
isSelected: boolean
|
|
51
53
|
}
|
|
52
|
-
|
|
53
54
|
</script>
|
|
54
55
|
|
|
55
|
-
<script lang=
|
|
56
|
-
|
|
56
|
+
<script lang="ts">
|
|
57
57
|
import { getContext, onMount, setContext, tick, untrack, type Snippet } from 'svelte'
|
|
58
|
-
import Column, { type
|
|
58
|
+
import Column, { type ColumnProps, type RowCtx, type ColumnState } from './Column.svelte'
|
|
59
59
|
import Panel, { PanelTween, type Panel as TPanel } from './Panel.svelte'
|
|
60
60
|
import { fly } from 'svelte/transition'
|
|
61
61
|
import { sineInOut } from 'svelte/easing'
|
|
62
|
-
import
|
|
62
|
+
import { on } from 'svelte/events'
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
type T = $$Generic<Record<PropertyKey, unknown>>
|
|
65
|
+
|
|
66
|
+
type ConstructorReturnType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer K ? K : never
|
|
67
|
+
type ConstructorParams<T extends new (...args: any[]) => any> = T extends new (...args: infer K) => any ? K : never
|
|
68
|
+
|
|
69
|
+
type ContentCtx<T extends Record<PropertyKey, unknown>> = {
|
|
70
|
+
Column: {
|
|
71
|
+
new <V>(...args: ConstructorParams<typeof Column<T, V>>): ConstructorReturnType<typeof Column<T, V>>
|
|
72
|
+
<V>(...args: Parameters<typeof Column<T, V>>): ReturnType<typeof Column<T, V>>
|
|
73
|
+
}
|
|
74
|
+
Panel: typeof Panel
|
|
75
|
+
readonly table: TableState<T>
|
|
76
|
+
readonly data: T[]
|
|
77
|
+
}
|
|
65
78
|
|
|
66
79
|
interface Props {
|
|
67
|
-
content: Snippet<[context:
|
|
80
|
+
content: Snippet<[context: ContentCtx<T>]>
|
|
68
81
|
|
|
69
82
|
panel?: string
|
|
70
83
|
data?: T[]
|
|
@@ -73,38 +86,40 @@
|
|
|
73
86
|
/**
|
|
74
87
|
* Can you change the width of the columns?
|
|
75
88
|
* @default true
|
|
76
|
-
|
|
89
|
+
*/
|
|
77
90
|
resizeable?: boolean
|
|
78
91
|
|
|
79
92
|
selected?: T[]
|
|
80
|
-
select?:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
93
|
+
select?:
|
|
94
|
+
| boolean
|
|
95
|
+
| {
|
|
96
|
+
/**
|
|
97
|
+
* The style, in which the selection is shown
|
|
98
|
+
*
|
|
99
|
+
* NOTE: If using `edge` | 'side', "show" will always be `hover`. This is due to
|
|
100
|
+
* an inconsistency/limitation of matching the scroll between the selection div and the rows.
|
|
101
|
+
*
|
|
102
|
+
* @default 'column'
|
|
103
|
+
*/
|
|
104
|
+
style?: 'column'
|
|
105
|
+
/**
|
|
106
|
+
* When to show the row-select, when not selected?
|
|
107
|
+
* @default 'hover'
|
|
108
|
+
*/
|
|
109
|
+
show?: 'hover' | 'always' | 'never'
|
|
110
|
+
/**
|
|
111
|
+
* Custom snippet
|
|
112
|
+
*/
|
|
113
|
+
headerSnippet?: Snippet<[context: HeaderSelectCtx]>
|
|
114
|
+
rowSnippet?: Snippet<[context: RowSelectCtx<T>]>
|
|
115
|
+
}
|
|
101
116
|
// | {
|
|
102
117
|
// /**
|
|
103
118
|
// * The style, in which the selection is shown
|
|
104
|
-
// *
|
|
119
|
+
// *
|
|
105
120
|
// * NOTE: If using `edge` | 'side', "show" will always be `hover`. This is due to
|
|
106
121
|
// * an inconsistency/limitation of matching the scroll between the selection div and the rows.
|
|
107
|
-
// *
|
|
122
|
+
// *
|
|
108
123
|
// * @default 'column'
|
|
109
124
|
// */
|
|
110
125
|
// style?: 'edge' | 'side'
|
|
@@ -133,24 +148,85 @@
|
|
|
133
148
|
selected = $bindable([]),
|
|
134
149
|
panel = $bindable(),
|
|
135
150
|
data: _data = [],
|
|
136
|
-
id = Array.from({length: 12}, () => String.fromCharCode(Math.floor(Math.random() * 26) + 97)).join(''),
|
|
151
|
+
id = Array.from({ length: 12 }, () => String.fromCharCode(Math.floor(Math.random() * 26) + 97)).join(''),
|
|
137
152
|
href,
|
|
138
153
|
resizeable = true,
|
|
139
154
|
select
|
|
140
155
|
}: Props = $props()
|
|
141
|
-
|
|
156
|
+
|
|
142
157
|
let mounted = $state(false)
|
|
143
|
-
onMount(() => mounted = true)
|
|
158
|
+
onMount(() => (mounted = true))
|
|
144
159
|
|
|
145
160
|
const data = $derived([..._data])
|
|
146
161
|
|
|
147
|
-
const elements = $state({}) as Record<
|
|
162
|
+
const elements = $state({}) as Record<
|
|
163
|
+
'headers' | 'statusbar' | 'rows' | 'virtualTop' | 'virtualBottom' | 'selects',
|
|
164
|
+
HTMLElement
|
|
165
|
+
>
|
|
148
166
|
|
|
167
|
+
let cols: TableState<T>['columns'] = $state({})
|
|
168
|
+
let positions: TableState<T>['positions'] = $state({
|
|
169
|
+
fixed: [],
|
|
170
|
+
sticky: [],
|
|
171
|
+
scroll: [],
|
|
172
|
+
hidden: [],
|
|
173
|
+
toggle(key) {
|
|
174
|
+
if (table.positions.hidden.includes(key))
|
|
175
|
+
table.positions.hidden = table.positions.hidden.filter((column) => column !== key)
|
|
176
|
+
else table.positions.hidden.push(key)
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const table: TableState<T> = $state({
|
|
181
|
+
columns: cols,
|
|
182
|
+
selected,
|
|
183
|
+
panels: {},
|
|
184
|
+
positions,
|
|
185
|
+
sortReverse: false,
|
|
186
|
+
get href() {
|
|
187
|
+
return href
|
|
188
|
+
},
|
|
189
|
+
get data() {
|
|
190
|
+
return data
|
|
191
|
+
},
|
|
192
|
+
get resizeable() {
|
|
193
|
+
return resizeable
|
|
194
|
+
},
|
|
195
|
+
addColumn(key, column) {
|
|
196
|
+
table.columns[key] = column
|
|
197
|
+
|
|
198
|
+
if (column.defaults.sort) sortBy(key)
|
|
199
|
+
|
|
200
|
+
if (column.fixed) {
|
|
201
|
+
// @ts-expect-error
|
|
202
|
+
table.positions.fixed.push(key)
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!column.defaults.show) table.positions.hidden.push(key)
|
|
207
|
+
|
|
208
|
+
if (column.defaults.sticky) table.positions.sticky.push(key)
|
|
209
|
+
else table.positions.scroll.push(key)
|
|
210
|
+
},
|
|
211
|
+
removeColumn(key) {
|
|
212
|
+
delete table.columns[key]
|
|
213
|
+
// @ts-expect-error fixed is not typed
|
|
214
|
+
table.positions.fixed = table.positions.fixed.filter((column) => column !== key)
|
|
215
|
+
table.positions.sticky = table.positions.sticky.filter((column) => column !== key)
|
|
216
|
+
table.positions.scroll = table.positions.scroll.filter((column) => column !== key)
|
|
217
|
+
table.positions.hidden = table.positions.hidden.filter((column) => column !== key)
|
|
218
|
+
}
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
setContext('svelte5-table', table)
|
|
222
|
+
|
|
223
|
+
// * --- *
|
|
149
224
|
|
|
150
225
|
// * --- Virtualization --- *
|
|
226
|
+
// #region Virtualization
|
|
151
227
|
let scrollTop = $state(0)
|
|
152
228
|
let viewportHeight = $state(0)
|
|
153
|
-
|
|
229
|
+
|
|
154
230
|
let heightPerItem = $state(8)
|
|
155
231
|
|
|
156
232
|
const spacing = () => viewportHeight / 2
|
|
@@ -161,7 +237,7 @@
|
|
|
161
237
|
return result
|
|
162
238
|
})
|
|
163
239
|
let virtualBottom = $derived.by(() => {
|
|
164
|
-
let result =
|
|
240
|
+
let result = heightPerItem * data.length - virtualTop - spacing() * 4
|
|
165
241
|
result = Math.max(result, 0)
|
|
166
242
|
return result
|
|
167
243
|
})
|
|
@@ -170,23 +246,23 @@
|
|
|
170
246
|
|
|
171
247
|
/** The area of data being rendered */
|
|
172
248
|
let area = $derived.by(() => {
|
|
173
|
-
|
|
249
|
+
table.sortReverse
|
|
250
|
+
table.sortby
|
|
251
|
+
const index = virtualTop / heightPerItem || 0
|
|
174
252
|
const end = index + renderItemLength
|
|
175
|
-
const result = data.slice(
|
|
176
|
-
index,
|
|
177
|
-
end
|
|
178
|
-
)
|
|
253
|
+
const result = data.slice(index, end)
|
|
179
254
|
return result
|
|
180
255
|
})
|
|
181
256
|
|
|
182
257
|
function calculateHeightPerItem() {
|
|
183
|
-
if(!elements.rows) {
|
|
258
|
+
if (!elements.rows) {
|
|
184
259
|
heightPerItem = 8
|
|
185
260
|
return
|
|
186
261
|
}
|
|
187
262
|
tick().then(() => {
|
|
188
263
|
const firstRow = elements.rows.children[0].getBoundingClientRect().top
|
|
189
|
-
const lastRow =
|
|
264
|
+
const lastRow =
|
|
265
|
+
elements.rows.children[elements.rows.children.length - 1].getBoundingClientRect().bottom
|
|
190
266
|
heightPerItem = (lastRow - firstRow) / area.length
|
|
191
267
|
})
|
|
192
268
|
}
|
|
@@ -195,71 +271,49 @@
|
|
|
195
271
|
data
|
|
196
272
|
untrack(calculateHeightPerItem)
|
|
197
273
|
})
|
|
274
|
+
// #endregion
|
|
198
275
|
// * --- Virtualization --- *
|
|
199
276
|
|
|
200
|
-
let cols: TableState<T>['columns'] = $state({})
|
|
201
|
-
let positions: TableState<T>['positions'] = $state({
|
|
202
|
-
fixed: [],
|
|
203
|
-
sticky: [],
|
|
204
|
-
scroll: [],
|
|
205
|
-
hidden: [],
|
|
206
|
-
toggle(key) {
|
|
207
|
-
if(table.positions.hidden.includes(key))
|
|
208
|
-
table.positions.hidden = table.positions.hidden.filter(column => column !== key)
|
|
209
|
-
else
|
|
210
|
-
table.positions.hidden.push(key)
|
|
211
|
-
}
|
|
212
|
-
})
|
|
213
277
|
|
|
214
278
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
panels: {},
|
|
219
|
-
positions,
|
|
220
|
-
get href() {
|
|
221
|
-
return href
|
|
222
|
-
},
|
|
223
|
-
get data() {
|
|
224
|
-
return data
|
|
225
|
-
},
|
|
226
|
-
get resizeable() {
|
|
227
|
-
return resizeable
|
|
228
|
-
},
|
|
229
|
-
addColumn(key, column) {
|
|
230
|
-
table.columns[key] = column
|
|
279
|
+
function sortBy(column: string) {
|
|
280
|
+
const { sorting, value } = table.columns[column]!.options
|
|
281
|
+
if(!sorting || !value) return
|
|
231
282
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
283
|
+
if (table.sortby === column) {
|
|
284
|
+
table.sortReverse = !table.sortReverse
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
table.sortReverse = false
|
|
288
|
+
table.sortby = column
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function sortAction(node: HTMLElement, column: string) {
|
|
292
|
+
$effect(() => on(node, 'click', () => sortBy(column)))
|
|
293
|
+
}
|
|
240
294
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
},
|
|
249
|
-
removeColumn(key) {
|
|
250
|
-
delete table.columns[key]
|
|
251
|
-
// @ts-expect-error fixed is not typed
|
|
252
|
-
table.positions.fixed = table.positions.fixed.filter(column => column !== key)
|
|
253
|
-
table.positions.sticky = table.positions.sticky.filter(column => column !== key)
|
|
254
|
-
table.positions.scroll = table.positions.scroll.filter(column => column !== key)
|
|
255
|
-
table.positions.hidden = table.positions.hidden.filter(column => column !== key)
|
|
295
|
+
function sortTable() {
|
|
296
|
+
if (!table.sortby) return
|
|
297
|
+
const column = table.columns[table.sortby]
|
|
298
|
+
let { sorting, value } = column.options
|
|
299
|
+
if(!sorting || !value) return
|
|
300
|
+
if(sorting === true) {
|
|
301
|
+
sorting = (a, b) => String(a).localeCompare(String(b))
|
|
256
302
|
}
|
|
257
|
-
|
|
303
|
+
if(table.sortReverse) {
|
|
304
|
+
data.sort((a, b) => sorting(value(b), value(a)))
|
|
305
|
+
} else {
|
|
306
|
+
data.sort((a, b) => sorting(value(a), value(b)))
|
|
307
|
+
}
|
|
308
|
+
}
|
|
258
309
|
|
|
259
|
-
|
|
310
|
+
$effect.pre(() => {
|
|
311
|
+
data
|
|
312
|
+
table.sortby
|
|
313
|
+
table.sortReverse
|
|
314
|
+
untrack(sortTable)
|
|
315
|
+
})
|
|
260
316
|
|
|
261
|
-
// * --- *
|
|
262
|
-
|
|
263
317
|
const panelTween = new PanelTween(() => panel, 24)
|
|
264
318
|
|
|
265
319
|
let hoveredRow: T | null = $state(null)
|
|
@@ -273,47 +327,49 @@
|
|
|
273
327
|
const notHidden = (key: string) => !positions.hidden.includes(key)
|
|
274
328
|
const sticky = $derived(positions.sticky.filter(notHidden))
|
|
275
329
|
const scrolled = $derived(positions.scroll.filter(notHidden))
|
|
276
|
-
const columns = $derived([
|
|
330
|
+
const columns = $derived([...fixed, ...sticky, ...scrolled])
|
|
277
331
|
|
|
278
332
|
/** Width of each column */
|
|
279
333
|
const columnWidths = $state({}) as Record<string, number>
|
|
280
334
|
|
|
281
|
-
const getWidth = (key: string, def: number = 150) =>
|
|
335
|
+
const getWidth = (key: string, def: number = 150) =>
|
|
336
|
+
columnWidths[key] || table.columns[key]?.defaults.width || def
|
|
282
337
|
|
|
283
338
|
/** grid-template-columns for widths */
|
|
284
339
|
const style = $derived.by(() => {
|
|
285
|
-
if(!mounted) return ''
|
|
340
|
+
if (!mounted) return ''
|
|
286
341
|
const templateColumns = `
|
|
287
342
|
#${id} > .headers,
|
|
288
343
|
#${id} > tbody > .row,
|
|
289
344
|
#${id} > tfoot > tr,
|
|
290
345
|
#${id} > .content > .virtual.bottom {
|
|
291
|
-
grid-template-columns: ${
|
|
292
|
-
|
|
346
|
+
grid-template-columns: ${columns
|
|
347
|
+
.map((key, i, arr) => {
|
|
293
348
|
const width = getWidth(key)
|
|
294
|
-
if(i === arr.length - 1)
|
|
295
|
-
return `minmax(${width}px, 1fr)`
|
|
349
|
+
if (i === arr.length - 1) return `minmax(${width}px, 1fr)`
|
|
296
350
|
return `${width}px`
|
|
297
|
-
})
|
|
298
|
-
|
|
351
|
+
})
|
|
352
|
+
.join(' ')};
|
|
299
353
|
}
|
|
300
354
|
`
|
|
301
|
-
|
|
355
|
+
|
|
302
356
|
let sum = 0
|
|
303
|
-
const stickyLeft = [...fixed, ...sticky]
|
|
304
|
-
|
|
305
|
-
|
|
357
|
+
const stickyLeft = [...fixed, ...sticky]
|
|
358
|
+
.map((key, i, arr) => {
|
|
359
|
+
sum += getWidth(arr[i - 1], i === 0 ? 0 : undefined)
|
|
360
|
+
return `
|
|
306
361
|
#${id} .column.sticky[data-column='${key}'] {
|
|
307
362
|
left: ${sum}px;
|
|
308
363
|
}
|
|
309
364
|
`
|
|
310
|
-
|
|
365
|
+
})
|
|
366
|
+
.join('')
|
|
311
367
|
|
|
312
368
|
return templateColumns + stickyLeft
|
|
313
369
|
})
|
|
314
370
|
|
|
315
371
|
function observeColumnWidth(node: HTMLDivElement, isHeader = false) {
|
|
316
|
-
if(!isHeader) return
|
|
372
|
+
if (!isHeader) return
|
|
317
373
|
|
|
318
374
|
const key = node.getAttribute('data-column')!
|
|
319
375
|
node.style.width = getWidth(key) + 'px'
|
|
@@ -322,214 +378,276 @@
|
|
|
322
378
|
columnWidths[key] = parseFloat(node.style.width)
|
|
323
379
|
})
|
|
324
380
|
|
|
325
|
-
observer.observe(node, {attributes: true})
|
|
381
|
+
observer.observe(node, { attributes: true })
|
|
326
382
|
return { destroy: () => observer.disconnect() }
|
|
327
383
|
}
|
|
328
384
|
|
|
329
385
|
async function onscroll() {
|
|
330
386
|
const target = elements.rows
|
|
331
|
-
if(target.scrollTop !== scrollTop) {
|
|
387
|
+
if (target.scrollTop !== scrollTop) {
|
|
332
388
|
scrollTop = target?.scrollTop ?? scrollTop
|
|
333
389
|
}
|
|
334
390
|
|
|
335
|
-
if(elements.selects) {
|
|
391
|
+
if (elements.selects) {
|
|
336
392
|
elements.selects.scrollTop = target?.scrollTop
|
|
337
393
|
}
|
|
338
|
-
|
|
339
|
-
if(!elements.headers) return
|
|
394
|
+
|
|
395
|
+
if (!elements.headers) return
|
|
340
396
|
elements.headers.scrollLeft = target.scrollLeft
|
|
341
397
|
elements.statusbar.scrollLeft = target.scrollLeft
|
|
342
398
|
}
|
|
343
399
|
|
|
344
|
-
|
|
345
|
-
export {
|
|
346
|
-
selected,
|
|
347
|
-
positions,
|
|
348
|
-
data,
|
|
349
|
-
href,
|
|
350
|
-
cols as columns
|
|
351
|
-
}
|
|
352
|
-
|
|
400
|
+
export { selected, positions, data, href, cols as columns }
|
|
353
401
|
</script>
|
|
402
|
+
|
|
354
403
|
<!---------------------------------------------------->
|
|
355
404
|
|
|
356
405
|
<svelte:head>
|
|
357
406
|
{@html `<style>${style}</style>`}
|
|
358
407
|
</svelte:head>
|
|
359
408
|
|
|
409
|
+
{#snippet chevronSnippet(reversed: boolean)}
|
|
410
|
+
<svg
|
|
411
|
+
class='sorting-icon'
|
|
412
|
+
class:reversed
|
|
413
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
414
|
+
width="16"
|
|
415
|
+
height="16"
|
|
416
|
+
viewBox="0 0 16 16"
|
|
417
|
+
style='margin: auto; margin-right: var(--tably-padding-x, 1rem);'
|
|
418
|
+
>
|
|
419
|
+
<path
|
|
420
|
+
fill="currentColor"
|
|
421
|
+
d="M3.2 5.74a.75.75 0 0 1 1.06-.04L8 9.227L11.74 5.7a.75.75 0 1 1 1.02 1.1l-4.25 4a.75.75 0 0 1-1.02 0l-4.25-4a.75.75 0 0 1-.04-1.06"
|
|
422
|
+
/>
|
|
423
|
+
</svg>
|
|
424
|
+
{/snippet}
|
|
425
|
+
|
|
360
426
|
{#snippet columnsSnippet(
|
|
361
|
-
renderable: (column: string) => Snippet<[arg0?: any, arg1?: any]> | undefined,
|
|
427
|
+
renderable: (column: string) => Snippet<[arg0?: any, arg1?: any]> | undefined,
|
|
362
428
|
arg: null | ((column: string) => any[]) = null,
|
|
363
429
|
isHeader = false
|
|
364
430
|
)}
|
|
365
431
|
{#each fixed as column, i (column)}
|
|
366
432
|
{#if !hidden.includes(column)}
|
|
367
433
|
{@const args = arg ? arg(column) : []}
|
|
434
|
+
{@const sortable = isHeader && table.columns[column]!.options.sorting}
|
|
435
|
+
{@const sortClick = isHeader ? sortAction : ()=>{}}
|
|
368
436
|
<svelte:element
|
|
369
437
|
this={isHeader ? 'th' : 'td'}
|
|
370
|
-
class=
|
|
438
|
+
class="column sticky fixed"
|
|
371
439
|
data-column={column}
|
|
372
440
|
class:header={isHeader}
|
|
441
|
+
class:sortable={sortable}
|
|
442
|
+
use:sortClick={column}
|
|
373
443
|
>
|
|
374
444
|
{@render renderable(column)?.(args[0], args[1])}
|
|
445
|
+
{#if isHeader && table.sortby === column && sortable}
|
|
446
|
+
{@render chevronSnippet(table.sortReverse)}
|
|
447
|
+
{/if}
|
|
375
448
|
</svelte:element>
|
|
376
449
|
{/if}
|
|
377
450
|
{/each}
|
|
378
451
|
{#each sticky as column, i (column)}
|
|
379
452
|
{#if !hidden.includes(column)}
|
|
380
453
|
{@const args = arg ? arg(column) : []}
|
|
454
|
+
{@const sortable = isHeader && table.columns[column]!.options.sorting}
|
|
455
|
+
{@const sortClick = isHeader ? sortAction : ()=>{}}
|
|
381
456
|
<svelte:element
|
|
382
457
|
this={isHeader ? 'th' : 'td'}
|
|
383
|
-
class=
|
|
458
|
+
class="column sticky"
|
|
384
459
|
use:observeColumnWidth={isHeader}
|
|
385
460
|
data-column={column}
|
|
386
461
|
class:header={isHeader}
|
|
387
462
|
class:resizeable={isHeader && table.columns[column].options.resizeable && table.resizeable}
|
|
388
463
|
class:border={i == sticky.length - 1}
|
|
464
|
+
class:sortable={sortable}
|
|
465
|
+
use:sortClick={column}
|
|
389
466
|
>
|
|
390
467
|
{@render renderable(column)?.(args[0], args[1])}
|
|
468
|
+
{#if isHeader && table.sortby === column && sortable}
|
|
469
|
+
{@render chevronSnippet(table.sortReverse)}
|
|
470
|
+
{/if}
|
|
391
471
|
</svelte:element>
|
|
392
472
|
{/if}
|
|
393
473
|
{/each}
|
|
394
474
|
{#each scrolled as column, i (column)}
|
|
395
475
|
{#if !hidden.includes(column)}
|
|
396
476
|
{@const args = arg ? arg(column) : []}
|
|
477
|
+
{@const sortable = isHeader && table.columns[column]!.options.sorting}
|
|
478
|
+
{@const sortClick = isHeader ? sortAction : ()=>{}}
|
|
397
479
|
<svelte:element
|
|
398
480
|
this={isHeader ? 'th' : 'td'}
|
|
399
|
-
class=
|
|
481
|
+
class="column"
|
|
400
482
|
data-column={column}
|
|
401
483
|
use:observeColumnWidth={isHeader}
|
|
402
484
|
class:resizeable={isHeader && table.columns[column].options.resizeable && table.resizeable}
|
|
485
|
+
class:sortable={sortable}
|
|
486
|
+
use:sortClick={column}
|
|
403
487
|
>
|
|
404
488
|
{@render renderable(column)?.(args[0], args[1])}
|
|
489
|
+
{#if isHeader && table.sortby === column && sortable}
|
|
490
|
+
{@render chevronSnippet(table.sortReverse)}
|
|
491
|
+
{/if}
|
|
405
492
|
</svelte:element>
|
|
406
493
|
{/if}
|
|
407
494
|
{/each}
|
|
408
495
|
{/snippet}
|
|
409
496
|
|
|
410
497
|
<table
|
|
411
|
-
|
|
412
|
-
class=
|
|
413
|
-
style=
|
|
414
|
-
aria-rowcount=
|
|
498
|
+
{id}
|
|
499
|
+
class="table svelte-tably"
|
|
500
|
+
style="--t: {virtualTop}px; --b: {virtualBottom}px;"
|
|
501
|
+
aria-rowcount={data.length}
|
|
415
502
|
>
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
503
|
+
<thead class="headers" bind:this={elements.headers}>
|
|
504
|
+
{@render columnsSnippet(
|
|
505
|
+
(column) => table.columns[column]?.header,
|
|
506
|
+
() => [true],
|
|
507
|
+
true
|
|
508
|
+
)}
|
|
419
509
|
</thead>
|
|
420
510
|
|
|
421
|
-
<tbody class=
|
|
511
|
+
<tbody class="content" bind:this={elements.rows} onscrollcapture={onscroll} bind:clientHeight={viewportHeight}>
|
|
422
512
|
{#each area as item, i (item)}
|
|
423
513
|
{@const props = table.href ? { href: table.href(item) } : {}}
|
|
424
514
|
{@const index = data.indexOf(item) + 1}
|
|
425
515
|
<svelte:element
|
|
426
516
|
this={table.href ? 'a' : 'tr'}
|
|
427
|
-
class=
|
|
517
|
+
class="row"
|
|
428
518
|
class:hover={hoveredRow === item}
|
|
429
519
|
class:selected={table.selected?.includes(item)}
|
|
430
520
|
class:first={i === 0}
|
|
431
521
|
class:last={i === area.length - 1}
|
|
432
522
|
{...props}
|
|
433
|
-
aria-rowindex=
|
|
434
|
-
onpointerenter={() => hoveredRow = item}
|
|
435
|
-
onpointerleave={() => hoveredRow = null}
|
|
523
|
+
aria-rowindex={index}
|
|
524
|
+
onpointerenter={() => (hoveredRow = item)}
|
|
525
|
+
onpointerleave={() => (hoveredRow = null)}
|
|
436
526
|
>
|
|
437
527
|
{@render columnsSnippet(
|
|
438
528
|
(column) => table.columns[column]!.row,
|
|
439
529
|
(column) => {
|
|
440
530
|
const col = table.columns[column]!
|
|
441
|
-
return [
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
531
|
+
return [
|
|
532
|
+
item,
|
|
533
|
+
{
|
|
534
|
+
get index() {
|
|
535
|
+
return index - 1
|
|
536
|
+
},
|
|
537
|
+
get value() {
|
|
538
|
+
return col.options.value ? col.options.value(item) : undefined
|
|
539
|
+
},
|
|
540
|
+
get isHovered() {
|
|
541
|
+
return hoveredRow === item
|
|
542
|
+
},
|
|
543
|
+
get selected() {
|
|
544
|
+
return table.selected?.includes(item)
|
|
545
|
+
},
|
|
546
|
+
set selected(value) {
|
|
547
|
+
value ?
|
|
548
|
+
table.selected!.push(item)
|
|
549
|
+
: table.selected!.splice(table.selected!.indexOf(item), 1)
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
]
|
|
448
553
|
}
|
|
449
554
|
)}
|
|
450
555
|
</svelte:element>
|
|
451
556
|
{/each}
|
|
452
557
|
</tbody>
|
|
453
|
-
|
|
454
|
-
<tfoot class=
|
|
558
|
+
|
|
559
|
+
<tfoot class="statusbar" bind:this={elements.statusbar}>
|
|
455
560
|
<tr>
|
|
456
561
|
{@render columnsSnippet((column) => table.columns[column]?.statusbar)}
|
|
457
562
|
</tr>
|
|
458
563
|
</tfoot>
|
|
459
564
|
|
|
460
|
-
<caption
|
|
565
|
+
<caption
|
|
566
|
+
class="panel"
|
|
567
|
+
style="width: {panelTween.current}px;"
|
|
568
|
+
style:overflow={panelTween.transitioning ? 'hidden' : 'auto'}
|
|
569
|
+
>
|
|
461
570
|
{#if panel && panel in table.panels}
|
|
462
|
-
<div
|
|
463
|
-
class=
|
|
571
|
+
<div
|
|
572
|
+
class="panel-content"
|
|
464
573
|
bind:clientWidth={panelTween.width}
|
|
465
|
-
in:fly={{ x: 100, easing: sineInOut, duration:300 }}
|
|
466
|
-
out:fly={{ x:100, duration:200, easing: sineInOut }}
|
|
574
|
+
in:fly={{ x: 100, easing: sineInOut, duration: 300 }}
|
|
575
|
+
out:fly={{ x: 100, duration: 200, easing: sineInOut }}
|
|
467
576
|
>
|
|
468
|
-
{@render table.panels[panel].content({
|
|
577
|
+
{@render table.panels[panel].content({
|
|
578
|
+
get table() {
|
|
579
|
+
return table
|
|
580
|
+
},
|
|
581
|
+
get data() {
|
|
582
|
+
return data
|
|
583
|
+
}
|
|
584
|
+
})}
|
|
469
585
|
</div>
|
|
470
586
|
{/if}
|
|
471
587
|
</caption>
|
|
472
|
-
<caption
|
|
473
|
-
|
|
474
|
-
aria-hidden={panel && table.panels[panel]?.backdrop ? false : true}
|
|
475
|
-
>
|
|
476
|
-
<button
|
|
477
|
-
aria-label='Panel backdrop'
|
|
478
|
-
tabindex='-1'
|
|
479
|
-
onclick={() => panel = undefined}
|
|
480
|
-
></button>
|
|
588
|
+
<caption class="backdrop" aria-hidden={panel && table.panels[panel]?.backdrop ? false : true}>
|
|
589
|
+
<button aria-label="Panel backdrop" tabindex="-1" onclick={() => (panel = undefined)}></button>
|
|
481
590
|
</caption>
|
|
482
591
|
</table>
|
|
483
592
|
|
|
484
593
|
{#snippet headerSelected(ctx: HeaderSelectCtx<T>)}
|
|
485
|
-
<input
|
|
486
|
-
type='checkbox'
|
|
487
|
-
indeterminate={ctx.indeterminate}
|
|
488
|
-
bind:checked={ctx.isSelected}
|
|
489
|
-
/>
|
|
594
|
+
<input type="checkbox" indeterminate={ctx.indeterminate} bind:checked={ctx.isSelected} />
|
|
490
595
|
{/snippet}
|
|
491
596
|
|
|
492
597
|
{#snippet rowSelected(ctx: RowSelectCtx<T>)}
|
|
493
|
-
<input
|
|
494
|
-
type='checkbox'
|
|
495
|
-
bind:checked={ctx.isSelected}
|
|
496
|
-
/>
|
|
598
|
+
<input type="checkbox" bind:checked={ctx.isSelected} />
|
|
497
599
|
{/snippet}
|
|
498
600
|
|
|
499
601
|
{#if select}
|
|
500
|
-
{@const {
|
|
602
|
+
{@const {
|
|
603
|
+
show = 'hover',
|
|
604
|
+
style = 'column',
|
|
605
|
+
rowSnippet = rowSelected,
|
|
606
|
+
headerSnippet = headerSelected
|
|
607
|
+
} = typeof select === 'boolean' ? {} : select}
|
|
501
608
|
{#if show !== 'never'}
|
|
502
|
-
<Column id=
|
|
609
|
+
<Column id="__fixed" {table} fixed width={56} resizeable={false}>
|
|
503
610
|
{#snippet header()}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
611
|
+
<div class="__fixed">
|
|
612
|
+
{@render headerSnippet({
|
|
613
|
+
get isSelected() {
|
|
614
|
+
return table.data.length === table.selected?.length
|
|
615
|
+
},
|
|
616
|
+
set isSelected(value) {
|
|
617
|
+
if (value) {
|
|
618
|
+
table.selected = table.data
|
|
619
|
+
} else {
|
|
620
|
+
table.selected = []
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
get selected() {
|
|
624
|
+
return table.selected!
|
|
625
|
+
},
|
|
626
|
+
get indeterminate() {
|
|
627
|
+
return (table.selected?.length || 0) > 0 && table.data.length !== table.selected?.length
|
|
513
628
|
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
return table.selected!
|
|
517
|
-
},
|
|
518
|
-
get indeterminate() {
|
|
519
|
-
return (table.selected?.length || 0) > 0
|
|
520
|
-
&& table.data.length !== table.selected?.length
|
|
521
|
-
}
|
|
522
|
-
})}
|
|
629
|
+
})}
|
|
630
|
+
</div>
|
|
523
631
|
{/snippet}
|
|
524
632
|
{#snippet row(item, row)}
|
|
525
|
-
<div class=
|
|
633
|
+
<div class="__fixed">
|
|
526
634
|
{#if row.selected || show === 'always' || (row.isHovered && show === 'hover')}
|
|
527
635
|
{@render rowSnippet({
|
|
528
|
-
get isSelected() {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
636
|
+
get isSelected() {
|
|
637
|
+
return row.selected
|
|
638
|
+
},
|
|
639
|
+
set isSelected(value) {
|
|
640
|
+
row.selected = value
|
|
641
|
+
},
|
|
642
|
+
get row() {
|
|
643
|
+
return row
|
|
644
|
+
},
|
|
645
|
+
get item() {
|
|
646
|
+
return item
|
|
647
|
+
},
|
|
648
|
+
get data() {
|
|
649
|
+
return table.data
|
|
650
|
+
}
|
|
533
651
|
})}
|
|
534
652
|
{/if}
|
|
535
653
|
</div>
|
|
@@ -538,15 +656,21 @@
|
|
|
538
656
|
{/if}
|
|
539
657
|
{/if}
|
|
540
658
|
|
|
541
|
-
{@render content?.({
|
|
542
|
-
|
|
543
|
-
|
|
659
|
+
{@render content?.({
|
|
660
|
+
Column,
|
|
661
|
+
Panel,
|
|
662
|
+
get table() {
|
|
663
|
+
return table
|
|
664
|
+
},
|
|
665
|
+
get data() {
|
|
666
|
+
return data
|
|
667
|
+
}
|
|
668
|
+
})}
|
|
544
669
|
|
|
545
670
|
<!---------------------------------------------------->
|
|
546
671
|
<style>
|
|
547
|
-
|
|
548
|
-
.svelte-tably
|
|
549
|
-
all: unset;
|
|
672
|
+
.svelte-tably *,
|
|
673
|
+
.svelte-tably {
|
|
550
674
|
box-sizing: border-box;
|
|
551
675
|
background-color: inherit;
|
|
552
676
|
}
|
|
@@ -557,49 +681,61 @@
|
|
|
557
681
|
}
|
|
558
682
|
|
|
559
683
|
input[type='checkbox'] {
|
|
560
|
-
all: revert;
|
|
561
684
|
width: 18px;
|
|
562
685
|
height: 18px;
|
|
563
686
|
cursor: pointer;
|
|
564
687
|
}
|
|
565
688
|
|
|
689
|
+
.sorting-icon {
|
|
690
|
+
transition: transform .15s ease;
|
|
691
|
+
transform: rotateZ(0deg);
|
|
692
|
+
&.reversed {
|
|
693
|
+
transform: rotateZ(-180deg);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
566
697
|
.__fixed {
|
|
567
698
|
display: flex;
|
|
568
699
|
align-items: center;
|
|
569
700
|
justify-content: center;
|
|
570
|
-
gap: .5rem;
|
|
701
|
+
gap: 0.5rem;
|
|
571
702
|
position: absolute;
|
|
572
|
-
top: 0;
|
|
573
|
-
|
|
703
|
+
top: 0;
|
|
704
|
+
left: 0;
|
|
705
|
+
right: 0;
|
|
706
|
+
bottom: 0;
|
|
574
707
|
width: 100%;
|
|
575
708
|
}
|
|
576
709
|
|
|
577
710
|
.first .__fixed {
|
|
578
|
-
top: var(--tably-padding-y, .5rem);
|
|
711
|
+
top: var(--tably-padding-y, 0.5rem);
|
|
579
712
|
}
|
|
580
713
|
.last .__fixed {
|
|
581
|
-
bottom: var(--tably-padding-y, .5rem);
|
|
714
|
+
bottom: var(--tably-padding-y, 0.5rem);
|
|
582
715
|
}
|
|
583
716
|
|
|
584
|
-
tbody::before,
|
|
717
|
+
tbody::before,
|
|
718
|
+
tbody::after,
|
|
719
|
+
selects::before,
|
|
720
|
+
selects::after {
|
|
585
721
|
content: '';
|
|
586
722
|
display: grid;
|
|
587
723
|
min-height: 100%;
|
|
588
724
|
}
|
|
589
725
|
|
|
590
|
-
tbody::before,
|
|
726
|
+
tbody::before,
|
|
727
|
+
selects::before {
|
|
591
728
|
height: var(--t);
|
|
592
729
|
}
|
|
593
|
-
tbody::after,
|
|
730
|
+
tbody::after,
|
|
731
|
+
selects::after {
|
|
594
732
|
height: var(--b);
|
|
595
|
-
}
|
|
733
|
+
}
|
|
596
734
|
|
|
597
735
|
a.row {
|
|
598
736
|
color: inherit;
|
|
599
737
|
text-decoration: inherit;
|
|
600
738
|
}
|
|
601
|
-
|
|
602
|
-
|
|
603
739
|
|
|
604
740
|
.backdrop {
|
|
605
741
|
position: absolute;
|
|
@@ -607,10 +743,10 @@
|
|
|
607
743
|
top: 0px;
|
|
608
744
|
bottom: 0px;
|
|
609
745
|
right: 0px;
|
|
610
|
-
background-color: hsla(0, 0%, 0%, .3);
|
|
746
|
+
background-color: hsla(0, 0%, 0%, 0.3);
|
|
611
747
|
z-index: 3;
|
|
612
748
|
opacity: 1;
|
|
613
|
-
transition: .15s ease;
|
|
749
|
+
transition: 0.15s ease;
|
|
614
750
|
border: none;
|
|
615
751
|
outline: none;
|
|
616
752
|
cursor: pointer;
|
|
@@ -629,7 +765,8 @@
|
|
|
629
765
|
}
|
|
630
766
|
}
|
|
631
767
|
|
|
632
|
-
.headers,
|
|
768
|
+
.headers,
|
|
769
|
+
.statusbar {
|
|
633
770
|
/* So that the scrollbar doesn't cause the headers/statusbar to shift */
|
|
634
771
|
padding-right: 11px;
|
|
635
772
|
}
|
|
@@ -652,29 +789,34 @@
|
|
|
652
789
|
.headers > .column {
|
|
653
790
|
border-right: 1px solid var(--tably-border, hsl(0, 0%, 90%));
|
|
654
791
|
overflow: hidden;
|
|
655
|
-
padding: var(--tably-padding-y, .5rem) 0;
|
|
792
|
+
padding: var(--tably-padding-y, 0.5rem) 0;
|
|
793
|
+
cursor: default;
|
|
794
|
+
user-select: none;
|
|
795
|
+
|
|
796
|
+
&.sortable {
|
|
797
|
+
cursor: pointer;
|
|
798
|
+
}
|
|
656
799
|
|
|
657
800
|
&.resizeable {
|
|
658
801
|
resize: horizontal;
|
|
659
802
|
}
|
|
660
803
|
}
|
|
661
|
-
|
|
804
|
+
|
|
662
805
|
.table {
|
|
663
806
|
display: grid;
|
|
664
807
|
height: 100%;
|
|
665
808
|
position: relative;
|
|
666
809
|
|
|
667
|
-
grid-template-areas:
|
|
810
|
+
grid-template-areas:
|
|
668
811
|
'headers panel'
|
|
669
812
|
'rows panel'
|
|
670
|
-
'statusbar panel'
|
|
671
|
-
;
|
|
813
|
+
'statusbar panel';
|
|
672
814
|
|
|
673
815
|
grid-template-columns: auto min-content;
|
|
674
816
|
grid-template-rows: auto 1fr auto;
|
|
675
817
|
|
|
676
818
|
border: 1px solid var(--tably-border, hsl(0, 0%, 90%));
|
|
677
|
-
border-radius: var(--tably-radius, .25rem);
|
|
819
|
+
border-radius: var(--tably-radius, 0.25rem);
|
|
678
820
|
|
|
679
821
|
max-height: 100%;
|
|
680
822
|
}
|
|
@@ -708,10 +850,12 @@
|
|
|
708
850
|
|
|
709
851
|
.statusbar > tr > .column {
|
|
710
852
|
border-top: 1px solid var(--tably-border, hsl(0, 0%, 90%));
|
|
711
|
-
padding: calc(var(--tably-padding-y, .5rem) / 2) 0;
|
|
853
|
+
padding: calc(var(--tably-padding-y, 0.5rem) / 2) 0;
|
|
712
854
|
}
|
|
713
855
|
|
|
714
|
-
.headers,
|
|
856
|
+
.headers,
|
|
857
|
+
.row,
|
|
858
|
+
.statusbar > tr {
|
|
715
859
|
position: relative;
|
|
716
860
|
display: grid;
|
|
717
861
|
width: 100%;
|
|
@@ -730,14 +874,14 @@
|
|
|
730
874
|
}
|
|
731
875
|
|
|
732
876
|
.row:first-child > * {
|
|
733
|
-
padding-top: calc(var(--tably-padding-y, .5rem) + calc(var(--tably-padding-y, .5rem) / 2));
|
|
877
|
+
padding-top: calc(var(--tably-padding-y, 0.5rem) + calc(var(--tably-padding-y, 0.5rem) / 2));
|
|
734
878
|
}
|
|
735
879
|
.row:last-child > * {
|
|
736
|
-
padding-bottom: calc(var(--tably-padding-y, .5rem) + calc(var(--tably-padding-y, .5rem) / 2));
|
|
880
|
+
padding-bottom: calc(var(--tably-padding-y, 0.5rem) + calc(var(--tably-padding-y, 0.5rem) / 2));
|
|
737
881
|
}
|
|
738
882
|
|
|
739
883
|
.row > * {
|
|
740
|
-
padding: calc(var(--tably-padding-y, .5rem) / 2) 0;
|
|
884
|
+
padding: calc(var(--tably-padding-y, 0.5rem) / 2) 0;
|
|
741
885
|
}
|
|
742
886
|
|
|
743
887
|
.panel {
|
|
@@ -756,8 +900,7 @@
|
|
|
756
900
|
right: 0;
|
|
757
901
|
width: min-content;
|
|
758
902
|
overflow: auto;
|
|
759
|
-
padding: var(--tably-padding-y, .5rem) 0;
|
|
903
|
+
padding: var(--tably-padding-y, 0.5rem) 0;
|
|
760
904
|
}
|
|
761
905
|
}
|
|
762
|
-
|
|
763
|
-
</style>
|
|
906
|
+
</style>
|