@beeblock/svelar-datatable 0.1.6 → 0.1.8

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.
@@ -0,0 +1,168 @@
1
+ import type { DataTableConfig, DataTableRequest, DataTableResponse, DataTableState, FilterState, SortState } from '../types.js';
2
+ import { DataTableStore } from './DataTableStore.js';
3
+
4
+ function getCsrfToken(cookieName = 'XSRF-TOKEN'): string | null {
5
+ if (typeof document === 'undefined') return null;
6
+ const escaped = cookieName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7
+ const match = document.cookie.match(new RegExp(`(?:^|;\\s*)${escaped}=([^;]*)`));
8
+ return match ? decodeURIComponent(match[1]) : null;
9
+ }
10
+
11
+ export class ServerDataTableStore<T = any> extends DataTableStore<T> {
12
+ private _serverUrl: string;
13
+ private _serverMethod: 'GET' | 'POST';
14
+ private _drawCounter = 0;
15
+ private _abortController: AbortController | null = null;
16
+ private _fetchDebounceTimer: ReturnType<typeof setTimeout> | null = null;
17
+ private _serverColumns: { data: string; name: string; searchable: boolean; orderable: boolean }[];
18
+ private _csrfCookieName: string;
19
+ private _csrfHeaderName: string;
20
+
21
+ constructor(config: DataTableConfig<T>) {
22
+ super(config);
23
+ this._serverUrl = config.serverUrl!;
24
+ this._serverMethod = config.serverMethod ?? 'GET';
25
+ this._csrfCookieName = config.csrfCookieName ?? 'XSRF-TOKEN';
26
+ this._csrfHeaderName = config.csrfHeaderName ?? 'X-CSRF-Token';
27
+ this._serverColumns = config.columns.map((c) => ({
28
+ data: c.key,
29
+ name: c.key,
30
+ searchable: c.searchable !== false,
31
+ orderable: c.sortable !== false,
32
+ }));
33
+ }
34
+
35
+ private _buildHeaders(extra: Record<string, string> = {}): Record<string, string> {
36
+ const headers: Record<string, string> = { ...extra };
37
+ const token = getCsrfToken(this._csrfCookieName);
38
+ if (token) {
39
+ headers[this._csrfHeaderName] = token;
40
+ }
41
+ return headers;
42
+ }
43
+
44
+ override setSort(sort: SortState[]) {
45
+ const state = this.getState();
46
+ // Update sort in state without recomputing locally
47
+ (this as any)._state = { ...state, sort, pagination: { ...state.pagination, page: 1 } };
48
+ this._debouncedFetch();
49
+ }
50
+
51
+ override setGlobalSearch(search: string) {
52
+ const state = this.getState();
53
+ (this as any)._state = { ...state, globalSearch: search, pagination: { ...state.pagination, page: 1 } };
54
+ this._debouncedFetch();
55
+ }
56
+
57
+ override setFilters(filters: FilterState[]) {
58
+ const state = this.getState();
59
+ (this as any)._state = { ...state, filters, pagination: { ...state.pagination, page: 1 } };
60
+ this._debouncedFetch();
61
+ }
62
+
63
+ override setPage(page: number) {
64
+ const state = this.getState();
65
+ (this as any)._state = { ...state, pagination: { ...state.pagination, page } };
66
+ this._fetchFromServer();
67
+ }
68
+
69
+ override setPerPage(perPage: number) {
70
+ const state = this.getState();
71
+ (this as any)._state = { ...state, pagination: { ...state.pagination, perPage, page: 1 } };
72
+ this._fetchFromServer();
73
+ }
74
+
75
+ private _debouncedFetch() {
76
+ if (this._fetchDebounceTimer) clearTimeout(this._fetchDebounceTimer);
77
+ this._fetchDebounceTimer = setTimeout(() => this._fetchFromServer(), 300);
78
+ }
79
+
80
+ async _fetchFromServer() {
81
+ if (this._abortController) this._abortController.abort();
82
+ this._abortController = new AbortController();
83
+
84
+ const state = this.getState();
85
+ this.setLoading(true);
86
+ this.setError(null);
87
+
88
+ const draw = ++this._drawCounter;
89
+ const request: DataTableRequest = {
90
+ draw,
91
+ start: (state.pagination.page - 1) * state.pagination.perPage,
92
+ length: state.pagination.perPage,
93
+ search: { value: state.globalSearch, regex: false },
94
+ order: state.sort.map((s) => ({
95
+ column: this._serverColumns.findIndex((c) => c.data === s.column),
96
+ dir: s.direction,
97
+ })),
98
+ columns: this._serverColumns.map((col) => ({
99
+ ...col,
100
+ search: {
101
+ value: state.filters.find((f) => f.column === col.data)?.value ?? '',
102
+ regex: false,
103
+ },
104
+ })),
105
+ };
106
+
107
+ try {
108
+ let response: Response;
109
+
110
+ if (this._serverMethod === 'POST') {
111
+ response = await fetch(this._serverUrl, {
112
+ method: 'POST',
113
+ headers: this._buildHeaders({ 'Content-Type': 'application/json' }),
114
+ body: JSON.stringify(request),
115
+ signal: this._abortController.signal,
116
+ });
117
+ } else {
118
+ const params = new URLSearchParams();
119
+ params.set('draw', String(request.draw));
120
+ params.set('start', String(request.start));
121
+ params.set('length', String(request.length));
122
+ params.set('search[value]', request.search.value);
123
+ params.set('search[regex]', String(request.search.regex));
124
+
125
+ request.order.forEach((o, i) => {
126
+ params.set(`order[${i}][column]`, String(o.column));
127
+ params.set(`order[${i}][dir]`, o.dir);
128
+ });
129
+
130
+ request.columns.forEach((c, i) => {
131
+ params.set(`columns[${i}][data]`, c.data);
132
+ params.set(`columns[${i}][name]`, c.name);
133
+ params.set(`columns[${i}][searchable]`, String(c.searchable));
134
+ params.set(`columns[${i}][orderable]`, String(c.orderable));
135
+ params.set(`columns[${i}][search][value]`, c.search.value);
136
+ params.set(`columns[${i}][search][regex]`, String(c.search.regex));
137
+ });
138
+
139
+ const url = `${this._serverUrl}?${params.toString()}`;
140
+ response = await fetch(url, { signal: this._abortController.signal });
141
+ }
142
+
143
+ if (!response.ok) {
144
+ throw new Error(`Server responded with ${response.status}`);
145
+ }
146
+
147
+ const json: DataTableResponse<T> = await response.json();
148
+
149
+ // Only accept if this is the latest draw
150
+ if (json.draw === draw) {
151
+ this.setServerResponse(json);
152
+ }
153
+ } catch (err: any) {
154
+ if (err.name === 'AbortError') return;
155
+ this.setLoading(false);
156
+ this.setError(err.message ?? 'Failed to fetch data');
157
+ }
158
+ }
159
+
160
+ async initialFetch() {
161
+ await this._fetchFromServer();
162
+ }
163
+
164
+ destroy() {
165
+ if (this._abortController) this._abortController.abort();
166
+ if (this._fetchDebounceTimer) clearTimeout(this._fetchDebounceTimer);
167
+ }
168
+ }
@@ -0,0 +1,2 @@
1
+ export { DataTableStore } from './DataTableStore.js';
2
+ export { ServerDataTableStore } from './ServerDataTableStore.js';
package/src/types.ts ADDED
@@ -0,0 +1,241 @@
1
+ import type { Component } from 'svelte';
2
+
3
+ // Column definition
4
+ export type ColumnType = 'string' | 'number' | 'date' | 'boolean' | 'html' | 'custom';
5
+ export type FilterOperator = '=' | '!=' | '>' | '<' | '>=' | '<=' | 'like' | 'not_like' | 'in' | 'between' | 'null' | 'not_null';
6
+ export type SelectionMode = 'none' | 'single' | 'multi';
7
+ export type EditorMode = 'inline' | 'bubble' | 'modal' | 'excel';
8
+ export type FieldType = 'text' | 'textarea' | 'number' | 'select' | 'multi-select' | 'checkbox' | 'radio' | 'date' | 'datetime' | 'upload' | 'hidden' | 'readonly';
9
+ export type ExportFormat = 'csv' | 'excel' | 'pdf' | 'clipboard' | 'print';
10
+
11
+ export interface ColumnDef<T = any> {
12
+ key: string;
13
+ header: string;
14
+ type?: ColumnType;
15
+ sortable?: boolean;
16
+ searchable?: boolean;
17
+ filterable?: boolean;
18
+ visible?: boolean;
19
+ width?: string;
20
+ minWidth?: string;
21
+ maxWidth?: string;
22
+ className?: string;
23
+ headerClassName?: string;
24
+ orderable?: boolean;
25
+ defaultSort?: 'asc' | 'desc';
26
+ // Footer
27
+ footer?: string | ((rows: T[]) => string | number);
28
+ // Editor
29
+ editable?: boolean;
30
+ editorField?: EditorFieldDef;
31
+ }
32
+
33
+ export interface SortState {
34
+ column: string;
35
+ direction: 'asc' | 'desc';
36
+ }
37
+
38
+ export interface FilterState {
39
+ column: string;
40
+ value: any;
41
+ operator: FilterOperator;
42
+ }
43
+
44
+ export interface PaginationState {
45
+ page: number;
46
+ perPage: number;
47
+ total: number;
48
+ lastPage: number;
49
+ }
50
+
51
+ // Server-side protocol (compatible with jQuery DataTables wire protocol)
52
+ export interface DataTableRequest {
53
+ draw: number;
54
+ start: number;
55
+ length: number;
56
+ search: { value: string; regex: boolean };
57
+ order: { column: number; dir: 'asc' | 'desc' }[];
58
+ columns: {
59
+ data: string;
60
+ name: string;
61
+ searchable: boolean;
62
+ orderable: boolean;
63
+ search: { value: string; regex: boolean };
64
+ }[];
65
+ }
66
+
67
+ export interface DataTableResponse<T = any> {
68
+ draw: number;
69
+ recordsTotal: number;
70
+ recordsFiltered: number;
71
+ data: T[];
72
+ error?: string;
73
+ }
74
+
75
+ // Editor
76
+ export interface EditorFieldDef {
77
+ name: string;
78
+ type: FieldType;
79
+ label: string;
80
+ placeholder?: string;
81
+ options?: { label: string; value: any }[];
82
+ multiple?: boolean;
83
+ required?: boolean;
84
+ disabled?: boolean;
85
+ className?: string;
86
+ dependsOn?: string;
87
+ dependsOnValue?: any;
88
+ showWhen?: (formData: Record<string, any>) => boolean;
89
+ defaultValue?: any;
90
+ }
91
+
92
+ // Buttons
93
+ export interface ButtonDef {
94
+ key: string;
95
+ label: string;
96
+ icon?: Component<any>;
97
+ action?: string | ((selectedRows: any[], allData: any[]) => void | Promise<void>);
98
+ variant?: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost';
99
+ disabled?: boolean | ((selectedRows: any[]) => boolean);
100
+ collection?: ButtonDef[];
101
+ }
102
+
103
+ // CSS class overrides for full Tailwind customization
104
+ export interface DataTableClassNames {
105
+ container?: string;
106
+ toolbar?: string;
107
+ toolbarLeft?: string;
108
+ toolbarRight?: string;
109
+ searchInput?: string;
110
+ table?: string;
111
+ thead?: string;
112
+ th?: string;
113
+ thSortable?: string;
114
+ tbody?: string;
115
+ tr?: string;
116
+ trSelected?: string;
117
+ trEven?: string;
118
+ td?: string;
119
+ tfoot?: string;
120
+ tf?: string;
121
+ pagination?: string;
122
+ paginationInfo?: string;
123
+ paginationControls?: string;
124
+ pageButton?: string;
125
+ pageButtonActive?: string;
126
+ perPageSelect?: string;
127
+ btn?: string;
128
+ btnCreate?: string;
129
+ btnEdit?: string;
130
+ btnDelete?: string;
131
+ editorModal?: string;
132
+ editorBackdrop?: string;
133
+ editorField?: string;
134
+ editorInput?: string;
135
+ editorLabel?: string;
136
+ loading?: string;
137
+ empty?: string;
138
+ error?: string;
139
+ }
140
+
141
+ // Full config
142
+ export interface DataTableConfig<T = any> {
143
+ // Data source
144
+ data?: T[];
145
+ serverUrl?: string;
146
+ serverMethod?: 'GET' | 'POST';
147
+ // CSRF token config (defaults to Svelar conventions)
148
+ csrfCookieName?: string;
149
+ csrfHeaderName?: string;
150
+ // Columns
151
+ columns: ColumnDef<T>[];
152
+ // Features
153
+ sortable?: boolean;
154
+ searchable?: boolean;
155
+ paginate?: boolean;
156
+ selectable?: SelectionMode;
157
+ // Pagination
158
+ perPage?: number;
159
+ perPageOptions?: number[];
160
+ // Search
161
+ searchDebounceMs?: number;
162
+ // State
163
+ stateSaveKey?: string;
164
+ // Row identity
165
+ rowId?: string | ((row: T) => string | number);
166
+ // Row classes
167
+ rowClass?: string | ((row: T, index: number) => string);
168
+ // Buttons
169
+ buttons?: (ButtonDef | ExportFormat)[];
170
+ // Editor
171
+ editorMode?: EditorMode;
172
+ editorFields?: EditorFieldDef[];
173
+ // Callbacks
174
+ onSort?: (sort: SortState[]) => void;
175
+ onFilter?: (filters: FilterState[]) => void;
176
+ onPageChange?: (page: number, perPage: number) => void;
177
+ onSelect?: (selectedRows: T[]) => void;
178
+ onRowClick?: (row: T, event: MouseEvent) => void;
179
+ onEdit?: (row: T, data: Record<string, any>) => void | Promise<void>;
180
+ onCellEdit?: (row: T, columnKey: string, newValue: any, oldValue: any) => void | Promise<void>;
181
+ onCreate?: (data: Record<string, any>) => void | Promise<void>;
182
+ onDelete?: (rows: T[]) => void | Promise<void>;
183
+ // Virtual scroll
184
+ virtualScroll?: boolean;
185
+ virtualRowHeight?: number;
186
+ // Responsive
187
+ responsive?: boolean;
188
+ // Row grouping
189
+ groupBy?: string;
190
+ // Detail/child rows
191
+ expandable?: boolean;
192
+ // Empty state
193
+ emptyText?: string;
194
+ // Loading
195
+ loadingText?: string;
196
+ // CSS
197
+ className?: string;
198
+ compact?: boolean;
199
+ striped?: boolean;
200
+ hover?: boolean;
201
+ bordered?: boolean;
202
+ // Tailwind / custom class overrides
203
+ classNames?: DataTableClassNames;
204
+ // Unstyled mode — disables all built-in CSS
205
+ unstyled?: boolean;
206
+ }
207
+
208
+ // Store state shape
209
+ export interface DataTableState<T = any> {
210
+ // Source data
211
+ allRows: T[];
212
+ // Derived
213
+ filteredRows: T[];
214
+ sortedRows: T[];
215
+ paginatedRows: T[];
216
+ // State
217
+ sort: SortState[];
218
+ filters: FilterState[];
219
+ globalSearch: string;
220
+ pagination: PaginationState;
221
+ // Selection
222
+ selectedIds: Set<string | number>;
223
+ // Column state
224
+ columnVisibility: Record<string, boolean>;
225
+ columnOrder: string[];
226
+ // Loading
227
+ loading: boolean;
228
+ error: string | null;
229
+ // Editor
230
+ editingRowId: string | number | null;
231
+ editingColumn: string | null;
232
+ editorMode: EditorMode | null;
233
+ formData: Record<string, any>;
234
+ validationErrors: Record<string, string>;
235
+ // Server draw
236
+ draw: number;
237
+ // Excel mode
238
+ excelFocusedCell: { rowIndex: number; columnKey: string } | null;
239
+ excelEditingCell: { rowIndex: number; columnKey: string } | null;
240
+ excelEditValue: string;
241
+ }
@@ -1,8 +1,8 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
- import type { DataTableConfig, DataTableClassNames, ColumnDef, EditorFieldDef } from '../types.js';
4
- import { DataTableStore } from '../state/DataTableStore.js';
5
- import { ServerDataTableStore } from '../state/ServerDataTableStore.js';
3
+ import type { DataTableConfig, DataTableClassNames, ColumnDef, EditorFieldDef } from '../types.ts';
4
+ import { DataTableStore } from '../state/DataTableStore.ts';
5
+ import { ServerDataTableStore } from '../state/ServerDataTableStore.ts';
6
6
  import DataTableToolbar from './DataTableToolbar.svelte';
