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.
Files changed (76) hide show
  1. package/bin/cli.js +121 -72
  2. package/lib/components/alert.ts +143 -92
  3. package/lib/components/badge.ts +93 -94
  4. package/lib/components/base/BaseComponent.ts +397 -0
  5. package/lib/components/base/FormInput.ts +322 -0
  6. package/lib/components/button.ts +40 -131
  7. package/lib/components/card.ts +57 -79
  8. package/lib/components/charts/areachart.ts +315 -0
  9. package/lib/components/charts/barchart.ts +421 -0
  10. package/lib/components/charts/doughnutchart.ts +263 -0
  11. package/lib/components/charts/lib/BaseChart.ts +402 -0
  12. package/lib/components/{chart-types.ts → charts/lib/chart-types.ts} +1 -1
  13. package/lib/components/{chart-utils.ts → charts/lib/chart-utils.ts} +1 -1
  14. package/lib/components/{chart.ts → charts/lib/chart.ts} +3 -3
  15. package/lib/components/checkbox.ts +255 -204
  16. package/lib/components/code.ts +31 -78
  17. package/lib/components/container.ts +113 -130
  18. package/lib/components/data.ts +37 -5
  19. package/lib/components/datepicker.ts +180 -147
  20. package/lib/components/dialog.ts +218 -221
  21. package/lib/components/divider.ts +63 -87
  22. package/lib/components/docs-data.json +498 -2404
  23. package/lib/components/dropdown.ts +191 -236
  24. package/lib/components/element.ts +196 -145
  25. package/lib/components/fileupload.ts +253 -167
  26. package/lib/components/guard.ts +92 -0
  27. package/lib/components/heading.ts +31 -97
  28. package/lib/components/helpers.ts +13 -6
  29. package/lib/components/hero.ts +51 -114
  30. package/lib/components/icon.ts +33 -120
  31. package/lib/components/icons.ts +2 -1
  32. package/lib/components/include.ts +76 -3
  33. package/lib/components/input.ts +155 -407
  34. package/lib/components/kpicard.ts +16 -16
  35. package/lib/components/list.ts +358 -261
  36. package/lib/components/loading.ts +142 -211
  37. package/lib/components/menu.ts +63 -152
  38. package/lib/components/modal.ts +42 -129
  39. package/lib/components/nav.ts +79 -101
  40. package/lib/components/paragraph.ts +38 -102
  41. package/lib/components/progress.ts +108 -166
  42. package/lib/components/radio.ts +283 -234
  43. package/lib/components/script.ts +19 -87
  44. package/lib/components/select.ts +189 -199
  45. package/lib/components/sidebar.ts +110 -141
  46. package/lib/components/style.ts +19 -82
  47. package/lib/components/switch.ts +254 -183
  48. package/lib/components/table.ts +1078 -208
  49. package/lib/components/tabs.ts +42 -106
  50. package/lib/components/theme-toggle.ts +73 -165
  51. package/lib/components/tooltip.ts +85 -316
  52. package/lib/components/write.ts +108 -127
  53. package/lib/jux.ts +67 -41
  54. package/machinery/build.js +466 -0
  55. package/machinery/compiler.js +354 -105
  56. package/machinery/server.js +23 -100
  57. package/machinery/watcher.js +153 -130
  58. package/package.json +1 -1
  59. package/presets/base.css +1166 -0
  60. package/presets/notion.css +2 -1975
  61. package/lib/adapters/base-adapter.js +0 -35
  62. package/lib/adapters/index.js +0 -33
  63. package/lib/adapters/mysql-adapter.js +0 -65
  64. package/lib/adapters/postgres-adapter.js +0 -70
  65. package/lib/adapters/sqlite-adapter.js +0 -56
  66. package/lib/components/areachart.ts +0 -1128
  67. package/lib/components/areachartsmooth.ts +0 -1380
  68. package/lib/components/barchart.ts +0 -1322
  69. package/lib/components/doughnutchart.ts +0 -1259
  70. package/lib/components/footer.ts +0 -165
  71. package/lib/components/header.ts +0 -187
  72. package/lib/components/layout.ts +0 -239
  73. package/lib/components/main.ts +0 -137
  74. package/lib/layouts/default.jux +0 -8
  75. package/lib/layouts/figma.jux +0 -0
  76. /package/lib/{themes → components/charts/lib}/charts.js +0 -0
