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