svelte-tably 1.0.1 → 1.0.2-next.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/LICENSE +20 -20
- package/README.md +301 -301
- package/dist/column/Column.svelte +38 -38
- package/dist/expandable/Expandable.svelte +24 -24
- package/dist/panel/Panel.svelte +20 -20
- package/dist/row/Row.svelte +24 -24
- package/dist/table/Table.svelte +1146 -1140
- package/package.json +1 -1
package/dist/table/Table.svelte
CHANGED
|
@@ -1,1140 +1,1146 @@
|
|
|
1
|
-
<!-- @component
|
|
2
|
-
|
|
3
|
-
This is a description, \
|
|
4
|
-
on how to use this.
|
|
5
|
-
|
|
6
|
-
@example
|
|
7
|
-
<Component />
|
|
8
|
-
|
|
9
|
-
-->
|
|
10
|
-
|
|
11
|
-
<script lang="ts">
|
|
12
|
-
import { type Snippet } from 'svelte'
|
|
13
|
-
import { fly } from 'svelte/transition'
|
|
14
|
-
import { sineInOut } from 'svelte/easing'
|
|
15
|
-
import reorder, { type ItemState } from 'runic-reorder'
|
|
16
|
-
import { Virtualization } from './virtualization.svelte.js'
|
|
17
|
-
import {
|
|
18
|
-
TableState,
|
|
19
|
-
type HeaderSelectCtx,
|
|
20
|
-
type RowCtx,
|
|
21
|
-
type RowSelectCtx,
|
|
22
|
-
type TableProps
|
|
23
|
-
} from './table-state.svelte.js'
|
|
24
|
-
import Panel from '../panel/Panel.svelte'
|
|
25
|
-
import Column from '../column/Column.svelte'
|
|
26
|
-
import { assignDescriptors, capitalize, fromProps, mounted, segmentize } from '../utility.svelte.js'
|
|
27
|
-
import { conditional } from '../conditional.svelte.js'
|
|
28
|
-
import { ColumnState, type RowColumnCtx } from '../column/column-state.svelte.js'
|
|
29
|
-
import Expandable from '../expandable/Expandable.svelte'
|
|
30
|
-
import { SizeTween } from '../size-tween.svelte.js'
|
|
31
|
-
import { on } from 'svelte/events'
|
|
32
|
-
import Row from '../row/Row.svelte'
|
|
33
|
-
|
|
34
|
-
type T = $$Generic<Record<PropertyKey, unknown>>
|
|
35
|
-
|
|
36
|
-
type ConstructorReturnType<T extends new (...args: any[]) => any> =
|
|
37
|
-
T extends new (...args: any[]) => infer K ? K : never
|
|
38
|
-
type ConstructorParams<T extends new (...args: any[]) => any> =
|
|
39
|
-
T extends new (...args: infer K) => any ? K : never
|
|
40
|
-
|
|
41
|
-
type ContentCtx<T extends Record<PropertyKey, unknown>> = {
|
|
42
|
-
Column: {
|
|
43
|
-
new <V>(...args: ConstructorParams<typeof Column<T, V>>): ConstructorReturnType<typeof Column<T, V>>
|
|
44
|
-
<V>(...args: Parameters<typeof Column<T, V>>): ReturnType<typeof Column<T, V>>
|
|
45
|
-
}
|
|
46
|
-
Panel: typeof Panel<T>
|
|
47
|
-
Expandable: typeof Expandable<T>
|
|
48
|
-
Row: typeof Row<T>
|
|
49
|
-
readonly table: TableState<T>
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
type ContentSnippet = Snippet<[context: ContentCtx<T>]>
|
|
53
|
-
|
|
54
|
-
let {
|
|
55
|
-
content,
|
|
56
|
-
selected: _selected = $bindable([]),
|
|
57
|
-
panel: _panel = $bindable(),
|
|
58
|
-
data: _data = $bindable([]),
|
|
59
|
-
...restProps
|
|
60
|
-
}: TableProps<T> & { content?: ContentSnippet } = $props()
|
|
61
|
-
|
|
62
|
-
const properties = fromProps(restProps, {
|
|
63
|
-
selected: [() => _selected, (v) => (_selected = v)],
|
|
64
|
-
panel: [() => _panel, (v) => (_panel = v)],
|
|
65
|
-
data: [() => _data, (v) => (_data = v)]
|
|
66
|
-
}) as TableProps<T>
|
|
67
|
-
|
|
68
|
-
const mount = mounted()
|
|
69
|
-
|
|
70
|
-
const reorderArea = reorder(rowSnippet)
|
|
71
|
-
|
|
72
|
-
const elements = $state({}) as Record<
|
|
73
|
-
'headers' | 'statusbar' | 'rows' | 'virtualTop' | 'virtualBottom' | 'selects',
|
|
74
|
-
HTMLElement
|
|
75
|
-
>
|
|
76
|
-
|
|
77
|
-
const table = new TableState<T>(properties) as TableState<T>
|
|
78
|
-
|
|
79
|
-
const virtualization = new Virtualization(table)
|
|
80
|
-
|
|
81
|
-
const panelTween = new SizeTween(() => !!properties.panel)
|
|
82
|
-
|
|
83
|
-
let hoveredRow: T | null = $state(null)
|
|
84
|
-
let hoveredColumn: ColumnState | null = $state(null)
|
|
85
|
-
|
|
86
|
-
/** Order of columns */
|
|
87
|
-
const fixed = $derived(table.positions.fixed)
|
|
88
|
-
const hidden = $derived(table.positions.hidden)
|
|
89
|
-
const notHidden = (column: ColumnState) => !table.positions.hidden.includes(column)
|
|
90
|
-
const sticky = $derived(table.positions.sticky.filter(notHidden))
|
|
91
|
-
const scrolled = $derived(table.positions.scroll.filter(notHidden))
|
|
92
|
-
const columns = $derived([...fixed, ...sticky, ...scrolled])
|
|
93
|
-
|
|
94
|
-
/** Width of each column */
|
|
95
|
-
const columnWidths = $state({}) as Record<string, number>
|
|
96
|
-
|
|
97
|
-
const getWidth = (key: string, def: number = 150) =>
|
|
98
|
-
columnWidths[key] || table.columns[key]?.defaults.width || def
|
|
99
|
-
|
|
100
|
-
/** grid-template-columns for widths */
|
|
101
|
-
const style = $derived.by(() => {
|
|
102
|
-
if (!mount.isMounted) return ''
|
|
103
|
-
|
|
104
|
-
const context = table.row?.snippets.context ? table.row?.options.context.width : ''
|
|
105
|
-
|
|
106
|
-
const templateColumns =
|
|
107
|
-
columns
|
|
108
|
-
.map((column, i, arr) => {
|
|
109
|
-
const width = getWidth(column.id)
|
|
110
|
-
if (i === arr.length - 1) return `minmax(${width}px, 1fr)`
|
|
111
|
-
return `${width}px`
|
|
112
|
-
})
|
|
113
|
-
.join(' ') + context
|
|
114
|
-
|
|
115
|
-
const theadTempla3teColumns = `
|
|
116
|
-
#${table.id} > thead > tr,
|
|
117
|
-
#${table.id} > tfoot > tr {
|
|
118
|
-
grid-template-columns: ${templateColumns};
|
|
119
|
-
}
|
|
120
|
-
`
|
|
121
|
-
|
|
122
|
-
const tbodyTemplateColumns = `
|
|
123
|
-
[data-area-class='${table.id}'] tr.row,
|
|
124
|
-
#${table.id} > tbody::after {
|
|
125
|
-
grid-template-columns: ${templateColumns};
|
|
126
|
-
}
|
|
127
|
-
`
|
|
128
|
-
|
|
129
|
-
let sum = 0
|
|
130
|
-
const stickyLeft = [...fixed, ...sticky]
|
|
131
|
-
.map((column, i, arr) => {
|
|
132
|
-
sum += getWidth(arr[i - 1]?.id, i === 0 ? 0 : undefined)
|
|
133
|
-
return `
|
|
134
|
-
#${table.id} .column.sticky[data-column='${column.id}'],
|
|
135
|
-
[data-svelte-tably='${table.id}'] .column.sticky[data-column='${column.id}'] {
|
|
136
|
-
left: ${sum}px;
|
|
137
|
-
}
|
|
138
|
-
`
|
|
139
|
-
})
|
|
140
|
-
.join('')
|
|
141
|
-
|
|
142
|
-
const columnStyling = columns
|
|
143
|
-
.map((column) =>
|
|
144
|
-
!column.options.style ?
|
|
145
|
-
''
|
|
146
|
-
: `
|
|
147
|
-
[data-area-class='${table.id}'] .column[data-column='${column.id}'] {
|
|
148
|
-
${column.options.style}
|
|
149
|
-
}
|
|
150
|
-
`
|
|
151
|
-
)
|
|
152
|
-
.join('')
|
|
153
|
-
|
|
154
|
-
return theadTempla3teColumns + tbodyTemplateColumns + stickyLeft + columnStyling
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
function observeColumnWidth(node: HTMLDivElement, isHeader = false) {
|
|
158
|
-
if (!isHeader) return
|
|
159
|
-
|
|
160
|
-
const key = node.getAttribute('data-column')!
|
|
161
|
-
node.style.width = getWidth(key) + 'px'
|
|
162
|
-
|
|
163
|
-
let mouseup = false
|
|
164
|
-
|
|
165
|
-
const observer = new MutationObserver(() => {
|
|
166
|
-
const width = parseFloat(node.style.width)
|
|
167
|
-
if (width === columnWidths[key]) return
|
|
168
|
-
columnWidths[key] = width
|
|
169
|
-
if (!mouseup) {
|
|
170
|
-
mouseup = true
|
|
171
|
-
window.addEventListener(
|
|
172
|
-
'click',
|
|
173
|
-
(e) => {
|
|
174
|
-
e.preventDefault()
|
|
175
|
-
e.stopPropagation()
|
|
176
|
-
mouseup = false
|
|
177
|
-
},
|
|
178
|
-
{ once: true, capture: true }
|
|
179
|
-
)
|
|
180
|
-
}
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
observer.observe(node, { attributes: true })
|
|
184
|
-
return { destroy: () => observer.disconnect() }
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
let tbody = $state({
|
|
188
|
-
width: 0
|
|
189
|
-
})
|
|
190
|
-
async function onscroll() {
|
|
191
|
-
const target = virtualization.viewport.element!
|
|
192
|
-
if (target.scrollTop !== virtualization.scrollTop) {
|
|
193
|
-
virtualization.scrollTop = target?.scrollTop ?? virtualization.scrollTop
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (elements.selects) {
|
|
197
|
-
elements.selects.scrollTop = target?.scrollTop
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (!elements.headers) return
|
|
201
|
-
elements.headers.scrollLeft = target.scrollLeft
|
|
202
|
-
elements.statusbar.scrollLeft = target.scrollLeft
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
// * --- CSV --- *
|
|
206
|
-
let csv = $state(false) as false | { selected?: boolean }
|
|
207
|
-
let csvElement = $state() as undefined | HTMLTableElement
|
|
208
|
-
interface CSVOptions {
|
|
209
|
-
/** Semi-colons as separator? */
|
|
210
|
-
semicolon?: boolean
|
|
211
|
-
/** Only selected rows */
|
|
212
|
-
selected?: boolean
|
|
213
|
-
}
|
|
214
|
-
export async function toCSV(opts: CSVOptions = {}) {
|
|
215
|
-
csv = { selected: !!opts.selected }
|
|
216
|
-
let resolve: (value: HTMLTableElement) => void
|
|
217
|
-
const promise = new Promise<HTMLTableElement>((r) => (resolve = r))
|
|
218
|
-
|
|
219
|
-
const clean = $effect.root(() => {
|
|
220
|
-
$effect(() => {
|
|
221
|
-
if (csvElement) {
|
|
222
|
-
resolve(csvElement)
|
|
223
|
-
}
|
|
224
|
-
})
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
let table = await promise
|
|
228
|
-
clean()
|
|
229
|
-
|
|
230
|
-
const separator = opts.semicolon ? ';' : ','
|
|
231
|
-
const rows = Array.from(table.rows)
|
|
232
|
-
const csvRows = [] as string[]
|
|
233
|
-
|
|
234
|
-
for (const row of rows) {
|
|
235
|
-
const cells = Array.from(row.cells)
|
|
236
|
-
const csvCells = cells.map((cell) => {
|
|
237
|
-
let text = cell.textContent?.trim() || ''
|
|
238
|
-
|
|
239
|
-
// Escape double quotes and wrap in quotes if needed
|
|
240
|
-
if (text.includes('"')) {
|
|
241
|
-
text = text.replace(/"/g, '""')
|
|
242
|
-
}
|
|
243
|
-
if (text.includes(separator) || text.includes('"') || text.includes('\n')) {
|
|
244
|
-
text = `"${text}"`
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return text
|
|
248
|
-
})
|
|
249
|
-
csvRows.push(csvCells.join(separator))
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
csv = false
|
|
253
|
-
return csvRows.join('\n')
|
|
254
|
-
}
|
|
255
|
-
// * --- CSV --- *
|
|
256
|
-
|
|
257
|
-
let expandedRow = $state([]) as T[]
|
|
258
|
-
let expandTick = false
|
|
259
|
-
function toggleExpand(item: T, value?: boolean) {
|
|
260
|
-
if (expandTick) return
|
|
261
|
-
expandTick = true
|
|
262
|
-
requestAnimationFrame(() => (expandTick = false))
|
|
263
|
-
|
|
264
|
-
let indexOf = expandedRow.indexOf(item)
|
|
265
|
-
if (value === undefined) {
|
|
266
|
-
value = indexOf === -1
|
|
267
|
-
}
|
|
268
|
-
if (!value) {
|
|
269
|
-
expandedRow.splice(indexOf, 1)
|
|
270
|
-
return
|
|
271
|
-
}
|
|
272
|
-
if (table.expandable?.options.multiple === true) {
|
|
273
|
-
expandedRow.push(item)
|
|
274
|
-
} else {
|
|
275
|
-
expandedRow[0] = item
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function addRowColumnEvents(
|
|
280
|
-
node: HTMLTableColElement,
|
|
281
|
-
opts: ['header' | 'row' | 'statusbar', ColumnState, () => RowColumnCtx<T, any>]
|
|
282
|
-
) {
|
|
283
|
-
const [where, column, value] = opts
|
|
284
|
-
if (where !== 'row') return
|
|
285
|
-
if (column.options.onclick) {
|
|
286
|
-
$effect(() => on(node, 'click', (e) => column.options.onclick!(e, value())))
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function addRowEvents(node: HTMLTableRowElement, ctx: RowCtx<T>) {
|
|
291
|
-
if (table.row?.events.onclick) {
|
|
292
|
-
$effect(() => on(node, 'click', (e) => table.row?.events.onclick!(e, ctx)))
|
|
293
|
-
}
|
|
294
|
-
if (table.row?.events.oncontextmenu) {
|
|
295
|
-
$effect(() => on(node, 'contextmenu', (e) => table.row?.events.oncontextmenu!(e, ctx)))
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
</script>
|
|
299
|
-
|
|
300
|
-
<!---------------------------------------------------->
|
|
301
|
-
|
|
302
|
-
{#if csv !== false}
|
|
303
|
-
{@const renderedColumns = columns.filter((v) => v.id !== '__fixed')}
|
|
304
|
-
<table bind:this={csvElement} hidden>
|
|
305
|
-
<thead>
|
|
306
|
-
<tr>
|
|
307
|
-
{#each renderedColumns as column}
|
|
308
|
-
<th>{@render column.snippets.title()}</th>
|
|
309
|
-
{/each}
|
|
310
|
-
</tr>
|
|
311
|
-
</thead>
|
|
312
|
-
<tbody>
|
|
313
|
-
{#each table.data as row, i}
|
|
314
|
-
{#if (csv.selected && table.selected.includes(row)) || !csv.selected}
|
|
315
|
-
<tr>
|
|
316
|
-
{#each renderedColumns as column}
|
|
317
|
-
<td>
|
|
318
|
-
{#if column.snippets.row}
|
|
319
|
-
{@render column.snippets.row(row, {
|
|
320
|
-
index: i,
|
|
321
|
-
value: column.options.value?.(row),
|
|
322
|
-
columnHovered: false,
|
|
323
|
-
rowHovered: false,
|
|
324
|
-
itemState: {
|
|
325
|
-
index: i,
|
|
326
|
-
dragging: false,
|
|
327
|
-
positioning: false
|
|
328
|
-
} as ItemState<any>,
|
|
329
|
-
selected: false,
|
|
330
|
-
expanded: false
|
|
331
|
-
})}
|
|
332
|
-
{:else}
|
|
333
|
-
{column.options.value?.(row)}
|
|
334
|
-
{/if}
|
|
335
|
-
</td>
|
|
336
|
-
{/each}
|
|
337
|
-
</tr>
|
|
338
|
-
{/if}
|
|
339
|
-
{/each}
|
|
340
|
-
</tbody>
|
|
341
|
-
</table>
|
|
342
|
-
{/if}
|
|
343
|
-
|
|
344
|
-
<svelte:head>
|
|
345
|
-
{@html `<`+`style>${style}</style>`}
|
|
346
|
-
</svelte:head>
|
|
347
|
-
|
|
348
|
-
{#snippet chevronSnippet(rotation: number = 0)}
|
|
349
|
-
<svg
|
|
350
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
351
|
-
width="16"
|
|
352
|
-
height="16"
|
|
353
|
-
viewBox="0 0 16 16"
|
|
354
|
-
style="transform: rotate({rotation}deg)"
|
|
355
|
-
>
|
|
356
|
-
<path
|
|
357
|
-
fill="currentColor"
|
|
358
|
-
d="M3.2 10.26a.75.75 0 0 0 1.06.04L8 6.773l3.74 3.527a.75.75 0 1 0 1.02-1.1l-4.25-4a.75.75 0 0 0-1.02 0l-4.25 4a.75.75 0 0 0-.04 1.06"
|
|
359
|
-
></path>
|
|
360
|
-
</svg>
|
|
361
|
-
{/snippet}
|
|
362
|
-
|
|
363
|
-
{#snippet dragSnippet()}
|
|
364
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" style="opacity: .3">
|
|
365
|
-
<path
|
|
366
|
-
fill="currentColor"
|
|
367
|
-
d="M5.5 5a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3m0 4.5a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3m1.5 3a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0M10.5 5a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3M12 8a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0m-1.5 6a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3"
|
|
368
|
-
></path>
|
|
369
|
-
</svg>
|
|
370
|
-
{/snippet}
|
|
371
|
-
|
|
372
|
-
{#snippet columnsSnippet(
|
|
373
|
-
renderable: (column: ColumnState) => Snippet<[arg0?: any, arg1?: any]> | undefined,
|
|
374
|
-
arg: null | ((column: ColumnState) => any[]) = null,
|
|
375
|
-
where: 'header' | 'row' | 'statusbar'
|
|
376
|
-
)}
|
|
377
|
-
{@const isHeader = where === 'header'}
|
|
378
|
-
{#each fixed as column, i (column)}
|
|
379
|
-
{#if !hidden.includes(column)}
|
|
380
|
-
{@const args = arg ? arg(column) : []}
|
|
381
|
-
{@const sortable = isHeader && column.options.sort && !table.options.reorderable}
|
|
382
|
-
<svelte:element
|
|
383
|
-
this={isHeader ? 'th' : 'td'}
|
|
384
|
-
class={column.options.class ?? ''}
|
|
385
|
-
class:column={true}
|
|
386
|
-
class:sticky={true}
|
|
387
|
-
class:fixed={true}
|
|
388
|
-
use:addRowColumnEvents={[where, column, () => args[1]]}
|
|
389
|
-
data-column={column.id}
|
|
390
|
-
class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
|
|
391
|
-
class:header={isHeader}
|
|
392
|
-
class:sortable
|
|
393
|
-
use:conditional={[isHeader, (node) => table.dataState.sortAction(node, column.id)]}
|
|
394
|
-
onpointerenter={() => (hoveredColumn = column)}
|
|
395
|
-
onpointerleave={() => (hoveredColumn = null)}
|
|
396
|
-
>
|
|
397
|
-
{@render renderable(column)?.(args[0], args[1])}
|
|
398
|
-
{#if isHeader && table.dataState.sortby === column.id && sortable}
|
|
399
|
-
<span class="sorting-icon">
|
|
400
|
-
{@render chevronSnippet(table.dataState.sortReverse ? 0 : 180)}
|
|
401
|
-
</span>
|
|
402
|
-
{/if}
|
|
403
|
-
</svelte:element>
|
|
404
|
-
{/if}
|
|
405
|
-
{/each}
|
|
406
|
-
{#each sticky as column, i (column)}
|
|
407
|
-
{#if !hidden.includes(column)}
|
|
408
|
-
{@const args = arg ? arg(column) : []}
|
|
409
|
-
{@const sortable = isHeader && column.options.sort && !table.options.reorderable}
|
|
410
|
-
<svelte:element
|
|
411
|
-
this={isHeader ? 'th' : 'td'}
|
|
412
|
-
class={column.options.class ?? ''}
|
|
413
|
-
class:column={true}
|
|
414
|
-
class:sticky={true}
|
|
415
|
-
use:addRowColumnEvents={[where, column, () => args[1]]}
|
|
416
|
-
use:observeColumnWidth={isHeader}
|
|
417
|
-
data-column={column.id}
|
|
418
|
-
class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
|
|
419
|
-
class:header={isHeader}
|
|
420
|
-
class:resizeable={isHeader && column.options.resizeable && table.options.resizeable}
|
|
421
|
-
class:border={i == sticky.length - 1}
|
|
422
|
-
class:sortable
|
|
423
|
-
use:conditional={[isHeader, (node) => table.dataState.sortAction(node, column.id)]}
|
|
424
|
-
onpointerenter={() => (hoveredColumn = column)}
|
|
425
|
-
onpointerleave={() => (hoveredColumn = null)}
|
|
426
|
-
>
|
|
427
|
-
{@render renderable(column)?.(args[0], args[1])}
|
|
428
|
-
{#if isHeader && table.dataState.sortby === column.id && sortable}
|
|
429
|
-
<span class="sorting-icon">
|
|
430
|
-
{@render chevronSnippet(table.dataState.sortReverse ? 0 : 180)}
|
|
431
|
-
</span>
|
|
432
|
-
{/if}
|
|
433
|
-
</svelte:element>
|
|
434
|
-
{/if}
|
|
435
|
-
{/each}
|
|
436
|
-
{#each scrolled as column, i (column)}
|
|
437
|
-
{#if !hidden.includes(column)}
|
|
438
|
-
{@const args = arg ? arg(column) : []}
|
|
439
|
-
{@const sortable = isHeader && column!.options.sort && !table.options.reorderable}
|
|
440
|
-
<svelte:element
|
|
441
|
-
this={isHeader ? 'th' : 'td'}
|
|
442
|
-
class={column.options.class ?? ''}
|
|
443
|
-
class:column={true}
|
|
444
|
-
data-column={column.id}
|
|
445
|
-
class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
|
|
446
|
-
use:addRowColumnEvents={[where, column, () => args[1]]}
|
|
447
|
-
use:observeColumnWidth={isHeader}
|
|
448
|
-
class:resizeable={isHeader && column.options.resizeable && table.options.resizeable}
|
|
449
|
-
class:sortable
|
|
450
|
-
use:conditional={[isHeader, (node) => table.dataState.sortAction(node, column.id)]}
|
|
451
|
-
onpointerenter={() => (hoveredColumn = column)}
|
|
452
|
-
onpointerleave={() => (hoveredColumn = null)}
|
|
453
|
-
>
|
|
454
|
-
{@render renderable(column)?.(args[0], args[1])}
|
|
455
|
-
{#if isHeader && table.dataState.sortby === column.id && sortable}
|
|
456
|
-
<span class="sorting-icon">
|
|
457
|
-
{@render chevronSnippet(table.dataState.sortReverse ? 0 : 180)}
|
|
458
|
-
</span>
|
|
459
|
-
{/if}
|
|
460
|
-
</svelte:element>
|
|
461
|
-
{/if}
|
|
462
|
-
{/each}
|
|
463
|
-
{/snippet}
|
|
464
|
-
|
|
465
|
-
{#snippet defaultRow(item: T, ctx: RowColumnCtx<T, any>)}
|
|
466
|
-
{ctx.value}
|
|
467
|
-
{/snippet}
|
|
468
|
-
|
|
469
|
-
{#snippet rowSnippet(item: T, itemState?: ItemState<T>)}
|
|
470
|
-
{@const index = itemState?.index ?? 0}
|
|
471
|
-
|
|
472
|
-
{@const ctx: RowCtx<T> = {
|
|
473
|
-
get index() {
|
|
474
|
-
return index
|
|
475
|
-
},
|
|
476
|
-
get rowHovered() {
|
|
477
|
-
return hoveredRow === item
|
|
478
|
-
},
|
|
479
|
-
get selected() {
|
|
480
|
-
return table.selected?.includes(item)
|
|
481
|
-
},
|
|
482
|
-
set selected(value) {
|
|
483
|
-
value ?
|
|
484
|
-
table.selected!.push(item)
|
|
485
|
-
: table.selected!.splice(table.selected!.indexOf(item), 1)
|
|
486
|
-
},
|
|
487
|
-
get itemState() {
|
|
488
|
-
return itemState
|
|
489
|
-
},
|
|
490
|
-
get expanded() {
|
|
491
|
-
return expandedRow.includes(item)
|
|
492
|
-
},
|
|
493
|
-
set expanded(value) {
|
|
494
|
-
toggleExpand(item, value)
|
|
495
|
-
}
|
|
496
|
-
}}
|
|
497
|
-
|
|
498
|
-
<tr
|
|
499
|
-
aria-rowindex={index + 1}
|
|
500
|
-
style:opacity={itemState?.positioning ? 0 : 1}
|
|
501
|
-
class="row"
|
|
502
|
-
class:dragging={itemState?.dragging}
|
|
503
|
-
class:selected={table.selected?.includes(item)}
|
|
504
|
-
class:first={index === 0}
|
|
505
|
-
class:last={index === virtualization.area.length - 1}
|
|
506
|
-
{...itemState?.dragging ? { 'data-svelte-tably': table.id } : {}}
|
|
507
|
-
onpointerenter={() => (hoveredRow = item)}
|
|
508
|
-
onpointerleave={() => (hoveredRow = null)}
|
|
509
|
-
use:addRowEvents={ctx}
|
|
510
|
-
onclick={(e) => {
|
|
511
|
-
if (table.expandable?.options.click === true) {
|
|
512
|
-
let target = e.target as HTMLElement
|
|
513
|
-
if (['INPUT', 'TEXTAREA', 'BUTTON', 'A'].includes(target.tagName)) {
|
|
514
|
-
return
|
|
515
|
-
}
|
|
516
|
-
ctx.expanded = !ctx.expanded
|
|
517
|
-
}
|
|
518
|
-
}}
|
|
519
|
-
>
|
|
520
|
-
{@render columnsSnippet(
|
|
521
|
-
(column) => column.snippets.row ?? defaultRow,
|
|
522
|
-
(column) => {
|
|
523
|
-
return [
|
|
524
|
-
item,
|
|
525
|
-
assignDescriptors(
|
|
526
|
-
{
|
|
527
|
-
get value() {
|
|
528
|
-
return column.options.value ? column.options.value(item) : undefined
|
|
529
|
-
},
|
|
530
|
-
get columnHovered() {
|
|
531
|
-
return hoveredColumn === column
|
|
532
|
-
}
|
|
533
|
-
},
|
|
534
|
-
ctx
|
|
535
|
-
)
|
|
536
|
-
]
|
|
537
|
-
},
|
|
538
|
-
'row'
|
|
539
|
-
)}
|
|
540
|
-
{#if table.row?.snippets.context}
|
|
541
|
-
{#if table.row?.snippets.contextHeader || !table.row?.options.context.hover || hoveredRow === item}
|
|
542
|
-
<td
|
|
543
|
-
class="context-col"
|
|
544
|
-
class:hover={!table.row?.snippets.contextHeader && table.row?.options.context.hover}
|
|
545
|
-
class:hidden={table.row?.options.context.hover &&
|
|
546
|
-
table.row?.snippets.contextHeader &&
|
|
547
|
-
hoveredRow !== item}
|
|
548
|
-
>
|
|
549
|
-
{@render table.row?.snippets.context?.(item, ctx)}
|
|
550
|
-
</td>
|
|
551
|
-
{/if}
|
|
552
|
-
{/if}
|
|
553
|
-
</tr>
|
|
554
|
-
|
|
555
|
-
{@const expandableTween = new SizeTween(() => table.expandable && expandedRow.includes(item), {
|
|
556
|
-
min: 1,
|
|
557
|
-
duration: table.expandable?.options.slide.duration,
|
|
558
|
-
easing: table.expandable?.options.slide.easing
|
|
559
|
-
})}
|
|
560
|
-
{#if expandableTween.current > 0}
|
|
561
|
-
<tr class="expandable" style="height: {expandableTween.current}px">
|
|
562
|
-
<td colspan={columns.length} style="height: {expandableTween.current}px">
|
|
563
|
-
<div bind:offsetHeight={expandableTween.size} style="width: {tbody.width - 3}px">
|
|
564
|
-
{@render table.expandable!.snippets.content?.(item, ctx)}
|
|
565
|
-
</div>
|
|
566
|
-
</td>
|
|
567
|
-
</tr>
|
|
568
|
-
{/if}
|
|
569
|
-
{/snippet}
|
|
570
|
-
|
|
571
|
-
<table
|
|
572
|
-
id={table.id}
|
|
573
|
-
class="table svelte-tably"
|
|
574
|
-
style="--t: {virtualization.virtualTop}px; --b: {virtualization.virtualBottom}px;"
|
|
575
|
-
aria-rowcount={table.data.length}
|
|
576
|
-
>
|
|
577
|
-
{#if columns.some((v) => v.snippets.header)}
|
|
578
|
-
<thead class="headers" bind:this={elements.headers}>
|
|
579
|
-
<tr style="min-width: {tbody.width}px">
|
|
580
|
-
{@render columnsSnippet(
|
|
581
|
-
(column) => column.snippets.header,
|
|
582
|
-
() => [
|
|
583
|
-
{
|
|
584
|
-
get header() {
|
|
585
|
-
return true
|
|
586
|
-
},
|
|
587
|
-
get data() {
|
|
588
|
-
return table.data
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
],
|
|
592
|
-
'header'
|
|
593
|
-
)}
|
|
594
|
-
{#if table.row?.snippets.contextHeader}
|
|
595
|
-
<th class="context-col">
|
|
596
|
-
{@render table.row?.snippets.contextHeader()}
|
|
597
|
-
</th>
|
|
598
|
-
{/if}
|
|
599
|
-
</tr>
|
|
600
|
-
<tr style="width:400px;background:none;pointer-events:none;"></tr>
|
|
601
|
-
</thead>
|
|
602
|
-
{/if}
|
|
603
|
-
|
|
604
|
-
<tbody
|
|
605
|
-
class="content"
|
|
606
|
-
use:reorderArea={{ axis: 'y', class: table.id }}
|
|
607
|
-
bind:this={virtualization.viewport.element}
|
|
608
|
-
onscrollcapture={onscroll}
|
|
609
|
-
bind:clientHeight={virtualization.viewport.height}
|
|
610
|
-
bind:clientWidth={tbody.width}
|
|
611
|
-
>
|
|
612
|
-
{#if table.options.reorderable}
|
|
613
|
-
{@render reorderArea({
|
|
614
|
-
get view() {
|
|
615
|
-
return virtualization.area
|
|
616
|
-
},
|
|
617
|
-
get modify() {
|
|
618
|
-
return table.dataState.origin
|
|
619
|
-
},
|
|
620
|
-
get startIndex() {
|
|
621
|
-
return virtualization.topIndex
|
|
622
|
-
}
|
|
623
|
-
})}
|
|
624
|
-
{:else}
|
|
625
|
-
{#each virtualization.area as item, i (item)}
|
|
626
|
-
{@render rowSnippet(item, { index: i + virtualization.topIndex } as ItemState)}
|
|
627
|
-
{/each}
|
|
628
|
-
{/if}
|
|
629
|
-
</tbody>
|
|
630
|
-
|
|
631
|
-
{#if columns.some((v) => v.snippets.statusbar)}
|
|
632
|
-
<tfoot class="statusbar" bind:this={elements.statusbar}>
|
|
633
|
-
<tr>
|
|
634
|
-
{@render columnsSnippet(
|
|
635
|
-
(column) => column.snippets.statusbar,
|
|
636
|
-
() => [
|
|
637
|
-
{
|
|
638
|
-
get data() {
|
|
639
|
-
return table.data
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
],
|
|
643
|
-
'statusbar'
|
|
644
|
-
)}
|
|
645
|
-
</tr>
|
|
646
|
-
<tr style="width:400px;background:none;pointer-events:none;"></tr>
|
|
647
|
-
</tfoot>
|
|
648
|
-
{/if}
|
|
649
|
-
|
|
650
|
-
<caption class="panel" style="width: {panelTween.current}px;">
|
|
651
|
-
{#if properties.panel && properties.panel in table.panels}
|
|
652
|
-
<div
|
|
653
|
-
class="panel-content"
|
|
654
|
-
bind:offsetWidth={panelTween.size}
|
|
655
|
-
in:fly={{ x: 100, easing: sineInOut, duration: 300 }}
|
|
656
|
-
out:fly={{ x: 100, duration: 200, easing: sineInOut }}
|
|
657
|
-
>
|
|
658
|
-
{@render table.panels[properties.panel].children({
|
|
659
|
-
get table() {
|
|
660
|
-
return table
|
|
661
|
-
},
|
|
662
|
-
get data() {
|
|
663
|
-
return table.data
|
|
664
|
-
}
|
|
665
|
-
})}
|
|
666
|
-
</div>
|
|
667
|
-
{/if}
|
|
668
|
-
</caption>
|
|
669
|
-
<caption
|
|
670
|
-
class="backdrop"
|
|
671
|
-
aria-hidden={properties.panel && table.panels[properties.panel]?.backdrop ? false : true}
|
|
672
|
-
>
|
|
673
|
-
<button
|
|
674
|
-
aria-label="Panel backdrop"
|
|
675
|
-
class="btn-backdrop"
|
|
676
|
-
tabindex="-1"
|
|
677
|
-
onclick={() => (properties.panel = undefined)}
|
|
678
|
-
></button>
|
|
679
|
-
</caption>
|
|
680
|
-
</table>
|
|
681
|
-
|
|
682
|
-
{#snippet headerSelected(ctx: HeaderSelectCtx<T>)}
|
|
683
|
-
<input type="checkbox" indeterminate={ctx.indeterminate} bind:checked={ctx.isSelected} />
|
|
684
|
-
{/snippet}
|
|
685
|
-
|
|
686
|
-
{#snippet rowSelected(ctx: RowSelectCtx<T>)}
|
|
687
|
-
<input type="checkbox" bind:checked={ctx.isSelected} tabindex="-1" />
|
|
688
|
-
{/snippet}
|
|
689
|
-
|
|
690
|
-
{#if table.options.select || table.options.reorderable || table.expandable}
|
|
691
|
-
{@const { select, reorderable } = table.options}
|
|
692
|
-
{@const expandable = table.expandable}
|
|
693
|
-
{@const {
|
|
694
|
-
show = 'hover',
|
|
695
|
-
style = 'column',
|
|
696
|
-
rowSnippet = rowSelected,
|
|
697
|
-
headerSnippet = headerSelected
|
|
698
|
-
} = typeof select === 'boolean' ? {} : select}
|
|
699
|
-
{#if show !== 'never' || reorderable || expandable?.options.chevron !== 'never'}
|
|
700
|
-
<Column
|
|
701
|
-
id="__fixed"
|
|
702
|
-
{table}
|
|
703
|
-
fixed
|
|
704
|
-
width={Math.max(
|
|
705
|
-
48,
|
|
706
|
-
0 +
|
|
707
|
-
(select && show !== 'never' ? 34 : 0) +
|
|
708
|
-
(reorderable ? 34 : 0) +
|
|
709
|
-
(expandable && expandable?.options.chevron !== 'never' ? 34 : 0)
|
|
710
|
-
)}
|
|
711
|
-
resizeable={false}
|
|
712
|
-
>
|
|
713
|
-
{#snippet header()}
|
|
714
|
-
<div class="__fixed">
|
|
715
|
-
{#if reorderable}
|
|
716
|
-
<span style="width: 16px; display: flex; align-items: center;"></span>
|
|
717
|
-
{/if}
|
|
718
|
-
{#if select}
|
|
719
|
-
{@render headerSnippet({
|
|
720
|
-
get isSelected() {
|
|
721
|
-
return table.data.length === table.selected?.length && table.data.length > 0
|
|
722
|
-
},
|
|
723
|
-
set isSelected(value) {
|
|
724
|
-
if (value) {
|
|
725
|
-
table.selected = table.data
|
|
726
|
-
} else {
|
|
727
|
-
table.selected = []
|
|
728
|
-
}
|
|
729
|
-
},
|
|
730
|
-
get selected() {
|
|
731
|
-
return table.selected!
|
|
732
|
-
},
|
|
733
|
-
get indeterminate() {
|
|
734
|
-
return (
|
|
735
|
-
(table.selected?.length || 0) > 0 &&
|
|
736
|
-
table.data.length !== table.selected?.length
|
|
737
|
-
)
|
|
738
|
-
}
|
|
739
|
-
})}
|
|
740
|
-
{/if}
|
|
741
|
-
</div>
|
|
742
|
-
{/snippet}
|
|
743
|
-
{#snippet row(item, row)}
|
|
744
|
-
<div class="__fixed">
|
|
745
|
-
{#if reorderable && row.itemState}
|
|
746
|
-
<span style="width: 16px; display: flex; align-items: center;" use:row.itemState.handle>
|
|
747
|
-
{#if (row.rowHovered && !row.itemState.area.isTarget) || row.itemState.dragging}
|
|
748
|
-
{@render dragSnippet()}
|
|
749
|
-
{/if}
|
|
750
|
-
</span>
|
|
751
|
-
{/if}
|
|
752
|
-
{#if select && (row.selected || show === 'always' || (row.rowHovered && show === 'hover') || row.expanded)}
|
|
753
|
-
{@render rowSnippet({
|
|
754
|
-
get isSelected() {
|
|
755
|
-
return row.selected
|
|
756
|
-
},
|
|
757
|
-
set isSelected(value) {
|
|
758
|
-
row.selected = value
|
|
759
|
-
},
|
|
760
|
-
get row() {
|
|
761
|
-
return row
|
|
762
|
-
},
|
|
763
|
-
get item() {
|
|
764
|
-
return item
|
|
765
|
-
},
|
|
766
|
-
get data() {
|
|
767
|
-
return table.data
|
|
768
|
-
}
|
|
769
|
-
})}
|
|
770
|
-
{/if}
|
|
771
|
-
{#if expandable && expandable?.options.chevron !== 'never'}
|
|
772
|
-
<button class="expand-row" tabindex="-1" onclick={() => (row.expanded = !row.expanded)}>
|
|
773
|
-
{#if row.expanded || expandable.options.chevron === 'always' || (row.rowHovered && expandable.options.chevron === 'hover')}
|
|
774
|
-
{@render chevronSnippet(row.expanded ? 180 : 90)}
|
|
775
|
-
{/if}
|
|
776
|
-
</button>
|
|
777
|
-
{/if}
|
|
778
|
-
</div>
|
|
779
|
-
{/snippet}
|
|
780
|
-
</Column>
|
|
781
|
-
{/if}
|
|
782
|
-
{/if}
|
|
783
|
-
|
|
784
|
-
{#if table.options.auto}
|
|
785
|
-
{#each Object.keys(table.data[0] || {}) as key}
|
|
786
|
-
<Column
|
|
787
|
-
id={key}
|
|
788
|
-
value={(r) => r[key]}
|
|
789
|
-
header={capitalize(segmentize(key))}
|
|
790
|
-
sort={typeof table.data[0]?.[key] === 'number' ?
|
|
791
|
-
(a, b) => a - b
|
|
792
|
-
: (a, b) => String(a).localeCompare(String(b))}
|
|
793
|
-
/>
|
|
794
|
-
{/each}
|
|
795
|
-
{/if}
|
|
796
|
-
|
|
797
|
-
{@render content?.({
|
|
798
|
-
Column,
|
|
799
|
-
Panel,
|
|
800
|
-
Expandable,
|
|
801
|
-
Row,
|
|
802
|
-
get table() {
|
|
803
|
-
return table
|
|
804
|
-
}
|
|
805
|
-
})}
|
|
806
|
-
|
|
807
|
-
<!---------------------------------------------------->
|
|
808
|
-
<style>
|
|
809
|
-
.svelte-tably *,
|
|
810
|
-
.svelte-tably {
|
|
811
|
-
box-sizing: border-box;
|
|
812
|
-
background-color: inherit;
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
.context-col {
|
|
816
|
-
display: flex;
|
|
817
|
-
align-items: center;
|
|
818
|
-
justify-content: center;
|
|
819
|
-
position: sticky;
|
|
820
|
-
right: 0;
|
|
821
|
-
height: 100%;
|
|
822
|
-
z-index: 3;
|
|
823
|
-
padding: 0;
|
|
824
|
-
|
|
825
|
-
&.hover {
|
|
826
|
-
position: absolute;
|
|
827
|
-
}
|
|
828
|
-
&.hidden {
|
|
829
|
-
pointer-events: none;
|
|
830
|
-
user-select: none;
|
|
831
|
-
border-left: none;
|
|
832
|
-
background: none;
|
|
833
|
-
> :global(*) {
|
|
834
|
-
opacity: 0;
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
:global(:root) {
|
|
840
|
-
--tably-color: hsl(0, 0%, 0%);
|
|
841
|
-
--tably-bg: hsl(0, 0%, 100%);
|
|
842
|
-
--tably-statusbar: hsl(0, 0%, 98%);
|
|
843
|
-
|
|
844
|
-
--tably-border: hsl(0, 0%, 90%);
|
|
845
|
-
--tably-border-grid: hsl(0, 0%, 98%);
|
|
846
|
-
|
|
847
|
-
--tably-padding-x: 1rem;
|
|
848
|
-
--tably-padding-y: 0.5rem;
|
|
849
|
-
|
|
850
|
-
--tably-radius: 0.25rem;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
.svelte-tably {
|
|
854
|
-
position: relative;
|
|
855
|
-
overflow: visible;
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
.expandable {
|
|
859
|
-
position: relative;
|
|
860
|
-
|
|
861
|
-
& > td {
|
|
862
|
-
position: sticky;
|
|
863
|
-
left: 1px;
|
|
864
|
-
> div {
|
|
865
|
-
position: absolute;
|
|
866
|
-
overflow: auto;
|
|
867
|
-
top: -1.5px;
|
|
868
|
-
left: 0;
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
|
|
873
|
-
.expand-row {
|
|
874
|
-
display: flex;
|
|
875
|
-
justify-content: center;
|
|
876
|
-
align-items: center;
|
|
877
|
-
padding: 0;
|
|
878
|
-
outline: none;
|
|
879
|
-
border: none;
|
|
880
|
-
cursor: pointer;
|
|
881
|
-
background-color: transparent;
|
|
882
|
-
color: inherit;
|
|
883
|
-
width: 20px;
|
|
884
|
-
height: 100%;
|
|
885
|
-
|
|
886
|
-
> svg {
|
|
887
|
-
transition: transform 0.15s ease;
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
caption {
|
|
892
|
-
all: unset;
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
input[type='checkbox'] {
|
|
896
|
-
width: 18px;
|
|
897
|
-
height: 18px;
|
|
898
|
-
cursor: pointer;
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
button.btn-backdrop {
|
|
902
|
-
outline: none;
|
|
903
|
-
border: none;
|
|
904
|
-
cursor: pointer;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
.sorting-icon {
|
|
908
|
-
align-items: center;
|
|
909
|
-
justify-items: end;
|
|
910
|
-
margin: 0;
|
|
911
|
-
margin-left: auto;
|
|
912
|
-
> svg {
|
|
913
|
-
transition: transform 0.15s ease;
|
|
914
|
-
}
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
th:not(:last-child) .sorting-icon {
|
|
918
|
-
margin-right: var(--tably-padding-x);
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
.__fixed {
|
|
922
|
-
display: flex;
|
|
923
|
-
align-items: center;
|
|
924
|
-
justify-content: center;
|
|
925
|
-
gap: 0.25rem;
|
|
926
|
-
position: absolute;
|
|
927
|
-
top: 0;
|
|
928
|
-
left: 0;
|
|
929
|
-
right: 0;
|
|
930
|
-
bottom: 0;
|
|
931
|
-
width: 100%;
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
tbody::before,
|
|
935
|
-
tbody::after,
|
|
936
|
-
selects::before,
|
|
937
|
-
selects::after {
|
|
938
|
-
content: '';
|
|
939
|
-
display: grid;
|
|
940
|
-
min-height: 100%;
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
tbody::before,
|
|
944
|
-
selects::before {
|
|
945
|
-
height: var(--t);
|
|
946
|
-
}
|
|
947
|
-
tbody::after,
|
|
948
|
-
selects::after {
|
|
949
|
-
height: var(--b);
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
.row:global(:is(a)) {
|
|
953
|
-
color: inherit;
|
|
954
|
-
text-decoration: inherit;
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
.backdrop {
|
|
958
|
-
position: absolute;
|
|
959
|
-
left: 0px;
|
|
960
|
-
top: 0px;
|
|
961
|
-
bottom: 0px;
|
|
962
|
-
right: 0px;
|
|
963
|
-
background-color: hsla(0, 0%, 0%, 0.3);
|
|
964
|
-
z-index: 3;
|
|
965
|
-
opacity:
|
|
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
|
-
.sticky
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
grid-template-
|
|
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
|
-
overflow:
|
|
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
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
border
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1
|
+
<!-- @component
|
|
2
|
+
|
|
3
|
+
This is a description, \
|
|
4
|
+
on how to use this.
|
|
5
|
+
|
|
6
|
+
@example
|
|
7
|
+
<Component />
|
|
8
|
+
|
|
9
|
+
-->
|
|
10
|
+
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
import { type Snippet } from 'svelte'
|
|
13
|
+
import { fly } from 'svelte/transition'
|
|
14
|
+
import { sineInOut } from 'svelte/easing'
|
|
15
|
+
import reorder, { type ItemState } from 'runic-reorder'
|
|
16
|
+
import { Virtualization } from './virtualization.svelte.js'
|
|
17
|
+
import {
|
|
18
|
+
TableState,
|
|
19
|
+
type HeaderSelectCtx,
|
|
20
|
+
type RowCtx,
|
|
21
|
+
type RowSelectCtx,
|
|
22
|
+
type TableProps
|
|
23
|
+
} from './table-state.svelte.js'
|
|
24
|
+
import Panel from '../panel/Panel.svelte'
|
|
25
|
+
import Column from '../column/Column.svelte'
|
|
26
|
+
import { assignDescriptors, capitalize, fromProps, mounted, segmentize } from '../utility.svelte.js'
|
|
27
|
+
import { conditional } from '../conditional.svelte.js'
|
|
28
|
+
import { ColumnState, type RowColumnCtx } from '../column/column-state.svelte.js'
|
|
29
|
+
import Expandable from '../expandable/Expandable.svelte'
|
|
30
|
+
import { SizeTween } from '../size-tween.svelte.js'
|
|
31
|
+
import { on } from 'svelte/events'
|
|
32
|
+
import Row from '../row/Row.svelte'
|
|
33
|
+
|
|
34
|
+
type T = $$Generic<Record<PropertyKey, unknown>>
|
|
35
|
+
|
|
36
|
+
type ConstructorReturnType<T extends new (...args: any[]) => any> =
|
|
37
|
+
T extends new (...args: any[]) => infer K ? K : never
|
|
38
|
+
type ConstructorParams<T extends new (...args: any[]) => any> =
|
|
39
|
+
T extends new (...args: infer K) => any ? K : never
|
|
40
|
+
|
|
41
|
+
type ContentCtx<T extends Record<PropertyKey, unknown>> = {
|
|
42
|
+
Column: {
|
|
43
|
+
new <V>(...args: ConstructorParams<typeof Column<T, V>>): ConstructorReturnType<typeof Column<T, V>>
|
|
44
|
+
<V>(...args: Parameters<typeof Column<T, V>>): ReturnType<typeof Column<T, V>>
|
|
45
|
+
}
|
|
46
|
+
Panel: typeof Panel<T>
|
|
47
|
+
Expandable: typeof Expandable<T>
|
|
48
|
+
Row: typeof Row<T>
|
|
49
|
+
readonly table: TableState<T>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type ContentSnippet = Snippet<[context: ContentCtx<T>]>
|
|
53
|
+
|
|
54
|
+
let {
|
|
55
|
+
content,
|
|
56
|
+
selected: _selected = $bindable([]),
|
|
57
|
+
panel: _panel = $bindable(),
|
|
58
|
+
data: _data = $bindable([]),
|
|
59
|
+
...restProps
|
|
60
|
+
}: TableProps<T> & { content?: ContentSnippet } = $props()
|
|
61
|
+
|
|
62
|
+
const properties = fromProps(restProps, {
|
|
63
|
+
selected: [() => _selected, (v) => (_selected = v)],
|
|
64
|
+
panel: [() => _panel, (v) => (_panel = v)],
|
|
65
|
+
data: [() => _data, (v) => (_data = v)]
|
|
66
|
+
}) as TableProps<T>
|
|
67
|
+
|
|
68
|
+
const mount = mounted()
|
|
69
|
+
|
|
70
|
+
const reorderArea = reorder(rowSnippet)
|
|
71
|
+
|
|
72
|
+
const elements = $state({}) as Record<
|
|
73
|
+
'headers' | 'statusbar' | 'rows' | 'virtualTop' | 'virtualBottom' | 'selects',
|
|
74
|
+
HTMLElement
|
|
75
|
+
>
|
|
76
|
+
|
|
77
|
+
const table = new TableState<T>(properties) as TableState<T>
|
|
78
|
+
|
|
79
|
+
const virtualization = new Virtualization(table)
|
|
80
|
+
|
|
81
|
+
const panelTween = new SizeTween(() => !!properties.panel)
|
|
82
|
+
|
|
83
|
+
let hoveredRow: T | null = $state(null)
|
|
84
|
+
let hoveredColumn: ColumnState | null = $state(null)
|
|
85
|
+
|
|
86
|
+
/** Order of columns */
|
|
87
|
+
const fixed = $derived(table.positions.fixed)
|
|
88
|
+
const hidden = $derived(table.positions.hidden)
|
|
89
|
+
const notHidden = (column: ColumnState) => !table.positions.hidden.includes(column)
|
|
90
|
+
const sticky = $derived(table.positions.sticky.filter(notHidden))
|
|
91
|
+
const scrolled = $derived(table.positions.scroll.filter(notHidden))
|
|
92
|
+
const columns = $derived([...fixed, ...sticky, ...scrolled])
|
|
93
|
+
|
|
94
|
+
/** Width of each column */
|
|
95
|
+
const columnWidths = $state({}) as Record<string, number>
|
|
96
|
+
|
|
97
|
+
const getWidth = (key: string, def: number = 150) =>
|
|
98
|
+
columnWidths[key] || table.columns[key]?.defaults.width || def
|
|
99
|
+
|
|
100
|
+
/** grid-template-columns for widths */
|
|
101
|
+
const style = $derived.by(() => {
|
|
102
|
+
if (!mount.isMounted) return ''
|
|
103
|
+
|
|
104
|
+
const context = table.row?.snippets.context ? table.row?.options.context.width : ''
|
|
105
|
+
|
|
106
|
+
const templateColumns =
|
|
107
|
+
columns
|
|
108
|
+
.map((column, i, arr) => {
|
|
109
|
+
const width = getWidth(column.id)
|
|
110
|
+
if (i === arr.length - 1) return `minmax(${width}px, 1fr)`
|
|
111
|
+
return `${width}px`
|
|
112
|
+
})
|
|
113
|
+
.join(' ') + context
|
|
114
|
+
|
|
115
|
+
const theadTempla3teColumns = `
|
|
116
|
+
#${table.id} > thead > tr,
|
|
117
|
+
#${table.id} > tfoot > tr {
|
|
118
|
+
grid-template-columns: ${templateColumns};
|
|
119
|
+
}
|
|
120
|
+
`
|
|
121
|
+
|
|
122
|
+
const tbodyTemplateColumns = `
|
|
123
|
+
[data-area-class='${table.id}'] tr.row,
|
|
124
|
+
#${table.id} > tbody::after {
|
|
125
|
+
grid-template-columns: ${templateColumns};
|
|
126
|
+
}
|
|
127
|
+
`
|
|
128
|
+
|
|
129
|
+
let sum = 0
|
|
130
|
+
const stickyLeft = [...fixed, ...sticky]
|
|
131
|
+
.map((column, i, arr) => {
|
|
132
|
+
sum += getWidth(arr[i - 1]?.id, i === 0 ? 0 : undefined)
|
|
133
|
+
return `
|
|
134
|
+
#${table.id} .column.sticky[data-column='${column.id}'],
|
|
135
|
+
[data-svelte-tably='${table.id}'] .column.sticky[data-column='${column.id}'] {
|
|
136
|
+
left: ${sum}px;
|
|
137
|
+
}
|
|
138
|
+
`
|
|
139
|
+
})
|
|
140
|
+
.join('')
|
|
141
|
+
|
|
142
|
+
const columnStyling = columns
|
|
143
|
+
.map((column) =>
|
|
144
|
+
!column.options.style ?
|
|
145
|
+
''
|
|
146
|
+
: `
|
|
147
|
+
[data-area-class='${table.id}'] .column[data-column='${column.id}'] {
|
|
148
|
+
${column.options.style}
|
|
149
|
+
}
|
|
150
|
+
`
|
|
151
|
+
)
|
|
152
|
+
.join('')
|
|
153
|
+
|
|
154
|
+
return theadTempla3teColumns + tbodyTemplateColumns + stickyLeft + columnStyling
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
function observeColumnWidth(node: HTMLDivElement, isHeader = false) {
|
|
158
|
+
if (!isHeader) return
|
|
159
|
+
|
|
160
|
+
const key = node.getAttribute('data-column')!
|
|
161
|
+
node.style.width = getWidth(key) + 'px'
|
|
162
|
+
|
|
163
|
+
let mouseup = false
|
|
164
|
+
|
|
165
|
+
const observer = new MutationObserver(() => {
|
|
166
|
+
const width = parseFloat(node.style.width)
|
|
167
|
+
if (width === columnWidths[key]) return
|
|
168
|
+
columnWidths[key] = width
|
|
169
|
+
if (!mouseup) {
|
|
170
|
+
mouseup = true
|
|
171
|
+
window.addEventListener(
|
|
172
|
+
'click',
|
|
173
|
+
(e) => {
|
|
174
|
+
e.preventDefault()
|
|
175
|
+
e.stopPropagation()
|
|
176
|
+
mouseup = false
|
|
177
|
+
},
|
|
178
|
+
{ once: true, capture: true }
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
observer.observe(node, { attributes: true })
|
|
184
|
+
return { destroy: () => observer.disconnect() }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let tbody = $state({
|
|
188
|
+
width: 0
|
|
189
|
+
})
|
|
190
|
+
async function onscroll() {
|
|
191
|
+
const target = virtualization.viewport.element!
|
|
192
|
+
if (target.scrollTop !== virtualization.scrollTop) {
|
|
193
|
+
virtualization.scrollTop = target?.scrollTop ?? virtualization.scrollTop
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (elements.selects) {
|
|
197
|
+
elements.selects.scrollTop = target?.scrollTop
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!elements.headers) return
|
|
201
|
+
elements.headers.scrollLeft = target.scrollLeft
|
|
202
|
+
elements.statusbar.scrollLeft = target.scrollLeft
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// * --- CSV --- *
|
|
206
|
+
let csv = $state(false) as false | { selected?: boolean }
|
|
207
|
+
let csvElement = $state() as undefined | HTMLTableElement
|
|
208
|
+
interface CSVOptions {
|
|
209
|
+
/** Semi-colons as separator? */
|
|
210
|
+
semicolon?: boolean
|
|
211
|
+
/** Only selected rows */
|
|
212
|
+
selected?: boolean
|
|
213
|
+
}
|
|
214
|
+
export async function toCSV(opts: CSVOptions = {}) {
|
|
215
|
+
csv = { selected: !!opts.selected }
|
|
216
|
+
let resolve: (value: HTMLTableElement) => void
|
|
217
|
+
const promise = new Promise<HTMLTableElement>((r) => (resolve = r))
|
|
218
|
+
|
|
219
|
+
const clean = $effect.root(() => {
|
|
220
|
+
$effect(() => {
|
|
221
|
+
if (csvElement) {
|
|
222
|
+
resolve(csvElement)
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
let table = await promise
|
|
228
|
+
clean()
|
|
229
|
+
|
|
230
|
+
const separator = opts.semicolon ? ';' : ','
|
|
231
|
+
const rows = Array.from(table.rows)
|
|
232
|
+
const csvRows = [] as string[]
|
|
233
|
+
|
|
234
|
+
for (const row of rows) {
|
|
235
|
+
const cells = Array.from(row.cells)
|
|
236
|
+
const csvCells = cells.map((cell) => {
|
|
237
|
+
let text = cell.textContent?.trim() || ''
|
|
238
|
+
|
|
239
|
+
// Escape double quotes and wrap in quotes if needed
|
|
240
|
+
if (text.includes('"')) {
|
|
241
|
+
text = text.replace(/"/g, '""')
|
|
242
|
+
}
|
|
243
|
+
if (text.includes(separator) || text.includes('"') || text.includes('\n')) {
|
|
244
|
+
text = `"${text}"`
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return text
|
|
248
|
+
})
|
|
249
|
+
csvRows.push(csvCells.join(separator))
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
csv = false
|
|
253
|
+
return csvRows.join('\n')
|
|
254
|
+
}
|
|
255
|
+
// * --- CSV --- *
|
|
256
|
+
|
|
257
|
+
let expandedRow = $state([]) as T[]
|
|
258
|
+
let expandTick = false
|
|
259
|
+
function toggleExpand(item: T, value?: boolean) {
|
|
260
|
+
if (expandTick) return
|
|
261
|
+
expandTick = true
|
|
262
|
+
requestAnimationFrame(() => (expandTick = false))
|
|
263
|
+
|
|
264
|
+
let indexOf = expandedRow.indexOf(item)
|
|
265
|
+
if (value === undefined) {
|
|
266
|
+
value = indexOf === -1
|
|
267
|
+
}
|
|
268
|
+
if (!value) {
|
|
269
|
+
expandedRow.splice(indexOf, 1)
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
if (table.expandable?.options.multiple === true) {
|
|
273
|
+
expandedRow.push(item)
|
|
274
|
+
} else {
|
|
275
|
+
expandedRow[0] = item
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function addRowColumnEvents(
|
|
280
|
+
node: HTMLTableColElement,
|
|
281
|
+
opts: ['header' | 'row' | 'statusbar', ColumnState, () => RowColumnCtx<T, any>]
|
|
282
|
+
) {
|
|
283
|
+
const [where, column, value] = opts
|
|
284
|
+
if (where !== 'row') return
|
|
285
|
+
if (column.options.onclick) {
|
|
286
|
+
$effect(() => on(node, 'click', (e) => column.options.onclick!(e, value())))
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function addRowEvents(node: HTMLTableRowElement, ctx: RowCtx<T>) {
|
|
291
|
+
if (table.row?.events.onclick) {
|
|
292
|
+
$effect(() => on(node, 'click', (e) => table.row?.events.onclick!(e, ctx)))
|
|
293
|
+
}
|
|
294
|
+
if (table.row?.events.oncontextmenu) {
|
|
295
|
+
$effect(() => on(node, 'contextmenu', (e) => table.row?.events.oncontextmenu!(e, ctx)))
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
</script>
|
|
299
|
+
|
|
300
|
+
<!---------------------------------------------------->
|
|
301
|
+
|
|
302
|
+
{#if csv !== false}
|
|
303
|
+
{@const renderedColumns = columns.filter((v) => v.id !== '__fixed')}
|
|
304
|
+
<table bind:this={csvElement} hidden>
|
|
305
|
+
<thead>
|
|
306
|
+
<tr>
|
|
307
|
+
{#each renderedColumns as column}
|
|
308
|
+
<th>{@render column.snippets.title()}</th>
|
|
309
|
+
{/each}
|
|
310
|
+
</tr>
|
|
311
|
+
</thead>
|
|
312
|
+
<tbody>
|
|
313
|
+
{#each table.data as row, i}
|
|
314
|
+
{#if (csv.selected && table.selected.includes(row)) || !csv.selected}
|
|
315
|
+
<tr>
|
|
316
|
+
{#each renderedColumns as column}
|
|
317
|
+
<td>
|
|
318
|
+
{#if column.snippets.row}
|
|
319
|
+
{@render column.snippets.row(row, {
|
|
320
|
+
index: i,
|
|
321
|
+
value: column.options.value?.(row),
|
|
322
|
+
columnHovered: false,
|
|
323
|
+
rowHovered: false,
|
|
324
|
+
itemState: {
|
|
325
|
+
index: i,
|
|
326
|
+
dragging: false,
|
|
327
|
+
positioning: false
|
|
328
|
+
} as ItemState<any>,
|
|
329
|
+
selected: false,
|
|
330
|
+
expanded: false
|
|
331
|
+
})}
|
|
332
|
+
{:else}
|
|
333
|
+
{column.options.value?.(row)}
|
|
334
|
+
{/if}
|
|
335
|
+
</td>
|
|
336
|
+
{/each}
|
|
337
|
+
</tr>
|
|
338
|
+
{/if}
|
|
339
|
+
{/each}
|
|
340
|
+
</tbody>
|
|
341
|
+
</table>
|
|
342
|
+
{/if}
|
|
343
|
+
|
|
344
|
+
<svelte:head>
|
|
345
|
+
{@html `<`+`style>${style}</style>`}
|
|
346
|
+
</svelte:head>
|
|
347
|
+
|
|
348
|
+
{#snippet chevronSnippet(rotation: number = 0)}
|
|
349
|
+
<svg
|
|
350
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
351
|
+
width="16"
|
|
352
|
+
height="16"
|
|
353
|
+
viewBox="0 0 16 16"
|
|
354
|
+
style="transform: rotate({rotation}deg)"
|
|
355
|
+
>
|
|
356
|
+
<path
|
|
357
|
+
fill="currentColor"
|
|
358
|
+
d="M3.2 10.26a.75.75 0 0 0 1.06.04L8 6.773l3.74 3.527a.75.75 0 1 0 1.02-1.1l-4.25-4a.75.75 0 0 0-1.02 0l-4.25 4a.75.75 0 0 0-.04 1.06"
|
|
359
|
+
></path>
|
|
360
|
+
</svg>
|
|
361
|
+
{/snippet}
|
|
362
|
+
|
|
363
|
+
{#snippet dragSnippet()}
|
|
364
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" style="opacity: .3">
|
|
365
|
+
<path
|
|
366
|
+
fill="currentColor"
|
|
367
|
+
d="M5.5 5a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3m0 4.5a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3m1.5 3a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0M10.5 5a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3M12 8a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0m-1.5 6a1.5 1.5 0 1 0 0-3a1.5 1.5 0 0 0 0 3"
|
|
368
|
+
></path>
|
|
369
|
+
</svg>
|
|
370
|
+
{/snippet}
|
|
371
|
+
|
|
372
|
+
{#snippet columnsSnippet(
|
|
373
|
+
renderable: (column: ColumnState) => Snippet<[arg0?: any, arg1?: any]> | undefined,
|
|
374
|
+
arg: null | ((column: ColumnState) => any[]) = null,
|
|
375
|
+
where: 'header' | 'row' | 'statusbar'
|
|
376
|
+
)}
|
|
377
|
+
{@const isHeader = where === 'header'}
|
|
378
|
+
{#each fixed as column, i (column)}
|
|
379
|
+
{#if !hidden.includes(column)}
|
|
380
|
+
{@const args = arg ? arg(column) : []}
|
|
381
|
+
{@const sortable = isHeader && column.options.sort && !table.options.reorderable}
|
|
382
|
+
<svelte:element
|
|
383
|
+
this={isHeader ? 'th' : 'td'}
|
|
384
|
+
class={column.options.class ?? ''}
|
|
385
|
+
class:column={true}
|
|
386
|
+
class:sticky={true}
|
|
387
|
+
class:fixed={true}
|
|
388
|
+
use:addRowColumnEvents={[where, column, () => args[1]]}
|
|
389
|
+
data-column={column.id}
|
|
390
|
+
class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
|
|
391
|
+
class:header={isHeader}
|
|
392
|
+
class:sortable
|
|
393
|
+
use:conditional={[isHeader, (node) => table.dataState.sortAction(node, column.id)]}
|
|
394
|
+
onpointerenter={() => (hoveredColumn = column)}
|
|
395
|
+
onpointerleave={() => (hoveredColumn = null)}
|
|
396
|
+
>
|
|
397
|
+
{@render renderable(column)?.(args[0], args[1])}
|
|
398
|
+
{#if isHeader && table.dataState.sortby === column.id && sortable}
|
|
399
|
+
<span class="sorting-icon">
|
|
400
|
+
{@render chevronSnippet(table.dataState.sortReverse ? 0 : 180)}
|
|
401
|
+
</span>
|
|
402
|
+
{/if}
|
|
403
|
+
</svelte:element>
|
|
404
|
+
{/if}
|
|
405
|
+
{/each}
|
|
406
|
+
{#each sticky as column, i (column)}
|
|
407
|
+
{#if !hidden.includes(column)}
|
|
408
|
+
{@const args = arg ? arg(column) : []}
|
|
409
|
+
{@const sortable = isHeader && column.options.sort && !table.options.reorderable}
|
|
410
|
+
<svelte:element
|
|
411
|
+
this={isHeader ? 'th' : 'td'}
|
|
412
|
+
class={column.options.class ?? ''}
|
|
413
|
+
class:column={true}
|
|
414
|
+
class:sticky={true}
|
|
415
|
+
use:addRowColumnEvents={[where, column, () => args[1]]}
|
|
416
|
+
use:observeColumnWidth={isHeader}
|
|
417
|
+
data-column={column.id}
|
|
418
|
+
class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
|
|
419
|
+
class:header={isHeader}
|
|
420
|
+
class:resizeable={isHeader && column.options.resizeable && table.options.resizeable}
|
|
421
|
+
class:border={i == sticky.length - 1}
|
|
422
|
+
class:sortable
|
|
423
|
+
use:conditional={[isHeader, (node) => table.dataState.sortAction(node, column.id)]}
|
|
424
|
+
onpointerenter={() => (hoveredColumn = column)}
|
|
425
|
+
onpointerleave={() => (hoveredColumn = null)}
|
|
426
|
+
>
|
|
427
|
+
{@render renderable(column)?.(args[0], args[1])}
|
|
428
|
+
{#if isHeader && table.dataState.sortby === column.id && sortable}
|
|
429
|
+
<span class="sorting-icon">
|
|
430
|
+
{@render chevronSnippet(table.dataState.sortReverse ? 0 : 180)}
|
|
431
|
+
</span>
|
|
432
|
+
{/if}
|
|
433
|
+
</svelte:element>
|
|
434
|
+
{/if}
|
|
435
|
+
{/each}
|
|
436
|
+
{#each scrolled as column, i (column)}
|
|
437
|
+
{#if !hidden.includes(column)}
|
|
438
|
+
{@const args = arg ? arg(column) : []}
|
|
439
|
+
{@const sortable = isHeader && column!.options.sort && !table.options.reorderable}
|
|
440
|
+
<svelte:element
|
|
441
|
+
this={isHeader ? 'th' : 'td'}
|
|
442
|
+
class={column.options.class ?? ''}
|
|
443
|
+
class:column={true}
|
|
444
|
+
data-column={column.id}
|
|
445
|
+
class:pad={(isHeader && column.options.padHeader) || (!isHeader && column.options.padRow)}
|
|
446
|
+
use:addRowColumnEvents={[where, column, () => args[1]]}
|
|
447
|
+
use:observeColumnWidth={isHeader}
|
|
448
|
+
class:resizeable={isHeader && column.options.resizeable && table.options.resizeable}
|
|
449
|
+
class:sortable
|
|
450
|
+
use:conditional={[isHeader, (node) => table.dataState.sortAction(node, column.id)]}
|
|
451
|
+
onpointerenter={() => (hoveredColumn = column)}
|
|
452
|
+
onpointerleave={() => (hoveredColumn = null)}
|
|
453
|
+
>
|
|
454
|
+
{@render renderable(column)?.(args[0], args[1])}
|
|
455
|
+
{#if isHeader && table.dataState.sortby === column.id && sortable}
|
|
456
|
+
<span class="sorting-icon">
|
|
457
|
+
{@render chevronSnippet(table.dataState.sortReverse ? 0 : 180)}
|
|
458
|
+
</span>
|
|
459
|
+
{/if}
|
|
460
|
+
</svelte:element>
|
|
461
|
+
{/if}
|
|
462
|
+
{/each}
|
|
463
|
+
{/snippet}
|
|
464
|
+
|
|
465
|
+
{#snippet defaultRow(item: T, ctx: RowColumnCtx<T, any>)}
|
|
466
|
+
{ctx.value}
|
|
467
|
+
{/snippet}
|
|
468
|
+
|
|
469
|
+
{#snippet rowSnippet(item: T, itemState?: ItemState<T>)}
|
|
470
|
+
{@const index = itemState?.index ?? 0}
|
|
471
|
+
|
|
472
|
+
{@const ctx: RowCtx<T> = {
|
|
473
|
+
get index() {
|
|
474
|
+
return index
|
|
475
|
+
},
|
|
476
|
+
get rowHovered() {
|
|
477
|
+
return hoveredRow === item
|
|
478
|
+
},
|
|
479
|
+
get selected() {
|
|
480
|
+
return table.selected?.includes(item)
|
|
481
|
+
},
|
|
482
|
+
set selected(value) {
|
|
483
|
+
value ?
|
|
484
|
+
table.selected!.push(item)
|
|
485
|
+
: table.selected!.splice(table.selected!.indexOf(item), 1)
|
|
486
|
+
},
|
|
487
|
+
get itemState() {
|
|
488
|
+
return itemState
|
|
489
|
+
},
|
|
490
|
+
get expanded() {
|
|
491
|
+
return expandedRow.includes(item)
|
|
492
|
+
},
|
|
493
|
+
set expanded(value) {
|
|
494
|
+
toggleExpand(item, value)
|
|
495
|
+
}
|
|
496
|
+
}}
|
|
497
|
+
|
|
498
|
+
<tr
|
|
499
|
+
aria-rowindex={index + 1}
|
|
500
|
+
style:opacity={itemState?.positioning ? 0 : 1}
|
|
501
|
+
class="row"
|
|
502
|
+
class:dragging={itemState?.dragging}
|
|
503
|
+
class:selected={table.selected?.includes(item)}
|
|
504
|
+
class:first={index === 0}
|
|
505
|
+
class:last={index === virtualization.area.length - 1}
|
|
506
|
+
{...itemState?.dragging ? { 'data-svelte-tably': table.id } : {}}
|
|
507
|
+
onpointerenter={() => (hoveredRow = item)}
|
|
508
|
+
onpointerleave={() => (hoveredRow = null)}
|
|
509
|
+
use:addRowEvents={ctx}
|
|
510
|
+
onclick={(e) => {
|
|
511
|
+
if (table.expandable?.options.click === true) {
|
|
512
|
+
let target = e.target as HTMLElement
|
|
513
|
+
if (['INPUT', 'TEXTAREA', 'BUTTON', 'A'].includes(target.tagName)) {
|
|
514
|
+
return
|
|
515
|
+
}
|
|
516
|
+
ctx.expanded = !ctx.expanded
|
|
517
|
+
}
|
|
518
|
+
}}
|
|
519
|
+
>
|
|
520
|
+
{@render columnsSnippet(
|
|
521
|
+
(column) => column.snippets.row ?? defaultRow,
|
|
522
|
+
(column) => {
|
|
523
|
+
return [
|
|
524
|
+
item,
|
|
525
|
+
assignDescriptors(
|
|
526
|
+
{
|
|
527
|
+
get value() {
|
|
528
|
+
return column.options.value ? column.options.value(item) : undefined
|
|
529
|
+
},
|
|
530
|
+
get columnHovered() {
|
|
531
|
+
return hoveredColumn === column
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
ctx
|
|
535
|
+
)
|
|
536
|
+
]
|
|
537
|
+
},
|
|
538
|
+
'row'
|
|
539
|
+
)}
|
|
540
|
+
{#if table.row?.snippets.context}
|
|
541
|
+
{#if table.row?.snippets.contextHeader || !table.row?.options.context.hover || hoveredRow === item}
|
|
542
|
+
<td
|
|
543
|
+
class="context-col"
|
|
544
|
+
class:hover={!table.row?.snippets.contextHeader && table.row?.options.context.hover}
|
|
545
|
+
class:hidden={table.row?.options.context.hover &&
|
|
546
|
+
table.row?.snippets.contextHeader &&
|
|
547
|
+
hoveredRow !== item}
|
|
548
|
+
>
|
|
549
|
+
{@render table.row?.snippets.context?.(item, ctx)}
|
|
550
|
+
</td>
|
|
551
|
+
{/if}
|
|
552
|
+
{/if}
|
|
553
|
+
</tr>
|
|
554
|
+
|
|
555
|
+
{@const expandableTween = new SizeTween(() => table.expandable && expandedRow.includes(item), {
|
|
556
|
+
min: 1,
|
|
557
|
+
duration: table.expandable?.options.slide.duration,
|
|
558
|
+
easing: table.expandable?.options.slide.easing
|
|
559
|
+
})}
|
|
560
|
+
{#if expandableTween.current > 0}
|
|
561
|
+
<tr class="expandable" style="height: {expandableTween.current}px">
|
|
562
|
+
<td colspan={columns.length} style="height: {expandableTween.current}px">
|
|
563
|
+
<div bind:offsetHeight={expandableTween.size} style="width: {tbody.width - 3}px">
|
|
564
|
+
{@render table.expandable!.snippets.content?.(item, ctx)}
|
|
565
|
+
</div>
|
|
566
|
+
</td>
|
|
567
|
+
</tr>
|
|
568
|
+
{/if}
|
|
569
|
+
{/snippet}
|
|
570
|
+
|
|
571
|
+
<table
|
|
572
|
+
id={table.id}
|
|
573
|
+
class="table svelte-tably"
|
|
574
|
+
style="--t: {virtualization.virtualTop}px; --b: {virtualization.virtualBottom}px;"
|
|
575
|
+
aria-rowcount={table.data.length}
|
|
576
|
+
>
|
|
577
|
+
{#if columns.some((v) => v.snippets.header)}
|
|
578
|
+
<thead class="headers" bind:this={elements.headers}>
|
|
579
|
+
<tr style="min-width: {tbody.width}px">
|
|
580
|
+
{@render columnsSnippet(
|
|
581
|
+
(column) => column.snippets.header,
|
|
582
|
+
() => [
|
|
583
|
+
{
|
|
584
|
+
get header() {
|
|
585
|
+
return true
|
|
586
|
+
},
|
|
587
|
+
get data() {
|
|
588
|
+
return table.data
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
],
|
|
592
|
+
'header'
|
|
593
|
+
)}
|
|
594
|
+
{#if table.row?.snippets.contextHeader}
|
|
595
|
+
<th class="context-col">
|
|
596
|
+
{@render table.row?.snippets.contextHeader()}
|
|
597
|
+
</th>
|
|
598
|
+
{/if}
|
|
599
|
+
</tr>
|
|
600
|
+
<tr style="width:400px;background:none;pointer-events:none;"></tr>
|
|
601
|
+
</thead>
|
|
602
|
+
{/if}
|
|
603
|
+
|
|
604
|
+
<tbody
|
|
605
|
+
class="content"
|
|
606
|
+
use:reorderArea={{ axis: 'y', class: table.id }}
|
|
607
|
+
bind:this={virtualization.viewport.element}
|
|
608
|
+
onscrollcapture={onscroll}
|
|
609
|
+
bind:clientHeight={virtualization.viewport.height}
|
|
610
|
+
bind:clientWidth={tbody.width}
|
|
611
|
+
>
|
|
612
|
+
{#if table.options.reorderable}
|
|
613
|
+
{@render reorderArea({
|
|
614
|
+
get view() {
|
|
615
|
+
return virtualization.area
|
|
616
|
+
},
|
|
617
|
+
get modify() {
|
|
618
|
+
return table.dataState.origin
|
|
619
|
+
},
|
|
620
|
+
get startIndex() {
|
|
621
|
+
return virtualization.topIndex
|
|
622
|
+
}
|
|
623
|
+
})}
|
|
624
|
+
{:else}
|
|
625
|
+
{#each virtualization.area as item, i (item)}
|
|
626
|
+
{@render rowSnippet(item, { index: i + virtualization.topIndex } as ItemState)}
|
|
627
|
+
{/each}
|
|
628
|
+
{/if}
|
|
629
|
+
</tbody>
|
|
630
|
+
|
|
631
|
+
{#if columns.some((v) => v.snippets.statusbar)}
|
|
632
|
+
<tfoot class="statusbar" bind:this={elements.statusbar}>
|
|
633
|
+
<tr>
|
|
634
|
+
{@render columnsSnippet(
|
|
635
|
+
(column) => column.snippets.statusbar,
|
|
636
|
+
() => [
|
|
637
|
+
{
|
|
638
|
+
get data() {
|
|
639
|
+
return table.data
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
],
|
|
643
|
+
'statusbar'
|
|
644
|
+
)}
|
|
645
|
+
</tr>
|
|
646
|
+
<tr style="width:400px;background:none;pointer-events:none;"></tr>
|
|
647
|
+
</tfoot>
|
|
648
|
+
{/if}
|
|
649
|
+
|
|
650
|
+
<caption class="panel" style="width: {panelTween.current}px;">
|
|
651
|
+
{#if properties.panel && properties.panel in table.panels}
|
|
652
|
+
<div
|
|
653
|
+
class="panel-content"
|
|
654
|
+
bind:offsetWidth={panelTween.size}
|
|
655
|
+
in:fly={{ x: 100, easing: sineInOut, duration: 300 }}
|
|
656
|
+
out:fly={{ x: 100, duration: 200, easing: sineInOut }}
|
|
657
|
+
>
|
|
658
|
+
{@render table.panels[properties.panel].children({
|
|
659
|
+
get table() {
|
|
660
|
+
return table
|
|
661
|
+
},
|
|
662
|
+
get data() {
|
|
663
|
+
return table.data
|
|
664
|
+
}
|
|
665
|
+
})}
|
|
666
|
+
</div>
|
|
667
|
+
{/if}
|
|
668
|
+
</caption>
|
|
669
|
+
<caption
|
|
670
|
+
class="backdrop"
|
|
671
|
+
aria-hidden={properties.panel && table.panels[properties.panel]?.backdrop ? false : true}
|
|
672
|
+
>
|
|
673
|
+
<button
|
|
674
|
+
aria-label="Panel backdrop"
|
|
675
|
+
class="btn-backdrop"
|
|
676
|
+
tabindex="-1"
|
|
677
|
+
onclick={() => (properties.panel = undefined)}
|
|
678
|
+
></button>
|
|
679
|
+
</caption>
|
|
680
|
+
</table>
|
|
681
|
+
|
|
682
|
+
{#snippet headerSelected(ctx: HeaderSelectCtx<T>)}
|
|
683
|
+
<input type="checkbox" indeterminate={ctx.indeterminate} bind:checked={ctx.isSelected} />
|
|
684
|
+
{/snippet}
|
|
685
|
+
|
|
686
|
+
{#snippet rowSelected(ctx: RowSelectCtx<T>)}
|
|
687
|
+
<input type="checkbox" bind:checked={ctx.isSelected} tabindex="-1" />
|
|
688
|
+
{/snippet}
|
|
689
|
+
|
|
690
|
+
{#if table.options.select || table.options.reorderable || table.expandable}
|
|
691
|
+
{@const { select, reorderable } = table.options}
|
|
692
|
+
{@const expandable = table.expandable}
|
|
693
|
+
{@const {
|
|
694
|
+
show = 'hover',
|
|
695
|
+
style = 'column',
|
|
696
|
+
rowSnippet = rowSelected,
|
|
697
|
+
headerSnippet = headerSelected
|
|
698
|
+
} = typeof select === 'boolean' ? {} : select}
|
|
699
|
+
{#if show !== 'never' || reorderable || expandable?.options.chevron !== 'never'}
|
|
700
|
+
<Column
|
|
701
|
+
id="__fixed"
|
|
702
|
+
{table}
|
|
703
|
+
fixed
|
|
704
|
+
width={Math.max(
|
|
705
|
+
48,
|
|
706
|
+
0 +
|
|
707
|
+
(select && show !== 'never' ? 34 : 0) +
|
|
708
|
+
(reorderable ? 34 : 0) +
|
|
709
|
+
(expandable && expandable?.options.chevron !== 'never' ? 34 : 0)
|
|
710
|
+
)}
|
|
711
|
+
resizeable={false}
|
|
712
|
+
>
|
|
713
|
+
{#snippet header()}
|
|
714
|
+
<div class="__fixed">
|
|
715
|
+
{#if reorderable}
|
|
716
|
+
<span style="width: 16px; display: flex; align-items: center;"></span>
|
|
717
|
+
{/if}
|
|
718
|
+
{#if select}
|
|
719
|
+
{@render headerSnippet({
|
|
720
|
+
get isSelected() {
|
|
721
|
+
return table.data.length === table.selected?.length && table.data.length > 0
|
|
722
|
+
},
|
|
723
|
+
set isSelected(value) {
|
|
724
|
+
if (value) {
|
|
725
|
+
table.selected = table.data
|
|
726
|
+
} else {
|
|
727
|
+
table.selected = []
|
|
728
|
+
}
|
|
729
|
+
},
|
|
730
|
+
get selected() {
|
|
731
|
+
return table.selected!
|
|
732
|
+
},
|
|
733
|
+
get indeterminate() {
|
|
734
|
+
return (
|
|
735
|
+
(table.selected?.length || 0) > 0 &&
|
|
736
|
+
table.data.length !== table.selected?.length
|
|
737
|
+
)
|
|
738
|
+
}
|
|
739
|
+
})}
|
|
740
|
+
{/if}
|
|
741
|
+
</div>
|
|
742
|
+
{/snippet}
|
|
743
|
+
{#snippet row(item, row)}
|
|
744
|
+
<div class="__fixed">
|
|
745
|
+
{#if reorderable && row.itemState}
|
|
746
|
+
<span style="width: 16px; display: flex; align-items: center;" use:row.itemState.handle>
|
|
747
|
+
{#if (row.rowHovered && !row.itemState.area.isTarget) || row.itemState.dragging}
|
|
748
|
+
{@render dragSnippet()}
|
|
749
|
+
{/if}
|
|
750
|
+
</span>
|
|
751
|
+
{/if}
|
|
752
|
+
{#if select && (row.selected || show === 'always' || (row.rowHovered && show === 'hover') || row.expanded)}
|
|
753
|
+
{@render rowSnippet({
|
|
754
|
+
get isSelected() {
|
|
755
|
+
return row.selected
|
|
756
|
+
},
|
|
757
|
+
set isSelected(value) {
|
|
758
|
+
row.selected = value
|
|
759
|
+
},
|
|
760
|
+
get row() {
|
|
761
|
+
return row
|
|
762
|
+
},
|
|
763
|
+
get item() {
|
|
764
|
+
return item
|
|
765
|
+
},
|
|
766
|
+
get data() {
|
|
767
|
+
return table.data
|
|
768
|
+
}
|
|
769
|
+
})}
|
|
770
|
+
{/if}
|
|
771
|
+
{#if expandable && expandable?.options.chevron !== 'never'}
|
|
772
|
+
<button class="expand-row" tabindex="-1" onclick={() => (row.expanded = !row.expanded)}>
|
|
773
|
+
{#if row.expanded || expandable.options.chevron === 'always' || (row.rowHovered && expandable.options.chevron === 'hover')}
|
|
774
|
+
{@render chevronSnippet(row.expanded ? 180 : 90)}
|
|
775
|
+
{/if}
|
|
776
|
+
</button>
|
|
777
|
+
{/if}
|
|
778
|
+
</div>
|
|
779
|
+
{/snippet}
|
|
780
|
+
</Column>
|
|
781
|
+
{/if}
|
|
782
|
+
{/if}
|
|
783
|
+
|
|
784
|
+
{#if table.options.auto}
|
|
785
|
+
{#each Object.keys(table.data[0] || {}) as key}
|
|
786
|
+
<Column
|
|
787
|
+
id={key}
|
|
788
|
+
value={(r) => r[key]}
|
|
789
|
+
header={capitalize(segmentize(key))}
|
|
790
|
+
sort={typeof table.data[0]?.[key] === 'number' ?
|
|
791
|
+
(a, b) => a - b
|
|
792
|
+
: (a, b) => String(a).localeCompare(String(b))}
|
|
793
|
+
/>
|
|
794
|
+
{/each}
|
|
795
|
+
{/if}
|
|
796
|
+
|
|
797
|
+
{@render content?.({
|
|
798
|
+
Column,
|
|
799
|
+
Panel,
|
|
800
|
+
Expandable,
|
|
801
|
+
Row,
|
|
802
|
+
get table() {
|
|
803
|
+
return table
|
|
804
|
+
}
|
|
805
|
+
})}
|
|
806
|
+
|
|
807
|
+
<!---------------------------------------------------->
|
|
808
|
+
<style>
|
|
809
|
+
.svelte-tably *,
|
|
810
|
+
.svelte-tably {
|
|
811
|
+
box-sizing: border-box;
|
|
812
|
+
background-color: inherit;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
.context-col {
|
|
816
|
+
display: flex;
|
|
817
|
+
align-items: center;
|
|
818
|
+
justify-content: center;
|
|
819
|
+
position: sticky;
|
|
820
|
+
right: 0;
|
|
821
|
+
height: 100%;
|
|
822
|
+
z-index: 3;
|
|
823
|
+
padding: 0;
|
|
824
|
+
|
|
825
|
+
&.hover {
|
|
826
|
+
position: absolute;
|
|
827
|
+
}
|
|
828
|
+
&.hidden {
|
|
829
|
+
pointer-events: none;
|
|
830
|
+
user-select: none;
|
|
831
|
+
border-left: none;
|
|
832
|
+
background: none;
|
|
833
|
+
> :global(*) {
|
|
834
|
+
opacity: 0;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
:global(:root) {
|
|
840
|
+
--tably-color: hsl(0, 0%, 0%);
|
|
841
|
+
--tably-bg: hsl(0, 0%, 100%);
|
|
842
|
+
--tably-statusbar: hsl(0, 0%, 98%);
|
|
843
|
+
|
|
844
|
+
--tably-border: hsl(0, 0%, 90%);
|
|
845
|
+
--tably-border-grid: hsl(0, 0%, 98%);
|
|
846
|
+
|
|
847
|
+
--tably-padding-x: 1rem;
|
|
848
|
+
--tably-padding-y: 0.5rem;
|
|
849
|
+
|
|
850
|
+
--tably-radius: 0.25rem;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
.svelte-tably {
|
|
854
|
+
position: relative;
|
|
855
|
+
overflow: visible;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
.expandable {
|
|
859
|
+
position: relative;
|
|
860
|
+
|
|
861
|
+
& > td {
|
|
862
|
+
position: sticky;
|
|
863
|
+
left: 1px;
|
|
864
|
+
> div {
|
|
865
|
+
position: absolute;
|
|
866
|
+
overflow: auto;
|
|
867
|
+
top: -1.5px;
|
|
868
|
+
left: 0;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
.expand-row {
|
|
874
|
+
display: flex;
|
|
875
|
+
justify-content: center;
|
|
876
|
+
align-items: center;
|
|
877
|
+
padding: 0;
|
|
878
|
+
outline: none;
|
|
879
|
+
border: none;
|
|
880
|
+
cursor: pointer;
|
|
881
|
+
background-color: transparent;
|
|
882
|
+
color: inherit;
|
|
883
|
+
width: 20px;
|
|
884
|
+
height: 100%;
|
|
885
|
+
|
|
886
|
+
> svg {
|
|
887
|
+
transition: transform 0.15s ease;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
caption {
|
|
892
|
+
all: unset;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
input[type='checkbox'] {
|
|
896
|
+
width: 18px;
|
|
897
|
+
height: 18px;
|
|
898
|
+
cursor: pointer;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
button.btn-backdrop {
|
|
902
|
+
outline: none;
|
|
903
|
+
border: none;
|
|
904
|
+
cursor: pointer;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
.sorting-icon {
|
|
908
|
+
align-items: center;
|
|
909
|
+
justify-items: end;
|
|
910
|
+
margin: 0;
|
|
911
|
+
margin-left: auto;
|
|
912
|
+
> svg {
|
|
913
|
+
transition: transform 0.15s ease;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
th:not(:last-child) .sorting-icon {
|
|
918
|
+
margin-right: var(--tably-padding-x);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
.__fixed {
|
|
922
|
+
display: flex;
|
|
923
|
+
align-items: center;
|
|
924
|
+
justify-content: center;
|
|
925
|
+
gap: 0.25rem;
|
|
926
|
+
position: absolute;
|
|
927
|
+
top: 0;
|
|
928
|
+
left: 0;
|
|
929
|
+
right: 0;
|
|
930
|
+
bottom: 0;
|
|
931
|
+
width: 100%;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
tbody::before,
|
|
935
|
+
tbody::after,
|
|
936
|
+
selects::before,
|
|
937
|
+
selects::after {
|
|
938
|
+
content: '';
|
|
939
|
+
display: grid;
|
|
940
|
+
min-height: 100%;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
tbody::before,
|
|
944
|
+
selects::before {
|
|
945
|
+
height: var(--t);
|
|
946
|
+
}
|
|
947
|
+
tbody::after,
|
|
948
|
+
selects::after {
|
|
949
|
+
height: var(--b);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
.row:global(:is(a)) {
|
|
953
|
+
color: inherit;
|
|
954
|
+
text-decoration: inherit;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
.backdrop {
|
|
958
|
+
position: absolute;
|
|
959
|
+
left: 0px;
|
|
960
|
+
top: 0px;
|
|
961
|
+
bottom: 0px;
|
|
962
|
+
right: 0px;
|
|
963
|
+
background-color: hsla(0, 0%, 0%, 0.3);
|
|
964
|
+
z-index: 3;
|
|
965
|
+
opacity: 0;
|
|
966
|
+
pointer-events: none;
|
|
967
|
+
transition: 0.15s ease;
|
|
968
|
+
border: none;
|
|
969
|
+
outline: none;
|
|
970
|
+
cursor: pointer;
|
|
971
|
+
|
|
972
|
+
> button {
|
|
973
|
+
position: absolute;
|
|
974
|
+
left: 0px;
|
|
975
|
+
top: 0px;
|
|
976
|
+
bottom: 0px;
|
|
977
|
+
right: 0px;
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
&[aria-hidden='false'], &:not([aria-hidden]) {
|
|
981
|
+
opacity: 1;
|
|
982
|
+
pointer-events: all;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
@starting-style {
|
|
986
|
+
opacity: 0;
|
|
987
|
+
pointer-events: none;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
.sticky {
|
|
992
|
+
position: sticky;
|
|
993
|
+
/* right: 100px; */
|
|
994
|
+
z-index: 1;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
.sticky.border {
|
|
998
|
+
border-right: 1px solid var(--tably-border);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
.headers > tr > .column {
|
|
1002
|
+
overflow: hidden;
|
|
1003
|
+
padding: var(--tably-padding-y) 0;
|
|
1004
|
+
cursor: default;
|
|
1005
|
+
user-select: none;
|
|
1006
|
+
|
|
1007
|
+
&:last-child {
|
|
1008
|
+
border-right: none;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
&.sortable {
|
|
1012
|
+
cursor: pointer;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
&.resizeable {
|
|
1016
|
+
resize: horizontal;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
.table {
|
|
1021
|
+
display: grid;
|
|
1022
|
+
height: auto;
|
|
1023
|
+
max-height: 100%;
|
|
1024
|
+
position: relative;
|
|
1025
|
+
|
|
1026
|
+
color: var(--tably-color);
|
|
1027
|
+
background-color: var(--tably-bg);
|
|
1028
|
+
|
|
1029
|
+
grid-template-areas:
|
|
1030
|
+
'headers panel'
|
|
1031
|
+
'rows panel'
|
|
1032
|
+
'statusbar panel';
|
|
1033
|
+
|
|
1034
|
+
grid-template-columns: auto min-content;
|
|
1035
|
+
grid-template-rows: auto 1fr auto;
|
|
1036
|
+
|
|
1037
|
+
border: 1px solid var(--tably-border);
|
|
1038
|
+
border-radius: var(--tably-radius);
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
.headers {
|
|
1042
|
+
display: flex;
|
|
1043
|
+
grid-area: headers;
|
|
1044
|
+
z-index: 2;
|
|
1045
|
+
overflow: hidden;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
.headers > tr > .column {
|
|
1049
|
+
width: auto !important;
|
|
1050
|
+
border-bottom: 1px solid var(--tably-border);
|
|
1051
|
+
}
|
|
1052
|
+
.headers > tr > .column,
|
|
1053
|
+
.headers > tr > .context-col {
|
|
1054
|
+
border-bottom: 1px solid var(--tably-border);
|
|
1055
|
+
border-left: 1px solid var(--tably-border-grid);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
.content {
|
|
1059
|
+
display: grid;
|
|
1060
|
+
grid-auto-rows: max-content;
|
|
1061
|
+
|
|
1062
|
+
grid-area: rows;
|
|
1063
|
+
scrollbar-width: thin;
|
|
1064
|
+
overflow: auto;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
.statusbar {
|
|
1068
|
+
display: flex;
|
|
1069
|
+
grid-area: statusbar;
|
|
1070
|
+
overflow: hidden;
|
|
1071
|
+
background-color: var(--tably-statusbar);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
.statusbar > tr > .column {
|
|
1075
|
+
border-top: 1px solid var(--tably-border);
|
|
1076
|
+
padding: calc(var(--tably-padding-y) / 2) 0;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
.headers > tr,
|
|
1080
|
+
.row,
|
|
1081
|
+
.statusbar > tr {
|
|
1082
|
+
position: relative;
|
|
1083
|
+
display: grid;
|
|
1084
|
+
width: 100%;
|
|
1085
|
+
height: 100%;
|
|
1086
|
+
|
|
1087
|
+
& > .column {
|
|
1088
|
+
display: flex;
|
|
1089
|
+
overflow: hidden;
|
|
1090
|
+
|
|
1091
|
+
&:not(.pad),
|
|
1092
|
+
&.pad > :global(*:first-child) {
|
|
1093
|
+
padding-left: var(--tably-padding-x);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
& > *:last-child:not(.context-col) {
|
|
1098
|
+
width: 100%;
|
|
1099
|
+
|
|
1100
|
+
&:not(.pad),
|
|
1101
|
+
&.pad > :global(*:first-child) {
|
|
1102
|
+
padding-right: var(--tably-padding-x);
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
.row > .column {
|
|
1108
|
+
background-color: var(--tably-bg);
|
|
1109
|
+
&:not(.pad),
|
|
1110
|
+
&.pad > :global(*:first-child) {
|
|
1111
|
+
padding-top: var(--tably-padding-y);
|
|
1112
|
+
padding-bottom: var(--tably-padding-y);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
:global(#runic-drag .row) {
|
|
1117
|
+
border: 1px solid var(--tably-border-grid);
|
|
1118
|
+
border-top: 2px solid var(--tably-border-grid);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
.row > * {
|
|
1122
|
+
border-left: 1px solid var(--tably-border-grid);
|
|
1123
|
+
border-bottom: 1px solid var(--tably-border-grid);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
.panel {
|
|
1127
|
+
position: relative;
|
|
1128
|
+
grid-area: panel;
|
|
1129
|
+
height: 100%;
|
|
1130
|
+
overflow: hidden;
|
|
1131
|
+
border-left: 1px solid var(--tably-border);
|
|
1132
|
+
|
|
1133
|
+
z-index: 4;
|
|
1134
|
+
|
|
1135
|
+
> .panel-content {
|
|
1136
|
+
position: absolute;
|
|
1137
|
+
top: 0;
|
|
1138
|
+
right: 0;
|
|
1139
|
+
bottom: 0;
|
|
1140
|
+
width: min-content;
|
|
1141
|
+
overflow: auto;
|
|
1142
|
+
scrollbar-width: thin;
|
|
1143
|
+
padding: var(--tably-padding-y) 0;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
</style>
|