@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 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.4.0`, `svelte ^5.0.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
- The server controller handles pagination, sorting, searching, and filtering — compatible with the jQuery DataTables wire protocol.
85
-
86
- ## Editing
87
-
88
- Four editor modes, configured via `editorMode`:
89
-
90
- ```svelte
91
- <!-- Modal editor -->
92
- <DataTable editorMode="modal" editorFields={fields} onEdit={save} onCreate={create} />
93
-
94
- <!-- Bubble (popover anchored to row) -->
95
- <DataTable editorMode="bubble" editorFields={fields} onEdit={save} />
96
-
97
- <!-- Inline (double-click a cell) -->
98
- <DataTable editorMode="inline" onCellEdit={saveCellEdit} />
99
-
100
- <!-- Excel (spreadsheet navigation) -->
101
- <DataTable editorMode="excel" onCellEdit={saveCellEdit} />
102
- ```
103
-
104
- ## Tailwind Customization
105
-
106
- Style every element with Tailwind utility classes via the `classNames` prop:
107
-
108
- ```svelte
109
- <DataTable
110
- {data}
111
- {columns}
112
- striped={false}
113
- hover={false}
114
- classNames={{
115
- container: '!bg-slate-900 !border-slate-800',
116
- thead: '!bg-slate-950',
117
- th: '!bg-slate-950 !text-slate-400 !tracking-widest',
118
- tr: 'hover:!bg-slate-800',
119
- td: '!text-slate-300 !border-b-slate-800',
120
- pagination: '!border-t-slate-800 !text-slate-400 !bg-slate-900',
121
- pageButton: '!bg-slate-800 !text-slate-400 !border-slate-700',
122
- searchInput: '!bg-slate-800 !text-slate-200 !border-slate-700',
123
- }}
124
- />
125
- ```
126
-
127
- 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`.
128
-
129
- ## Imports
130
-
131
- ```ts
132
- // UI component (Svelte source — not compiled)
133
- import { DataTable } from '@beeblock/svelar-datatable/ui';
134
-
135
- // Types
136
- import type {
137
- ColumnDef, EditorFieldDef, ButtonDef, DataTableClassNames,
138
- DataTableConfig, DataTableState, ExportFormat,
139
- } from '@beeblock/svelar-datatable';
140
-
141
- // Stores
142
- import { DataTableStore, ServerDataTableStore } from '@beeblock/svelar-datatable';
143
-
144
- // Server controller (API routes)
145
- import { DataTableController, DataTableService } from '@beeblock/svelar-datatable/server';
146
- ```
147
-
148
- ## Documentation
149
-
150
- Full documentation with all props, callbacks, store API, and examples: [svelar.dev/docs/datatable](https://svelar.dev/docs/datatable)
151
-
152
- ## License
153
-
154
- MIT
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 = "svelar-datatable";
9
- readonly version = "0.1.4";
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(c,t){let e=t.filter(i=>i.visible!==!1),s=e.map(i=>v(i.header)).join(","),a=c.map(i=>e.map(o=>v(i[o.key])).join(","));return"\uFEFF"+[s,...a].join(`\r
3
- `)}function x(c,t="export.csv"){let e=new Blob([c],{type:"text/csv;charset=utf-8;"});S(e,t)}function S(c,t){let e=URL.createObjectURL(c),s=document.createElement("a");s.href=e,s.download=t,document.body.appendChild(s),s.click(),document.body.removeChild(s),URL.revokeObjectURL(e)}async function T(c,t){let e=t.filter(o=>o.visible!==!1),s=e.map(o=>o.header).join(" "),a=c.map(o=>e.map(d=>{let r=o[d.key];return r==null?"":String(r)}).join(" ")),i=[s,...a].join(`
4
- `);await navigator.clipboard.writeText(i)}function b(c,t,e){let s=t.filter(r=>r.visible!==!1),a=s.map(r=>`<th style="border:1px solid #ddd;padding:8px 12px;text-align:left;background:#f5f5f5;font-weight:600;">${_(r.header)}</th>`).join(""),i=c.map(r=>`<tr>${s.map(u=>{let l=r[u.key];return`<td style="border:1px solid #ddd;padding:8px 12px;">${_(l==null?"":String(l))}</td>`}).join("")}</tr>`).join(""),o=`<!DOCTYPE html>
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>${a}</tr></thead>
17
- <tbody>${i}</tbody>
16
+ <thead><tr>${r}</tr></thead>
17
+ <tbody>${s}</tbody>
18
18
  </table>