@@ -1,19 +1,46 @@
1
- import { getOrCreateContainer } from './helpers.js';
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
- * Column definition
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
- * Table component
46
- *
47
- * Usage:
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
- // Store table element reference
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
- this.state = {
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
- * Fluent API
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
- style(value: string): this {
145
- this.state.style = value;
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
- class(value: string): this {
150
- this.state.class = value;
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
- * Sync with state (one-way: State → Component)
165
- *
166
- * @param property - Component property to sync ('rows', 'columns')
167
- * @param stateObj - State object to sync with
168
- * @param transform - Optional transform function from state to component
169
- */
170
- sync(property: string, stateObj: State<any>, toState?: Function, toComponent?: Function): this {
171
- if (!stateObj || typeof stateObj.subscribe !== 'function') {
172
- throw new Error(`Table.sync: Expected a State object for property "${property}"`);
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
- this._syncBindings.push({ property, stateObj, toState, toComponent });
387
+
175
388
  return this;
176
389
  }
390
+ selectionTrigger(value: SelectionTrigger): this {
391
+ this.state.selectionTrigger = value;
177
392
 
178
- /* -------------------------
179
- * Helpers
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
- private _updateTable(): void {
183
- if (!this._tableElement) return;
405
+ return this;
406
+ }
184
407
 
185
- const tbody = this._tableElement.querySelector('tbody');
186
- if (!tbody) return;
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
- // Clear existing rows
189
- tbody.innerHTML = '';
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
- // Add new rows
192
- this.state.rows.forEach(rowData => {
193
- const tr = document.createElement('tr');
194
- rowData.forEach(cellData => {
195
- const td = document.createElement('td');
196
- td.textContent = String(cellData);
197
- tr.appendChild(td);
198
- });
199
- tbody.appendChild(tr);
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
- * Render
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
- // === 1. SETUP: Get or create container ===
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
- container = getOrCreateContainer(this._id);
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
- // === 2. PREPARE: Destructure state and check sync flags ===
222
- const { columns, rows, striped, hoverable, bordered, style, class: className } = this.state;
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
- // Header
592
+ private _buildTableHeader(): HTMLTableSectionElement {
238
593
  const thead = document.createElement('thead');
239
594
  const headerRow = document.createElement('tr');
240
- columns.forEach(col => {
241
- const th = document.createElement('th');
242
- th.textContent = col.label;
243
- if (col.width) th.style.width = col.width;
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
- table.appendChild(thead);
612
+ return thead;
613
+ }
248
614
 
249
- // Body
250
- const tbody = document.createElement('tbody');
251
- rows.forEach(row => {
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
- columns.forEach(col => {
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 (col.render) {
258
- const rendered = col.render(cellValue, row);
259
- if (typeof rendered === 'string') {
260
- td.innerHTML = rendered;
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
- td.appendChild(rendered);
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.textContent = cellValue != null ? String(cellValue) : '';
1066
+ td.appendChild(rendered);
266
1067
  }
267
1068
 
268
1069
  tr.appendChild(td);
269
1070
  });
1071
+
270
1072
  tbody.appendChild(tr);
271
1073
  });
272
- table.appendChild(tbody);
273
- wrapper.appendChild(table);
274
-
275
- // === 4. WIRE: Attach event listeners and sync bindings ===
1074
+ }
276
1075
 
277
- // Wire custom bindings from .bind() calls
278
- this._bindings.forEach(({ event, handler }) => {
279
- wrapper.addEventListener(event, handler as EventListener);
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
- // Wire sync bindings from .sync() calls
283
- this._syncBindings.forEach(({ property, stateObj, toState, toComponent }) => {
284
- if (property === 'rows' || property === 'data') {
285
- const transformToComponent = toComponent || ((v: any) => v);
286
-
287
- stateObj.subscribe((val: any) => {
288
- const transformed = transformToComponent(val);
289
- this.state.rows = transformed;
290
-
291
- // Re-render tbody
292
- tbody.innerHTML = '';
293
- transformed.forEach((row: any) => {
294
- const tr = document.createElement('tr');
295
- this.state.columns.forEach(col => {
296
- const td = document.createElement('td');
297
- const cellValue = row[col.key];
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
- tr.appendChild(td);
311
- });
312
- tbody.appendChild(tr);
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
- else if (property === 'columns') {
317
- const transformToComponent = toComponent || ((v: any) => v);
318
-
319
- stateObj.subscribe((val: any) => {
320
- const transformed = transformToComponent(val);
321
- this.state.columns = transformed;
322
-
323
- // Re-render entire table
324
- table.innerHTML = '';
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
- tr.appendChild(td);
356
- });
357
- tbody.appendChild(tr);
358
- });
359
- table.appendChild(tbody);
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
- // === 5. RENDER: Append to DOM and finalize ===
365
- container.appendChild(wrapper);
366
- return this;
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
+ }