juxscript 1.1.2 → 1.1.4
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/machinery/build3.js +7 -91
- package/machinery/compiler3.js +3 -209
- package/machinery/config.js +93 -6
- package/machinery/serve.js +255 -0
- package/machinery/watcher.js +49 -161
- package/package.json +19 -5
- package/lib/components/alert.ts +0 -200
- package/lib/components/app.ts +0 -247
- package/lib/components/badge.ts +0 -101
- package/lib/components/base/BaseComponent.ts +0 -421
- package/lib/components/base/FormInput.ts +0 -227
- package/lib/components/button.ts +0 -178
- package/lib/components/card.ts +0 -173
- package/lib/components/chart.ts +0 -231
- package/lib/components/checkbox.ts +0 -242
- package/lib/components/code.ts +0 -123
- package/lib/components/container.ts +0 -140
- package/lib/components/data.ts +0 -135
- package/lib/components/datepicker.ts +0 -234
- package/lib/components/dialog.ts +0 -172
- package/lib/components/divider.ts +0 -100
- package/lib/components/dropdown.ts +0 -186
- package/lib/components/element.ts +0 -267
- package/lib/components/fileupload.ts +0 -309
- package/lib/components/grid.ts +0 -291
- package/lib/components/guard.ts +0 -92
- package/lib/components/heading.ts +0 -96
- package/lib/components/helpers.ts +0 -41
- package/lib/components/hero.ts +0 -224
- package/lib/components/icon.ts +0 -178
- package/lib/components/icons.ts +0 -464
- package/lib/components/include.ts +0 -410
- package/lib/components/input.ts +0 -457
- package/lib/components/list.ts +0 -419
- package/lib/components/loading.ts +0 -100
- package/lib/components/menu.ts +0 -275
- package/lib/components/modal.ts +0 -284
- package/lib/components/nav.ts +0 -257
- package/lib/components/paragraph.ts +0 -97
- package/lib/components/progress.ts +0 -159
- package/lib/components/radio.ts +0 -278
- package/lib/components/req.ts +0 -303
- package/lib/components/script.ts +0 -41
- package/lib/components/select.ts +0 -252
- package/lib/components/sidebar.ts +0 -275
- package/lib/components/style.ts +0 -41
- package/lib/components/switch.ts +0 -246
- package/lib/components/table.ts +0 -1249
- package/lib/components/tabs.ts +0 -250
- package/lib/components/theme-toggle.ts +0 -293
- package/lib/components/tooltip.ts +0 -144
- package/lib/components/view.ts +0 -190
- package/lib/components/write.ts +0 -272
- package/lib/layouts/default.css +0 -260
- package/lib/layouts/figma.css +0 -334
- package/lib/reactivity/state.ts +0 -78
- package/lib/utils/fetch.ts +0 -553
- package/machinery/ast.js +0 -347
- package/machinery/build.js +0 -466
- package/machinery/bundleAssets.js +0 -0
- package/machinery/bundleJux.js +0 -0
- package/machinery/bundleVendors.js +0 -0
- package/machinery/doc-generator.js +0 -136
- package/machinery/imports.js +0 -155
- package/machinery/server.js +0 -166
- package/machinery/ts-shim.js +0 -46
- package/machinery/validators/file-validator.js +0 -123
package/lib/components/table.ts
DELETED
|
@@ -1,1249 +0,0 @@
|
|
|
1
|
-
/* ═════════════════════════════════════════════════════════════════
|
|
2
|
-
* SECTION 1: DEFINITIONS
|
|
3
|
-
* Type definitions, constants, interfaces
|
|
4
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
5
|
-
|
|
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 {
|
|
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;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface TableOptions {
|
|
45
|
-
columns?: (string | ColumnDef)[];
|
|
46
|
-
rows?: any[][];
|
|
47
|
-
headers?: boolean;
|
|
48
|
-
striped?: boolean;
|
|
49
|
-
hoverable?: boolean;
|
|
50
|
-
bordered?: boolean;
|
|
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;
|
|
61
|
-
style?: string;
|
|
62
|
-
class?: string;
|
|
63
|
-
rowIdField?: string;
|
|
64
|
-
selectionBehavior?: SelectionBehaviorOnDataChange;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
type TableState = {
|
|
68
|
-
columns: ColumnDef[];
|
|
69
|
-
rows: any[][];
|
|
70
|
-
// ✨ NEW: Store computed column definitions
|
|
71
|
-
computedColumns: Map<string, ComputedColumnDef>;
|
|
72
|
-
headers: boolean;
|
|
73
|
-
striped: boolean;
|
|
74
|
-
hoverable: boolean;
|
|
75
|
-
bordered: boolean;
|
|
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>;
|
|
91
|
-
style: string;
|
|
92
|
-
class: string;
|
|
93
|
-
rowIdField?: string;
|
|
94
|
-
selectionBehavior: SelectionBehaviorOnDataChange;
|
|
95
|
-
};
|
|
96
|
-
|
|
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;
|
|
104
|
-
|
|
105
|
-
constructor(id: string, options: TableOptions = {}) {
|
|
106
|
-
const normalizedColumns = (options.columns ?? []).map(col =>
|
|
107
|
-
typeof col === 'string' ? { key: col, label: col } : col
|
|
108
|
-
);
|
|
109
|
-
|
|
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,
|
|
116
|
-
striped: options.striped ?? false,
|
|
117
|
-
hoverable: options.hoverable ?? false,
|
|
118
|
-
bordered: options.bordered ?? false,
|
|
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>(),
|
|
134
|
-
style: options.style ?? '',
|
|
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;
|
|
205
|
-
}
|
|
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
|
|
297
|
-
headers(value: boolean): this {
|
|
298
|
-
this.state.headers = value;
|
|
299
|
-
return this;
|
|
300
|
-
}
|
|
301
|
-
striped(value: boolean): this {
|
|
302
|
-
this.state.striped = value;
|
|
303
|
-
return this;
|
|
304
|
-
}
|
|
305
|
-
hoverable(value: boolean): this {
|
|
306
|
-
this.state.hoverable = value;
|
|
307
|
-
return this;
|
|
308
|
-
}
|
|
309
|
-
bordered(value: boolean): this {
|
|
310
|
-
this.state.bordered = value;
|
|
311
|
-
return this;
|
|
312
|
-
}
|
|
313
|
-
compact(value: boolean): this {
|
|
314
|
-
this.state.compact = value;
|
|
315
|
-
return this;
|
|
316
|
-
}
|
|
317
|
-
|
|
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;
|
|
333
|
-
return this;
|
|
334
|
-
}
|
|
335
|
-
|
|
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
|
-
}
|
|
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
|
-
}
|
|
365
|
-
|
|
366
|
-
return this;
|
|
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
|
-
}
|
|
375
|
-
|
|
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
|
-
}
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
return this;
|
|
389
|
-
}
|
|
390
|
-
selectionTrigger(value: SelectionTrigger): this {
|
|
391
|
-
this.state.selectionTrigger = value;
|
|
392
|
-
|
|
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;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
// Selection actions
|
|
409
|
-
selectAll(): this {
|
|
410
|
-
this.state.rows.forEach((_, index) => {
|
|
411
|
-
this.state.selectedIndexes.add(index);
|
|
412
|
-
});
|
|
413
|
-
this._updateRowSelectionUI();
|
|
414
|
-
|
|
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;
|
|
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
|
-
}
|
|
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
|
-
* ═════════════════════════════════════════════════════════════════ */
|
|
476
|
-
|
|
477
|
-
render(targetId?: string): this {
|
|
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 {
|
|
490
|
-
let container: HTMLElement;
|
|
491
|
-
if (targetId) {
|
|
492
|
-
const target = document.querySelector(targetId);
|
|
493
|
-
if (!target || !(target instanceof HTMLElement)) {
|
|
494
|
-
throw new Error(`Table: Target "${targetId}" not found`);
|
|
495
|
-
}
|
|
496
|
-
container = target;
|
|
497
|
-
} else {
|
|
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;
|
|
506
|
-
}
|
|
507
|
-
this.container = container;
|
|
508
|
-
return container;
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
// Step 2: BUILD wrapper
|
|
512
|
-
private _buildWrapper(): HTMLElement {
|
|
513
|
-
const { style, class: className } = this.state;
|
|
514
|
-
|
|
515
|
-
const wrapper = document.createElement('div');
|
|
516
|
-
wrapper.className = 'jux-table-wrapper';
|
|
517
|
-
wrapper.id = this._id;
|
|
518
|
-
if (className) wrapper.className += ` ${className}`;
|
|
519
|
-
if (style) wrapper.setAttribute('style', style);
|
|
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;
|
|
574
|
-
const table = document.createElement('table');
|
|
575
|
-
table.className = 'jux-table';
|
|
576
|
-
if (striped) table.classList.add('jux-table-striped');
|
|
577
|
-
if (hoverable) table.classList.add('jux-table-hoverable');
|
|
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
|
-
}
|
|
591
|
-
|
|
592
|
-
private _buildTableHeader(): HTMLTableSectionElement {
|
|
593
|
-
const thead = document.createElement('thead');
|
|
594
|
-
const headerRow = document.createElement('tr');
|
|
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);
|
|
608
|
-
headerRow.appendChild(th);
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
thead.appendChild(headerRow);
|
|
612
|
-
return thead;
|
|
613
|
-
}
|
|
614
|
-
|
|
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) => {
|
|
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);
|
|
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);
|
|
1057
|
-
} else {
|
|
1058
|
-
rendered = cellValue != null ? String(cellValue) : '';
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
// Insert rendered content
|
|
1063
|
-
if (typeof rendered === 'string') {
|
|
1064
|
-
td.innerHTML = rendered;
|
|
1065
|
-
} else {
|
|
1066
|
-
td.appendChild(rendered);
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
tr.appendChild(td);
|
|
1070
|
-
});
|
|
1071
|
-
|
|
1072
|
-
tbody.appendChild(tr);
|
|
1073
|
-
});
|
|
1074
|
-
}
|
|
1075
|
-
|
|
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
|
-
}
|
|
1112
|
-
|
|
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
|
-
}
|
|
1130
|
-
|
|
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;
|
|
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
|
-
}
|
|
1183
|
-
|
|
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);
|
|
1232
|
-
}
|
|
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
|
-
}
|
|
1243
|
-
return this.render(`#${juxComponent._id}`);
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
export function table(id: string, options: TableOptions = {}): Table {
|
|
1248
|
-
return new Table(id, options);
|
|
1249
|
-
}
|