@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
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ButtonDef, ExportFormat, DataTableStore, ColumnDef } from '../index.js';
|
|
3
|
+
import { ExportManager } from '../export/ExportManager.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
buttons: (ButtonDef | ExportFormat)[];
|
|
7
|
+
store: DataTableStore;
|
|
8
|
+
columns: ColumnDef[];
|
|
9
|
+
}
|
|
10
|
+
let { buttons, store, columns }: Props = $props();
|
|
11
|
+
|
|
12
|
+
const exportManager = new ExportManager();
|
|
13
|
+
|
|
14
|
+
const exportLabels: Record<ExportFormat, string> = {
|
|
15
|
+
csv: 'CSV',
|
|
16
|
+
excel: 'Excel',
|
|
17
|
+
pdf: 'PDF',
|
|
18
|
+
clipboard: 'Copy',
|
|
19
|
+
print: 'Print',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function isExportFormat(btn: ButtonDef | ExportFormat): btn is ExportFormat {
|
|
23
|
+
return typeof btn === 'string';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function handleExport(format: ExportFormat) {
|
|
27
|
+
const state = store.getState();
|
|
28
|
+
const data = state.filteredRows;
|
|
29
|
+
const visibleCols = columns.filter((c) => state.columnVisibility[c.key] !== false);
|
|
30
|
+
await exportManager.export(format, data, visibleCols);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function handleAction(btn: ButtonDef) {
|
|
34
|
+
if (typeof btn.action === 'function') {
|
|
35
|
+
const selected = store.getSelectedRows();
|
|
36
|
+
const state = store.getState();
|
|
37
|
+
await btn.action(selected, state.allRows);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isDisabled(btn: ButtonDef): boolean {
|
|
42
|
+
if (typeof btn.disabled === 'function') {
|
|
43
|
+
return btn.disabled(store.getSelectedRows());
|
|
44
|
+
}
|
|
45
|
+
return btn.disabled ?? false;
|
|
46
|
+
}
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<div class="sdt-buttons">
|
|
50
|
+
{#each buttons as btn}
|
|
51
|
+
{#if isExportFormat(btn)}
|
|
52
|
+
<button type="button" class="sdt-btn sdt-btn-outline" onclick={() => handleExport(btn)}>
|
|
53
|
+
{exportLabels[btn]}
|
|
54
|
+
</button>
|
|
55
|
+
{:else if btn.collection}
|
|
56
|
+
<div class="sdt-btn-group">
|
|
57
|
+
<button type="button" class="sdt-btn sdt-btn-{btn.variant ?? 'default'}" disabled={isDisabled(btn)}>
|
|
58
|
+
{btn.label}
|
|
59
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="12" height="12">
|
|
60
|
+
<path d="m6 9 6 6 6-6" />
|
|
61
|
+
</svg>
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
{:else}
|
|
65
|
+
<button
|
|
66
|
+
type="button"
|
|
67
|
+
class="sdt-btn sdt-btn-{btn.variant ?? 'default'}"
|
|
68
|
+
disabled={isDisabled(btn)}
|
|
69
|
+
onclick={() => handleAction(btn)}
|
|
70
|
+
>
|
|
71
|
+
{btn.label}
|
|
72
|
+
</button>
|
|
73
|
+
{/if}
|
|
74
|
+
{/each}
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<style>
|
|
78
|
+
.sdt-buttons {
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
gap: 0.375rem;
|
|
82
|
+
flex-wrap: wrap;
|
|
83
|
+
}
|
|
84
|
+
.sdt-btn {
|
|
85
|
+
display: inline-flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
gap: 0.25rem;
|
|
88
|
+
padding: 0.5rem 0.75rem;
|
|
89
|
+
border-radius: 0.375rem;
|
|
90
|
+
font-size: 0.8125rem;
|
|
91
|
+
font-weight: 500;
|
|
92
|
+
cursor: pointer;
|
|
93
|
+
transition: background-color 0.15s, border-color 0.15s;
|
|
94
|
+
border: 1px solid transparent;
|
|
95
|
+
}
|
|
96
|
+
.sdt-btn:disabled {
|
|
97
|
+
opacity: 0.4;
|
|
98
|
+
cursor: not-allowed;
|
|
99
|
+
}
|
|
100
|
+
.sdt-btn-default {
|
|
101
|
+
background: var(--sdt-primary, #3b82f6);
|
|
102
|
+
color: #fff;
|
|
103
|
+
}
|
|
104
|
+
.sdt-btn-default:hover:not(:disabled) {
|
|
105
|
+
background: var(--sdt-primary-hover, #2563eb);
|
|
106
|
+
}
|
|
107
|
+
.sdt-btn-destructive {
|
|
108
|
+
background: #ef4444;
|
|
109
|
+
color: #fff;
|
|
110
|
+
}
|
|
111
|
+
.sdt-btn-destructive:hover:not(:disabled) {
|
|
112
|
+
background: #dc2626;
|
|
113
|
+
}
|
|
114
|
+
.sdt-btn-outline {
|
|
115
|
+
background: var(--sdt-input-bg, #fff);
|
|
116
|
+
border-color: var(--sdt-border, #e5e7eb);
|
|
117
|
+
color: var(--sdt-text, #111827);
|
|
118
|
+
}
|
|
119
|
+
.sdt-btn-outline:hover:not(:disabled) {
|
|
120
|
+
background: var(--sdt-hover, #f3f4f6);
|
|
121
|
+
}
|
|
122
|
+
.sdt-btn-secondary {
|
|
123
|
+
background: var(--sdt-hover, #f3f4f6);
|
|
124
|
+
color: var(--sdt-text, #111827);
|
|
125
|
+
}
|
|
126
|
+
.sdt-btn-secondary:hover:not(:disabled) {
|
|
127
|
+
background: #e5e7eb;
|
|
128
|
+
}
|
|
129
|
+
.sdt-btn-ghost {
|
|
130
|
+
background: transparent;
|
|
131
|
+
color: var(--sdt-text, #111827);
|
|
132
|
+
}
|
|
133
|
+
.sdt-btn-ghost:hover:not(:disabled) {
|
|
134
|
+
background: var(--sdt-hover, #f3f4f6);
|
|
135
|
+
}
|
|
136
|
+
.sdt-btn-group {
|
|
137
|
+
position: relative;
|
|
138
|
+
}
|
|
139
|
+
</style>
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ColumnDef, DataTableStore } from '../index.js';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
row: any;
|
|
7
|
+
rowIndex: number;
|
|
8
|
+
column: ColumnDef;
|
|
9
|
+
store: DataTableStore;
|
|
10
|
+
editorMode?: string | null;
|
|
11
|
+
customCell?: Snippet<[{ row: any; column: ColumnDef; value: any }]>;
|
|
12
|
+
onExcelNavigate?: (direction: 'up' | 'down' | 'left' | 'right') => void;
|
|
13
|
+
onExcelCommit?: () => void;
|
|
14
|
+
onInlineCommit?: (row: any, columnKey: string, newValue: any, oldValue: any) => void;
|
|
15
|
+
tdClass?: string;
|
|
16
|
+
}
|
|
17
|
+
let {
|
|
18
|
+
row, rowIndex, column, store, editorMode = null, customCell,
|
|
19
|
+
onExcelNavigate, onExcelCommit, onInlineCommit, tdClass = '',
|
|
20
|
+
}: Props = $props();
|
|
21
|
+
|
|
22
|
+
let value = $derived(row[column.key]);
|
|
23
|
+
|
|
24
|
+
let state = $state(store.getState());
|
|
25
|
+
$effect(() => {
|
|
26
|
+
return store.subscribe(() => {
|
|
27
|
+
state = store.getState();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
let isExcelMode = $derived(editorMode === 'excel');
|
|
32
|
+
let isFocused = $derived(
|
|
33
|
+
isExcelMode
|
|
34
|
+
&& state.excelFocusedCell?.rowIndex === rowIndex
|
|
35
|
+
&& state.excelFocusedCell?.columnKey === column.key
|
|
36
|
+
);
|
|
37
|
+
let isEditing = $derived(
|
|
38
|
+
isExcelMode
|
|
39
|
+
&& state.excelEditingCell?.rowIndex === rowIndex
|
|
40
|
+
&& state.excelEditingCell?.columnKey === column.key
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Inline editor state
|
|
44
|
+
let isInlineEditing = $derived(
|
|
45
|
+
editorMode === 'inline'
|
|
46
|
+
&& state.editorMode === 'inline'
|
|
47
|
+
&& state.editingRowId === store.getRowId(row)
|
|
48
|
+
&& state.editingColumn === column.key
|
|
49
|
+
);
|
|
50
|
+
let inlineValue = $state('');
|
|
51
|
+
|
|
52
|
+
$effect(() => {
|
|
53
|
+
if (isInlineEditing) {
|
|
54
|
+
inlineValue = value != null ? String(value) : '';
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
let cellEl: HTMLTableCellElement | undefined = $state();
|
|
59
|
+
let inputEl: HTMLInputElement | HTMLSelectElement | undefined = $state();
|
|
60
|
+
|
|
61
|
+
let inlineInputEl: HTMLInputElement | HTMLSelectElement | undefined = $state();
|
|
62
|
+
|
|
63
|
+
// Auto-focus input when entering edit mode (excel or inline)
|
|
64
|
+
$effect(() => {
|
|
65
|
+
if (isEditing && inputEl) {
|
|
66
|
+
inputEl.focus();
|
|
67
|
+
if (inputEl instanceof HTMLInputElement) {
|
|
68
|
+
inputEl.select();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
$effect(() => {
|
|
74
|
+
if (isInlineEditing && inlineInputEl) {
|
|
75
|
+
inlineInputEl.focus();
|
|
76
|
+
if (inlineInputEl instanceof HTMLInputElement) {
|
|
77
|
+
inlineInputEl.select();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
function commitInlineEdit() {
|
|
83
|
+
if (!isInlineEditing) return;
|
|
84
|
+
const col = column;
|
|
85
|
+
const oldVal = value;
|
|
86
|
+
let newVal: any = inlineValue;
|
|
87
|
+
if (col.type === 'number') newVal = newVal === '' ? null : Number(newVal);
|
|
88
|
+
else if (col.type === 'boolean') newVal = newVal === 'true' || newVal === '1';
|
|
89
|
+
else if (newVal === '') newVal = null;
|
|
90
|
+
|
|
91
|
+
store.closeEditor();
|
|
92
|
+
if (newVal !== oldVal) {
|
|
93
|
+
onInlineCommit?.(row, col.key, newVal, oldVal);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function cancelInlineEdit() {
|
|
98
|
+
store.closeEditor();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function handleInlineKeydown(e: KeyboardEvent) {
|
|
102
|
+
if (e.key === 'Enter') {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
commitInlineEdit();
|
|
105
|
+
} else if (e.key === 'Escape') {
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
cancelInlineEdit();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function formatValue(val: any, type?: string): string {
|
|
112
|
+
if (val === null || val === undefined) return '';
|
|
113
|
+
if (type === 'boolean') return val ? 'Yes' : 'No';
|
|
114
|
+
if (type === 'date' && val) {
|
|
115
|
+
try { return new Date(val).toLocaleDateString(); } catch { return String(val); }
|
|
116
|
+
}
|
|
117
|
+
if (type === 'number') return String(val);
|
|
118
|
+
return String(val);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function handleDblClick() {
|
|
122
|
+
if (column.editable && editorMode === 'inline') {
|
|
123
|
+
store.openEditor(store.getRowId(row), column.key, 'inline');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function handleCellClick() {
|
|
128
|
+
if (isExcelMode && column.editable !== false) {
|
|
129
|
+
store.focusCell(rowIndex, column.key);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function handleCellKeydown(e: KeyboardEvent) {
|
|
134
|
+
if (!isExcelMode || !isFocused) return;
|
|
135
|
+
|
|
136
|
+
if (isEditing) {
|
|
137
|
+
switch (e.key) {
|
|
138
|
+
case 'Enter':
|
|
139
|
+
e.preventDefault();
|
|
140
|
+
onExcelCommit?.();
|
|
141
|
+
break;
|
|
142
|
+
case 'Escape':
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
store.cancelCellEdit();
|
|
145
|
+
cellEl?.focus();
|
|
146
|
+
break;
|
|
147
|
+
case 'Tab':
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
onExcelCommit?.();
|
|
150
|
+
onExcelNavigate?.(e.shiftKey ? 'left' : 'right');
|
|
151
|
+
break;
|
|
152
|
+
case 'ArrowUp':
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
onExcelCommit?.();
|
|
155
|
+
onExcelNavigate?.('up');
|
|
156
|
+
break;
|
|
157
|
+
case 'ArrowDown':
|
|
158
|
+
e.preventDefault();
|
|
159
|
+
onExcelCommit?.();
|
|
160
|
+
onExcelNavigate?.('down');
|
|
161
|
+
break;
|
|
162
|
+
case 'ArrowLeft':
|
|
163
|
+
case 'ArrowRight':
|
|
164
|
+
// Allow cursor movement inside text inputs
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// While focused (not editing)
|
|
171
|
+
switch (e.key) {
|
|
172
|
+
case 'Enter':
|
|
173
|
+
case 'F2':
|
|
174
|
+
e.preventDefault();
|
|
175
|
+
if (column.editable !== false) {
|
|
176
|
+
store.startCellEdit();
|
|
177
|
+
}
|
|
178
|
+
break;
|
|
179
|
+
case 'ArrowUp':
|
|
180
|
+
e.preventDefault();
|
|
181
|
+
onExcelNavigate?.('up');
|
|
182
|
+
break;
|
|
183
|
+
case 'ArrowDown':
|
|
184
|
+
e.preventDefault();
|
|
185
|
+
onExcelNavigate?.('down');
|
|
186
|
+
break;
|
|
187
|
+
case 'ArrowLeft':
|
|
188
|
+
e.preventDefault();
|
|
189
|
+
onExcelNavigate?.('left');
|
|
190
|
+
break;
|
|
191
|
+
case 'ArrowRight':
|
|
192
|
+
case 'Tab':
|
|
193
|
+
e.preventDefault();
|
|
194
|
+
onExcelNavigate?.(e.shiftKey ? 'left' : 'right');
|
|
195
|
+
break;
|
|
196
|
+
case 'Escape':
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
store.clearExcelFocus();
|
|
199
|
+
break;
|
|
200
|
+
default:
|
|
201
|
+
// Typing a character starts editing
|
|
202
|
+
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey && column.editable !== false) {
|
|
203
|
+
store.startCellEdit();
|
|
204
|
+
// Let the character through to the input
|
|
205
|
+
store.setExcelEditValue(e.key);
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function handleInputKeydown(e: KeyboardEvent) {
|
|
212
|
+
// For select/boolean: left/right arrows also commit+navigate (no cursor to move)
|
|
213
|
+
if (column.type === 'boolean' || column.editorField?.type === 'select') {
|
|
214
|
+
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
|
|
215
|
+
e.preventDefault();
|
|
216
|
+
onExcelCommit?.();
|
|
217
|
+
onExcelNavigate?.(e.key === 'ArrowLeft' ? 'left' : 'right');
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
handleCellKeydown(e);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Auto-focus cell element when it becomes focused in excel mode
|
|
225
|
+
$effect(() => {
|
|
226
|
+
if (isFocused && !isEditing && cellEl) {
|
|
227
|
+
cellEl.focus();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
</script>
|
|
231
|
+
|
|
232
|
+
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
233
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
234
|
+
<td
|
|
235
|
+
bind:this={cellEl}
|
|
236
|
+
class="sdt-cell {column.className ?? ''} {tdClass}"
|
|
237
|
+
class:sdt-cell-editable={column.editable !== false}
|
|
238
|
+
class:sdt-excel-focused={isFocused && !isEditing}
|
|
239
|
+
class:sdt-excel-editing={isEditing}
|
|
240
|
+
style:width={column.width}
|
|
241
|
+
style:min-width={column.minWidth}
|
|
242
|
+
style:max-width={column.maxWidth}
|
|
243
|
+
ondblclick={handleDblClick}
|
|
244
|
+
onclick={handleCellClick}
|
|
245
|
+
onkeydown={handleCellKeydown}
|
|
246
|
+
tabindex={isExcelMode && column.editable !== false ? 0 : undefined}
|
|
247
|
+
>
|
|
248
|
+
{#if isInlineEditing}
|
|
249
|
+
{#if column.type === 'boolean'}
|
|
250
|
+
<select
|
|
251
|
+
bind:this={inlineInputEl}
|
|
252
|
+
class="sdt-inline-input"
|
|
253
|
+
value={inlineValue}
|
|
254
|
+
onchange={(e) => { inlineValue = e.currentTarget.value; commitInlineEdit(); }}
|
|
255
|
+
onkeydown={handleInlineKeydown}
|
|
256
|
+
onblur={commitInlineEdit}
|
|
257
|
+
>
|
|
258
|
+
<option value="true">Yes</option>
|
|
259
|
+
<option value="false">No</option>
|
|
260
|
+
</select>
|
|
261
|
+
{:else if column.editorField?.type === 'select' && column.editorField.options}
|
|
262
|
+
<select
|
|
263
|
+
bind:this={inlineInputEl}
|
|
264
|
+
class="sdt-inline-input"
|
|
265
|
+
value={inlineValue}
|
|
266
|
+
onchange={(e) => { inlineValue = e.currentTarget.value; commitInlineEdit(); }}
|
|
267
|
+
onkeydown={handleInlineKeydown}
|
|
268
|
+
onblur={commitInlineEdit}
|
|
269
|
+
>
|
|
270
|
+
{#each column.editorField.options as opt}
|
|
271
|
+
<option value={opt.value}>{opt.label}</option>
|
|
272
|
+
{/each}
|
|
273
|
+
</select>
|
|
274
|
+
{:else}
|
|
275
|
+
<input
|
|
276
|
+
bind:this={inlineInputEl}
|
|
277
|
+
class="sdt-inline-input"
|
|
278
|
+
type={column.type === 'number' ? 'number' : 'text'}
|
|
279
|
+
bind:value={inlineValue}
|
|
280
|
+
onkeydown={handleInlineKeydown}
|
|
281
|
+
onblur={commitInlineEdit}
|
|
282
|
+
/>
|
|
283
|
+
{/if}
|
|
284
|
+
{:else if isEditing}
|
|
285
|
+
{#if column.type === 'boolean'}
|
|
286
|
+
<select
|
|
287
|
+
bind:this={inputEl}
|
|
288
|
+
class="sdt-excel-input"
|
|
289
|
+
value={state.excelEditValue}
|
|
290
|
+
onchange={(e) => store.setExcelEditValue(e.currentTarget.value)}
|
|
291
|
+
onkeydown={handleInputKeydown}
|
|
292
|
+
>
|
|
293
|
+
<option value="true">Yes</option>
|
|
294
|
+
<option value="false">No</option>
|
|
295
|
+
</select>
|
|
296
|
+
{:else if column.editorField?.type === 'select' && column.editorField.options}
|
|
297
|
+
<select
|
|
298
|
+
bind:this={inputEl}
|
|
299
|
+
class="sdt-excel-input"
|
|
300
|
+
value={state.excelEditValue}
|
|
301
|
+
onchange={(e) => store.setExcelEditValue(e.currentTarget.value)}
|
|
302
|
+
onkeydown={handleInputKeydown}
|
|
303
|
+
>
|
|
304
|
+
{#each column.editorField.options as opt}
|
|
305
|
+
<option value={opt.value}>{opt.label}</option>
|
|
306
|
+
{/each}
|
|
307
|
+
</select>
|
|
308
|
+
{:else}
|
|
309
|
+
<input
|
|
310
|
+
bind:this={inputEl}
|
|
311
|
+
class="sdt-excel-input"
|
|
312
|
+
type={column.type === 'number' ? 'number' : 'text'}
|
|
313
|
+
value={state.excelEditValue}
|
|
314
|
+
oninput={(e) => store.setExcelEditValue(e.currentTarget.value)}
|
|
315
|
+
onkeydown={handleInputKeydown}
|
|
316
|
+
/>
|
|
317
|
+
{/if}
|
|
318
|
+
{:else if customCell}
|
|
319
|
+
{@render customCell({ row, column, value })}
|
|
320
|
+
{:else if column.type === 'html'}
|
|
321
|
+
{@html value}
|
|
322
|
+
{:else}
|
|
323
|
+
{formatValue(value, column.type)}
|
|
324
|
+
{/if}
|
|
325
|
+
</td>
|
|
326
|
+
|
|
327
|
+
<style>
|
|
328
|
+
.sdt-cell {
|
|
329
|
+
padding: var(--sdt-cell-padding, 0.625rem 0.75rem);
|
|
330
|
+
border-bottom: 1px solid var(--sdt-border, #e5e7eb);
|
|
331
|
+
font-size: 0.875rem;
|
|
332
|
+
white-space: nowrap;
|
|
333
|
+
overflow: hidden;
|
|
334
|
+
text-overflow: ellipsis;
|
|
335
|
+
}
|
|
336
|
+
.sdt-cell-editable {
|
|
337
|
+
cursor: pointer;
|
|
338
|
+
}
|
|
339
|
+
.sdt-cell-editable:hover {
|
|
340
|
+
background-color: var(--sdt-cell-edit-hover, rgba(59, 130, 246, 0.05));
|
|
341
|
+
}
|
|
342
|
+
.sdt-excel-focused {
|
|
343
|
+
outline: 2px solid var(--sdt-primary, #3b82f6);
|
|
344
|
+
outline-offset: -2px;
|
|
345
|
+
background-color: var(--sdt-excel-focus-bg, rgba(59, 130, 246, 0.06));
|
|
346
|
+
}
|
|
347
|
+
.sdt-excel-editing {
|
|
348
|
+
padding: 0.125rem;
|
|
349
|
+
background-color: var(--sdt-excel-edit-bg, #fff);
|
|
350
|
+
outline: 2px solid var(--sdt-primary, #3b82f6);
|
|
351
|
+
outline-offset: -2px;
|
|
352
|
+
}
|
|
353
|
+
.sdt-excel-input {
|
|
354
|
+
width: 100%;
|
|
355
|
+
height: 100%;
|
|
356
|
+
border: none;
|
|
357
|
+
outline: none;
|
|
358
|
+
background: transparent;
|
|
359
|
+
font-size: 0.875rem;
|
|
360
|
+
font-family: inherit;
|
|
361
|
+
padding: 0.375rem 0.5rem;
|
|
362
|
+
box-sizing: border-box;
|
|
363
|
+
}
|
|
364
|
+
.sdt-excel-input:focus {
|
|
365
|
+
outline: none;
|
|
366
|
+
}
|
|
367
|
+
.sdt-inline-input {
|
|
368
|
+
width: 100%;
|
|
369
|
+
border: 1px solid var(--sdt-primary, #3b82f6);
|
|
370
|
+
border-radius: 0.25rem;
|
|
371
|
+
outline: none;
|
|
372
|
+
background: var(--sdt-bg, #fff);
|
|
373
|
+
font-size: 0.875rem;
|
|
374
|
+
font-family: inherit;
|
|
375
|
+
padding: 0.25rem 0.375rem;
|
|
376
|
+
box-sizing: border-box;
|
|
377
|
+
}
|
|
378
|
+
.sdt-inline-input:focus {
|
|
379
|
+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
|
380
|
+
}
|
|
381
|
+
</style>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ColumnDef, DataTableStore } from '../index.js';
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
store: DataTableStore;
|
|
6
|
+
columns: ColumnDef[];
|
|
7
|
+
}
|
|
8
|
+
let { store, columns }: Props = $props();
|
|
9
|
+
|
|
10
|
+
let open = $state(false);
|
|
11
|
+
let state = $state(store.getState());
|
|
12
|
+
|
|
13
|
+
$effect(() => {
|
|
14
|
+
return store.subscribe(() => {
|
|
15
|
+
state = store.getState();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
function toggle(key: string) {
|
|
20
|
+
store.toggleColumnVisibility(key);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function handleClickOutside(e: MouseEvent) {
|
|
24
|
+
const target = e.target as HTMLElement;
|
|
25
|
+
if (!target.closest('.sdt-col-toggle')) {
|
|
26
|
+
open = false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
$effect(() => {
|
|
31
|
+
if (open) {
|
|
32
|
+
document.addEventListener('click', handleClickOutside);
|
|
33
|
+
return () => document.removeEventListener('click', handleClickOutside);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<div class="sdt-col-toggle">
|
|
39
|
+
<button type="button" class="sdt-col-toggle-btn" onclick={() => (open = !open)} aria-label="Toggle columns">
|
|
40
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
|
|
41
|
+
<path d="M12 3v18M3 12h18M9 3h6M9 21h6M3 9v6M21 9v6" />
|
|
42
|
+
</svg>
|
|
43
|
+
Columns
|
|
44
|
+
</button>
|
|
45
|
+
|
|
46
|
+
{#if open}
|
|
47
|
+
<div class="sdt-col-toggle-menu">
|
|
48
|
+
{#each columns as col}
|
|
49
|
+
<label class="sdt-col-toggle-item">
|
|
50
|
+
<input
|
|
51
|
+
type="checkbox"
|
|
52
|
+
checked={state.columnVisibility[col.key] !== false}
|
|
53
|
+
onchange={() => toggle(col.key)}
|
|
54
|
+
/>
|
|
55
|
+
<span>{col.header}</span>
|
|
56
|
+
</label>
|
|
57
|
+
{/each}
|
|
58
|
+
</div>
|
|
59
|
+
{/if}
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<style>
|
|
63
|
+
.sdt-col-toggle {
|
|
64
|
+
position: relative;
|
|
65
|
+
}
|
|
66
|
+
.sdt-col-toggle-btn {
|
|
67
|
+
display: inline-flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
gap: 0.375rem;
|
|
70
|
+
padding: 0.5rem 0.75rem;
|
|
71
|
+
border: 1px solid var(--sdt-border, #e5e7eb);
|
|
72
|
+
border-radius: 0.375rem;
|
|
73
|
+
background: var(--sdt-input-bg, #fff);
|
|
74
|
+
color: var(--sdt-text, #111827);
|
|
75
|
+
font-size: 0.8125rem;
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
transition: background-color 0.15s;
|
|
78
|
+
}
|
|
79
|
+
.sdt-col-toggle-btn:hover {
|
|
80
|
+
background-color: var(--sdt-hover, #f3f4f6);
|
|
81
|
+
}
|
|
82
|
+
.sdt-col-toggle-menu {
|
|
83
|
+
position: absolute;
|
|
84
|
+
top: 100%;
|
|
85
|
+
right: 0;
|
|
86
|
+
margin-top: 0.25rem;
|
|
87
|
+
min-width: 180px;
|
|
88
|
+
padding: 0.5rem;
|
|
89
|
+
background: var(--sdt-bg, #fff);
|
|
90
|
+
border: 1px solid var(--sdt-border, #e5e7eb);
|
|
91
|
+
border-radius: 0.375rem;
|
|
92
|
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
|
93
|
+
z-index: 20;
|
|
94
|
+
}
|
|
95
|
+
.sdt-col-toggle-item {
|
|
96
|
+
display: flex;
|
|
97
|
+
align-items: center;
|
|
98
|
+
gap: 0.5rem;
|
|
99
|
+
padding: 0.375rem 0.5rem;
|
|
100
|
+
font-size: 0.8125rem;
|
|
101
|
+
border-radius: 0.25rem;
|
|
102
|
+
cursor: pointer;
|
|
103
|
+
color: var(--sdt-text, #111827);
|
|
104
|
+
}
|
|
105
|
+
.sdt-col-toggle-item:hover {
|
|
106
|
+
background-color: var(--sdt-hover, #f3f4f6);
|
|
107
|
+
}
|
|
108
|
+
.sdt-col-toggle-item input {
|
|
109
|
+
accent-color: var(--sdt-primary, #3b82f6);
|
|
110
|
+
}
|
|
111
|
+
</style>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { EditorFieldDef, DataTableStore, DataTableClassNames } from '../index.js';
|
|
3
|
+
import DataTableModalEditor from './DataTableModalEditor.svelte';
|
|
4
|
+
import DataTableBubbleEditor from './DataTableBubbleEditor.svelte';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
fields: EditorFieldDef[];
|
|
8
|
+
store: DataTableStore;
|
|
9
|
+
classNames?: DataTableClassNames;
|
|
10
|
+
onsubmit: (formData: Record<string, any>) => void | Promise<void>;
|
|
11
|
+
anchorEl?: HTMLElement | null;
|
|
12
|
+
}
|
|
13
|
+
let { fields, store, classNames = {}, onsubmit, anchorEl = null }: Props = $props();
|
|
14
|
+
|
|
15
|
+
let state = $state(store.getState());
|
|
16
|
+
$effect(() => {
|
|
17
|
+
return store.subscribe(() => {
|
|
18
|
+
state = store.getState();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
{#if state.editorMode === 'modal'}
|
|
24
|
+
<DataTableModalEditor {fields} {store} {onsubmit} {classNames} />
|
|
25
|
+
{:else if state.editorMode === 'bubble'}
|
|
26
|
+
<DataTableBubbleEditor {fields} {store} {onsubmit} {anchorEl} {classNames} />
|
|
27
|
+
{/if}
|