@beeblock/svelar-datatable 0.1.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 +149 -0
- package/dist/SvelarDatatablePlugin.d.ts +13 -0
- package/dist/export/ExportManager.d.ts +4 -0
- package/dist/export/clipboard.d.ts +2 -0
- package/dist/export/csv.d.ts +4 -0
- package/dist/export/excel.d.ts +2 -0
- package/dist/export/index.d.ts +6 -0
- package/dist/export/pdf.d.ts +2 -0
- package/dist/export/print.d.ts +2 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +19 -0
- package/dist/server/DataTableController.d.ts +4 -0
- package/dist/server/DataTableRequest.d.ts +3 -0
- package/dist/server/DataTableService.d.ts +25 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +1 -0
- package/dist/state/DataTableStore.d.ts +64 -0
- package/dist/state/ServerDataTableStore.d.ts +23 -0
- package/dist/state/index.d.ts +2 -0
- package/dist/types.d.ts +208 -0
- package/dist/types.js +0 -0
- package/dist/ui/index.d.ts +20 -0
- package/package.json +45 -0
- package/src/ui/DataTable.svelte +385 -0
- package/src/ui/DataTableBody.svelte +180 -0
- package/src/ui/DataTableBubbleEditor.svelte +93 -0
- package/src/ui/DataTableButtons.svelte +139 -0
- package/src/ui/DataTableCell.svelte +381 -0
- package/src/ui/DataTableColumnToggle.svelte +111 -0
- package/src/ui/DataTableEditor.svelte +27 -0
- package/src/ui/DataTableEditorField.svelte +190 -0
- package/src/ui/DataTableEditorForm.svelte +94 -0
- package/src/ui/DataTableEmpty.svelte +40 -0
- package/src/ui/DataTableExpandedRow.svelte +37 -0
- package/src/ui/DataTableFooter.svelte +65 -0
- package/src/ui/DataTableHead.svelte +169 -0
- package/src/ui/DataTableLoading.svelte +44 -0
- package/src/ui/DataTableModalEditor.svelte +126 -0
- package/src/ui/DataTablePagination.svelte +205 -0
- package/src/ui/DataTableRow.svelte +192 -0
- package/src/ui/DataTableSearch.svelte +95 -0
- package/src/ui/DataTableToolbar.svelte +164 -0
- package/src/ui/index.ts +20 -0
- package/src/ui/virtual/VirtualScroller.svelte +62 -0
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@beeblock/svelar-datatable",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Full-featured DataTable plugin for Svelar — sorting, searching, pagination, inline editing, export, and server-side processing",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": ["svelar-plugin", "datatable", "svelte", "sveltekit", "data-grid"],
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
"./package.json": "./package.json",
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./server": {
|
|
17
|
+
"types": "./dist/server/index.d.ts",
|
|
18
|
+
"default": "./dist/server/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./types": {
|
|
21
|
+
"types": "./dist/types.d.ts",
|
|
22
|
+
"default": "./dist/types.js"
|
|
23
|
+
},
|
|
24
|
+
"./ui": "./src/ui/index.ts",
|
|
25
|
+
"./ui/*": "./src/ui/*"
|
|
26
|
+
},
|
|
27
|
+
"files": ["dist", "src/ui", "LICENSE"],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsup && (tsc --emitDeclarationOnly || echo 'Warning: tsc declaration emit had errors')",
|
|
30
|
+
"dev": "tsup --watch"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@beeblock/svelar": ">=0.4.0",
|
|
34
|
+
"svelte": "^5.0.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependenciesMeta": {
|
|
37
|
+
"exceljs": { "optional": true },
|
|
38
|
+
"@beeblock/svelar": { "optional": false }
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"tsup": "^8.5.0",
|
|
42
|
+
"typescript": "^5.7.0",
|
|
43
|
+
"svelte": "^5.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { DataTableConfig, DataTableClassNames, ColumnDef, EditorFieldDef } from '../types.js';
|
|
4
|
+
import { DataTableStore } from '../state/DataTableStore.js';
|
|
5
|
+
import { ServerDataTableStore } from '../state/ServerDataTableStore.js';
|
|
6
|
+
import DataTableToolbar from './DataTableToolbar.svelte';
|
|
7
|
+
import DataTableHead from './DataTableHead.svelte';
|
|
8
|
+
import DataTableBody from './DataTableBody.svelte';
|
|
9
|
+
import DataTableFooter from './DataTableFooter.svelte';
|
|
10
|
+
import DataTablePagination from './DataTablePagination.svelte';
|
|
11
|
+
import DataTableEditor from './DataTableEditor.svelte';
|
|
12
|
+
import { onMount } from 'svelte';
|
|
13
|
+
|
|
14
|
+
interface Props extends DataTableConfig {
|
|
15
|
+
// Snippet overrides
|
|
16
|
+
customCell?: Snippet<[{ row: any; column: ColumnDef; value: any }]>;
|
|
17
|
+
expandContent?: Snippet<[{ row: any }]>;
|
|
18
|
+
// Expose store for external control (e.g. opening editor)
|
|
19
|
+
storeRef?: DataTableStore;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
// Data source
|
|
24
|
+
data,
|
|
25
|
+
serverUrl,
|
|
26
|
+
serverMethod,
|
|
27
|
+
// Columns
|
|
28
|
+
columns,
|
|
29
|
+
// Features
|
|
30
|
+
sortable = true,
|
|
31
|
+
searchable = true,
|
|
32
|
+
paginate = true,
|
|
33
|
+
selectable = 'none',
|
|
34
|
+
// Pagination
|
|
35
|
+
perPage = 15,
|
|
36
|
+
perPageOptions = [10, 15, 25, 50, 100],
|
|
37
|
+
// Search
|
|
38
|
+
searchDebounceMs = 300,
|
|
39
|
+
// State persistence
|
|
40
|
+
stateSaveKey,
|
|
41
|
+
// Row identity
|
|
42
|
+
rowId = 'id',
|
|
43
|
+
// Row appearance
|
|
44
|
+
rowClass,
|
|
45
|
+
// Buttons
|
|
46
|
+
buttons,
|
|
47
|
+
// Editor
|
|
48
|
+
editorMode,
|
|
49
|
+
editorFields,
|
|
50
|
+
// Callbacks
|
|
51
|
+
onSort,
|
|
52
|
+
onFilter,
|
|
53
|
+
onPageChange,
|
|
54
|
+
onSelect,
|
|
55
|
+
onRowClick,
|
|
56
|
+
onEdit,
|
|
57
|
+
onCellEdit,
|
|
58
|
+
onCreate,
|
|
59
|
+
onDelete,
|
|
60
|
+
// Virtual scroll
|
|
61
|
+
virtualScroll = false,
|
|
62
|
+
virtualRowHeight = 48,
|
|
63
|
+
// Features
|
|
64
|
+
responsive = true,
|
|
65
|
+
groupBy,
|
|
66
|
+
expandable = false,
|
|
67
|
+
// Text
|
|
68
|
+
emptyText = 'No data available',
|
|
69
|
+
loadingText = 'Loading...',
|
|
70
|
+
// CSS
|
|
71
|
+
className = '',
|
|
72
|
+
compact = false,
|
|
73
|
+
striped = true,
|
|
74
|
+
hover = true,
|
|
75
|
+
bordered = false,
|
|
76
|
+
classNames = {} as DataTableClassNames,
|
|
77
|
+
unstyled = false,
|
|
78
|
+
// Snippets
|
|
79
|
+
customCell,
|
|
80
|
+
expandContent,
|
|
81
|
+
// Store ref
|
|
82
|
+
storeRef = $bindable(),
|
|
83
|
+
}: Props = $props();
|
|
84
|
+
|
|
85
|
+
// Build config object
|
|
86
|
+
let config: DataTableConfig = $derived({
|
|
87
|
+
data, serverUrl, serverMethod, columns, sortable, searchable, paginate,
|
|
88
|
+
selectable, perPage, perPageOptions, searchDebounceMs, stateSaveKey,
|
|
89
|
+
rowId, rowClass, buttons, editorMode, editorFields,
|
|
90
|
+
onSort, onFilter, onPageChange, onSelect, onRowClick, onEdit, onCellEdit, onCreate, onDelete,
|
|
91
|
+
virtualScroll, virtualRowHeight, responsive, groupBy, expandable,
|
|
92
|
+
emptyText, loadingText, className, compact, striped, hover, bordered, classNames, unstyled,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Create store
|
|
96
|
+
let store: DataTableStore = $state(
|
|
97
|
+
serverUrl
|
|
98
|
+
? new ServerDataTableStore(config)
|
|
99
|
+
: new DataTableStore(config)
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
// Expose store ref for external control
|
|
103
|
+
$effect(() => {
|
|
104
|
+
storeRef = store;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
let state = $state(store.getState());
|
|
108
|
+
|
|
109
|
+
// Subscribe to store changes
|
|
110
|
+
$effect(() => {
|
|
111
|
+
return store.subscribe(() => {
|
|
112
|
+
state = store.getState();
|
|
113
|
+
|
|
114
|
+
// Fire callbacks
|
|
115
|
+
if (onSort) onSort(state.sort);
|
|
116
|
+
if (onFilter) onFilter(state.filters);
|
|
117
|
+
if (onSelect) onSelect(store.getSelectedRows());
|
|
118
|
+
if (onPageChange) onPageChange(state.pagination.page, state.pagination.perPage);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Update data when prop changes (client-side only)
|
|
123
|
+
$effect(() => {
|
|
124
|
+
if (data && !serverUrl) {
|
|
125
|
+
store.setData(data);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Initial server fetch
|
|
130
|
+
onMount(() => {
|
|
131
|
+
if (serverUrl && store instanceof ServerDataTableStore) {
|
|
132
|
+
store.initialFetch();
|
|
133
|
+
}
|
|
134
|
+
return () => {
|
|
135
|
+
if (store instanceof ServerDataTableStore) {
|
|
136
|
+
store.destroy();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Editor submit handler
|
|
142
|
+
async function handleEditorSubmit(formData: Record<string, any>) {
|
|
143
|
+
const isCreate = state.editingRowId === null;
|
|
144
|
+
try {
|
|
145
|
+
if (isCreate && onCreate) {
|
|
146
|
+
await onCreate(formData);
|
|
147
|
+
} else if (!isCreate && onEdit) {
|
|
148
|
+
const row = state.allRows.find((r) => store.getRowId(r) === state.editingRowId);
|
|
149
|
+
if (row) await onEdit(row, formData);
|
|
150
|
+
}
|
|
151
|
+
store.closeEditor();
|
|
152
|
+
} catch (err: any) {
|
|
153
|
+
if (err.errors) {
|
|
154
|
+
store.setValidationErrors(err.errors);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Inline mode: handle cell edit commit
|
|
160
|
+
async function handleInlineCommit(row: any, columnKey: string, newValue: any, oldValue: any) {
|
|
161
|
+
try {
|
|
162
|
+
if (onCellEdit) {
|
|
163
|
+
await onCellEdit(row, columnKey, newValue, oldValue);
|
|
164
|
+
}
|
|
165
|
+
// Update local data
|
|
166
|
+
(row as any)[columnKey] = newValue;
|
|
167
|
+
} catch (err: any) {
|
|
168
|
+
// Revert on error
|
|
169
|
+
(row as any)[columnKey] = oldValue;
|
|
170
|
+
store.setError(err.message ?? 'Failed to save cell');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Excel mode: commit cell edit and fire callback
|
|
175
|
+
async function handleExcelCommit() {
|
|
176
|
+
const result = store.commitCellEdit();
|
|
177
|
+
if (!result) return;
|
|
178
|
+
try {
|
|
179
|
+
if (onCellEdit) {
|
|
180
|
+
await onCellEdit(result.row, result.columnKey, result.newValue, result.oldValue);
|
|
181
|
+
}
|
|
182
|
+
} catch (err: any) {
|
|
183
|
+
// Revert on error
|
|
184
|
+
(result.row as any)[result.columnKey] = result.oldValue;
|
|
185
|
+
store.setError(err.message ?? 'Failed to save cell');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Excel mode: navigate with cross-page support
|
|
190
|
+
function handleExcelNavigate(direction: 'up' | 'down' | 'left' | 'right') {
|
|
191
|
+
// Commit any pending edit first
|
|
192
|
+
const result = store.commitCellEdit();
|
|
193
|
+
if (result && onCellEdit) {
|
|
194
|
+
onCellEdit(result.row, result.columnKey, result.newValue, result.oldValue).catch(() => {
|
|
195
|
+
(result.row as any)[result.columnKey] = result.oldValue;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const pageResult = store.navigateCell(direction);
|
|
200
|
+
if (!pageResult) return;
|
|
201
|
+
|
|
202
|
+
const pagination = store.getState().pagination;
|
|
203
|
+
const visibleCols = store.getVisibleColumns();
|
|
204
|
+
const firstEditableCol = visibleCols.find((c) => c.editable !== false);
|
|
205
|
+
if (!firstEditableCol) return;
|
|
206
|
+
|
|
207
|
+
if (pageResult === 'page-next' && pagination.page < pagination.lastPage) {
|
|
208
|
+
store.setPage(pagination.page + 1);
|
|
209
|
+
// Focus first row of new page after recompute
|
|
210
|
+
setTimeout(() => store.focusCell(0, store.getState().excelFocusedCell?.columnKey ?? firstEditableCol.key), 0);
|
|
211
|
+
} else if (pageResult === 'page-prev' && pagination.page > 1) {
|
|
212
|
+
store.setPage(pagination.page - 1);
|
|
213
|
+
// Focus last row of previous page after recompute
|
|
214
|
+
setTimeout(() => {
|
|
215
|
+
const rows = store.getState().paginatedRows;
|
|
216
|
+
const colKey = store.getState().excelFocusedCell?.columnKey ?? firstEditableCol.key;
|
|
217
|
+
store.focusCell(rows.length - 1, colKey);
|
|
218
|
+
}, 0);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Bubble editor anchor tracking
|
|
223
|
+
let bubbleAnchor: HTMLElement | null = $state(null);
|
|
224
|
+
|
|
225
|
+
// Virtual scroll tracking
|
|
226
|
+
let wrapperEl: HTMLDivElement | undefined = $state();
|
|
227
|
+
let scrollTop = $state(0);
|
|
228
|
+
let wrapperHeight = $state(400);
|
|
229
|
+
|
|
230
|
+
function handleWrapperScroll(e: Event) {
|
|
231
|
+
scrollTop = (e.target as HTMLDivElement).scrollTop;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Table CSS classes
|
|
235
|
+
let tableClasses = $derived([
|
|
236
|
+
'sdt-table',
|
|
237
|
+
compact ? 'sdt-compact' : '',
|
|
238
|
+
striped ? 'sdt-striped' : '',
|
|
239
|
+
hover ? 'sdt-hover' : '',
|
|
240
|
+
bordered ? 'sdt-bordered' : '',
|
|
241
|
+
responsive ? 'sdt-responsive' : '',
|
|
242
|
+
editorMode === 'excel' ? 'sdt-excel' : '',
|
|
243
|
+
className,
|
|
244
|
+
].filter(Boolean).join(' '));
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
</script>
|
|
248
|
+
|
|
249
|
+
<div class="{unstyled ? '' : 'sdt-container'} {classNames.container ?? ''} {className}">
|
|
250
|
+
<DataTableToolbar
|
|
251
|
+
{store}
|
|
252
|
+
{columns}
|
|
253
|
+
{searchable}
|
|
254
|
+
{searchDebounceMs}
|
|
255
|
+
{buttons}
|
|
256
|
+
{editorMode}
|
|
257
|
+
{classNames}
|
|
258
|
+
onDeleteClick={onDelete}
|
|
259
|
+
/>
|
|
260
|
+
|
|
261
|
+
{#if state.error}
|
|
262
|
+
<div class="sdt-error">
|
|
263
|
+
<span>{state.error}</span>
|
|
264
|
+
<button type="button" onclick={() => store.setError(null)}>Dismiss</button>
|
|
265
|
+
</div>
|
|
266
|
+
{/if}
|
|
267
|
+
|
|
268
|
+
<div
|
|
269
|
+
bind:this={wrapperEl}
|
|
270
|
+
class="{unstyled ? '' : 'sdt-table-wrapper'} {classNames.table ?? ''}"
|
|
271
|
+
class:sdt-responsive={!unstyled && responsive}
|
|
272
|
+
class:sdt-virtual-wrapper={virtualScroll}
|
|
273
|
+
style:max-height={virtualScroll ? '500px' : undefined}
|
|
274
|
+
onscroll={virtualScroll ? handleWrapperScroll : undefined}
|
|
275
|
+
>
|
|
276
|
+
<table class="{unstyled ? '' : tableClasses}">
|
|
277
|
+
<DataTableHead {columns} {store} {selectable} {sortable} {expandable} {classNames} />
|
|
278
|
+
<DataTableBody
|
|
279
|
+
{columns}
|
|
280
|
+
{store}
|
|
281
|
+
{selectable}
|
|
282
|
+
{expandable}
|
|
283
|
+
{editorMode}
|
|
284
|
+
{emptyText}
|
|
285
|
+
{loadingText}
|
|
286
|
+
{rowClass}
|
|
287
|
+
{groupBy}
|
|
288
|
+
{classNames}
|
|
289
|
+
{onRowClick}
|
|
290
|
+
{customCell}
|
|
291
|
+
{expandContent}
|
|
292
|
+
onExcelNavigate={editorMode === 'excel' ? handleExcelNavigate : undefined}
|
|
293
|
+
onExcelCommit={editorMode === 'excel' ? handleExcelCommit : undefined}
|
|
294
|
+
onInlineCommit={editorMode === 'inline' ? handleInlineCommit : undefined}
|
|
295
|
+
onBubbleAnchor={editorMode === 'bubble' ? (el) => { bubbleAnchor = el; } : undefined}
|
|
296
|
+
{virtualScroll}
|
|
297
|
+
{virtualRowHeight}
|
|
298
|
+
{scrollTop}
|
|
299
|
+
containerHeight={500}
|
|
300
|
+
/>
|
|
301
|
+
<DataTableFooter {columns} {store} {selectable} {expandable} {classNames} />
|
|
302
|
+
</table>
|
|
303
|
+
</div>
|
|
304
|
+
|
|
305
|
+
{#if paginate}
|
|
306
|
+
<DataTablePagination {store} {perPageOptions} {classNames} />
|
|
307
|
+
{/if}
|
|
308
|
+
|
|
309
|
+
{#if editorMode && editorMode !== 'excel' && editorFields}
|
|
310
|
+
<DataTableEditor
|
|
311
|
+
fields={editorFields}
|
|
312
|
+
{store}
|
|
313
|
+
{classNames}
|
|
314
|
+
onsubmit={handleEditorSubmit}
|
|
315
|
+
anchorEl={bubbleAnchor}
|
|
316
|
+
/>
|
|
317
|
+
{/if}
|
|
318
|
+
</div>
|
|
319
|
+
|
|
320
|
+
<style>
|
|
321
|
+
.sdt-container {
|
|
322
|
+
background: var(--sdt-bg, #fff);
|
|
323
|
+
border: 1px solid var(--sdt-border, #e5e7eb);
|
|
324
|
+
border-radius: 0.5rem;
|
|
325
|
+
font-family: var(--sdt-font, inherit);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.sdt-table-wrapper {
|
|
329
|
+
overflow-x: auto;
|
|
330
|
+
}
|
|
331
|
+
.sdt-table-wrapper.sdt-responsive {
|
|
332
|
+
overflow-x: auto;
|
|
333
|
+
-webkit-overflow-scrolling: touch;
|
|
334
|
+
}
|
|
335
|
+
.sdt-virtual-wrapper {
|
|
336
|
+
overflow-y: auto;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.sdt-table {
|
|
340
|
+
width: 100%;
|
|
341
|
+
border-collapse: collapse;
|
|
342
|
+
table-layout: auto;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.sdt-table :global(.sdt-compact .sdt-cell),
|
|
346
|
+
.sdt-table :global(.sdt-compact .sdt-th) {
|
|
347
|
+
padding: 0.375rem 0.5rem;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.sdt-error {
|
|
351
|
+
display: flex;
|
|
352
|
+
align-items: center;
|
|
353
|
+
justify-content: space-between;
|
|
354
|
+
padding: 0.625rem 1rem;
|
|
355
|
+
background: #fef2f2;
|
|
356
|
+
border-bottom: 1px solid #fecaca;
|
|
357
|
+
color: #dc2626;
|
|
358
|
+
font-size: 0.8125rem;
|
|
359
|
+
}
|
|
360
|
+
.sdt-error button {
|
|
361
|
+
background: none;
|
|
362
|
+
border: none;
|
|
363
|
+
color: #dc2626;
|
|
364
|
+
cursor: pointer;
|
|
365
|
+
text-decoration: underline;
|
|
366
|
+
font-size: 0.8125rem;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/* Stripe via CSS variables so DataTableRow can use them */
|
|
370
|
+
:global(.sdt-striped .sdt-row-even) {
|
|
371
|
+
--sdt-row-stripe: rgba(0, 0, 0, 0.02);
|
|
372
|
+
background-color: var(--sdt-row-stripe);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
:global(.sdt-hover .sdt-row:hover) {
|
|
376
|
+
--sdt-row-hover: #f9fafb;
|
|
377
|
+
background-color: var(--sdt-row-hover) !important;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
:global(.sdt-bordered .sdt-cell),
|
|
381
|
+
:global(.sdt-bordered .sdt-th),
|
|
382
|
+
:global(.sdt-bordered .sdt-tf) {
|
|
383
|
+
border: 1px solid var(--sdt-border, #e5e7eb);
|
|
384
|
+
}
|
|
385
|
+
</style>
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ColumnDef, DataTableStore, SelectionMode, EditorMode, DataTableClassNames } from '../index.js';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
import DataTableRow from './DataTableRow.svelte';
|
|
5
|
+
import DataTableLoading from './DataTableLoading.svelte';
|
|
6
|
+
import DataTableEmpty from './DataTableEmpty.svelte';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
columns: ColumnDef[];
|
|
10
|
+
store: DataTableStore;
|
|
11
|
+
selectable?: SelectionMode;
|
|
12
|
+
expandable?: boolean;
|
|
13
|
+
editorMode?: EditorMode;
|
|
14
|
+
emptyText?: string;
|
|
15
|
+
loadingText?: string;
|
|
16
|
+
rowClass?: string | ((row: any, index: number) => string);
|
|
17
|
+
onRowClick?: (row: any, event: MouseEvent) => void;
|
|
18
|
+
customCell?: Snippet<[{ row: any; column: ColumnDef; value: any }]>;
|
|
19
|
+
expandContent?: Snippet<[{ row: any }]>;
|
|
20
|
+
onExcelNavigate?: (direction: 'up' | 'down' | 'left' | 'right') => void;
|
|
21
|
+
onExcelCommit?: () => void;
|
|
22
|
+
onInlineCommit?: (row: any, columnKey: string, newValue: any, oldValue: any) => void;
|
|
23
|
+
groupBy?: string;
|
|
24
|
+
onBubbleAnchor?: (el: HTMLElement) => void;
|
|
25
|
+
classNames?: DataTableClassNames;
|
|
26
|
+
virtualScroll?: boolean;
|
|
27
|
+
virtualRowHeight?: number;
|
|
28
|
+
scrollTop?: number;
|
|
29
|
+
containerHeight?: number;
|
|
30
|
+
}
|
|
31
|
+
let {
|
|
32
|
+
columns,
|
|
33
|
+
store,
|
|
34
|
+
selectable = 'none',
|
|
35
|
+
expandable = false,
|
|
36
|
+
editorMode,
|
|
37
|
+
emptyText = 'No data available',
|
|
38
|
+
loadingText = 'Loading...',
|
|
39
|
+
rowClass,
|
|
40
|
+
onRowClick,
|
|
41
|
+
customCell,
|
|
42
|
+
expandContent,
|
|
43
|
+
onExcelNavigate,
|
|
44
|
+
onExcelCommit,
|
|
45
|
+
onInlineCommit,
|
|
46
|
+
groupBy,
|
|
47
|
+
onBubbleAnchor,
|
|
48
|
+
classNames = {},
|
|
49
|
+
virtualScroll = false,
|
|
50
|
+
virtualRowHeight = 48,
|
|
51
|
+
scrollTop = 0,
|
|
52
|
+
containerHeight = 500,
|
|
53
|
+
}: Props = $props();
|
|
54
|
+
|
|
55
|
+
let state = $state(store.getState());
|
|
56
|
+
$effect(() => {
|
|
57
|
+
return store.subscribe(() => {
|
|
58
|
+
state = store.getState();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
let visibleColCount = $derived(
|
|
63
|
+
state.columnOrder.filter((key) => state.columnVisibility[key] !== false).length
|
|
64
|
+
+ (selectable !== 'none' ? 1 : 0)
|
|
65
|
+
+ (expandable ? 1 : 0)
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// Group rows by column value when groupBy is set
|
|
69
|
+
let groupedRows = $derived(() => {
|
|
70
|
+
if (!groupBy) return null;
|
|
71
|
+
const groups: { key: string; rows: any[] }[] = [];
|
|
72
|
+
const map = new Map<string, any[]>();
|
|
73
|
+
for (const row of state.paginatedRows) {
|
|
74
|
+
const key = String((row as any)[groupBy] ?? 'Other');
|
|
75
|
+
if (!map.has(key)) {
|
|
76
|
+
const arr: any[] = [];
|
|
77
|
+
map.set(key, arr);
|
|
78
|
+
groups.push({ key, rows: arr });
|
|
79
|
+
}
|
|
80
|
+
map.get(key)!.push(row);
|
|
81
|
+
}
|
|
82
|
+
return groups;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Virtual scroll: compute visible row range
|
|
86
|
+
let overscan = 5;
|
|
87
|
+
let virtualVisibleStart = $derived(
|
|
88
|
+
virtualScroll ? Math.max(0, Math.floor(scrollTop / virtualRowHeight) - overscan) : 0
|
|
89
|
+
);
|
|
90
|
+
let virtualVisibleEnd = $derived(
|
|
91
|
+
virtualScroll
|
|
92
|
+
? Math.min(state.paginatedRows.length, Math.ceil((scrollTop + containerHeight) / virtualRowHeight) + overscan)
|
|
93
|
+
: state.paginatedRows.length
|
|
94
|
+
);
|
|
95
|
+
let virtualTopPad = $derived(virtualScroll ? virtualVisibleStart * virtualRowHeight : 0);
|
|
96
|
+
let virtualBottomPad = $derived(
|
|
97
|
+
virtualScroll ? Math.max(0, (state.paginatedRows.length - virtualVisibleEnd) * virtualRowHeight) : 0
|
|
98
|
+
);
|
|
99
|
+
let virtualRows = $derived(
|
|
100
|
+
virtualScroll
|
|
101
|
+
? state.paginatedRows.slice(virtualVisibleStart, virtualVisibleEnd)
|
|
102
|
+
: state.paginatedRows
|
|
103
|
+
);
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<tbody class="sdt-tbody {classNames.tbody ?? ''}">
|
|
107
|
+
{#if state.loading}
|
|
108
|
+
<DataTableLoading text={loadingText} colSpan={visibleColCount} />
|
|
109
|
+
{:else if state.paginatedRows.length === 0}
|
|
110
|
+
<DataTableEmpty text={emptyText} colSpan={visibleColCount} />
|
|
111
|
+
{:else if groupBy && groupedRows()}
|
|
112
|
+
{#each groupedRows()! as group}
|
|
113
|
+
<tr class="sdt-group-header">
|
|
114
|
+
<td colspan={visibleColCount} class="sdt-group-cell">{group.key}</td>
|
|
115
|
+
</tr>
|
|
116
|
+
{#each group.rows as row, index (store.getRowId(row))}
|
|
117
|
+
<DataTableRow
|
|
118
|
+
{row}
|
|
119
|
+
{index}
|
|
120
|
+
{columns}
|
|
121
|
+
{store}
|
|
122
|
+
{selectable}
|
|
123
|
+
{expandable}
|
|
124
|
+
{editorMode}
|
|
125
|
+
{rowClass}
|
|
126
|
+
{classNames}
|
|
127
|
+
{onRowClick}
|
|
128
|
+
{customCell}
|
|
129
|
+
{expandContent}
|
|
130
|
+
{onExcelNavigate}
|
|
131
|
+
{onExcelCommit}
|
|
132
|
+
{onInlineCommit}
|
|
133
|
+
{onBubbleAnchor}
|
|
134
|
+
/>
|
|
135
|
+
{/each}
|
|
136
|
+
{/each}
|
|
137
|
+
{:else}
|
|
138
|
+
{#if virtualScroll && virtualTopPad > 0}
|
|
139
|
+
<tr style="height: {virtualTopPad}px;"><td colspan={visibleColCount}></td></tr>
|
|
140
|
+
{/if}
|
|
141
|
+
{#each virtualRows as row, i (store.getRowId(row))}
|
|
142
|
+
{@const index = virtualScroll ? virtualVisibleStart + i : i}
|
|
143
|
+
<DataTableRow
|
|
144
|
+
{row}
|
|
145
|
+
{index}
|
|
146
|
+
{columns}
|
|
147
|
+
{store}
|
|
148
|
+
{selectable}
|
|
149
|
+
{expandable}
|
|
150
|
+
{editorMode}
|
|
151
|
+
{rowClass}
|
|
152
|
+
{classNames}
|
|
153
|
+
{onRowClick}
|
|
154
|
+
{customCell}
|
|
155
|
+
{expandContent}
|
|
156
|
+
{onExcelNavigate}
|
|
157
|
+
{onExcelCommit}
|
|
158
|
+
{onInlineCommit}
|
|
159
|
+
/>
|
|
160
|
+
{/each}
|
|
161
|
+
{#if virtualScroll && virtualBottomPad > 0}
|
|
162
|
+
<tr style="height: {virtualBottomPad}px;"><td colspan={visibleColCount}></td></tr>
|
|
163
|
+
{/if}
|
|
164
|
+
{/if}
|
|
165
|
+
</tbody>
|
|
166
|
+
|
|
167
|
+
<style>
|
|
168
|
+
.sdt-group-header {
|
|
169
|
+
background: var(--sdt-group-bg, #f3f4f6);
|
|
170
|
+
}
|
|
171
|
+
.sdt-group-cell {
|
|
172
|
+
padding: 0.5rem 0.75rem;
|
|
173
|
+
font-weight: 600;
|
|
174
|
+
font-size: 0.8125rem;
|
|
175
|
+
color: var(--sdt-text-muted, #6b7280);
|
|
176
|
+
text-transform: uppercase;
|
|
177
|
+
letter-spacing: 0.03em;
|
|
178
|
+
border-bottom: 1px solid var(--sdt-border, #e5e7eb);
|
|
179
|
+
}
|
|
180
|
+
</style>
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { EditorFieldDef, DataTableStore, DataTableClassNames } from '../index.js';
|
|
3
|
+
import DataTableEditorForm from './DataTableEditorForm.svelte';
|
|
4
|
+
import { tick } from 'svelte';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
fields: EditorFieldDef[];
|
|
8
|
+
store: DataTableStore;
|
|
9
|
+
anchorEl?: HTMLElement | null;
|
|
10
|
+
classNames?: DataTableClassNames;
|
|
11
|
+
onsubmit: (formData: Record<string, any>) => void | Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
let { fields, store, anchorEl = null, classNames = {}, onsubmit }: Props = $props();
|
|
14
|
+
|
|
15
|
+
let state = $state(store.getState());
|
|
16
|
+
$effect(() => {
|
|
17
|
+
return store.subscribe(() => {
|
|
18
|
+
state = store.getState();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
let isOpen = $derived(state.editorMode === 'bubble');
|
|
23
|
+
let popoverStyle = $state('');
|
|
24
|
+
|
|
25
|
+
// Get only the field for the column being edited
|
|
26
|
+
let activeFields = $derived(() => {
|
|
27
|
+
if (state.editingColumn) {
|
|
28
|
+
const field = fields.find((f) => f.name === state.editingColumn);
|
|
29
|
+
return field ? [field] : fields;
|
|
30
|
+
}
|
|
31
|
+
return fields;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Position the bubble editor relative to the editing row
|
|
35
|
+
$effect(() => {
|
|
36
|
+
if (!isOpen) return;
|
|
37
|
+
|
|
38
|
+
// Use anchorEl if provided, otherwise find the selected row in the DOM
|
|
39
|
+
async function position() {
|
|
40
|
+
await tick();
|
|
41
|
+
let el = anchorEl;
|
|
42
|
+
if (!el) {
|
|
43
|
+
el = document.querySelector('.sdt-row-selected') as HTMLElement | null;
|
|
44
|
+
}
|
|
45
|
+
if (el) {
|
|
46
|
+
const rect = el.getBoundingClientRect();
|
|
47
|
+
popoverStyle = `top: ${rect.bottom + 4}px; left: ${rect.left}px;`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
position();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function close() {
|
|
54
|
+
store.closeEditor();
|
|
55
|
+
}
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
{#if isOpen}
|
|
59
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
60
|
+
<div class="sdt-bubble-backdrop" onclick={close}></div>
|
|
61
|
+
<div class="sdt-bubble-editor" style={popoverStyle}>
|
|
62
|
+
<DataTableEditorForm
|
|
63
|
+
fields={activeFields()}
|
|
64
|
+
{store}
|
|
65
|
+
{onsubmit}
|
|
66
|
+
oncancel={close}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
{/if}
|
|
70
|
+
|
|
71
|
+
<style>
|
|
72
|
+
.sdt-bubble-backdrop {
|
|
73
|
+
position: fixed;
|
|
74
|
+
inset: 0;
|
|
75
|
+
z-index: 40;
|
|
76
|
+
}
|
|
77
|
+
.sdt-bubble-editor {
|
|
78
|
+
position: fixed;
|
|
79
|
+
z-index: 41;
|
|
80
|
+
background: var(--sdt-bg, #fff);
|
|
81
|
+
border: 1px solid var(--sdt-border, #e5e7eb);
|
|
82
|
+
border-radius: 0.375rem;
|
|
83
|
+
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
|
84
|
+
padding: 0.75rem;
|
|
85
|
+
min-width: 220px;
|
|
86
|
+
max-width: 320px;
|
|
87
|
+
animation: sdt-pop-in 0.15s ease;
|
|
88
|
+
}
|
|
89
|
+
@keyframes sdt-pop-in {
|
|
90
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
91
|
+
to { opacity: 1; transform: translateY(0); }
|
|
92
|
+
}
|
|
93
|
+
</style>
|