svelte-tably 1.0.0-next.12 → 1.0.0-next.14
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 +3 -3
- package/dist/column/column.svelte.js +3 -0
- package/dist/expandable/Expandable.svelte +24 -0
- package/dist/expandable/Expandable.svelte.d.ts +25 -0
- package/dist/expandable/expandable.svelte.js +27 -0
- package/dist/size-tween.svelte.d.ts +16 -0
- package/dist/size-tween.svelte.js +33 -0
- package/dist/table/Table.svelte +338 -102
- package/dist/table/Table.svelte.d.ts +8 -1
- package/dist/table/table.svelte.js +1 -0
- package/dist/utility.svelte.d.ts +1 -0
- package/dist/utility.svelte.js +12 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -19,8 +19,8 @@ A high performant, feature rich, dynamic table
|
|
|
19
19
|
- [x] filtering
|
|
20
20
|
- [x] reorderable table
|
|
21
21
|
- [ ] row context-menu
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
22
|
+
- [x] Expandable rows
|
|
23
|
+
- [x] to CSV
|
|
24
24
|
|
|
25
25
|
### Usage Notes
|
|
26
26
|
|
|
@@ -34,7 +34,7 @@ A high performant, feature rich, dynamic table
|
|
|
34
34
|
])
|
|
35
35
|
|
|
36
36
|
let activePanel = $state('columns') as string | undefined
|
|
37
|
-
|
|
37
|
+
let selected = $state([]) as typeof data
|
|
38
38
|
</script>
|
|
39
39
|
|
|
40
40
|
<Table {data} panel={activePanel} select bind:selected>
|
|
@@ -40,6 +40,9 @@ export class ColumnState {
|
|
|
40
40
|
filter: this.#props.filter,
|
|
41
41
|
value: this.#props.value,
|
|
42
42
|
resizeable: this.#props.resizeable ?? true,
|
|
43
|
+
style: this.#props.style,
|
|
44
|
+
class: this.#props.class,
|
|
45
|
+
onclick: this.#props.onclick
|
|
43
46
|
});
|
|
44
47
|
toggleVisiblity() {
|
|
45
48
|
const index = this.table.positions.hidden.indexOf(this);
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
|
|
13
|
+
import { ExpandableState, type ExpandableProps } from './expandable.svelte.js'
|
|
14
|
+
import type { AnyRecord } from '../utility.svelte.js'
|
|
15
|
+
import { fromProps } from '../utility.svelte.js'
|
|
16
|
+
|
|
17
|
+
type T = $$Generic<AnyRecord>
|
|
18
|
+
|
|
19
|
+
let { ...restProps }: ExpandableProps<T> = $props()
|
|
20
|
+
|
|
21
|
+
const properties = fromProps(restProps)
|
|
22
|
+
new ExpandableState<T>(properties)
|
|
23
|
+
|
|
24
|
+
</script>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AnyRecord } from '../utility.svelte.js';
|
|
2
|
+
declare class __sveltets_Render<T extends AnyRecord> {
|
|
3
|
+
props(): ExpandableProps<T_1>;
|
|
4
|
+
events(): {};
|
|
5
|
+
slots(): {};
|
|
6
|
+
bindings(): "";
|
|
7
|
+
exports(): {};
|
|
8
|
+
}
|
|
9
|
+
interface $$IsomorphicComponent {
|
|
10
|
+
new <T extends AnyRecord>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
11
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
12
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
13
|
+
<T extends AnyRecord>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
14
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* This is a description, \
|
|
18
|
+
* on how to use this.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* <Component />
|
|
22
|
+
*/
|
|
23
|
+
declare const Expandable: $$IsomorphicComponent;
|
|
24
|
+
type Expandable<T extends AnyRecord> = InstanceType<typeof Expandable<T>>;
|
|
25
|
+
export default Expandable;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { TableState } from '../table/table.svelte.js';
|
|
2
|
+
import { sineInOut } from 'svelte/easing';
|
|
3
|
+
export class ExpandableState {
|
|
4
|
+
#table;
|
|
5
|
+
#props = {};
|
|
6
|
+
snippets = $derived({
|
|
7
|
+
content: this.#props.content
|
|
8
|
+
});
|
|
9
|
+
options = $derived({
|
|
10
|
+
slide: {
|
|
11
|
+
duration: this.#props.slide?.duration ?? 150,
|
|
12
|
+
easing: this.#props.slide?.easing ?? sineInOut
|
|
13
|
+
},
|
|
14
|
+
click: this.#props.click ?? true,
|
|
15
|
+
chevron: this.#props.chevron ?? 'hover',
|
|
16
|
+
multiple: this.#props.multiple ?? false
|
|
17
|
+
});
|
|
18
|
+
constructor(props) {
|
|
19
|
+
this.#props = props;
|
|
20
|
+
this.#table = TableState.getContext();
|
|
21
|
+
if (!this.#table) {
|
|
22
|
+
throw new Error('svelte-tably: Expandable must be associated with a Table');
|
|
23
|
+
}
|
|
24
|
+
this.#table.expandable = this;
|
|
25
|
+
$effect(() => () => this.#table.expandable === this && (this.#table.expandable = undefined));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { EasingFunction } from 'svelte/transition';
|
|
2
|
+
interface SizeOptions {
|
|
3
|
+
min?: number;
|
|
4
|
+
duration?: number;
|
|
5
|
+
easing?: EasingFunction;
|
|
6
|
+
}
|
|
7
|
+
export declare class SizeTween {
|
|
8
|
+
#private;
|
|
9
|
+
current: number;
|
|
10
|
+
transitioning: boolean;
|
|
11
|
+
/** bind:offsetWidth bind:offsetHeight */
|
|
12
|
+
size: number;
|
|
13
|
+
set target(value: number);
|
|
14
|
+
constructor(cb: () => boolean | undefined, opts?: SizeOptions);
|
|
15
|
+
}
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { tick, untrack } from 'svelte';
|
|
2
|
+
import { sineInOut } from 'svelte/easing';
|
|
3
|
+
import { Tween } from 'svelte/motion';
|
|
4
|
+
export class SizeTween {
|
|
5
|
+
#tweenOptions = { duration: 300, easing: sineInOut };
|
|
6
|
+
#tween = new Tween(0, this.#tweenOptions);
|
|
7
|
+
current = $derived(this.#tween.current);
|
|
8
|
+
transitioning = $state(false);
|
|
9
|
+
/** bind:offsetWidth bind:offsetHeight */
|
|
10
|
+
size = $state(0);
|
|
11
|
+
set target(value) {
|
|
12
|
+
this.transitioning = true;
|
|
13
|
+
this.#tween.set(value, this.#tweenOptions).then(() => this.transitioning = false);
|
|
14
|
+
}
|
|
15
|
+
constructor(cb, opts = {}) {
|
|
16
|
+
if ('duration' in opts) {
|
|
17
|
+
this.#tweenOptions.duration = opts.duration;
|
|
18
|
+
}
|
|
19
|
+
if ('easing' in opts) {
|
|
20
|
+
this.#tweenOptions.easing = opts.easing;
|
|
21
|
+
}
|
|
22
|
+
untrack(() => {
|
|
23
|
+
if (cb()) {
|
|
24
|
+
requestAnimationFrame(() => {
|
|
25
|
+
this.#tween.set(Math.max(this.size, opts.min ?? 0), { duration: 0 });
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
$effect.pre(() => {
|
|
30
|
+
this.target = cb() ? Math.max(this.size, opts.min ?? 0) : 0;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
package/dist/table/Table.svelte
CHANGED
|
@@ -18,12 +18,15 @@
|
|
|
18
18
|
import { sineInOut } from 'svelte/easing'
|
|
19
19
|
import reorder, { type ItemState } from 'runic-reorder'
|
|
20
20
|
import { Virtualization } from './virtualization.svelte.js'
|
|
21
|
-
import { TableState, type HeaderSelectCtx, type RowSelectCtx, type TableProps } from './table.svelte.js'
|
|
21
|
+
import { TableState, type HeaderSelectCtx, type RowCtx, type RowSelectCtx, type TableProps } from './table.svelte.js'
|
|
22
22
|
import Panel, { PanelTween } from '../panel/Panel.svelte'
|
|
23
23
|
import Column from '../column/Column.svelte'
|
|
24
|
-
import { fromProps, mounted } from '../utility.svelte.js'
|
|
24
|
+
import { assignDescriptors, fromProps, mounted } from '../utility.svelte.js'
|
|
25
25
|
import { conditional } from '../conditional.svelte.js'
|
|
26
|
-
import { ColumnState } from '../column/column.svelte.js'
|
|
26
|
+
import { ColumnState, type RowColumnCtx } from '../column/column.svelte.js'
|
|
27
|
+
import Expandable from '../expandable/Expandable.svelte'
|
|
28
|
+
import { SizeTween } from '../size-tween.svelte.js'
|
|
29
|
+
import { on } from 'svelte/events'
|
|
27
30
|
|
|
28
31
|
type T = $$Generic<Record<PropertyKey, unknown>>
|
|
29
32
|
|
|
@@ -35,7 +38,8 @@
|
|
|
35
38
|
new <V>(...args: ConstructorParams<typeof Column<T, V>>): ConstructorReturnType<typeof Column<T, V>>
|
|
36
39
|
<V>(...args: Parameters<typeof Column<T, V>>): ReturnType<typeof Column<T, V>>
|
|
37
40
|
}
|
|
38
|
-
Panel: typeof Panel
|
|
41
|
+
Panel: typeof Panel<T>
|
|
42
|
+
Expandable: typeof Expandable<T>
|
|
39
43
|
readonly table: TableState<T>
|
|
40
44
|
readonly data: T[]
|
|
41
45
|
}
|
|
@@ -70,7 +74,7 @@
|
|
|
70
74
|
|
|
71
75
|
const virtualization = new Virtualization(table)
|
|
72
76
|
|
|
73
|
-
const panelTween = new
|
|
77
|
+
const panelTween = new SizeTween(() => !!properties.panel)
|
|
74
78
|
|
|
75
79
|
let hoveredRow: T | null = $state(null)
|
|
76
80
|
|
|
@@ -111,6 +115,7 @@
|
|
|
111
115
|
.map((column, i, arr) => {
|
|
112
116
|
sum += getWidth(arr[i - 1]?.id, i === 0 ? 0 : undefined)
|
|
113
117
|
return `
|
|
118
|
+
#${table.id} .column.sticky[data-column='${column.id}'],
|
|
114
119
|
[data-svelte-tably='${table.id}'] .column.sticky[data-column='${column.id}'] {
|
|
115
120
|
left: ${sum}px;
|
|
116
121
|
}
|
|
@@ -118,7 +123,13 @@
|
|
|
118
123
|
})
|
|
119
124
|
.join('')
|
|
120
125
|
|
|
121
|
-
|
|
126
|
+
const columnStyling = columns.map(column => !column.options.style ? '' : `
|
|
127
|
+
#${table.id} .column[data-column='${column.id}'] {
|
|
128
|
+
${column.options.style}
|
|
129
|
+
}
|
|
130
|
+
`).join('')
|
|
131
|
+
|
|
132
|
+
return templateColumns + stickyLeft + columnStyling
|
|
122
133
|
})
|
|
123
134
|
|
|
124
135
|
function observeColumnWidth(node: HTMLDivElement, isHeader = false) {
|
|
@@ -147,6 +158,9 @@
|
|
|
147
158
|
return { destroy: () => observer.disconnect() }
|
|
148
159
|
}
|
|
149
160
|
|
|
161
|
+
let tbody = $state({
|
|
162
|
+
width: 0
|
|
163
|
+
})
|
|
150
164
|
async function onscroll() {
|
|
151
165
|
const target = virtualization.viewport.element!
|
|
152
166
|
if (target.scrollTop !== virtualization.scrollTop) {
|
|
@@ -161,27 +175,126 @@
|
|
|
161
175
|
elements.headers.scrollLeft = target.scrollLeft
|
|
162
176
|
elements.statusbar.scrollLeft = target.scrollLeft
|
|
163
177
|
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
// * --- CSV --- *
|
|
181
|
+
let csv = $state(false) as false | { selected?: boolean }
|
|
182
|
+
let csvElement = $state() as undefined | HTMLTableElement
|
|
183
|
+
interface CSVOptions {
|
|
184
|
+
/** Semi-colons as separator? */
|
|
185
|
+
semicolon?: boolean
|
|
186
|
+
/** Only selected rows */
|
|
187
|
+
selected?: boolean
|
|
188
|
+
}
|
|
189
|
+
export async function toCSV(opts: CSVOptions = {}) {
|
|
190
|
+
csv = { selected: !!opts.selected }
|
|
191
|
+
let resolve: (value: HTMLTableElement) => void
|
|
192
|
+
const promise = new Promise<HTMLTableElement>(r => resolve = r)
|
|
193
|
+
|
|
194
|
+
const clean = $effect.root(() => {
|
|
195
|
+
$effect(() => {
|
|
196
|
+
if(csvElement) {
|
|
197
|
+
resolve(csvElement)
|
|
198
|
+
}
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
let table = await promise
|
|
203
|
+
clean()
|
|
204
|
+
|
|
205
|
+
const separator = opts.semicolon ? ";" : ","
|
|
206
|
+
const rows = Array.from(table.rows)
|
|
207
|
+
const csvRows = []
|
|
208
|
+
|
|
209
|
+
for (const row of rows) {
|
|
210
|
+
const cells = Array.from(row.cells)
|
|
211
|
+
const csvCells = cells.map(cell => {
|
|
212
|
+
let text = cell.textContent?.trim() || ''
|
|
213
|
+
|
|
214
|
+
// Escape double quotes and wrap in quotes if needed
|
|
215
|
+
if(text.includes('"')) {
|
|
216
|
+
text = text.replace(/"/g, '""')
|
|
217
|
+
}
|
|
218
|
+
if(text.includes(separator) || text.includes('"') || text.includes('\n')) {
|
|
219
|
+
text = `"${text}"`
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return text
|
|
223
|
+
})
|
|
224
|
+
csvRows.push(csvCells.join(separator))
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
csv = false
|
|
228
|
+
return csvRows.join("\n")
|
|
229
|
+
}
|
|
230
|
+
// * --- CSV --- *
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
let expandedRow = $state([]) as T[]
|
|
234
|
+
|
|
235
|
+
function addRowColumnEvents(
|
|
236
|
+
node: HTMLTableColElement,
|
|
237
|
+
opts: ['header' | 'row' | 'statusbar', ColumnState, () => RowColumnCtx<T, any>]
|
|
238
|
+
) {
|
|
239
|
+
const [where, column, value] = opts
|
|
240
|
+
if(where !== 'row') return
|
|
241
|
+
if(column.options.onclick) {
|
|
242
|
+
$effect(() => on(node, 'click', e => column.options.onclick!(e, value())))
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
164
246
|
</script>
|
|
165
247
|
|
|
166
248
|
<!---------------------------------------------------->
|
|
167
249
|
|
|
250
|
+
{#if csv !== false}
|
|
251
|
+
{@const renderedColumns = columns.filter(v => v.id !== '__fixed')}
|
|
252
|
+
<table bind:this={csvElement} hidden>
|
|
253
|
+
<thead>
|
|
254
|
+
<tr>
|
|
255
|
+
{#each renderedColumns as column}
|
|
256
|
+
<th>{@render column.snippets.title()}</th>
|
|
257
|
+
{/each}
|
|
258
|
+
</tr>
|
|
259
|
+
</thead>
|
|
260
|
+
<tbody>
|
|
261
|
+
{#each data.current as row, i}
|
|
262
|
+
{#if (csv.selected && table.selected.includes(row)) || !csv.selected}
|
|
263
|
+
<tr>
|
|
264
|
+
{#each renderedColumns as column}
|
|
265
|
+
<td>
|
|
266
|
+
{@render column.snippets.row?.(row, {
|
|
267
|
+
index: i,
|
|
268
|
+
value: column.options.value?.(row),
|
|
269
|
+
isHovered: false,
|
|
270
|
+
itemState: { index: i, dragging: false, positioning: false } as ItemState<any>,
|
|
271
|
+
selected: false,
|
|
272
|
+
expanded: false
|
|
273
|
+
})}
|
|
274
|
+
</td>
|
|
275
|
+
{/each}
|
|
276
|
+
</tr>
|
|
277
|
+
{/if}
|
|
278
|
+
{/each}
|
|
279
|
+
</tbody>
|
|
280
|
+
</table>
|
|
281
|
+
{/if}
|
|
282
|
+
|
|
168
283
|
<svelte:head>
|
|
169
284
|
{@html `<style>${style}</style>`}
|
|
170
285
|
</svelte:head>
|
|
171
286
|
|
|
172
|
-
{#snippet chevronSnippet(
|
|
287
|
+
{#snippet chevronSnippet(rotation: number = 0)}
|
|
173
288
|
<svg
|
|
174
|
-
class="sorting-icon"
|
|
175
|
-
class:reversed
|
|
176
289
|
xmlns="http://www.w3.org/2000/svg"
|
|
177
290
|
width="16"
|
|
178
291
|
height="16"
|
|
179
292
|
viewBox="0 0 16 16"
|
|
180
|
-
style="
|
|
293
|
+
style="transform: rotate({rotation}deg)"
|
|
181
294
|
>
|
|
182
295
|
<path
|
|
183
296
|
fill="currentColor"
|
|
184
|
-
d="M3.2
|
|
297
|
+
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"
|
|
185
298
|
></path>
|
|
186
299
|
</svg>
|
|
187
300
|
{/snippet}
|
|
@@ -198,15 +311,20 @@
|
|
|
198
311
|
{#snippet columnsSnippet(
|
|
199
312
|
renderable: (column: ColumnState) => Snippet<[arg0?: any, arg1?: any]> | undefined,
|
|
200
313
|
arg: null | ((column: ColumnState) => any[]) = null,
|
|
201
|
-
|
|
314
|
+
where: 'header' | 'row' | 'statusbar'
|
|
202
315
|
)}
|
|
316
|
+
{@const isHeader = where === 'header'}
|
|
203
317
|
{#each fixed as column, i (column)}
|
|
204
318
|
{#if !hidden.includes(column)}
|
|
205
319
|
{@const args = arg ? arg(column) : []}
|
|
206
320
|
{@const sortable = isHeader && column.options.sort && !table.options.reorderable}
|
|
207
321
|
<svelte:element
|
|
208
322
|
this={isHeader ? 'th' : 'td'}
|
|
209
|
-
class=
|
|
323
|
+
class={column.options.class ?? ''}
|
|
324
|
+
class:column={true}
|
|
325
|
+
class:sticky={true}
|
|
326
|
+
class:fixed={true}
|
|
327
|
+
use:addRowColumnEvents={[where, column, () => args[1]]}
|
|
210
328
|
data-column={column.id}
|
|
211
329
|
class:header={isHeader}
|
|
212
330
|
class:sortable
|
|
@@ -214,7 +332,9 @@
|
|
|
214
332
|
>
|
|
215
333
|
{@render renderable(column)?.(args[0], args[1])}
|
|
216
334
|
{#if isHeader && data.sortby === column.id && sortable}
|
|
217
|
-
|
|
335
|
+
<span class='sorting-icon'>
|
|
336
|
+
{@render chevronSnippet(data.sortReverse ? 0 : 180)}
|
|
337
|
+
</span>
|
|
218
338
|
{/if}
|
|
219
339
|
</svelte:element>
|
|
220
340
|
{/if}
|
|
@@ -225,7 +345,10 @@
|
|
|
225
345
|
{@const sortable = isHeader && column.options.sort && !table.options.reorderable}
|
|
226
346
|
<svelte:element
|
|
227
347
|
this={isHeader ? 'th' : 'td'}
|
|
228
|
-
class=
|
|
348
|
+
class={column.options.class ?? ''}
|
|
349
|
+
class:column={true}
|
|
350
|
+
class:sticky={true}
|
|
351
|
+
use:addRowColumnEvents={[where, column, () => args[1]]}
|
|
229
352
|
use:observeColumnWidth={isHeader}
|
|
230
353
|
data-column={column.id}
|
|
231
354
|
class:header={isHeader}
|
|
@@ -236,7 +359,9 @@
|
|
|
236
359
|
>
|
|
237
360
|
{@render renderable(column)?.(args[0], args[1])}
|
|
238
361
|
{#if isHeader && data.sortby === column.id && sortable}
|
|
239
|
-
|
|
362
|
+
<span class='sorting-icon'>
|
|
363
|
+
{@render chevronSnippet(data.sortReverse ? 0 : 180)}
|
|
364
|
+
</span>
|
|
240
365
|
{/if}
|
|
241
366
|
</svelte:element>
|
|
242
367
|
{/if}
|
|
@@ -247,8 +372,10 @@
|
|
|
247
372
|
{@const sortable = isHeader && column!.options.sort && !table.options.reorderable}
|
|
248
373
|
<svelte:element
|
|
249
374
|
this={isHeader ? 'th' : 'td'}
|
|
250
|
-
class=
|
|
375
|
+
class={column.options.class ?? ''}
|
|
376
|
+
class:column={true}
|
|
251
377
|
data-column={column.id}
|
|
378
|
+
use:addRowColumnEvents={[where, column, () => args[1]]}
|
|
252
379
|
use:observeColumnWidth={isHeader}
|
|
253
380
|
class:resizeable={isHeader && column.options.resizeable && table.options.resizeable}
|
|
254
381
|
class:sortable
|
|
@@ -256,7 +383,9 @@
|
|
|
256
383
|
>
|
|
257
384
|
{@render renderable(column)?.(args[0], args[1])}
|
|
258
385
|
{#if isHeader && data.sortby === column.id && sortable}
|
|
259
|
-
|
|
386
|
+
<span class='sorting-icon'>
|
|
387
|
+
{@render chevronSnippet(data.sortReverse ? 0 : 180)}
|
|
388
|
+
</span>
|
|
260
389
|
{/if}
|
|
261
390
|
</svelte:element>
|
|
262
391
|
{/if}
|
|
@@ -264,80 +393,137 @@
|
|
|
264
393
|
{/snippet}
|
|
265
394
|
|
|
266
395
|
{#snippet rowSnippet(item: T, itemState?: ItemState<T>)}
|
|
267
|
-
{@const
|
|
268
|
-
{@const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
396
|
+
{@const index = itemState?.index ?? 0}
|
|
397
|
+
{@const toggleExpand = (value?: boolean) => {
|
|
398
|
+
let indexOf = expandedRow.indexOf(item)
|
|
399
|
+
if(value !== undefined) {
|
|
400
|
+
value = indexOf === -1
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if(!value) {
|
|
404
|
+
expandedRow.splice(indexOf, 1)
|
|
405
|
+
return
|
|
406
|
+
}
|
|
407
|
+
if(table.expandable?.options.multiple === true) {
|
|
408
|
+
expandedRow.push(item)
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
expandedRow[0] = item
|
|
412
|
+
}
|
|
413
|
+
}}
|
|
414
|
+
|
|
415
|
+
{@const ctx: RowCtx<T> = {
|
|
416
|
+
get index() {
|
|
417
|
+
return index
|
|
418
|
+
},
|
|
419
|
+
get isHovered() {
|
|
420
|
+
return hoveredRow === item
|
|
421
|
+
},
|
|
422
|
+
get selected() {
|
|
423
|
+
return table.selected?.includes(item)
|
|
424
|
+
},
|
|
425
|
+
set selected(value) {
|
|
426
|
+
value ?
|
|
427
|
+
table.selected!.push(item)
|
|
428
|
+
: table.selected!.splice(table.selected!.indexOf(item), 1)
|
|
429
|
+
},
|
|
430
|
+
get itemState() {
|
|
431
|
+
return itemState
|
|
432
|
+
},
|
|
433
|
+
get expanded() {
|
|
434
|
+
return expandedRow.includes(item)
|
|
435
|
+
},
|
|
436
|
+
set expanded(value) {
|
|
437
|
+
toggleExpand(value)
|
|
438
|
+
}
|
|
439
|
+
}}
|
|
440
|
+
|
|
441
|
+
<tr
|
|
272
442
|
aria-rowindex={index + 1}
|
|
273
443
|
data-svelte-tably={table.id}
|
|
274
444
|
style:opacity={itemState?.positioning ? 0 : 1}
|
|
275
|
-
class=
|
|
445
|
+
class='row'
|
|
276
446
|
class:hover={hoveredRow === item}
|
|
277
447
|
class:dragging={itemState?.dragging}
|
|
278
448
|
class:selected={table.selected?.includes(item)}
|
|
279
|
-
class:first={
|
|
280
|
-
class:last={
|
|
281
|
-
{...
|
|
449
|
+
class:first={index === 0}
|
|
450
|
+
class:last={index === virtualization.area.length - 1}
|
|
451
|
+
{...(table.options.href ? { href: table.options.href(item) } : {})}
|
|
452
|
+
{...(itemState?.dragging ? { 'data-svelte-tably': table.id } : {})}
|
|
282
453
|
onpointerenter={() => (hoveredRow = item)}
|
|
283
454
|
onpointerleave={() => (hoveredRow = null)}
|
|
455
|
+
onclick={(e) => {
|
|
456
|
+
if (table.expandable?.options.click === true) {
|
|
457
|
+
let target = e.target as HTMLElement
|
|
458
|
+
if(['INPUT', 'TEXTAREA', 'BUTTON', 'A'].includes(target.tagName)) {
|
|
459
|
+
return
|
|
460
|
+
}
|
|
461
|
+
toggleExpand()
|
|
462
|
+
}
|
|
463
|
+
}}
|
|
284
464
|
>
|
|
285
465
|
{@render columnsSnippet(
|
|
286
466
|
(column) => column.snippets.row,
|
|
287
467
|
(column) => {
|
|
288
468
|
return [
|
|
289
469
|
item,
|
|
290
|
-
{
|
|
291
|
-
get index() {
|
|
292
|
-
return index
|
|
293
|
-
},
|
|
470
|
+
assignDescriptors({
|
|
294
471
|
get value() {
|
|
295
472
|
return column.options.value ? column.options.value(item) : undefined
|
|
296
|
-
},
|
|
297
|
-
get isHovered() {
|
|
298
|
-
return hoveredRow === item
|
|
299
|
-
},
|
|
300
|
-
get selected() {
|
|
301
|
-
return table.selected?.includes(item)
|
|
302
|
-
},
|
|
303
|
-
set selected(value) {
|
|
304
|
-
value ?
|
|
305
|
-
table.selected!.push(item)
|
|
306
|
-
: table.selected!.splice(table.selected!.indexOf(item), 1)
|
|
307
|
-
},
|
|
308
|
-
get itemState() {
|
|
309
|
-
return itemState
|
|
310
473
|
}
|
|
311
|
-
}
|
|
474
|
+
}, ctx)
|
|
312
475
|
]
|
|
313
|
-
}
|
|
476
|
+
},
|
|
477
|
+
'row'
|
|
314
478
|
)}
|
|
315
|
-
</
|
|
479
|
+
</tr>
|
|
480
|
+
|
|
481
|
+
{@const expandableTween = new SizeTween(
|
|
482
|
+
() => table.expandable && expandedRow.includes(item),
|
|
483
|
+
{ min: 1, duration: table.expandable?.options.slide.duration, easing: table.expandable?.options.slide.easing })}
|
|
484
|
+
{#if expandableTween.current > 0}
|
|
485
|
+
<tr class='expandable' style='height: {expandableTween.current}px'>
|
|
486
|
+
<td
|
|
487
|
+
colspan={columns.length}
|
|
488
|
+
style='height: {expandableTween.current}px'
|
|
489
|
+
>
|
|
490
|
+
<div
|
|
491
|
+
bind:offsetHeight={expandableTween.size}
|
|
492
|
+
style='width: {tbody.width - 3}px'
|
|
493
|
+
>
|
|
494
|
+
{@render table.expandable!.snippets.content?.(item, ctx)}
|
|
495
|
+
</div>
|
|
496
|
+
</td>
|
|
497
|
+
</tr>
|
|
498
|
+
{/if}
|
|
316
499
|
{/snippet}
|
|
317
500
|
|
|
318
501
|
<table
|
|
319
502
|
id={table.id}
|
|
320
|
-
class=
|
|
321
|
-
style=
|
|
503
|
+
class='table svelte-tably'
|
|
504
|
+
style='--t: {virtualization.virtualTop}px; --b: {virtualization.virtualBottom}px;'
|
|
322
505
|
aria-rowcount={data.current.length}
|
|
323
506
|
>
|
|
324
|
-
|
|
325
|
-
{
|
|
326
|
-
(
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
507
|
+
{#if columns.some(v => v.snippets.header)}
|
|
508
|
+
<thead class='headers' bind:this={elements.headers}>
|
|
509
|
+
{@render columnsSnippet(
|
|
510
|
+
(column) => column.snippets.header,
|
|
511
|
+
() => [{
|
|
512
|
+
get header() { return true },
|
|
513
|
+
get data() { return data.current }
|
|
514
|
+
}],
|
|
515
|
+
'header'
|
|
516
|
+
)}
|
|
517
|
+
</thead>
|
|
518
|
+
{/if}
|
|
334
519
|
|
|
335
520
|
<tbody
|
|
336
|
-
class=
|
|
521
|
+
class='content'
|
|
337
522
|
use:reorderArea={{ axis: 'y' }}
|
|
338
523
|
bind:this={virtualization.viewport.element}
|
|
339
524
|
onscrollcapture={onscroll}
|
|
340
525
|
bind:clientHeight={virtualization.viewport.height}
|
|
526
|
+
bind:clientWidth={tbody.width}
|
|
341
527
|
>
|
|
342
528
|
{#if table.options.reorderable}
|
|
343
529
|
{@render reorderArea({
|
|
@@ -352,32 +538,34 @@
|
|
|
352
538
|
}
|
|
353
539
|
})}
|
|
354
540
|
{:else}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
541
|
+
{#each virtualization.area as item, i (item)}
|
|
542
|
+
{@render rowSnippet(item, { index: i + virtualization.topIndex } as ItemState)}
|
|
543
|
+
{/each}
|
|
358
544
|
{/if}
|
|
359
545
|
</tbody>
|
|
360
546
|
|
|
361
|
-
|
|
362
|
-
<
|
|
363
|
-
|
|
364
|
-
(
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
547
|
+
{#if columns.some(v => v.snippets.statusbar)}
|
|
548
|
+
<tfoot class='statusbar' bind:this={elements.statusbar}>
|
|
549
|
+
<tr>
|
|
550
|
+
{@render columnsSnippet(
|
|
551
|
+
(column) => column.snippets.statusbar,
|
|
552
|
+
() => [{
|
|
553
|
+
get data() { return data.current }
|
|
554
|
+
}],
|
|
555
|
+
'statusbar'
|
|
556
|
+
)}
|
|
557
|
+
</tr>
|
|
558
|
+
</tfoot>
|
|
559
|
+
{/if}
|
|
371
560
|
|
|
372
561
|
<caption
|
|
373
|
-
class=
|
|
374
|
-
style=
|
|
375
|
-
style:overflow={panelTween.transitioning ? 'hidden' : 'auto'}
|
|
562
|
+
class='panel'
|
|
563
|
+
style='width: {panelTween.current}px;'
|
|
376
564
|
>
|
|
377
565
|
{#if properties.panel && properties.panel in table.panels}
|
|
378
566
|
<div
|
|
379
|
-
class=
|
|
380
|
-
bind:
|
|
567
|
+
class='panel-content'
|
|
568
|
+
bind:offsetWidth={panelTween.size}
|
|
381
569
|
in:fly={{ x: 100, easing: sineInOut, duration: 300 }}
|
|
382
570
|
out:fly={{ x: 100, duration: 200, easing: sineInOut }}
|
|
383
571
|
>
|
|
@@ -392,40 +580,45 @@
|
|
|
392
580
|
</div>
|
|
393
581
|
{/if}
|
|
394
582
|
</caption>
|
|
395
|
-
<caption class=
|
|
396
|
-
<button aria-label=
|
|
583
|
+
<caption class='backdrop' aria-hidden={properties.panel && table.panels[properties.panel]?.backdrop ? false : true}>
|
|
584
|
+
<button aria-label='Panel backdrop' class='btn-backdrop' tabindex='-1' onclick={() => (properties.panel = undefined)}
|
|
397
585
|
></button>
|
|
398
586
|
</caption>
|
|
399
587
|
</table>
|
|
400
588
|
|
|
401
589
|
{#snippet headerSelected(ctx: HeaderSelectCtx<T>)}
|
|
402
|
-
<input type=
|
|
590
|
+
<input type='checkbox' indeterminate={ctx.indeterminate} bind:checked={ctx.isSelected} />
|
|
403
591
|
{/snippet}
|
|
404
592
|
|
|
405
593
|
{#snippet rowSelected(ctx: RowSelectCtx<T>)}
|
|
406
|
-
<input type=
|
|
594
|
+
<input type='checkbox' bind:checked={ctx.isSelected} />
|
|
407
595
|
{/snippet}
|
|
408
596
|
|
|
409
|
-
{#if table.options.select || table.options.reorderable}
|
|
597
|
+
{#if table.options.select || table.options.reorderable || table.expandable}
|
|
410
598
|
{@const { select, reorderable } = table.options}
|
|
599
|
+
{@const expandable = table.expandable}
|
|
411
600
|
{@const {
|
|
412
601
|
show = 'hover',
|
|
413
602
|
style = 'column',
|
|
414
603
|
rowSnippet = rowSelected,
|
|
415
604
|
headerSnippet = headerSelected
|
|
416
605
|
} = typeof select === 'boolean' ? {} : select}
|
|
417
|
-
{#if show !== 'never' || reorderable}
|
|
606
|
+
{#if show !== 'never' || reorderable || expandable?.options.chevron !== 'never'}
|
|
418
607
|
<Column
|
|
419
|
-
id=
|
|
608
|
+
id='__fixed'
|
|
420
609
|
{table}
|
|
421
610
|
fixed
|
|
422
|
-
width={Math.max(56,
|
|
611
|
+
width={Math.max(56, 0
|
|
612
|
+
+ (select && show !== 'never' ? 34 : 0)
|
|
613
|
+
+ (reorderable ? 34 : 0)
|
|
614
|
+
+ (expandable?.options.chevron !== 'never' ? 34 : 0)
|
|
615
|
+
)}
|
|
423
616
|
resizeable={false}
|
|
424
617
|
>
|
|
425
618
|
{#snippet header()}
|
|
426
|
-
<div class=
|
|
619
|
+
<div class='__fixed'>
|
|
427
620
|
{#if reorderable}
|
|
428
|
-
<span style=
|
|
621
|
+
<span style='width: 16px; display: flex; align-items: center;'></span>
|
|
429
622
|
{/if}
|
|
430
623
|
{#if select}
|
|
431
624
|
{@render headerSnippet({
|
|
@@ -453,15 +646,15 @@
|
|
|
453
646
|
</div>
|
|
454
647
|
{/snippet}
|
|
455
648
|
{#snippet row(item, row)}
|
|
456
|
-
<div class=
|
|
457
|
-
{#if reorderable}
|
|
458
|
-
<span style=
|
|
459
|
-
{#if (row.isHovered && !row.itemState
|
|
649
|
+
<div class='__fixed'>
|
|
650
|
+
{#if reorderable && row.itemState}
|
|
651
|
+
<span style='width: 16px; display: flex; align-items: center;' use:row.itemState.handle>
|
|
652
|
+
{#if (row.isHovered && !row.itemState.area.isTarget) || row.itemState.dragging}
|
|
460
653
|
{@render dragSnippet()}
|
|
461
654
|
{/if}
|
|
462
655
|
</span>
|
|
463
656
|
{/if}
|
|
464
|
-
{#if select && (row.selected || show === 'always' || (row.isHovered && show === 'hover'))}
|
|
657
|
+
{#if select && (row.selected || show === 'always' || (row.isHovered && show === 'hover') || row.expanded)}
|
|
465
658
|
{@render rowSnippet({
|
|
466
659
|
get isSelected() {
|
|
467
660
|
return row.selected
|
|
@@ -480,6 +673,11 @@
|
|
|
480
673
|
}
|
|
481
674
|
})}
|
|
482
675
|
{/if}
|
|
676
|
+
{#if expandable && (row.expanded || expandable.options.chevron === 'always' || (row.isHovered && expandable.options.chevron === 'hover'))}
|
|
677
|
+
<button class='expand-row' onclick={() => row.expanded = !row.expanded}>
|
|
678
|
+
{@render chevronSnippet(row.expanded ? 180 : 90)}
|
|
679
|
+
</button>
|
|
680
|
+
{/if}
|
|
483
681
|
</div>
|
|
484
682
|
{/snippet}
|
|
485
683
|
</Column>
|
|
@@ -489,6 +687,7 @@
|
|
|
489
687
|
{@render content?.({
|
|
490
688
|
Column,
|
|
491
689
|
Panel,
|
|
690
|
+
Expandable,
|
|
492
691
|
get table() {
|
|
493
692
|
return table
|
|
494
693
|
},
|
|
@@ -510,6 +709,39 @@
|
|
|
510
709
|
overflow: visible;
|
|
511
710
|
}
|
|
512
711
|
|
|
712
|
+
.expandable {
|
|
713
|
+
position: relative;
|
|
714
|
+
|
|
715
|
+
& > td {
|
|
716
|
+
position: sticky;
|
|
717
|
+
left: 1px;
|
|
718
|
+
> div {
|
|
719
|
+
position: absolute;
|
|
720
|
+
overflow: auto;
|
|
721
|
+
top: -1.5px;
|
|
722
|
+
left: 0;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
.expand-row {
|
|
728
|
+
display: flex;
|
|
729
|
+
justify-content: center;
|
|
730
|
+
align-items: center;
|
|
731
|
+
padding: 0;
|
|
732
|
+
outline: none;
|
|
733
|
+
border: none;
|
|
734
|
+
cursor: pointer;
|
|
735
|
+
background-color: transparent;
|
|
736
|
+
color: inherit;
|
|
737
|
+
width: 22px;
|
|
738
|
+
height: 100%;
|
|
739
|
+
|
|
740
|
+
> svg {
|
|
741
|
+
transition: transform 0.15s ease;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
513
745
|
caption {
|
|
514
746
|
all: unset;
|
|
515
747
|
}
|
|
@@ -527,10 +759,13 @@
|
|
|
527
759
|
}
|
|
528
760
|
|
|
529
761
|
.sorting-icon {
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
762
|
+
align-items: center;
|
|
763
|
+
justify-items: end;
|
|
764
|
+
margin: 0;
|
|
765
|
+
margin-left: auto;
|
|
766
|
+
margin-right: var(--tably-padding-x, 1rem);
|
|
767
|
+
> svg {
|
|
768
|
+
transition: transform 0.15s ease;
|
|
534
769
|
}
|
|
535
770
|
}
|
|
536
771
|
|
|
@@ -572,7 +807,7 @@
|
|
|
572
807
|
height: var(--b);
|
|
573
808
|
}
|
|
574
809
|
|
|
575
|
-
|
|
810
|
+
.row:global(:is(a)) {
|
|
576
811
|
color: inherit;
|
|
577
812
|
text-decoration: inherit;
|
|
578
813
|
}
|
|
@@ -728,18 +963,19 @@
|
|
|
728
963
|
position: relative;
|
|
729
964
|
grid-area: panel;
|
|
730
965
|
height: 100%;
|
|
731
|
-
|
|
966
|
+
overflow: hidden;
|
|
732
967
|
border-left: 1px solid var(--tably-border, hsl(0, 0%, 90%));
|
|
733
|
-
|
|
734
|
-
scrollbar-width: thin;
|
|
968
|
+
|
|
735
969
|
z-index: 4;
|
|
736
970
|
|
|
737
971
|
> .panel-content {
|
|
738
972
|
position: absolute;
|
|
739
973
|
top: 0;
|
|
740
974
|
right: 0;
|
|
975
|
+
bottom: 0;
|
|
741
976
|
width: min-content;
|
|
742
977
|
overflow: auto;
|
|
978
|
+
scrollbar-width: thin;
|
|
743
979
|
padding: var(--tably-padding-y, 0.5rem) 0;
|
|
744
980
|
}
|
|
745
981
|
}
|
|
@@ -3,7 +3,14 @@ declare class __sveltets_Render<T extends Record<PropertyKey, unknown>> {
|
|
|
3
3
|
events(): {};
|
|
4
4
|
slots(): {};
|
|
5
5
|
bindings(): "selected" | "panel" | "data";
|
|
6
|
-
exports(): {
|
|
6
|
+
exports(): {
|
|
7
|
+
toCSV: (opts?: {
|
|
8
|
+
/** Semi-colons as separator? */
|
|
9
|
+
semicolon?: boolean;
|
|
10
|
+
/** Only selected rows */
|
|
11
|
+
selected?: boolean;
|
|
12
|
+
}) => Promise<string>;
|
|
13
|
+
};
|
|
7
14
|
}
|
|
8
15
|
interface $$IsomorphicComponent {
|
|
9
16
|
new <T extends Record<PropertyKey, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
package/dist/utility.svelte.d.ts
CHANGED
|
@@ -13,4 +13,5 @@ export declare function getters<T extends AnyRecord>(obj: T): { readonly [K in k
|
|
|
13
13
|
type SetterRecord = Record<PropertyKey, [() => any, (v: any) => void]>;
|
|
14
14
|
export declare function withSetters<T extends SetterRecord>(obj: T): T;
|
|
15
15
|
export declare function fromProps<T extends AnyRecord, B extends SetterRecord>(props: T, boundProps?: B): Simplify<{ [K in keyof B]: ReturnType<B[K][0]>; } & { readonly [K in keyof T]: T[K]; }>;
|
|
16
|
+
export declare function assignDescriptors<T extends AnyRecord, B extends AnyRecord>(target: T, source: B): T & B;
|
|
16
17
|
export {};
|
package/dist/utility.svelte.js
CHANGED
|
@@ -66,3 +66,15 @@ export function fromProps(props, boundProps) {
|
|
|
66
66
|
...withSetters(boundProps ?? {})
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
|
+
export function assignDescriptors(target, source) {
|
|
70
|
+
for (const key of Object.keys(source)) {
|
|
71
|
+
const descriptor = Object.getOwnPropertyDescriptor(source, key);
|
|
72
|
+
if (descriptor) {
|
|
73
|
+
Object.defineProperty(target, key, descriptor);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
target[key] = source[key]; // Copy regular values if descriptor is missing
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return target;
|
|
80
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-tably",
|
|
3
|
-
"version": "1.0.0-next.
|
|
3
|
+
"version": "1.0.0-next.14",
|
|
4
4
|
"repository": "github:refzlund/svelte-tably",
|
|
5
5
|
"homepage": "https://github.com/Refzlund/svelte-tably",
|
|
6
6
|
"bugs": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
|
14
14
|
"floating-runes": "^1.0.0",
|
|
15
15
|
"publint": "^0.2.0",
|
|
16
|
-
"runic-reorder": "
|
|
16
|
+
"runic-reorder": "^1.0.0",
|
|
17
17
|
"svelte": "^5.0.0",
|
|
18
18
|
"svelte-check": "^4.0.0",
|
|
19
19
|
"typescript": "^5.0.0",
|