@beeblock/svelar-datatable 0.1.8 → 0.2.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 +347 -154
- package/dist/SvelarDatatablePlugin.d.ts +57 -2
- package/dist/index.js +7 -7
- package/dist/plugin.js +1 -1
- package/dist/server/DataTableService.d.ts +21 -0
- package/dist/server/index.js +1 -1
- package/dist/state/ServerDataTableStore.d.ts +2 -0
- package/dist/types.d.ts +2 -0
- package/package.json +82 -76
- package/src/state/ServerDataTableStore.ts +14 -0
- package/src/types.ts +2 -0
- package/src/ui/DataTable.svelte +5 -1
- package/src/ui/DataTableBody.svelte +2 -0
- package/src/ui/DataTableBubbleEditor.svelte +3 -0
- package/src/ui/DataTableButtons.svelte +10 -4
- package/src/ui/DataTableCell.svelte +2 -0
- package/src/ui/DataTableColumnToggle.svelte +1 -0
- package/src/ui/DataTableEditor.svelte +2 -0
- package/src/ui/DataTableEditorField.svelte +2 -0
- package/src/ui/DataTableFooter.svelte +2 -0
- package/src/ui/DataTableHead.svelte +2 -0
- package/src/ui/DataTableModalEditor.svelte +3 -0
- package/src/ui/DataTablePagination.svelte +2 -0
- package/src/ui/DataTableRow.svelte +21 -2
- package/src/ui/DataTableSearch.svelte +2 -0
- package/src/ui/DataTableToolbar.svelte +2 -0
package/README.md
CHANGED
|
@@ -1,154 +1,347 @@
|
|
|
1
|
-
# @beeblock/svelar-datatable
|
|
2
|
-
|
|
3
|
-
Full-featured DataTable plugin for [Svelar](https://svelar.dev) / SvelteKit 2 with Svelte 5.
|
|
4
|
-
|
|
5
|
-
## Features
|
|
6
|
-
|
|
7
|
-
- **Sorting** — single-click or multi-sort (Shift+click)
|
|
8
|
-
- **Searching** — global search with debounce
|
|
9
|
-
- **Pagination** — configurable per-page options, page navigation
|
|
10
|
-
- **Selection** — single, multi, range (Shift+click), select all
|
|
11
|
-
- **Column management** — visibility toggle, reorder, resize
|
|
12
|
-
- **Editing** — four modes: modal, bubble (popover), inline (double-click), excel (spreadsheet)
|
|
13
|
-
- **Export** — CSV, clipboard, print (Excel/PDF with optional deps)
|
|
14
|
-
- **Virtual scroll** — render 10,000+ rows with only visible DOM nodes
|
|
15
|
-
- **Row grouping** — group rows by any column value
|
|
16
|
-
- **Per-column filters** — programmatic column-level filtering
|
|
17
|
-
- **Custom cells** — Svelte 5 snippets for full cell rendering control
|
|
18
|
-
- **Row customization** — `rowClass`, `rowId` function, expandable detail rows
|
|
19
|
-
- **Server-side** — jQuery DataTables wire protocol compatible, GET or POST
|
|
20
|
-
- **State persistence** — save sort/filter/page/column state to localStorage
|
|
21
|
-
- **Tailwind CSS** — `classNames` prop for pure Tailwind customization of every element
|
|
22
|
-
- **CSS variables** — 12+ CSS custom properties for quick theming
|
|
23
|
-
- **Footer aggregation** — per-column footer with custom functions
|
|
24
|
-
- **Responsive** — horizontal scroll for small screens
|
|
25
|
-
|
|
26
|
-
## Installation
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
# Install and publish route stubs
|
|
30
|
-
npx svelar plugin:install @beeblock/svelar-datatable
|
|
31
|
-
|
|
32
|
-
# Or manual install
|
|
33
|
-
npm install @beeblock/svelar-datatable
|
|
34
|
-
npx svelar plugin:publish @beeblock/svelar-datatable
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Peer dependencies: `@beeblock/svelar >= 0.
|
|
38
|
-
|
|
39
|
-
## Quick Start
|
|
40
|
-
|
|
41
|
-
```svelte
|
|
42
|
-
<script lang="ts">
|
|
43
|
-
import { DataTable } from '@beeblock/svelar-datatable/ui';
|
|
44
|
-
import type { ColumnDef } from '@beeblock/svelar-datatable';
|
|
45
|
-
|
|
46
|
-
const columns: ColumnDef[] = [
|
|
47
|
-
{ key: 'id', header: 'ID', type: 'number', sortable: true },
|
|
48
|
-
{ key: 'name', header: 'Name', sortable: true, searchable: true },
|
|
49
|
-
{ key: 'email', header: 'Email', sortable: true, searchable: true },
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
const data = [
|
|
53
|
-
{ id: 1, name: 'Alice', email: 'alice@example.com' },
|
|
54
|
-
{ id: 2, name: 'Bob', email: 'bob@example.com' },
|
|
55
|
-
];
|
|
56
|
-
</script>
|
|
57
|
-
|
|
58
|
-
<DataTable {data} {columns} sortable searchable paginate perPage={10} />
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
## Server-Side
|
|
62
|
-
|
|
63
|
-
```svelte
|
|
64
|
-
<!-- Frontend -->
|
|
65
|
-
<DataTable
|
|
66
|
-
serverUrl="/api/datatable/users"
|
|
67
|
-
columns={columns}
|
|
68
|
-
sortable
|
|
69
|
-
searchable
|
|
70
|
-
paginate
|
|
71
|
-
perPage={25}
|
|
72
|
-
/>
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
```ts
|
|
76
|
-
// src/routes/api/datatable/users/+server.ts
|
|
77
|
-
import { DataTableController } from '@beeblock/svelar-datatable/server';
|
|
78
|
-
import { User } from '$lib/models/User';
|
|
79
|
-
|
|
80
|
-
const dt = new DataTableController(User);
|
|
81
|
-
export const GET = dt.handle();
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
1
|
+
# @beeblock/svelar-datatable
|
|
2
|
+
|
|
3
|
+
Full-featured DataTable plugin for [Svelar](https://svelar.dev) / SvelteKit 2 with Svelte 5.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Sorting** — single-click or multi-sort (Shift+click)
|
|
8
|
+
- **Searching** — global search with debounce
|
|
9
|
+
- **Pagination** — configurable per-page options, page navigation
|
|
10
|
+
- **Selection** — single, multi, range (Shift+click), select all
|
|
11
|
+
- **Column management** — visibility toggle, reorder, resize
|
|
12
|
+
- **Editing** — four modes: modal, bubble (popover), inline (double-click), excel (spreadsheet)
|
|
13
|
+
- **Export** — CSV, clipboard, print (Excel/PDF with optional deps)
|
|
14
|
+
- **Virtual scroll** — render 10,000+ rows with only visible DOM nodes
|
|
15
|
+
- **Row grouping** — group rows by any column value
|
|
16
|
+
- **Per-column filters** — programmatic column-level filtering
|
|
17
|
+
- **Custom cells** — Svelte 5 snippets for full cell rendering control
|
|
18
|
+
- **Row customization** — `rowClass`, `rowId` function, expandable detail rows
|
|
19
|
+
- **Server-side** — jQuery DataTables wire protocol compatible, GET or POST
|
|
20
|
+
- **State persistence** — save sort/filter/page/column state to localStorage
|
|
21
|
+
- **Tailwind CSS** — `classNames` prop for pure Tailwind customization of every element
|
|
22
|
+
- **CSS variables** — 12+ CSS custom properties for quick theming
|
|
23
|
+
- **Footer aggregation** — per-column footer with custom functions
|
|
24
|
+
- **Responsive** — horizontal scroll for small screens
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# Install and publish route stubs
|
|
30
|
+
npx svelar plugin:install @beeblock/svelar-datatable
|
|
31
|
+
|
|
32
|
+
# Or manual install
|
|
33
|
+
npm install @beeblock/svelar-datatable
|
|
34
|
+
npx svelar plugin:publish @beeblock/svelar-datatable
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Peer dependencies: `@beeblock/svelar >= 0.6.7`, `svelte ^5.0.0`. `exceljs` is an optional peer dependency used only for Excel export.
|
|
38
|
+
|
|
39
|
+
## Quick Start
|
|
40
|
+
|
|
41
|
+
```svelte
|
|
42
|
+
<script lang="ts">
|
|
43
|
+
import { DataTable } from '@beeblock/svelar-datatable/ui';
|
|
44
|
+
import type { ColumnDef } from '@beeblock/svelar-datatable';
|
|
45
|
+
|
|
46
|
+
const columns: ColumnDef[] = [
|
|
47
|
+
{ key: 'id', header: 'ID', type: 'number', sortable: true },
|
|
48
|
+
{ key: 'name', header: 'Name', sortable: true, searchable: true },
|
|
49
|
+
{ key: 'email', header: 'Email', sortable: true, searchable: true },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const data = [
|
|
53
|
+
{ id: 1, name: 'Alice', email: 'alice@example.com' },
|
|
54
|
+
{ id: 2, name: 'Bob', email: 'bob@example.com' },
|
|
55
|
+
];
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<DataTable {data} {columns} sortable searchable paginate perPage={10} />
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Server-Side
|
|
62
|
+
|
|
63
|
+
```svelte
|
|
64
|
+
<!-- Frontend -->
|
|
65
|
+
<DataTable
|
|
66
|
+
serverUrl="/api/datatable/users"
|
|
67
|
+
columns={columns}
|
|
68
|
+
sortable
|
|
69
|
+
searchable
|
|
70
|
+
paginate
|
|
71
|
+
perPage={25}
|
|
72
|
+
/>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
// src/routes/api/datatable/users/+server.ts
|
|
77
|
+
import { DataTableController } from '@beeblock/svelar-datatable/server';
|
|
78
|
+
import { User } from '$lib/modules/auth/domain/models/User.js';
|
|
79
|
+
|
|
80
|
+
const dt = new DataTableController(User);
|
|
81
|
+
export const GET = dt.handle('query');
|
|
82
|
+
export const POST = dt.handle('query');
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The server controller handles pagination, sorting, searching, and filtering — compatible with the jQuery DataTables wire protocol.
|
|
86
|
+
|
|
87
|
+
Server search is case-insensitive on PostgreSQL (`ILIKE`) and uses the database driver's normal `LIKE` behavior on SQLite/MySQL.
|
|
88
|
+
|
|
89
|
+
### Server Filters
|
|
90
|
+
|
|
91
|
+
Use `serverParams` on the component for custom server filters. GET requests send these as `filters[name]` query params; POST requests include them in `customFilters`.
|
|
92
|
+
|
|
93
|
+
```svelte
|
|
94
|
+
<script lang="ts">
|
|
95
|
+
let priority = $state('');
|
|
96
|
+
|
|
97
|
+
function serverParams() {
|
|
98
|
+
return priority ? { priority } : {};
|
|
99
|
+
}
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<DataTable
|
|
103
|
+
serverUrl="/api/datatable/cards"
|
|
104
|
+
columns={columns}
|
|
105
|
+
serverParams={serverParams}
|
|
106
|
+
searchable
|
|
107
|
+
sortable
|
|
108
|
+
/>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Register matching filters on the server:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { DataTableController } from '@beeblock/svelar-datatable/server';
|
|
115
|
+
import { Card } from '$lib/modules/boards/domain/models/Card.js';
|
|
116
|
+
|
|
117
|
+
const dt = new DataTableController(Card, {
|
|
118
|
+
searchable: ['title', 'description', 'priority'],
|
|
119
|
+
orderable: ['title', 'priority', 'updated_at'],
|
|
120
|
+
filters: {
|
|
121
|
+
priority: (query, value) => query.where('priority', value),
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
export const GET = dt.handle('query');
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Meilisearch
|
|
129
|
+
|
|
130
|
+
When a model uses Svelar's `Searchable` mixin, global server search can use Meilisearch:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const dt = new DataTableController(Card, {
|
|
134
|
+
searchDriver: 'auto',
|
|
135
|
+
filters: {
|
|
136
|
+
priority: (query, value) => query.where('priority', value),
|
|
137
|
+
},
|
|
138
|
+
meilisearchFilter: (filters) => {
|
|
139
|
+
if (!filters.priority) return undefined;
|
|
140
|
+
return `priority = "${String(filters.priority).replaceAll('"', '\\"')}"`;
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`searchDriver: 'auto'` is the default. It tries Meilisearch for global search when the model exposes `search()`, then falls back to database search if Meilisearch is not configured. Use `searchDriver: 'database'` to force SQL search, or `searchDriver: 'meilisearch'` to fail instead of falling back.
|
|
146
|
+
|
|
147
|
+
Meilisearch mode returns the indexed document fields from `model.search()`. Keep the datatable columns in the model's searchable/displayed attributes, and mark filtered/sorted attributes in Meilisearch index settings.
|
|
148
|
+
|
|
149
|
+
## Extending and Customizing
|
|
150
|
+
|
|
151
|
+
The plugin is meant to stay a reusable table black box, but it exposes extension points instead of forcing app-specific forks.
|
|
152
|
+
|
|
153
|
+
### Frontend Extension Points
|
|
154
|
+
|
|
155
|
+
- `customCell` snippet for custom badges, links, menus, previews, or composed app UI.
|
|
156
|
+
- `buttons` for export buttons and custom actions that receive selected rows and all rows.
|
|
157
|
+
- `classNames` for Tailwind/shadcn styling without editing plugin CSS.
|
|
158
|
+
- `bind:storeRef` for page-level controls such as external filters, refresh buttons, selected-row panels, or programmatic column visibility.
|
|
159
|
+
- `editorField` and `editorMode` for inline, modal, bubble, or Excel-style editing.
|
|
160
|
+
|
|
161
|
+
```svelte
|
|
162
|
+
<script lang="ts">
|
|
163
|
+
import { DataTable } from '@beeblock/svelar-datatable/ui';
|
|
164
|
+
import type { ButtonDef, ColumnDef } from '@beeblock/svelar-datatable';
|
|
165
|
+
|
|
166
|
+
const columns: ColumnDef[] = [
|
|
167
|
+
{ key: 'title', header: 'Title' },
|
|
168
|
+
{ key: 'priority', header: 'Priority', type: 'custom' },
|
|
169
|
+
{ key: 'actions', header: 'Actions', type: 'custom', sortable: false, searchable: false },
|
|
170
|
+
];
|
|
171
|
+
|
|
172
|
+
const buttons: ButtonDef[] = [
|
|
173
|
+
{
|
|
174
|
+
key: 'archive',
|
|
175
|
+
label: 'Archive selected',
|
|
176
|
+
disabled: (rows) => rows.length === 0,
|
|
177
|
+
action: (rows) => archiveCards(rows),
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
</script>
|
|
181
|
+
|
|
182
|
+
{#snippet customCell({ row, column, value })}
|
|
183
|
+
{#if column.key === 'priority'}
|
|
184
|
+
<span class="rounded border px-2 py-0.5 text-xs">{value}</span>
|
|
185
|
+
{:else if column.key === 'actions'}
|
|
186
|
+
<button type="button" onclick={() => openCard(row)}>Open</button>
|
|
187
|
+
{:else}
|
|
188
|
+
{value}
|
|
189
|
+
{/if}
|
|
190
|
+
{/snippet}
|
|
191
|
+
|
|
192
|
+
<DataTable {columns} data={cards} {buttons} {customCell} selectable="multi" />
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Server Extension Points
|
|
196
|
+
|
|
197
|
+
- `filters` on `DataTableController` or `DataTableService.addFilter()` for app-specific server filters.
|
|
198
|
+
- `baseQuery` for tenant/team/user scoping.
|
|
199
|
+
- `scopes` for named reusable query constraints.
|
|
200
|
+
- `computedColumns` for derived SQL columns.
|
|
201
|
+
- `searchDriver`, `meilisearchFilter`, and `meilisearchSort` for Searchable/Meilisearch-backed server search.
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
const dt = new DataTableController(Card, {
|
|
205
|
+
baseQuery: (query) => query.where('board_id', board.id),
|
|
206
|
+
searchable: ['title', 'description', 'priority'],
|
|
207
|
+
orderable: ['title', 'priority', 'position', 'updated_at'],
|
|
208
|
+
filters: {
|
|
209
|
+
priority: (query, value) => query.where('priority', value),
|
|
210
|
+
completed: (query, value) => query.where('completed', value === 'true'),
|
|
211
|
+
},
|
|
212
|
+
searchDriver: 'auto',
|
|
213
|
+
meilisearchFilter: (filters) => {
|
|
214
|
+
const clauses = [];
|
|
215
|
+
if (filters.priority) clauses.push(`priority = "${filters.priority}"`);
|
|
216
|
+
if (filters.completed) clauses.push(`completed = ${filters.completed === 'true'}`);
|
|
217
|
+
return clauses.length ? clauses.join(' AND ') : undefined;
|
|
218
|
+
},
|
|
219
|
+
});
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Plugin Contract
|
|
223
|
+
|
|
224
|
+
The package exposes the required Svelar plugin entry at `@beeblock/svelar-datatable/plugin`.
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import DatatablePlugin from '@beeblock/svelar-datatable/plugin';
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The plugin publishes a route stub to `src/routes/api/datatable/+server.ts` and registers default config under the `datatable` config key.
|
|
231
|
+
|
|
232
|
+
## Editing
|
|
233
|
+
|
|
234
|
+
Four editor modes, configured via `editorMode`:
|
|
235
|
+
|
|
236
|
+
```svelte
|
|
237
|
+
<!-- Modal editor -->
|
|
238
|
+
<DataTable editorMode="modal" editorFields={fields} onEdit={save} onCreate={create} />
|
|
239
|
+
|
|
240
|
+
<!-- Bubble (popover anchored to row) -->
|
|
241
|
+
<DataTable editorMode="bubble" editorFields={fields} onEdit={save} />
|
|
242
|
+
|
|
243
|
+
<!-- Inline (double-click a cell) -->
|
|
244
|
+
<DataTable editorMode="inline" onCellEdit={saveCellEdit} />
|
|
245
|
+
|
|
246
|
+
<!-- Excel (spreadsheet navigation) -->
|
|
247
|
+
<DataTable editorMode="excel" onCellEdit={saveCellEdit} />
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Server-side Excel editing should still go through normal Svelar backend layers. Wire `onCellEdit` to a PATCH route that validates with a FormRequest, creates a DTO, runs an action/service, and returns a resource response. Throw from `onCellEdit` when the API rejects the update so the table can keep the old value.
|
|
251
|
+
|
|
252
|
+
```svelte
|
|
253
|
+
<script lang="ts">
|
|
254
|
+
import { apiFetchJson } from '@beeblock/svelar/http';
|
|
255
|
+
import type { ButtonDef, ExportFormat } from '@beeblock/svelar-datatable';
|
|
256
|
+
|
|
257
|
+
async function saveCell(row, columnKey, newValue) {
|
|
258
|
+
const response = await apiFetchJson(`/api/datatable/cards/${row.public_id}`, {
|
|
259
|
+
method: 'PATCH',
|
|
260
|
+
body: JSON.stringify({ column: columnKey, value: newValue }),
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
if (!response.ok) {
|
|
264
|
+
throw new Error(response.error?.message ?? 'Failed to update row');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const buttons: (ButtonDef | ExportFormat)[] = [
|
|
269
|
+
'csv',
|
|
270
|
+
{
|
|
271
|
+
key: 'mark-urgent',
|
|
272
|
+
label: 'Mark urgent',
|
|
273
|
+
disabled: (rows) => rows.length === 0,
|
|
274
|
+
action: (rows) => Promise.all(rows.map((row) => saveCell(row, 'priority', 'urgent'))),
|
|
275
|
+
},
|
|
276
|
+
];
|
|
277
|
+
</script>
|
|
278
|
+
|
|
279
|
+
<DataTable
|
|
280
|
+
serverUrl="/api/datatable/cards"
|
|
281
|
+
{columns}
|
|
282
|
+
selectable="multi"
|
|
283
|
+
editorMode="excel"
|
|
284
|
+
onCellEdit={saveCell}
|
|
285
|
+
{buttons}
|
|
286
|
+
/>
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## Tailwind Customization
|
|
290
|
+
|
|
291
|
+
Style every element with Tailwind utility classes via the `classNames` prop:
|
|
292
|
+
|
|
293
|
+
```svelte
|
|
294
|
+
<DataTable
|
|
295
|
+
{data}
|
|
296
|
+
{columns}
|
|
297
|
+
striped={false}
|
|
298
|
+
hover={false}
|
|
299
|
+
classNames={{
|
|
300
|
+
container: '!bg-slate-900 !border-slate-800',
|
|
301
|
+
thead: '!bg-slate-950',
|
|
302
|
+
th: '!bg-slate-950 !text-slate-400 !tracking-widest',
|
|
303
|
+
tr: 'hover:!bg-slate-800',
|
|
304
|
+
td: '!text-slate-300 !border-b-slate-800',
|
|
305
|
+
pagination: '!border-t-slate-800 !text-slate-400 !bg-slate-900',
|
|
306
|
+
pageButton: '!bg-slate-800 !text-slate-400 !border-slate-700',
|
|
307
|
+
searchInput: '!bg-slate-800 !text-slate-200 !border-slate-700',
|
|
308
|
+
}}
|
|
309
|
+
/>
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
All 39 keys from `DataTableClassNames` are wired: `container`, `toolbar`, `toolbarLeft`, `toolbarRight`, `searchInput`, `thead`, `th`, `tbody`, `tr`, `trSelected`, `trEven`, `td`, `tfoot`, `tf`, `pagination`, `paginationInfo`, `paginationControls`, `pageButton`, `pageButtonActive`, `perPageSelect`, `btn`, `btnCreate`, `btnEdit`, `btnDelete`, `editorModal`, `editorBackdrop`, `editorField`, `editorInput`, `editorLabel`, `loading`, `empty`, `error`.
|
|
313
|
+
|
|
314
|
+
## Imports
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
// UI component (Svelte source — not compiled)
|
|
318
|
+
import { DataTable } from '@beeblock/svelar-datatable/ui';
|
|
319
|
+
|
|
320
|
+
// Types
|
|
321
|
+
import type {
|
|
322
|
+
ColumnDef, EditorFieldDef, ButtonDef, DataTableClassNames,
|
|
323
|
+
DataTableConfig, DataTableState, ExportFormat,
|
|
324
|
+
} from '@beeblock/svelar-datatable';
|
|
325
|
+
|
|
326
|
+
// Stores
|
|
327
|
+
import { DataTableStore, ServerDataTableStore } from '@beeblock/svelar-datatable';
|
|
328
|
+
|
|
329
|
+
// Server controller (API routes)
|
|
330
|
+
import { DataTableController, DataTableService } from '@beeblock/svelar-datatable/server';
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
## Documentation
|
|
334
|
+
|
|
335
|
+
Full documentation with all props, callbacks, store API, and examples: [svelar.dev/docs/datatable](https://svelar.dev/docs/datatable)
|
|
336
|
+
|
|
337
|
+
## Local Validation
|
|
338
|
+
|
|
339
|
+
```bash
|
|
340
|
+
npm run lint
|
|
341
|
+
npm run test
|
|
342
|
+
npm run build
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
## License
|
|
346
|
+
|
|
347
|
+
MIT
|
|
@@ -1,17 +1,72 @@
|
|
|
1
1
|
import { Plugin } from '@beeblock/svelar/plugins';
|
|
2
2
|
import type { DataTableConfig } from './types.js';
|
|
3
|
+
import type { Container } from '@beeblock/svelar/container';
|
|
3
4
|
interface DatatablePluginConfig {
|
|
4
5
|
prefix?: string;
|
|
5
6
|
defaults?: Partial<DataTableConfig>;
|
|
6
7
|
}
|
|
8
|
+
export declare const SVELAR_DATATABLE_PACKAGE: string;
|
|
9
|
+
export declare const SVELAR_DATATABLE_VERSION: string;
|
|
7
10
|
export declare class SvelarDatatablePlugin extends Plugin {
|
|
8
|
-
readonly name
|
|
9
|
-
readonly version
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly version: string;
|
|
10
13
|
readonly description = "Full-featured DataTable plugin for Svelar \u2014 sorting, searching, pagination, inline editing, export, and server-side processing";
|
|
11
14
|
private _config;
|
|
12
15
|
constructor(config?: DatatablePluginConfig);
|
|
13
16
|
get datatableConfig(): DatatablePluginConfig;
|
|
14
17
|
static defaults(): Partial<DataTableConfig>;
|
|
18
|
+
config(): {
|
|
19
|
+
key: string;
|
|
20
|
+
defaults: {
|
|
21
|
+
prefix: string | undefined;
|
|
22
|
+
defaults: {
|
|
23
|
+
data?: any[] | undefined;
|
|
24
|
+
serverUrl?: string;
|
|
25
|
+
serverMethod?: "GET" | "POST";
|
|
26
|
+
serverParams?: Record<string, any> | (() => Record<string, any>);
|
|
27
|
+
csrfCookieName?: string;
|
|
28
|
+
csrfHeaderName?: string;
|
|
29
|
+
columns?: import("./types.js").ColumnDef<any>[] | undefined;
|
|
30
|
+
sortable?: boolean;
|
|
31
|
+
searchable?: boolean;
|
|
32
|
+
paginate?: boolean;
|
|
33
|
+
selectable?: import("./types.js").SelectionMode;
|
|
34
|
+
perPage?: number;
|
|
35
|
+
perPageOptions?: number[];
|
|
36
|
+
searchDebounceMs?: number;
|
|
37
|
+
stateSaveKey?: string;
|
|
38
|
+
rowId?: string | ((row: any) => string | number) | undefined;
|
|
39
|
+
rowClass?: string | ((row: any, index: number) => string) | undefined;
|
|
40
|
+
buttons?: (import("./types.js").ButtonDef | import("./types.js").ExportFormat)[];
|
|
41
|
+
editorMode?: import("./types.js").EditorMode;
|
|
42
|
+
editorFields?: import("./types.js").EditorFieldDef[];
|
|
43
|
+
onSort?: (sort: import("./types.js").SortState[]) => void;
|
|
44
|
+
onFilter?: (filters: import("./types.js").FilterState[]) => void;
|
|
45
|
+
onPageChange?: (page: number, perPage: number) => void;
|
|
46
|
+
onSelect?: ((selectedRows: any[]) => void) | undefined;
|
|
47
|
+
onRowClick?: ((row: any, event: MouseEvent) => void) | undefined;
|
|
48
|
+
onEdit?: ((row: any, data: Record<string, any>) => void | Promise<void>) | undefined;
|
|
49
|
+
onCellEdit?: ((row: any, columnKey: string, newValue: any, oldValue: any) => void | Promise<void>) | undefined;
|
|
50
|
+
onCreate?: (data: Record<string, any>) => void | Promise<void>;
|
|
51
|
+
onDelete?: ((rows: any[]) => void | Promise<void>) | undefined;
|
|
52
|
+
virtualScroll?: boolean;
|
|
53
|
+
virtualRowHeight?: number;
|
|
54
|
+
responsive?: boolean;
|
|
55
|
+
groupBy?: string;
|
|
56
|
+
expandable?: boolean;
|
|
57
|
+
emptyText?: string;
|
|
58
|
+
loadingText?: string;
|
|
59
|
+
className?: string;
|
|
60
|
+
compact?: boolean;
|
|
61
|
+
striped?: boolean;
|
|
62
|
+
hover?: boolean;
|
|
63
|
+
bordered?: boolean;
|
|
64
|
+
classNames?: import("./types.js").DataTableClassNames;
|
|
65
|
+
unstyled?: boolean;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
register(app: Container): Promise<void>;
|
|
15
70
|
publishables(): {
|
|
16
71
|
routes: {
|
|
17
72
|
source: string;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
var m=class{_state;_listeners=new Set;_columns=[];_rowIdFn;_stateSaveKey=null;_lastSelectedId=null;_paginate=!0;constructor(t){this._columns=t.columns,this._stateSaveKey=t.stateSaveKey??null,this._paginate=t.paginate!==!1,this._rowIdFn=typeof t.rowId=="function"?t.rowId:s=>s[t.rowId??"id"];let e=this._loadState();this._state={allRows:t.data??[],filteredRows:[],sortedRows:[],paginatedRows:[],sort:e?.sort??[],filters:e?.filters??[],globalSearch:e?.globalSearch??"",pagination:{page:e?.page??1,perPage:t.perPage??15,total:0,lastPage:1},selectedIds:new Set,columnVisibility:e?.columnVisibility??Object.fromEntries(t.columns.map(s=>[s.key,s.visible!==!1])),columnOrder:e?.columnOrder??t.columns.map(s=>s.key),loading:!1,error:null,editingRowId:null,editingColumn:null,editorMode:null,formData:{},validationErrors:{},draw:0,excelFocusedCell:null,excelEditingCell:null,excelEditValue:""},this._recompute()}subscribe(t){return this._listeners.add(t),()=>this._listeners.delete(t)}getState(){return this._state}_notify(){for(let t of this._listeners)t()}_saveState(){if(this._stateSaveKey)try{let t={sort:this._state.sort,filters:this._state.filters,globalSearch:this._state.globalSearch,page:this._state.pagination.page,columnVisibility:this._state.columnVisibility,columnOrder:this._state.columnOrder};localStorage.setItem(this._stateSaveKey,JSON.stringify(t))}catch{}}_loadState(){if(!this._stateSaveKey)return null;try{let t=localStorage.getItem(this._stateSaveKey);return t?JSON.parse(t):null}catch{return null}}setData(t){this._state={...this._state,allRows:t},this._recompute()}setSort(t){this._state={...this._state,sort:t,pagination:{...this._state.pagination,page:1}},this._recompute(),this._saveState()}toggleSort(t,e=!1){let s=this._state.sort.find(i=>i.column===t),a;s?s.direction==="asc"?a=e?this._state.sort.map(i=>i.column===t?{...i,direction:"desc"}:i):[{column:t,direction:"desc"}]:a=e?this._state.sort.filter(i=>i.column!==t):[]:a=e?[...this._state.sort,{column:t,direction:"asc"}]:[{column:t,direction:"asc"}],this.setSort(a)}setGlobalSearch(t){this._state={...this._state,globalSearch:t,pagination:{...this._state.pagination,page:1}},this._recompute(),this._saveState()}setFilters(t){this._state={...this._state,filters:t,pagination:{...this._state.pagination,page:1}},this._recompute(),this._saveState()}setColumnFilter(t,e,s="like"){let a=this._state.filters.filter(i=>i.column!==t);e!==""&&e!==null&&e!==void 0&&a.push({column:t,value:e,operator:s}),this.setFilters(a)}setPage(t){let e=this._state.pagination.lastPage,s=Math.max(1,Math.min(t,e));this._state={...this._state,pagination:{...this._state.pagination,page:s}},this._recompute(),this._saveState()}setPerPage(t){this._state={...this._state,pagination:{...this._state.pagination,perPage:t,page:1}},this._recompute(),this._saveState()}toggleColumnVisibility(t){let e={...this._state.columnVisibility};e[t]=!e[t],this._state={...this._state,columnVisibility:e},this._notify(),this._saveState()}setColumnVisibility(t){this._state={...this._state,columnVisibility:t},this._notify(),this._saveState()}reorderColumns(t){this._state={...this._state,columnOrder:t},this._notify(),this._saveState()}getRowId(t){return this._rowIdFn(t)}toggleSelect(t){let e=new Set(this._state.selectedIds);e.has(t)?e.delete(t):e.add(t),this._lastSelectedId=t,this._state={...this._state,selectedIds:e},this._notify()}getLastSelectedId(){return this._lastSelectedId}selectSingle(t){this._state={...this._state,selectedIds:new Set([t])},this._notify()}selectAll(){let t=this._state.filteredRows.map(e=>this.getRowId(e));this._state={...this._state,selectedIds:new Set(t)},this._notify()}deselectAll(){this._state={...this._state,selectedIds:new Set},this._notify()}selectRange(t,e){let s=this._state.sortedRows,a=s.findIndex(n=>this.getRowId(n)===t),i=s.findIndex(n=>this.getRowId(n)===e);if(a===-1||i===-1)return;let o=Math.min(a,i),d=Math.max(a,i),r=new Set(this._state.selectedIds);for(let n=o;n<=d;n++)r.add(this.getRowId(s[n]));this._state={...this._state,selectedIds:r},this._notify()}getSelectedRows(){return this._state.allRows.filter(t=>this._state.selectedIds.has(this.getRowId(t)))}openEditor(t,e,s){let a=t!==null?this._state.allRows.find(o=>this.getRowId(o)===t):null,i=a?{...a}:{};this._state={...this._state,editingRowId:t,editingColumn:e,editorMode:s,formData:i,validationErrors:{}},this._notify()}closeEditor(){this._state={...this._state,editingRowId:null,editingColumn:null,editorMode:null,formData:{},validationErrors:{}},this._notify()}setFormField(t,e){this._state={...this._state,formData:{...this._state.formData,[t]:e}},this._notify()}setValidationErrors(t){this._state={...this._state,validationErrors:t},this._notify()}setLoading(t){this._state={...this._state,loading:t},this._notify()}setError(t){this._state={...this._state,error:t},this._notify()}setServerResponse(t){this._state={...this._state,allRows:t.data,filteredRows:t.data,sortedRows:t.data,paginatedRows:t.data,loading:!1,draw:t.draw,pagination:{...this._state.pagination,total:t.recordsFiltered,lastPage:Math.ceil(t.recordsFiltered/this._state.pagination.perPage)||1}},this._notify()}resetState(){if(this._state={...this._state,sort:[],filters:[],globalSearch:"",pagination:{...this._state.pagination,page:1},selectedIds:new Set},this._recompute(),this._stateSaveKey)try{localStorage.removeItem(this._stateSaveKey)}catch{}}focusCell(t,e){this._state={...this._state,excelFocusedCell:{rowIndex:t,columnKey:e}},this._notify()}startCellEdit(){let t=this._state.excelFocusedCell;if(!t)return;let e=this._state.paginatedRows[t.rowIndex];if(!e)return;let s=this._columns.find(a=>a.key===t.columnKey);!s||s.editable===!1||(this._state={...this._state,excelEditingCell:{...t},excelEditValue:e[t.columnKey]!=null?String(e[t.columnKey]):""},this._notify())}setExcelEditValue(t){this._state={...this._state,excelEditValue:t}}commitCellEdit(){let t=this._state.excelEditingCell;if(!t)return null;let e=this._state.paginatedRows[t.rowIndex];if(!e)return this.cancelCellEdit(),null;let s=this._columns.find(o=>o.key===t.columnKey),a=e[t.columnKey],i=this._state.excelEditValue;return s?.type==="number"?i=i===""?null:Number(i):s?.type==="boolean"?i=i==="true"||i==="1":i===""&&(i=null),this._state={...this._state,excelEditingCell:null,excelEditValue:""},i===a||i===null&&a==null?(this._notify(),null):(e[t.columnKey]=i,this._notify(),{row:e,columnKey:t.columnKey,oldValue:a,newValue:i})}cancelCellEdit(){this._state={...this._state,excelEditingCell:null,excelEditValue:""},this._notify()}clearExcelFocus(){this._state={...this._state,excelFocusedCell:null,excelEditingCell:null,excelEditValue:""},this._notify()}getVisibleColumns(){return this._state.columnOrder.filter(t=>this._state.columnVisibility[t]!==!1).map(t=>this._columns.find(e=>e.key===t)).filter(Boolean)}navigateCell(t){let e=this._state.excelFocusedCell,s=this._state.paginatedRows,a=this.getVisibleColumns();if(a.length===0)return null;if(!e){let r=a.find(n=>n.editable!==!1);return r&&s.length>0&&this.focusCell(0,r.key),null}let{rowIndex:i,columnKey:o}=e,d=a.findIndex(r=>r.key===o);if(d===-1)return null;switch(t){case"left":{for(let r=d-1;r>=0;r--)if(a[r].editable!==!1)return this.focusCell(i,a[r].key),null;for(let r=a.length-1;r>=0;r--)if(a[r].editable!==!1)return i>0?(this.focusCell(i-1,a[r].key),null):"page-prev";return null}case"right":{for(let r=d+1;r<a.length;r++)if(a[r].editable!==!1)return this.focusCell(i,a[r].key),null;for(let r=0;r<a.length;r++)if(a[r].editable!==!1)return i<s.length-1?(this.focusCell(i+1,a[r].key),null):"page-next";return null}case"up":return i>0?(this.focusCell(i-1,o),null):"page-prev";case"down":return i<s.length-1?(this.focusCell(i+1,o),null):"page-next"}}_recompute(){let t=[...this._state.allRows];if(this._state.globalSearch){let n=this._state.globalSearch.toLowerCase(),u=this._columns.filter(l=>l.searchable!==!1);t=t.filter(l=>u.some(p=>{let h=l[p.key];return h!=null&&String(h).toLowerCase().includes(n)}))}for(let n of this._state.filters)t=t.filter(u=>{let l=u[n.column];switch(n.operator){case"=":return l==n.value;case"!=":return l!=n.value;case">":return l>n.value;case"<":return l<n.value;case">=":return l>=n.value;case"<=":return l<=n.value;case"like":return l!=null&&String(l).toLowerCase().includes(String(n.value).toLowerCase());case"not_like":return l==null||!String(l).toLowerCase().includes(String(n.value).toLowerCase());case"in":return Array.isArray(n.value)&&n.value.includes(l);case"between":return Array.isArray(n.value)&&l>=n.value[0]&&l<=n.value[1];case"null":return l==null;case"not_null":return l!=null;default:return!0}});let e=t;this._state.sort.length>0&&(t=[...t].sort((n,u)=>{for(let l of this._state.sort){let p=this._columns.find(E=>E.key===l.column),h=n[l.column],g=u[l.column],f=0;if(h==null?f=-1:g==null?f=1:p?.type==="number"?f=Number(h)-Number(g):p?.type==="date"?f=new Date(h).getTime()-new Date(g).getTime():p?.type==="boolean"?f=(h?1:0)-(g?1:0):f=String(h).localeCompare(String(g)),f!==0)return l.direction==="desc"?-f:f}return 0}));let s=t,a=e.length,i=this._state.pagination.perPage,o,d,r;if(this._paginate){r=Math.ceil(a/i)||1,d=Math.min(this._state.pagination.page,r);let n=(d-1)*i;o=s.slice(n,n+i)}else o=s,d=1,r=1;this._state={...this._state,filteredRows:e,sortedRows:s,paginatedRows:o,pagination:{page:d,perPage:i,total:a,lastPage:r}},this._notify()}};function F(c="XSRF-TOKEN"){if(typeof document>"u")return null;let t=c.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),e=document.cookie.match(new RegExp(`(?:^|;\\s*)${t}=([^;]*)`));return e?decodeURIComponent(e[1]):null}var y=class extends m{_serverUrl;_serverMethod;_drawCounter=0;_abortController=null;_fetchDebounceTimer=null;_serverColumns;_csrfCookieName;_csrfHeaderName;constructor(t){super(t),this._serverUrl=t.serverUrl,this._serverMethod=t.serverMethod??"GET",this._csrfCookieName=t.csrfCookieName??"XSRF-TOKEN",this._csrfHeaderName=t.csrfHeaderName??"X-CSRF-Token",this._serverColumns=t.columns.map(e=>({data:e.key,name:e.key,searchable:e.searchable!==!1,orderable:e.sortable!==!1}))}_buildHeaders(t={}){let e={...t},s=F(this._csrfCookieName);return s&&(e[this._csrfHeaderName]=s),e}setSort(t){let e=this.getState();this._state={...e,sort:t,pagination:{...e.pagination,page:1}},this._debouncedFetch()}setGlobalSearch(t){let e=this.getState();this._state={...e,globalSearch:t,pagination:{...e.pagination,page:1}},this._debouncedFetch()}setFilters(t){let e=this.getState();this._state={...e,filters:t,pagination:{...e.pagination,page:1}},this._debouncedFetch()}setPage(t){let e=this.getState();this._state={...e,pagination:{...e.pagination,page:t}},this._fetchFromServer()}setPerPage(t){let e=this.getState();this._state={...e,pagination:{...e.pagination,perPage:t,page:1}},this._fetchFromServer()}_debouncedFetch(){this._fetchDebounceTimer&&clearTimeout(this._fetchDebounceTimer),this._fetchDebounceTimer=setTimeout(()=>this._fetchFromServer(),300)}async _fetchFromServer(){this._abortController&&this._abortController.abort(),this._abortController=new AbortController;let t=this.getState();this.setLoading(!0),this.setError(null);let e=++this._drawCounter,s={draw:e,start:(t.pagination.page-1)*t.pagination.perPage,length:t.pagination.perPage,search:{value:t.globalSearch,regex:!1},order:t.sort.map(a=>({column:this._serverColumns.findIndex(i=>i.data===a.column),dir:a.direction})),columns:this._serverColumns.map(a=>({...a,search:{value:t.filters.find(i=>i.column===a.data)?.value??"",regex:!1}}))};try{let a;if(this._serverMethod==="POST")a=await fetch(this._serverUrl,{method:"POST",headers:this._buildHeaders({"Content-Type":"application/json"}),body:JSON.stringify(s),signal:this._abortController.signal});else{let o=new URLSearchParams;o.set("draw",String(s.draw)),o.set("start",String(s.start)),o.set("length",String(s.length)),o.set("search[value]",s.search.value),o.set("search[regex]",String(s.search.regex)),s.order.forEach((r,n)=>{o.set(`order[${n}][column]`,String(r.column)),o.set(`order[${n}][dir]`,r.dir)}),s.columns.forEach((r,n)=>{o.set(`columns[${n}][data]`,r.data),o.set(`columns[${n}][name]`,r.name),o.set(`columns[${n}][searchable]`,String(r.searchable)),o.set(`columns[${n}][orderable]`,String(r.orderable)),o.set(`columns[${n}][search][value]`,r.search.value),o.set(`columns[${n}][search][regex]`,String(r.search.regex))});let d=`${this._serverUrl}?${o.toString()}`;a=await fetch(d,{signal:this._abortController.signal})}if(!a.ok)throw new Error(`Server responded with ${a.status}`);let i=await a.json();i.draw===e&&this.setServerResponse(i)}catch(a){if(a.name==="AbortError")return;this.setLoading(!1),this.setError(a.message??"Failed to fetch data")}}async initialFetch(){await this._fetchFromServer()}destroy(){this._abortController&&this._abortController.abort(),this._fetchDebounceTimer&&clearTimeout(this._fetchDebounceTimer)}};function v(c){if(c==null)return"";let t=String(c);return t.includes(",")||t.includes('"')||t.includes(`
|
|
2
|
-
`)||t.includes("\r")?'"'+t.replace(/"/g,'""')+'"':t}function C(
|
|
3
|
-
`)}function x(
|
|
4
|
-
`);await navigator.clipboard.writeText(
|
|
1
|
+
var g=class{_state;_listeners=new Set;_columns=[];_rowIdFn;_stateSaveKey=null;_lastSelectedId=null;_paginate=!0;constructor(t){this._columns=t.columns,this._stateSaveKey=t.stateSaveKey??null,this._paginate=t.paginate!==!1,this._rowIdFn=typeof t.rowId=="function"?t.rowId:a=>a[t.rowId??"id"];let e=this._loadState();this._state={allRows:t.data??[],filteredRows:[],sortedRows:[],paginatedRows:[],sort:e?.sort??[],filters:e?.filters??[],globalSearch:e?.globalSearch??"",pagination:{page:e?.page??1,perPage:t.perPage??15,total:0,lastPage:1},selectedIds:new Set,columnVisibility:e?.columnVisibility??Object.fromEntries(t.columns.map(a=>[a.key,a.visible!==!1])),columnOrder:e?.columnOrder??t.columns.map(a=>a.key),loading:!1,error:null,editingRowId:null,editingColumn:null,editorMode:null,formData:{},validationErrors:{},draw:0,excelFocusedCell:null,excelEditingCell:null,excelEditValue:""},this._recompute()}subscribe(t){return this._listeners.add(t),()=>this._listeners.delete(t)}getState(){return this._state}_notify(){for(let t of this._listeners)t()}_saveState(){if(this._stateSaveKey)try{let t={sort:this._state.sort,filters:this._state.filters,globalSearch:this._state.globalSearch,page:this._state.pagination.page,columnVisibility:this._state.columnVisibility,columnOrder:this._state.columnOrder};localStorage.setItem(this._stateSaveKey,JSON.stringify(t))}catch{}}_loadState(){if(!this._stateSaveKey)return null;try{let t=localStorage.getItem(this._stateSaveKey);return t?JSON.parse(t):null}catch{return null}}setData(t){this._state={...this._state,allRows:t},this._recompute()}setSort(t){this._state={...this._state,sort:t,pagination:{...this._state.pagination,page:1}},this._recompute(),this._saveState()}toggleSort(t,e=!1){let a=this._state.sort.find(s=>s.column===t),r;a?a.direction==="asc"?r=e?this._state.sort.map(s=>s.column===t?{...s,direction:"desc"}:s):[{column:t,direction:"desc"}]:r=e?this._state.sort.filter(s=>s.column!==t):[]:r=e?[...this._state.sort,{column:t,direction:"asc"}]:[{column:t,direction:"asc"}],this.setSort(r)}setGlobalSearch(t){this._state={...this._state,globalSearch:t,pagination:{...this._state.pagination,page:1}},this._recompute(),this._saveState()}setFilters(t){this._state={...this._state,filters:t,pagination:{...this._state.pagination,page:1}},this._recompute(),this._saveState()}setColumnFilter(t,e,a="like"){let r=this._state.filters.filter(s=>s.column!==t);e!==""&&e!==null&&e!==void 0&&r.push({column:t,value:e,operator:a}),this.setFilters(r)}setPage(t){let e=this._state.pagination.lastPage,a=Math.max(1,Math.min(t,e));this._state={...this._state,pagination:{...this._state.pagination,page:a}},this._recompute(),this._saveState()}setPerPage(t){this._state={...this._state,pagination:{...this._state.pagination,perPage:t,page:1}},this._recompute(),this._saveState()}toggleColumnVisibility(t){let e={...this._state.columnVisibility};e[t]=!e[t],this._state={...this._state,columnVisibility:e},this._notify(),this._saveState()}setColumnVisibility(t){this._state={...this._state,columnVisibility:t},this._notify(),this._saveState()}reorderColumns(t){this._state={...this._state,columnOrder:t},this._notify(),this._saveState()}getRowId(t){return this._rowIdFn(t)}toggleSelect(t){let e=new Set(this._state.selectedIds);e.has(t)?e.delete(t):e.add(t),this._lastSelectedId=t,this._state={...this._state,selectedIds:e},this._notify()}getLastSelectedId(){return this._lastSelectedId}selectSingle(t){this._state={...this._state,selectedIds:new Set([t])},this._notify()}selectAll(){let t=this._state.filteredRows.map(e=>this.getRowId(e));this._state={...this._state,selectedIds:new Set(t)},this._notify()}deselectAll(){this._state={...this._state,selectedIds:new Set},this._notify()}selectRange(t,e){let a=this._state.sortedRows,r=a.findIndex(i=>this.getRowId(i)===t),s=a.findIndex(i=>this.getRowId(i)===e);if(r===-1||s===-1)return;let c=Math.min(r,s),o=Math.max(r,s),n=new Set(this._state.selectedIds);for(let i=c;i<=o;i++)n.add(this.getRowId(a[i]));this._state={...this._state,selectedIds:n},this._notify()}getSelectedRows(){return this._state.allRows.filter(t=>this._state.selectedIds.has(this.getRowId(t)))}openEditor(t,e,a){let r=t!==null?this._state.allRows.find(c=>this.getRowId(c)===t):null,s=r?{...r}:{};this._state={...this._state,editingRowId:t,editingColumn:e,editorMode:a,formData:s,validationErrors:{}},this._notify()}closeEditor(){this._state={...this._state,editingRowId:null,editingColumn:null,editorMode:null,formData:{},validationErrors:{}},this._notify()}setFormField(t,e){this._state={...this._state,formData:{...this._state.formData,[t]:e}},this._notify()}setValidationErrors(t){this._state={...this._state,validationErrors:t},this._notify()}setLoading(t){this._state={...this._state,loading:t},this._notify()}setError(t){this._state={...this._state,error:t},this._notify()}setServerResponse(t){this._state={...this._state,allRows:t.data,filteredRows:t.data,sortedRows:t.data,paginatedRows:t.data,loading:!1,draw:t.draw,pagination:{...this._state.pagination,total:t.recordsFiltered,lastPage:Math.ceil(t.recordsFiltered/this._state.pagination.perPage)||1}},this._notify()}resetState(){if(this._state={...this._state,sort:[],filters:[],globalSearch:"",pagination:{...this._state.pagination,page:1},selectedIds:new Set},this._recompute(),this._stateSaveKey)try{localStorage.removeItem(this._stateSaveKey)}catch{}}focusCell(t,e){this._state={...this._state,excelFocusedCell:{rowIndex:t,columnKey:e}},this._notify()}startCellEdit(){let t=this._state.excelFocusedCell;if(!t)return;let e=this._state.paginatedRows[t.rowIndex];if(!e)return;let a=this._columns.find(r=>r.key===t.columnKey);!a||a.editable===!1||(this._state={...this._state,excelEditingCell:{...t},excelEditValue:e[t.columnKey]!=null?String(e[t.columnKey]):""},this._notify())}setExcelEditValue(t){this._state={...this._state,excelEditValue:t}}commitCellEdit(){let t=this._state.excelEditingCell;if(!t)return null;let e=this._state.paginatedRows[t.rowIndex];if(!e)return this.cancelCellEdit(),null;let a=this._columns.find(c=>c.key===t.columnKey),r=e[t.columnKey],s=this._state.excelEditValue;return a?.type==="number"?s=s===""?null:Number(s):a?.type==="boolean"?s=s==="true"||s==="1":s===""&&(s=null),this._state={...this._state,excelEditingCell:null,excelEditValue:""},s===r||s===null&&r==null?(this._notify(),null):(e[t.columnKey]=s,this._notify(),{row:e,columnKey:t.columnKey,oldValue:r,newValue:s})}cancelCellEdit(){this._state={...this._state,excelEditingCell:null,excelEditValue:""},this._notify()}clearExcelFocus(){this._state={...this._state,excelFocusedCell:null,excelEditingCell:null,excelEditValue:""},this._notify()}getVisibleColumns(){return this._state.columnOrder.filter(t=>this._state.columnVisibility[t]!==!1).map(t=>this._columns.find(e=>e.key===t)).filter(Boolean)}navigateCell(t){let e=this._state.excelFocusedCell,a=this._state.paginatedRows,r=this.getVisibleColumns();if(r.length===0)return null;if(!e){let n=r.find(i=>i.editable!==!1);return n&&a.length>0&&this.focusCell(0,n.key),null}let{rowIndex:s,columnKey:c}=e,o=r.findIndex(n=>n.key===c);if(o===-1)return null;switch(t){case"left":{for(let n=o-1;n>=0;n--)if(r[n].editable!==!1)return this.focusCell(s,r[n].key),null;for(let n=r.length-1;n>=0;n--)if(r[n].editable!==!1)return s>0?(this.focusCell(s-1,r[n].key),null):"page-prev";return null}case"right":{for(let n=o+1;n<r.length;n++)if(r[n].editable!==!1)return this.focusCell(s,r[n].key),null;for(let n=0;n<r.length;n++)if(r[n].editable!==!1)return s<a.length-1?(this.focusCell(s+1,r[n].key),null):"page-next";return null}case"up":return s>0?(this.focusCell(s-1,c),null):"page-prev";case"down":return s<a.length-1?(this.focusCell(s+1,c),null):"page-next"}}_recompute(){let t=[...this._state.allRows];if(this._state.globalSearch){let i=this._state.globalSearch.toLowerCase(),d=this._columns.filter(l=>l.searchable!==!1);t=t.filter(l=>d.some(p=>{let h=l[p.key];return h!=null&&String(h).toLowerCase().includes(i)}))}for(let i of this._state.filters)t=t.filter(d=>{let l=d[i.column];switch(i.operator){case"=":return l==i.value;case"!=":return l!=i.value;case">":return l>i.value;case"<":return l<i.value;case">=":return l>=i.value;case"<=":return l<=i.value;case"like":return l!=null&&String(l).toLowerCase().includes(String(i.value).toLowerCase());case"not_like":return l==null||!String(l).toLowerCase().includes(String(i.value).toLowerCase());case"in":return Array.isArray(i.value)&&i.value.includes(l);case"between":return Array.isArray(i.value)&&l>=i.value[0]&&l<=i.value[1];case"null":return l==null;case"not_null":return l!=null;default:return!0}});let e=t;this._state.sort.length>0&&(t=[...t].sort((i,d)=>{for(let l of this._state.sort){let p=this._columns.find(E=>E.key===l.column),h=i[l.column],m=d[l.column],f=0;if(h==null?f=-1:m==null?f=1:p?.type==="number"?f=Number(h)-Number(m):p?.type==="date"?f=new Date(h).getTime()-new Date(m).getTime():p?.type==="boolean"?f=(h?1:0)-(m?1:0):f=String(h).localeCompare(String(m)),f!==0)return l.direction==="desc"?-f:f}return 0}));let a=t,r=e.length,s=this._state.pagination.perPage,c,o,n;if(this._paginate){n=Math.ceil(r/s)||1,o=Math.min(this._state.pagination.page,n);let i=(o-1)*s;c=a.slice(i,i+s)}else c=a,o=1,n=1;this._state={...this._state,filteredRows:e,sortedRows:a,paginatedRows:c,pagination:{page:o,perPage:s,total:r,lastPage:n}},this._notify()}};function F(u="XSRF-TOKEN"){if(typeof document>"u")return null;let t=u.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),e=document.cookie.match(new RegExp(`(?:^|;\\s*)${t}=([^;]*)`));return e?decodeURIComponent(e[1]):null}var y=class extends g{_serverUrl;_serverMethod;_serverParams;_drawCounter=0;_abortController=null;_fetchDebounceTimer=null;_serverColumns;_csrfCookieName;_csrfHeaderName;constructor(t){super(t),this._serverUrl=t.serverUrl,this._serverMethod=t.serverMethod??"GET",this._serverParams=t.serverParams,this._csrfCookieName=t.csrfCookieName??"XSRF-TOKEN",this._csrfHeaderName=t.csrfHeaderName??"X-CSRF-Token",this._serverColumns=t.columns.map(e=>({data:e.key,name:e.key,searchable:e.searchable!==!1,orderable:e.sortable!==!1}))}_buildHeaders(t={}){let e={...t},a=F(this._csrfCookieName);return a&&(e[this._csrfHeaderName]=a),e}_resolveServerParams(){return this._serverParams?typeof this._serverParams=="function"?this._serverParams():this._serverParams:{}}setSort(t){let e=this.getState();this._state={...e,sort:t,pagination:{...e.pagination,page:1}},this._debouncedFetch()}setGlobalSearch(t){let e=this.getState();this._state={...e,globalSearch:t,pagination:{...e.pagination,page:1}},this._debouncedFetch()}setFilters(t){let e=this.getState();this._state={...e,filters:t,pagination:{...e.pagination,page:1}},this._debouncedFetch()}setPage(t){let e=this.getState();this._state={...e,pagination:{...e.pagination,page:t}},this._fetchFromServer()}setPerPage(t){let e=this.getState();this._state={...e,pagination:{...e.pagination,perPage:t,page:1}},this._fetchFromServer()}_debouncedFetch(){this._fetchDebounceTimer&&clearTimeout(this._fetchDebounceTimer),this._fetchDebounceTimer=setTimeout(()=>this._fetchFromServer(),300)}async _fetchFromServer(){this._abortController&&this._abortController.abort(),this._abortController=new AbortController;let t=this.getState(),e=this._resolveServerParams();this.setLoading(!0),this.setError(null);let a=++this._drawCounter,r={draw:a,start:(t.pagination.page-1)*t.pagination.perPage,length:t.pagination.perPage,search:{value:t.globalSearch,regex:!1},customFilters:e,order:t.sort.map(s=>({column:this._serverColumns.findIndex(c=>c.data===s.column),dir:s.direction})),columns:this._serverColumns.map(s=>({...s,search:{value:t.filters.find(c=>c.column===s.data)?.value??"",regex:!1}}))};try{let s;if(this._serverMethod==="POST")s=await fetch(this._serverUrl,{method:"POST",headers:this._buildHeaders({"Content-Type":"application/json"}),body:JSON.stringify(r),signal:this._abortController.signal});else{let o=new URLSearchParams;o.set("draw",String(r.draw)),o.set("start",String(r.start)),o.set("length",String(r.length)),o.set("search[value]",r.search.value),o.set("search[regex]",String(r.search.regex)),Object.entries(e).forEach(([i,d])=>{d!=null&&d!==""&&o.set(`filters[${i}]`,String(d))}),r.order.forEach((i,d)=>{o.set(`order[${d}][column]`,String(i.column)),o.set(`order[${d}][dir]`,i.dir)}),r.columns.forEach((i,d)=>{o.set(`columns[${d}][data]`,i.data),o.set(`columns[${d}][name]`,i.name),o.set(`columns[${d}][searchable]`,String(i.searchable)),o.set(`columns[${d}][orderable]`,String(i.orderable)),o.set(`columns[${d}][search][value]`,i.search.value),o.set(`columns[${d}][search][regex]`,String(i.search.regex))});let n=`${this._serverUrl}?${o.toString()}`;s=await fetch(n,{signal:this._abortController.signal})}if(!s.ok)throw new Error(`Server responded with ${s.status}`);let c=await s.json();c.draw===a&&this.setServerResponse(c)}catch(s){if(s.name==="AbortError")return;this.setLoading(!1),this.setError(s.message??"Failed to fetch data")}}async initialFetch(){await this._fetchFromServer()}destroy(){this._abortController&&this._abortController.abort(),this._fetchDebounceTimer&&clearTimeout(this._fetchDebounceTimer)}};function w(u){if(u==null)return"";let t=String(u);return t.includes(",")||t.includes('"')||t.includes(`
|
|
2
|
+
`)||t.includes("\r")?'"'+t.replace(/"/g,'""')+'"':t}function C(u,t){let e=t.filter(s=>s.visible!==!1),a=e.map(s=>w(s.header)).join(","),r=u.map(s=>e.map(c=>w(s[c.key])).join(","));return"\uFEFF"+[a,...r].join(`\r
|
|
3
|
+
`)}function x(u,t="export.csv"){let e=new Blob([u],{type:"text/csv;charset=utf-8;"});S(e,t)}function S(u,t){let e=URL.createObjectURL(u),a=document.createElement("a");a.href=e,a.download=t,document.body.appendChild(a),a.click(),document.body.removeChild(a),URL.revokeObjectURL(e)}async function T(u,t){let e=t.filter(c=>c.visible!==!1),a=e.map(c=>c.header).join(" "),r=u.map(c=>e.map(o=>{let n=c[o.key];return n==null?"":String(n)}).join(" ")),s=[a,...r].join(`
|
|
4
|
+
`);await navigator.clipboard.writeText(s)}function b(u,t,e){let a=t.filter(n=>n.visible!==!1),r=a.map(n=>`<th style="border:1px solid #ddd;padding:8px 12px;text-align:left;background:#f5f5f5;font-weight:600;">${_(n.header)}</th>`).join(""),s=u.map(n=>`<tr>${a.map(d=>{let l=n[d.key];return`<td style="border:1px solid #ddd;padding:8px 12px;">${_(l==null?"":String(l))}</td>`}).join("")}</tr>`).join(""),c=`<!DOCTYPE html>
|
|
5
5
|
<html><head>
|
|
6
6
|
<title>${_(e??"Data Export")}</title>
|
|
7
7
|
<style>
|
|
@@ -13,7 +13,7 @@ var m=class{_state;_listeners=new Set;_columns=[];_rowIdFn;_stateSaveKey=null;_l
|
|
|
13
13
|
</head><body>
|
|
14
14
|
${e?`<h1>${_(e)}</h1>`:""}
|
|
15
15
|
<table>
|
|
16
|
-
<thead><tr>${
|
|
17
|
-
<tbody>${
|
|
16
|
+
<thead><tr>${r}</tr></thead>
|
|
17
|
+
<tbody>${s}</tbody>
|
|
18
18
|
</table>
|
|
19
|
-
</body></html>`,
|
|
19
|
+
</body></html>`,o=window.open("","_blank");o&&(o.document.write(c),o.document.close(),o.focus(),setTimeout(()=>o.print(),250))}function _(u){return u.replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}async function R(u,t,e="export.xlsx"){let a;try{a=await import("exceljs")}catch{throw new Error("exceljs is required for Excel export. Install it: npm install exceljs")}let r=new a.Workbook,s=r.addWorksheet("Data"),c=t.filter(d=>d.visible!==!1);s.columns=c.map(d=>({header:d.header,key:d.key,width:20}));let o=s.getRow(1);o.font={bold:!0},o.fill={type:"pattern",pattern:"solid",fgColor:{argb:"FFF0F0F0"}};for(let d of u){let l={};for(let p of c)l[p.key]=d[p.key]??"";s.addRow(l)}let n=await r.xlsx.writeBuffer(),i=new Blob([n],{type:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"});S(i,e)}function D(u,t,e="export.pdf"){b(u,t,e.replace(".pdf",""))}var v=class{async export(t,e,a,r){switch(t){case"csv":{let s=C(e,a);x(s,r??"export.csv");break}case"excel":await R(e,a,r??"export.xlsx");break;case"pdf":D(e,a,r??"export.pdf");break;case"clipboard":await T(e,a);break;case"print":b(e,a);break}}};export{g as DataTableStore,v as ExportManager,y as ServerDataTableStore};
|
package/dist/plugin.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{Plugin as
|
|
1
|
+
import{Plugin as f}from"@beeblock/svelar/plugins";import{fileURLToPath as c}from"url";import{dirname as o,join as r}from"path";import{readFileSync as u}from"fs";var a={prefix:"/api",defaults:{perPage:15,perPageOptions:[10,15,25,50,100],searchDebounceMs:300,sortable:!0,searchable:!0,paginate:!0,selectable:"none",striped:!0,hover:!0,bordered:!1,compact:!1,responsive:!0,virtualScroll:!1,virtualRowHeight:48}},g=o(c(import.meta.url)),s=o(g),p=r(s,"src","publishable");function d(){let i=u(r(s,"package.json"),"utf8"),e=JSON.parse(i);return{name:e.name??"@beeblock/svelar-datatable",version:e.version??"0.0.0"}}var l=d(),n=l.name,b=l.version,t=class extends f{name=n;version=b;description="Full-featured DataTable plugin for Svelar \u2014 sorting, searching, pagination, inline editing, export, and server-side processing";_config;constructor(e={}){super(),this._config={...a,...e,defaults:{...a.defaults,...e.defaults}}}get datatableConfig(){return this._config}static defaults(){return{...a.defaults}}config(){return{key:"datatable",defaults:{prefix:this._config.prefix,defaults:{...this._config.defaults}}}}async register(e){e.instance("datatable.config",this.datatableConfig),e.instance(`${n}.config`,this.datatableConfig)}publishables(){return{routes:[{source:r(p,"routes/datatable.ts"),dest:"src/routes/api/datatable/+server.ts",type:"asset"}]}}};var x=t;export{t as SvelarDatatablePlugin,x as default};
|
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import type { DataTableRequest, DataTableResponse } from '../types.js';
|
|
2
|
+
export type DataTableServerFilterHandler = (query: any, value: any, context: {
|
|
3
|
+
request: DataTableRequest;
|
|
4
|
+
filter: string;
|
|
5
|
+
}) => any | void;
|
|
2
6
|
export interface DataTableServiceOptions {
|
|
3
7
|
searchable?: string[];
|
|
4
8
|
orderable?: string[];
|
|
5
9
|
baseQuery?: (query: any) => any;
|
|
6
10
|
scopes?: Record<string, (query: any) => any>;
|
|
7
11
|
computedColumns?: Record<string, string>;
|
|
12
|
+
filters?: Record<string, DataTableServerFilterHandler>;
|
|
13
|
+
searchDriver?: 'database' | 'meilisearch' | 'auto';
|
|
14
|
+
meilisearchFilter?: (filters: Record<string, any>, request: DataTableRequest) => string | string[] | undefined;
|
|
15
|
+
meilisearchSort?: (request: DataTableRequest) => string[] | undefined;
|
|
8
16
|
}
|
|
9
17
|
export declare class DataTableService<T = any> {
|
|
10
18
|
private _model;
|
|
@@ -14,6 +22,11 @@ export declare class DataTableService<T = any> {
|
|
|
14
22
|
private _scopes;
|
|
15
23
|
private _computedColumns;
|
|
16
24
|
private _activeScopes;
|
|
25
|
+
private _filters;
|
|
26
|
+
private _activeFilters;
|
|
27
|
+
private _searchDriver;
|
|
28
|
+
private _meilisearchFilter?;
|
|
29
|
+
private _meilisearchSort?;
|
|
17
30
|
constructor(model: any, options?: DataTableServiceOptions);
|
|
18
31
|
searchable(columns: string[]): this;
|
|
19
32
|
orderable(columns: string[]): this;
|
|
@@ -21,5 +34,13 @@ export declare class DataTableService<T = any> {
|
|
|
21
34
|
addScope(name: string, fn: (query: any) => any): this;
|
|
22
35
|
applyScope(name: string): this;
|
|
23
36
|
addComputedColumn(name: string, sqlExpr: string): this;
|
|
37
|
+
addFilter(name: string, fn: DataTableServerFilterHandler): this;
|
|
38
|
+
applyFilter(name: string, value: any): this;
|
|
39
|
+
applyFilters(filters?: Record<string, any>): this;
|
|
40
|
+
private searchOperator;
|
|
41
|
+
private shouldUseMeilisearch;
|
|
42
|
+
private sortableColumns;
|
|
43
|
+
private meilisearchSort;
|
|
44
|
+
private processWithMeilisearch;
|
|
24
45
|
process(request: DataTableRequest): Promise<DataTableResponse<T>>;
|
|
25
46
|
}
|
package/dist/server/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
import{Connection as v}from"@beeblock/svelar/database";var m=class{_model;_searchable=[];_orderable=[];_baseQueryFn=null;_scopes={};_computedColumns={};_activeScopes=[];_filters={};_activeFilters={};_searchDriver="auto";_meilisearchFilter;_meilisearchSort;constructor(e,r){this._model=e,r?.searchable&&(this._searchable=r.searchable),r?.orderable&&(this._orderable=r.orderable),r?.baseQuery&&(this._baseQueryFn=r.baseQuery),r?.scopes&&(this._scopes=r.scopes),r?.computedColumns&&(this._computedColumns=r.computedColumns),r?.filters&&(this._filters=r.filters),r?.searchDriver&&(this._searchDriver=r.searchDriver),r?.meilisearchFilter&&(this._meilisearchFilter=r.meilisearchFilter),r?.meilisearchSort&&(this._meilisearchSort=r.meilisearchSort)}searchable(e){return this._searchable=e,this}orderable(e){return this._orderable=e,this}setBaseQuery(e){return this._baseQueryFn=e,this}addScope(e,r){return this._scopes[e]=r,this}applyScope(e){return this._activeScopes.push(e),this}addComputedColumn(e,r){return this._computedColumns[e]=r,this}addFilter(e,r){return this._filters[e]=r,this}applyFilter(e,r){return r!=null&&r!==""&&(this._activeFilters[e]=r),this}applyFilters(e={}){for(let[r,i]of Object.entries(e))this.applyFilter(r,i);return this}searchOperator(){try{return v.getDriver(this._model.connection)==="postgres"?"ILIKE":"LIKE"}catch{return"LIKE"}}shouldUseMeilisearch(e){return!e.search.value||this._searchDriver==="database"?!1:typeof this._model.search=="function"}sortableColumns(e){return this._orderable.length>0?this._orderable:e.columns.filter(r=>r.orderable).map(r=>r.data)}meilisearchSort(e){let r=this._meilisearchSort?.(e);if(r)return r;let i=this.sortableColumns(e),l=e.order.map(t=>{let n=e.columns[t.column];return!n||!i.includes(n.data)?null:`${n.data}:${t.dir}`}).filter(Boolean);return l.length>0?l:void 0}async processWithMeilisearch(e,r,i){let l={limit:e.length,offset:e.start},t=this._meilisearchFilter?.(i,e);t&&(l.filter=t);let n=this.meilisearchSort(e);n&&(l.sort=n);let h=await this._model.search(e.search.value,l);return{draw:e.draw,recordsTotal:r,recordsFiltered:h.estimatedTotalHits??h.hits.length,data:h.hits}}async process(e){try{let r=this._model.query();this._baseQueryFn&&this._baseQueryFn(r);let i=await r.count(),l={...e.customFilters,...this._activeFilters};if(this.shouldUseMeilisearch(e))try{return await this.processWithMeilisearch(e,i,l)}catch(a){if(this._searchDriver==="meilisearch")throw a}let t=this._model.query();this._baseQueryFn&&this._baseQueryFn(t);for(let a of this._activeScopes){let o=this._scopes[a];o&&o(t)}for(let[a,o]of Object.entries(l)){let d=this._filters[a];if(d&&o!==void 0&&o!==null&&o!==""){let c=d(t,o,{request:e,filter:a});c&&(t=c)}}for(let[a,o]of Object.entries(this._computedColumns))t=t.selectRaw(`(${o}) as ${a}`);if(e.search.value){let a=e.search.value,o=this.searchOperator(),d=this._searchable.length>0?this._searchable:e.columns.filter(c=>c.searchable).map(c=>c.data);d.length>0&&(t=t.whereNested(c=>{for(let f=0;f<d.length;f++){let _=d[f];f===0?c.where(_,o,`%${a}%`):c.orWhere(_,o,`%${a}%`)}}))}for(let a of e.columns)a.search.value&&a.searchable&&(t=t.where(a.data,this.searchOperator(),`%${a.search.value}%`));let h=await t.clone().count();for(let a of e.order){let o=e.columns[a.column];o&&this.sortableColumns(e).includes(o.data)&&(t=t.orderBy(o.data,a.dir))}t=t.offset(e.start).limit(e.length);let u=await t.get();return{draw:e.draw,recordsTotal:i,recordsFiltered:h,data:u}}catch(r){return{draw:e.draw,recordsTotal:0,recordsFiltered:0,data:[],error:r.message??"Server error"}}}};import{Controller as T}from"@beeblock/svelar/routing";function b(s,e,r=0){let i=Number.parseInt(s??"",10);return Number.isFinite(i)?Math.max(r,i):e}function D(s){return s==="desc"?"desc":"asc"}function p(s){let e=b(s.get("draw"),1,1),r=b(s.get("start"),0,0),i=b(s.get("length"),15,1),l={value:s.get("search[value]")??"",regex:s.get("search[regex]")==="true"},t=[],n=0;for(;s.has(`order[${n}][column]`);)t.push({column:b(s.get(`order[${n}][column]`),0,0),dir:D(s.get(`order[${n}][dir]`))}),n++;let h=[],u=0;for(;s.has(`columns[${u}][data]`);)h.push({data:s.get(`columns[${u}][data]`)??"",name:s.get(`columns[${u}][name]`)??"",searchable:s.get(`columns[${u}][searchable]`)!=="false",orderable:s.get(`columns[${u}][orderable]`)!=="false",search:{value:s.get(`columns[${u}][search][value]`)??"",regex:s.get(`columns[${u}][search][regex]`)==="true"}}),u++;let a={};for(let[o,d]of s.entries()){let c=o.match(/^filters\[(.+)\]$/);c&&d!==""&&(a[c[1]]=d)}return{draw:e,start:r,length:i,search:l,order:t,columns:h,...Object.keys(a).length>0?{customFilters:a}:{}}}async function y(s){return await s.json()}var g=class extends T{_model;_options;constructor(e,r){super(),this._model=e,this._options=r}async query(e){try{let r=new m(this._model,this._options),i;if(e.request.method==="POST"?i=await y(e.request):i=p(e.url.searchParams),this._options?.filters&&e.url?.searchParams){let t={};for(let n of Object.keys(this._options.filters)){let h=e.url.searchParams.get(`filters[${n}]`)??e.url.searchParams.get(n);h!==null&&(t[n]=h)}r.applyFilters(t)}let l=await r.process(i);return this.json(l)}catch(r){let i={draw:0,recordsTotal:0,recordsFiltered:0,data:[],error:r.message??"Internal server error"};return this.json(i,500)}}};export{g as DataTableController,m as DataTableService,p as parseDataTableRequest,y as parseDataTableRequestFromBody};
|
|
@@ -3,6 +3,7 @@ import { DataTableStore } from './DataTableStore.js';
|
|
|
3
3
|
export declare class ServerDataTableStore<T = any> extends DataTableStore<T> {
|
|
4
4
|
private _serverUrl;
|
|
5
5
|
private _serverMethod;
|
|
6
|
+
private _serverParams?;
|
|
6
7
|
private _drawCounter;
|
|
7
8
|
private _abortController;
|
|
8
9
|
private _fetchDebounceTimer;
|
|
@@ -11,6 +12,7 @@ export declare class ServerDataTableStore<T = any> extends DataTableStore<T> {
|
|
|
11
12
|
private _csrfHeaderName;
|
|
12
13
|
constructor(config: DataTableConfig<T>);
|
|
13
14
|
private _buildHeaders;
|
|
15
|
+
private _resolveServerParams;
|
|
14
16
|
setSort(sort: SortState[]): void;
|
|
15
17
|
setGlobalSearch(search: string): void;
|
|
16
18
|
setFilters(filters: FilterState[]): void;
|
package/dist/types.d.ts
CHANGED
|
@@ -51,6 +51,7 @@ export interface DataTableRequest {
|
|
|
51
51
|
column: number;
|
|
52
52
|
dir: 'asc' | 'desc';
|
|
53
53
|
}[];
|
|
54
|
+
customFilters?: Record<string, any>;
|
|
54
55
|
columns: {
|
|
55
56
|
data: string;
|
|
56
57
|
name: string;
|
|
@@ -136,6 +137,7 @@ export interface DataTableConfig<T = any> {
|
|
|
136
137
|
data?: T[];
|
|
137
138
|
serverUrl?: string;
|
|
138
139
|
serverMethod?: 'GET' | 'POST';
|
|
140
|
+
serverParams?: Record<string, any> | (() => Record<string, any>);
|
|
139
141
|
csrfCookieName?: string;
|
|
140
142
|
csrfHeaderName?: string;
|
|
141
143
|
columns: ColumnDef<T>[];
|
package/package.json
CHANGED
|
@@ -1,76 +1,82 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@beeblock/svelar-datatable",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Full-featured DataTable plugin for Svelar — sorting, searching, pagination, inline editing, export, and server-side processing",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"keywords": [
|
|
7
|
-
"svelar-plugin",
|
|
8
|
-
"datatable",
|
|
9
|
-
"svelte",
|
|
10
|
-
"sveltekit",
|
|
11
|
-
"data-grid"
|
|
12
|
-
],
|
|
13
|
-
"license": "MIT",
|
|
14
|
-
"main": "dist/index.js",
|
|
15
|
-
"types": "dist/index.d.ts",
|
|
16
|
-
"svelte": "./src/ui/index.ts",
|
|
17
|
-
"exports": {
|
|
18
|
-
"./package.json": "./package.json",
|
|
19
|
-
".": {
|
|
20
|
-
"types": "./dist/index.d.ts",
|
|
21
|
-
"default": "./dist/index.js"
|
|
22
|
-
},
|
|
23
|
-
"./plugin": {
|
|
24
|
-
"types": "./dist/plugin.d.ts",
|
|
25
|
-
"default": "./dist/plugin.js"
|
|
26
|
-
},
|
|
27
|
-
"./server": {
|
|
28
|
-
"types": "./dist/server/index.d.ts",
|
|
29
|
-
"default": "./dist/server/index.js"
|
|
30
|
-
},
|
|
31
|
-
"./types": {
|
|
32
|
-
"types": "./dist/types.d.ts",
|
|
33
|
-
"default": "./dist/types.js"
|
|
34
|
-
},
|
|
35
|
-
"./ui": {
|
|
36
|
-
"types": "./src/ui/index.ts",
|
|
37
|
-
"svelte": "./src/ui/index.ts",
|
|
38
|
-
"import": "./src/ui/index.ts"
|
|
39
|
-
},
|
|
40
|
-
"./ui/*": {
|
|
41
|
-
"svelte": "./src/ui/*",
|
|
42
|
-
"import": "./src/ui/*"
|
|
43
|
-
}
|
|
44
|
-
},
|
|
45
|
-
"files": [
|
|
46
|
-
"dist",
|
|
47
|
-
"src/ui",
|
|
48
|
-
"src/state",
|
|
49
|
-
"src/export",
|
|
50
|
-
"src/types.ts",
|
|
51
|
-
"src/index.ts",
|
|
52
|
-
"src/publishable",
|
|
53
|
-
"LICENSE"
|
|
54
|
-
],
|
|
55
|
-
"scripts": {
|
|
56
|
-
"build": "tsup &&
|
|
57
|
-
"dev": "tsup --watch"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
"exceljs":
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@beeblock/svelar-datatable",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Full-featured DataTable plugin for Svelar — sorting, searching, pagination, inline editing, export, and server-side processing",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"svelar-plugin",
|
|
8
|
+
"datatable",
|
|
9
|
+
"svelte",
|
|
10
|
+
"sveltekit",
|
|
11
|
+
"data-grid"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"main": "dist/index.js",
|
|
15
|
+
"types": "dist/index.d.ts",
|
|
16
|
+
"svelte": "./src/ui/index.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
"./package.json": "./package.json",
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./plugin": {
|
|
24
|
+
"types": "./dist/plugin.d.ts",
|
|
25
|
+
"default": "./dist/plugin.js"
|
|
26
|
+
},
|
|
27
|
+
"./server": {
|
|
28
|
+
"types": "./dist/server/index.d.ts",
|
|
29
|
+
"default": "./dist/server/index.js"
|
|
30
|
+
},
|
|
31
|
+
"./types": {
|
|
32
|
+
"types": "./dist/types.d.ts",
|
|
33
|
+
"default": "./dist/types.js"
|
|
34
|
+
},
|
|
35
|
+
"./ui": {
|
|
36
|
+
"types": "./src/ui/index.ts",
|
|
37
|
+
"svelte": "./src/ui/index.ts",
|
|
38
|
+
"import": "./src/ui/index.ts"
|
|
39
|
+
},
|
|
40
|
+
"./ui/*": {
|
|
41
|
+
"svelte": "./src/ui/*",
|
|
42
|
+
"import": "./src/ui/*"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist",
|
|
47
|
+
"src/ui",
|
|
48
|
+
"src/state",
|
|
49
|
+
"src/export",
|
|
50
|
+
"src/types.ts",
|
|
51
|
+
"src/index.ts",
|
|
52
|
+
"src/publishable",
|
|
53
|
+
"LICENSE"
|
|
54
|
+
],
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "tsup && tsc --emitDeclarationOnly",
|
|
57
|
+
"dev": "tsup --watch",
|
|
58
|
+
"lint": "tsc --noEmit",
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"prepublishOnly": "npm run build && npm run test"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"@beeblock/svelar": ">=0.6.7",
|
|
64
|
+
"exceljs": ">=4.4.0",
|
|
65
|
+
"svelte": "^5.0.0"
|
|
66
|
+
},
|
|
67
|
+
"peerDependenciesMeta": {
|
|
68
|
+
"exceljs": {
|
|
69
|
+
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"@beeblock/svelar": {
|
|
72
|
+
"optional": false
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
"devDependencies": {
|
|
76
|
+
"@types/node": "^22.0.0",
|
|
77
|
+
"tsup": "^8.5.0",
|
|
78
|
+
"typescript": "^5.7.0",
|
|
79
|
+
"vitest": "^4.1.8",
|
|
80
|
+
"svelte": "^5.0.0"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -11,6 +11,7 @@ function getCsrfToken(cookieName = 'XSRF-TOKEN'): string | null {
|
|
|
11
11
|
export class ServerDataTableStore<T = any> extends DataTableStore<T> {
|
|
12
12
|
private _serverUrl: string;
|
|
13
13
|
private _serverMethod: 'GET' | 'POST';
|
|
14
|
+
private _serverParams?: Record<string, any> | (() => Record<string, any>);
|
|
14
15
|
private _drawCounter = 0;
|
|
15
16
|
private _abortController: AbortController | null = null;
|
|
16
17
|
private _fetchDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -22,6 +23,7 @@ export class ServerDataTableStore<T = any> extends DataTableStore<T> {
|
|
|
22
23
|
super(config);
|
|
23
24
|
this._serverUrl = config.serverUrl!;
|
|
24
25
|
this._serverMethod = config.serverMethod ?? 'GET';
|
|
26
|
+
this._serverParams = config.serverParams;
|
|
25
27
|
this._csrfCookieName = config.csrfCookieName ?? 'XSRF-TOKEN';
|
|
26
28
|
this._csrfHeaderName = config.csrfHeaderName ?? 'X-CSRF-Token';
|
|
27
29
|
this._serverColumns = config.columns.map((c) => ({
|
|
@@ -41,6 +43,11 @@ export class ServerDataTableStore<T = any> extends DataTableStore<T> {
|
|
|
41
43
|
return headers;
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
private _resolveServerParams(): Record<string, any> {
|
|
47
|
+
if (!this._serverParams) return {};
|
|
48
|
+
return typeof this._serverParams === 'function' ? this._serverParams() : this._serverParams;
|
|
49
|
+
}
|
|
50
|
+
|
|
44
51
|
override setSort(sort: SortState[]) {
|
|
45
52
|
const state = this.getState();
|
|
46
53
|
// Update sort in state without recomputing locally
|
|
@@ -82,6 +89,7 @@ export class ServerDataTableStore<T = any> extends DataTableStore<T> {
|
|
|
82
89
|
this._abortController = new AbortController();
|
|
83
90
|
|
|
84
91
|
const state = this.getState();
|
|
92
|
+
const customFilters = this._resolveServerParams();
|
|
85
93
|
this.setLoading(true);
|
|
86
94
|
this.setError(null);
|
|
87
95
|
|
|
@@ -91,6 +99,7 @@ export class ServerDataTableStore<T = any> extends DataTableStore<T> {
|
|
|
91
99
|
start: (state.pagination.page - 1) * state.pagination.perPage,
|
|
92
100
|
length: state.pagination.perPage,
|
|
93
101
|
search: { value: state.globalSearch, regex: false },
|
|
102
|
+
customFilters,
|
|
94
103
|
order: state.sort.map((s) => ({
|
|
95
104
|
column: this._serverColumns.findIndex((c) => c.data === s.column),
|
|
96
105
|
dir: s.direction,
|
|
@@ -121,6 +130,11 @@ export class ServerDataTableStore<T = any> extends DataTableStore<T> {
|
|
|
121
130
|
params.set('length', String(request.length));
|
|
122
131
|
params.set('search[value]', request.search.value);
|
|
123
132
|
params.set('search[regex]', String(request.search.regex));
|
|
133
|
+
Object.entries(customFilters).forEach(([key, value]) => {
|
|
134
|
+
if (value !== undefined && value !== null && value !== '') {
|
|
135
|
+
params.set(`filters[${key}]`, String(value));
|
|
136
|
+
}
|
|
137
|
+
});
|
|
124
138
|
|
|
125
139
|
request.order.forEach((o, i) => {
|
|
126
140
|
params.set(`order[${i}][column]`, String(o.column));
|
package/src/types.ts
CHANGED
|
@@ -55,6 +55,7 @@ export interface DataTableRequest {
|
|
|
55
55
|
length: number;
|
|
56
56
|
search: { value: string; regex: boolean };
|
|
57
57
|
order: { column: number; dir: 'asc' | 'desc' }[];
|
|
58
|
+
customFilters?: Record<string, any>;
|
|
58
59
|
columns: {
|
|
59
60
|
data: string;
|
|
60
61
|
name: string;
|
|
@@ -144,6 +145,7 @@ export interface DataTableConfig<T = any> {
|
|
|
144
145
|
data?: T[];
|
|
145
146
|
serverUrl?: string;
|
|
146
147
|
serverMethod?: 'GET' | 'POST';
|
|
148
|
+
serverParams?: Record<string, any> | (() => Record<string, any>);
|
|
147
149
|
// CSRF token config (defaults to Svelar conventions)
|
|
148
150
|
csrfCookieName?: string;
|
|
149
151
|
csrfHeaderName?: string;
|
package/src/ui/DataTable.svelte
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
data,
|
|
25
25
|
serverUrl,
|
|
26
26
|
serverMethod,
|
|
27
|
+
serverParams,
|
|
27
28
|
// Columns
|
|
28
29
|
columns,
|
|
29
30
|
// Features
|
|
@@ -84,7 +85,7 @@
|
|
|
84
85
|
|
|
85
86
|
// Build config object
|
|
86
87
|
let config: DataTableConfig = $derived({
|
|
87
|
-
data, serverUrl, serverMethod, columns, sortable, searchable, paginate,
|
|
88
|
+
data, serverUrl, serverMethod, serverParams, columns, sortable, searchable, paginate,
|
|
88
89
|
selectable, perPage, perPageOptions, searchDebounceMs, stateSaveKey,
|
|
89
90
|
rowId, rowClass, buttons, editorMode, editorFields,
|
|
90
91
|
onSort, onFilter, onPageChange, onSelect, onRowClick, onEdit, onCellEdit, onCreate, onDelete,
|
|
@@ -93,6 +94,7 @@
|
|
|
93
94
|
});
|
|
94
95
|
|
|
95
96
|
// Create store
|
|
97
|
+
// svelte-ignore state_referenced_locally
|
|
96
98
|
let store: DataTableStore = $state(
|
|
97
99
|
serverUrl
|
|
98
100
|
? new ServerDataTableStore(config)
|
|
@@ -104,6 +106,8 @@
|
|
|
104
106
|
storeRef = store;
|
|
105
107
|
});
|
|
106
108
|
|
|
109
|
+
// svelte-ignore state_referenced_locally
|
|
110
|
+
|
|
107
111
|
let state = $state(store.getState());
|
|
108
112
|
|
|
109
113
|
// Subscribe to store changes
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
}
|
|
13
13
|
let { fields, store, anchorEl = null, classNames = {}, onsubmit }: Props = $props();
|
|
14
14
|
|
|
15
|
+
// svelte-ignore state_referenced_locally
|
|
16
|
+
|
|
15
17
|
let state = $state(store.getState());
|
|
16
18
|
$effect(() => {
|
|
17
19
|
return store.subscribe(() => {
|
|
@@ -57,6 +59,7 @@
|
|
|
57
59
|
|
|
58
60
|
{#if isOpen}
|
|
59
61
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
62
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
60
63
|
<div class="sdt-bubble-backdrop" onclick={close}></div>
|
|
61
64
|
<div class="sdt-bubble-editor" style={popoverStyle}>
|
|
62
65
|
<DataTableEditorForm
|
|
@@ -10,6 +10,14 @@
|
|
|
10
10
|
let { buttons, store, columns }: Props = $props();
|
|
11
11
|
|
|
12
12
|
const exportManager = new ExportManager();
|
|
13
|
+
let state = $state(store.getState());
|
|
14
|
+
let selectedRows = $derived(state.allRows.filter((row) => state.selectedIds.has(store.getRowId(row))));
|
|
15
|
+
|
|
16
|
+
$effect(() => {
|
|
17
|
+
return store.subscribe(() => {
|
|
18
|
+
state = store.getState();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
13
21
|
|
|
14
22
|
const exportLabels: Record<ExportFormat, string> = {
|
|
15
23
|
csv: 'CSV',
|
|
@@ -32,15 +40,13 @@
|
|
|
32
40
|
|
|
33
41
|
async function handleAction(btn: ButtonDef) {
|
|
34
42
|
if (typeof btn.action === 'function') {
|
|
35
|
-
|
|
36
|
-
const state = store.getState();
|
|
37
|
-
await btn.action(selected, state.allRows);
|
|
43
|
+
await btn.action(selectedRows, state.allRows);
|
|
38
44
|
}
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
function isDisabled(btn: ButtonDef): boolean {
|
|
42
48
|
if (typeof btn.disabled === 'function') {
|
|
43
|
-
return btn.disabled(
|
|
49
|
+
return btn.disabled(selectedRows);
|
|
44
50
|
}
|
|
45
51
|
return btn.disabled ?? false;
|
|
46
52
|
}
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
}
|
|
12
12
|
let { columns, store, selectable = 'none', sortable = true, expandable = false, classNames = {} }: Props = $props();
|
|
13
13
|
|
|
14
|
+
// svelte-ignore state_referenced_locally
|
|
15
|
+
|
|
14
16
|
let state = $state(store.getState());
|
|
15
17
|
$effect(() => {
|
|
16
18
|
return store.subscribe(() => {
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
}
|
|
12
12
|
let { fields, store, title = 'Edit Record', classNames = {}, onsubmit }: Props = $props();
|
|
13
13
|
|
|
14
|
+
// svelte-ignore state_referenced_locally
|
|
15
|
+
|
|
14
16
|
let state = $state(store.getState());
|
|
15
17
|
$effect(() => {
|
|
16
18
|
return store.subscribe(() => {
|
|
@@ -40,6 +42,7 @@
|
|
|
40
42
|
|
|
41
43
|
{#if isOpen}
|
|
42
44
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
45
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
43
46
|
<div class="sdt-modal-backdrop {classNames.editorBackdrop ?? ''}" onclick={handleBackdrop}>
|
|
44
47
|
<div class="sdt-modal {classNames.editorModal ?? ''}" role="dialog" aria-modal="true">
|
|
45
48
|
<div class="sdt-modal-header">
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
classNames = {},
|
|
42
42
|
}: Props = $props();
|
|
43
43
|
|
|
44
|
+
// svelte-ignore state_referenced_locally
|
|
45
|
+
|
|
44
46
|
let state = $state(store.getState());
|
|
45
47
|
$effect(() => {
|
|
46
48
|
return store.subscribe(() => {
|
|
@@ -113,11 +115,28 @@
|
|
|
113
115
|
>
|
|
114
116
|
{#if selectable === 'multi'}
|
|
115
117
|
<td class="sdt-cell sdt-cell-checkbox {classNames.td ?? ''}">
|
|
116
|
-
<input
|
|
118
|
+
<input
|
|
119
|
+
type="checkbox"
|
|
120
|
+
checked={isSelected}
|
|
121
|
+
onclick={(e) => e.stopPropagation()}
|
|
122
|
+
onchange={(e) => {
|
|
123
|
+
e.stopPropagation();
|
|
124
|
+
store.toggleSelect(rowId);
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
117
127
|
</td>
|
|
118
128
|
{:else if selectable === 'single'}
|
|
119
129
|
<td class="sdt-cell sdt-cell-checkbox {classNames.td ?? ''}">
|
|
120
|
-
<input
|
|
130
|
+
<input
|
|
131
|
+
type="radio"
|
|
132
|
+
checked={isSelected}
|
|
133
|
+
name="sdt-select"
|
|
134
|
+
onclick={(e) => e.stopPropagation()}
|
|
135
|
+
onchange={(e) => {
|
|
136
|
+
e.stopPropagation();
|
|
137
|
+
store.selectSingle(rowId);
|
|
138
|
+
}}
|
|
139
|
+
/>
|
|
121
140
|
</td>
|
|
122
141
|
{/if}
|
|
123
142
|
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
}
|
|
10
10
|
let { store, debounceMs = 300, placeholder = 'Search...', searchInputClass = '' }: Props = $props();
|
|
11
11
|
|
|
12
|
+
// svelte-ignore state_referenced_locally
|
|
13
|
+
|
|
12
14
|
let inputValue = $state(store.getState().globalSearch);
|
|
13
15
|
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
14
16
|
|