@dodlhuat/basix 1.2.0 → 1.2.1

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 (93) hide show
  1. package/README.md +56 -1
  2. package/css/accordion.scss +86 -87
  3. package/css/alert.scss +137 -137
  4. package/css/button.scss +48 -0
  5. package/css/calendar.scss +957 -0
  6. package/css/card.scss +65 -65
  7. package/css/chart.scss +270 -157
  8. package/css/chat-bubbles.scss +134 -68
  9. package/css/chips.scss +109 -19
  10. package/css/colors.scss +32 -32
  11. package/css/datepicker.scss +336 -336
  12. package/css/defaults.scss +90 -90
  13. package/css/docs.scss +529 -0
  14. package/css/editor.scss +36 -0
  15. package/css/file-uploader.scss +1 -1
  16. package/css/flyout-menu.scss +361 -361
  17. package/css/form.scss +0 -15
  18. package/css/gallery.scss +65 -6
  19. package/css/grid.scss +41 -40
  20. package/css/group-picker.scss +345 -0
  21. package/css/guitar-chords.css +250 -250
  22. package/css/icons.scss +330 -330
  23. package/css/parameters.scss +3 -3
  24. package/css/placeholder.scss +33 -33
  25. package/css/popover.scss +206 -0
  26. package/css/progress.scss +76 -32
  27. package/css/properties.scss +51 -36
  28. package/css/push-menu.scss +302 -174
  29. package/css/reset.scss +39 -39
  30. package/css/scrollbar.scss +62 -5
  31. package/css/sidebar-nav.scss +92 -0
  32. package/css/spinner.scss +65 -65
  33. package/css/stepper.scss +48 -12
  34. package/css/style.css +3159 -254
  35. package/css/style.css.map +1 -1
  36. package/css/style.min.css +1 -1
  37. package/css/style.scss +51 -45
  38. package/css/table.scss +199 -199
  39. package/css/tabs.scss +154 -123
  40. package/css/timeline.scss +83 -38
  41. package/css/timepicker.scss +100 -5
  42. package/css/toast.scss +81 -81
  43. package/css/virtual-dropdown.scss +35 -29
  44. package/js/calendar.js +532 -0
  45. package/js/calendar.ts +706 -0
  46. package/js/chart.js +573 -257
  47. package/js/chart.ts +692 -0
  48. package/js/code-viewer.js +10 -10
  49. package/js/code-viewer.ts +188 -188
  50. package/js/datepicker.ts +627 -627
  51. package/js/docs-nav.js +204 -0
  52. package/js/dropdown.ts +179 -179
  53. package/js/editor.js +50 -6
  54. package/js/editor.ts +483 -444
  55. package/js/file-uploader.js +1 -0
  56. package/js/file-uploader.ts +1 -0
  57. package/js/flyout-menu.js +14 -14
  58. package/js/flyout-menu.ts +249 -249
  59. package/js/form-builder.js +106 -106
  60. package/js/gallery.js +14 -8
  61. package/js/gallery.ts +245 -236
  62. package/js/group-picker.js +342 -0
  63. package/js/group-picker.ts +447 -0
  64. package/js/guitar-chords.js +268 -268
  65. package/js/lazy-loader.js +121 -121
  66. package/js/modal.ts +166 -166
  67. package/js/popover.js +163 -0
  68. package/js/popover.ts +219 -0
  69. package/js/position.js +108 -0
  70. package/js/position.ts +111 -0
  71. package/js/push-menu.js +113 -0
  72. package/js/push-menu.ts +284 -145
  73. package/js/request.js +50 -50
  74. package/js/scroll.ts +47 -47
  75. package/js/scrollbar.js +13 -0
  76. package/js/scrollbar.ts +324 -307
  77. package/js/select.ts +216 -216
  78. package/js/sidebar-nav.js +41 -0
  79. package/js/sidebar-nav.ts +66 -0
  80. package/js/table.ts +452 -452
  81. package/js/tabs.ts +279 -279
  82. package/js/theme.js +17 -6
  83. package/js/theme.ts +234 -224
  84. package/js/toast.ts +137 -137
  85. package/js/tooltip.js +6 -60
  86. package/js/tooltip.ts +184 -251
  87. package/js/tsconfig.json +18 -18
  88. package/js/utils.ts +83 -83
  89. package/js/virtual-dropdown.js +25 -25
  90. package/js/virtual-dropdown.ts +365 -365
  91. package/package.json +39 -39
  92. package/js/index.js +0 -816
  93. package/js/index.ts +0 -987