7
7
  import DataTableHead from './DataTableHead.svelte';
8
8
  import DataTableBody from './DataTableBody.svelte';
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { ColumnDef, DataTableStore, SelectionMode, EditorMode, DataTableClassNames } from '../index.js';
2
+ import type { ColumnDef, DataTableStore, SelectionMode, EditorMode, DataTableClassNames } from '../index.ts';
3
3
  import type { Snippet } from 'svelte';
4
4
  import DataTableRow from './DataTableRow.svelte';
5
5
  import DataTableLoading from './DataTableLoading.svelte';
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { EditorFieldDef, DataTableStore, DataTableClassNames } from '../index.js';
2
+ import type { EditorFieldDef, DataTableStore, DataTableClassNames } from '../index.ts';
3
3
  import DataTableEditorForm from './DataTableEditorForm.svelte';
4
4
  import { tick } from 'svelte';
5
5
 
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
- import type { ButtonDef, ExportFormat, DataTableStore, ColumnDef } from '../index.js';
3
- import { ExportManager } from '../export/ExportManager.js';
2
+ import type { ButtonDef, ExportFormat, DataTableStore, ColumnDef } from '../index.ts';
3
+ import { ExportManager } from '../export/ExportManager.ts';
4
4
 
5
5
  interface Props {
6
6
  buttons: (ButtonDef | ExportFormat)[];
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { ColumnDef, DataTableStore } from '../index.js';
2
+ import type { ColumnDef, DataTableStore } from '../index.ts';
3
3
  import type { Snippet } from 'svelte';
4
4
 
5
5
  interface Props {
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { ColumnDef, DataTableStore } from '../index.js';
2
+ import type { ColumnDef, DataTableStore } from '../index.ts';
3
3
 
4
4
  interface Props {
5
5
  store: DataTableStore;
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { EditorFieldDef, DataTableStore, DataTableClassNames } from '../index.js';
2
+ import type { EditorFieldDef, DataTableStore, DataTableClassNames } from '../index.ts';
3
3
  import DataTableModalEditor from './DataTableModalEditor.svelte';
4
4
  import DataTableBubbleEditor from './DataTableBubbleEditor.svelte';
5
5
 
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { EditorFieldDef, DataTableStore } from '../index.js';
2
+ import type { EditorFieldDef, DataTableStore } from '../index.ts';
3
3
 
4
4
  interface Props {
5
5
  field: EditorFieldDef;
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { EditorFieldDef, DataTableStore } from '../index.js';
2
+ import type { EditorFieldDef, DataTableStore } from '../index.ts';
3
3
  import DataTableEditorField from './DataTableEditorField.svelte';
4
4
 
5
5
  interface Props {
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { ColumnDef, DataTableStore, SelectionMode, DataTableClassNames } from '../index.js';
2
+ import type { ColumnDef, DataTableStore, SelectionMode, DataTableClassNames } from '../index.ts';
3
3
 
4
4
  interface Props {
5
5
  columns: ColumnDef[];
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { ColumnDef, DataTableStore, SelectionMode, DataTableClassNames } from '../index.js';
2
+ import type { ColumnDef, DataTableStore, SelectionMode, DataTableClassNames } from '../index.ts';
3
3
 
4
4
  interface Props {
5
5
  columns: ColumnDef[];
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { EditorFieldDef, DataTableStore, DataTableClassNames } from '../index.js';
2
+ import type { EditorFieldDef, DataTableStore, DataTableClassNames } from '../index.ts';
3
3
  import DataTableEditorForm from './DataTableEditorForm.svelte';
4
4
 
5
5
  interface Props {
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { DataTableStore, DataTableClassNames } from '../index.js';
2
+ import type { DataTableStore, DataTableClassNames } from '../index.ts';
3
3
 
4
4
  interface Props {
5
5
  store: DataTableStore;
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { ColumnDef, DataTableStore, SelectionMode, EditorMode, DataTableClassNames } from '../index.js';
2
+ import type { ColumnDef, DataTableStore, SelectionMode, EditorMode, DataTableClassNames } from '../index.ts';
3
3
  import type { Snippet } from 'svelte';
4
4
  import DataTableCell from './DataTableCell.svelte';
5
5
  import DataTableExpandedRow from './DataTableExpandedRow.svelte';
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { DataTableStore } from '../index.js';
2
+ import type { DataTableStore } from '../index.ts';
3
3
 
4
4
  interface Props {
5
5
  store: DataTableStore;
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import type { ColumnDef, ButtonDef, ExportFormat, DataTableStore, EditorMode, DataTableClassNames } from '../index.js';
2
+ import type { ColumnDef, ButtonDef, ExportFormat, DataTableStore, EditorMode, DataTableClassNames } from '../index.ts';
3
3
  import DataTableSearch from './DataTableSearch.svelte';
4
4
  import DataTableColumnToggle from './DataTableColumnToggle.svelte';
5
5
  import DataTableButtons from './DataTableButtons.svelte';