adminator-admin-dashboard 2.7.1 → 2.8.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.
- package/CHANGELOG.md +116 -0
- package/CLAUDE.md +5 -5
- package/README.md +63 -24
- package/dist/main.js +1779 -2571
- package/dist/main.js.map +1 -1
- package/package.json +28 -36
- package/src/assets/scripts/app 2.js +645 -0
- package/src/assets/scripts/app.js +3 -3
- package/src/assets/scripts/utils/theme.js +4 -2
- package/src/assets/scripts/vectorMaps/index.js +5 -5
- package/dist/55b07f26c86c8e3d3754.svg +0 -1
- package/dist/9fad440d8ee7a949a9a9.svg +0 -1
- package/dist/test.html +0 -91
- package/src/assets/scripts/app.ts +0 -757
- package/src/assets/scripts/components/Chart.ts +0 -1350
- package/src/assets/scripts/components/Sidebar.ts +0 -388
- package/src/assets/scripts/datatable/index.ts +0 -707
- package/src/assets/scripts/datepicker/index.ts +0 -699
- package/src/assets/scripts/ui/index.ts +0 -740
- package/src/assets/scripts/utils/date.ts +0 -363
- package/src/assets/scripts/utils/dom.ts +0 -513
- package/src/assets/scripts/utils/theme.ts +0 -313
- package/src/assets/scripts/vectorMaps/index.ts +0 -542
- package/src/test.html +0 -96
- package/src/types/index.ts +0 -236
- /package/dist/assets/{c1e38fd9e0e74ba58f7a2b77ef29fdd3.svg → fontawesome-webfont.svg} +0 -0
- /package/dist/assets/{f0fc8c798eac5636249c4ea287832422.svg → themify.svg} +0 -0
|
@@ -1,757 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Modern Adminator Application with TypeScript
|
|
3
|
-
* Main application entry point with enhanced mobile support and type safety
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { DOM } from './utils/dom';
|
|
7
|
-
import { ThemeManager } from './utils/theme';
|
|
8
|
-
import { Sidebar } from './components/Sidebar';
|
|
9
|
-
import { ChartComponent } from './components/Chart';
|
|
10
|
-
import UIComponents from './ui';
|
|
11
|
-
import DataTable from './datatable';
|
|
12
|
-
import DatePicker from './datepicker';
|
|
13
|
-
import VectorMaps from './vectorMaps';
|
|
14
|
-
import type { ComponentInterface } from '../../types';
|
|
15
|
-
|
|
16
|
-
// Import styles
|
|
17
|
-
import '../styles/index.scss';
|
|
18
|
-
|
|
19
|
-
// Import other modules that don't need immediate modernization
|
|
20
|
-
import './fullcalendar';
|
|
21
|
-
import './masonry';
|
|
22
|
-
import './popover';
|
|
23
|
-
import './scrollbar';
|
|
24
|
-
import './search';
|
|
25
|
-
import './skycons';
|
|
26
|
-
import './chat';
|
|
27
|
-
import './email';
|
|
28
|
-
import './googleMaps';
|
|
29
|
-
|
|
30
|
-
// Type definitions for the application
|
|
31
|
-
export interface AdminatorAppOptions {
|
|
32
|
-
autoInit?: boolean;
|
|
33
|
-
theme?: 'light' | 'dark' | 'auto';
|
|
34
|
-
mobile?: {
|
|
35
|
-
enhanced?: boolean;
|
|
36
|
-
fullWidthSearch?: boolean;
|
|
37
|
-
disableDropdowns?: boolean;
|
|
38
|
-
};
|
|
39
|
-
debug?: boolean;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface AdminatorAppState {
|
|
43
|
-
isInitialized: boolean;
|
|
44
|
-
isMobile: boolean;
|
|
45
|
-
currentTheme: 'light' | 'dark' | 'auto';
|
|
46
|
-
components: Map<string, ComponentInterface>;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface AdminatorAppEvents {
|
|
50
|
-
ready: CustomEvent<{ app: AdminatorApp }>;
|
|
51
|
-
themeChanged: CustomEvent<{ theme: string; previousTheme: string }>;
|
|
52
|
-
mobileStateChanged: CustomEvent<{ isMobile: boolean }>;
|
|
53
|
-
componentAdded: CustomEvent<{ name: string; component: ComponentInterface }>;
|
|
54
|
-
componentRemoved: CustomEvent<{ name: string }>;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
declare global {
|
|
58
|
-
interface Window {
|
|
59
|
-
AdminatorApp?: AdminatorApp;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export class AdminatorApp {
|
|
64
|
-
public options: AdminatorAppOptions;
|
|
65
|
-
public state: AdminatorAppState;
|
|
66
|
-
|
|
67
|
-
private resizeTimeout: number | null = null;
|
|
68
|
-
private eventHandlers: Map<string, EventListener> = new Map();
|
|
69
|
-
private themeManager: typeof ThemeManager;
|
|
70
|
-
|
|
71
|
-
constructor(options: AdminatorAppOptions = {}) {
|
|
72
|
-
this.options = {
|
|
73
|
-
autoInit: true,
|
|
74
|
-
theme: 'auto',
|
|
75
|
-
mobile: {
|
|
76
|
-
enhanced: true,
|
|
77
|
-
fullWidthSearch: true,
|
|
78
|
-
disableDropdowns: false,
|
|
79
|
-
},
|
|
80
|
-
debug: false,
|
|
81
|
-
...options,
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
this.themeManager = ThemeManager;
|
|
85
|
-
|
|
86
|
-
this.state = {
|
|
87
|
-
isInitialized: false,
|
|
88
|
-
isMobile: this.checkMobileState(),
|
|
89
|
-
currentTheme: 'light',
|
|
90
|
-
components: new Map(),
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
if (this.options.autoInit) {
|
|
94
|
-
// Initialize when DOM is ready
|
|
95
|
-
DOM.ready(() => {
|
|
96
|
-
this.init();
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Initialize the application
|
|
103
|
-
*/
|
|
104
|
-
public init(): void {
|
|
105
|
-
if (this.state.isInitialized) return;
|
|
106
|
-
|
|
107
|
-
this.log('Initializing Adminator App...');
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
// Initialize core components
|
|
111
|
-
this.initSidebar();
|
|
112
|
-
this.initCharts();
|
|
113
|
-
this.initDataTables();
|
|
114
|
-
this.initDatePickers();
|
|
115
|
-
this.initUIComponents();
|
|
116
|
-
this.initVectorMaps();
|
|
117
|
-
this.initTheme();
|
|
118
|
-
this.initMobileEnhancements();
|
|
119
|
-
|
|
120
|
-
// Setup global event listeners
|
|
121
|
-
this.setupGlobalEvents();
|
|
122
|
-
|
|
123
|
-
this.state.isInitialized = true;
|
|
124
|
-
this.log('Adminator App initialized successfully');
|
|
125
|
-
|
|
126
|
-
// Dispatch custom event for other scripts
|
|
127
|
-
this.dispatchEvent('ready', { app: this });
|
|
128
|
-
|
|
129
|
-
} catch (error) {
|
|
130
|
-
console.error('Error initializing Adminator App:', error);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Initialize Sidebar component
|
|
136
|
-
*/
|
|
137
|
-
private initSidebar(): void {
|
|
138
|
-
if (DOM.exists('.sidebar')) {
|
|
139
|
-
const sidebar = new Sidebar();
|
|
140
|
-
this.addComponent('sidebar', sidebar);
|
|
141
|
-
this.log('Sidebar component initialized');
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Initialize Chart components
|
|
147
|
-
*/
|
|
148
|
-
private initCharts(): void {
|
|
149
|
-
// Check if we have any chart elements
|
|
150
|
-
const hasCharts = DOM.exists('#sparklinedash') ||
|
|
151
|
-
DOM.exists('.sparkline') ||
|
|
152
|
-
DOM.exists('.sparkbar') ||
|
|
153
|
-
DOM.exists('.sparktri') ||
|
|
154
|
-
DOM.exists('.sparkdisc') ||
|
|
155
|
-
DOM.exists('.sparkbull') ||
|
|
156
|
-
DOM.exists('.sparkbox') ||
|
|
157
|
-
DOM.exists('.easy-pie-chart') ||
|
|
158
|
-
DOM.exists('#line-chart') ||
|
|
159
|
-
DOM.exists('#area-chart') ||
|
|
160
|
-
DOM.exists('#scatter-chart') ||
|
|
161
|
-
DOM.exists('#bar-chart');
|
|
162
|
-
|
|
163
|
-
if (hasCharts) {
|
|
164
|
-
const charts = new ChartComponent();
|
|
165
|
-
this.addComponent('charts', charts);
|
|
166
|
-
this.log('Chart components initialized');
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Initialize DataTables
|
|
172
|
-
*/
|
|
173
|
-
private initDataTables(): void {
|
|
174
|
-
const dataTableElement = DOM.select('#dataTable');
|
|
175
|
-
if (dataTableElement) {
|
|
176
|
-
DataTable.init();
|
|
177
|
-
this.log('DataTable initialized');
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Initialize Date Pickers
|
|
183
|
-
*/
|
|
184
|
-
private initDatePickers(): void {
|
|
185
|
-
const startDatePickers = DOM.selectAll('.start-date');
|
|
186
|
-
const endDatePickers = DOM.selectAll('.end-date');
|
|
187
|
-
|
|
188
|
-
if (startDatePickers.length > 0 || endDatePickers.length > 0) {
|
|
189
|
-
DatePicker.init();
|
|
190
|
-
this.log('Date pickers initialized');
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Initialize UI Components
|
|
196
|
-
*/
|
|
197
|
-
private initUIComponents(): void {
|
|
198
|
-
UIComponents.init();
|
|
199
|
-
this.log('UI components initialized');
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Initialize Vector Maps
|
|
204
|
-
*/
|
|
205
|
-
private initVectorMaps(): void {
|
|
206
|
-
if (DOM.exists('#world-map-marker')) {
|
|
207
|
-
VectorMaps.init();
|
|
208
|
-
this.log('Vector maps initialized');
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Initialize theme system with toggle
|
|
214
|
-
*/
|
|
215
|
-
private initTheme(): void {
|
|
216
|
-
this.log('Initializing theme system...');
|
|
217
|
-
|
|
218
|
-
// Initialize theme system first
|
|
219
|
-
this.themeManager.init();
|
|
220
|
-
this.state.currentTheme = this.themeManager.current();
|
|
221
|
-
|
|
222
|
-
// Inject theme toggle if missing
|
|
223
|
-
setTimeout(() => {
|
|
224
|
-
this.injectThemeToggle();
|
|
225
|
-
}, 100);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Inject theme toggle button
|
|
230
|
-
*/
|
|
231
|
-
private injectThemeToggle(): void {
|
|
232
|
-
const navRight = DOM.select('.nav-right');
|
|
233
|
-
|
|
234
|
-
if (navRight && !DOM.exists('#theme-toggle')) {
|
|
235
|
-
const li = document.createElement('li');
|
|
236
|
-
li.className = 'theme-toggle d-flex ai-c';
|
|
237
|
-
li.innerHTML = `
|
|
238
|
-
<div class="form-check form-switch d-flex ai-c" style="margin: 0; padding: 0;">
|
|
239
|
-
<label class="form-check-label me-2 text-nowrap c-grey-700" for="theme-toggle" style="font-size: 12px; margin-right: 8px;">
|
|
240
|
-
<i class="ti-sun" style="margin-right: 4px;"></i><span class="theme-label">Light</span>
|
|
241
|
-
</label>
|
|
242
|
-
<input class="form-check-input" type="checkbox" id="theme-toggle" style="margin: 0;">
|
|
243
|
-
<label class="form-check-label ms-2 text-nowrap c-grey-700" for="theme-toggle" style="font-size: 12px; margin-left: 8px;">
|
|
244
|
-
<span class="theme-label">Dark</span><i class="ti-moon" style="margin-left: 4px;"></i>
|
|
245
|
-
</label>
|
|
246
|
-
</div>
|
|
247
|
-
`;
|
|
248
|
-
|
|
249
|
-
// Insert before user dropdown (last item)
|
|
250
|
-
const lastItem = navRight.querySelector('li:last-child');
|
|
251
|
-
if (lastItem && lastItem.parentNode === navRight) {
|
|
252
|
-
navRight.insertBefore(li, lastItem);
|
|
253
|
-
} else {
|
|
254
|
-
navRight.appendChild(li);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
this.setupThemeToggle();
|
|
258
|
-
this.log('Theme toggle injected');
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Setup theme toggle functionality
|
|
264
|
-
*/
|
|
265
|
-
private setupThemeToggle(): void {
|
|
266
|
-
const toggle = DOM.select('#theme-toggle') as HTMLInputElement;
|
|
267
|
-
if (!toggle) return;
|
|
268
|
-
|
|
269
|
-
// Set initial state
|
|
270
|
-
toggle.checked = this.state.currentTheme === 'dark';
|
|
271
|
-
|
|
272
|
-
// Add change handler
|
|
273
|
-
const changeHandler = (): void => {
|
|
274
|
-
const newTheme = toggle.checked ? 'dark' : 'light';
|
|
275
|
-
const previousTheme = this.state.currentTheme;
|
|
276
|
-
|
|
277
|
-
this.themeManager.apply(newTheme);
|
|
278
|
-
this.state.currentTheme = newTheme;
|
|
279
|
-
|
|
280
|
-
this.dispatchEvent('themeChanged', { theme: newTheme, previousTheme });
|
|
281
|
-
};
|
|
282
|
-
|
|
283
|
-
DOM.on(toggle, 'change', changeHandler);
|
|
284
|
-
this.eventHandlers.set('theme-toggle', changeHandler);
|
|
285
|
-
|
|
286
|
-
// Listen for theme changes from other sources
|
|
287
|
-
const themeChangeHandler = (event: CustomEvent): void => {
|
|
288
|
-
const newTheme = event.detail.theme;
|
|
289
|
-
toggle.checked = newTheme === 'dark';
|
|
290
|
-
this.state.currentTheme = newTheme;
|
|
291
|
-
|
|
292
|
-
// Update charts when theme changes
|
|
293
|
-
const charts = this.getComponent('charts') as ChartComponent;
|
|
294
|
-
if (charts && typeof charts.redrawCharts === 'function') {
|
|
295
|
-
charts.redrawCharts();
|
|
296
|
-
}
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
window.addEventListener('adminator:themeChanged', themeChangeHandler as EventListener);
|
|
300
|
-
this.eventHandlers.set('theme-change', themeChangeHandler as EventListener);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Initialize mobile-specific enhancements
|
|
305
|
-
*/
|
|
306
|
-
private initMobileEnhancements(): void {
|
|
307
|
-
if (!this.options.mobile?.enhanced) return;
|
|
308
|
-
|
|
309
|
-
this.log('Initializing mobile enhancements...');
|
|
310
|
-
this.enhanceMobileDropdowns();
|
|
311
|
-
this.enhanceMobileSearch();
|
|
312
|
-
|
|
313
|
-
// Prevent horizontal scroll on mobile
|
|
314
|
-
if (this.state.isMobile) {
|
|
315
|
-
document.body.style.overflowX = 'hidden';
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/**
|
|
320
|
-
* Setup global event listeners
|
|
321
|
-
*/
|
|
322
|
-
private setupGlobalEvents(): void {
|
|
323
|
-
// Global click handler
|
|
324
|
-
const globalClickHandler = (event: Event): void => {
|
|
325
|
-
this.handleGlobalClick(event);
|
|
326
|
-
};
|
|
327
|
-
DOM.on(document, 'click', globalClickHandler);
|
|
328
|
-
this.eventHandlers.set('global-click', globalClickHandler);
|
|
329
|
-
|
|
330
|
-
// Window resize handler with debouncing
|
|
331
|
-
const resizeHandler = (): void => {
|
|
332
|
-
if (this.resizeTimeout) {
|
|
333
|
-
clearTimeout(this.resizeTimeout);
|
|
334
|
-
}
|
|
335
|
-
this.resizeTimeout = window.setTimeout(() => {
|
|
336
|
-
this.handleResize();
|
|
337
|
-
}, 250);
|
|
338
|
-
};
|
|
339
|
-
DOM.on(window, 'resize', resizeHandler);
|
|
340
|
-
this.eventHandlers.set('resize', resizeHandler);
|
|
341
|
-
|
|
342
|
-
this.log('Global event listeners set up');
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Handle window resize events
|
|
347
|
-
*/
|
|
348
|
-
private handleResize(): void {
|
|
349
|
-
const wasMobile = this.state.isMobile;
|
|
350
|
-
this.state.isMobile = this.checkMobileState();
|
|
351
|
-
|
|
352
|
-
if (wasMobile !== this.state.isMobile) {
|
|
353
|
-
this.dispatchEvent('mobileStateChanged', { isMobile: this.state.isMobile });
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
this.log('Window resized, updating mobile features');
|
|
357
|
-
|
|
358
|
-
// Close all mobile-specific overlays when switching to desktop
|
|
359
|
-
if (!this.state.isMobile) {
|
|
360
|
-
document.body.style.overflow = '';
|
|
361
|
-
document.body.style.overflowX = '';
|
|
362
|
-
|
|
363
|
-
// Close dropdowns
|
|
364
|
-
const dropdowns = DOM.selectAll('.nav-right .dropdown');
|
|
365
|
-
dropdowns.forEach(dropdown => {
|
|
366
|
-
dropdown.classList.remove('show');
|
|
367
|
-
const menu = dropdown.querySelector('.dropdown-menu');
|
|
368
|
-
if (menu) menu.classList.remove('show');
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
// Close search
|
|
372
|
-
this.closeSearch();
|
|
373
|
-
} else {
|
|
374
|
-
// Re-enable mobile overflow protection
|
|
375
|
-
document.body.style.overflowX = 'hidden';
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Re-apply mobile enhancements
|
|
379
|
-
if (this.options.mobile?.enhanced) {
|
|
380
|
-
this.enhanceMobileDropdowns();
|
|
381
|
-
this.enhanceMobileSearch();
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* Handle global click events
|
|
387
|
-
*/
|
|
388
|
-
private handleGlobalClick(event: Event): void {
|
|
389
|
-
const target = event.target as HTMLElement;
|
|
390
|
-
|
|
391
|
-
// Close mobile dropdowns when clicking outside
|
|
392
|
-
if (!target.closest('.dropdown')) {
|
|
393
|
-
const dropdowns = DOM.selectAll('.nav-right .dropdown');
|
|
394
|
-
dropdowns.forEach(dropdown => {
|
|
395
|
-
dropdown.classList.remove('show');
|
|
396
|
-
const menu = dropdown.querySelector('.dropdown-menu');
|
|
397
|
-
if (menu) menu.classList.remove('show');
|
|
398
|
-
});
|
|
399
|
-
document.body.style.overflow = '';
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Close search when clicking outside
|
|
403
|
-
if (!target.closest('.search-box') && !target.closest('.search-input')) {
|
|
404
|
-
this.closeSearch();
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Check if we're on a mobile device
|
|
410
|
-
*/
|
|
411
|
-
private checkMobileState(): boolean {
|
|
412
|
-
return window.innerWidth <= 768;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/**
|
|
416
|
-
* Enhanced mobile dropdown handling
|
|
417
|
-
*/
|
|
418
|
-
private enhanceMobileDropdowns(): void {
|
|
419
|
-
if (!this.state.isMobile || this.options.mobile?.disableDropdowns) return;
|
|
420
|
-
|
|
421
|
-
const dropdowns = DOM.selectAll('.nav-right .dropdown');
|
|
422
|
-
|
|
423
|
-
dropdowns.forEach(dropdown => {
|
|
424
|
-
const toggle = dropdown.querySelector('.dropdown-toggle') as HTMLElement;
|
|
425
|
-
const menu = dropdown.querySelector('.dropdown-menu') as HTMLElement;
|
|
426
|
-
|
|
427
|
-
if (toggle && menu) {
|
|
428
|
-
// Remove existing listeners to prevent duplicates
|
|
429
|
-
const newToggle = toggle.cloneNode(true) as HTMLElement;
|
|
430
|
-
toggle.replaceWith(newToggle);
|
|
431
|
-
|
|
432
|
-
// Add click functionality for mobile dropdowns
|
|
433
|
-
DOM.on(newToggle, 'click', (e: Event) => {
|
|
434
|
-
e.preventDefault();
|
|
435
|
-
e.stopPropagation();
|
|
436
|
-
|
|
437
|
-
// Close search if open
|
|
438
|
-
this.closeSearch();
|
|
439
|
-
|
|
440
|
-
// Close other dropdowns first
|
|
441
|
-
dropdowns.forEach(otherDropdown => {
|
|
442
|
-
if (otherDropdown !== dropdown) {
|
|
443
|
-
otherDropdown.classList.remove('show');
|
|
444
|
-
const otherMenu = otherDropdown.querySelector('.dropdown-menu');
|
|
445
|
-
if (otherMenu) otherMenu.classList.remove('show');
|
|
446
|
-
}
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
// Toggle current dropdown
|
|
450
|
-
const isOpen = dropdown.classList.contains('show');
|
|
451
|
-
if (isOpen) {
|
|
452
|
-
dropdown.classList.remove('show');
|
|
453
|
-
menu.classList.remove('show');
|
|
454
|
-
document.body.style.overflow = '';
|
|
455
|
-
document.body.classList.remove('mobile-menu-open');
|
|
456
|
-
} else {
|
|
457
|
-
dropdown.classList.add('show');
|
|
458
|
-
menu.classList.add('show');
|
|
459
|
-
document.body.style.overflow = 'hidden';
|
|
460
|
-
document.body.classList.add('mobile-menu-open');
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
// Enhanced mobile close button functionality
|
|
465
|
-
DOM.on(menu, 'click', (e: Event) => {
|
|
466
|
-
const rect = menu.getBoundingClientRect();
|
|
467
|
-
const clickY = (e as MouseEvent).clientY - rect.top;
|
|
468
|
-
|
|
469
|
-
// If clicked in top 50px (close button area)
|
|
470
|
-
if (clickY <= 50) {
|
|
471
|
-
dropdown.classList.remove('show');
|
|
472
|
-
menu.classList.remove('show');
|
|
473
|
-
document.body.style.overflow = '';
|
|
474
|
-
document.body.classList.remove('mobile-menu-open');
|
|
475
|
-
e.preventDefault();
|
|
476
|
-
e.stopPropagation();
|
|
477
|
-
}
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
// Close dropdowns on escape key
|
|
483
|
-
const escapeHandler = (e: Event): void => {
|
|
484
|
-
const keyEvent = e as KeyboardEvent;
|
|
485
|
-
if (keyEvent.key === 'Escape') {
|
|
486
|
-
dropdowns.forEach(dropdown => {
|
|
487
|
-
dropdown.classList.remove('show');
|
|
488
|
-
const menu = dropdown.querySelector('.dropdown-menu');
|
|
489
|
-
if (menu) menu.classList.remove('show');
|
|
490
|
-
});
|
|
491
|
-
document.body.style.overflow = '';
|
|
492
|
-
document.body.classList.remove('mobile-menu-open');
|
|
493
|
-
}
|
|
494
|
-
};
|
|
495
|
-
|
|
496
|
-
DOM.on(document, 'keydown', escapeHandler);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* Enhanced mobile search handling
|
|
501
|
-
*/
|
|
502
|
-
private enhanceMobileSearch(): void {
|
|
503
|
-
if (!this.options.mobile?.fullWidthSearch) return;
|
|
504
|
-
|
|
505
|
-
const searchBox = DOM.select('.search-box') as HTMLElement;
|
|
506
|
-
const searchInput = DOM.select('.search-input') as HTMLElement;
|
|
507
|
-
|
|
508
|
-
if (searchBox && searchInput) {
|
|
509
|
-
const searchToggle = searchBox.querySelector('a') as HTMLAnchorElement;
|
|
510
|
-
const searchField = searchInput.querySelector('input') as HTMLInputElement;
|
|
511
|
-
|
|
512
|
-
if (searchToggle && searchField) {
|
|
513
|
-
// Remove existing listeners to prevent duplication
|
|
514
|
-
const newSearchToggle = searchToggle.cloneNode(true) as HTMLAnchorElement;
|
|
515
|
-
searchToggle.replaceWith(newSearchToggle);
|
|
516
|
-
|
|
517
|
-
DOM.on(newSearchToggle, 'click', (e: Event) => {
|
|
518
|
-
e.preventDefault();
|
|
519
|
-
e.stopPropagation();
|
|
520
|
-
|
|
521
|
-
// Close any open dropdowns first
|
|
522
|
-
const dropdowns = DOM.selectAll('.nav-right .dropdown');
|
|
523
|
-
dropdowns.forEach(dropdown => {
|
|
524
|
-
dropdown.classList.remove('show');
|
|
525
|
-
const menu = dropdown.querySelector('.dropdown-menu');
|
|
526
|
-
if (menu) menu.classList.remove('show');
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
// Toggle search state
|
|
530
|
-
const isActive = searchInput.classList.contains('active');
|
|
531
|
-
const searchIcon = newSearchToggle.querySelector('i') as HTMLElement;
|
|
532
|
-
|
|
533
|
-
if (isActive) {
|
|
534
|
-
this.closeSearch();
|
|
535
|
-
} else {
|
|
536
|
-
this.openSearch(searchField, searchIcon);
|
|
537
|
-
}
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
// Handle search input
|
|
541
|
-
DOM.on(searchField, 'keypress', (e: Event) => {
|
|
542
|
-
const keyEvent = e as KeyboardEvent;
|
|
543
|
-
if (keyEvent.key === 'Enter') {
|
|
544
|
-
keyEvent.preventDefault();
|
|
545
|
-
const query = searchField.value.trim();
|
|
546
|
-
if (query) {
|
|
547
|
-
this.handleSearch(query);
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
});
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
/**
|
|
556
|
-
* Open search interface
|
|
557
|
-
*/
|
|
558
|
-
private openSearch(searchField: HTMLInputElement, searchIcon: HTMLElement): void {
|
|
559
|
-
const searchInput = DOM.select('.search-input') as HTMLElement;
|
|
560
|
-
|
|
561
|
-
searchInput.classList.add('active');
|
|
562
|
-
document.body.classList.add('search-open');
|
|
563
|
-
|
|
564
|
-
// Change icon to close
|
|
565
|
-
if (searchIcon) {
|
|
566
|
-
searchIcon.className = 'ti-close';
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
// Focus the input after a short delay
|
|
570
|
-
setTimeout(() => {
|
|
571
|
-
searchField.focus();
|
|
572
|
-
}, 100);
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
/**
|
|
576
|
-
* Close search interface
|
|
577
|
-
*/
|
|
578
|
-
private closeSearch(): void {
|
|
579
|
-
const searchBox = DOM.select('.search-box') as HTMLElement;
|
|
580
|
-
const searchInput = DOM.select('.search-input') as HTMLElement;
|
|
581
|
-
|
|
582
|
-
if (searchBox && searchInput) {
|
|
583
|
-
searchInput.classList.remove('active');
|
|
584
|
-
document.body.classList.remove('search-open');
|
|
585
|
-
document.body.classList.remove('mobile-menu-open');
|
|
586
|
-
|
|
587
|
-
// Reset icon
|
|
588
|
-
const searchIcon = searchBox.querySelector('i') as HTMLElement;
|
|
589
|
-
if (searchIcon) {
|
|
590
|
-
searchIcon.className = 'ti-search';
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// Clear input
|
|
594
|
-
const searchField = searchInput.querySelector('input') as HTMLInputElement;
|
|
595
|
-
if (searchField) {
|
|
596
|
-
searchField.value = '';
|
|
597
|
-
searchField.blur();
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
/**
|
|
603
|
-
* Handle search query
|
|
604
|
-
*/
|
|
605
|
-
private handleSearch(query: string): void {
|
|
606
|
-
this.log(`Search query: ${query}`);
|
|
607
|
-
|
|
608
|
-
// Implement your search logic here
|
|
609
|
-
// For demo, close search after "searching"
|
|
610
|
-
this.closeSearch();
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
/**
|
|
614
|
-
* Add component to the application
|
|
615
|
-
*/
|
|
616
|
-
public addComponent(name: string, component: ComponentInterface): void {
|
|
617
|
-
this.state.components.set(name, component);
|
|
618
|
-
this.dispatchEvent('componentAdded', { name, component });
|
|
619
|
-
this.log(`Component added: ${name}`);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
/**
|
|
623
|
-
* Remove component from the application
|
|
624
|
-
*/
|
|
625
|
-
public removeComponent(name: string): void {
|
|
626
|
-
const component = this.state.components.get(name);
|
|
627
|
-
if (component) {
|
|
628
|
-
if (typeof component.destroy === 'function') {
|
|
629
|
-
component.destroy();
|
|
630
|
-
}
|
|
631
|
-
this.state.components.delete(name);
|
|
632
|
-
this.dispatchEvent('componentRemoved', { name });
|
|
633
|
-
this.log(`Component removed: ${name}`);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
/**
|
|
638
|
-
* Get a component by name
|
|
639
|
-
*/
|
|
640
|
-
public getComponent(name: string): ComponentInterface | undefined {
|
|
641
|
-
return this.state.components.get(name);
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
/**
|
|
645
|
-
* Get all components
|
|
646
|
-
*/
|
|
647
|
-
public getComponents(): Map<string, ComponentInterface> {
|
|
648
|
-
return new Map(this.state.components);
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
/**
|
|
652
|
-
* Check if app is ready
|
|
653
|
-
*/
|
|
654
|
-
public isReady(): boolean {
|
|
655
|
-
return this.state.isInitialized;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
/**
|
|
659
|
-
* Get current application state
|
|
660
|
-
*/
|
|
661
|
-
public getState(): Readonly<AdminatorAppState> {
|
|
662
|
-
return {
|
|
663
|
-
...this.state,
|
|
664
|
-
components: new Map(this.state.components),
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
/**
|
|
669
|
-
* Update application options
|
|
670
|
-
*/
|
|
671
|
-
public updateOptions(newOptions: Partial<AdminatorAppOptions>): void {
|
|
672
|
-
this.options = { ...this.options, ...newOptions };
|
|
673
|
-
this.log('Options updated');
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
/**
|
|
677
|
-
* Dispatch custom event
|
|
678
|
-
*/
|
|
679
|
-
private dispatchEvent<T extends keyof AdminatorAppEvents>(
|
|
680
|
-
type: T,
|
|
681
|
-
detail: AdminatorAppEvents[T]['detail']
|
|
682
|
-
): void {
|
|
683
|
-
const event = new CustomEvent(`adminator:${type}`, {
|
|
684
|
-
detail,
|
|
685
|
-
bubbles: true,
|
|
686
|
-
});
|
|
687
|
-
window.dispatchEvent(event);
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
/**
|
|
691
|
-
* Log message if debugging is enabled
|
|
692
|
-
*/
|
|
693
|
-
private log(message: string): void {
|
|
694
|
-
if (this.options.debug) {
|
|
695
|
-
console.log(`[AdminatorApp] ${message}`);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
/**
|
|
700
|
-
* Destroy the application
|
|
701
|
-
*/
|
|
702
|
-
public destroy(): void {
|
|
703
|
-
this.log('Destroying Adminator App');
|
|
704
|
-
|
|
705
|
-
// Destroy all components
|
|
706
|
-
this.state.components.forEach((component, name) => {
|
|
707
|
-
if (typeof component.destroy === 'function') {
|
|
708
|
-
component.destroy();
|
|
709
|
-
}
|
|
710
|
-
this.log(`Component destroyed: ${name}`);
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
// Remove event listeners
|
|
714
|
-
this.eventHandlers.forEach((_, name) => {
|
|
715
|
-
// Note: We'd need to track which element each handler was attached to
|
|
716
|
-
// For now, we'll rely on the browser's garbage collection
|
|
717
|
-
this.log(`Event handler removed: ${name}`);
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
// Clear state
|
|
721
|
-
this.state.components.clear();
|
|
722
|
-
this.eventHandlers.clear();
|
|
723
|
-
this.state.isInitialized = false;
|
|
724
|
-
|
|
725
|
-
// Clear timeout
|
|
726
|
-
if (this.resizeTimeout) {
|
|
727
|
-
clearTimeout(this.resizeTimeout);
|
|
728
|
-
this.resizeTimeout = null;
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
/**
|
|
733
|
-
* Refresh/reinitialize the application
|
|
734
|
-
*/
|
|
735
|
-
public refresh(): void {
|
|
736
|
-
this.log('Refreshing Adminator App');
|
|
737
|
-
|
|
738
|
-
if (this.state.isInitialized) {
|
|
739
|
-
this.destroy();
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
setTimeout(() => {
|
|
743
|
-
this.init();
|
|
744
|
-
}, 100);
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
// Initialize the application
|
|
749
|
-
const app = new AdminatorApp({
|
|
750
|
-
debug: process.env.NODE_ENV === 'development',
|
|
751
|
-
});
|
|
752
|
-
|
|
753
|
-
// Make app globally available for debugging
|
|
754
|
-
window.AdminatorApp = app;
|
|
755
|
-
|
|
756
|
-
// Export for module usage
|
|
757
|
-
export default app;
|