adminator-admin-dashboard 2.7.1 → 2.8.0

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.
@@ -1,707 +0,0 @@
1
- /**
2
- * DataTable Implementation with TypeScript
3
- * Vanilla JavaScript DataTable with sorting, searching, and pagination
4
- */
5
-
6
- import type { ComponentInterface } from '../../types';
7
-
8
- // Type definitions for DataTable
9
- export interface DataTableOptions {
10
- sortable?: boolean;
11
- searchable?: boolean;
12
- pagination?: boolean;
13
- pageSize?: number;
14
- responsive?: boolean;
15
- striped?: boolean;
16
- bordered?: boolean;
17
- hover?: boolean;
18
- }
19
-
20
- export interface DataTableColumn {
21
- title: string;
22
- data: string | number;
23
- sortable?: boolean;
24
- searchable?: boolean;
25
- width?: string;
26
- className?: string;
27
- render?: (data: any, row: any[], index: number) => string;
28
- }
29
-
30
- export interface DataTableData {
31
- columns: DataTableColumn[];
32
- rows: any[][];
33
- }
34
-
35
- export interface DataTableState {
36
- currentPage: number;
37
- sortColumn: number | null;
38
- sortDirection: 'asc' | 'desc';
39
- searchQuery: string;
40
- filteredData: any[][];
41
- totalPages: number;
42
- }
43
-
44
- export type SortDirection = 'asc' | 'desc';
45
-
46
- declare global {
47
- interface HTMLTableElement {
48
- dataTableInstance?: VanillaDataTable;
49
- }
50
- }
51
-
52
- // Enhanced DataTable implementation
53
- export class VanillaDataTable implements ComponentInterface {
54
- public name: string = 'VanillaDataTable';
55
- public element: HTMLTableElement;
56
- public options: DataTableOptions;
57
- public isInitialized: boolean = false;
58
-
59
- private originalData: any[][] = [];
60
- private filteredData: any[][] = [];
61
- private state: DataTableState;
62
- private wrapper: HTMLElement | null = null;
63
- private searchInput: HTMLInputElement | null = null;
64
- private infoElement: HTMLElement | null = null;
65
- private paginationElement: HTMLElement | null = null;
66
-
67
- constructor(element: HTMLTableElement, options: DataTableOptions = {}) {
68
- this.element = element;
69
- this.options = {
70
- sortable: true,
71
- searchable: true,
72
- pagination: true,
73
- pageSize: 10,
74
- responsive: true,
75
- striped: true,
76
- bordered: true,
77
- hover: true,
78
- ...options,
79
- };
80
-
81
- this.state = {
82
- currentPage: 1,
83
- sortColumn: null,
84
- sortDirection: 'asc',
85
- searchQuery: '',
86
- filteredData: [],
87
- totalPages: 0,
88
- };
89
-
90
- this.init();
91
- }
92
-
93
- public init(): void {
94
- this.extractData();
95
- this.createControls();
96
- this.applyStyles();
97
- this.bindEvents();
98
- this.render();
99
- this.isInitialized = true;
100
- }
101
-
102
- public destroy(): void {
103
- if (this.wrapper && this.wrapper.parentNode) {
104
- this.wrapper.parentNode.replaceChild(this.element, this.wrapper);
105
- }
106
- this.isInitialized = false;
107
- }
108
-
109
- private extractData(): void {
110
- const tbody = this.element.querySelector('tbody');
111
- if (!tbody) return;
112
-
113
- const rows = tbody.querySelectorAll('tr');
114
- this.originalData = Array.from(rows).map(row => {
115
- const cells = row.querySelectorAll('td');
116
- return Array.from(cells).map(cell => cell.textContent?.trim() || '');
117
- });
118
- this.filteredData = [...this.originalData];
119
- this.state.filteredData = this.filteredData;
120
- }
121
-
122
- private createControls(): void {
123
- const wrapper = document.createElement('div');
124
- wrapper.className = 'datatable-wrapper';
125
-
126
- // Create top controls container
127
- const topControls = document.createElement('div');
128
- topControls.className = 'datatable-top-controls';
129
-
130
- // Create search input
131
- if (this.options.searchable) {
132
- const searchWrapper = document.createElement('div');
133
- searchWrapper.className = 'datatable-search';
134
-
135
- const searchLabel = document.createElement('label');
136
- searchLabel.textContent = 'Search: ';
137
-
138
- this.searchInput = document.createElement('input');
139
- this.searchInput.type = 'text';
140
- this.searchInput.className = 'form-control';
141
- this.searchInput.placeholder = 'Search...';
142
-
143
- searchLabel.appendChild(this.searchInput);
144
- searchWrapper.appendChild(searchLabel);
145
- topControls.appendChild(searchWrapper);
146
- }
147
-
148
- // Create info display
149
- if (this.options.pagination) {
150
- this.infoElement = document.createElement('div');
151
- this.infoElement.className = 'datatable-info';
152
- topControls.appendChild(this.infoElement);
153
- }
154
-
155
- wrapper.appendChild(topControls);
156
-
157
- // Wrap the table
158
- if (this.element.parentNode) {
159
- this.element.parentNode.insertBefore(wrapper, this.element);
160
- }
161
- wrapper.appendChild(this.element);
162
-
163
- // Create pagination controls
164
- if (this.options.pagination) {
165
- this.paginationElement = document.createElement('div');
166
- this.paginationElement.className = 'datatable-pagination';
167
- wrapper.appendChild(this.paginationElement);
168
- }
169
-
170
- this.wrapper = wrapper;
171
- }
172
-
173
- private applyStyles(): void {
174
- // Apply Bootstrap-like styles
175
- const classes = ['table'];
176
- if (this.options.striped) classes.push('table-striped');
177
- if (this.options.bordered) classes.push('table-bordered');
178
- if (this.options.hover) classes.push('table-hover');
179
- if (this.options.responsive) {
180
- const responsiveWrapper = document.createElement('div');
181
- responsiveWrapper.className = 'table-responsive';
182
- if (this.element.parentNode) {
183
- this.element.parentNode.insertBefore(responsiveWrapper, this.element);
184
- responsiveWrapper.appendChild(this.element);
185
- }
186
- }
187
-
188
- this.element.className = classes.join(' ');
189
-
190
- // Add custom styles
191
- this.injectStyles();
192
- }
193
-
194
- private injectStyles(): void {
195
- const styleId = 'datatable-styles';
196
- if (document.getElementById(styleId)) return;
197
-
198
- const style = document.createElement('style');
199
- style.id = styleId;
200
- style.textContent = `
201
- .datatable-wrapper {
202
- margin: 20px 0;
203
- }
204
-
205
- .datatable-top-controls {
206
- display: flex;
207
- justify-content: space-between;
208
- align-items: center;
209
- margin-bottom: 15px;
210
- flex-wrap: wrap;
211
- gap: 10px;
212
- }
213
-
214
- .datatable-search {
215
- display: flex;
216
- align-items: center;
217
- gap: 8px;
218
- }
219
-
220
- .datatable-search label {
221
- margin: 0;
222
- font-weight: 500;
223
- }
224
-
225
- .datatable-search input {
226
- width: 250px;
227
- padding: 6px 12px;
228
- border: 1px solid var(--c-border, #dee2e6);
229
- border-radius: 4px;
230
- font-size: 14px;
231
- }
232
-
233
- .datatable-info {
234
- color: var(--c-text-muted, #6c757d);
235
- font-size: 14px;
236
- margin: 0;
237
- }
238
-
239
- .datatable-pagination {
240
- margin-top: 15px;
241
- display: flex;
242
- justify-content: center;
243
- align-items: center;
244
- gap: 4px;
245
- flex-wrap: wrap;
246
- }
247
-
248
- .datatable-pagination button {
249
- background: var(--c-bkg-card, #fff);
250
- border: 1px solid var(--c-border, #dee2e6);
251
- color: var(--c-text-base, #333);
252
- padding: 8px 12px;
253
- cursor: pointer;
254
- border-radius: 4px;
255
- font-size: 14px;
256
- transition: all 0.2s ease;
257
- min-width: 40px;
258
- }
259
-
260
- .datatable-pagination button:hover:not(:disabled) {
261
- background: var(--c-primary, #007bff);
262
- border-color: var(--c-primary, #007bff);
263
- color: white;
264
- }
265
-
266
- .datatable-pagination button.active {
267
- background: var(--c-primary, #007bff);
268
- border-color: var(--c-primary, #007bff);
269
- color: white;
270
- }
271
-
272
- .datatable-pagination button:disabled {
273
- opacity: 0.6;
274
- cursor: not-allowed;
275
- background: var(--c-bkg-muted, #f8f9fa);
276
- }
277
-
278
- .datatable-sort {
279
- cursor: pointer;
280
- user-select: none;
281
- position: relative;
282
- padding-right: 20px !important;
283
- transition: background-color 0.2s ease;
284
- }
285
-
286
- .datatable-sort:hover {
287
- background: var(--c-bkg-hover, #f8f9fa);
288
- }
289
-
290
- .datatable-sort::after {
291
- content: '↕';
292
- position: absolute;
293
- right: 8px;
294
- top: 50%;
295
- transform: translateY(-50%);
296
- opacity: 0.5;
297
- font-size: 12px;
298
- }
299
-
300
- .datatable-sort.asc::after {
301
- content: '↑';
302
- opacity: 1;
303
- color: var(--c-primary, #007bff);
304
- }
305
-
306
- .datatable-sort.desc::after {
307
- content: '↓';
308
- opacity: 1;
309
- color: var(--c-primary, #007bff);
310
- }
311
-
312
- .datatable-no-results {
313
- text-align: center;
314
- color: var(--c-text-muted, #6c757d);
315
- font-style: italic;
316
- padding: 20px;
317
- }
318
-
319
- @media (max-width: 768px) {
320
- .datatable-top-controls {
321
- flex-direction: column;
322
- align-items: stretch;
323
- }
324
-
325
- .datatable-search input {
326
- width: 100%;
327
- }
328
-
329
- .datatable-pagination {
330
- justify-content: center;
331
- }
332
-
333
- .datatable-pagination button {
334
- padding: 6px 10px;
335
- font-size: 13px;
336
- }
337
- }
338
- `;
339
- document.head.appendChild(style);
340
- }
341
-
342
- private bindEvents(): void {
343
- // Search functionality
344
- if (this.options.searchable && this.searchInput) {
345
- this.searchInput.addEventListener('input', (e) => {
346
- const target = e.target as HTMLInputElement;
347
- this.search(target.value);
348
- });
349
- }
350
-
351
- // Sorting functionality
352
- if (this.options.sortable) {
353
- const headers = this.element.querySelectorAll<HTMLTableCellElement>('thead th');
354
- headers.forEach((header, index) => {
355
- header.classList.add('datatable-sort');
356
- header.addEventListener('click', () => {
357
- this.sort(index);
358
- });
359
- header.setAttribute('tabindex', '0');
360
- header.setAttribute('role', 'button');
361
- header.setAttribute('aria-label', `Sort by ${header.textContent}`);
362
- });
363
- }
364
- }
365
-
366
- public search(query: string): void {
367
- this.state.searchQuery = query;
368
-
369
- if (!query.trim()) {
370
- this.filteredData = [...this.originalData];
371
- } else {
372
- const searchTerm = query.toLowerCase().trim();
373
- this.filteredData = this.originalData.filter(row =>
374
- row.some(cell =>
375
- cell.toString().toLowerCase().includes(searchTerm)
376
- )
377
- );
378
- }
379
-
380
- this.state.filteredData = this.filteredData;
381
- this.state.currentPage = 1;
382
- this.render();
383
- }
384
-
385
- public sort(columnIndex: number): void {
386
- if (this.state.sortColumn === columnIndex) {
387
- this.state.sortDirection = this.state.sortDirection === 'asc' ? 'desc' : 'asc';
388
- } else {
389
- this.state.sortColumn = columnIndex;
390
- this.state.sortDirection = 'asc';
391
- }
392
-
393
- this.filteredData.sort((a, b) => {
394
- const aVal = a[columnIndex];
395
- const bVal = b[columnIndex];
396
-
397
- // Try to parse as numbers
398
- const aNum = parseFloat(aVal);
399
- const bNum = parseFloat(bVal);
400
-
401
- let comparison = 0;
402
- if (!isNaN(aNum) && !isNaN(bNum)) {
403
- comparison = aNum - bNum;
404
- } else {
405
- // Try to parse as dates
406
- const aDate = new Date(aVal);
407
- const bDate = new Date(bVal);
408
-
409
- if (aDate.getTime() && bDate.getTime()) {
410
- comparison = aDate.getTime() - bDate.getTime();
411
- } else {
412
- comparison = aVal.toString().localeCompare(bVal.toString());
413
- }
414
- }
415
-
416
- return this.state.sortDirection === 'asc' ? comparison : -comparison;
417
- });
418
-
419
- this.updateSortHeaders();
420
- this.render();
421
- }
422
-
423
- private updateSortHeaders(): void {
424
- const headers = this.element.querySelectorAll<HTMLTableCellElement>('thead th');
425
- headers.forEach((header, index) => {
426
- header.classList.remove('asc', 'desc');
427
- if (index === this.state.sortColumn) {
428
- header.classList.add(this.state.sortDirection);
429
- }
430
- });
431
- }
432
-
433
- public render(): void {
434
- const tbody = this.element.querySelector('tbody');
435
- if (!tbody) return;
436
-
437
- const startIndex = (this.state.currentPage - 1) * this.options.pageSize!;
438
- const endIndex = startIndex + this.options.pageSize!;
439
- const pageData = this.filteredData.slice(startIndex, endIndex);
440
-
441
- // Clear tbody
442
- tbody.innerHTML = '';
443
-
444
- if (pageData.length === 0) {
445
- // Show no results message
446
- const noResultsRow = document.createElement('tr');
447
- const noResultsCell = document.createElement('td');
448
- noResultsCell.colSpan = this.getColumnCount();
449
- noResultsCell.className = 'datatable-no-results';
450
- noResultsCell.textContent = this.state.searchQuery ?
451
- 'No matching records found' : 'No data available';
452
- noResultsRow.appendChild(noResultsCell);
453
- tbody.appendChild(noResultsRow);
454
- } else {
455
- // Add rows
456
- pageData.forEach((rowData, rowIndex) => {
457
- const row = document.createElement('tr');
458
- rowData.forEach((cellData, colIndex) => {
459
- const cell = document.createElement('td');
460
- cell.textContent = cellData.toString();
461
- row.appendChild(cell);
462
- });
463
- tbody.appendChild(row);
464
- });
465
- }
466
-
467
- // Update pagination
468
- if (this.options.pagination) {
469
- this.updatePagination();
470
- }
471
-
472
- // Update info
473
- this.updateInfo();
474
- }
475
-
476
- private getColumnCount(): number {
477
- const headerRow = this.element.querySelector('thead tr');
478
- return headerRow ? headerRow.querySelectorAll('th').length : 0;
479
- }
480
-
481
- private updatePagination(): void {
482
- if (!this.paginationElement) return;
483
-
484
- this.state.totalPages = Math.ceil(this.filteredData.length / this.options.pageSize!);
485
- this.paginationElement.innerHTML = '';
486
-
487
- if (this.state.totalPages <= 1) return;
488
-
489
- // Previous button
490
- const prevBtn = this.createPaginationButton('Previous', () => {
491
- if (this.state.currentPage > 1) {
492
- this.state.currentPage--;
493
- this.render();
494
- }
495
- });
496
- prevBtn.disabled = this.state.currentPage === 1;
497
- this.paginationElement.appendChild(prevBtn);
498
-
499
- // Calculate page range to show
500
- const maxButtons = 5;
501
- let startPage = Math.max(1, this.state.currentPage - Math.floor(maxButtons / 2));
502
- let endPage = Math.min(this.state.totalPages, startPage + maxButtons - 1);
503
-
504
- // Adjust if we're at the end
505
- if (endPage - startPage + 1 < maxButtons) {
506
- startPage = Math.max(1, endPage - maxButtons + 1);
507
- }
508
-
509
- // First page if not in range
510
- if (startPage > 1) {
511
- const firstBtn = this.createPaginationButton('1', () => {
512
- this.state.currentPage = 1;
513
- this.render();
514
- });
515
- this.paginationElement.appendChild(firstBtn);
516
-
517
- if (startPage > 2) {
518
- const ellipsis = document.createElement('span');
519
- ellipsis.textContent = '...';
520
- ellipsis.className = 'pagination-ellipsis';
521
- this.paginationElement.appendChild(ellipsis);
522
- }
523
- }
524
-
525
- // Page numbers
526
- for (let i = startPage; i <= endPage; i++) {
527
- const pageBtn = this.createPaginationButton(i.toString(), () => {
528
- this.state.currentPage = i;
529
- this.render();
530
- });
531
- pageBtn.classList.toggle('active', i === this.state.currentPage);
532
- this.paginationElement.appendChild(pageBtn);
533
- }
534
-
535
- // Last page if not in range
536
- if (endPage < this.state.totalPages) {
537
- if (endPage < this.state.totalPages - 1) {
538
- const ellipsis = document.createElement('span');
539
- ellipsis.textContent = '...';
540
- ellipsis.className = 'pagination-ellipsis';
541
- this.paginationElement.appendChild(ellipsis);
542
- }
543
-
544
- const lastBtn = this.createPaginationButton(this.state.totalPages.toString(), () => {
545
- this.state.currentPage = this.state.totalPages;
546
- this.render();
547
- });
548
- this.paginationElement.appendChild(lastBtn);
549
- }
550
-
551
- // Next button
552
- const nextBtn = this.createPaginationButton('Next', () => {
553
- if (this.state.currentPage < this.state.totalPages) {
554
- this.state.currentPage++;
555
- this.render();
556
- }
557
- });
558
- nextBtn.disabled = this.state.currentPage === this.state.totalPages;
559
- this.paginationElement.appendChild(nextBtn);
560
- }
561
-
562
- private createPaginationButton(text: string, onClick: () => void): HTMLButtonElement {
563
- const button = document.createElement('button');
564
- button.textContent = text;
565
- button.addEventListener('click', onClick);
566
- return button;
567
- }
568
-
569
- private updateInfo(): void {
570
- if (!this.infoElement) return;
571
-
572
- const startIndex = (this.state.currentPage - 1) * this.options.pageSize! + 1;
573
- const endIndex = Math.min(startIndex + this.options.pageSize! - 1, this.filteredData.length);
574
- const total = this.filteredData.length;
575
- const originalTotal = this.originalData.length;
576
-
577
- if (total === 0) {
578
- this.infoElement.textContent = 'No entries to show';
579
- } else if (total === originalTotal) {
580
- this.infoElement.textContent = `Showing ${startIndex} to ${endIndex} of ${total} entries`;
581
- } else {
582
- this.infoElement.textContent = `Showing ${startIndex} to ${endIndex} of ${total} entries (filtered from ${originalTotal} total entries)`;
583
- }
584
- }
585
-
586
- // Public API methods
587
- public goToPage(page: number): void {
588
- if (page >= 1 && page <= this.state.totalPages) {
589
- this.state.currentPage = page;
590
- this.render();
591
- }
592
- }
593
-
594
- public setPageSize(size: number): void {
595
- this.options.pageSize = size;
596
- this.state.currentPage = 1;
597
- this.render();
598
- }
599
-
600
- public getState(): Readonly<DataTableState> {
601
- return { ...this.state };
602
- }
603
-
604
- public refresh(): void {
605
- this.extractData();
606
- this.state.currentPage = 1;
607
- this.render();
608
- }
609
-
610
- public clear(): void {
611
- this.originalData = [];
612
- this.filteredData = [];
613
- this.state.currentPage = 1;
614
- this.render();
615
- }
616
- }
617
-
618
- // DataTable Manager
619
- export class DataTableManager {
620
- private instances: Map<string, VanillaDataTable> = new Map();
621
-
622
- public initialize(selector: string = '#dataTable', options: DataTableOptions = {}): VanillaDataTable | null {
623
- const element = document.querySelector<HTMLTableElement>(selector);
624
- if (!element) {
625
- // Silently return null if element doesn't exist (normal for pages without tables)
626
- return null;
627
- }
628
-
629
- // Clean up existing instance
630
- if (element.dataTableInstance) {
631
- element.dataTableInstance.destroy();
632
- }
633
-
634
- // Create new instance
635
- const dataTable = new VanillaDataTable(element, options);
636
- element.dataTableInstance = dataTable;
637
-
638
- // Store in manager
639
- this.instances.set(selector, dataTable);
640
-
641
- return dataTable;
642
- }
643
-
644
- public getInstance(selector: string): VanillaDataTable | undefined {
645
- return this.instances.get(selector);
646
- }
647
-
648
- public destroyInstance(selector: string): void {
649
- const instance = this.instances.get(selector);
650
- if (instance) {
651
- instance.destroy();
652
- this.instances.delete(selector);
653
- }
654
- }
655
-
656
- public destroyAll(): void {
657
- this.instances.forEach((instance, selector) => {
658
- instance.destroy();
659
- });
660
- this.instances.clear();
661
- }
662
- }
663
-
664
- // Create singleton manager
665
- const dataTableManager = new DataTableManager();
666
-
667
- // Initialize DataTable
668
- const initializeDataTable = (): void => {
669
- // Only initialize if the table exists
670
- if (document.querySelector('#dataTable')) {
671
- dataTableManager.initialize('#dataTable', {
672
- sortable: true,
673
- searchable: true,
674
- pagination: true,
675
- pageSize: 10,
676
- responsive: true,
677
- striped: true,
678
- bordered: true,
679
- hover: true,
680
- });
681
- }
682
- };
683
-
684
- // Initialize on load
685
- if (document.readyState === 'loading') {
686
- document.addEventListener('DOMContentLoaded', initializeDataTable);
687
- } else {
688
- initializeDataTable();
689
- }
690
-
691
- // Reinitialize on theme change
692
- window.addEventListener('adminator:themeChanged', () => {
693
- setTimeout(initializeDataTable, 100);
694
- });
695
-
696
- // Cleanup on page unload
697
- window.addEventListener('beforeunload', () => {
698
- dataTableManager.destroyAll();
699
- });
700
-
701
- // Export default for compatibility
702
- export default {
703
- init: initializeDataTable,
704
- manager: dataTableManager,
705
- VanillaDataTable,
706
- DataTableManager,
707
- };