adminator-admin-dashboard 2.8.0 → 2.9.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.
@@ -0,0 +1,645 @@
1
+ /**
2
+ * Modern Adminator Application
3
+ * Main application entry point with enhanced mobile support
4
+ */
5
+
6
+ // Note: Bootstrap 5 CSS is still available via SCSS imports
7
+ // Bootstrap JS components removed to eliminate jQuery dependency
8
+ import { DOM } from './utils/dom';
9
+ import DateUtils from './utils/date';
10
+ import { ThemeManager } from './utils/theme';
11
+ import { Sidebar } from './components/Sidebar';
12
+ import { ChartComponent } from './components/Chart';
13
+
14
+ // Import styles
15
+ import '../styles/index.scss';
16
+
17
+ // Import other modules that don't need immediate modernization
18
+ import './fullcalendar';
19
+ import './masonry';
20
+ import './popover';
21
+ import './scrollbar';
22
+ import './search';
23
+ import './skycons';
24
+ import './vectorMaps';
25
+ import './chat';
26
+ import './email';
27
+ import './googleMaps';
28
+ import './ui';
29
+
30
+ class AdminatorApp {
31
+ constructor() {
32
+ this.components = new Map();
33
+ this.isInitialized = false;
34
+ this.themeManager = ThemeManager;
35
+
36
+ // Initialize when DOM is ready
37
+ DOM.ready(() => {
38
+ this.init();
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Initialize the application
44
+ */
45
+ init() {
46
+ if (this.isInitialized) return;
47
+
48
+
49
+ try {
50
+ // Initialize core components
51
+ this.initSidebar();
52
+ this.initCharts();
53
+ this.initDataTables();
54
+ this.initDatePickers();
55
+ this.initTheme();
56
+ this.initMobileEnhancements();
57
+
58
+ // Setup global event listeners
59
+ this.setupGlobalEvents();
60
+
61
+ this.isInitialized = true;
62
+
63
+ // Dispatch custom event for other scripts
64
+ window.dispatchEvent(new CustomEvent('adminator:ready', {
65
+ detail: { app: this },
66
+ }));
67
+
68
+ } catch {
69
+ // Error initializing Adminator App
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Initialize Sidebar component
75
+ */
76
+ initSidebar() {
77
+ if (DOM.exists('.sidebar')) {
78
+ const sidebar = new Sidebar();
79
+ this.components.set('sidebar', sidebar);
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Initialize Chart components
85
+ */
86
+ initCharts() {
87
+ // Check if we have any chart elements
88
+ const hasCharts = DOM.exists('#sparklinedash') ||
89
+ DOM.exists('.sparkline') ||
90
+ DOM.exists('.sparkbar') ||
91
+ DOM.exists('.sparktri') ||
92
+ DOM.exists('.sparkdisc') ||
93
+ DOM.exists('.sparkbull') ||
94
+ DOM.exists('.sparkbox') ||
95
+ DOM.exists('.easy-pie-chart') ||
96
+ DOM.exists('#line-chart') ||
97
+ DOM.exists('#area-chart') ||
98
+ DOM.exists('#scatter-chart') ||
99
+ DOM.exists('#bar-chart');
100
+
101
+ if (hasCharts) {
102
+ const charts = new ChartComponent();
103
+ this.components.set('charts', charts);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Initialize DataTables (modern approach)
109
+ */
110
+ initDataTables() {
111
+ const dataTableElement = DOM.select('#dataTable');
112
+ if (dataTableElement) {
113
+ // For now, use a lightweight approach
114
+ // In future iterations, we can replace with a modern table library
115
+ this.initBasicDataTable(dataTableElement);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Basic DataTable implementation (placeholder for full modernization)
121
+ */
122
+ initBasicDataTable(table) {
123
+ // Add basic sorting functionality
124
+ const headers = DOM.selectAll('th', table);
125
+
126
+ headers.forEach(header => {
127
+ if (header.textContent.trim()) {
128
+ header.style.cursor = 'pointer';
129
+ header.style.userSelect = 'none';
130
+
131
+ DOM.on(header, 'click', () => {
132
+ // Basic sort functionality can be added here
133
+ // For now, we'll keep the existing DataTables library
134
+ });
135
+ }
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Initialize Date Pickers (modern approach with Day.js)
141
+ */
142
+ initDatePickers() {
143
+ const startDatePickers = DOM.selectAll('.start-date');
144
+ const endDatePickers = DOM.selectAll('.end-date');
145
+
146
+ [...startDatePickers, ...endDatePickers].forEach(picker => {
147
+ // Convert to HTML5 date input for better UX
148
+ if (picker.type !== 'date') {
149
+ picker.type = 'date';
150
+ picker.classList.add('form-control');
151
+
152
+ // Clear the placeholder since HTML5 date inputs don't need it
153
+ picker.removeAttribute('placeholder');
154
+
155
+ // Set default value to today if no value is set
156
+ if (!picker.value) {
157
+ picker.value = DateUtils.form.toInputValue(DateUtils.now());
158
+ }
159
+
160
+ // Make sure the input is clickable and focusable
161
+ picker.style.pointerEvents = 'auto';
162
+ picker.style.cursor = 'pointer';
163
+
164
+ // Ensure proper styling for HTML5 date input
165
+ picker.style.minHeight = '38px';
166
+ picker.style.lineHeight = '1.5';
167
+
168
+ // Date picker converted to HTML5 with Day.js support
169
+ }
170
+ });
171
+
172
+ // Add enhanced interaction - handle both field and icon clicks
173
+ [...startDatePickers, ...endDatePickers].forEach(picker => {
174
+ // Handle direct field clicks
175
+ DOM.on(picker, 'click', (event) => {
176
+ event.target.focus();
177
+ // For mobile browsers, trigger the date picker
178
+ if (event.target.showPicker && typeof event.target.showPicker === 'function') {
179
+ try {
180
+ event.target.showPicker();
181
+ } catch {
182
+ // Fallback if showPicker is not supported
183
+ }
184
+ }
185
+ });
186
+
187
+ // Handle calendar icon clicks (find the icon in the input group)
188
+ const inputGroup = picker.closest('.input-group');
189
+ if (inputGroup) {
190
+ const calendarIcon = inputGroup.querySelector('.input-group-text i.ti-calendar');
191
+ if (calendarIcon) {
192
+ DOM.on(calendarIcon, 'click', (event) => {
193
+ event.preventDefault();
194
+ event.stopPropagation();
195
+ picker.focus();
196
+ if (picker.showPicker && typeof picker.showPicker === 'function') {
197
+ try {
198
+ picker.showPicker();
199
+ } catch {
200
+ // Date picker opened via icon click
201
+ }
202
+ }
203
+ });
204
+ }
205
+ }
206
+ });
207
+ }
208
+
209
+ /**
210
+ * Initialize theme system with toggle
211
+ */
212
+ initTheme() {
213
+ // Initializing theme system
214
+
215
+ // Initialize theme system first
216
+ this.themeManager.init();
217
+
218
+ // Inject theme toggle if missing - with retry mechanism
219
+ setTimeout(() => {
220
+ const navRight = DOM.select('.nav-right');
221
+ // Check for nav-right and theme-toggle existence
222
+
223
+ if (navRight && !DOM.exists('#theme-toggle')) {
224
+ const li = document.createElement('li');
225
+ li.className = 'theme-toggle d-flex ai-c';
226
+ li.innerHTML = `
227
+ <div class="form-check form-switch d-flex ai-c" style="margin: 0; padding: 0;">
228
+ <label class="form-check-label me-2 text-nowrap c-grey-700" for="theme-toggle" style="font-size: 12px; margin-right: 8px;">
229
+ <i class="ti-sun" style="margin-right: 4px;"></i><span class="theme-label">Light</span>
230
+ </label>
231
+ <input class="form-check-input" type="checkbox" id="theme-toggle" style="margin: 0;">
232
+ <label class="form-check-label ms-2 text-nowrap c-grey-700" for="theme-toggle" style="font-size: 12px; margin-left: 8px;">
233
+ <span class="theme-label">Dark</span><i class="ti-moon" style="margin-left: 4px;"></i>
234
+ </label>
235
+ </div>
236
+ `;
237
+
238
+ // Insert before user dropdown (last item) - safer approach
239
+ const lastItem = navRight.querySelector('li:last-child');
240
+
241
+ if (lastItem && lastItem.parentNode === navRight) {
242
+ navRight.insertBefore(li, lastItem);
243
+ // Theme toggle inserted before last item
244
+ } else {
245
+ navRight.appendChild(li);
246
+ // Theme toggle appended to nav-right (safer approach)
247
+ }
248
+
249
+ // Add toggle functionality
250
+ const toggle = DOM.select('#theme-toggle');
251
+ if (toggle) {
252
+ // Set initial state
253
+ const currentTheme = this.themeManager.current();
254
+ toggle.checked = currentTheme === 'dark';
255
+
256
+ DOM.on(toggle, 'change', () => {
257
+ this.themeManager.apply(toggle.checked ? 'dark' : 'light');
258
+ });
259
+
260
+ // Listen for theme changes from other sources
261
+ window.addEventListener('adminator:themeChanged', (event) => {
262
+ toggle.checked = event.detail.theme === 'dark';
263
+
264
+ // Update charts when theme changes
265
+ const charts = this.components.get('charts');
266
+ if (charts) charts.redrawCharts();
267
+ });
268
+ }
269
+ } else {
270
+ // No nav-right found or theme-toggle already exists
271
+ }
272
+ }, 100); // Wait 100ms for DOM to be fully ready
273
+ }
274
+
275
+ /**
276
+ * Initialize mobile-specific enhancements
277
+ */
278
+ initMobileEnhancements() {
279
+ // Initializing mobile enhancements
280
+ this.enhanceMobileDropdowns();
281
+ this.enhanceMobileSearch();
282
+
283
+ // Prevent horizontal scroll on mobile
284
+ if (this.isMobile()) {
285
+ document.body.style.overflowX = 'hidden';
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Setup global event listeners
291
+ */
292
+ setupGlobalEvents() {
293
+ // Global click handler
294
+ DOM.on(document, 'click', (event) => this.handleGlobalClick(event));
295
+
296
+ // Window resize handler with debouncing
297
+ let resizeTimeout;
298
+ DOM.on(window, 'resize', () => {
299
+ clearTimeout(resizeTimeout);
300
+ resizeTimeout = setTimeout(() => this.handleResize(), 250);
301
+ });
302
+
303
+ // Global event listeners set up
304
+ }
305
+
306
+ /**
307
+ * Handle window resize events
308
+ */
309
+ handleResize() {
310
+ // Window resized, updating mobile features
311
+
312
+ // Close all mobile-specific overlays when switching to desktop
313
+ if (!this.isMobile()) {
314
+ document.body.style.overflow = '';
315
+ document.body.style.overflowX = '';
316
+
317
+ // Close dropdowns
318
+ const dropdowns = DOM.selectAll('.nav-right .dropdown');
319
+ dropdowns.forEach(dropdown => {
320
+ dropdown.classList.remove('show');
321
+ const menu = dropdown.querySelector('.dropdown-menu');
322
+ if (menu) menu.classList.remove('show');
323
+ });
324
+
325
+ // Close search
326
+ const searchBox = DOM.select('.search-box');
327
+ const searchInput = DOM.select('.search-input');
328
+ if (searchBox && searchInput) {
329
+ searchBox.classList.remove('active');
330
+ searchInput.classList.remove('active');
331
+ }
332
+ } else {
333
+ // Re-enable mobile overflow protection
334
+ document.body.style.overflowX = 'hidden';
335
+ }
336
+
337
+ // Re-apply mobile enhancements
338
+ this.enhanceMobileDropdowns();
339
+ this.enhanceMobileSearch();
340
+ }
341
+
342
+ /**
343
+ * Handle global click events
344
+ */
345
+ handleGlobalClick(event) {
346
+ // Close mobile dropdowns when clicking outside
347
+ if (!event.target.closest('.dropdown')) {
348
+ const dropdowns = DOM.selectAll('.nav-right .dropdown');
349
+ dropdowns.forEach(dropdown => {
350
+ dropdown.classList.remove('show');
351
+ const menu = dropdown.querySelector('.dropdown-menu');
352
+ if (menu) menu.classList.remove('show');
353
+ });
354
+ document.body.style.overflow = '';
355
+ }
356
+
357
+ // Close search when clicking outside
358
+ if (!event.target.closest('.search-box') && !event.target.closest('.search-input')) {
359
+ const searchBox = DOM.select('.search-box');
360
+ const searchInput = DOM.select('.search-input');
361
+ if (searchBox && searchInput) {
362
+ searchBox.classList.remove('active');
363
+ searchInput.classList.remove('active');
364
+ document.body.style.overflow = '';
365
+ document.body.classList.remove('mobile-menu-open');
366
+ }
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Check if we're on a mobile device
372
+ */
373
+ isMobile() {
374
+ return window.innerWidth <= 768;
375
+ }
376
+
377
+ /**
378
+ * Enhanced mobile dropdown handling with improved email layout
379
+ */
380
+ enhanceMobileDropdowns() {
381
+ if (!this.isMobile()) return;
382
+
383
+ const dropdowns = DOM.selectAll('.nav-right .dropdown');
384
+
385
+ dropdowns.forEach(dropdown => {
386
+ const toggle = dropdown.querySelector('.dropdown-toggle');
387
+ const menu = dropdown.querySelector('.dropdown-menu');
388
+
389
+ if (toggle && menu) {
390
+ // Remove existing listeners to prevent duplicates
391
+ const newToggle = toggle.cloneNode(true);
392
+ toggle.replaceWith(newToggle);
393
+
394
+ // Add click functionality for mobile dropdowns
395
+ DOM.on(newToggle, 'click', (e) => {
396
+ e.preventDefault();
397
+ e.stopPropagation();
398
+
399
+ // Close search if open
400
+ const searchBox = DOM.select('.search-box');
401
+ const searchInput = DOM.select('.search-input');
402
+ if (searchBox && searchInput) {
403
+ searchBox.classList.remove('active');
404
+ searchInput.classList.remove('active');
405
+ }
406
+
407
+ // Close other dropdowns first
408
+ dropdowns.forEach(otherDropdown => {
409
+ if (otherDropdown !== dropdown) {
410
+ otherDropdown.classList.remove('show');
411
+ const otherMenu = otherDropdown.querySelector('.dropdown-menu');
412
+ if (otherMenu) otherMenu.classList.remove('show');
413
+ }
414
+ });
415
+
416
+ // Toggle current dropdown
417
+ const isOpen = dropdown.classList.contains('show');
418
+ if (isOpen) {
419
+ dropdown.classList.remove('show');
420
+ menu.classList.remove('show');
421
+ document.body.style.overflow = '';
422
+ document.body.classList.remove('mobile-menu-open');
423
+ } else {
424
+ dropdown.classList.add('show');
425
+ menu.classList.add('show');
426
+ document.body.style.overflow = 'hidden';
427
+ document.body.classList.add('mobile-menu-open');
428
+ }
429
+ });
430
+
431
+ // Enhanced mobile close button functionality
432
+ DOM.on(menu, 'click', (e) => {
433
+ // Check if clicked on the close area (::before pseudo-element area)
434
+ const rect = menu.getBoundingClientRect();
435
+ const clickY = e.clientY - rect.top;
436
+
437
+ // If clicked in top 50px (close button area)
438
+ if (clickY <= 50) {
439
+ dropdown.classList.remove('show');
440
+ menu.classList.remove('show');
441
+ document.body.style.overflow = '';
442
+ document.body.classList.remove('mobile-menu-open');
443
+ e.preventDefault();
444
+ e.stopPropagation();
445
+ }
446
+ });
447
+ }
448
+ });
449
+
450
+ // Close dropdowns on escape key
451
+ DOM.on(document, 'keydown', (e) => {
452
+ if (e.key === 'Escape') {
453
+ dropdowns.forEach(dropdown => {
454
+ dropdown.classList.remove('show');
455
+ const menu = dropdown.querySelector('.dropdown-menu');
456
+ if (menu) menu.classList.remove('show');
457
+ });
458
+ document.body.style.overflow = '';
459
+ document.body.classList.remove('mobile-menu-open');
460
+ }
461
+ });
462
+ }
463
+
464
+ /**
465
+ * Enhanced mobile search handling - Full-width search bar
466
+ */
467
+ enhanceMobileSearch() {
468
+ const searchBox = DOM.select('.search-box');
469
+ const searchInput = DOM.select('.search-input');
470
+
471
+ if (searchBox && searchInput) {
472
+ const searchToggle = searchBox.querySelector('a');
473
+ const searchField = searchInput.querySelector('input');
474
+
475
+ if (searchToggle && searchField) {
476
+ // Setting up full-width search functionality
477
+
478
+ // Remove existing listeners to prevent duplication
479
+ const newSearchToggle = searchToggle.cloneNode(true);
480
+ searchToggle.replaceWith(newSearchToggle);
481
+
482
+ DOM.on(newSearchToggle, 'click', (e) => {
483
+ e.preventDefault();
484
+ e.stopPropagation();
485
+
486
+ // Full-width search toggle clicked
487
+
488
+ // Close any open dropdowns first
489
+ const dropdowns = DOM.selectAll('.nav-right .dropdown');
490
+ dropdowns.forEach(dropdown => {
491
+ dropdown.classList.remove('show');
492
+ const menu = dropdown.querySelector('.dropdown-menu');
493
+ if (menu) menu.classList.remove('show');
494
+ });
495
+
496
+ // Toggle search state
497
+ const isActive = searchInput.classList.contains('active');
498
+ const searchIcon = newSearchToggle.querySelector('i');
499
+
500
+ if (isActive) {
501
+ // Close search
502
+ searchInput.classList.remove('active');
503
+ document.body.classList.remove('search-open');
504
+
505
+ // Change icon back to search
506
+ if (searchIcon) {
507
+ searchIcon.className = 'ti-search';
508
+ }
509
+
510
+ // Clear input
511
+ if (searchField) {
512
+ searchField.value = '';
513
+ searchField.blur();
514
+ }
515
+
516
+ // Full-width search closed
517
+ } else {
518
+ // Open search
519
+ searchInput.classList.add('active');
520
+ document.body.classList.add('search-open');
521
+
522
+ // Change icon to close
523
+ if (searchIcon) {
524
+ searchIcon.className = 'ti-close';
525
+ }
526
+
527
+ // Focus the input after a short delay
528
+ setTimeout(() => {
529
+ if (searchField) {
530
+ searchField.focus();
531
+ // Search field focused
532
+ }
533
+ }, 100);
534
+
535
+ // Full-width search opened
536
+ }
537
+ });
538
+
539
+ // Close search on escape
540
+ DOM.on(document, 'keydown', (e) => {
541
+ if (e.key === 'Escape' && searchInput.classList.contains('active')) {
542
+ searchInput.classList.remove('active');
543
+ document.body.classList.remove('search-open');
544
+
545
+ // Reset icon
546
+ const searchIcon = newSearchToggle.querySelector('i');
547
+ if (searchIcon) {
548
+ searchIcon.className = 'ti-search';
549
+ }
550
+
551
+ // Clear input
552
+ if (searchField) {
553
+ searchField.value = '';
554
+ searchField.blur();
555
+ }
556
+
557
+ // Full-width search closed via escape
558
+ }
559
+ });
560
+
561
+ // Handle search input
562
+ DOM.on(searchField, 'keypress', (e) => {
563
+ if (e.key === 'Enter') {
564
+ e.preventDefault();
565
+ const query = searchField.value.trim();
566
+ if (query) {
567
+ // Search query submitted
568
+ // Implement your search logic here
569
+
570
+ // For demo, close search after "searching"
571
+ searchInput.classList.remove('active');
572
+ document.body.classList.remove('search-open');
573
+
574
+ const searchIcon = newSearchToggle.querySelector('i');
575
+ if (searchIcon) {
576
+ searchIcon.className = 'ti-search';
577
+ }
578
+
579
+ searchField.value = '';
580
+ searchField.blur();
581
+ }
582
+ }
583
+ });
584
+
585
+ // Full-width search functionality initialized
586
+ }
587
+ }
588
+ }
589
+
590
+ /**
591
+ * Get a component by name
592
+ */
593
+ getComponent(name) {
594
+ return this.components.get(name);
595
+ }
596
+
597
+ /**
598
+ * Check if app is ready
599
+ */
600
+ isReady() {
601
+ return this.isInitialized;
602
+ }
603
+
604
+ /**
605
+ * Destroy the application
606
+ */
607
+ destroy() {
608
+ // Destroying Adminator App
609
+
610
+ // Destroy all components
611
+ this.components.forEach((component) => {
612
+ if (typeof component.destroy === 'function') {
613
+ component.destroy();
614
+ }
615
+ // Component destroyed
616
+ });
617
+
618
+ this.components.clear();
619
+ this.isInitialized = false;
620
+ }
621
+
622
+ /**
623
+ * Refresh/reinitialize the application
624
+ */
625
+ refresh() {
626
+ // Refreshing Adminator App
627
+
628
+ if (this.isInitialized) {
629
+ this.destroy();
630
+ }
631
+
632
+ setTimeout(() => {
633
+ this.init();
634
+ }, 100);
635
+ }
636
+ }
637
+
638
+ // Initialize the application
639
+ const app = new AdminatorApp();
640
+
641
+ // Make app globally available for debugging
642
+ window.AdminatorApp = app;
643
+
644
+ // Export for module usage
645
+ export default app;