juxscript 1.0.19 → 1.0.21
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/bin/cli.js +121 -72
- package/lib/components/alert.ts +212 -165
- package/lib/components/badge.ts +93 -103
- package/lib/components/base/BaseComponent.ts +397 -0
- package/lib/components/base/FormInput.ts +322 -0
- package/lib/components/button.ts +63 -122
- package/lib/components/card.ts +109 -155
- package/lib/components/charts/areachart.ts +315 -0
- package/lib/components/charts/barchart.ts +421 -0
- package/lib/components/charts/doughnutchart.ts +263 -0
- package/lib/components/charts/lib/BaseChart.ts +402 -0
- package/lib/components/charts/lib/chart-types.ts +159 -0
- package/lib/components/charts/lib/chart-utils.ts +160 -0
- package/lib/components/charts/lib/chart.ts +707 -0
- package/lib/components/checkbox.ts +264 -127
- package/lib/components/code.ts +75 -108
- package/lib/components/container.ts +113 -130
- package/lib/components/data.ts +37 -5
- package/lib/components/datepicker.ts +195 -147
- package/lib/components/dialog.ts +187 -157
- package/lib/components/divider.ts +85 -191
- package/lib/components/docs-data.json +544 -2027
- package/lib/components/dropdown.ts +178 -136
- package/lib/components/element.ts +227 -171
- package/lib/components/fileupload.ts +285 -228
- package/lib/components/guard.ts +92 -0
- package/lib/components/heading.ts +46 -69
- package/lib/components/helpers.ts +13 -6
- package/lib/components/hero.ts +107 -95
- package/lib/components/icon.ts +160 -0
- package/lib/components/icons.ts +175 -0
- package/lib/components/include.ts +153 -5
- package/lib/components/input.ts +174 -374
- package/lib/components/kpicard.ts +16 -16
- package/lib/components/list.ts +378 -240
- package/lib/components/loading.ts +142 -211
- package/lib/components/menu.ts +103 -97
- package/lib/components/modal.ts +138 -144
- package/lib/components/nav.ts +169 -90
- package/lib/components/paragraph.ts +49 -150
- package/lib/components/progress.ts +118 -200
- package/lib/components/radio.ts +297 -149
- package/lib/components/script.ts +19 -87
- package/lib/components/select.ts +184 -186
- package/lib/components/sidebar.ts +152 -140
- package/lib/components/style.ts +19 -82
- package/lib/components/switch.ts +258 -188
- package/lib/components/table.ts +1117 -170
- package/lib/components/tabs.ts +162 -145
- package/lib/components/theme-toggle.ts +108 -169
- package/lib/components/tooltip.ts +86 -157
- package/lib/components/write.ts +108 -127
- package/lib/jux.ts +86 -41
- package/machinery/build.js +466 -0
- package/machinery/compiler.js +354 -105
- package/machinery/server.js +23 -100
- package/machinery/watcher.js +153 -130
- package/package.json +1 -2
- package/presets/base.css +1166 -0
- package/presets/notion.css +2 -1975
- package/lib/adapters/base-adapter.js +0 -35
- package/lib/adapters/index.js +0 -33
- package/lib/adapters/mysql-adapter.js +0 -65
- package/lib/adapters/postgres-adapter.js +0 -70
- package/lib/adapters/sqlite-adapter.js +0 -56
- package/lib/components/areachart.ts +0 -1246
- package/lib/components/areachartsmooth.ts +0 -1380
- package/lib/components/barchart.ts +0 -1250
- package/lib/components/chart.ts +0 -127
- package/lib/components/doughnutchart.ts +0 -1191
- package/lib/components/footer.ts +0 -165
- package/lib/components/header.ts +0 -187
- package/lib/components/layout.ts +0 -239
- package/lib/components/main.ts +0 -137
- package/lib/layouts/default.jux +0 -8
- package/lib/layouts/figma.jux +0 -0
- /package/lib/{themes → components/charts/lib}/charts.js +0 -0
package/lib/components/table.ts
CHANGED
|
@@ -1,302 +1,1249 @@
|
|
|
1
|
-
|
|
1
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
2
|
+
* SECTION 1: DEFINITIONS
|
|
3
|
+
* Type definitions, constants, interfaces
|
|
4
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
2
5
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
import { BaseComponent } from './base/BaseComponent.js';
|
|
7
|
+
import { State } from '../reactivity/state.js';
|
|
8
|
+
|
|
9
|
+
// Event definitions
|
|
10
|
+
const TRIGGER_EVENTS = [
|
|
11
|
+
'rowClick', 'rowHover', 'cellClick',
|
|
12
|
+
'selected', 'deselected', 'selectionChange'
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
const CALLBACK_EVENTS = [
|
|
16
|
+
'sortChange', 'filterChange', 'pageChange', 'dataChange'
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
type TriggerEvent = typeof TRIGGER_EVENTS[number];
|
|
20
|
+
type CallbackEvent = typeof CALLBACK_EVENTS[number];
|
|
21
|
+
|
|
22
|
+
// Type definitions
|
|
23
|
+
export type SelectionBehaviorOnDataChange = 'clear' | 'preserve';
|
|
24
|
+
export type SelectionTrigger = 'row' | 'checkbox';
|
|
25
|
+
|
|
26
|
+
export interface ColumnDef {
|
|
27
|
+
key: string;
|
|
28
|
+
label: string;
|
|
29
|
+
width?: string;
|
|
30
|
+
render?: (value: any, row: any) => string | HTMLElement;
|
|
31
|
+
// ✨ NEW: Mark as computed column
|
|
32
|
+
computed?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ✨ NEW: Computed column definition
|
|
36
|
+
export interface ComputedColumnDef {
|
|
7
37
|
key: string;
|
|
8
38
|
label: string;
|
|
39
|
+
compute: (row: any, rowIndex: number) => any;
|
|
40
|
+
render?: (value: any, row: any, rowIndex: number) => string | HTMLElement;
|
|
9
41
|
width?: string;
|
|
10
|
-
align?: 'left' | 'center' | 'right';
|
|
11
|
-
renderCell?: (value: any, row: any) => string | HTMLElement;
|
|
12
42
|
}
|
|
13
43
|
|
|
14
|
-
/**
|
|
15
|
-
* Table component options
|
|
16
|
-
*/
|
|
17
44
|
export interface TableOptions {
|
|
18
|
-
columns?:
|
|
19
|
-
|
|
45
|
+
columns?: (string | ColumnDef)[];
|
|
46
|
+
rows?: any[][];
|
|
47
|
+
headers?: boolean;
|
|
20
48
|
striped?: boolean;
|
|
21
49
|
hoverable?: boolean;
|
|
22
50
|
bordered?: boolean;
|
|
23
|
-
|
|
51
|
+
compact?: boolean;
|
|
52
|
+
sortable?: boolean;
|
|
53
|
+
filterable?: boolean;
|
|
54
|
+
paginated?: boolean;
|
|
55
|
+
rowsPerPage?: number;
|
|
56
|
+
selectable?: boolean;
|
|
57
|
+
multiSelect?: boolean;
|
|
58
|
+
showCheckboxes?: boolean;
|
|
59
|
+
showBulkCheckbox?: boolean;
|
|
60
|
+
selectionTrigger?: SelectionTrigger;
|
|
24
61
|
style?: string;
|
|
25
62
|
class?: string;
|
|
63
|
+
rowIdField?: string;
|
|
64
|
+
selectionBehavior?: SelectionBehaviorOnDataChange;
|
|
26
65
|
}
|
|
27
66
|
|
|
28
|
-
/**
|
|
29
|
-
* Table component state
|
|
30
|
-
*/
|
|
31
67
|
type TableState = {
|
|
32
|
-
columns:
|
|
33
|
-
|
|
68
|
+
columns: ColumnDef[];
|
|
69
|
+
rows: any[][];
|
|
70
|
+
// ✨ NEW: Store computed column definitions
|
|
71
|
+
computedColumns: Map<string, ComputedColumnDef>;
|
|
72
|
+
headers: boolean;
|
|
34
73
|
striped: boolean;
|
|
35
74
|
hoverable: boolean;
|
|
36
75
|
bordered: boolean;
|
|
37
|
-
|
|
76
|
+
compact: boolean;
|
|
77
|
+
sortable: boolean;
|
|
78
|
+
filterable: boolean;
|
|
79
|
+
paginated: boolean;
|
|
80
|
+
rowsPerPage: number;
|
|
81
|
+
currentPage: number;
|
|
82
|
+
sortColumn: string | null;
|
|
83
|
+
sortDirection: 'asc' | 'desc';
|
|
84
|
+
filterText: string;
|
|
85
|
+
selectable: boolean;
|
|
86
|
+
multiSelect: boolean;
|
|
87
|
+
showCheckboxes: boolean;
|
|
88
|
+
showBulkCheckbox: boolean;
|
|
89
|
+
selectionTrigger: SelectionTrigger;
|
|
90
|
+
selectedIndexes: Set<number>;
|
|
38
91
|
style: string;
|
|
39
92
|
class: string;
|
|
93
|
+
rowIdField?: string;
|
|
94
|
+
selectionBehavior: SelectionBehaviorOnDataChange;
|
|
40
95
|
};
|
|
41
96
|
|
|
42
|
-
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
* { name: 'Alice', age: 30 },
|
|
50
|
-
* { name: 'Bob', age: 25 }
|
|
51
|
-
* ],
|
|
52
|
-
* striped: true,
|
|
53
|
-
* allowHtml: true
|
|
54
|
-
* });
|
|
55
|
-
* table.render();
|
|
56
|
-
*
|
|
57
|
-
* // Or specify columns explicitly
|
|
58
|
-
* const table = jux.table('myTable', {
|
|
59
|
-
* columns: [
|
|
60
|
-
* { key: 'name', label: 'Name' },
|
|
61
|
-
* { key: 'age', label: 'Age', align: 'center' }
|
|
62
|
-
* ],
|
|
63
|
-
* data: [
|
|
64
|
-
* { name: 'Alice', age: 30 },
|
|
65
|
-
* { name: 'Bob', age: 25 }
|
|
66
|
-
* ]
|
|
67
|
-
* });
|
|
68
|
-
* table.render();
|
|
69
|
-
*/
|
|
70
|
-
export class Table {
|
|
71
|
-
state: TableState;
|
|
72
|
-
container: HTMLElement | null = null;
|
|
73
|
-
_id: string;
|
|
74
|
-
id: string;
|
|
97
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
98
|
+
* SECTION 2: CONSTRUCTOR & STORAGE
|
|
99
|
+
* Class declaration, instance variables, initialization
|
|
100
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
101
|
+
|
|
102
|
+
export class Table extends BaseComponent<TableState> {
|
|
103
|
+
private _tableElement: HTMLTableElement | null = null;
|
|
75
104
|
|
|
76
105
|
constructor(id: string, options: TableOptions = {}) {
|
|
77
|
-
|
|
78
|
-
|
|
106
|
+
const normalizedColumns = (options.columns ?? []).map(col =>
|
|
107
|
+
typeof col === 'string' ? { key: col, label: col } : col
|
|
108
|
+
);
|
|
79
109
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
110
|
+
// Initialize base with state
|
|
111
|
+
super(id, {
|
|
112
|
+
columns: normalizedColumns,
|
|
113
|
+
rows: options.rows ?? [],
|
|
114
|
+
computedColumns: new Map(), // ✨ NEW: Initialize empty Map
|
|
115
|
+
headers: options.headers ?? true,
|
|
83
116
|
striped: options.striped ?? false,
|
|
84
|
-
hoverable: options.hoverable ??
|
|
117
|
+
hoverable: options.hoverable ?? false,
|
|
85
118
|
bordered: options.bordered ?? false,
|
|
86
|
-
|
|
119
|
+
compact: options.compact ?? false,
|
|
120
|
+
sortable: options.sortable ?? false,
|
|
121
|
+
filterable: options.filterable ?? false,
|
|
122
|
+
paginated: options.paginated ?? false,
|
|
123
|
+
rowsPerPage: options.rowsPerPage ?? 10,
|
|
124
|
+
currentPage: 1,
|
|
125
|
+
sortColumn: null,
|
|
126
|
+
sortDirection: 'asc',
|
|
127
|
+
filterText: '',
|
|
128
|
+
selectable: options.selectable ?? false,
|
|
129
|
+
multiSelect: options.multiSelect ?? false,
|
|
130
|
+
showCheckboxes: options.showCheckboxes ?? false,
|
|
131
|
+
showBulkCheckbox: options.showBulkCheckbox ?? false,
|
|
132
|
+
selectionTrigger: options.selectionTrigger ?? 'row',
|
|
133
|
+
selectedIndexes: new Set<number>(),
|
|
87
134
|
style: options.style ?? '',
|
|
88
|
-
class: options.class ?? ''
|
|
89
|
-
|
|
135
|
+
class: options.class ?? '',
|
|
136
|
+
rowIdField: options.rowIdField,
|
|
137
|
+
selectionBehavior: options.selectionBehavior ?? 'clear'
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
142
|
+
* ABSTRACT METHOD IMPLEMENTATIONS
|
|
143
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
144
|
+
|
|
145
|
+
protected getTriggerEvents(): readonly string[] {
|
|
146
|
+
return TRIGGER_EVENTS;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
protected getCallbackEvents(): readonly string[] {
|
|
150
|
+
return CALLBACK_EVENTS;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
154
|
+
* SECTION 3: FLUENT API
|
|
155
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
156
|
+
|
|
157
|
+
// ✅ Inherited from BaseComponent: style(), class(), bind(), sync(), renderTo()
|
|
158
|
+
|
|
159
|
+
// Configuration methods
|
|
160
|
+
columns(value: (string | ColumnDef)[]): this {
|
|
161
|
+
this.state.columns = value.map(col =>
|
|
162
|
+
typeof col === 'string' ? { key: col, label: col } : col
|
|
163
|
+
);
|
|
164
|
+
return this;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
rows(value: any[][]): this {
|
|
168
|
+
const previousRows = this.state.rows;
|
|
169
|
+
const hadSelections = this.state.selectedIndexes.size > 0;
|
|
170
|
+
|
|
171
|
+
// Handle selections based on behavior
|
|
172
|
+
if (this.state.selectionBehavior === 'preserve' && this.state.rowIdField) {
|
|
173
|
+
this._preserveSelections(previousRows, value);
|
|
174
|
+
} else {
|
|
175
|
+
this.state.selectedIndexes.clear();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
this.state.rows = value;
|
|
179
|
+
|
|
180
|
+
// Auto-reset pagination if current page is now invalid
|
|
181
|
+
if (this.state.paginated) {
|
|
182
|
+
const totalPages = Math.ceil(value.length / this.state.rowsPerPage);
|
|
183
|
+
if (this.state.currentPage > totalPages && totalPages > 0) {
|
|
184
|
+
this.state.currentPage = totalPages;
|
|
185
|
+
}
|
|
186
|
+
if (totalPages === 0) {
|
|
187
|
+
this.state.currentPage = 1;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this._updateTable();
|
|
192
|
+
|
|
193
|
+
// Fire callbacks
|
|
194
|
+
this._triggerCallback('dataChange', value, previousRows);
|
|
195
|
+
|
|
196
|
+
if (hadSelections && this._triggerHandlers.has('selectionChange')) {
|
|
197
|
+
this._triggerHandlers.get('selectionChange')!(
|
|
198
|
+
this.getSelectedRows(),
|
|
199
|
+
this.getSelectedIndexes(),
|
|
200
|
+
new CustomEvent('dataChange')
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return this;
|
|
90
205
|
}
|
|
91
206
|
|
|
92
|
-
|
|
93
|
-
*
|
|
94
|
-
*
|
|
207
|
+
/**
|
|
208
|
+
* Add a computed column that evaluates dynamically at render time
|
|
209
|
+
*
|
|
210
|
+
* @param key - Unique key for the column
|
|
211
|
+
* @param label - Display label in header
|
|
212
|
+
* @param compute - Function to compute value from row data
|
|
213
|
+
* @param render - Optional custom renderer for the computed value
|
|
214
|
+
* @param width - Optional column width
|
|
215
|
+
*/
|
|
216
|
+
computedColumn(
|
|
217
|
+
key: string,
|
|
218
|
+
label: string,
|
|
219
|
+
compute: (row: any, rowIndex: number) => any,
|
|
220
|
+
render?: (value: any, row: any, rowIndex: number) => string | HTMLElement,
|
|
221
|
+
width?: string
|
|
222
|
+
): this {
|
|
223
|
+
// Store computed column definition
|
|
224
|
+
this.state.computedColumns.set(key, {
|
|
225
|
+
key,
|
|
226
|
+
label,
|
|
227
|
+
compute,
|
|
228
|
+
render,
|
|
229
|
+
width
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Add to columns list if not already present
|
|
233
|
+
const existingColumn = this.state.columns.find(col => col.key === key);
|
|
234
|
+
if (!existingColumn) {
|
|
235
|
+
this.state.columns.push({
|
|
236
|
+
key,
|
|
237
|
+
label,
|
|
238
|
+
width,
|
|
239
|
+
computed: true,
|
|
240
|
+
render: render as any
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// If already rendered, update table
|
|
245
|
+
if (this._tableElement) {
|
|
246
|
+
const tbody = this._tableElement.querySelector('tbody');
|
|
247
|
+
const thead = this._tableElement.querySelector('thead');
|
|
248
|
+
|
|
249
|
+
if (tbody && thead) {
|
|
250
|
+
// Rebuild header with new column
|
|
251
|
+
const table = this._tableElement;
|
|
252
|
+
table.innerHTML = '';
|
|
253
|
+
const newThead = this._buildTableHeader();
|
|
254
|
+
table.appendChild(newThead);
|
|
255
|
+
|
|
256
|
+
const newTbody = document.createElement('tbody');
|
|
257
|
+
this._renderTableBody(newTbody);
|
|
258
|
+
table.appendChild(newTbody);
|
|
259
|
+
|
|
260
|
+
// Re-wire events
|
|
261
|
+
this._wireTriggerEvents(newTbody);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
95
264
|
|
|
96
|
-
columns(value: TableColumn[]): this {
|
|
97
|
-
this.state.columns = value;
|
|
98
265
|
return this;
|
|
99
266
|
}
|
|
100
267
|
|
|
101
|
-
|
|
102
|
-
|
|
268
|
+
/**
|
|
269
|
+
* Remove a computed column
|
|
270
|
+
*/
|
|
271
|
+
removeComputedColumn(key: string): this {
|
|
272
|
+
this.state.computedColumns.delete(key);
|
|
273
|
+
this.state.columns = this.state.columns.filter(col => col.key !== key);
|
|
274
|
+
|
|
275
|
+
if (this._tableElement) {
|
|
276
|
+
const tbody = this._tableElement.querySelector('tbody');
|
|
277
|
+
const thead = this._tableElement.querySelector('thead');
|
|
278
|
+
|
|
279
|
+
if (tbody && thead) {
|
|
280
|
+
const table = this._tableElement;
|
|
281
|
+
table.innerHTML = '';
|
|
282
|
+
const newThead = this._buildTableHeader();
|
|
283
|
+
table.appendChild(newThead);
|
|
284
|
+
|
|
285
|
+
const newTbody = document.createElement('tbody');
|
|
286
|
+
this._renderTableBody(newTbody);
|
|
287
|
+
table.appendChild(newTbody);
|
|
288
|
+
|
|
289
|
+
this._wireTriggerEvents(newTbody);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
103
293
|
return this;
|
|
104
294
|
}
|
|
105
295
|
|
|
296
|
+
// Visual options
|
|
297
|
+
headers(value: boolean): this {
|
|
298
|
+
this.state.headers = value;
|
|
299
|
+
return this;
|
|
300
|
+
}
|
|
106
301
|
striped(value: boolean): this {
|
|
107
302
|
this.state.striped = value;
|
|
108
303
|
return this;
|
|
109
304
|
}
|
|
110
|
-
|
|
111
305
|
hoverable(value: boolean): this {
|
|
112
306
|
this.state.hoverable = value;
|
|
113
307
|
return this;
|
|
114
308
|
}
|
|
115
|
-
|
|
116
309
|
bordered(value: boolean): this {
|
|
117
310
|
this.state.bordered = value;
|
|
118
311
|
return this;
|
|
119
312
|
}
|
|
313
|
+
compact(value: boolean): this {
|
|
314
|
+
this.state.compact = value;
|
|
315
|
+
return this;
|
|
316
|
+
}
|
|
120
317
|
|
|
121
|
-
|
|
122
|
-
|
|
318
|
+
// Feature toggles
|
|
319
|
+
sortable(value: boolean): this {
|
|
320
|
+
this.state.sortable = value;
|
|
321
|
+
return this;
|
|
322
|
+
}
|
|
323
|
+
filterable(value: boolean): this {
|
|
324
|
+
this.state.filterable = value;
|
|
325
|
+
return this;
|
|
326
|
+
}
|
|
327
|
+
paginated(value: boolean): this {
|
|
328
|
+
this.state.paginated = value;
|
|
329
|
+
return this;
|
|
330
|
+
}
|
|
331
|
+
rowsPerPage(value: number): this {
|
|
332
|
+
this.state.rowsPerPage = value;
|
|
123
333
|
return this;
|
|
124
334
|
}
|
|
125
335
|
|
|
126
|
-
|
|
127
|
-
|
|
336
|
+
// Selection configuration
|
|
337
|
+
selectable(value: boolean): this {
|
|
338
|
+
this.state.selectable = value;
|
|
128
339
|
return this;
|
|
129
340
|
}
|
|
341
|
+
multiSelect(value: boolean): this {
|
|
342
|
+
this.state.multiSelect = value;
|
|
343
|
+
if (value) {
|
|
344
|
+
this.state.selectable = true; // multi-select implies selectable
|
|
345
|
+
}
|
|
346
|
+
return this;
|
|
347
|
+
}
|
|
348
|
+
showCheckboxes(value: boolean): this {
|
|
349
|
+
this.state.showCheckboxes = value;
|
|
350
|
+
|
|
351
|
+
// If already rendered, update immediately
|
|
352
|
+
if (this._tableElement) {
|
|
353
|
+
const tbody = this._tableElement.querySelector('tbody');
|
|
354
|
+
const thead = this._tableElement.querySelector('thead');
|
|
355
|
+
|
|
356
|
+
if (tbody && thead) {
|
|
357
|
+
// Update header
|
|
358
|
+
this._updateHeaderCheckbox(thead);
|
|
359
|
+
// Re-render body with/without checkboxes
|
|
360
|
+
this._renderTableBody(tbody);
|
|
361
|
+
// Update bulk checkbox state
|
|
362
|
+
this._updateBulkCheckboxState();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
130
365
|
|
|
131
|
-
class(value: string): this {
|
|
132
|
-
this.state.class = value;
|
|
133
366
|
return this;
|
|
134
367
|
}
|
|
368
|
+
showBulkCheckbox(value: boolean): this {
|
|
369
|
+
this.state.showBulkCheckbox = value;
|
|
370
|
+
if (value) {
|
|
371
|
+
this.state.multiSelect = true;
|
|
372
|
+
this.state.selectable = true;
|
|
373
|
+
this.state.showCheckboxes = true;
|
|
374
|
+
}
|
|
135
375
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
376
|
+
// If already rendered, update immediately
|
|
377
|
+
if (this._tableElement) {
|
|
378
|
+
const thead = this._tableElement.querySelector('thead');
|
|
379
|
+
const tbody = this._tableElement.querySelector('tbody');
|
|
139
380
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return [];
|
|
381
|
+
if (thead && tbody) {
|
|
382
|
+
this._updateHeaderCheckbox(thead);
|
|
383
|
+
this._renderTableBody(tbody);
|
|
384
|
+
this._updateBulkCheckboxState();
|
|
385
|
+
}
|
|
146
386
|
}
|
|
147
387
|
|
|
148
|
-
|
|
149
|
-
|
|
388
|
+
return this;
|
|
389
|
+
}
|
|
390
|
+
selectionTrigger(value: SelectionTrigger): this {
|
|
391
|
+
this.state.selectionTrigger = value;
|
|
150
392
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
393
|
+
// If already rendered, update cursor style
|
|
394
|
+
if (this._tableElement) {
|
|
395
|
+
const tbody = this._tableElement.querySelector('tbody');
|
|
396
|
+
if (tbody) {
|
|
397
|
+
if (this.state.selectionTrigger === 'row' && this.state.selectable) {
|
|
398
|
+
tbody.style.cursor = 'pointer';
|
|
399
|
+
} else {
|
|
400
|
+
tbody.style.cursor = '';
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return this;
|
|
155
406
|
}
|
|
156
407
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
*/
|
|
164
|
-
private _formatLabel(key: string): string {
|
|
165
|
-
const words = key
|
|
166
|
-
.replace(/([A-Z])/g, ' $1')
|
|
167
|
-
.replace(/_/g, ' ')
|
|
168
|
-
.trim()
|
|
169
|
-
.split(/\s+/);
|
|
408
|
+
// Selection actions
|
|
409
|
+
selectAll(): this {
|
|
410
|
+
this.state.rows.forEach((_, index) => {
|
|
411
|
+
this.state.selectedIndexes.add(index);
|
|
412
|
+
});
|
|
413
|
+
this._updateRowSelectionUI();
|
|
170
414
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
.
|
|
415
|
+
// Fire selectionChange
|
|
416
|
+
if (this._triggerHandlers.has('selectionChange')) {
|
|
417
|
+
this._triggerHandlers.get('selectionChange')!(
|
|
418
|
+
this.getSelectedRows(),
|
|
419
|
+
this.getSelectedIndexes(),
|
|
420
|
+
new CustomEvent('bulkSelect')
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return this;
|
|
174
425
|
}
|
|
426
|
+
deselectAll(): this {
|
|
427
|
+
this.state.selectedIndexes.clear();
|
|
428
|
+
this._updateRowSelectionUI();
|
|
429
|
+
|
|
430
|
+
// Fire selectionChange
|
|
431
|
+
if (this._triggerHandlers.has('selectionChange')) {
|
|
432
|
+
this._triggerHandlers.get('selectionChange')!(
|
|
433
|
+
[],
|
|
434
|
+
[],
|
|
435
|
+
new CustomEvent('bulkDeselect')
|
|
436
|
+
);
|
|
437
|
+
}
|
|
175
438
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
439
|
+
return this;
|
|
440
|
+
}
|
|
441
|
+
clearSelection(): this {
|
|
442
|
+
this.state.selectedIndexes.clear();
|
|
443
|
+
this._updateRowSelectionUI();
|
|
444
|
+
return this;
|
|
445
|
+
}
|
|
446
|
+
selectRows(indexes: number[]): this {
|
|
447
|
+
if (!this.state.multiSelect) {
|
|
448
|
+
this.state.selectedIndexes.clear();
|
|
449
|
+
}
|
|
450
|
+
indexes.forEach(index => {
|
|
451
|
+
if (index >= 0 && index < this.state.rows.length) {
|
|
452
|
+
this.state.selectedIndexes.add(index);
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
this._updateRowSelectionUI();
|
|
456
|
+
return this;
|
|
457
|
+
}
|
|
458
|
+
deselectRows(indexes: number[]): this {
|
|
459
|
+
indexes.forEach(index => this.state.selectedIndexes.delete(index));
|
|
460
|
+
this._updateRowSelectionUI();
|
|
461
|
+
return this;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Public utilities
|
|
465
|
+
getSelectedIndexes(): number[] {
|
|
466
|
+
return Array.from(this.state.selectedIndexes);
|
|
467
|
+
}
|
|
468
|
+
getSelectedRows(): any[] {
|
|
469
|
+
return Array.from(this.state.selectedIndexes).map(index => this.state.rows[index]);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
473
|
+
* SECTION 5: RENDER LIFECYCLE
|
|
474
|
+
* Main render method and BUILD phase helpers
|
|
475
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
179
476
|
|
|
180
477
|
render(targetId?: string): this {
|
|
181
|
-
|
|
478
|
+
const container = this._setupContainer(targetId);
|
|
479
|
+
const wrapper = this._buildWrapper();
|
|
480
|
+
const table = this._buildTable(wrapper);
|
|
481
|
+
const tbody = table.querySelector('tbody')!;
|
|
482
|
+
this._wireAllEvents(wrapper, tbody);
|
|
483
|
+
container.appendChild(wrapper);
|
|
484
|
+
this._tableElement = table;
|
|
485
|
+
return this;
|
|
486
|
+
}
|
|
182
487
|
|
|
488
|
+
// Step 1: SETUP
|
|
489
|
+
protected _setupContainer(targetId?: string): HTMLElement {
|
|
490
|
+
let container: HTMLElement;
|
|
183
491
|
if (targetId) {
|
|
184
492
|
const target = document.querySelector(targetId);
|
|
185
493
|
if (!target || !(target instanceof HTMLElement)) {
|
|
186
|
-
throw new Error(`Table: Target
|
|
494
|
+
throw new Error(`Table: Target "${targetId}" not found`);
|
|
187
495
|
}
|
|
188
496
|
container = target;
|
|
189
497
|
} else {
|
|
190
|
-
|
|
498
|
+
// Inline getOrCreateContainer functionality
|
|
499
|
+
let element = document.getElementById(this._id);
|
|
500
|
+
if (!element) {
|
|
501
|
+
element = document.createElement('div');
|
|
502
|
+
element.id = this._id;
|
|
503
|
+
document.body.appendChild(element);
|
|
504
|
+
}
|
|
505
|
+
container = element;
|
|
191
506
|
}
|
|
192
|
-
|
|
193
507
|
this.container = container;
|
|
194
|
-
|
|
508
|
+
return container;
|
|
509
|
+
}
|
|
195
510
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
511
|
+
// Step 2: BUILD wrapper
|
|
512
|
+
private _buildWrapper(): HTMLElement {
|
|
513
|
+
const { style, class: className } = this.state;
|
|
200
514
|
|
|
201
515
|
const wrapper = document.createElement('div');
|
|
202
516
|
wrapper.className = 'jux-table-wrapper';
|
|
203
517
|
wrapper.id = this._id;
|
|
518
|
+
if (className) wrapper.className += ` ${className}`;
|
|
519
|
+
if (style) wrapper.setAttribute('style', style);
|
|
204
520
|
|
|
205
|
-
if (
|
|
206
|
-
|
|
521
|
+
if (this.state.selectable) {
|
|
522
|
+
const selectionStyles = document.createElement('style');
|
|
523
|
+
selectionStyles.textContent = `
|
|
524
|
+
.jux-table-row-selected { background-color: #e3f2fd !important; }
|
|
525
|
+
.jux-table-row-selected:hover { background-color: #bbdefb !important; }
|
|
526
|
+
`;
|
|
527
|
+
wrapper.appendChild(selectionStyles);
|
|
207
528
|
}
|
|
208
529
|
|
|
209
|
-
if (
|
|
210
|
-
wrapper.
|
|
530
|
+
if (this.state.filterable) {
|
|
531
|
+
wrapper.appendChild(this._buildFilterInput());
|
|
211
532
|
}
|
|
212
533
|
|
|
534
|
+
return wrapper;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private _buildFilterInput(): HTMLInputElement {
|
|
538
|
+
const input = document.createElement('input');
|
|
539
|
+
input.type = 'text';
|
|
540
|
+
input.placeholder = 'Filter...';
|
|
541
|
+
|
|
542
|
+
input.className = 'jux-table-filter';
|
|
543
|
+
input.style.cssText = 'margin-bottom: 10px; padding: 5px; width: 100%;';
|
|
544
|
+
|
|
545
|
+
// Add event listener to handle filtering
|
|
546
|
+
input.addEventListener('input', (e) => {
|
|
547
|
+
const target = e.target as HTMLInputElement;
|
|
548
|
+
this.state.filterText = target.value;
|
|
549
|
+
|
|
550
|
+
// Re-render table body
|
|
551
|
+
const tbody = this._tableElement?.querySelector('tbody');
|
|
552
|
+
if (tbody) {
|
|
553
|
+
this._renderTableBody(tbody);
|
|
554
|
+
|
|
555
|
+
// Update pagination if enabled
|
|
556
|
+
const wrapper = this._tableElement?.closest('.jux-table-wrapper') as HTMLElement;
|
|
557
|
+
if (wrapper && this.state.paginated) {
|
|
558
|
+
// Reset to page 1 when filtering
|
|
559
|
+
this.state.currentPage = 1;
|
|
560
|
+
this._updatePagination(wrapper, tbody);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Fire callback
|
|
565
|
+
this._triggerCallback('filterChange', target.value, e);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
return input;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// Step 3: BUILD table
|
|
572
|
+
private _buildTable(wrapper: HTMLElement): HTMLTableElement {
|
|
573
|
+
const { striped, hoverable, bordered } = this.state;
|
|
213
574
|
const table = document.createElement('table');
|
|
214
575
|
table.className = 'jux-table';
|
|
215
|
-
|
|
216
576
|
if (striped) table.classList.add('jux-table-striped');
|
|
217
577
|
if (hoverable) table.classList.add('jux-table-hoverable');
|
|
218
578
|
if (bordered) table.classList.add('jux-table-bordered');
|
|
579
|
+
// Build and append header
|
|
580
|
+
if (this.state.headers) {
|
|
581
|
+
const thead = this._buildTableHeader();
|
|
582
|
+
table.appendChild(thead);
|
|
583
|
+
}
|
|
584
|
+
// Build and append body
|
|
585
|
+
const tbody = document.createElement('tbody');
|
|
586
|
+
this._renderTableBody(tbody);
|
|
587
|
+
table.appendChild(tbody);
|
|
588
|
+
wrapper.appendChild(table);
|
|
589
|
+
return table;
|
|
590
|
+
}
|
|
219
591
|
|
|
220
|
-
|
|
592
|
+
private _buildTableHeader(): HTMLTableSectionElement {
|
|
221
593
|
const thead = document.createElement('thead');
|
|
222
594
|
const headerRow = document.createElement('tr');
|
|
223
595
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
596
|
+
// Add bulk checkbox or empty checkbox column
|
|
597
|
+
if (this.state.showBulkCheckbox) {
|
|
598
|
+
headerRow.appendChild(this._buildBulkCheckboxCell());
|
|
599
|
+
} else if (this.state.showCheckboxes) {
|
|
600
|
+
const emptyTh = document.createElement('th');
|
|
601
|
+
emptyTh.style.width = '40px';
|
|
602
|
+
headerRow.appendChild(emptyTh);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Add column headers with optional sort
|
|
606
|
+
this.state.columns.forEach(col => {
|
|
607
|
+
const th = this._buildColumnHeader(col);
|
|
229
608
|
headerRow.appendChild(th);
|
|
230
609
|
});
|
|
231
610
|
|
|
232
611
|
thead.appendChild(headerRow);
|
|
233
|
-
|
|
612
|
+
return thead;
|
|
613
|
+
}
|
|
234
614
|
|
|
235
|
-
|
|
236
|
-
const
|
|
615
|
+
private _buildBulkCheckboxCell(): HTMLTableCellElement {
|
|
616
|
+
const bulkTh = document.createElement('th');
|
|
617
|
+
bulkTh.style.width = '40px';
|
|
618
|
+
bulkTh.style.textAlign = 'center';
|
|
237
619
|
|
|
238
|
-
|
|
239
|
-
|
|
620
|
+
const bulkCheckbox = document.createElement('input');
|
|
621
|
+
bulkCheckbox.type = 'checkbox';
|
|
622
|
+
bulkCheckbox.className = 'jux-bulk-checkbox';
|
|
623
|
+
bulkCheckbox.style.cursor = 'pointer';
|
|
624
|
+
bulkCheckbox.title = 'Select all';
|
|
240
625
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
626
|
+
bulkCheckbox.addEventListener('change', (e) => {
|
|
627
|
+
if (bulkCheckbox.checked) {
|
|
628
|
+
this.selectAll();
|
|
629
|
+
} else {
|
|
630
|
+
this.deselectAll();
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
bulkTh.appendChild(bulkCheckbox);
|
|
635
|
+
return bulkTh;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
private _buildColumnHeader(col: ColumnDef): HTMLTableCellElement {
|
|
639
|
+
const th = document.createElement('th');
|
|
640
|
+
th.textContent = col.label;
|
|
641
|
+
th.setAttribute('data-column-key', col.key);
|
|
642
|
+
if (col.width) th.style.width = col.width;
|
|
643
|
+
|
|
644
|
+
if (this.state.sortable) {
|
|
645
|
+
th.style.cursor = 'pointer';
|
|
646
|
+
th.style.userSelect = 'none';
|
|
647
|
+
|
|
648
|
+
th.addEventListener('click', (e) => {
|
|
649
|
+
// Update state
|
|
650
|
+
if (this.state.sortColumn === col.key) {
|
|
651
|
+
this.state.sortDirection = this.state.sortDirection === 'asc' ? 'desc' : 'asc';
|
|
652
|
+
} else {
|
|
653
|
+
this.state.sortColumn = col.key;
|
|
654
|
+
this.state.sortDirection = 'asc';
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// Re-render
|
|
658
|
+
const tbody = this._tableElement!.querySelector('tbody')!;
|
|
659
|
+
this._renderTableBody(tbody);
|
|
660
|
+
|
|
661
|
+
// Update UI indicators
|
|
662
|
+
const headerRow = th.parentElement!;
|
|
663
|
+
headerRow.querySelectorAll('th[data-column-key]').forEach(h => {
|
|
664
|
+
h.textContent = h.textContent?.replace(' ▲', '').replace(' ▼', '') || '';
|
|
665
|
+
});
|
|
666
|
+
th.textContent = col.label + (this.state.sortDirection === 'asc' ? ' ▲' : ' ▼');
|
|
667
|
+
|
|
668
|
+
// Fire callback
|
|
669
|
+
this._triggerCallback('sortChange', col.key, this.state.sortDirection, e);
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return th;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Step 4: WIRE orchestrator
|
|
677
|
+
private _wireAllEvents(wrapper: HTMLElement, tbody: HTMLTableSectionElement): void {
|
|
678
|
+
this._wireTriggerEvents(tbody);
|
|
679
|
+
|
|
680
|
+
if (this.state.paginated) {
|
|
681
|
+
this._updatePagination(wrapper, tbody);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
this._wireStandardEvents(wrapper); // ✅ Use inherited method
|
|
685
|
+
this._wireSyncBindings(wrapper, tbody);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
689
|
+
* SECTION 6: EVENT WIRING
|
|
690
|
+
* WIRE phase - connects storage to actual DOM listeners/subscriptions
|
|
691
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
692
|
+
|
|
693
|
+
private _wireTriggerEvents(tbody: HTMLTableSectionElement): void {
|
|
694
|
+
// === rowClick: Fire immediately when row is clicked ===
|
|
695
|
+
if (this._triggerHandlers.has('rowClick')) {
|
|
696
|
+
const handler = this._triggerHandlers.get('rowClick')!;
|
|
697
|
+
tbody.addEventListener('click', (e) => {
|
|
698
|
+
const tr = (e.target as HTMLElement).closest('tr');
|
|
699
|
+
if (tr && tbody.contains(tr)) {
|
|
700
|
+
const rowIndex = Array.from(tbody.children).indexOf(tr);
|
|
701
|
+
let rows = this._getFilteredRows();
|
|
702
|
+
rows = this._getSortedRows(rows);
|
|
703
|
+
rows = this._getPaginatedRows(rows);
|
|
704
|
+
const rowData = rows[rowIndex];
|
|
705
|
+
if (rowData) {
|
|
706
|
+
handler(rowData, rowIndex, e);
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
tbody.style.cursor = 'pointer';
|
|
711
|
+
}
|
|
712
|
+
// === cellClick: Fire immediately when cell is clicked ===
|
|
713
|
+
if (this._triggerHandlers.has('cellClick')) {
|
|
714
|
+
const handler = this._triggerHandlers.get('cellClick')!;
|
|
715
|
+
tbody.addEventListener('click', (e) => {
|
|
716
|
+
const td = (e.target as HTMLElement).closest('td');
|
|
717
|
+
if (td) {
|
|
718
|
+
const tr = td.closest('tr');
|
|
719
|
+
if (tr && tbody.contains(tr)) {
|
|
720
|
+
const rowIndex = Array.from(tbody.children).indexOf(tr);
|
|
721
|
+
const cellIndex = Array.from(tr.children).indexOf(td);
|
|
722
|
+
let rows = this._getFilteredRows();
|
|
723
|
+
rows = this._getSortedRows(rows);
|
|
724
|
+
rows = this._getPaginatedRows(rows);
|
|
725
|
+
const rowData = rows[rowIndex];
|
|
726
|
+
const columnKey = this.state.columns[cellIndex]?.key;
|
|
727
|
+
const cellValue = rowData?.[columnKey];
|
|
728
|
+
if (rowData && columnKey) {
|
|
729
|
+
handler(cellValue, rowData, columnKey, rowIndex, cellIndex, e);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
// === Selection events: Fire with internal logic ===
|
|
736
|
+
if (this.state.selectable) {
|
|
737
|
+
tbody.addEventListener('click', (e) => {
|
|
738
|
+
const target = e.target as HTMLElement;
|
|
739
|
+
|
|
740
|
+
// Check if click was on checkbox
|
|
741
|
+
const isCheckboxClick = target.tagName === 'INPUT' && target.getAttribute('type') === 'checkbox';
|
|
742
|
+
|
|
743
|
+
// If trigger is 'checkbox' and click wasn't on checkbox, ignore
|
|
744
|
+
if (this.state.selectionTrigger === 'checkbox' && !isCheckboxClick) {
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// ✨ FIX: If trigger is 'row' and click WAS on checkbox, toggle it manually
|
|
749
|
+
if (this.state.selectionTrigger === 'row' && isCheckboxClick) {
|
|
750
|
+
e.preventDefault(); // Prevent default checkbox behavior
|
|
751
|
+
const checkbox = target as HTMLInputElement;
|
|
752
|
+
checkbox.checked = !checkbox.checked; // ✨ Toggle the checkbox
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const tr = target.closest('tr');
|
|
756
|
+
if (tr && tbody.contains(tr)) {
|
|
757
|
+
const visualRowIndex = Array.from(tbody.children).indexOf(tr);
|
|
758
|
+
let rows = this._getFilteredRows();
|
|
759
|
+
rows = this._getSortedRows(rows);
|
|
760
|
+
const actualRowIndex = this.state.rows.indexOf(rows[
|
|
761
|
+
(this.state.currentPage - 1) * this.state.rowsPerPage + visualRowIndex
|
|
762
|
+
]);
|
|
763
|
+
|
|
764
|
+
if (actualRowIndex === -1) return;
|
|
765
|
+
const wasSelected = this.state.selectedIndexes.has(actualRowIndex);
|
|
766
|
+
|
|
767
|
+
if (this.state.multiSelect) {
|
|
768
|
+
// Toggle selection
|
|
769
|
+
if (wasSelected) {
|
|
770
|
+
this.state.selectedIndexes.delete(actualRowIndex);
|
|
771
|
+
tr.classList.remove('jux-table-row-selected');
|
|
772
|
+
|
|
773
|
+
// Update checkbox if present
|
|
774
|
+
const checkbox = tr.querySelector('input[type="checkbox"]') as HTMLInputElement;
|
|
775
|
+
if (checkbox) checkbox.checked = false;
|
|
776
|
+
|
|
777
|
+
if (this._triggerHandlers.has('deselected')) {
|
|
778
|
+
this._triggerHandlers.get('deselected')!(this.state.rows[actualRowIndex], actualRowIndex, e);
|
|
779
|
+
}
|
|
255
780
|
} else {
|
|
256
|
-
|
|
781
|
+
this.state.selectedIndexes.add(actualRowIndex);
|
|
782
|
+
tr.classList.add('jux-table-row-selected');
|
|
783
|
+
|
|
784
|
+
// Update checkbox if present
|
|
785
|
+
const checkbox = tr.querySelector('input[type="checkbox"]') as HTMLInputElement;
|
|
786
|
+
if (checkbox) checkbox.checked = true;
|
|
787
|
+
|
|
788
|
+
if (this._triggerHandlers.has('selected')) {
|
|
789
|
+
this._triggerHandlers.get('selected')!(this.state.rows[actualRowIndex], actualRowIndex, e);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
} else {
|
|
793
|
+
// Single select
|
|
794
|
+
const previousSelection = Array.from(this.state.selectedIndexes);
|
|
795
|
+
this.state.selectedIndexes.clear();
|
|
796
|
+
tbody.querySelectorAll('tr').forEach(row => {
|
|
797
|
+
row.classList.remove('jux-table-row-selected');
|
|
798
|
+
const checkbox = row.querySelector('input[type="checkbox"]') as HTMLInputElement;
|
|
799
|
+
if (checkbox) checkbox.checked = false;
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
if (!wasSelected) {
|
|
803
|
+
this.state.selectedIndexes.add(actualRowIndex);
|
|
804
|
+
tr.classList.add('jux-table-row-selected');
|
|
805
|
+
|
|
806
|
+
// Update checkbox if present
|
|
807
|
+
const checkbox = tr.querySelector('input[type="checkbox"]') as HTMLInputElement;
|
|
808
|
+
if (checkbox) checkbox.checked = true;
|
|
809
|
+
|
|
810
|
+
if (this._triggerHandlers.has('selected')) {
|
|
811
|
+
this._triggerHandlers.get('selected')!(this.state.rows[actualRowIndex], actualRowIndex, e);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (this._triggerHandlers.has('deselected') && previousSelection.length > 0) {
|
|
816
|
+
previousSelection.forEach(idx => {
|
|
817
|
+
if (idx !== actualRowIndex) {
|
|
818
|
+
this._triggerHandlers.get('deselected')!(this.state.rows[idx], idx, e);
|
|
819
|
+
}
|
|
820
|
+
});
|
|
257
821
|
}
|
|
258
822
|
}
|
|
823
|
+
|
|
824
|
+
// Update bulk checkbox state if present
|
|
825
|
+
this._updateBulkCheckboxState();
|
|
826
|
+
|
|
827
|
+
// Fire selectionChange
|
|
828
|
+
if (this._triggerHandlers.has('selectionChange')) {
|
|
829
|
+
this._triggerHandlers.get('selectionChange')!(this.getSelectedRows(), this.getSelectedIndexes(), e);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
// Only set pointer cursor if row is trigger
|
|
835
|
+
if (this.state.selectionTrigger === 'row') {
|
|
836
|
+
tbody.style.cursor = 'pointer';
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
private _wireSyncBindings(wrapper: HTMLElement, tbody: HTMLTableSectionElement): void {
|
|
842
|
+
this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
|
|
843
|
+
if (property === 'rows' || property === 'data') {
|
|
844
|
+
this._wireSyncRows(stateObj, toComponent, tbody, wrapper);
|
|
845
|
+
} else if (property === 'columns') {
|
|
846
|
+
this._wireSyncColumns(stateObj, toComponent, wrapper, tbody);
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
private _wireSyncRows(
|
|
852
|
+
stateObj: State<any>,
|
|
853
|
+
toComponent: Function | undefined,
|
|
854
|
+
tbody: HTMLTableSectionElement,
|
|
855
|
+
wrapper: HTMLElement
|
|
856
|
+
): void {
|
|
857
|
+
const transform = toComponent || ((v: any) => v);
|
|
858
|
+
stateObj.subscribe((val: any) => {
|
|
859
|
+
const previousRows = this.state.rows;
|
|
860
|
+
const transformed = transform(val);
|
|
861
|
+
// Handle selections
|
|
862
|
+
const hadSelections = this.state.selectedIndexes.size > 0;
|
|
863
|
+
if (this.state.selectionBehavior === 'preserve' && this.state.rowIdField) {
|
|
864
|
+
this._preserveSelections(previousRows, transformed);
|
|
865
|
+
} else {
|
|
866
|
+
const isAppend = transformed.length > previousRows.length;
|
|
867
|
+
if (isAppend) {
|
|
868
|
+
const maxValidIndex = Math.min(previousRows.length, transformed.length) - 1;
|
|
869
|
+
const validSelections = new Set<number>();
|
|
870
|
+
this.state.selectedIndexes.forEach(idx => {
|
|
871
|
+
if (idx <= maxValidIndex) validSelections.add(idx);
|
|
872
|
+
});
|
|
873
|
+
this.state.selectedIndexes = validSelections;
|
|
259
874
|
} else {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
875
|
+
this.state.selectedIndexes.clear();
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
this.state.rows = transformed;
|
|
879
|
+
// Auto-reset pagination
|
|
880
|
+
if (this.state.paginated) {
|
|
881
|
+
const totalPages = Math.ceil(transformed.length / this.state.rowsPerPage);
|
|
882
|
+
if (this.state.currentPage > totalPages && totalPages > 0) {
|
|
883
|
+
this.state.currentPage = totalPages;
|
|
884
|
+
}
|
|
885
|
+
if (totalPages === 0) {
|
|
886
|
+
this.state.currentPage = 1;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
// Re-render
|
|
890
|
+
this._renderTableBody(tbody);
|
|
891
|
+
if (this.state.paginated) {
|
|
892
|
+
this._updatePagination(wrapper, tbody);
|
|
893
|
+
}
|
|
894
|
+
// Fire callbacks
|
|
895
|
+
this._triggerCallback('dataChange', transformed, previousRows);
|
|
896
|
+
if (hadSelections || this.state.selectedIndexes.size > 0) {
|
|
897
|
+
this._triggerHandlers.get('selectionChange')?.(this.getSelectedRows(), this.getSelectedIndexes(), new CustomEvent('dataChange'));
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
private _wireSyncColumns(
|
|
903
|
+
stateObj: State<any>,
|
|
904
|
+
toComponent: Function | undefined,
|
|
905
|
+
wrapper: HTMLElement,
|
|
906
|
+
tbody: HTMLTableSectionElement
|
|
907
|
+
): void {
|
|
908
|
+
const transform = toComponent || ((v: any) => v);
|
|
909
|
+
stateObj.subscribe((val: any) => {
|
|
910
|
+
const transformed = transform(val);
|
|
911
|
+
this.state.columns = transformed;
|
|
912
|
+
// Full re-render needed for columns
|
|
913
|
+
const table = this._tableElement!;
|
|
914
|
+
table.innerHTML = '';
|
|
915
|
+
const thead = this._buildTableHeader();
|
|
916
|
+
table.appendChild(thead);
|
|
917
|
+
const newTbody = document.createElement('tbody');
|
|
918
|
+
this._renderTableBody(newTbody);
|
|
919
|
+
table.appendChild(newTbody);
|
|
920
|
+
// Re-wire events
|
|
921
|
+
this._wireTriggerEvents(newTbody);
|
|
922
|
+
if (this.state.paginated) {
|
|
923
|
+
this._updatePagination(wrapper, newTbody);
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
929
|
+
* SECTION 7: INTERNAL HELPERS
|
|
930
|
+
* Data processing, DOM updates, selection management
|
|
931
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
932
|
+
|
|
933
|
+
// Data processing
|
|
934
|
+
private _getFilteredRows(): any[][] {
|
|
935
|
+
if (!this.state.filterText) return this.state.rows;
|
|
936
|
+
const searchText = this.state.filterText.toLowerCase();
|
|
937
|
+
return this.state.rows.filter(row => {
|
|
938
|
+
return this.state.columns.some(col => {
|
|
939
|
+
const value = row[col.key];
|
|
940
|
+
return String(value).toLowerCase().includes(searchText);
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
private _getSortedRows(rows: any[][]): any[][] {
|
|
946
|
+
if (!this.state.sortColumn) return rows;
|
|
947
|
+
|
|
948
|
+
// ✨ Check if sorting by computed column
|
|
949
|
+
const computedDef = this.state.computedColumns.get(this.state.sortColumn);
|
|
950
|
+
|
|
951
|
+
const sorted = [...rows].sort((a, b) => {
|
|
952
|
+
let aVal: any;
|
|
953
|
+
let bVal: any;
|
|
954
|
+
|
|
955
|
+
if (computedDef) {
|
|
956
|
+
// ✨ Compute values on the fly for computed columns
|
|
957
|
+
const aIndex = this.state.rows.indexOf(a);
|
|
958
|
+
const bIndex = this.state.rows.indexOf(b);
|
|
959
|
+
aVal = computedDef.compute(a, aIndex);
|
|
960
|
+
bVal = computedDef.compute(b, bIndex);
|
|
961
|
+
} else {
|
|
962
|
+
// Normal column: get value from row data
|
|
963
|
+
aVal = a[this.state.sortColumn!];
|
|
964
|
+
bVal = b[this.state.sortColumn!];
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Standard comparison logic
|
|
968
|
+
if (aVal === bVal) return 0;
|
|
969
|
+
if (aVal == null) return 1;
|
|
970
|
+
if (bVal == null) return -1;
|
|
971
|
+
|
|
972
|
+
const comparison = aVal < bVal ? -1 : 1;
|
|
973
|
+
return this.state.sortDirection === 'asc' ? comparison : -comparison;
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
return sorted;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
private _getPaginatedRows(rows: any[][]): any[][] {
|
|
980
|
+
if (!this.state.paginated) return rows;
|
|
981
|
+
const start = (this.state.currentPage - 1) * this.state.rowsPerPage;
|
|
982
|
+
const end = start + this.state.rowsPerPage;
|
|
983
|
+
return rows.slice(start, end);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
// DOM updates
|
|
987
|
+
private _updateTable(): void {
|
|
988
|
+
if (!this._tableElement) return;
|
|
989
|
+
const tbody = this._tableElement.querySelector('tbody');
|
|
990
|
+
if (!tbody) return;
|
|
991
|
+
// Re-render using the same logic as initial render
|
|
992
|
+
this._renderTableBody(tbody);
|
|
993
|
+
// Update pagination if enabled
|
|
994
|
+
const wrapper = this._tableElement.closest('.jux-table-wrapper');
|
|
995
|
+
if (wrapper && this.state.paginated) {
|
|
996
|
+
this._updatePagination(wrapper as HTMLElement, tbody);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
private _renderTableBody(tbody: HTMLTableSectionElement): void {
|
|
1001
|
+
tbody.innerHTML = '';
|
|
1002
|
+
let rows = this._getFilteredRows();
|
|
1003
|
+
rows = this._getSortedRows(rows);
|
|
1004
|
+
const totalPages = Math.ceil(rows.length / this.state.rowsPerPage);
|
|
1005
|
+
rows = this._getPaginatedRows(rows);
|
|
1006
|
+
|
|
1007
|
+
rows.forEach((row, visualIndex) => {
|
|
1008
|
+
const tr = document.createElement('tr');
|
|
1009
|
+
|
|
1010
|
+
// Check if this row is selected
|
|
1011
|
+
const actualRowIndex = this.state.rows.indexOf(row);
|
|
1012
|
+
const isSelected = this.state.selectedIndexes.has(actualRowIndex);
|
|
1013
|
+
if (isSelected) {
|
|
1014
|
+
tr.classList.add('jux-table-row-selected');
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Add checkbox column if enabled
|
|
1018
|
+
if (this.state.showCheckboxes) {
|
|
1019
|
+
const checkboxTd = document.createElement('td');
|
|
1020
|
+
checkboxTd.style.width = '40px';
|
|
1021
|
+
checkboxTd.style.textAlign = 'center';
|
|
1022
|
+
const checkbox = document.createElement('input');
|
|
1023
|
+
checkbox.type = 'checkbox';
|
|
1024
|
+
checkbox.checked = isSelected;
|
|
1025
|
+
checkbox.style.cursor = 'pointer';
|
|
1026
|
+
checkboxTd.appendChild(checkbox);
|
|
1027
|
+
tr.appendChild(checkboxTd);
|
|
1028
|
+
}
|
|
1029
|
+
// Add data columns (including computed columns)
|
|
1030
|
+
this.state.columns.forEach(col => {
|
|
1031
|
+
const td = document.createElement('td');
|
|
1032
|
+
|
|
1033
|
+
// ✨ NEW: Check if this is a computed column
|
|
1034
|
+
const computedDef = this.state.computedColumns.get(col.key);
|
|
1035
|
+
|
|
1036
|
+
let cellValue: any;
|
|
1037
|
+
let rendered: string | HTMLElement;
|
|
1038
|
+
|
|
1039
|
+
if (computedDef) {
|
|
1040
|
+
// ✨ Computed column: Evaluate compute function
|
|
1041
|
+
cellValue = computedDef.compute(row, actualRowIndex);
|
|
1042
|
+
|
|
1043
|
+
// Use computed column's custom renderer if provided
|
|
1044
|
+
if (computedDef.render) {
|
|
1045
|
+
rendered = computedDef.render(cellValue, row, actualRowIndex);
|
|
263
1046
|
} else {
|
|
264
|
-
|
|
1047
|
+
// Default: stringify the computed value
|
|
1048
|
+
rendered = cellValue != null ? String(cellValue) : '';
|
|
1049
|
+
}
|
|
1050
|
+
} else {
|
|
1051
|
+
// Normal column: Get value from row data
|
|
1052
|
+
cellValue = row[col.key];
|
|
1053
|
+
|
|
1054
|
+
// Use column's render function if provided
|
|
1055
|
+
if (col.render) {
|
|
1056
|
+
rendered = col.render(cellValue, row);
|
|
1057
|
+
} else {
|
|
1058
|
+
rendered = cellValue != null ? String(cellValue) : '';
|
|
265
1059
|
}
|
|
266
1060
|
}
|
|
267
1061
|
|
|
1062
|
+
// Insert rendered content
|
|
1063
|
+
if (typeof rendered === 'string') {
|
|
1064
|
+
td.innerHTML = rendered;
|
|
1065
|
+
} else {
|
|
1066
|
+
td.appendChild(rendered);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
268
1069
|
tr.appendChild(td);
|
|
269
1070
|
});
|
|
270
1071
|
|
|
271
1072
|
tbody.appendChild(tr);
|
|
272
1073
|
});
|
|
1074
|
+
}
|
|
273
1075
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
1076
|
+
private _updateHeaderCheckbox(thead: HTMLTableSectionElement): void {
|
|
1077
|
+
const headerRow = thead.querySelector('tr');
|
|
1078
|
+
if (!headerRow) return;
|
|
1079
|
+
// Remove existing checkbox column(s)
|
|
1080
|
+
const existingCheckboxThs = headerRow.querySelectorAll('th:first-child');
|
|
1081
|
+
existingCheckboxThs.forEach(th => {
|
|
1082
|
+
if (th.querySelector('.jux-bulk-checkbox') || th.textContent === '') {
|
|
1083
|
+
th.remove();
|
|
1084
|
+
}
|
|
1085
|
+
});
|
|
1086
|
+
// Add bulk checkbox or empty checkbox column
|
|
1087
|
+
if (this.state.showBulkCheckbox) {
|
|
1088
|
+
const bulkTh = document.createElement('th');
|
|
1089
|
+
bulkTh.style.width = '40px';
|
|
1090
|
+
bulkTh.style.textAlign = 'center';
|
|
1091
|
+
const bulkCheckbox = document.createElement('input');
|
|
1092
|
+
bulkCheckbox.type = 'checkbox';
|
|
1093
|
+
bulkCheckbox.className = 'jux-bulk-checkbox';
|
|
1094
|
+
bulkCheckbox.style.cursor = 'pointer';
|
|
1095
|
+
bulkCheckbox.title = 'Select all';
|
|
1096
|
+
bulkCheckbox.addEventListener('change', (e) => {
|
|
1097
|
+
if (bulkCheckbox.checked) {
|
|
1098
|
+
this.selectAll();
|
|
1099
|
+
} else {
|
|
1100
|
+
this.deselectAll();
|
|
1101
|
+
}
|
|
1102
|
+
});
|
|
1103
|
+
bulkTh.appendChild(bulkCheckbox);
|
|
1104
|
+
headerRow.insertBefore(bulkTh, headerRow.firstChild);
|
|
1105
|
+
} else if (this.state.showCheckboxes) {
|
|
1106
|
+
// Add empty header cell for checkbox column (no bulk select)
|
|
1107
|
+
const checkboxTh = document.createElement('th');
|
|
1108
|
+
checkboxTh.style.width = '40px';
|
|
1109
|
+
headerRow.insertBefore(checkboxTh, headerRow.firstChild);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
277
1112
|
|
|
278
|
-
|
|
1113
|
+
private _updateBulkCheckboxState(): void {
|
|
1114
|
+
if (!this.state.showBulkCheckbox || !this._tableElement) return;
|
|
1115
|
+
const bulkCheckbox = this._tableElement.querySelector('.jux-bulk-checkbox') as HTMLInputElement;
|
|
1116
|
+
if (!bulkCheckbox) return;
|
|
1117
|
+
const totalRows = this.state.rows.length;
|
|
1118
|
+
const selectedRows = this.state.selectedIndexes.size;
|
|
1119
|
+
if (selectedRows === 0) {
|
|
1120
|
+
bulkCheckbox.checked = false;
|
|
1121
|
+
bulkCheckbox.indeterminate = false;
|
|
1122
|
+
} else if (selectedRows === totalRows) {
|
|
1123
|
+
bulkCheckbox.checked = true;
|
|
1124
|
+
bulkCheckbox.indeterminate = false;
|
|
1125
|
+
} else {
|
|
1126
|
+
bulkCheckbox.checked = false;
|
|
1127
|
+
bulkCheckbox.indeterminate = true;
|
|
1128
|
+
}
|
|
279
1129
|
}
|
|
280
1130
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
1131
|
+
private _updateRowSelectionUI(): void {
|
|
1132
|
+
if (!this._tableElement) return;
|
|
1133
|
+
const tbody = this._tableElement.querySelector('tbody');
|
|
1134
|
+
if (!tbody) return;
|
|
1135
|
+
// Get current page rows
|
|
1136
|
+
let rows = this._getFilteredRows();
|
|
1137
|
+
rows = this._getSortedRows(rows);
|
|
1138
|
+
const pageRows = this._getPaginatedRows(rows);
|
|
1139
|
+
// Update each visible row's selection state
|
|
1140
|
+
Array.from(tbody.children).forEach((tr, visualIndex) => {
|
|
1141
|
+
const pageRowData = pageRows[visualIndex];
|
|
1142
|
+
const actualRowIndex = this.state.rows.indexOf(pageRowData);
|
|
1143
|
+
const isSelected = this.state.selectedIndexes.has(actualRowIndex);
|
|
1144
|
+
// Update row highlight
|
|
1145
|
+
if (isSelected) {
|
|
1146
|
+
tr.classList.add('jux-table-row-selected');
|
|
1147
|
+
} else {
|
|
1148
|
+
tr.classList.remove('jux-table-row-selected');
|
|
1149
|
+
}
|
|
1150
|
+
// Update checkbox if present
|
|
1151
|
+
const checkbox = tr.querySelector('input[type="checkbox"]') as HTMLInputElement;
|
|
1152
|
+
if (checkbox) {
|
|
1153
|
+
checkbox.checked = isSelected;
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
// Update bulk checkbox state
|
|
1157
|
+
this._updateBulkCheckboxState();
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// Selection management
|
|
1161
|
+
private _preserveSelections(previousRows: any[][], newRows: any[][]): void {
|
|
1162
|
+
if (!this.state.rowIdField || this.state.selectionBehavior === 'clear') {
|
|
1163
|
+
this.state.selectedIndexes.clear();
|
|
1164
|
+
return;
|
|
287
1165
|
}
|
|
1166
|
+
// Build map of old selections by ID
|
|
1167
|
+
const selectedIds = new Set<any>();
|
|
1168
|
+
Array.from(this.state.selectedIndexes).forEach(index => {
|
|
1169
|
+
const row = previousRows[index];
|
|
1170
|
+
if (row && row[this.state.rowIdField!] != null) {
|
|
1171
|
+
selectedIds.add(row[this.state.rowIdField!]);
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
// Find new indexes for selected IDs
|
|
1175
|
+
this.state.selectedIndexes.clear();
|
|
1176
|
+
newRows.forEach((row, index) => {
|
|
1177
|
+
const rowId = row[this.state.rowIdField!];
|
|
1178
|
+
if (rowId != null && selectedIds.has(rowId)) {
|
|
1179
|
+
this.state.selectedIndexes.add(index);
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
288
1183
|
|
|
289
|
-
|
|
290
|
-
|
|
1184
|
+
// Pagination
|
|
1185
|
+
private _updatePagination(wrapper: HTMLElement, tbody: HTMLTableSectionElement): void {
|
|
1186
|
+
// Remove existing pagination
|
|
1187
|
+
const existingPagination = wrapper.querySelector('.jux-table-pagination');
|
|
1188
|
+
if (existingPagination) {
|
|
1189
|
+
existingPagination.remove();
|
|
291
1190
|
}
|
|
1191
|
+
let rows = this._getFilteredRows();
|
|
1192
|
+
rows = this._getSortedRows(rows);
|
|
1193
|
+
const totalPages = Math.ceil(rows.length / this.state.rowsPerPage);
|
|
1194
|
+
if (totalPages <= 1) return;
|
|
1195
|
+
const pagination = document.createElement('div');
|
|
1196
|
+
pagination.className = 'jux-table-pagination';
|
|
1197
|
+
pagination.style.cssText = 'margin-top: 10px; display: flex; gap: 5px; justify-content: center;';
|
|
1198
|
+
// Previous button
|
|
1199
|
+
const prevBtn = document.createElement('button');
|
|
1200
|
+
prevBtn.textContent = 'Previous';
|
|
1201
|
+
prevBtn.disabled = this.state.currentPage === 1;
|
|
1202
|
+
prevBtn.addEventListener('click', (e) => {
|
|
1203
|
+
if (this.state.currentPage > 1) {
|
|
1204
|
+
const previousPage = this.state.currentPage;
|
|
1205
|
+
this.state.currentPage--;
|
|
1206
|
+
this._renderTableBody(tbody);
|
|
1207
|
+
this._updatePagination(wrapper, tbody);
|
|
1208
|
+
this._triggerCallback('pageChange', this.state.currentPage, previousPage, e);
|
|
1209
|
+
}
|
|
1210
|
+
});
|
|
1211
|
+
pagination.appendChild(prevBtn);
|
|
1212
|
+
// Page info
|
|
1213
|
+
const pageInfo = document.createElement('span');
|
|
1214
|
+
pageInfo.textContent = `Page ${this.state.currentPage} of ${totalPages}`;
|
|
1215
|
+
pageInfo.style.cssText = 'display: flex; align-items: center; padding: 0 10px;';
|
|
1216
|
+
pagination.appendChild(pageInfo);
|
|
1217
|
+
// Next button
|
|
1218
|
+
const nextBtn = document.createElement('button');
|
|
1219
|
+
nextBtn.textContent = 'Next';
|
|
1220
|
+
nextBtn.disabled = this.state.currentPage === totalPages;
|
|
1221
|
+
nextBtn.addEventListener('click', (e) => {
|
|
1222
|
+
if (this.state.currentPage < totalPages) {
|
|
1223
|
+
const previousPage = this.state.currentPage;
|
|
1224
|
+
this.state.currentPage++;
|
|
1225
|
+
this._renderTableBody(tbody);
|
|
1226
|
+
this._updatePagination(wrapper, tbody);
|
|
1227
|
+
this._triggerCallback('pageChange', this.state.currentPage, previousPage, e);
|
|
1228
|
+
}
|
|
1229
|
+
});
|
|
1230
|
+
pagination.appendChild(nextBtn);
|
|
1231
|
+
wrapper.appendChild(pagination);
|
|
1232
|
+
}
|
|
292
1233
|
|
|
1234
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
1235
|
+
* SECTION 8: EXPORTS
|
|
1236
|
+
* Convenience wrappers and factory function
|
|
1237
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
1238
|
+
|
|
1239
|
+
renderTo(juxComponent: any): this {
|
|
1240
|
+
if (!juxComponent?._id) {
|
|
1241
|
+
throw new Error('Table.renderTo: Invalid component');
|
|
1242
|
+
}
|
|
293
1243
|
return this.render(`#${juxComponent._id}`);
|
|
294
1244
|
}
|
|
295
1245
|
}
|
|
296
1246
|
|
|
297
|
-
/**
|
|
298
|
-
* Factory helper
|
|
299
|
-
*/
|
|
300
1247
|
export function table(id: string, options: TableOptions = {}): Table {
|
|
301
1248
|
return new Table(id, options);
|
|
302
|
-
}
|
|
1249
|
+
}
|