juxscript 1.0.20 → 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 +143 -92
- package/lib/components/badge.ts +93 -94
- package/lib/components/base/BaseComponent.ts +397 -0
- package/lib/components/base/FormInput.ts +322 -0
- package/lib/components/button.ts +40 -131
- package/lib/components/card.ts +57 -79
- 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/{chart-types.ts → charts/lib/chart-types.ts} +1 -1
- package/lib/components/{chart-utils.ts → charts/lib/chart-utils.ts} +1 -1
- package/lib/components/{chart.ts → charts/lib/chart.ts} +3 -3
- package/lib/components/checkbox.ts +255 -204
- package/lib/components/code.ts +31 -78
- package/lib/components/container.ts +113 -130
- package/lib/components/data.ts +37 -5
- package/lib/components/datepicker.ts +180 -147
- package/lib/components/dialog.ts +218 -221
- package/lib/components/divider.ts +63 -87
- package/lib/components/docs-data.json +498 -2404
- package/lib/components/dropdown.ts +191 -236
- package/lib/components/element.ts +196 -145
- package/lib/components/fileupload.ts +253 -167
- package/lib/components/guard.ts +92 -0
- package/lib/components/heading.ts +31 -97
- package/lib/components/helpers.ts +13 -6
- package/lib/components/hero.ts +51 -114
- package/lib/components/icon.ts +33 -120
- package/lib/components/icons.ts +2 -1
- package/lib/components/include.ts +76 -3
- package/lib/components/input.ts +155 -407
- package/lib/components/kpicard.ts +16 -16
- package/lib/components/list.ts +358 -261
- package/lib/components/loading.ts +142 -211
- package/lib/components/menu.ts +63 -152
- package/lib/components/modal.ts +42 -129
- package/lib/components/nav.ts +79 -101
- package/lib/components/paragraph.ts +38 -102
- package/lib/components/progress.ts +108 -166
- package/lib/components/radio.ts +283 -234
- package/lib/components/script.ts +19 -87
- package/lib/components/select.ts +189 -199
- package/lib/components/sidebar.ts +110 -141
- package/lib/components/style.ts +19 -82
- package/lib/components/switch.ts +254 -183
- package/lib/components/table.ts +1078 -208
- package/lib/components/tabs.ts +42 -106
- package/lib/components/theme-toggle.ts +73 -165
- package/lib/components/tooltip.ts +85 -316
- package/lib/components/write.ts +108 -127
- package/lib/jux.ts +67 -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 -1
- 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 -1128
- package/lib/components/areachartsmooth.ts +0 -1380
- package/lib/components/barchart.ts +0 -1322
- package/lib/components/doughnutchart.ts +0 -1259
- 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,19 +1,46 @@
|
|
|
1
|
-
|
|
1
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
2
|
+
* SECTION 1: DEFINITIONS
|
|
3
|
+
* Type definitions, constants, interfaces
|
|
4
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
5
|
+
|
|
6
|
+
import { BaseComponent } from './base/BaseComponent.js';
|
|
2
7
|
import { State } from '../reactivity/state.js';
|
|
3
8
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
+
|
|
7
26
|
export interface ColumnDef {
|
|
8
27
|
key: string;
|
|
9
28
|
label: string;
|
|
10
29
|
width?: string;
|
|
11
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 {
|
|
37
|
+
key: string;
|
|
38
|
+
label: string;
|
|
39
|
+
compute: (row: any, rowIndex: number) => any;
|
|
40
|
+
render?: (value: any, row: any, rowIndex: number) => string | HTMLElement;
|
|
41
|
+
width?: string;
|
|
12
42
|
}
|
|
13
43
|
|
|
14
|
-
/**
|
|
15
|
-
* Table options
|
|
16
|
-
*/
|
|
17
44
|
export interface TableOptions {
|
|
18
45
|
columns?: (string | ColumnDef)[];
|
|
19
46
|
rows?: any[][];
|
|
@@ -22,87 +49,114 @@ export interface TableOptions {
|
|
|
22
49
|
hoverable?: boolean;
|
|
23
50
|
bordered?: boolean;
|
|
24
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;
|
|
25
61
|
style?: string;
|
|
26
62
|
class?: string;
|
|
63
|
+
rowIdField?: string;
|
|
64
|
+
selectionBehavior?: SelectionBehaviorOnDataChange;
|
|
27
65
|
}
|
|
28
66
|
|
|
29
|
-
/**
|
|
30
|
-
* Table state
|
|
31
|
-
*/
|
|
32
67
|
type TableState = {
|
|
33
68
|
columns: ColumnDef[];
|
|
34
69
|
rows: any[][];
|
|
70
|
+
// ✨ NEW: Store computed column definitions
|
|
71
|
+
computedColumns: Map<string, ComputedColumnDef>;
|
|
35
72
|
headers: boolean;
|
|
36
73
|
striped: boolean;
|
|
37
74
|
hoverable: boolean;
|
|
38
75
|
bordered: boolean;
|
|
39
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>;
|
|
40
91
|
style: string;
|
|
41
92
|
class: string;
|
|
93
|
+
rowIdField?: string;
|
|
94
|
+
selectionBehavior: SelectionBehaviorOnDataChange;
|
|
42
95
|
};
|
|
43
96
|
|
|
44
|
-
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* jux.table('users', {
|
|
49
|
-
* columns: ['Name', 'Email', 'Role'],
|
|
50
|
-
* rows: [['John', 'john@example.com', 'Admin']]
|
|
51
|
-
* }).render('#app');
|
|
52
|
-
*
|
|
53
|
-
* // With state binding
|
|
54
|
-
* const todos = state([]);
|
|
55
|
-
* jux.table('todos-table')
|
|
56
|
-
* .columns(['ID', 'Text', 'Done'])
|
|
57
|
-
* .sync('rows', todos, (items) =>
|
|
58
|
-
* items.map(item => [item.id, item.text, item.done])
|
|
59
|
-
* )
|
|
60
|
-
* .render('#app');
|
|
61
|
-
*/
|
|
62
|
-
export class Table {
|
|
63
|
-
state: TableState;
|
|
64
|
-
container: HTMLElement | null = null;
|
|
65
|
-
_id: string;
|
|
66
|
-
id: string;
|
|
67
|
-
|
|
68
|
-
// CRITICAL: Store bind/sync instructions for deferred wiring
|
|
69
|
-
private _bindings: Array<{ event: string, handler: Function }> = [];
|
|
70
|
-
private _syncBindings: Array<{
|
|
71
|
-
property: string,
|
|
72
|
-
stateObj: State<any>,
|
|
73
|
-
toState?: Function,
|
|
74
|
-
toComponent?: Function
|
|
75
|
-
}> = [];
|
|
97
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
98
|
+
* SECTION 2: CONSTRUCTOR & STORAGE
|
|
99
|
+
* Class declaration, instance variables, initialization
|
|
100
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
76
101
|
|
|
77
|
-
|
|
102
|
+
export class Table extends BaseComponent<TableState> {
|
|
78
103
|
private _tableElement: HTMLTableElement | null = null;
|
|
79
104
|
|
|
80
105
|
constructor(id: string, options: TableOptions = {}) {
|
|
81
|
-
this._id = id;
|
|
82
|
-
this.id = id;
|
|
83
|
-
|
|
84
|
-
// Normalize columns to ColumnDef format
|
|
85
106
|
const normalizedColumns = (options.columns ?? []).map(col =>
|
|
86
107
|
typeof col === 'string' ? { key: col, label: col } : col
|
|
87
108
|
);
|
|
88
109
|
|
|
89
|
-
|
|
110
|
+
// Initialize base with state
|
|
111
|
+
super(id, {
|
|
90
112
|
columns: normalizedColumns,
|
|
91
113
|
rows: options.rows ?? [],
|
|
114
|
+
computedColumns: new Map(), // ✨ NEW: Initialize empty Map
|
|
92
115
|
headers: options.headers ?? true,
|
|
93
116
|
striped: options.striped ?? false,
|
|
94
117
|
hoverable: options.hoverable ?? false,
|
|
95
118
|
bordered: options.bordered ?? false,
|
|
96
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>(),
|
|
97
134
|
style: options.style ?? '',
|
|
98
|
-
class: options.class ?? ''
|
|
99
|
-
|
|
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;
|
|
100
147
|
}
|
|
101
148
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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()
|
|
105
158
|
|
|
159
|
+
// Configuration methods
|
|
106
160
|
columns(value: (string | ColumnDef)[]): this {
|
|
107
161
|
this.state.columns = value.map(col =>
|
|
108
162
|
typeof col === 'string' ? { key: col, label: col } : col
|
|
@@ -111,101 +165,328 @@ export class Table {
|
|
|
111
165
|
}
|
|
112
166
|
|
|
113
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
|
+
|
|
114
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
|
+
|
|
115
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
|
+
|
|
116
204
|
return this;
|
|
117
205
|
}
|
|
118
206
|
|
|
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
|
+
}
|
|
264
|
+
|
|
265
|
+
return this;
|
|
266
|
+
}
|
|
267
|
+
|
|
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
|
+
|
|
293
|
+
return this;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Visual options
|
|
119
297
|
headers(value: boolean): this {
|
|
120
298
|
this.state.headers = value;
|
|
121
299
|
return this;
|
|
122
300
|
}
|
|
123
|
-
|
|
124
301
|
striped(value: boolean): this {
|
|
125
302
|
this.state.striped = value;
|
|
126
303
|
return this;
|
|
127
304
|
}
|
|
128
|
-
|
|
129
305
|
hoverable(value: boolean): this {
|
|
130
306
|
this.state.hoverable = value;
|
|
131
307
|
return this;
|
|
132
308
|
}
|
|
133
|
-
|
|
134
309
|
bordered(value: boolean): this {
|
|
135
310
|
this.state.bordered = value;
|
|
136
311
|
return this;
|
|
137
312
|
}
|
|
138
|
-
|
|
139
313
|
compact(value: boolean): this {
|
|
140
314
|
this.state.compact = value;
|
|
141
315
|
return this;
|
|
142
316
|
}
|
|
143
317
|
|
|
144
|
-
|
|
145
|
-
|
|
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;
|
|
146
333
|
return this;
|
|
147
334
|
}
|
|
148
335
|
|
|
149
|
-
|
|
150
|
-
|
|
336
|
+
// Selection configuration
|
|
337
|
+
selectable(value: boolean): this {
|
|
338
|
+
this.state.selectable = value;
|
|
339
|
+
return this;
|
|
340
|
+
}
|
|
341
|
+
multiSelect(value: boolean): this {
|
|
342
|
+
this.state.multiSelect = value;
|
|
343
|
+
if (value) {
|
|
344
|
+
this.state.selectable = true; // multi-select implies selectable
|
|
345
|
+
}
|
|
151
346
|
return this;
|
|
152
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
|
+
}
|
|
153
365
|
|
|
154
|
-
/**
|
|
155
|
-
* Bind event handler (stores for wiring in render)
|
|
156
|
-
* DOM events only: click, mouseenter, etc.
|
|
157
|
-
*/
|
|
158
|
-
bind(event: string, handler: Function): this {
|
|
159
|
-
this._bindings.push({ event, handler });
|
|
160
366
|
return this;
|
|
161
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
|
+
}
|
|
162
375
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
376
|
+
// If already rendered, update immediately
|
|
377
|
+
if (this._tableElement) {
|
|
378
|
+
const thead = this._tableElement.querySelector('thead');
|
|
379
|
+
const tbody = this._tableElement.querySelector('tbody');
|
|
380
|
+
|
|
381
|
+
if (thead && tbody) {
|
|
382
|
+
this._updateHeaderCheckbox(thead);
|
|
383
|
+
this._renderTableBody(tbody);
|
|
384
|
+
this._updateBulkCheckboxState();
|
|
385
|
+
}
|
|
173
386
|
}
|
|
174
|
-
|
|
387
|
+
|
|
175
388
|
return this;
|
|
176
389
|
}
|
|
390
|
+
selectionTrigger(value: SelectionTrigger): this {
|
|
391
|
+
this.state.selectionTrigger = value;
|
|
177
392
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
+
}
|
|
181
404
|
|
|
182
|
-
|
|
183
|
-
|
|
405
|
+
return this;
|
|
406
|
+
}
|
|
184
407
|
|
|
185
|
-
|
|
186
|
-
|
|
408
|
+
// Selection actions
|
|
409
|
+
selectAll(): this {
|
|
410
|
+
this.state.rows.forEach((_, index) => {
|
|
411
|
+
this.state.selectedIndexes.add(index);
|
|
412
|
+
});
|
|
413
|
+
this._updateRowSelectionUI();
|
|
187
414
|
|
|
188
|
-
//
|
|
189
|
-
|
|
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
|
+
}
|
|
190
423
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
424
|
+
return this;
|
|
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
|
+
}
|
|
438
|
+
|
|
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
|
+
}
|
|
200
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]);
|
|
201
470
|
}
|
|
202
471
|
|
|
203
|
-
/*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
472
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
473
|
+
* SECTION 5: RENDER LIFECYCLE
|
|
474
|
+
* Main render method and BUILD phase helpers
|
|
475
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
206
476
|
|
|
207
477
|
render(targetId?: string): this {
|
|
208
|
-
|
|
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
|
+
}
|
|
487
|
+
|
|
488
|
+
// Step 1: SETUP
|
|
489
|
+
protected _setupContainer(targetId?: string): HTMLElement {
|
|
209
490
|
let container: HTMLElement;
|
|
210
491
|
if (targetId) {
|
|
211
492
|
const target = document.querySelector(targetId);
|
|
@@ -214,158 +495,747 @@ export class Table {
|
|
|
214
495
|
}
|
|
215
496
|
container = target;
|
|
216
497
|
} else {
|
|
217
|
-
|
|
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;
|
|
218
506
|
}
|
|
219
507
|
this.container = container;
|
|
508
|
+
return container;
|
|
509
|
+
}
|
|
220
510
|
|
|
221
|
-
|
|
222
|
-
|
|
511
|
+
// Step 2: BUILD wrapper
|
|
512
|
+
private _buildWrapper(): HTMLElement {
|
|
513
|
+
const { style, class: className } = this.state;
|
|
223
514
|
|
|
224
|
-
// === 3. BUILD: Create DOM elements ===
|
|
225
515
|
const wrapper = document.createElement('div');
|
|
226
516
|
wrapper.className = 'jux-table-wrapper';
|
|
227
517
|
wrapper.id = this._id;
|
|
228
518
|
if (className) wrapper.className += ` ${className}`;
|
|
229
519
|
if (style) wrapper.setAttribute('style', style);
|
|
230
520
|
|
|
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);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (this.state.filterable) {
|
|
531
|
+
wrapper.appendChild(this._buildFilterInput());
|
|
532
|
+
}
|
|
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;
|
|
231
574
|
const table = document.createElement('table');
|
|
232
575
|
table.className = 'jux-table';
|
|
233
576
|
if (striped) table.classList.add('jux-table-striped');
|
|
234
577
|
if (hoverable) table.classList.add('jux-table-hoverable');
|
|
235
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
|
+
}
|
|
236
591
|
|
|
237
|
-
|
|
592
|
+
private _buildTableHeader(): HTMLTableSectionElement {
|
|
238
593
|
const thead = document.createElement('thead');
|
|
239
594
|
const headerRow = document.createElement('tr');
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
595
|
+
|
|
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);
|
|
244
608
|
headerRow.appendChild(th);
|
|
245
609
|
});
|
|
610
|
+
|
|
246
611
|
thead.appendChild(headerRow);
|
|
247
|
-
|
|
612
|
+
return thead;
|
|
613
|
+
}
|
|
248
614
|
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
615
|
+
private _buildBulkCheckboxCell(): HTMLTableCellElement {
|
|
616
|
+
const bulkTh = document.createElement('th');
|
|
617
|
+
bulkTh.style.width = '40px';
|
|
618
|
+
bulkTh.style.textAlign = 'center';
|
|
619
|
+
|
|
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';
|
|
625
|
+
|
|
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
|
+
}
|
|
780
|
+
} else {
|
|
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
|
+
});
|
|
821
|
+
}
|
|
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;
|
|
874
|
+
} else {
|
|
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) => {
|
|
252
1008
|
const tr = document.createElement('tr');
|
|
253
|
-
|
|
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 => {
|
|
254
1031
|
const td = document.createElement('td');
|
|
255
|
-
const cellValue = row[col.key];
|
|
256
1032
|
|
|
257
|
-
if
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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);
|
|
1046
|
+
} else {
|
|
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);
|
|
261
1057
|
} else {
|
|
262
|
-
|
|
1058
|
+
rendered = cellValue != null ? String(cellValue) : '';
|
|
263
1059
|
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// Insert rendered content
|
|
1063
|
+
if (typeof rendered === 'string') {
|
|
1064
|
+
td.innerHTML = rendered;
|
|
264
1065
|
} else {
|
|
265
|
-
td.
|
|
1066
|
+
td.appendChild(rendered);
|
|
266
1067
|
}
|
|
267
1068
|
|
|
268
1069
|
tr.appendChild(td);
|
|
269
1070
|
});
|
|
1071
|
+
|
|
270
1072
|
tbody.appendChild(tr);
|
|
271
1073
|
});
|
|
272
|
-
|
|
273
|
-
wrapper.appendChild(table);
|
|
274
|
-
|
|
275
|
-
// === 4. WIRE: Attach event listeners and sync bindings ===
|
|
1074
|
+
}
|
|
276
1075
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
+
}
|
|
280
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
|
+
}
|
|
281
1112
|
|
|
282
|
-
|
|
283
|
-
this.
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (col.render) {
|
|
300
|
-
const rendered = col.render(cellValue, row);
|
|
301
|
-
if (typeof rendered === 'string') {
|
|
302
|
-
td.innerHTML = rendered;
|
|
303
|
-
} else {
|
|
304
|
-
td.appendChild(rendered);
|
|
305
|
-
}
|
|
306
|
-
} else {
|
|
307
|
-
td.textContent = cellValue != null ? String(cellValue) : '';
|
|
308
|
-
}
|
|
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
|
+
}
|
|
1129
|
+
}
|
|
309
1130
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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');
|
|
315
1149
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const thead = document.createElement('thead');
|
|
327
|
-
const headerRow = document.createElement('tr');
|
|
328
|
-
transformed.forEach((col: any) => {
|
|
329
|
-
const th = document.createElement('th');
|
|
330
|
-
th.textContent = col.label;
|
|
331
|
-
if (col.width) th.style.width = col.width;
|
|
332
|
-
headerRow.appendChild(th);
|
|
333
|
-
});
|
|
334
|
-
thead.appendChild(headerRow);
|
|
335
|
-
table.appendChild(thead);
|
|
336
|
-
|
|
337
|
-
const tbody = document.createElement('tbody');
|
|
338
|
-
this.state.rows.forEach(row => {
|
|
339
|
-
const tr = document.createElement('tr');
|
|
340
|
-
transformed.forEach((col: any) => {
|
|
341
|
-
const td = document.createElement('td');
|
|
342
|
-
const cellValue = row[col.key];
|
|
343
|
-
|
|
344
|
-
if (col.render) {
|
|
345
|
-
const rendered = col.render(cellValue, row);
|
|
346
|
-
if (typeof rendered === 'string') {
|
|
347
|
-
td.innerHTML = rendered;
|
|
348
|
-
} else {
|
|
349
|
-
td.appendChild(rendered);
|
|
350
|
-
}
|
|
351
|
-
} else {
|
|
352
|
-
td.textContent = cellValue != null ? String(cellValue) : '';
|
|
353
|
-
}
|
|
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
|
+
}
|
|
354
1159
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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;
|
|
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);
|
|
361
1180
|
}
|
|
362
1181
|
});
|
|
1182
|
+
}
|
|
363
1183
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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();
|
|
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);
|
|
367
1232
|
}
|
|
368
1233
|
|
|
1234
|
+
/* ═════════════════════════════════════════════════════════════════
|
|
1235
|
+
* SECTION 8: EXPORTS
|
|
1236
|
+
* Convenience wrappers and factory function
|
|
1237
|
+
* ═════════════════════════════════════════════════════════════════ */
|
|
1238
|
+
|
|
369
1239
|
renderTo(juxComponent: any): this {
|
|
370
1240
|
if (!juxComponent?._id) {
|
|
371
1241
|
throw new Error('Table.renderTo: Invalid component');
|
|
@@ -376,4 +1246,4 @@ export class Table {
|
|
|
376
1246
|
|
|
377
1247
|
export function table(id: string, options: TableOptions = {}): Table {
|
|
378
1248
|
return new Table(id, options);
|
|
379
|
-
}
|
|
1249
|
+
}
|