svelte-tably 1.0.0-next.0
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 +19 -0
- package/dist/Table/Column.svelte +98 -0
- package/dist/Table/Column.svelte.d.ts +50 -0
- package/dist/Table/Panel.svelte +73 -0
- package/dist/Table/Panel.svelte.d.ts +31 -0
- package/dist/Table/Table.svelte +349 -0
- package/dist/Table/Table.svelte.d.ts +47 -0
- package/dist/Table/index.d.ts +12 -0
- package/dist/Table/index.js +31 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/prototype/Headers.svelte +33 -0
- package/dist/prototype/Headers.svelte.d.ts +15 -0
- package/dist/prototype/Panels.svelte +25 -0
- package/dist/prototype/Panels.svelte.d.ts +15 -0
- package/dist/prototype/Rows.svelte +35 -0
- package/dist/prototype/Rows.svelte.d.ts +27 -0
- package/dist/prototype/Statusbar.svelte +35 -0
- package/dist/prototype/Statusbar.svelte.d.ts +13 -0
- package/dist/prototype/Table.svelte +336 -0
- package/dist/prototype/Table.svelte.d.ts +51 -0
- package/package.json +47 -0
package/README.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# svelte-tably
|
|
2
|
+
|
|
3
|
+
Work in progress. I needed a break from my primary project, so here's a little side-project exploring that amazing capabilities of Svelte 5 with a Dynamic table!
|
|
4
|
+
|
|
5
|
+
A high performant dynamic table
|
|
6
|
+
|
|
7
|
+
- [x] Sticky columns
|
|
8
|
+
- [x] Show/hide columns
|
|
9
|
+
- [x] Re-order columns
|
|
10
|
+
- [x] Resize columns
|
|
11
|
+
- [x] Statusbar
|
|
12
|
+
- [x] Virtual data (for sorting/filtering)
|
|
13
|
+
- [x] Panels
|
|
14
|
+
- [ ] sorting
|
|
15
|
+
- [ ] select
|
|
16
|
+
- [ ] filtering
|
|
17
|
+
- [ ] orderable table
|
|
18
|
+
- [ ] row context-menu
|
|
19
|
+
- [ ] dropout section
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
|
|
3
|
+
This is a description, \
|
|
4
|
+
on how to use this.
|
|
5
|
+
|
|
6
|
+
@example
|
|
7
|
+
<Component />
|
|
8
|
+
|
|
9
|
+
-->
|
|
10
|
+
|
|
11
|
+
<script module lang='ts'>
|
|
12
|
+
|
|
13
|
+
export interface Column<T = unknown, V = unknown> {
|
|
14
|
+
header: Snippet
|
|
15
|
+
row: Snippet<[item: T, value?: V]>
|
|
16
|
+
statusbar?: Snippet<[data: T[]]>
|
|
17
|
+
|
|
18
|
+
/** Default options for initial table */
|
|
19
|
+
defaults: {
|
|
20
|
+
sticky?: boolean
|
|
21
|
+
sort?: boolean
|
|
22
|
+
show?: boolean
|
|
23
|
+
}
|
|
24
|
+
/** More options */
|
|
25
|
+
options: {
|
|
26
|
+
value?: (item: T) => V
|
|
27
|
+
sorting?: unknown extends V ? (a: T, b: T) => number : (a: V, b: V) => number
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<script lang='ts' generics='T extends Record<PropertyKey, any>, V'>
|
|
34
|
+
|
|
35
|
+
import { onDestroy, type Snippet } from 'svelte'
|
|
36
|
+
import { getTableState } from './Table.svelte'
|
|
37
|
+
|
|
38
|
+
interface Props {
|
|
39
|
+
header: Column<T, V>['header']
|
|
40
|
+
row: Column<T, V>['row']
|
|
41
|
+
statusbar?: Column<T, V>['statusbar']
|
|
42
|
+
|
|
43
|
+
// options
|
|
44
|
+
sticky?: boolean
|
|
45
|
+
sort?: boolean
|
|
46
|
+
show?: boolean
|
|
47
|
+
value?: Column<T, V>['options']['value']
|
|
48
|
+
sorting?: Column<T, V>['options']['sorting']
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let {
|
|
52
|
+
header, row, statusbar,
|
|
53
|
+
|
|
54
|
+
sticky = false,
|
|
55
|
+
sort = false,
|
|
56
|
+
show = true,
|
|
57
|
+
|
|
58
|
+
value, sorting,
|
|
59
|
+
|
|
60
|
+
...rest
|
|
61
|
+
}: Props = $props()
|
|
62
|
+
const key = (rest as unknown as { __key: string }).__key
|
|
63
|
+
|
|
64
|
+
const column: Column<T, V> = $state({
|
|
65
|
+
header,
|
|
66
|
+
row,
|
|
67
|
+
statusbar,
|
|
68
|
+
defaults: {
|
|
69
|
+
sticky,
|
|
70
|
+
sort,
|
|
71
|
+
show
|
|
72
|
+
},
|
|
73
|
+
options: {
|
|
74
|
+
value,
|
|
75
|
+
sorting
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
const table = getTableState()
|
|
80
|
+
table.addColumn(key, column as Column)
|
|
81
|
+
|
|
82
|
+
onDestroy(() => {
|
|
83
|
+
table.removeColumn(key)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
</script>
|
|
87
|
+
<!---------------------------------------------------->
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
<!---------------------------------------------------->
|
|
94
|
+
<style>
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
</style>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface Column<T = unknown, V = unknown> {
|
|
2
|
+
header: Snippet;
|
|
3
|
+
row: Snippet<[item: T, value?: V]>;
|
|
4
|
+
statusbar?: Snippet<[data: T[]]>;
|
|
5
|
+
/** Default options for initial table */
|
|
6
|
+
defaults: {
|
|
7
|
+
sticky?: boolean;
|
|
8
|
+
sort?: boolean;
|
|
9
|
+
show?: boolean;
|
|
10
|
+
};
|
|
11
|
+
/** More options */
|
|
12
|
+
options: {
|
|
13
|
+
value?: (item: T) => V;
|
|
14
|
+
sorting?: unknown extends V ? (a: T, b: T) => number : (a: V, b: V) => number;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
import { type Snippet } from 'svelte';
|
|
18
|
+
declare class __sveltets_Render<T extends Record<PropertyKey, any>, V> {
|
|
19
|
+
props(): {
|
|
20
|
+
header: Column<T_1, V_1>["header"];
|
|
21
|
+
row: Column<T_1, V_1>["row"];
|
|
22
|
+
statusbar?: Column<T_1, V_1>["statusbar"];
|
|
23
|
+
sticky?: boolean;
|
|
24
|
+
sort?: boolean;
|
|
25
|
+
show?: boolean;
|
|
26
|
+
value?: Column<T_1, V_1>["options"]["value"];
|
|
27
|
+
sorting?: Column<T_1, V_1>["options"]["sorting"];
|
|
28
|
+
};
|
|
29
|
+
events(): {};
|
|
30
|
+
slots(): {};
|
|
31
|
+
bindings(): "";
|
|
32
|
+
exports(): {};
|
|
33
|
+
}
|
|
34
|
+
interface $$IsomorphicComponent {
|
|
35
|
+
new <T extends Record<PropertyKey, any>, V>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T, V>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T, V>['props']>, ReturnType<__sveltets_Render<T, V>['events']>, ReturnType<__sveltets_Render<T, V>['slots']>> & {
|
|
36
|
+
$$bindings?: ReturnType<__sveltets_Render<T, V>['bindings']>;
|
|
37
|
+
} & ReturnType<__sveltets_Render<T, V>['exports']>;
|
|
38
|
+
<T extends Record<PropertyKey, any>, V>(internal: unknown, props: ReturnType<__sveltets_Render<T, V>['props']> & {}): ReturnType<__sveltets_Render<T, V>['exports']>;
|
|
39
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any, any>['bindings']>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* This is a description, \
|
|
43
|
+
* on how to use this.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* <Component />
|
|
47
|
+
*/
|
|
48
|
+
declare const Column: $$IsomorphicComponent;
|
|
49
|
+
type Column<T extends Record<PropertyKey, any>, V> = InstanceType<typeof Column<T, V>>;
|
|
50
|
+
export default Column;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
|
|
3
|
+
This is a description, \
|
|
4
|
+
on how to use this.
|
|
5
|
+
|
|
6
|
+
@example
|
|
7
|
+
<Component />
|
|
8
|
+
|
|
9
|
+
-->
|
|
10
|
+
|
|
11
|
+
<script module lang='ts'>
|
|
12
|
+
|
|
13
|
+
export interface Panel {
|
|
14
|
+
/** A darkened backdrop? */
|
|
15
|
+
backdrop: boolean
|
|
16
|
+
content: Snippet<[table: TableState]>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class PanelTween {
|
|
20
|
+
#tween = new Tween(0, { duration: 300, easing: sineInOut })
|
|
21
|
+
current = $derived(this.#tween.current)
|
|
22
|
+
transitioning = $state(false)
|
|
23
|
+
|
|
24
|
+
/** bind:clientWidth */
|
|
25
|
+
width = $state(0)
|
|
26
|
+
|
|
27
|
+
set target(value: number) {
|
|
28
|
+
this.transitioning = true
|
|
29
|
+
this.#tween.set(value).then(() => this.transitioning = false)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
constructor(cb: () => string | undefined) {
|
|
33
|
+
$effect.pre(() => {
|
|
34
|
+
this.target = cb() ? this.width : 0
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<script lang='ts'>
|
|
42
|
+
|
|
43
|
+
import { onDestroy, type Snippet } from 'svelte'
|
|
44
|
+
import { getTableState, type TableState } from './Table.svelte'
|
|
45
|
+
import { Tween } from 'svelte/motion'
|
|
46
|
+
import { sineInOut } from 'svelte/easing'
|
|
47
|
+
|
|
48
|
+
interface Props {
|
|
49
|
+
/** A darkened backdrop? */
|
|
50
|
+
backdrop?: boolean
|
|
51
|
+
children: Snippet<[table: TableState]>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let {
|
|
55
|
+
backdrop = true,
|
|
56
|
+
children,
|
|
57
|
+
...rest
|
|
58
|
+
}: Props = $props()
|
|
59
|
+
const key = (rest as unknown as { __key: string }).__key
|
|
60
|
+
|
|
61
|
+
const panel: Panel = $state({
|
|
62
|
+
backdrop,
|
|
63
|
+
content: children
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const table = getTableState()
|
|
67
|
+
table.panels[key] = panel
|
|
68
|
+
|
|
69
|
+
onDestroy(() => {
|
|
70
|
+
delete table.panels[key]
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
</script>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface Panel {
|
|
2
|
+
/** A darkened backdrop? */
|
|
3
|
+
backdrop: boolean;
|
|
4
|
+
content: Snippet<[table: TableState]>;
|
|
5
|
+
}
|
|
6
|
+
export declare class PanelTween {
|
|
7
|
+
#private;
|
|
8
|
+
current: number;
|
|
9
|
+
transitioning: boolean;
|
|
10
|
+
/** bind:clientWidth */
|
|
11
|
+
width: number;
|
|
12
|
+
set target(value: number);
|
|
13
|
+
constructor(cb: () => string | undefined);
|
|
14
|
+
}
|
|
15
|
+
import { type Snippet } from 'svelte';
|
|
16
|
+
import { type TableState } from './Table.svelte';
|
|
17
|
+
interface Props {
|
|
18
|
+
/** A darkened backdrop? */
|
|
19
|
+
backdrop?: boolean;
|
|
20
|
+
children: Snippet<[table: TableState]>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* This is a description, \
|
|
24
|
+
* on how to use this.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* <Component />
|
|
28
|
+
*/
|
|
29
|
+
declare const Panel: import("svelte").Component<Props, {}, "">;
|
|
30
|
+
type Panel = ReturnType<typeof Panel>;
|
|
31
|
+
export default Panel;
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
|
|
3
|
+
This is a description, \
|
|
4
|
+
on how to use this.
|
|
5
|
+
|
|
6
|
+
@example
|
|
7
|
+
<Component />
|
|
8
|
+
|
|
9
|
+
-->
|
|
10
|
+
|
|
11
|
+
<script module lang='ts'>
|
|
12
|
+
|
|
13
|
+
export interface TableState<T extends Record<PropertyKey, any> = Record<PropertyKey, any>> {
|
|
14
|
+
columns: Record<string, Column<T, unknown>>
|
|
15
|
+
panels: Record<string, Panel>
|
|
16
|
+
sortby?: string
|
|
17
|
+
positions: {
|
|
18
|
+
sticky: string[]
|
|
19
|
+
scroll: string[]
|
|
20
|
+
hidden: string[]
|
|
21
|
+
toggle(key: string): void
|
|
22
|
+
}
|
|
23
|
+
readonly data: T[]
|
|
24
|
+
addColumn(key: string, options: Column<T, unknown>): void
|
|
25
|
+
removeColumn(key: string): void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getTableState<T extends Record<PropertyKey, any> = Record<PropertyKey, any>>() {
|
|
29
|
+
return getContext<TableState<T>>('svelte5-table')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<script lang='ts' generics='T extends Record<PropertyKey, unknown>'>
|
|
35
|
+
|
|
36
|
+
import { getContext, setContext, type Snippet } from 'svelte'
|
|
37
|
+
import { type Column } from './Column.svelte'
|
|
38
|
+
import { PanelTween, type Panel } from './Panel.svelte'
|
|
39
|
+
import { fly } from 'svelte/transition'
|
|
40
|
+
import { sineInOut } from 'svelte/easing'
|
|
41
|
+
|
|
42
|
+
interface Props {
|
|
43
|
+
children?: Snippet
|
|
44
|
+
panel?: string
|
|
45
|
+
data?: T[]
|
|
46
|
+
id?: string
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let {
|
|
50
|
+
children,
|
|
51
|
+
panel,
|
|
52
|
+
data = [],
|
|
53
|
+
id = Array.from({length: 12}, () => String.fromCharCode(Math.floor(Math.random() * 26) + 97)).join('')
|
|
54
|
+
}: Props = $props()
|
|
55
|
+
|
|
56
|
+
const table: TableState<T> = $state({
|
|
57
|
+
columns: {},
|
|
58
|
+
panels: {},
|
|
59
|
+
positions: {
|
|
60
|
+
sticky: [],
|
|
61
|
+
scroll: [],
|
|
62
|
+
hidden: [],
|
|
63
|
+
toggle(key) {
|
|
64
|
+
if(table.positions.hidden.includes(key))
|
|
65
|
+
table.positions.hidden = table.positions.hidden.filter(column => column !== key)
|
|
66
|
+
else
|
|
67
|
+
table.positions.hidden.push(key)
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
get data() {
|
|
71
|
+
return data
|
|
72
|
+
},
|
|
73
|
+
addColumn(key, column) {
|
|
74
|
+
table.columns[key] = column
|
|
75
|
+
|
|
76
|
+
if(column.defaults.sort)
|
|
77
|
+
table.sortby = key
|
|
78
|
+
|
|
79
|
+
if(!column.defaults.show)
|
|
80
|
+
table.positions.hidden.push(key)
|
|
81
|
+
|
|
82
|
+
if(column.defaults.sticky)
|
|
83
|
+
table.positions.sticky.push(key)
|
|
84
|
+
else
|
|
85
|
+
table.positions.scroll.push(key)
|
|
86
|
+
},
|
|
87
|
+
removeColumn(key) {
|
|
88
|
+
delete table.columns[key]
|
|
89
|
+
table.positions.sticky = table.positions.sticky.filter(column => column !== key)
|
|
90
|
+
table.positions.scroll = table.positions.scroll.filter(column => column !== key)
|
|
91
|
+
table.positions.hidden = table.positions.hidden.filter(column => column !== key)
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
setContext('svelte5-table', table)
|
|
96
|
+
|
|
97
|
+
// * --- *
|
|
98
|
+
|
|
99
|
+
const panelTween = new PanelTween(() => panel)
|
|
100
|
+
const elements = $state({}) as Record<'headers' | 'statusbar', HTMLElement>
|
|
101
|
+
|
|
102
|
+
/** Order of columns */
|
|
103
|
+
const columns = $derived([...table.positions.sticky, ...table.positions.scroll].filter(key => !table.positions.hidden.includes(key)))
|
|
104
|
+
|
|
105
|
+
/** Width of each column */
|
|
106
|
+
const widths = $state({}) as Record<string, number>
|
|
107
|
+
|
|
108
|
+
/** grid-template-columns for widths */
|
|
109
|
+
const style = $derived(`
|
|
110
|
+
#${id} > .headers, #${id} > .rows > .row, #${id} > .statusbar {
|
|
111
|
+
grid-template-columns: ${columns.map((key, i, arr) => i === arr.length - 1 ? `minmax(${widths[key] || 150}px, 1fr)` : `${widths[key] || 150}px`).join(' ')};
|
|
112
|
+
}
|
|
113
|
+
`)
|
|
114
|
+
|
|
115
|
+
const observer = typeof MutationObserver === 'undefined' ? undefined : new MutationObserver(mutations => {
|
|
116
|
+
const target = mutations[0].target as HTMLDivElement
|
|
117
|
+
widths[target.getAttribute('data-column')!] = parseFloat(target.style.width)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
function observe(node: HTMLDivElement, column: string) {
|
|
121
|
+
observer?.observe(node, {attributes: true})
|
|
122
|
+
return { destroy: () => observer?.disconnect() }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function onscroll(event: Event) {
|
|
126
|
+
const target = event.target as HTMLDivElement
|
|
127
|
+
elements.headers.scrollLeft = target.scrollLeft
|
|
128
|
+
elements.statusbar.scrollLeft = target.scrollLeft
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
</script>
|
|
132
|
+
<!---------------------------------------------------->
|
|
133
|
+
|
|
134
|
+
<svelte:head>
|
|
135
|
+
{@html `<style>${style}</style>`}
|
|
136
|
+
</svelte:head>
|
|
137
|
+
|
|
138
|
+
<div id={id} class='table'>
|
|
139
|
+
|
|
140
|
+
<div class='headers' bind:this={elements.headers}>
|
|
141
|
+
{#each table.positions.sticky as column, i (column)}
|
|
142
|
+
{#if !table.positions.hidden.includes(column)}
|
|
143
|
+
<div class='column sticky' data-column="{column}" use:observe={column}>
|
|
144
|
+
{@render table.columns[column]?.header()}
|
|
145
|
+
</div>
|
|
146
|
+
{/if}
|
|
147
|
+
{/each}
|
|
148
|
+
{#each table.positions.scroll as column, i (column)}
|
|
149
|
+
{#if !table.positions.hidden.includes(column)}
|
|
150
|
+
<div class='column' use:observe={column}>
|
|
151
|
+
{@render table.columns[column]?.header()}
|
|
152
|
+
</div>
|
|
153
|
+
{/if}
|
|
154
|
+
{/each}
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
<div class="rows" {onscroll}>
|
|
158
|
+
{#each data as item}
|
|
159
|
+
<div class='row'>
|
|
160
|
+
{#each table.positions.sticky as column, i (column)}
|
|
161
|
+
{#if !table.positions.hidden.includes(column)}
|
|
162
|
+
{@const col = table.columns[column]}
|
|
163
|
+
<div class='column sticky' class:border={i == table.positions.sticky.length - 1}>
|
|
164
|
+
{@render col.row(item, col.options.value ? col.options.value(item) : undefined)}
|
|
165
|
+
</div>
|
|
166
|
+
{/if}
|
|
167
|
+
{/each}
|
|
168
|
+
{#each table.positions.scroll as column, i (column)}
|
|
169
|
+
{#if !table.positions.hidden.includes(column)}
|
|
170
|
+
{@const col = table.columns[column]}
|
|
171
|
+
<div class='column'>
|
|
172
|
+
{@render col.row(item, col.options.value ? col.options.value(item) : undefined)}
|
|
173
|
+
</div>
|
|
174
|
+
{/if}
|
|
175
|
+
{/each}
|
|
176
|
+
</div>
|
|
177
|
+
{/each}
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<div class='statusbar' bind:this={elements.statusbar}>
|
|
181
|
+
{#each table.positions.sticky as column, i (column)}
|
|
182
|
+
{#if !table.positions.hidden.includes(column)}
|
|
183
|
+
<div class='column sticky' class:border={i == table.positions.sticky.length - 1}>
|
|
184
|
+
{@render table.columns[column]?.statusbar?.(data)}
|
|
185
|
+
</div>
|
|
186
|
+
{/if}
|
|
187
|
+
{/each}
|
|
188
|
+
{#each table.positions.scroll as column, i (column)}
|
|
189
|
+
{#if !table.positions.hidden.includes(column)}
|
|
190
|
+
<div class='column'>
|
|
191
|
+
{@render table.columns[column]?.statusbar?.(data)}
|
|
192
|
+
</div>
|
|
193
|
+
{/if}
|
|
194
|
+
{/each}
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div class='panel' style='width: {panelTween.current}px;' style:overflow={panelTween.transitioning ? 'hidden' : 'auto'}>
|
|
198
|
+
{#if panel && panel in table.panels}
|
|
199
|
+
<div
|
|
200
|
+
class="panel-content"
|
|
201
|
+
bind:clientWidth={panelTween.width}
|
|
202
|
+
in:fly={{ x: 100, easing: sineInOut, duration:300 }}
|
|
203
|
+
out:fly={{ x:100, duration:200, easing: sineInOut }}
|
|
204
|
+
>
|
|
205
|
+
{@render table.panels[panel].content(table as TableState)}
|
|
206
|
+
</div>
|
|
207
|
+
{/if}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
{@render children?.()}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
<!---------------------------------------------------->
|
|
217
|
+
<style>
|
|
218
|
+
|
|
219
|
+
.table, .table * {
|
|
220
|
+
box-sizing: border-box;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.sticky {
|
|
224
|
+
position: sticky;
|
|
225
|
+
left: 0px;
|
|
226
|
+
/* right: 100px; */
|
|
227
|
+
background-color: white;
|
|
228
|
+
z-index: 1;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.sticky.border {
|
|
232
|
+
border-right: 1px solid hsla(0, 0%, 90%);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.headers > .column {
|
|
236
|
+
border-right: 1px solid hsla(0, 0%, 90%);
|
|
237
|
+
resize: horizontal;
|
|
238
|
+
overflow: hidden;
|
|
239
|
+
padding: var(--padding-y) 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.table {
|
|
243
|
+
--panel: 250px;
|
|
244
|
+
--padding-x: 1rem;
|
|
245
|
+
--padding-y: .5rem;
|
|
246
|
+
--gap: .25rem;
|
|
247
|
+
--header-height: 2.5rem;
|
|
248
|
+
|
|
249
|
+
display: grid;
|
|
250
|
+
|
|
251
|
+
grid-template-areas:
|
|
252
|
+
"headers panel"
|
|
253
|
+
"rows panel"
|
|
254
|
+
"statusbar panel"
|
|
255
|
+
;
|
|
256
|
+
|
|
257
|
+
grid-template-columns: auto min-content;
|
|
258
|
+
grid-template-rows: auto 1fr auto;
|
|
259
|
+
|
|
260
|
+
border: 1px solid hsla(0, 0%, 90%);
|
|
261
|
+
border-radius: .25rem;
|
|
262
|
+
|
|
263
|
+
max-height: 100%;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.headers {
|
|
267
|
+
grid-area: headers;
|
|
268
|
+
z-index: 2;
|
|
269
|
+
overflow: hidden;
|
|
270
|
+
padding-right: 1rem;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
.headers > .column {
|
|
274
|
+
width: auto !important;
|
|
275
|
+
background-color: hsla(0, 0%, 100%);
|
|
276
|
+
border-bottom: 1px solid hsla(0, 0%, 90%);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.rows {
|
|
280
|
+
grid-area: rows;
|
|
281
|
+
display: grid;
|
|
282
|
+
overflow: auto;
|
|
283
|
+
scrollbar-width: thin;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.statusbar {
|
|
287
|
+
grid-area: statusbar;
|
|
288
|
+
overflow: hidden;
|
|
289
|
+
padding-right: 1rem;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.statusbar > .column {
|
|
293
|
+
background-color: hsla(0, 0%, 99%);
|
|
294
|
+
border-top: 1px solid hsla(0, 0%, 90%);
|
|
295
|
+
padding: calc(var(--padding-y) / 2) 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.headers, .row, .statusbar {
|
|
299
|
+
display: grid;
|
|
300
|
+
width: 100%;
|
|
301
|
+
height: 100%;
|
|
302
|
+
|
|
303
|
+
& > .column {
|
|
304
|
+
display: flex;
|
|
305
|
+
padding-left: var(--padding-x);
|
|
306
|
+
overflow: hidden;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
& > *:last-child {
|
|
310
|
+
width: 100%;
|
|
311
|
+
padding-right: var(--padding-x);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.row:nth-child(1) > * {
|
|
316
|
+
padding-top: calc(var(--padding-y) + var(--gap));
|
|
317
|
+
}
|
|
318
|
+
.row:nth-last-child(1) > * {
|
|
319
|
+
padding-bottom: calc(var(--padding-y) + var(--gap));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.row > * {
|
|
323
|
+
padding: var(--gap) 0;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.panel {
|
|
327
|
+
position: relative;
|
|
328
|
+
grid-area: panel;
|
|
329
|
+
width: var(--panel);
|
|
330
|
+
height: 100%;
|
|
331
|
+
background-color: white;
|
|
332
|
+
|
|
333
|
+
border-left: 1px solid hsla(0, 0%, 90%);
|
|
334
|
+
|
|
335
|
+
> .panel-content {
|
|
336
|
+
position: absolute;
|
|
337
|
+
top: 0;
|
|
338
|
+
right: 0;
|
|
339
|
+
width: min-content;
|
|
340
|
+
overflow: hidden;
|
|
341
|
+
padding: var(--padding-y) var(--padding-x);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.statusbar {
|
|
346
|
+
grid-area: statusbar;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
</style>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface TableState<T extends Record<PropertyKey, any> = Record<PropertyKey, any>> {
|
|
2
|
+
columns: Record<string, Column<T, unknown>>;
|
|
3
|
+
panels: Record<string, Panel>;
|
|
4
|
+
sortby?: string;
|
|
5
|
+
positions: {
|
|
6
|
+
sticky: string[];
|
|
7
|
+
scroll: string[];
|
|
8
|
+
hidden: string[];
|
|
9
|
+
toggle(key: string): void;
|
|
10
|
+
};
|
|
11
|
+
readonly data: T[];
|
|
12
|
+
addColumn(key: string, options: Column<T, unknown>): void;
|
|
13
|
+
removeColumn(key: string): void;
|
|
14
|
+
}
|
|
15
|
+
export declare function getTableState<T extends Record<PropertyKey, any> = Record<PropertyKey, any>>(): TableState<T>;
|
|
16
|
+
import { type Snippet } from 'svelte';
|
|
17
|
+
import { type Column } from './Column.svelte';
|
|
18
|
+
import { type Panel } from './Panel.svelte';
|
|
19
|
+
declare class __sveltets_Render<T extends Record<PropertyKey, unknown>> {
|
|
20
|
+
props(): {
|
|
21
|
+
children?: Snippet;
|
|
22
|
+
panel?: string;
|
|
23
|
+
data?: T[] | undefined;
|
|
24
|
+
id?: string;
|
|
25
|
+
};
|
|
26
|
+
events(): {};
|
|
27
|
+
slots(): {};
|
|
28
|
+
bindings(): "";
|
|
29
|
+
exports(): {};
|
|
30
|
+
}
|
|
31
|
+
interface $$IsomorphicComponent {
|
|
32
|
+
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']>> & {
|
|
33
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
34
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
35
|
+
<T extends Record<PropertyKey, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
36
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* This is a description, \
|
|
40
|
+
* on how to use this.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* <Component />
|
|
44
|
+
*/
|
|
45
|
+
declare const Table: $$IsomorphicComponent;
|
|
46
|
+
type Table<T extends Record<PropertyKey, unknown>> = InstanceType<typeof Table<T>>;
|
|
47
|
+
export default Table;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { default as _Table } from './Table.svelte';
|
|
2
|
+
import Column from './Column.svelte';
|
|
3
|
+
import { default as _Panel } from './Panel.svelte';
|
|
4
|
+
declare const LATIN: readonly ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"];
|
|
5
|
+
type Capital = typeof LATIN[number];
|
|
6
|
+
declare const Table: typeof _Table & {
|
|
7
|
+
[key: `${Capital}${string}`]: typeof Column;
|
|
8
|
+
};
|
|
9
|
+
declare const Panel: {
|
|
10
|
+
[key: `${Capital}${string}`]: typeof _Panel;
|
|
11
|
+
};
|
|
12
|
+
export { Table, Panel };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { default as _Table } from './Table.svelte';
|
|
2
|
+
import Column from './Column.svelte';
|
|
3
|
+
import { default as _Panel } from './Panel.svelte';
|
|
4
|
+
const LATIN = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
|
|
5
|
+
const Table = new Proxy(_Table, {
|
|
6
|
+
get(target, p, receiver) {
|
|
7
|
+
if (typeof p !== 'string' || !LATIN.includes(p[0])) {
|
|
8
|
+
return Reflect.get(target, p, receiver);
|
|
9
|
+
}
|
|
10
|
+
return new Proxy(Column, {
|
|
11
|
+
apply(_, __, [anchor, props]) {
|
|
12
|
+
Object.assign(props, { __key: p });
|
|
13
|
+
return Column(anchor, props);
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
const Panel = new Proxy(_Panel, {
|
|
19
|
+
get(target, p, receiver) {
|
|
20
|
+
if (typeof p !== 'string' || !LATIN.includes(p[0])) {
|
|
21
|
+
return Reflect.get(target, p, receiver);
|
|
22
|
+
}
|
|
23
|
+
return new Proxy(_Panel, {
|
|
24
|
+
apply(_, __, [anchor, props]) {
|
|
25
|
+
Object.assign(props, { __key: p });
|
|
26
|
+
return _Panel(anchor, props);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
export { Table, Panel };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Table/index.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './Table/index.js';
|
|
@@ -0,0 +1,33 @@
|
|
|
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 { getTableState, type ColumnOptions } from '../Table.svelte'
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
[key: string]: Snippet<[options: ColumnOptions]>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let headers: Props = $props()
|
|
20
|
+
const table = getTableState()
|
|
21
|
+
|
|
22
|
+
let keys = [] as string[]
|
|
23
|
+
|
|
24
|
+
$effect.pre(() => {
|
|
25
|
+
keys.forEach((key) => delete table.columns[key].header)
|
|
26
|
+
keys = []
|
|
27
|
+
|
|
28
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
29
|
+
table.updateColumn(key, { header: value })
|
|
30
|
+
keys.push(key)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
</script>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import { type ColumnOptions } from '../Table.svelte';
|
|
3
|
+
interface Props {
|
|
4
|
+
[key: string]: Snippet<[options: ColumnOptions]>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* This is a description, \
|
|
8
|
+
* on how to use this.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* <Component />
|
|
12
|
+
*/
|
|
13
|
+
declare const Headers: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type Headers = ReturnType<typeof Headers>;
|
|
15
|
+
export default Headers;
|
|
@@ -0,0 +1,25 @@
|
|
|
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 type { Snippet } from 'svelte'
|
|
14
|
+
import { getTableState, type TableState } from './Table.svelte'
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
[key: string]: Snippet<[table: TableState]>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let panels: Props = $props()
|
|
21
|
+
|
|
22
|
+
getTableState().panels = panels
|
|
23
|
+
|
|
24
|
+
</script>
|
|
25
|
+
<!---------------------------------------------------->
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import { type TableState } from './Table.svelte';
|
|
3
|
+
interface Props {
|
|
4
|
+
[key: string]: Snippet<[table: TableState]>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* This is a description, \
|
|
8
|
+
* on how to use this.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* <Component />
|
|
12
|
+
*/
|
|
13
|
+
declare const Panels: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type Panels = ReturnType<typeof Panels>;
|
|
15
|
+
export default Panels;
|
|
@@ -0,0 +1,35 @@
|
|
|
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' generics='T extends Record<PropertyKey, unknown>'>
|
|
12
|
+
|
|
13
|
+
import type { Snippet } from 'svelte'
|
|
14
|
+
import { getTableState } from './Table.svelte'
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
[key: string]: Snippet<[data: T]>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let rows: Props = $props()
|
|
21
|
+
const table = getTableState<T>()
|
|
22
|
+
|
|
23
|
+
let keys = [] as string[]
|
|
24
|
+
|
|
25
|
+
$effect.pre(() => {
|
|
26
|
+
keys.forEach(key => delete table.columns[key].row)
|
|
27
|
+
keys = []
|
|
28
|
+
|
|
29
|
+
for(const [key, value] of Object.entries(rows)) {
|
|
30
|
+
table.updateColumn(key, { row: value })
|
|
31
|
+
keys.push(key)
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
declare class __sveltets_Render<T extends Record<PropertyKey, unknown>> {
|
|
3
|
+
props(): {
|
|
4
|
+
[key: string]: Snippet<[data: T]>;
|
|
5
|
+
};
|
|
6
|
+
events(): {};
|
|
7
|
+
slots(): {};
|
|
8
|
+
bindings(): "";
|
|
9
|
+
exports(): {};
|
|
10
|
+
}
|
|
11
|
+
interface $$IsomorphicComponent {
|
|
12
|
+
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']>> & {
|
|
13
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
14
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
15
|
+
<T extends Record<PropertyKey, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
16
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* This is a description, \
|
|
20
|
+
* on how to use this.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* <Component />
|
|
24
|
+
*/
|
|
25
|
+
declare const Rows: $$IsomorphicComponent;
|
|
26
|
+
type Rows<T extends Record<PropertyKey, unknown>> = InstanceType<typeof Rows<T>>;
|
|
27
|
+
export default Rows;
|
|
@@ -0,0 +1,35 @@
|
|
|
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 type { Snippet } from 'svelte'
|
|
14
|
+
import { getTableState, type TableState } from './Table.svelte'
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
[key: string]: Snippet<[data: T[]]>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let statusbars: Props = $props()
|
|
21
|
+
const table = getTableState()
|
|
22
|
+
|
|
23
|
+
let keys = [] as string[]
|
|
24
|
+
|
|
25
|
+
$effect.pre(() => {
|
|
26
|
+
keys.forEach(key => delete table.columns[key].statusbar)
|
|
27
|
+
keys = []
|
|
28
|
+
for(const [key, value] of Object.entries(statusbars)) {
|
|
29
|
+
table.updateColumn(key, { statusbar: value })
|
|
30
|
+
keys.push(key)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
</script>
|
|
35
|
+
<!---------------------------------------------------->
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
/**
|
|
3
|
+
* This is a description, \
|
|
4
|
+
* on how to use this.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* <Component />
|
|
8
|
+
*/
|
|
9
|
+
declare const Statusbar: import("svelte").Component<{
|
|
10
|
+
[key: string]: Snippet<[data: T[]]>;
|
|
11
|
+
}, {}, "">;
|
|
12
|
+
type Statusbar = ReturnType<typeof Statusbar>;
|
|
13
|
+
export default Statusbar;
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
<!-- @component
|
|
2
|
+
|
|
3
|
+
This is a description, \
|
|
4
|
+
on how to use this.
|
|
5
|
+
|
|
6
|
+
@example
|
|
7
|
+
<Component />
|
|
8
|
+
|
|
9
|
+
-->
|
|
10
|
+
|
|
11
|
+
<script module lang='ts'>
|
|
12
|
+
|
|
13
|
+
export interface ColumnOptions {
|
|
14
|
+
sticky(): ColumnOptions
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface Column<T = unknown> {
|
|
18
|
+
header?: Snippet<[options: ColumnOptions]>
|
|
19
|
+
row?: Snippet<[data: T]>
|
|
20
|
+
statusbar?: Snippet<[data: T[]]>
|
|
21
|
+
defaults?: {
|
|
22
|
+
sticky?: boolean
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TableState<T = unknown> {
|
|
27
|
+
columns: Record<string, Column<T>>
|
|
28
|
+
order: {
|
|
29
|
+
sticky: string[]
|
|
30
|
+
scroll: string[]
|
|
31
|
+
}
|
|
32
|
+
panels: Record<string, Snippet<[table: TableState]>>
|
|
33
|
+
readonly data: T[]
|
|
34
|
+
updateColumn(key: string, options: Column<T>): void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getTableState<T>() {
|
|
38
|
+
return getContext('svelte-dynamic-table') as TableState<T>
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<script lang='ts' generics='T extends Record<PropertyKey, unknown>'>
|
|
44
|
+
|
|
45
|
+
import { getContext, setContext, type Snippet } from 'svelte'
|
|
46
|
+
import { sineInOut } from 'svelte/easing'
|
|
47
|
+
import { Spring, Tween } from 'svelte/motion'
|
|
48
|
+
import { fly } from 'svelte/transition'
|
|
49
|
+
|
|
50
|
+
interface Props {
|
|
51
|
+
children?: Snippet
|
|
52
|
+
data: T[]
|
|
53
|
+
panel?: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let {
|
|
57
|
+
children,
|
|
58
|
+
data,
|
|
59
|
+
panel = $bindable()
|
|
60
|
+
}: Props = $props()
|
|
61
|
+
|
|
62
|
+
const table: TableState<T> = $state({
|
|
63
|
+
columns: {},
|
|
64
|
+
order: {
|
|
65
|
+
sticky: [],
|
|
66
|
+
scroll: []
|
|
67
|
+
},
|
|
68
|
+
panels: {},
|
|
69
|
+
get data() {
|
|
70
|
+
return data
|
|
71
|
+
},
|
|
72
|
+
updateColumn(key, options) {
|
|
73
|
+
if(!(key in table.columns)) {
|
|
74
|
+
const column = $state({})
|
|
75
|
+
table.columns[key] = column
|
|
76
|
+
}
|
|
77
|
+
Object.assign(table.columns[key], options)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const columns = $derived([...table.order.sticky, ...table.order.scroll])
|
|
82
|
+
|
|
83
|
+
/** Passed to header */
|
|
84
|
+
function columnOptions(key: string) {
|
|
85
|
+
const options = $state({}) as NonNullable<Column['defaults']>
|
|
86
|
+
table.updateColumn(key, { defaults: options })
|
|
87
|
+
return {
|
|
88
|
+
sticky() {
|
|
89
|
+
options.sticky = true
|
|
90
|
+
return this
|
|
91
|
+
}
|
|
92
|
+
} as ColumnOptions
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setContext('svelte-dynamic-table', table)
|
|
96
|
+
|
|
97
|
+
const widths = $state({}) as Record<string, number>
|
|
98
|
+
const headers = new WeakMap<HTMLDivElement, string>()
|
|
99
|
+
|
|
100
|
+
const tableId = Array.from({length: 12}, () => String.fromCharCode(Math.floor(Math.random() * 26) + 97)).join('')
|
|
101
|
+
|
|
102
|
+
const style = $derived(`
|
|
103
|
+
#${tableId} > .headers, #${tableId} > .rows > .row, #${tableId} > .statusbar {
|
|
104
|
+
grid-template-columns: ${columns.map((key, i, arr) => i === arr.length - 1 ? `minmax(${widths[key] || 150}px, 1fr)` : `${widths[key] || 150}px`).join(' ')};
|
|
105
|
+
}
|
|
106
|
+
`)
|
|
107
|
+
|
|
108
|
+
const observer = typeof MutationObserver === 'undefined' ? undefined : new MutationObserver(mutations => {
|
|
109
|
+
const target = mutations[0].target as HTMLDivElement
|
|
110
|
+
const column = headers.get(target)
|
|
111
|
+
if (!column) return
|
|
112
|
+
widths[column] = parseFloat(target.style.width)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
function observe(node: HTMLDivElement, column: string) {
|
|
116
|
+
observer?.observe(node, {attributes: true})
|
|
117
|
+
headers.set(node, column)
|
|
118
|
+
return {
|
|
119
|
+
destroy: () => observer?.disconnect()
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let elements = $state({}) as Record<string, HTMLElement>
|
|
124
|
+
function onscroll(event: Event) {
|
|
125
|
+
const target = event.target as HTMLDivElement
|
|
126
|
+
elements.headers.scrollLeft = target.scrollLeft
|
|
127
|
+
elements.statusbar.scrollLeft = target.scrollLeft
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const panelTween = new Tween(0, { duration: 300, easing: sineInOut })
|
|
131
|
+
let panelWidth = $state(0)
|
|
132
|
+
let panelTransitioning = $state(false)
|
|
133
|
+
$effect(() => {
|
|
134
|
+
panelTransitioning = true
|
|
135
|
+
panelTween.set(panel ? panelWidth : 0).then(() => panelTransitioning = false)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
</script>
|
|
139
|
+
<!---------------------------------------------------->
|
|
140
|
+
|
|
141
|
+
<svelte:head>
|
|
142
|
+
{@html `<style>${style}</style>`}
|
|
143
|
+
</svelte:head>
|
|
144
|
+
|
|
145
|
+
<div id={tableId} class='table'>
|
|
146
|
+
|
|
147
|
+
<div class='headers' bind:this={elements.headers}>
|
|
148
|
+
{#each table.order.sticky as column, i (column)}
|
|
149
|
+
<div class='column sticky' use:observe={column}>
|
|
150
|
+
{@render table.columns[column]?.header?.({ sticky() {} } as ColumnOptions)}
|
|
151
|
+
</div>
|
|
152
|
+
{/each}
|
|
153
|
+
{#each table.order.scroll as column, i (column)}
|
|
154
|
+
<div class='column' use:observe={column}>
|
|
155
|
+
{@render table.columns[column]?.header?.({ sticky() {} } as ColumnOptions)}
|
|
156
|
+
</div>
|
|
157
|
+
{/each}
|
|
158
|
+
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div class="rows" {onscroll}>
|
|
162
|
+
{#each data as item}
|
|
163
|
+
<div class='row'>
|
|
164
|
+
{#each table.order.sticky as column, i (column)}
|
|
165
|
+
<div class='column sticky' class:border={i == table.order.sticky.length - 1}>
|
|
166
|
+
{@render table.columns[column]?.row?.(item)}
|
|
167
|
+
</div>
|
|
168
|
+
{/each}
|
|
169
|
+
{#each table.order.scroll as column, i (column)}
|
|
170
|
+
<div class='column'>
|
|
171
|
+
{@render table.columns[column]?.row?.(item)}
|
|
172
|
+
</div>
|
|
173
|
+
{/each}
|
|
174
|
+
</div>
|
|
175
|
+
{/each}
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<div class='statusbar' bind:this={elements.statusbar}>
|
|
179
|
+
{#each table.order.sticky as column, i (column)}
|
|
180
|
+
<div class='column sticky' class:border={i == table.order.sticky.length - 1}>
|
|
181
|
+
{@render table.columns[column]?.statusbar?.(data)}
|
|
182
|
+
</div>
|
|
183
|
+
{/each}
|
|
184
|
+
{#each table.order.scroll as column, i (column)}
|
|
185
|
+
<div class='column'>
|
|
186
|
+
{@render table.columns[column]?.statusbar?.(data)}
|
|
187
|
+
</div>
|
|
188
|
+
{/each}
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<div class='panel' style='width: {panelTween.current}px;' style:overflow={panelTransitioning ? 'hidden' : 'auto'}>
|
|
192
|
+
{#if panel && table.panels[panel]}
|
|
193
|
+
<div class="panel-content" bind:clientWidth={panelWidth} in:fly={{ x: 100, easing: sineInOut, duration:300 }} out:fly={{ x:100, duration:200, easing: sineInOut }}>
|
|
194
|
+
{@render table.panels[panel](table as TableState)}
|
|
195
|
+
</div>
|
|
196
|
+
{/if}
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
{@render children?.()}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
<!---------------------------------------------------->
|
|
207
|
+
<style>
|
|
208
|
+
|
|
209
|
+
.table, .table * {
|
|
210
|
+
box-sizing: border-box;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.sticky {
|
|
214
|
+
position: sticky;
|
|
215
|
+
left: 0px;
|
|
216
|
+
/* right: 100px; */
|
|
217
|
+
background-color: white;
|
|
218
|
+
z-index: 1;
|
|
219
|
+
&.border {
|
|
220
|
+
border-right: 1px solid hsla(0, 0%, 90%);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.headers > .column {
|
|
225
|
+
border-right: 1px solid hsla(0, 0%, 90%);
|
|
226
|
+
resize: horizontal;
|
|
227
|
+
overflow: hidden;
|
|
228
|
+
padding: var(--padding-y) 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.table {
|
|
232
|
+
--panel: 250px;
|
|
233
|
+
--padding-x: 1rem;
|
|
234
|
+
--padding-y: .5rem;
|
|
235
|
+
--gap: .25rem;
|
|
236
|
+
--header-height: 2.5rem;
|
|
237
|
+
|
|
238
|
+
display: grid;
|
|
239
|
+
|
|
240
|
+
grid-template-areas:
|
|
241
|
+
"headers panel"
|
|
242
|
+
"rows panel"
|
|
243
|
+
"statusbar panel"
|
|
244
|
+
;
|
|
245
|
+
|
|
246
|
+
grid-template-columns: auto min-content;
|
|
247
|
+
grid-template-rows: auto 1fr auto;
|
|
248
|
+
|
|
249
|
+
border: 1px solid hsla(0, 0%, 90%);
|
|
250
|
+
border-radius: .25rem;
|
|
251
|
+
|
|
252
|
+
max-height: 100%;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.headers {
|
|
256
|
+
grid-area: headers;
|
|
257
|
+
z-index: 2;
|
|
258
|
+
overflow: hidden;
|
|
259
|
+
padding-right: 1rem;
|
|
260
|
+
> .column {
|
|
261
|
+
width: auto !important;
|
|
262
|
+
background-color: white;
|
|
263
|
+
border-bottom: 1px solid hsla(0, 0%, 90%);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.rows {
|
|
268
|
+
grid-area: rows;
|
|
269
|
+
display: grid;
|
|
270
|
+
overflow: auto;
|
|
271
|
+
scrollbar-width: thin;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.statusbar {
|
|
275
|
+
grid-area: statusbar;
|
|
276
|
+
overflow: hidden;
|
|
277
|
+
padding-right: 1rem;
|
|
278
|
+
> .column {
|
|
279
|
+
background-color: hsla(0, 0%, 99%);
|
|
280
|
+
border-top: 1px solid hsla(0, 0%, 90%);
|
|
281
|
+
padding: calc(var(--padding-y) / 2) 0;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.headers, .row, .statusbar {
|
|
286
|
+
display: grid;
|
|
287
|
+
width: 100%;
|
|
288
|
+
height: 100%;
|
|
289
|
+
|
|
290
|
+
& > .column {
|
|
291
|
+
display: flex;
|
|
292
|
+
padding-left: var(--padding-x);
|
|
293
|
+
overflow: hidden;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
& > *:last-child {
|
|
297
|
+
width: 100%;
|
|
298
|
+
padding-right: var(--padding-x);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.row:nth-child(1) > * {
|
|
303
|
+
padding-top: calc(var(--padding-y) + var(--gap));
|
|
304
|
+
}
|
|
305
|
+
.row:nth-last-child(1) > * {
|
|
306
|
+
padding-bottom: calc(var(--padding-y) + var(--gap));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.row > * {
|
|
310
|
+
padding: var(--gap) 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.panel {
|
|
314
|
+
position: relative;
|
|
315
|
+
grid-area: panel;
|
|
316
|
+
width: var(--panel);
|
|
317
|
+
height: 100%;
|
|
318
|
+
background-color: white;
|
|
319
|
+
|
|
320
|
+
border-left: 1px solid hsla(0, 0%, 90%);
|
|
321
|
+
|
|
322
|
+
> .panel-content {
|
|
323
|
+
position: absolute;
|
|
324
|
+
top: 0;
|
|
325
|
+
right: 0;
|
|
326
|
+
width: min-content;
|
|
327
|
+
overflow: hidden;
|
|
328
|
+
padding: var(--padding-y) var(--padding-x);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.statusbar {
|
|
333
|
+
grid-area: statusbar;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
</style>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface ColumnOptions {
|
|
2
|
+
sticky(): ColumnOptions;
|
|
3
|
+
}
|
|
4
|
+
interface Column<T = unknown> {
|
|
5
|
+
header?: Snippet<[options: ColumnOptions]>;
|
|
6
|
+
row?: Snippet<[data: T]>;
|
|
7
|
+
statusbar?: Snippet<[data: T[]]>;
|
|
8
|
+
defaults?: {
|
|
9
|
+
sticky?: boolean;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export interface TableState<T = unknown> {
|
|
13
|
+
columns: Record<string, Column<T>>;
|
|
14
|
+
order: {
|
|
15
|
+
sticky: string[];
|
|
16
|
+
scroll: string[];
|
|
17
|
+
};
|
|
18
|
+
panels: Record<string, Snippet<[table: TableState]>>;
|
|
19
|
+
readonly data: T[];
|
|
20
|
+
updateColumn(key: string, options: Column<T>): void;
|
|
21
|
+
}
|
|
22
|
+
export declare function getTableState<T>(): TableState<T>;
|
|
23
|
+
import { type Snippet } from 'svelte';
|
|
24
|
+
declare class __sveltets_Render<T extends Record<PropertyKey, unknown>> {
|
|
25
|
+
props(): {
|
|
26
|
+
children?: Snippet;
|
|
27
|
+
data: T[];
|
|
28
|
+
panel?: string;
|
|
29
|
+
};
|
|
30
|
+
events(): {};
|
|
31
|
+
slots(): {};
|
|
32
|
+
bindings(): "panel";
|
|
33
|
+
exports(): {};
|
|
34
|
+
}
|
|
35
|
+
interface $$IsomorphicComponent {
|
|
36
|
+
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']>> & {
|
|
37
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
38
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
39
|
+
<T extends Record<PropertyKey, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
40
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* This is a description, \
|
|
44
|
+
* on how to use this.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* <Component />
|
|
48
|
+
*/
|
|
49
|
+
declare const Table: $$IsomorphicComponent;
|
|
50
|
+
type Table<T extends Record<PropertyKey, unknown>> = InstanceType<typeof Table<T>>;
|
|
51
|
+
export default Table;
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "svelte-tably",
|
|
3
|
+
"version": "1.0.0-next.0",
|
|
4
|
+
"devDependencies": {
|
|
5
|
+
"@sveltejs/adapter-auto": "^3.0.0",
|
|
6
|
+
"@sveltejs/kit": "^2.9.0",
|
|
7
|
+
"@sveltejs/package": "^2.0.0",
|
|
8
|
+
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
|
9
|
+
"publint": "^0.2.0",
|
|
10
|
+
"svelte": "^5.0.0",
|
|
11
|
+
"svelte-check": "^4.0.0",
|
|
12
|
+
"typescript": "^5.0.0",
|
|
13
|
+
"vite": "^6.0.0"
|
|
14
|
+
},
|
|
15
|
+
"peerDependencies": {
|
|
16
|
+
"svelte": "^5.0.0"
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"svelte": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"!dist/**/*.test.*",
|
|
27
|
+
"!dist/**/*.spec.*"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"dev": "vite dev",
|
|
31
|
+
"build": "vite build && npm run package",
|
|
32
|
+
"preview": "vite preview",
|
|
33
|
+
"package": "svelte-kit sync && svelte-package && publint",
|
|
34
|
+
"prepublishOnly": "npm run package",
|
|
35
|
+
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
|
36
|
+
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
|
|
37
|
+
},
|
|
38
|
+
"sideEffects": [
|
|
39
|
+
"**/*.css"
|
|
40
|
+
],
|
|
41
|
+
"svelte": "./dist/index.js",
|
|
42
|
+
"type": "module",
|
|
43
|
+
"types": "./dist/index.d.ts",
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@faker-js/faker": "^9.3.0"
|
|
46
|
+
}
|
|
47
|
+
}
|