package/js/table.ts CHANGED
@@ -1,453 +1,453 @@
1
- import { Select } from "./select.js";
2
-
3
- interface TableColumn {
4
- key: string;
5
- label: string;
6
- sortable?: boolean;
7
- }
8
-
9
- interface TableRow {
10
- [key: string]: string | number | boolean;
11
- }
12
-
13
- interface TableOptions {
14
- data?: TableRow[];
15
- columns?: TableColumn[];
16
- pageSize?: number;
17
- }
18
-
19
- type SortDirection = 'asc' | 'desc';
20
-
21
- class Table {
22
- private container: HTMLElement;
23
- private data: TableRow[];
24
- private columns: TableColumn[];
25
- private pageSize: number;
26
- private currentPage: number;
27
- private sortColumn: string | null;
28
- private sortDirection: SortDirection;
29
- private filterText: string;
30
- private tableBody!: HTMLTableSectionElement;
31
- private tableHeader!: HTMLTableSectionElement;
32
- private paginationContainer!: HTMLDivElement;
33
-
34
- constructor(elementOrSelector: string | HTMLElement, options: TableOptions = {}) {
35
- const element = typeof elementOrSelector === 'string'
36
- ? document.querySelector<HTMLElement>(elementOrSelector)
37
- : elementOrSelector;
38
-
39
- if (!element) {
40
- throw new Error(`Table: Element not found for selector "${elementOrSelector}"`);
41
- }
42
-
43
- this.container = element;
44
- this.data = options.data || [];
45
- this.columns = options.columns || [];
46
- this.pageSize = options.pageSize || 10;
47
- this.currentPage = 1;
48
- this.sortColumn = null;
49
- this.sortDirection = 'asc';
50
- this.filterText = '';
51
-
52
- if (this.data.length === 0 && this.container.querySelector('table')) {
53
- this.parseTableFromDOM();
54
- }
55
-
56
- this.init();
57
- }
58
-
59
- /**
60
- * Parses an existing HTML table in the DOM to extract data and columns
61
- */
62
- private parseTableFromDOM(): void {
63
- const table = this.container.querySelector('table');
64
- if (!table) return;
65
-
66
- const thead = table.querySelector('thead');
67
- const tbody = table.querySelector('tbody');
68
-
69
- if (!thead || !tbody) return;
70
-
71
- // Parse columns from header
72
- const ths = thead.querySelectorAll('th');
73
- this.columns = Array.from(ths).map((th, index) => ({
74
- key: `col${index}`,
75
- label: th.textContent?.trim() || '',
76
- sortable: true
77
- }));
78
-
79
- // Parse data from body rows
80
- const trs = tbody.querySelectorAll('tr');
81
- this.data = Array.from(trs).map(tr => {
82
- const row: TableRow = {};
83
- const tds = tr.querySelectorAll('td');
84
-
85
- tds.forEach((td, index) => {
86
- if (this.columns[index]) {
87
- row[this.columns[index].key] = td.textContent?.trim() || '';
88
- }
89
- });
90
-
91
- return row;
92
- });
93
-
94
- // Clear the existing static table
95
- this.container.innerHTML = '';
96
- }
97
-
98
- /**
99
- * Initializes the table by rendering controls, structure, and content
100
- */
101
- private init(): void {
102
- this.renderControls();
103
- this.renderTableStructure();
104
- this.render();
105
- }
106
-
107
- /**
108
- * Renders the search and page size controls
109
- */
110
- private renderControls(): void {
111
- const controlsDiv = document.createElement('div');
112
- controlsDiv.className = 'table-controls';
113
-
114
- // Search input
115
- const searchInput = document.createElement('input');
116
- searchInput.type = 'text';
117
- searchInput.placeholder = 'Search...';
118
- searchInput.className = 'search-input';
119
- searchInput.addEventListener('input', (e) => {
120
- this.handleSearch((e.target as HTMLInputElement).value);
121
- });
122
- controlsDiv.appendChild(searchInput);
123
-
124
- // Page size selector
125
- const selectGroup = document.createElement('div');
126
- selectGroup.className = 'select-group';
127
-
128
- const label = document.createElement('label');
129
- label.textContent = 'Page Size';
130
- selectGroup.appendChild(label);
131
-
132
- const pageSizeSelect = document.createElement('select');
133
- pageSizeSelect.className = 'page-size-select';
134
-
135
- [5, 10, 20, 50].forEach(size => {
136
- const option = document.createElement('option');
137
- option.value = String(size);
138
- option.textContent = `${size} per page`;
139
- option.selected = size === this.pageSize;
140
- pageSizeSelect.appendChild(option);
141
- });
142
-
143
- pageSizeSelect.addEventListener('change', (e) => {
144
- this.handlePageSizeChange(parseInt((e.target as HTMLSelectElement).value, 10));
145
- });
146
-
147
- this.assignUniqueId(pageSizeSelect, 'page-size-select-0');
148
- selectGroup.appendChild(pageSizeSelect);
149
- controlsDiv.appendChild(selectGroup);
150
-
151
- this.container.appendChild(controlsDiv);
152
- new Select('#' + pageSizeSelect.id);
153
- }
154
-
155
- /**
156
- * Creates the table structure (table, thead, tbody, pagination container)
157
- */
158
- private renderTableStructure(): void {
159
- const wrapper = document.createElement('div');
160
- wrapper.className = 'table-wrapper';
161
-
162
- const table = document.createElement('table');
163
- const thead = document.createElement('thead');
164
- const tbody = document.createElement('tbody');
165
-
166
- // Create header row
167
- const tr = document.createElement('tr');
168
- this.columns.forEach(col => {
169
- const th = document.createElement('th');
170
- th.textContent = col.label;
171
- th.dataset.key = col.key;
172
-
173
- if (col.sortable !== false) {
174
- th.classList.add('sortable');
175
- th.addEventListener('click', () => this.handleSort(col.key));
176
- }
177
-
178
- tr.appendChild(th);
179
- });
180
- thead.appendChild(tr);
181
-
182
- table.appendChild(thead);
183
- table.appendChild(tbody);
184
- wrapper.appendChild(table);
185
- this.container.appendChild(wrapper);
186
-
187
- // Create pagination container
188
- const paginationDiv = document.createElement('div');
189
- paginationDiv.className = 'pagination';
190
- this.container.appendChild(paginationDiv);
191
-
192
- this.tableBody = tbody;
193
- this.tableHeader = thead;
194
- this.paginationContainer = paginationDiv;
195
- }
196
-
197
- /**
198
- * Returns filtered and sorted data based on current state
199
- */
200
- private getFilteredAndSortedData(): TableRow[] {
201
- let processedData = [...this.data];
202
-
203
- // Apply filter
204
- if (this.filterText) {
205
- const lowerFilter = this.filterText.toLowerCase();
206
- processedData = processedData.filter(row => {
207
- return this.columns.some(col => {
208
- const val = String(row[col.key] ?? '').toLowerCase();
209
- return val.includes(lowerFilter);
210
- });
211
- });
212
- }
213
-
214
- // Apply sort
215
- if (this.sortColumn) {
216
- processedData.sort((a, b) => {
217
- const valA = a[this.sortColumn!];
218
- const valB = b[this.sortColumn!];
219
-
220
- // Handle null/undefined values
221
- if (valA == null && valB == null) return 0;
222
- if (valA == null) return 1;
223
- if (valB == null) return -1;
224
-
225
- if (valA < valB) return this.sortDirection === 'asc' ? -1 : 1;
226
- if (valA > valB) return this.sortDirection === 'asc' ? 1 : -1;
227
- return 0;
228
- });
229
- }
230
-
231
- return processedData;
232
- }
233
-
234
- /**
235
- * Renders the table body, pagination, and header sort indicators
236
- */
237
- private render(): void {
238
- const processedData = this.getFilteredAndSortedData();
239
- const totalItems = processedData.length;
240
- const totalPages = Math.ceil(totalItems / this.pageSize);
241
-
242
- // Ensure current page is valid
243
- if (this.currentPage > totalPages && totalPages > 0) {
244
- this.currentPage = totalPages;
245
- }
246
- if (this.currentPage < 1 && totalPages > 0) {
247
- this.currentPage = 1;
248
- }
249
-
250
- const startIndex = (this.currentPage - 1) * this.pageSize;
251
- const endIndex = Math.min(startIndex + this.pageSize, totalItems);
252
- const pageData = processedData.slice(startIndex, endIndex);
253
-
254
- this.renderBody(pageData);
255
- this.renderPagination(totalItems, totalPages, startIndex, endIndex);
256
- this.updateHeaderSortIcons();
257
- }
258
-
259
- /**
260
- * Renders the table body rows
261
- */
262
- private renderBody(data: TableRow[]): void {
263
- this.tableBody.innerHTML = '';
264
-
265
- if (data.length === 0) {
266
- const tr = document.createElement('tr');
267
- const td = document.createElement('td');
268
- td.colSpan = this.columns.length;
269
- td.textContent = 'No results found';
270
- td.style.textAlign = 'center';
271
- tr.appendChild(td);
272
- this.tableBody.appendChild(tr);
273
- return;
274
- }
275
-
276
- data.forEach(row => {
277
- const tr = document.createElement('tr');
278
- this.columns.forEach(col => {
279
- const td = document.createElement('td');
280
- td.textContent = String(row[col.key] ?? '');
281
- td.setAttribute('data-label', col.label); // For mobile view
282
- tr.appendChild(td);
283
- });
284
- this.tableBody.appendChild(tr);
285
- });
286
- }
287
-
288
- /**
289
- * Updates the sort direction indicators in table headers
290
- */
291
- private updateHeaderSortIcons(): void {
292
- const ths = this.tableHeader.querySelectorAll('th');
293
- ths.forEach(th => {
294
- th.classList.remove('sort-asc', 'sort-desc');
295
- if (th.dataset.key === this.sortColumn) {
296
- th.classList.add(this.sortDirection === 'asc' ? 'sort-asc' : 'sort-desc');
297
- }
298
- });
299
- }
300
-
301
- /**
302
- * Renders pagination controls and info
303
- */
304
- private renderPagination(
305
- totalItems: number,
306
- totalPages: number,
307
- startIndex: number,
308
- endIndex: number
309
- ): void {
310
- this.paginationContainer.innerHTML = '';
311
-
312
- if (totalItems === 0) return;
313
-
314
- // Info text
315
- const info = document.createElement('div');
316
- info.className = 'pagination-info';
317
- info.textContent = `Showing ${startIndex + 1} to ${endIndex} of ${totalItems} entries`;
318
- this.paginationContainer.appendChild(info);
319
-
320
- // Pagination buttons
321
- const buttonsDiv = document.createElement('div');
322
- buttonsDiv.className = 'pagination-buttons';
323
-
324
- // Previous button
325
- const prevBtn = document.createElement('button');
326
- prevBtn.className = 'page-btn';
327
- prevBtn.textContent = 'Previous';
328
- prevBtn.disabled = this.currentPage === 1;
329
- prevBtn.addEventListener('click', () => this.setPage(this.currentPage - 1));
330
- buttonsDiv.appendChild(prevBtn);
331
-
332
- // Calculate page range to display (max 5 pages)
333
- let startPage = Math.max(1, this.currentPage - 2);
334
- let endPage = Math.min(totalPages, startPage + 4);
335
-
336
- if (endPage - startPage < 4) {
337
- startPage = Math.max(1, endPage - 4);
338
- }
339
-
340
- // Page number buttons
341
- for (let i = startPage; i <= endPage; i++) {
342
- const btn = document.createElement('button');
343
- btn.className = `page-btn ${i === this.currentPage ? 'active' : ''}`;
344
- btn.textContent = String(i);
345
- btn.addEventListener('click', () => this.setPage(i));
346
- buttonsDiv.appendChild(btn);
347
- }
348
-
349
- // Next button
350
- const nextBtn = document.createElement('button');
351
- nextBtn.className = 'page-btn';
352
- nextBtn.textContent = 'Next';
353
- nextBtn.disabled = this.currentPage === totalPages;
354
- nextBtn.addEventListener('click', () => this.setPage(this.currentPage + 1));
355
- buttonsDiv.appendChild(nextBtn);
356
-
357
- this.paginationContainer.appendChild(buttonsDiv);
358
- }
359
-
360
- /**
361
- * Handles search input changes
362
- */
363
- private handleSearch(text: string): void {
364
- this.filterText = text;
365
- this.currentPage = 1; // Reset to first page on search
366
- this.render();
367
- }
368
-
369
- /**
370
- * Handles column header clicks for sorting
371
- */
372
- private handleSort(key: string): void {
373
- if (this.sortColumn === key) {
374
- // Toggle sort direction
375
- this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
376
- } else {
377
- this.sortColumn = key;
378
- this.sortDirection = 'asc';
379
- }
380
- this.render();
381
- }
382
-
383
- /**
384
- * Handles page size changes
385
- */
386
- private handlePageSizeChange(size: number): void {
387
- this.pageSize = size;
388
- this.currentPage = 1;
389
- this.render();
390
- }
391
-
392
- /**
393
- * Sets the current page and re-renders
394
- */
395
- private setPage(page: number): void {
396
- this.currentPage = page;
397
- this.render();
398
- }
399
-
400
- /**
401
- * Assigns a unique ID to an element, incrementing if necessary
402
- */
403
- private assignUniqueId(element: HTMLElement, baseId: string): string | null {
404
- if (!element || !baseId) return null;
405
-
406
- let id = baseId;
407
- let counter = 1;
408
-
409
- // If baseId already ends with a number, extract it
410
- const match = baseId.match(/^(.*?)(\d+)$/);
411
- if (match) {
412
- id = match[1];
413
- counter = parseInt(match[2], 10);
414
- }
415
-
416
- let uniqueId = baseId;
417
-
418
- while (document.getElementById(uniqueId)) {
419
- counter++;
420
- uniqueId = `${id}${counter}`;
421
- }
422
-
423
- element.id = uniqueId;
424
- return uniqueId;
425
- }
426
-
427
- /**
428
- * Public API: Updates the table data and re-renders
429
- */
430
- public setData(data: TableRow[]): void {
431
- this.data = data;
432
- this.currentPage = 1;
433
- this.render();
434
- }
435
-
436
- /**
437
- * Public API: Updates the columns and re-renders
438
- */
439
- public setColumns(columns: TableColumn[]): void {
440
- this.columns = columns;
441
- this.container.innerHTML = '';
442
- this.init();
443
- }
444
-
445
- /**
446
- * Public API: Gets the current filtered and sorted data
447
- */
448
- public getData(): TableRow[] {
449
- return this.getFilteredAndSortedData();
450
- }
451
- }
452
-
1
+ import { Select } from "./select.js";
2
+
3
+ interface TableColumn {
4
+ key: string;
5
+ label: string;
6
+ sortable?: boolean;
7
+ }
8
+
9
+ interface TableRow {
10
+ [key: string]: string | number | boolean;
11
+ }
12
+
13
+ interface TableOptions {
14
+ data?: TableRow[];
15
+ columns?: TableColumn[];
16
+ pageSize?: number;
17
+ }
18
+
19
+ type SortDirection = 'asc' | 'desc';
20
+
21
+ class Table {
22
+ private container: HTMLElement;
23
+ private data: TableRow[];
24
+ private columns: TableColumn[];
25
+ private pageSize: number;
26
+ private currentPage: number;
27
+ private sortColumn: string | null;
28
+ private sortDirection: SortDirection;
29
+ private filterText: string;
30
+ private tableBody!: HTMLTableSectionElement;
31
+ private tableHeader!: HTMLTableSectionElement;
32
+ private paginationContainer!: HTMLDivElement;
33
+
34
+ constructor(elementOrSelector: string | HTMLElement, options: TableOptions = {}) {
35
+ const element = typeof elementOrSelector === 'string'
36
+ ? document.querySelector<HTMLElement>(elementOrSelector)
37
+ : elementOrSelector;
38
+
39
+ if (!element) {
40
+ throw new Error(`Table: Element not found for selector "${elementOrSelector}"`);
41
+ }
42
+
43
+ this.container = element;
44
+ this.data = options.data || [];
45
+ this.columns = options.columns || [];
46
+ this.pageSize = options.pageSize || 10;
47
+ this.currentPage = 1;
48
+ this.sortColumn = null;
49
+ this.sortDirection = 'asc';
50
+ this.filterText = '';
51
+
52
+ if (this.data.length === 0 && this.container.querySelector('table')) {
53
+ this.parseTableFromDOM();
54
+ }
55
+
56
+ this.init();
57
+ }
58
+
59
+ /**
60
+ * Parses an existing HTML table in the DOM to extract data and columns
61
+ */
62
+ private parseTableFromDOM(): void {
63
+ const table = this.container.querySelector('table');
64
+ if (!table) return;
65
+
66
+ const thead = table.querySelector('thead');
67
+ const tbody = table.querySelector('tbody');
68
+
69
+ if (!thead || !tbody) return;
70
+
71
+ // Parse columns from header
72
+ const ths = thead.querySelectorAll('th');
73
+ this.columns = Array.from(ths).map((th, index) => ({
74
+ key: `col${index}`,
75
+ label: th.textContent?.trim() || '',
76
+ sortable: true
77
+ }));
78
+
79
+ // Parse data from body rows
80
+ const trs = tbody.querySelectorAll('tr');
81
+ this.data = Array.from(trs).map(tr => {
82
+ const row: TableRow = {};
83
+ const tds = tr.querySelectorAll('td');
84
+
85
+ tds.forEach((td, index) => {
86
+ if (this.columns[index]) {
87
+ row[this.columns[index].key] = td.textContent?.trim() || '';
88
+ }
89
+ });
90
+
91
+ return row;
92
+ });
93
+
94
+ // Clear the existing static table
95
+ this.container.innerHTML = '';
96
+ }
97
+
98
+ /**
99
+ * Initializes the table by rendering controls, structure, and content
100
+ */
101
+ private init(): void {
102
+ this.renderControls();
103
+ this.renderTableStructure();
104
+ this.render();
105
+ }
106
+
107
+ /**
108
+ * Renders the search and page size controls
109
+ */
110
+ private renderControls(): void {
111
+ const controlsDiv = document.createElement('div');
112
+ controlsDiv.className = 'table-controls';
113
+
114
+ // Search input
115
+ const searchInput = document.createElement('input');
116
+ searchInput.type = 'text';
117
+ searchInput.placeholder = 'Search...';
118
+ searchInput.className = 'search-input';
119
+ searchInput.addEventListener('input', (e) => {
120
+ this.handleSearch((e.target as HTMLInputElement).value);
121
+ });
122
+ controlsDiv.appendChild(searchInput);
123
+
124
+ // Page size selector
125
+ const selectGroup = document.createElement('div');
126
+ selectGroup.className = 'select-group';
127
+
128
+ const label = document.createElement('label');
129
+ label.textContent = 'Page Size';
130
+ selectGroup.appendChild(label);
131
+
132
+ const pageSizeSelect = document.createElement('select');
133
+ pageSizeSelect.className = 'page-size-select';
134
+
135
+ [5, 10, 20, 50].forEach(size => {
136
+ const option = document.createElement('option');
137
+ option.value = String(size);
138
+ option.textContent = `${size} per page`;
139
+ option.selected = size === this.pageSize;
140
+ pageSizeSelect.appendChild(option);
141
+ });
142
+
143
+ pageSizeSelect.addEventListener('change', (e) => {
144
+ this.handlePageSizeChange(parseInt((e.target as HTMLSelectElement).value, 10));
145
+ });
146
+
147
+ this.assignUniqueId(pageSizeSelect, 'page-size-select-0');
148
+ selectGroup.appendChild(pageSizeSelect);
149
+ controlsDiv.appendChild(selectGroup);
150
+
151
+ this.container.appendChild(controlsDiv);
152
+ new Select('#' + pageSizeSelect.id);
153
+ }
154
+
155
+ /**
156
+ * Creates the table structure (table, thead, tbody, pagination container)
157
+ */
158
+ private renderTableStructure(): void {
159
+ const wrapper = document.createElement('div');
160
+ wrapper.className = 'table-wrapper';
161
+
162
+ const table = document.createElement('table');
163
+ const thead = document.createElement('thead');
164
+ const tbody = document.createElement('tbody');
165
+
166
+ // Create header row
167
+ const tr = document.createElement('tr');
168
+ this.columns.forEach(col => {
169
+ const th = document.createElement('th');
170
+ th.textContent = col.label;
171
+ th.dataset.key = col.key;
172
+
173
+ if (col.sortable !== false) {
174
+ th.classList.add('sortable');
175
+ th.addEventListener('click', () => this.handleSort(col.key));
176
+ }
177
+
178
+ tr.appendChild(th);
179
+ });
180
+ thead.appendChild(tr);
181
+
182
+ table.appendChild(thead);
183
+ table.appendChild(tbody);
184
+ wrapper.appendChild(table);
185
+ this.container.appendChild(wrapper);
186
+
187
+ // Create pagination container
188
+ const paginationDiv = document.createElement('div');
189
+ paginationDiv.className = 'pagination';
190
+ this.container.appendChild(paginationDiv);
191
+
192
+ this.tableBody = tbody;
193
+ this.tableHeader = thead;
194
+ this.paginationContainer = paginationDiv;
195
+ }
196
+
197
+ /**
198
+ * Returns filtered and sorted data based on current state
199
+ */
200
+ private getFilteredAndSortedData(): TableRow[] {
201
+ let processedData = [...this.data];
202
+
203
+ // Apply filter
204
+ if (this.filterText) {
205
+ const lowerFilter = this.filterText.toLowerCase();
206
+ processedData = processedData.filter(row => {
207
+ return this.columns.some(col => {
208
+ const val = String(row[col.key] ?? '').toLowerCase();
209
+ return val.includes(lowerFilter);
210
+ });
211
+ });
212
+ }
213
+
214
+ // Apply sort
215
+ if (this.sortColumn) {
216
+ processedData.sort((a, b) => {
217
+ const valA = a[this.sortColumn!];
218
+ const valB = b[this.sortColumn!];
219
+
220
+ // Handle null/undefined values
221
+ if (valA == null && valB == null) return 0;
222
+ if (valA == null) return 1;
223
+ if (valB == null) return -1;
224
+
225
+ if (valA < valB) return this.sortDirection === 'asc' ? -1 : 1;
226
+ if (valA > valB) return this.sortDirection === 'asc' ? 1 : -1;
227
+ return 0;
228
+ });
229
+ }
230
+
231
+ return processedData;
232
+ }
233
+
234
+ /**
235
+ * Renders the table body, pagination, and header sort indicators
236
+ */
237
+ private render(): void {
238
+ const processedData = this.getFilteredAndSortedData();
239
+ const totalItems = processedData.length;
240
+ const totalPages = Math.ceil(totalItems / this.pageSize);
241
+
242
+ // Ensure current page is valid
243
+ if (this.currentPage > totalPages && totalPages > 0) {
244
+ this.currentPage = totalPages;
245
+ }
246
+ if (this.currentPage < 1 && totalPages > 0) {
247
+ this.currentPage = 1;
248
+ }
249
+
250
+ const startIndex = (this.currentPage - 1) * this.pageSize;
251
+ const endIndex = Math.min(startIndex + this.pageSize, totalItems);
252
+ const pageData = processedData.slice(startIndex, endIndex);
253
+
254
+ this.renderBody(pageData);
255
+ this.renderPagination(totalItems, totalPages, startIndex, endIndex);
256
+ this.updateHeaderSortIcons();
257
+ }
258
+
259
+ /**
260
+ * Renders the table body rows
261
+ */
262
+ private renderBody(data: TableRow[]): void {
263
+ this.tableBody.innerHTML = '';
264
+
265
+ if (data.length === 0) {
266
+ const tr = document.createElement('tr');
267
+ const td = document.createElement('td');
268
+ td.colSpan = this.columns.length;
269
+ td.textContent = 'No results found';
270
+ td.style.textAlign = 'center';
271
+ tr.appendChild(td);
272
+ this.tableBody.appendChild(tr);
273
+ return;
274
+ }
275
+
276
+ data.forEach(row => {
277
+ const tr = document.createElement('tr');
278
+ this.columns.forEach(col => {
279
+ const td = document.createElement('td');
280
+ td.textContent = String(row[col.key] ?? '');
281
+ td.setAttribute('data-label', col.label); // For mobile view
282
+ tr.appendChild(td);
283
+ });
284
+ this.tableBody.appendChild(tr);
285
+ });
286
+ }
287
+
288
+ /**
289
+ * Updates the sort direction indicators in table headers
290
+ */
291
+ private updateHeaderSortIcons(): void {
292
+ const ths = this.tableHeader.querySelectorAll('th');
293
+ ths.forEach(th => {
294
+ th.classList.remove('sort-asc', 'sort-desc');
295
+ if (th.dataset.key === this.sortColumn) {
296
+ th.classList.add(this.sortDirection === 'asc' ? 'sort-asc' : 'sort-desc');
297
+ }
298
+ });
299
+ }
300
+
301
+ /**
302
+ * Renders pagination controls and info
303
+ */
304
+ private renderPagination(
305
+ totalItems: number,
306
+ totalPages: number,
307
+ startIndex: number,
308
+ endIndex: number
309
+ ): void {
310
+ this.paginationContainer.innerHTML = '';
311
+
312
+ if (totalItems === 0) return;
313
+
314
+ // Info text
315
+ const info = document.createElement('div');
316
+ info.className = 'pagination-info';
317
+ info.textContent = `Showing ${startIndex + 1} to ${endIndex} of ${totalItems} entries`;
318
+ this.paginationContainer.appendChild(info);
319
+
320
+ // Pagination buttons
321
+ const buttonsDiv = document.createElement('div');
322
+ buttonsDiv.className = 'pagination-buttons';
323
+
324
+ // Previous button
325
+ const prevBtn = document.createElement('button');
326
+ prevBtn.className = 'page-btn';
327
+ prevBtn.textContent = 'Previous';
328
+ prevBtn.disabled = this.currentPage === 1;
329
+ prevBtn.addEventListener('click', () => this.setPage(this.currentPage - 1));
330
+ buttonsDiv.appendChild(prevBtn);
331
+
332
+ // Calculate page range to display (max 5 pages)
333
+ let startPage = Math.max(1, this.currentPage - 2);
334
+ let endPage = Math.min(totalPages, startPage + 4);
335
+
336
+ if (endPage - startPage < 4) {
337
+ startPage = Math.max(1, endPage - 4);
338
+ }
339
+
340
+ // Page number buttons
341
+ for (let i = startPage; i <= endPage; i++) {
342
+ const btn = document.createElement('button');
343
+ btn.className = `page-btn ${i === this.currentPage ? 'active' : ''}`;
344
+ btn.textContent = String(i);
345
+ btn.addEventListener('click', () => this.setPage(i));
346
+ buttonsDiv.appendChild(btn);
347
+ }
348
+
349
+ // Next button
350
+ const nextBtn = document.createElement('button');
351
+ nextBtn.className = 'page-btn';
352
+ nextBtn.textContent = 'Next';
353
+ nextBtn.disabled = this.currentPage === totalPages;
354
+ nextBtn.addEventListener('click', () => this.setPage(this.currentPage + 1));
355
+ buttonsDiv.appendChild(nextBtn);
356
+
357
+ this.paginationContainer.appendChild(buttonsDiv);
358
+ }
359
+
360
+ /**
361
+ * Handles search input changes
362
+ */
363
+ private handleSearch(text: string): void {
364
+ this.filterText = text;
365
+ this.currentPage = 1; // Reset to first page on search
366
+ this.render();
367
+ }
368
+
369
+ /**
370
+ * Handles column header clicks for sorting
371
+ */
372
+ private handleSort(key: string): void {
373
+ if (this.sortColumn === key) {
374
+ // Toggle sort direction
375
+ this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
376
+ } else {
377
+ this.sortColumn = key;
378
+ this.sortDirection = 'asc';
379
+ }
380
+ this.render();
381
+ }
382
+
383
+ /**
384
+ * Handles page size changes
385
+ */
386
+ private handlePageSizeChange(size: number): void {
387
+ this.pageSize = size;
388
+ this.currentPage = 1;
389
+ this.render();
390
+ }
391
+
392
+ /**
393
+ * Sets the current page and re-renders
394
+ */
395
+ private setPage(page: number): void {
396
+ this.currentPage = page;
397
+ this.render();
398
+ }
399
+
400
+ /**
401
+ * Assigns a unique ID to an element, incrementing if necessary
402
+ */
403
+ private assignUniqueId(element: HTMLElement, baseId: string): string | null {
404
+ if (!element || !baseId) return null;
405
+
406
+ let id = baseId;
407
+ let counter = 1;
408
+
409
+ // If baseId already ends with a number, extract it
410
+ const match = baseId.match(/^(.*?)(\d+)$/);
411
+ if (match) {
412
+ id = match[1];
413
+ counter = parseInt(match[2], 10);
414
+ }
415
+
416
+ let uniqueId = baseId;
417
+
418
+ while (document.getElementById(uniqueId)) {
419
+ counter++;
420
+ uniqueId = `${id}${counter}`;
421
+ }
422
+
423
+ element.id = uniqueId;
424
+ return uniqueId;
425
+ }
426
+
427
+ /**
428
+ * Public API: Updates the table data and re-renders
429
+ */
430
+ public setData(data: TableRow[]): void {
431
+ this.data = data;
432
+ this.currentPage = 1;
433
+ this.render();
434
+ }
435
+
436
+ /**
437
+ * Public API: Updates the columns and re-renders
438
+ */
439
+ public setColumns(columns: TableColumn[]): void {
440
+ this.columns = columns;
441
+ this.container.innerHTML = '';
442
+ this.init();
443
+ }
444
+
445
+ /**
446
+ * Public API: Gets the current filtered and sorted data
447
+ */
448
+ public getData(): TableRow[] {
449
+ return this.getFilteredAndSortedData();
450
+ }
451
+ }
452
+
453
453
  export { Table, TableRow, TableColumn, TableOptions };