19
- </body></html>`,d=window.open("","_blank");d&&(d.document.write(o),d.document.close(),d.focus(),setTimeout(()=>d.print(),250))}function _(c){return c.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}async function R(c,t,e="export.xlsx"){let s;try{s=await import("exceljs")}catch{throw new Error("exceljs is required for Excel export. Install it: npm install exceljs")}let a=new s.Workbook,i=a.addWorksheet("Data"),o=t.filter(u=>u.visible!==!1);i.columns=o.map(u=>({header:u.header,key:u.key,width:20}));let d=i.getRow(1);d.font={bold:!0},d.fill={type:"pattern",pattern:"solid",fgColor:{argb:"FFF0F0F0"}};for(let u of c){let l={};for(let p of o)l[p.key]=u[p.key]??"";i.addRow(l)}let r=await a.xlsx.writeBuffer(),n=new Blob([r],{type:"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"});S(n,e)}function D(c,t,e="export.pdf"){b(c,t,e.replace(".pdf",""))}var w=class{async export(t,e,s,a){switch(t){case"csv":{let i=C(e,s);x(i,a??"export.csv");break}case"excel":await R(e,s,a??"export.xlsx");break;case"pdf":D(e,s,a??"export.pdf");break;case"clipboard":await T(e,s);break;case"print":b(e,s);break}}};export{m as DataTableStore,w as ExportManager,y as ServerDataTableStore};
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,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}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 s}from"@beeblock/svelar/plugins";import{fileURLToPath as o}from"url";import{dirname as r,join as i}from"path";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}},l=r(o(import.meta.url)),n=r(l),u=i(n,"src","publishable"),e=class extends s{name="svelar-datatable";version="0.1.4";description="Full-featured DataTable plugin for Svelar \u2014 sorting, searching, pagination, inline editing, export, and server-side processing";_config;constructor(t={}){super(),this._config={...a,...t,defaults:{...a.defaults,...t.defaults}}}get datatableConfig(){return this._config}static defaults(){return{...a.defaults}}publishables(){return{routes:[{source:i(u,"routes/datatable.ts"),dest:"src/routes/api/datatable/+server.ts",type:"asset"}]}}};var m=e;export{e as SvelarDatatablePlugin,m as default};
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
  }
@@ -1 +1 @@
1
- var u=class{_model;_searchable=[];_orderable=[];_baseQueryFn=null;_scopes={};_computedColumns={};_activeScopes=[];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)}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}async process(e){try{let r=this._model.query();this._baseQueryFn&&this._baseQueryFn(r);let n=await r.count(),a=this._model.query();this._baseQueryFn&&this._baseQueryFn(a);for(let t of this._activeScopes){let o=this._scopes[t];o&&o(a)}for(let[t,o]of Object.entries(this._computedColumns))a=a.selectRaw(`(${o}) as ${t}`);if(e.search.value){let t=e.search.value,o=this._searchable.length>0?this._searchable:e.columns.filter(l=>l.searchable).map(l=>l.data);o.length>0&&(a=a.whereNested(l=>{for(let c=0;c<o.length;c++){let m=o[c];c===0?l.where(m,"LIKE",`%${t}%`):l.orWhere(m,"LIKE",`%${t}%`)}}))}for(let t of e.columns)t.search.value&&t.searchable&&(a=a.where(t.data,"LIKE",`%${t.search.value}%`));let i=await a.clone().count();for(let t of e.order){let o=e.columns[t.column];o&&(this._orderable.length>0?this._orderable:e.columns.filter(c=>c.orderable).map(c=>c.data)).includes(o.data)&&(a=a.orderBy(o.data,t.dir))}a=a.offset(e.start).limit(e.length);let h=await a.get();return{draw:e.draw,recordsTotal:n,recordsFiltered:i,data:h}}catch(r){return{draw:e.draw,recordsTotal:0,recordsFiltered:0,data:[],error:r.message??"Server error"}}}};import{Controller as g}from"@beeblock/svelar/routing";function p(s){let e=parseInt(s.get("draw")??"1",10),r=parseInt(s.get("start")??"0",10),n=parseInt(s.get("length")??"15",10),a={value:s.get("search[value]")??"",regex:s.get("search[regex]")==="true"},d=[],i=0;for(;s.has(`order[${i}][column]`);)d.push({column:parseInt(s.get(`order[${i}][column]`)??"0",10),dir:s.get(`order[${i}][dir]`)??"asc"}),i++;let h=[],t=0;for(;s.has(`columns[${t}][data]`);)h.push({data:s.get(`columns[${t}][data]`)??"",name:s.get(`columns[${t}][name]`)??"",searchable:s.get(`columns[${t}][searchable]`)!=="false",orderable:s.get(`columns[${t}][orderable]`)!=="false",search:{value:s.get(`columns[${t}][search][value]`)??"",regex:s.get(`columns[${t}][search][regex]`)==="true"}}),t++;return{draw:e,start:r,length:n,search:a,order:d,columns:h}}async function b(s){return await s.json()}var y=class extends g{_model;_options;constructor(e,r){super(),this._model=e,this._options=r}async query(e){try{let r=new u(this._model,this._options),n;e.request.method==="POST"?n=await b(e.request):n=p(e.url.searchParams);let a=await r.process(n);return this.json(a)}catch(r){let n={draw:0,recordsTotal:0,recordsFiltered:0,data:[],error:r.message??"Internal server error"};return this.json(n,500)}}};export{y as DataTableController,u as DataTableService,p as parseDataTableRequest,b as parseDataTableRequestFromBody};
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.1.8",
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 || echo 'Warning: tsc declaration emit had errors')",
57
- "dev": "tsup --watch"
58
- },
59
- "peerDependencies": {
60
- "@beeblock/svelar": ">=0.4.0",
61
- "svelte": "^5.0.0"
62
- },
63
- "peerDependenciesMeta": {
64
- "exceljs": {
65
- "optional": true
66
- },
67
- "@beeblock/svelar": {
68
- "optional": false
69
- }
70
- },
71
- "devDependencies": {
72
- "tsup": "^8.5.0",
73
- "typescript": "^5.7.0",
74
- "svelte": "^5.0.0"
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;
@@ -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
@@ -52,6 +52,8 @@
52
52
  containerHeight = 500,
53
53
  }: Props = $props();
54
54
 
55
+ // svelte-ignore state_referenced_locally
56
+
55
57
  let state = $state(store.getState());
56
58
  $effect(() => {
57
59
  return store.subscribe(() => {
@@ -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
- const selected = store.getSelectedRows();
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(store.getSelectedRows());
49
+ return btn.disabled(selectedRows);
44
50
  }
45
51
  return btn.disabled ?? false;
46
52
  }
@@ -21,6 +21,8 @@
21
21
 
22
22
  let value = $derived(row[column.key]);
23
23
 
24
+ // svelte-ignore state_referenced_locally
25
+
24
26
  let state = $state(store.getState());
25
27
  $effect(() => {
26
28
  return store.subscribe(() => {
@@ -8,6 +8,7 @@
8
8
  let { store, columns }: Props = $props();
9
9
 
10
10
  let open = $state(false);
11
+ // svelte-ignore state_referenced_locally
11
12
  let state = $state(store.getState());
12
13
 
13
14
  $effect(() => {
@@ -12,6 +12,8 @@
12
12
  }
13
13
  let { fields, store, classNames = {}, onsubmit, anchorEl = null }: 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(() => {
@@ -7,6 +7,8 @@
7
7
  }
8
8
  let { field, store }: Props = $props();
9
9
 
10
+ // svelte-ignore state_referenced_locally
11
+
10
12
  let state = $state(store.getState());
11
13
  $effect(() => {
12
14
  return store.subscribe(() => {
@@ -10,6 +10,8 @@
10
10
  }
11
11
  let { columns, store, selectable = 'none', expandable = false, classNames = {} }: Props = $props();
12
12
 
13
+ // svelte-ignore state_referenced_locally
14
+
13
15
  let state = $state(store.getState());
14
16
  $effect(() => {
15
17
  return store.subscribe(() => {
@@ -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">
@@ -8,6 +8,8 @@
8
8
  }
9
9
  let { store, perPageOptions = [10, 15, 25, 50, 100], classNames = {} }: Props = $props();
10
10
 
11
+ // svelte-ignore state_referenced_locally
12
+
11
13
  let state = $state(store.getState());
12
14
  $effect(() => {
13
15
  return store.subscribe(() => {
@@ -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 type="checkbox" checked={isSelected} onchange={() => store.toggleSelect(rowId)} />
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 type="radio" checked={isSelected} name="sdt-select" />
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
 
@@ -29,6 +29,8 @@
29
29
  onDeleteClick,
30
30
  }: Props = $props();
31
31
 
32
+ // svelte-ignore state_referenced_locally
33
+
32
34
  let state = $state(store.getState());
33
35
  $effect(() => {
34
36
  return store.subscribe(() => {