| @ -0,0 +1,24 @@ | |||||
| { | |||||
| "permissions": { | |||||
| "allow": [ | |||||
| "Bash(npm run build:*)", | |||||
| "Bash(npm install)", | |||||
| "Bash(npm run lint)", | |||||
| "Bash(rm:*)", | |||||
| "Bash(ls:*)", | |||||
| "Bash(pkill:*)", | |||||
| "Bash(true)", | |||||
| "Bash(npm start)", | |||||
| "Bash(grep:*)", | |||||
| "Bash(sudo rm:*)", | |||||
| "Bash(npx eslint:*)", | |||||
| "Bash(npm run lint:*)", | |||||
| "Bash(gh release create:*)", | |||||
| "Bash(npm search:*)", | |||||
| "Bash(npm pack:*)", | |||||
| "Bash(npm:*)", | |||||
| "WebFetch(domain:keenthemes.com)" | |||||
| ], | |||||
| "deny": [] | |||||
| } | |||||
| } | |||||
| @ -0,0 +1,162 @@ | |||||
| # CLAUDE.md | |||||
| This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | |||||
| ## Development Commands | |||||
| ### Core Development | |||||
| - `npm start` - Start development server with hot reload (available at http://localhost:4000) | |||||
| - `npm run dev` - Start development server with webpack dashboard | |||||
| - `npm run build` - Build for production (optimized) | |||||
| - `npm run release:minified` - Build production with minification | |||||
| - `npm run release:unminified` - Build production without minification | |||||
| - `npm run preview` - Preview production build locally | |||||
| ### Code Quality | |||||
| - `npm run lint` - Run all linters (JavaScript + SCSS) | |||||
| - `npm run lint:js` - Lint JavaScript files using ESLint 9.x flat config | |||||
| - `npm run lint:scss` - Lint SCSS files using Stylelint | |||||
| ### Utility Commands | |||||
| - `npm run clean` - Clean the dist directory | |||||
| ## Project Architecture | |||||
| ### Technology Stack | |||||
| - **Build System**: Webpack 5.99.9 with modern configuration | |||||
| - **JavaScript**: ES6+ with Babel transpilation, ESLint 9.x flat config | |||||
| - **CSS**: Sass/SCSS with PostCSS processing, Bootstrap 5.3.7 | |||||
| - **Frontend Framework**: **100% jQuery-free** vanilla JavaScript with modern class-based architecture | |||||
| - **Theme System**: CSS variables-based dark/light mode system | |||||
| ### Core Application Structure | |||||
| The application follows a modular class-based architecture: | |||||
| **Main Application Class** (`src/assets/scripts/app.js`): | |||||
| - `AdminatorApp` - Main application controller with component management | |||||
| - Handles initialization, mobile optimizations, and global event coordination | |||||
| - Component registry system for managing feature modules | |||||
| **Core Components**: | |||||
| - `Sidebar` (`src/assets/scripts/components/Sidebar.js`) - Navigation sidebar logic | |||||
| - `ChartComponent` (`src/assets/scripts/components/Chart.js`) - Chart rendering and theme integration | |||||
| - `Theme` (`src/assets/scripts/utils/theme.js`) - Theme management with localStorage persistence | |||||
| **Utility Modules**: | |||||
| - `DOM` (`src/assets/scripts/utils/dom.js`) - DOM manipulation helpers | |||||
| - `DateUtils` (`src/assets/scripts/utils/date.js`) - Date handling with Day.js integration | |||||
| ### Dark Mode System | |||||
| The project features a comprehensive dark mode implementation: | |||||
| **Theme Toggle Integration**: | |||||
| - Automatically injects theme toggle into navigation if missing | |||||
| - Detects OS preference on first visit | |||||
| - Persists theme choice in localStorage | |||||
| - Real-time theme switching without page reload | |||||
| **Component Theme Awareness**: | |||||
| - Chart.js integration with dynamic color schemes | |||||
| - FullCalendar dark mode support | |||||
| - Vector maps with theme-specific palettes | |||||
| - All UI components use CSS variables for theming | |||||
| **CSS Variables Architecture**: | |||||
| - Semantic color variables (e.g., `--c-bkg-body`, `--c-text-base`) | |||||
| - Component-specific theme variables | |||||
| - Automatic contrast and accessibility considerations | |||||
| ### Mobile Optimization | |||||
| The application includes extensive mobile enhancements: | |||||
| **Responsive Features**: | |||||
| - Full-width search overlay for mobile | |||||
| - Enhanced dropdown behavior with overlay management | |||||
| - Touch-friendly interactions | |||||
| - Viewport-based responsive breakpoints | |||||
| **Mobile-Specific Behavior**: | |||||
| - Prevents horizontal scrolling on mobile | |||||
| - Auto-focus management for form inputs | |||||
| - Gesture-based navigation support | |||||
| ### File Organization | |||||
| ``` | |||||
| src/ | |||||
| ├── assets/ | |||||
| │ ├── scripts/ # JavaScript modules | |||||
| │ │ ├── components/ # Reusable UI components | |||||
| │ │ ├── utils/ # Utility functions | |||||
| │ │ ├── charts/ # Chart initialization modules | |||||
| │ │ ├── fullcalendar/ # Calendar integration | |||||
| │ │ └── app.js # Main application entry point | |||||
| │ ├── styles/ # SCSS stylesheets | |||||
| │ │ ├── spec/ # Custom component styles | |||||
| │ │ └── vendor/ # Third-party plugin styles | |||||
| │ └── static/ # Static assets (fonts, images) | |||||
| ├── *.html # HTML template pages | |||||
| ``` | |||||
| ### Build Configuration | |||||
| **Webpack Setup**: | |||||
| - Modern flat ESLint configuration | |||||
| - Sass compilation with PostCSS processing | |||||
| - Source map generation for development | |||||
| - Production optimization with minification | |||||
| - Hot module replacement for development | |||||
| **Development Server**: | |||||
| - Webpack dev server on port 4000 | |||||
| - Live reload and hot module replacement | |||||
| - Proxy configuration for API endpoints | |||||
| ## Working with This Codebase | |||||
| ### Adding New Components | |||||
| 1. Create component class in `src/assets/scripts/components/` | |||||
| 2. Register component in `AdminatorApp.init()` method | |||||
| 3. Add component-specific styles in `src/assets/styles/spec/components/` | |||||
| 4. Ensure theme compatibility using CSS variables | |||||
| ### Modifying Themes | |||||
| - Theme logic is centralized in `src/assets/scripts/utils/theme.js` | |||||
| - CSS variables are defined in `src/assets/styles/utils/theme.css` | |||||
| - Chart.js theme integration is automatic via `Chart.defaults` configuration | |||||
| ### Testing Changes | |||||
| - Always run `npm run lint` before committing | |||||
| - Test both light and dark themes | |||||
| - Verify mobile responsiveness at various breakpoints | |||||
| - Check component integration via browser developer tools | |||||
| ### Development Workflow | |||||
| 1. Run `npm start` for development server | |||||
| 2. Use `npm run dev` for enhanced debugging with webpack dashboard | |||||
| 3. Lint code with `npm run lint` before commits | |||||
| 4. Build production assets with `npm run build` | |||||
| 5. Preview production build with `npm run preview` | |||||
| ### Key Dependencies | |||||
| - **Bootstrap 5.3.7**: UI framework and CSS components (JS components replaced with vanilla alternatives) | |||||
| - **Chart.js 4.5.0**: Interactive charts with theme support (replaces jQuery Sparkline) | |||||
| - **FullCalendar 6.1.17**: Calendar component with dark mode | |||||
| - **Day.js 1.11.13**: Lightweight date manipulation | |||||
| - **Perfect Scrollbar 1.5.6**: Custom scrollbar implementation | |||||
| - **Masonry Layout 4.2.2**: Grid layouts (vanilla JS compatible) | |||||
| ### Removed jQuery Dependencies | |||||
| **Successfully removed all jQuery dependencies (~600KB bundle reduction):** | |||||
| - ❌ `jquery` (3.7.1) - Replaced with vanilla JS DOM manipulation | |||||
| - ❌ `jquery-sparkline` (2.4.0) - Replaced with Chart.js mini charts | |||||
| - ❌ `bootstrap-datepicker` (1.10.0) - Replaced with HTML5 date inputs + vanilla JS | |||||
| - ❌ `datatables` (1.10.18) - Replaced with vanilla JS table component | |||||
| - ❌ `easy-pie-chart` (2.1.7) - Replaced with vanilla JS SVG pie charts | |||||
| - ❌ `jvectormap` (2.0.4) - Replaced with vanilla JS SVG world map | |||||
| ### Modern Vanilla JS Implementations | |||||
| - **Sparkline Charts**: Chart.js-based mini charts with theme support | |||||
| - **Pie Charts**: Custom SVG-based circular progress indicators | |||||
| - **Data Tables**: Full-featured table with sorting, pagination, and search | |||||
| - **Date Pickers**: Enhanced HTML5 date inputs with custom styling | |||||
| - **Vector Maps**: SVG-based world map with markers and interactions | |||||
| - **Bootstrap 5 Components**: Vanilla JS implementations of modals, dropdowns, popovers, tooltips, and accordions | |||||
| @ -1,787 +0,0 @@ | |||||
| <!DOCTYPE html> | |||||
| <html> | |||||
| <head> | |||||
| <meta charset="utf-8"> | |||||
| <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | |||||
| <title>Forms</title> | |||||
| <style> | |||||
| #loader { | |||||
| transition: all 0.3s ease-in-out; | |||||
| opacity: 1; | |||||
| visibility: visible; | |||||
| position: fixed; | |||||
| height: 100vh; | |||||
| width: 100%; | |||||
| background: #fff; | |||||
| z-index: 90000; | |||||
| } | |||||
| #loader.fadeOut { | |||||
| opacity: 0; | |||||
| visibility: hidden; | |||||
| } | |||||
| .spinner { | |||||
| width: 40px; | |||||
| height: 40px; | |||||
| position: absolute; | |||||
| top: calc(50% - 20px); | |||||
| left: calc(50% - 20px); | |||||
| background-color: #333; | |||||
| border-radius: 100%; | |||||
| -webkit-animation: sk-scaleout 1.0s infinite ease-in-out; | |||||
| animation: sk-scaleout 1.0s infinite ease-in-out; | |||||
| } | |||||
| @-webkit-keyframes sk-scaleout { | |||||
| 0% { -webkit-transform: scale(0) } | |||||
| 100% { | |||||
| -webkit-transform: scale(1.0); | |||||
| opacity: 0; | |||||
| } | |||||
| } | |||||
| @keyframes sk-scaleout { | |||||
| 0% { | |||||
| -webkit-transform: scale(0); | |||||
| transform: scale(0); | |||||
| } 100% { | |||||
| -webkit-transform: scale(1.0); | |||||
| transform: scale(1.0); | |||||
| opacity: 0; | |||||
| } | |||||
| } | |||||
| </style> | |||||
| </head> | |||||
| <body class="app"> | |||||
| <!-- @TOC --> | |||||
| <!-- =================================================== --> | |||||
| <!-- | |||||
| + @Page Loader | |||||
| + @App Content | |||||
| - #Left Sidebar | |||||
| > $Sidebar Header | |||||
| > $Sidebar Menu | |||||
| - #Main | |||||
| > $Topbar | |||||
| > $App Screen Content | |||||
| --> | |||||
| <!-- @Page Loader --> | |||||
| <!-- =================================================== --> | |||||
| <div id='loader'> | |||||
| <div class="spinner"></div> | |||||
| </div> | |||||
| <script> | |||||
| window.addEventListener('load', function load() { | |||||
| const loader = document.getElementById('loader'); | |||||
| setTimeout(function() { | |||||
| loader.classList.add('fadeOut'); | |||||
| }, 300); | |||||
| }); | |||||
| </script> | |||||
| <!-- @App Content --> | |||||
| <!-- =================================================== --> | |||||
| <div> | |||||
| <!-- #Left Sidebar ==================== --> | |||||
| <div class="sidebar"> | |||||
| <div class="sidebar-inner"> | |||||
| <!-- ### $Sidebar Header ### --> | |||||
| <div class="sidebar-logo"> | |||||
| <div class="peers ai-c fxw-nw"> | |||||
| <div class="peer peer-greed"> | |||||
| <a class="sidebar-link td-n" href="index.html"> | |||||
| <div class="peers ai-c fxw-nw"> | |||||
| <div class="peer"> | |||||
| <div class="logo"> | |||||
| <img src="assets/static/images/logo.png" alt=""> | |||||
| </div> | |||||
| </div> | |||||
| <div class="peer peer-greed"> | |||||
| <h5 class="lh-1 mB-0 logo-text">Adminator</h5> | |||||
| </div> | |||||
| </div> | |||||
| </a> | |||||
| </div> | |||||
| <div class="peer"> | |||||
| <div class="mobile-toggle sidebar-toggle"> | |||||
| <a href="" class="td-n"> | |||||
| <i class="ti-arrow-circle-left"></i> | |||||
| </a> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <!-- ### $Sidebar Menu ### --> | |||||
| <ul class="sidebar-menu scrollable pos-r"> | |||||
| <li class="nav-item mT-30 actived"> | |||||
| <a class="sidebar-link" href="index.html"> | |||||
| <span class="icon-holder"> | |||||
| <i class="c-blue-500 ti-home"></i> | |||||
| </span> | |||||
| <span class="title">Dashboard</span> | |||||
| </a> | |||||
| </li> | |||||
| <li class="nav-item"> | |||||
| <a class='sidebar-link' href="email.html"> | |||||
| <span class="icon-holder"> | |||||
| <i class="c-brown-500 ti-email"></i> | |||||
| </span> | |||||
| <span class="title">Email</span> | |||||
| </a> | |||||
| </li> | |||||
| <li class="nav-item"> | |||||
| <a class='sidebar-link' href="compose.html"> | |||||
| <span class="icon-holder"> | |||||
| <i class="c-blue-500 ti-share"></i> | |||||
| </span> | |||||
| <span class="title">Compose</span> | |||||
| </a> | |||||
| </li> | |||||
| <li class="nav-item"> | |||||
| <a class='sidebar-link' href="calendar.html"> | |||||
| <span class="icon-holder"> | |||||
| <i class="c-deep-orange-500 ti-calendar"></i> | |||||
| </span> | |||||
| <span class="title">Calendar</span> | |||||
| </a> | |||||
| </li> | |||||
| <li class="nav-item"> | |||||
| <a class='sidebar-link' href="chat.html"> | |||||
| <span class="icon-holder"> | |||||
| <i class="c-deep-purple-500 ti-comment-alt"></i> | |||||
| </span> | |||||
| <span class="title">Chat</span> | |||||
| </a> | |||||
| </li> | |||||
| <li class="nav-item"> | |||||
| <a class='sidebar-link' href="charts.html"> | |||||
| <span class="icon-holder"> | |||||
| <i class="c-indigo-500 ti-bar-chart"></i> | |||||
| </span> | |||||
| <span class="title">Charts</span> | |||||
| </a> | |||||
| </li> | |||||
| <li class="nav-item"> | |||||
| <a class='sidebar-link' href="forms.html"> | |||||
| <span class="icon-holder"> | |||||
| <i class="c-light-blue-500 ti-pencil"></i> | |||||
| </span> | |||||
| <span class="title">Forms</span> | |||||
| </a> | |||||
| </li> | |||||
| <li class="nav-item dropdown"> | |||||
| <a class="sidebar-link" href="ui.html"> | |||||
| <span class="icon-holder"> | |||||
| <i class="c-pink-500 ti-palette"></i> | |||||
| </span> | |||||
| <span class="title">UI Elements</span> | |||||
| </a> | |||||
| </li> | |||||
| <li class="nav-item dropdown"> | |||||
| <a class="dropdown-toggle" href="javascript:void(0);"> | |||||
| <span class="icon-holder"> | |||||
| <i class="c-orange-500 ti-layout-list-thumb"></i> | |||||
| </span> | |||||
| <span class="title">Tables</span> | |||||
| <span class="arrow"> | |||||
| <i class="ti-angle-right"></i> | |||||
| </span> | |||||
| </a> | |||||
| <ul class="dropdown-menu"> | |||||
| <li> | |||||
| <a class='sidebar-link' href="basic-table.html">Basic Table</a> | |||||
| </li> | |||||
| <li> | |||||
| <a class='sidebar-link' href="datatable.html">Data Table</a> | |||||
| </li> | |||||
| </ul> | |||||
| </li> | |||||
| <li class="nav-item dropdown"> | |||||
| <a class="dropdown-toggle" href="javascript:void(0);"> | |||||
| <span class="icon-holder"> | |||||
| <i class="c-purple-500 ti-map"></i> | |||||
| </span> | |||||
| <span class="title">Maps</span> | |||||
| <span class="arrow"> | |||||
| <i class="ti-angle-right"></i> | |||||
| </span> | |||||
| </a> | |||||
| <ul class="dropdown-menu"> | |||||
| <li> | |||||
| <a href="google-maps.html">Google Map</a> | |||||
| </li> | |||||
| <li> | |||||
| <a href="vector-maps.html">Vector Map</a> | |||||
| </li> | |||||
| </ul> | |||||
| </li> | |||||
| <li class="nav-item dropdown"> | |||||
| <a class="dropdown-toggle" href="javascript:void(0);"> | |||||
| <span class="icon-holder"> | |||||
| <i class="c-red-500 ti-files"></i> | |||||
| </span> | |||||
| <span class="title">Pages</span> | |||||
| <span class="arrow"> | |||||
| <i class="ti-angle-right"></i> | |||||
| </span> | |||||
| </a> | |||||
| <ul class="dropdown-menu"> | |||||
| <li> | |||||
| <a class='sidebar-link' href="blank.html">Blank</a> | |||||
| </li> | |||||
| <li> | |||||
| <a class='sidebar-link' href="404.html">404</a> | |||||
| </li> | |||||
| <li> | |||||
| <a class='sidebar-link' href="500.html">500</a> | |||||
| </li> | |||||
| <li> | |||||
| <a class='sidebar-link' href="signin.html">Sign In</a> | |||||
| </li> | |||||
| <li> | |||||
| <a class='sidebar-link' href="signup.html">Sign Up</a> | |||||
| </li> | |||||
| </ul> | |||||
| </li> | |||||
| <li class="nav-item dropdown"> | |||||
| <a class="dropdown-toggle" href="javascript:void(0);"> | |||||
| <span class="icon-holder"> | |||||
| <i class="c-teal-500 ti-view-list-alt"></i> | |||||
| </span> | |||||
| <span class="title">Multiple Levels</span> | |||||
| <span class="arrow"> | |||||
| <i class="ti-angle-right"></i> | |||||
| </span> | |||||
| </a> | |||||
| <ul class="dropdown-menu"> | |||||
| <li class="nav-item dropdown"> | |||||
| <a href="javascript:void(0);"> | |||||
| <span>Menu Item</span> | |||||
| </a> | |||||
| </li> | |||||
| <li class="nav-item dropdown"> | |||||
| <a href="javascript:void(0);"> | |||||
| <span>Menu Item</span> | |||||
| <span class="arrow"> | |||||
| <i class="ti-angle-right"></i> | |||||
| </span> | |||||
| </a> | |||||
| <ul class="dropdown-menu"> | |||||
| <li> | |||||
| <a href="javascript:void(0);">Menu Item</a> | |||||
| </li> | |||||
| <li> | |||||
| <a href="javascript:void(0);">Menu Item</a> | |||||
| </li> | |||||
| </ul> | |||||
| </li> | |||||
| </ul> | |||||
| </li> | |||||
| </ul> | |||||
| </div> | |||||
| </div> | |||||
| <!-- #Main ============================ --> | |||||
| <div class="page-container"> | |||||
| <!-- ### $Topbar ### --> | |||||
| <div class="header navbar"> | |||||
| <div class="header-container"> | |||||
| <ul class="nav-left"> | |||||
| <li> | |||||
| <a id='sidebar-toggle' class="sidebar-toggle" href="javascript:void(0);"> | |||||
| <i class="ti-menu"></i> | |||||
| </a> | |||||
| </li> | |||||
| <li class="search-box"> | |||||
| <a class="search-toggle no-pdd-right" href="javascript:void(0);"> | |||||
| <i class="search-icon ti-search pdd-right-10"></i> | |||||
| <i class="search-icon-close ti-close pdd-right-10"></i> | |||||
| </a> | |||||
| </li> | |||||
| <li class="search-input"> | |||||
| <input class="form-control" type="text" placeholder="Search..."> | |||||
| </li> | |||||
| </ul> | |||||
| <ul class="nav-right"> | |||||
| <li class="notifications dropdown"> | |||||
| <span class="counter bgc-red">3</span> | |||||
| <a href="" class="dropdown-toggle no-after" data-bs-toggle="dropdown"> | |||||
| <i class="ti-bell"></i> | |||||
| </a> | |||||
| <ul class="dropdown-menu"> | |||||
| <li class="pX-20 pY-15 bdB"> | |||||
| <i class="ti-bell pR-10"></i> | |||||
| <span class="fsz-sm fw-600 c-grey-900">Notifications</span> | |||||
| </li> | |||||
| <li> | |||||
| <ul class="ovY-a pos-r scrollable lis-n p-0 m-0 fsz-sm"> | |||||
| <li> | |||||
| <a href="" class='peers fxw-nw td-n p-20 bdB c-grey-800 cH-blue bgcH-grey-100'> | |||||
| <div class="peer me-15"> | |||||
| <img class="w-3r bdrs-50p" src="https://randomuser.me/api/portraits/men/1.jpg" alt=""> | |||||
| </div> | |||||
| <div class="peer peer-greed"> | |||||
| <span> | |||||
| <span class="fw-500">John Doe</span> | |||||
| <span class="c-grey-600">liked your <span class="text-dark">post</span> | |||||
| </span> | |||||
| </span> | |||||
| <p class="m-0"> | |||||
| <small class="fsz-xs">5 mins ago</small> | |||||
| </p> | |||||
| </div> | |||||
| </a> | |||||
| </li> | |||||
| <li> | |||||
| <a href="" class='peers fxw-nw td-n p-20 bdB c-grey-800 cH-blue bgcH-grey-100'> | |||||
| <div class="peer me-15"> | |||||
| <img class="w-3r bdrs-50p" src="https://randomuser.me/api/portraits/men/2.jpg" alt=""> | |||||
| </div> | |||||
| <div class="peer peer-greed"> | |||||
| <span> | |||||
| <span class="fw-500">Moo Doe</span> | |||||
| <span class="c-grey-600">liked your <span class="text-dark">cover image</span> | |||||
| </span> | |||||
| </span> | |||||
| <p class="m-0"> | |||||
| <small class="fsz-xs">7 mins ago</small> | |||||
| </p> | |||||
| </div> | |||||
| </a> | |||||
| </li> | |||||
| <li> | |||||
| <a href="" class='peers fxw-nw td-n p-20 bdB c-grey-800 cH-blue bgcH-grey-100'> | |||||
| <div class="peer me-15"> | |||||
| <img class="w-3r bdrs-50p" src="https://randomuser.me/api/portraits/men/3.jpg" alt=""> | |||||
| </div> | |||||
| <div class="peer peer-greed"> | |||||
| <span> | |||||
| <span class="fw-500">Lee Doe</span> | |||||
| <span class="c-grey-600">commented on your <span class="text-dark">video</span> | |||||
| </span> | |||||
| </span> | |||||
| <p class="m-0"> | |||||
| <small class="fsz-xs">10 mins ago</small> | |||||
| </p> | |||||
| </div> | |||||
| </a> | |||||
| </li> | |||||
| </ul> | |||||
| </li> | |||||
| <li class="pX-20 pY-15 ta-c bdT"> | |||||
| <span> | |||||
| <a href="" class="c-grey-600 cH-blue fsz-sm td-n">View All Notifications <i class="ti-angle-right fsz-xs ms-10"></i></a> | |||||
| </span> | |||||
| </li> | |||||
| </ul> | |||||
| </li> | |||||
| <li class="notifications dropdown"> | |||||
| <span class="counter bgc-blue">3</span> | |||||
| <a href="" class="dropdown-toggle no-after" data-bs-toggle="dropdown"> | |||||
| <i class="ti-email"></i> | |||||
| </a> | |||||
| <ul class="dropdown-menu"> | |||||
| <li class="pX-20 pY-15 bdB"> | |||||
| <i class="ti-email pR-10"></i> | |||||
| <span class="fsz-sm fw-600 c-grey-900">Emails</span> | |||||
| </li> | |||||
| <li> | |||||
| <ul class="ovY-a pos-r scrollable lis-n p-0 m-0 fsz-sm"> | |||||
| <li> | |||||
| <a href="" class='peers fxw-nw td-n p-20 bdB c-grey-800 cH-blue bgcH-grey-100'> | |||||
| <div class="peer me-15"> | |||||
| <img class="w-3r bdrs-50p" src="https://randomuser.me/api/portraits/men/1.jpg" alt=""> | |||||
| </div> | |||||
| <div class="peer peer-greed"> | |||||
| <div> | |||||
| <div class="peers jc-sb fxw-nw mB-5"> | |||||
| <div class="peer"> | |||||
| <p class="fw-500 mB-0">John Doe</p> | |||||
| </div> | |||||
| <div class="peer"> | |||||
| <small class="fsz-xs">5 mins ago</small> | |||||
| </div> | |||||
| </div> | |||||
| <span class="c-grey-600 fsz-sm"> | |||||
| Want to create your own customized data generator for your app... | |||||
| </span> | |||||
| </div> | |||||
| </div> | |||||
| </a> | |||||
| </li> | |||||
| <li> | |||||
| <a href="" class='peers fxw-nw td-n p-20 bdB c-grey-800 cH-blue bgcH-grey-100'> | |||||
| <div class="peer me-15"> | |||||
| <img class="w-3r bdrs-50p" src="https://randomuser.me/api/portraits/men/2.jpg" alt=""> | |||||
| </div> | |||||
| <div class="peer peer-greed"> | |||||
| <div> | |||||
| <div class="peers jc-sb fxw-nw mB-5"> | |||||
| <div class="peer"> | |||||
| <p class="fw-500 mB-0">Moo Doe</p> | |||||
| </div> | |||||
| <div class="peer"> | |||||
| <small class="fsz-xs">15 mins ago</small> | |||||
| </div> | |||||
| </div> | |||||
| <span class="c-grey-600 fsz-sm"> | |||||
| Want to create your own customized data generator for your app... | |||||
| </span> | |||||
| </div> | |||||
| </div> | |||||
| </a> | |||||
| </li> | |||||
| <li> | |||||
| <a href="" class='peers fxw-nw td-n p-20 bdB c-grey-800 cH-blue bgcH-grey-100'> | |||||
| <div class="peer me-15"> | |||||
| <img class="w-3r bdrs-50p" src="https://randomuser.me/api/portraits/men/3.jpg" alt=""> | |||||
| </div> | |||||
| <div class="peer peer-greed"> | |||||
| <div> | |||||
| <div class="peers jc-sb fxw-nw mB-5"> | |||||
| <div class="peer"> | |||||
| <p class="fw-500 mB-0">Lee Doe</p> | |||||
| </div> | |||||
| <div class="peer"> | |||||
| <small class="fsz-xs">25 mins ago</small> | |||||
| </div> | |||||
| </div> | |||||
| <span class="c-grey-600 fsz-sm"> | |||||
| Want to create your own customized data generator for your app... | |||||
| </span> | |||||
| </div> | |||||
| </div> | |||||
| </a> | |||||
| </li> | |||||
| </ul> | |||||
| </li> | |||||
| <li class="pX-20 pY-15 ta-c bdT"> | |||||
| <span> | |||||
| <a href="email.html" class="c-grey-600 cH-blue fsz-sm td-n">View All Email <i class="fs-xs ti-angle-right ms-10"></i></a> | |||||
| </span> | |||||
| </li> | |||||
| </ul> | |||||
| </li> | |||||
| <li class="dropdown"> | |||||
| <a href="" class="dropdown-toggle no-after peers fxw-nw ai-c lh-1" data-bs-toggle="dropdown"> | |||||
| <div class="peer me-10"> | |||||
| <img class="w-2r bdrs-50p" src="https://randomuser.me/api/portraits/men/10.jpg" alt=""> | |||||
| </div> | |||||
| <div class="peer"> | |||||
| <span class="fsz-sm c-grey-900">John Doe</span> | |||||
| </div> | |||||
| </a> | |||||
| <ul class="dropdown-menu fsz-sm"> | |||||
| <li> | |||||
| <a href="" class="d-b td-n pY-5 bgcH-grey-100 c-grey-700"> | |||||
| <i class="ti-settings me-10"></i> | |||||
| <span>Setting</span> | |||||
| </a> | |||||
| </li> | |||||
| <li> | |||||
| <a href="" class="d-b td-n pY-5 bgcH-grey-100 c-grey-700"> | |||||
| <i class="ti-user me-10"></i> | |||||
| <span>Profile</span> | |||||
| </a> | |||||
| </li> | |||||
| <li> | |||||
| <a href="email.html" class="d-b td-n pY-5 bgcH-grey-100 c-grey-700"> | |||||
| <i class="ti-email me-10"></i> | |||||
| <span>Messages</span> | |||||
| </a> | |||||
| </li> | |||||
| <li role="separator" class="divider"></li> | |||||
| <li> | |||||
| <a href="" class="d-b td-n pY-5 bgcH-grey-100 c-grey-700"> | |||||
| <i class="ti-power-off me-10"></i> | |||||
| <span>Logout</span> | |||||
| </a> | |||||
| </li> | |||||
| </ul> | |||||
| </li> | |||||
| </ul> | |||||
| </div> | |||||
| </div> | |||||
| <!-- ### $App Screen Content ### --> | |||||
| <main class='main-content bgc-grey-100'> | |||||
| <div id='mainContent'> | |||||
| <div class="row gap-20 masonry pos-r"> | |||||
| <div class="masonry-sizer col-md-6"></div> | |||||
| <div class="masonry-item col-md-6"> | |||||
| <div class="bgc-white p-20 bd"> | |||||
| <h6 class="c-grey-900">Basic Form</h6> | |||||
| <div class="mT-30"> | |||||
| <form action="" method="POST"> | |||||
| <div class="mb-3"> | |||||
| <label class="form-label" for="exampleInputEmail1">Email address</label> | |||||
| <input type="email" class="form-control" name="email" id="exampleInputEmail1" aria-describedby="emailHelp" placeholder="Enter email"> | |||||
| <small id="emailHelp" class="text-muted">We'll never share your email with anyone else.</small> | |||||
| </div> | |||||
| <div class="mb-3"> | |||||
| <label class="form-label" for="exampleInputPassword1">Password</label> | |||||
| <input type="password" name="password" class="form-control" id="exampleInputPassword1" placeholder="Password"> | |||||
| </div> | |||||
| <div class="checkbox checkbox-circle checkbox-info peers ai-c mB-15"> | |||||
| <input type="checkbox" id="inputCall1" name="inputCheckboxesCall" class="peer"> | |||||
| <label for="inputCall1" class="form-label peers peer-greed js-sb ai-c"> | |||||
| <span class="peer peer-greed">Call John for Dinner</span> | |||||
| </label> | |||||
| </div> | |||||
| <input type="submit" class="btn btn-primary" value="Submit"> | |||||
| </form> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <!-- For basic form --> | |||||
| <?php | |||||
| if($_SERVER['REQUEST_METHOD']=="POST") | |||||
| { | |||||
| if(isset($_POST['email'])) | |||||
| { | |||||
| $name=$_POST['email']; | |||||
| } | |||||
| if(isset($_POST['password'])) | |||||
| { | |||||
| $password=$_POST['password']; | |||||
| } | |||||
| echo $name; | |||||
| echo "<br>"; | |||||
| echo $password; | |||||
| } | |||||
| ?> | |||||
| <div class="masonry-item col-md-6"> | |||||
| <div class="bgc-white p-20 bd"> | |||||
| <h6 class="c-grey-900">Complex Form Layout</h6> | |||||
| <div class="mT-30"> | |||||
| <form action="" method="POST"> | |||||
| <div class="row"> | |||||
| <div class="mb-3 col-md-6"> | |||||
| <label for="inputEmail4" class="form-label">Email</label> | |||||
| <input type="email" class="form-control" id="inputEmail4" placeholder="Email"> | |||||
| </div> | |||||
| <div class="mb-3 col-md-6"> | |||||
| <label for="inputPassword4" class="form-label">Password</label> | |||||
| <input type="password" class="form-control" id="inputPassword4" placeholder="Password"> | |||||
| </div> | |||||
| </div> | |||||
| <div class="mb-3"> | |||||
| <label for="inputAddress" class="form-label">Address</label> | |||||
| <input type="text" class="form-control" id="inputAddress" placeholder="1234 Main St"> | |||||
| </div> | |||||
| <div class="mb-3"> | |||||
| <label for="inputAddress2" class="form-label">Address 2</label> | |||||
| <input type="text" class="form-control" id="inputAddress2" placeholder="Apartment, studio, or floor"> | |||||
| </div> | |||||
| <div class="row"> | |||||
| <div class="mb-3 col-md-6"> | |||||
| <label for="inputCity" class="form-label">City</label> | |||||
| <input type="text" class="form-control" id="inputCity"> | |||||
| </div> | |||||
| <div class="mb-3 col-md-4"> | |||||
| <label for="inputState" class="form-label">State</label> | |||||
| <select id="inputState" class="form-control"> | |||||
| <option selected>Choose...</option> | |||||
| <option>...</option> | |||||
| </select> | |||||
| </div> | |||||
| <div class="mb-3 col-md-2"> | |||||
| <label for="inputZip" class="form-label">Zip</label> | |||||
| <input type="text" class="form-control" id="inputZip"> | |||||
| </div> | |||||
| </div> | |||||
| <div class="row"> | |||||
| <div class="mb-3 col-md-6"> | |||||
| <label class="form-label" class="fw-500">Birthdate</label> | |||||
| <div class="timepicker-input input-icon mb-3"> | |||||
| <div class="input-group"> | |||||
| <div class="input-group-text bgc-white bd bdwR-0"> | |||||
| <i class="ti-calendar"></i> | |||||
| </div> | |||||
| <input type="text" class="form-control bdc-grey-200 start-date" placeholder="Select Date"> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <div class="mb-3"> | |||||
| <div class="checkbox checkbox-circle checkbox-info peers ai-c"> | |||||
| <input type="checkbox" id="inputCall2" name="inputCheckboxesCall" class="peer"> | |||||
| <label for="inputCall2" class="form-label peers peer-greed js-sb ai-c"> | |||||
| <span class="peer peer-greed">Call John for Dinner</span> | |||||
| </label> | |||||
| </div> | |||||
| </div> | |||||
| <button type="submit" class="btn btn-primary">Sign in</button> | |||||
| </form> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <div class="masonry-item col-md-6"> | |||||
| <div class="bgc-white p-20 bd"> | |||||
| <h6 class="c-grey-900">Horizontal Form</h6> | |||||
| <div class="mT-30"> | |||||
| <form> | |||||
| <div class="mb-3 row"> | |||||
| <label for="inputEmail3" class="form-label col-sm-2 col-form-label">Email</label> | |||||
| <div class="col-sm-10"> | |||||
| <input type="email" class="form-control" id="inputEmail3" placeholder="Email"> | |||||
| </div> | |||||
| </div> | |||||
| <div class="mb-3 row"> | |||||
| <label for="inputPassword3" class="form-label col-sm-2 col-form-label">Password</label> | |||||
| <div class="col-sm-10"> | |||||
| <input type="password" class="form-control" id="inputPassword3" placeholder="Password"> | |||||
| </div> | |||||
| </div> | |||||
| <fieldset class="mb-3"> | |||||
| <div class="row"> | |||||
| <legend class="col-form-legend col-sm-2">Radios</legend> | |||||
| <div class="col-sm-10"> | |||||
| <div class="form-check"> | |||||
| <label class="form-check-label form-label"> | |||||
| <input class="form-check-input" type="radio" name="gridRadios" id="gridRadios1" value="option1" checked> | |||||
| Option one is this and that—be sure to include why it's great | |||||
| </label> | |||||
| </div> | |||||
| <div class="form-check"> | |||||
| <label class="form-check-label form-label"> | |||||
| <input class="form-check-input" type="radio" name="gridRadios" id="gridRadios2" value="option2"> | |||||
| Option two can be something else and selecting it will deselect option one | |||||
| </label> | |||||
| </div> | |||||
| <div class="form-check disabled"> | |||||
| <label class="form-check-label form-label"> | |||||
| <input class="form-check-input" type="radio" name="gridRadios" id="gridRadios3" value="option3" disabled> | |||||
| Option three is disabled | |||||
| </label> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </fieldset> | |||||
| <div class="mb-3 row"> | |||||
| <div class="col-sm-2">Checkbox</div> | |||||
| <div class="col-sm-10"> | |||||
| <div class="form-check"> | |||||
| <label class="form-check-label form-label"> | |||||
| <input class="form-check-input" type="checkbox"> Check me out | |||||
| </label> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <div class="mb-3 row"> | |||||
| <div class="col-sm-10"> | |||||
| <button type="submit" class="btn btn-primary">Sign in</button> | |||||
| </div> | |||||
| </div> | |||||
| </form> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <div class="masonry-item col-md-6"> | |||||
| <div class="bgc-white p-20 bd"> | |||||
| <h6 class="c-grey-900">Disabled Forms</h6> | |||||
| <div class="mT-30"> | |||||
| <form> | |||||
| <fieldset disabled> | |||||
| <div class="mb-3"> | |||||
| <label for="disabledTextInput" class="form-label">Disabled input</label> | |||||
| <input type="text" id="disabledTextInput" class="form-control" placeholder="Disabled input"> | |||||
| </div> | |||||
| <div class="mb-3"> | |||||
| <label for="disabledSelect" class="form-label">Disabled select menu</label> | |||||
| <select id="disabledSelect" class="form-control"> | |||||
| <option>Disabled select</option> | |||||
| </select> | |||||
| </div> | |||||
| <div class="form-check"> | |||||
| <label class="form-check-label" class="form-label"> | |||||
| <input class="form-check-input" type="checkbox"> Can't check this | |||||
| </label> | |||||
| </div> | |||||
| <button type="submit" class="btn btn-primary">Submit</button> | |||||
| </fieldset> | |||||
| </form> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <div class="masonry-item col-md-6"> | |||||
| <div class="bgc-white p-20 bd"> | |||||
| <h6 class="c-grey-900">Validation</h6> | |||||
| <div class="mT-30"> | |||||
| <form class="container" id="needs-validation" novalidate> | |||||
| <div class="row"> | |||||
| <div class="col-md-6 mb-3"> | |||||
| <label for="validationCustom01" class="form-label">First name</label> | |||||
| <input type="text" class="form-control" id="validationCustom01" placeholder="First name" value="Mark" required> | |||||
| </div> | |||||
| <div class="col-md-6 mb-3"> | |||||
| <label for="validationCustom02" class="form-label">Last name</label> | |||||
| <input type="text" class="form-control" id="validationCustom02" placeholder="Last name" value="Otto" required> | |||||
| </div> | |||||
| </div> | |||||
| <div class="row"> | |||||
| <div class="col-md-6 mb-3"> | |||||
| <label for="validationCustom03" class="form-label">City</label> | |||||
| <input type="text" class="form-control" id="validationCustom03" placeholder="City" required> | |||||
| <div class="invalid-feedback"> | |||||
| Please provide a valid city. | |||||
| </div> | |||||
| </div> | |||||
| <div class="col-md-3 mb-3"> | |||||
| <label for="validationCustom04" class="form-label">State</label> | |||||
| <input type="text" class="form-control" id="validationCustom04" placeholder="State" required> | |||||
| <div class="invalid-feedback"> | |||||
| Please provide a valid state. | |||||
| </div> | |||||
| </div> | |||||
| <div class="col-md-3 mb-3"> | |||||
| <label for="validationCustom05" class="form-label">Zip</label> | |||||
| <input type="text" class="form-control" id="validationCustom05" placeholder="Zip" required> | |||||
| <div class="invalid-feedback"> | |||||
| Please provide a valid zip. | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| <button class="btn btn-primary" type="submit">Submit form</button> | |||||
| </form> | |||||
| <script> | |||||
| // Example starter JavaScript for disabling form submissions if there are invalid fields | |||||
| (function() { | |||||
| 'use strict'; | |||||
| window.addEventListener('load', function() { | |||||
| var form = document.getElementById('needs-validation'); | |||||
| form.addEventListener('submit', function(event) { | |||||
| if (form.checkValidity() === false) { | |||||
| event.preventDefault(); | |||||
| event.stopPropagation(); | |||||
| } | |||||
| form.classList.add('was-validated'); | |||||
| }, false); | |||||
| }, false); | |||||
| })(); | |||||
| </script> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </div> | |||||
| </main> | |||||
| <!-- ### $App Screen Footer ### --> | |||||
| <footer class="bdT ta-c p-30 lh-0 fsz-sm c-grey-600"> | |||||
| <span>Copyright © 2025 Designed by <a href="https://colorlib.com" target='_blank' title="Colorlib">Colorlib</a>. All rights reserved.</span> | |||||
| </footer> | |||||
| </div> | |||||
| </div> | |||||
| </body> | |||||
| </html> | |||||
| @ -1,757 +0,0 @@ | |||||
| /** | |||||
| * Modern Adminator Application with TypeScript | |||||
| * Main application entry point with enhanced mobile support and type safety | |||||
| */ | |||||
| import { DOM } from './utils/dom'; | |||||
| import { ThemeManager } from './utils/theme'; | |||||
| import { Sidebar } from './components/Sidebar'; | |||||
| import { ChartComponent } from './components/Chart'; | |||||
| import UIComponents from './ui'; | |||||
| import DataTable from './datatable'; | |||||
| import DatePicker from './datepicker'; | |||||
| import VectorMaps from './vectorMaps'; | |||||
| import type { ComponentInterface } from '../../types'; | |||||
| // Import styles | |||||
| import '../styles/index.scss'; | |||||
| // Import other modules that don't need immediate modernization | |||||
| import './fullcalendar'; | |||||
| import './masonry'; | |||||
| import './popover'; | |||||
| import './scrollbar'; | |||||
| import './search'; | |||||
| import './skycons'; | |||||
| import './chat'; | |||||
| import './email'; | |||||
| import './googleMaps'; | |||||
| // Type definitions for the application | |||||
| export interface AdminatorAppOptions { | |||||
| autoInit?: boolean; | |||||
| theme?: 'light' | 'dark' | 'auto'; | |||||
| mobile?: { | |||||
| enhanced?: boolean; | |||||
| fullWidthSearch?: boolean; | |||||
| disableDropdowns?: boolean; | |||||
| }; | |||||
| debug?: boolean; | |||||
| } | |||||
| export interface AdminatorAppState { | |||||
| isInitialized: boolean; | |||||
| isMobile: boolean; | |||||
| currentTheme: 'light' | 'dark' | 'auto'; | |||||
| components: Map<string, ComponentInterface>; | |||||
| } | |||||
| export interface AdminatorAppEvents { | |||||
| ready: CustomEvent<{ app: AdminatorApp }>; | |||||
| themeChanged: CustomEvent<{ theme: string; previousTheme: string }>; | |||||
| mobileStateChanged: CustomEvent<{ isMobile: boolean }>; | |||||
| componentAdded: CustomEvent<{ name: string; component: ComponentInterface }>; | |||||
| componentRemoved: CustomEvent<{ name: string }>; | |||||
| } | |||||
| declare global { | |||||
| interface Window { | |||||
| AdminatorApp?: AdminatorApp; | |||||
| } | |||||
| } | |||||
| export class AdminatorApp { | |||||
| public options: AdminatorAppOptions; | |||||
| public state: AdminatorAppState; | |||||
| private resizeTimeout: number | null = null; | |||||
| private eventHandlers: Map<string, EventListener> = new Map(); | |||||
| private themeManager: typeof ThemeManager; | |||||
| constructor(options: AdminatorAppOptions = {}) { | |||||
| this.options = { | |||||
| autoInit: true, | |||||
| theme: 'auto', | |||||
| mobile: { | |||||
| enhanced: true, | |||||
| fullWidthSearch: true, | |||||
| disableDropdowns: false, | |||||
| }, | |||||
| debug: false, | |||||
| ...options, | |||||
| }; | |||||
| this.themeManager = ThemeManager; | |||||
| this.state = { | |||||
| isInitialized: false, | |||||
| isMobile: this.checkMobileState(), | |||||
| currentTheme: 'light', | |||||
| components: new Map(), | |||||
| }; | |||||
| if (this.options.autoInit) { | |||||
| // Initialize when DOM is ready | |||||
| DOM.ready(() => { | |||||
| this.init(); | |||||
| }); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Initialize the application | |||||
| */ | |||||
| public init(): void { | |||||
| if (this.state.isInitialized) return; | |||||
| this.log('Initializing Adminator App...'); | |||||
| try { | |||||
| // Initialize core components | |||||
| this.initSidebar(); | |||||
| this.initCharts(); | |||||
| this.initDataTables(); | |||||
| this.initDatePickers(); | |||||
| this.initUIComponents(); | |||||
| this.initVectorMaps(); | |||||
| this.initTheme(); | |||||
| this.initMobileEnhancements(); | |||||
| // Setup global event listeners | |||||
| this.setupGlobalEvents(); | |||||
| this.state.isInitialized = true; | |||||
| this.log('Adminator App initialized successfully'); | |||||
| // Dispatch custom event for other scripts | |||||
| this.dispatchEvent('ready', { app: this }); | |||||
| } catch (error) { | |||||
| console.error('Error initializing Adminator App:', error); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Initialize Sidebar component | |||||
| */ | |||||
| private initSidebar(): void { | |||||
| if (DOM.exists('.sidebar')) { | |||||
| const sidebar = new Sidebar(); | |||||
| this.addComponent('sidebar', sidebar); | |||||
| this.log('Sidebar component initialized'); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Initialize Chart components | |||||
| */ | |||||
| private initCharts(): void { | |||||
| // Check if we have any chart elements | |||||
| const hasCharts = DOM.exists('#sparklinedash') || | |||||
| DOM.exists('.sparkline') || | |||||
| DOM.exists('.sparkbar') || | |||||
| DOM.exists('.sparktri') || | |||||
| DOM.exists('.sparkdisc') || | |||||
| DOM.exists('.sparkbull') || | |||||
| DOM.exists('.sparkbox') || | |||||
| DOM.exists('.easy-pie-chart') || | |||||
| DOM.exists('#line-chart') || | |||||
| DOM.exists('#area-chart') || | |||||
| DOM.exists('#scatter-chart') || | |||||
| DOM.exists('#bar-chart'); | |||||
| if (hasCharts) { | |||||
| const charts = new ChartComponent(); | |||||
| this.addComponent('charts', charts); | |||||
| this.log('Chart components initialized'); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Initialize DataTables | |||||
| */ | |||||
| private initDataTables(): void { | |||||
| const dataTableElement = DOM.select('#dataTable'); | |||||
| if (dataTableElement) { | |||||
| DataTable.init(); | |||||
| this.log('DataTable initialized'); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Initialize Date Pickers | |||||
| */ | |||||
| private initDatePickers(): void { | |||||
| const startDatePickers = DOM.selectAll('.start-date'); | |||||
| const endDatePickers = DOM.selectAll('.end-date'); | |||||
| if (startDatePickers.length > 0 || endDatePickers.length > 0) { | |||||
| DatePicker.init(); | |||||
| this.log('Date pickers initialized'); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Initialize UI Components | |||||
| */ | |||||
| private initUIComponents(): void { | |||||
| UIComponents.init(); | |||||
| this.log('UI components initialized'); | |||||
| } | |||||
| /** | |||||
| * Initialize Vector Maps | |||||
| */ | |||||
| private initVectorMaps(): void { | |||||
| if (DOM.exists('#world-map-marker')) { | |||||
| VectorMaps.init(); | |||||
| this.log('Vector maps initialized'); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Initialize theme system with toggle | |||||
| */ | |||||
| private initTheme(): void { | |||||
| this.log('Initializing theme system...'); | |||||
| // Initialize theme system first | |||||
| this.themeManager.init(); | |||||
| this.state.currentTheme = this.themeManager.current(); | |||||
| // Inject theme toggle if missing | |||||
| setTimeout(() => { | |||||
| this.injectThemeToggle(); | |||||
| }, 100); | |||||
| } | |||||
| /** | |||||
| * Inject theme toggle button | |||||
| */ | |||||
| private injectThemeToggle(): void { | |||||
| const navRight = DOM.select('.nav-right'); | |||||
| if (navRight && !DOM.exists('#theme-toggle')) { | |||||
| const li = document.createElement('li'); | |||||
| li.className = 'theme-toggle d-flex ai-c'; | |||||
| li.innerHTML = ` | |||||
| <div class="form-check form-switch d-flex ai-c" style="margin: 0; padding: 0;"> | |||||
| <label class="form-check-label me-2 text-nowrap c-grey-700" for="theme-toggle" style="font-size: 12px; margin-right: 8px;"> | |||||
| <i class="ti-sun" style="margin-right: 4px;"></i><span class="theme-label">Light</span> | |||||
| </label> | |||||
| <input class="form-check-input" type="checkbox" id="theme-toggle" style="margin: 0;"> | |||||
| <label class="form-check-label ms-2 text-nowrap c-grey-700" for="theme-toggle" style="font-size: 12px; margin-left: 8px;"> | |||||
| <span class="theme-label">Dark</span><i class="ti-moon" style="margin-left: 4px;"></i> | |||||
| </label> | |||||
| </div> | |||||
| `; | |||||
| // Insert before user dropdown (last item) | |||||
| const lastItem = navRight.querySelector('li:last-child'); | |||||
| if (lastItem && lastItem.parentNode === navRight) { | |||||
| navRight.insertBefore(li, lastItem); | |||||
| } else { | |||||
| navRight.appendChild(li); | |||||
| } | |||||
| this.setupThemeToggle(); | |||||
| this.log('Theme toggle injected'); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Setup theme toggle functionality | |||||
| */ | |||||
| private setupThemeToggle(): void { | |||||
| const toggle = DOM.select('#theme-toggle') as HTMLInputElement; | |||||
| if (!toggle) return; | |||||
| // Set initial state | |||||
| toggle.checked = this.state.currentTheme === 'dark'; | |||||
| // Add change handler | |||||
| const changeHandler = (): void => { | |||||
| const newTheme = toggle.checked ? 'dark' : 'light'; | |||||
| const previousTheme = this.state.currentTheme; | |||||
| this.themeManager.apply(newTheme); | |||||
| this.state.currentTheme = newTheme; | |||||
| this.dispatchEvent('themeChanged', { theme: newTheme, previousTheme }); | |||||
| }; | |||||
| DOM.on(toggle, 'change', changeHandler); | |||||
| this.eventHandlers.set('theme-toggle', changeHandler); | |||||
| // Listen for theme changes from other sources | |||||
| const themeChangeHandler = (event: CustomEvent): void => { | |||||
| const newTheme = event.detail.theme; | |||||
| toggle.checked = newTheme === 'dark'; | |||||
| this.state.currentTheme = newTheme; | |||||
| // Update charts when theme changes | |||||
| const charts = this.getComponent('charts') as ChartComponent; | |||||
| if (charts && typeof charts.redrawCharts === 'function') { | |||||
| charts.redrawCharts(); | |||||
| } | |||||
| }; | |||||
| window.addEventListener('adminator:themeChanged', themeChangeHandler as EventListener); | |||||
| this.eventHandlers.set('theme-change', themeChangeHandler as EventListener); | |||||
| } | |||||
| /** | |||||
| * Initialize mobile-specific enhancements | |||||
| */ | |||||
| private initMobileEnhancements(): void { | |||||
| if (!this.options.mobile?.enhanced) return; | |||||
| this.log('Initializing mobile enhancements...'); | |||||
| this.enhanceMobileDropdowns(); | |||||
| this.enhanceMobileSearch(); | |||||
| // Prevent horizontal scroll on mobile | |||||
| if (this.state.isMobile) { | |||||
| document.body.style.overflowX = 'hidden'; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Setup global event listeners | |||||
| */ | |||||
| private setupGlobalEvents(): void { | |||||
| // Global click handler | |||||
| const globalClickHandler = (event: Event): void => { | |||||
| this.handleGlobalClick(event); | |||||
| }; | |||||
| DOM.on(document, 'click', globalClickHandler); | |||||
| this.eventHandlers.set('global-click', globalClickHandler); | |||||
| // Window resize handler with debouncing | |||||
| const resizeHandler = (): void => { | |||||
| if (this.resizeTimeout) { | |||||
| clearTimeout(this.resizeTimeout); | |||||
| } | |||||
| this.resizeTimeout = window.setTimeout(() => { | |||||
| this.handleResize(); | |||||
| }, 250); | |||||
| }; | |||||
| DOM.on(window, 'resize', resizeHandler); | |||||
| this.eventHandlers.set('resize', resizeHandler); | |||||
| this.log('Global event listeners set up'); | |||||
| } | |||||
| /** | |||||
| * Handle window resize events | |||||
| */ | |||||
| private handleResize(): void { | |||||
| const wasMobile = this.state.isMobile; | |||||
| this.state.isMobile = this.checkMobileState(); | |||||
| if (wasMobile !== this.state.isMobile) { | |||||
| this.dispatchEvent('mobileStateChanged', { isMobile: this.state.isMobile }); | |||||
| } | |||||
| this.log('Window resized, updating mobile features'); | |||||
| // Close all mobile-specific overlays when switching to desktop | |||||
| if (!this.state.isMobile) { | |||||
| document.body.style.overflow = ''; | |||||
| document.body.style.overflowX = ''; | |||||
| // Close dropdowns | |||||
| const dropdowns = DOM.selectAll('.nav-right .dropdown'); | |||||
| dropdowns.forEach(dropdown => { | |||||
| dropdown.classList.remove('show'); | |||||
| const menu = dropdown.querySelector('.dropdown-menu'); | |||||
| if (menu) menu.classList.remove('show'); | |||||
| }); | |||||
| // Close search | |||||
| this.closeSearch(); | |||||
| } else { | |||||
| // Re-enable mobile overflow protection | |||||
| document.body.style.overflowX = 'hidden'; | |||||
| } | |||||
| // Re-apply mobile enhancements | |||||
| if (this.options.mobile?.enhanced) { | |||||
| this.enhanceMobileDropdowns(); | |||||
| this.enhanceMobileSearch(); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Handle global click events | |||||
| */ | |||||
| private handleGlobalClick(event: Event): void { | |||||
| const target = event.target as HTMLElement; | |||||
| // Close mobile dropdowns when clicking outside | |||||
| if (!target.closest('.dropdown')) { | |||||
| const dropdowns = DOM.selectAll('.nav-right .dropdown'); | |||||
| dropdowns.forEach(dropdown => { | |||||
| dropdown.classList.remove('show'); | |||||
| const menu = dropdown.querySelector('.dropdown-menu'); | |||||
| if (menu) menu.classList.remove('show'); | |||||
| }); | |||||
| document.body.style.overflow = ''; | |||||
| } | |||||
| // Close search when clicking outside | |||||
| if (!target.closest('.search-box') && !target.closest('.search-input')) { | |||||
| this.closeSearch(); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Check if we're on a mobile device | |||||
| */ | |||||
| private checkMobileState(): boolean { | |||||
| return window.innerWidth <= 768; | |||||
| } | |||||
| /** | |||||
| * Enhanced mobile dropdown handling | |||||
| */ | |||||
| private enhanceMobileDropdowns(): void { | |||||
| if (!this.state.isMobile || this.options.mobile?.disableDropdowns) return; | |||||
| const dropdowns = DOM.selectAll('.nav-right .dropdown'); | |||||
| dropdowns.forEach(dropdown => { | |||||
| const toggle = dropdown.querySelector('.dropdown-toggle') as HTMLElement; | |||||
| const menu = dropdown.querySelector('.dropdown-menu') as HTMLElement; | |||||
| if (toggle && menu) { | |||||
| // Remove existing listeners to prevent duplicates | |||||
| const newToggle = toggle.cloneNode(true) as HTMLElement; | |||||
| toggle.replaceWith(newToggle); | |||||
| // Add click functionality for mobile dropdowns | |||||
| DOM.on(newToggle, 'click', (e: Event) => { | |||||
| e.preventDefault(); | |||||
| e.stopPropagation(); | |||||
| // Close search if open | |||||
| this.closeSearch(); | |||||
| // Close other dropdowns first | |||||
| dropdowns.forEach(otherDropdown => { | |||||
| if (otherDropdown !== dropdown) { | |||||
| otherDropdown.classList.remove('show'); | |||||
| const otherMenu = otherDropdown.querySelector('.dropdown-menu'); | |||||
| if (otherMenu) otherMenu.classList.remove('show'); | |||||
| } | |||||
| }); | |||||
| // Toggle current dropdown | |||||
| const isOpen = dropdown.classList.contains('show'); | |||||
| if (isOpen) { | |||||
| dropdown.classList.remove('show'); | |||||
| menu.classList.remove('show'); | |||||
| document.body.style.overflow = ''; | |||||
| document.body.classList.remove('mobile-menu-open'); | |||||
| } else { | |||||
| dropdown.classList.add('show'); | |||||
| menu.classList.add('show'); | |||||
| document.body.style.overflow = 'hidden'; | |||||
| document.body.classList.add('mobile-menu-open'); | |||||
| } | |||||
| }); | |||||
| // Enhanced mobile close button functionality | |||||
| DOM.on(menu, 'click', (e: Event) => { | |||||
| const rect = menu.getBoundingClientRect(); | |||||
| const clickY = (e as MouseEvent).clientY - rect.top; | |||||
| // If clicked in top 50px (close button area) | |||||
| if (clickY <= 50) { | |||||
| dropdown.classList.remove('show'); | |||||
| menu.classList.remove('show'); | |||||
| document.body.style.overflow = ''; | |||||
| document.body.classList.remove('mobile-menu-open'); | |||||
| e.preventDefault(); | |||||
| e.stopPropagation(); | |||||
| } | |||||
| }); | |||||
| } | |||||
| }); | |||||
| // Close dropdowns on escape key | |||||
| const escapeHandler = (e: Event): void => { | |||||
| const keyEvent = e as KeyboardEvent; | |||||
| if (keyEvent.key === 'Escape') { | |||||
| dropdowns.forEach(dropdown => { | |||||
| dropdown.classList.remove('show'); | |||||
| const menu = dropdown.querySelector('.dropdown-menu'); | |||||
| if (menu) menu.classList.remove('show'); | |||||
| }); | |||||
| document.body.style.overflow = ''; | |||||
| document.body.classList.remove('mobile-menu-open'); | |||||
| } | |||||
| }; | |||||
| DOM.on(document, 'keydown', escapeHandler); | |||||
| } | |||||
| /** | |||||
| * Enhanced mobile search handling | |||||
| */ | |||||
| private enhanceMobileSearch(): void { | |||||
| if (!this.options.mobile?.fullWidthSearch) return; | |||||
| const searchBox = DOM.select('.search-box') as HTMLElement; | |||||
| const searchInput = DOM.select('.search-input') as HTMLElement; | |||||
| if (searchBox && searchInput) { | |||||
| const searchToggle = searchBox.querySelector('a') as HTMLAnchorElement; | |||||
| const searchField = searchInput.querySelector('input') as HTMLInputElement; | |||||
| if (searchToggle && searchField) { | |||||
| // Remove existing listeners to prevent duplication | |||||
| const newSearchToggle = searchToggle.cloneNode(true) as HTMLAnchorElement; | |||||
| searchToggle.replaceWith(newSearchToggle); | |||||
| DOM.on(newSearchToggle, 'click', (e: Event) => { | |||||
| e.preventDefault(); | |||||
| e.stopPropagation(); | |||||
| // Close any open dropdowns first | |||||
| const dropdowns = DOM.selectAll('.nav-right .dropdown'); | |||||
| dropdowns.forEach(dropdown => { | |||||
| dropdown.classList.remove('show'); | |||||
| const menu = dropdown.querySelector('.dropdown-menu'); | |||||
| if (menu) menu.classList.remove('show'); | |||||
| }); | |||||
| // Toggle search state | |||||
| const isActive = searchInput.classList.contains('active'); | |||||
| const searchIcon = newSearchToggle.querySelector('i') as HTMLElement; | |||||
| if (isActive) { | |||||
| this.closeSearch(); | |||||
| } else { | |||||
| this.openSearch(searchField, searchIcon); | |||||
| } | |||||
| }); | |||||
| // Handle search input | |||||
| DOM.on(searchField, 'keypress', (e: Event) => { | |||||
| const keyEvent = e as KeyboardEvent; | |||||
| if (keyEvent.key === 'Enter') { | |||||
| keyEvent.preventDefault(); | |||||
| const query = searchField.value.trim(); | |||||
| if (query) { | |||||
| this.handleSearch(query); | |||||
| } | |||||
| } | |||||
| }); | |||||
| } | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Open search interface | |||||
| */ | |||||
| private openSearch(searchField: HTMLInputElement, searchIcon: HTMLElement): void { | |||||
| const searchInput = DOM.select('.search-input') as HTMLElement; | |||||
| searchInput.classList.add('active'); | |||||
| document.body.classList.add('search-open'); | |||||
| // Change icon to close | |||||
| if (searchIcon) { | |||||
| searchIcon.className = 'ti-close'; | |||||
| } | |||||
| // Focus the input after a short delay | |||||
| setTimeout(() => { | |||||
| searchField.focus(); | |||||
| }, 100); | |||||
| } | |||||
| /** | |||||
| * Close search interface | |||||
| */ | |||||
| private closeSearch(): void { | |||||
| const searchBox = DOM.select('.search-box') as HTMLElement; | |||||
| const searchInput = DOM.select('.search-input') as HTMLElement; | |||||
| if (searchBox && searchInput) { | |||||
| searchInput.classList.remove('active'); | |||||
| document.body.classList.remove('search-open'); | |||||
| document.body.classList.remove('mobile-menu-open'); | |||||
| // Reset icon | |||||
| const searchIcon = searchBox.querySelector('i') as HTMLElement; | |||||
| if (searchIcon) { | |||||
| searchIcon.className = 'ti-search'; | |||||
| } | |||||
| // Clear input | |||||
| const searchField = searchInput.querySelector('input') as HTMLInputElement; | |||||
| if (searchField) { | |||||
| searchField.value = ''; | |||||
| searchField.blur(); | |||||
| } | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Handle search query | |||||
| */ | |||||
| private handleSearch(query: string): void { | |||||
| this.log(`Search query: ${query}`); | |||||
| // Implement your search logic here | |||||
| // For demo, close search after "searching" | |||||
| this.closeSearch(); | |||||
| } | |||||
| /** | |||||
| * Add component to the application | |||||
| */ | |||||
| public addComponent(name: string, component: ComponentInterface): void { | |||||
| this.state.components.set(name, component); | |||||
| this.dispatchEvent('componentAdded', { name, component }); | |||||
| this.log(`Component added: ${name}`); | |||||
| } | |||||
| /** | |||||
| * Remove component from the application | |||||
| */ | |||||
| public removeComponent(name: string): void { | |||||
| const component = this.state.components.get(name); | |||||
| if (component) { | |||||
| if (typeof component.destroy === 'function') { | |||||
| component.destroy(); | |||||
| } | |||||
| this.state.components.delete(name); | |||||
| this.dispatchEvent('componentRemoved', { name }); | |||||
| this.log(`Component removed: ${name}`); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Get a component by name | |||||
| */ | |||||
| public getComponent(name: string): ComponentInterface | undefined { | |||||
| return this.state.components.get(name); | |||||
| } | |||||
| /** | |||||
| * Get all components | |||||
| */ | |||||
| public getComponents(): Map<string, ComponentInterface> { | |||||
| return new Map(this.state.components); | |||||
| } | |||||
| /** | |||||
| * Check if app is ready | |||||
| */ | |||||
| public isReady(): boolean { | |||||
| return this.state.isInitialized; | |||||
| } | |||||
| /** | |||||
| * Get current application state | |||||
| */ | |||||
| public getState(): Readonly<AdminatorAppState> { | |||||
| return { | |||||
| ...this.state, | |||||
| components: new Map(this.state.components), | |||||
| }; | |||||
| } | |||||
| /** | |||||
| * Update application options | |||||
| */ | |||||
| public updateOptions(newOptions: Partial<AdminatorAppOptions>): void { | |||||
| this.options = { ...this.options, ...newOptions }; | |||||
| this.log('Options updated'); | |||||
| } | |||||
| /** | |||||
| * Dispatch custom event | |||||
| */ | |||||
| private dispatchEvent<T extends keyof AdminatorAppEvents>( | |||||
| type: T, | |||||
| detail: AdminatorAppEvents[T]['detail'] | |||||
| ): void { | |||||
| const event = new CustomEvent(`adminator:${type}`, { | |||||
| detail, | |||||
| bubbles: true, | |||||
| }); | |||||
| window.dispatchEvent(event); | |||||
| } | |||||
| /** | |||||
| * Log message if debugging is enabled | |||||
| */ | |||||
| private log(message: string): void { | |||||
| if (this.options.debug) { | |||||
| console.log(`[AdminatorApp] ${message}`); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Destroy the application | |||||
| */ | |||||
| public destroy(): void { | |||||
| this.log('Destroying Adminator App'); | |||||
| // Destroy all components | |||||
| this.state.components.forEach((component, name) => { | |||||
| if (typeof component.destroy === 'function') { | |||||
| component.destroy(); | |||||
| } | |||||
| this.log(`Component destroyed: ${name}`); | |||||
| }); | |||||
| // Remove event listeners | |||||
| this.eventHandlers.forEach((_, name) => { | |||||
| // Note: We'd need to track which element each handler was attached to | |||||
| // For now, we'll rely on the browser's garbage collection | |||||
| this.log(`Event handler removed: ${name}`); | |||||
| }); | |||||
| // Clear state | |||||
| this.state.components.clear(); | |||||
| this.eventHandlers.clear(); | |||||
| this.state.isInitialized = false; | |||||
| // Clear timeout | |||||
| if (this.resizeTimeout) { | |||||
| clearTimeout(this.resizeTimeout); | |||||
| this.resizeTimeout = null; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Refresh/reinitialize the application | |||||
| */ | |||||
| public refresh(): void { | |||||
| this.log('Refreshing Adminator App'); | |||||
| if (this.state.isInitialized) { | |||||
| this.destroy(); | |||||
| } | |||||
| setTimeout(() => { | |||||
| this.init(); | |||||
| }, 100); | |||||
| } | |||||
| } | |||||
| // Initialize the application | |||||
| const app = new AdminatorApp({ | |||||
| debug: process.env.NODE_ENV === 'development', | |||||
| }); | |||||
| // Make app globally available for debugging | |||||
| window.AdminatorApp = app; | |||||
| // Export for module usage | |||||
| export default app; | |||||
| @ -1,388 +0,0 @@ | |||||
| /** | |||||
| * Modern Sidebar Component with TypeScript | |||||
| * Replaces jQuery-based sidebar functionality with vanilla JavaScript | |||||
| */ | |||||
| import type { ComponentInterface, SidebarOptions, SidebarState, AnimationOptions } from '../../../types'; | |||||
| export interface SidebarEventDetail { | |||||
| collapsed: boolean; | |||||
| } | |||||
| export interface SidebarToggleEvent extends CustomEvent { | |||||
| detail: SidebarEventDetail; | |||||
| } | |||||
| declare global { | |||||
| interface Window { | |||||
| EVENT?: Event; | |||||
| } | |||||
| } | |||||
| export class Sidebar implements ComponentInterface { | |||||
| public name: string = 'Sidebar'; | |||||
| public element: HTMLElement; | |||||
| public options: SidebarOptions; | |||||
| public isInitialized: boolean = false; | |||||
| private sidebar: HTMLElement | null; | |||||
| private sidebarMenu: HTMLElement | null; | |||||
| private sidebarToggleLinks: NodeListOf<HTMLAnchorElement>; | |||||
| private sidebarToggleById: HTMLElement | null; | |||||
| private app: HTMLElement | null; | |||||
| private state: SidebarState; | |||||
| constructor(element?: HTMLElement, options: SidebarOptions = {}) { | |||||
| this.element = element || document.body; | |||||
| this.options = { | |||||
| breakpoint: 768, | |||||
| collapsible: true, | |||||
| autoHide: true, | |||||
| animation: true, | |||||
| animationDuration: 200, | |||||
| ...options, | |||||
| }; | |||||
| this.sidebar = document.querySelector('.sidebar'); | |||||
| this.sidebarMenu = document.querySelector('.sidebar .sidebar-menu'); | |||||
| this.sidebarToggleLinks = document.querySelectorAll('.sidebar-toggle a'); | |||||
| this.sidebarToggleById = document.querySelector('#sidebar-toggle'); | |||||
| this.app = document.querySelector('.app'); | |||||
| this.state = { | |||||
| isCollapsed: false, | |||||
| isMobile: false, | |||||
| activeMenu: null, | |||||
| }; | |||||
| this.init(); | |||||
| } | |||||
| /** | |||||
| * Initialize the sidebar component | |||||
| */ | |||||
| public init(): void { | |||||
| if (!this.sidebar || !this.sidebarMenu) { | |||||
| console.warn('Sidebar: Required elements not found'); | |||||
| return; | |||||
| } | |||||
| this.setupMenuToggle(); | |||||
| this.setupSidebarToggle(); | |||||
| this.setActiveLink(); | |||||
| this.handleResize(); | |||||
| this.setupEventListeners(); | |||||
| this.isInitialized = true; | |||||
| } | |||||
| /** | |||||
| * Destroy the sidebar component | |||||
| */ | |||||
| public destroy(): void { | |||||
| this.removeEventListeners(); | |||||
| this.isInitialized = false; | |||||
| } | |||||
| /** | |||||
| * Setup dropdown menu functionality | |||||
| */ | |||||
| private setupMenuToggle(): void { | |||||
| if (!this.sidebarMenu) return; | |||||
| const menuLinks = this.sidebarMenu.querySelectorAll('li a'); | |||||
| menuLinks.forEach(link => { | |||||
| link.addEventListener('click', this.handleMenuClick.bind(this)); | |||||
| }); | |||||
| } | |||||
| /** | |||||
| * Handle menu item click | |||||
| */ | |||||
| private handleMenuClick(e: Event): void { | |||||
| const link = e.target as HTMLAnchorElement; | |||||
| const listItem = link.parentElement as HTMLLIElement; | |||||
| const dropdownMenu = listItem?.querySelector('.dropdown-menu') as HTMLElement; | |||||
| // If this is a regular navigation link (not dropdown), allow normal navigation | |||||
| if (!dropdownMenu) { | |||||
| return; | |||||
| } | |||||
| // Only prevent default for dropdown toggles | |||||
| e.preventDefault(); | |||||
| if (listItem.classList.contains('open')) { | |||||
| this.closeDropdown(listItem, dropdownMenu); | |||||
| } else { | |||||
| this.closeAllDropdowns(); | |||||
| this.openDropdown(listItem, dropdownMenu); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Open dropdown with smooth animation | |||||
| */ | |||||
| private openDropdown(listItem: HTMLLIElement, dropdownMenu: HTMLElement): void { | |||||
| listItem.classList.add('open'); | |||||
| dropdownMenu.style.display = 'block'; | |||||
| dropdownMenu.style.height = '0px'; | |||||
| dropdownMenu.style.overflow = 'hidden'; | |||||
| // Get the natural height | |||||
| const height = dropdownMenu.scrollHeight; | |||||
| // Animate to full height | |||||
| const animation = dropdownMenu.animate([ | |||||
| { height: '0px' }, | |||||
| { height: `${height}px` }, | |||||
| ], { | |||||
| duration: this.options.animationDuration, | |||||
| easing: 'ease-out', | |||||
| }); | |||||
| animation.onfinish = (): void => { | |||||
| dropdownMenu.style.height = 'auto'; | |||||
| dropdownMenu.style.overflow = 'visible'; | |||||
| }; | |||||
| } | |||||
| /** | |||||
| * Close dropdown with smooth animation | |||||
| */ | |||||
| private closeDropdown(listItem: HTMLLIElement, dropdownMenu: HTMLElement): void { | |||||
| const height = dropdownMenu.scrollHeight; | |||||
| dropdownMenu.style.height = `${height}px`; | |||||
| dropdownMenu.style.overflow = 'hidden'; | |||||
| const animation = dropdownMenu.animate([ | |||||
| { height: `${height}px` }, | |||||
| { height: '0px' }, | |||||
| ], { | |||||
| duration: this.options.animationDuration, | |||||
| easing: 'ease-in', | |||||
| }); | |||||
| animation.onfinish = (): void => { | |||||
| listItem.classList.remove('open'); | |||||
| dropdownMenu.style.display = 'none'; | |||||
| dropdownMenu.style.height = ''; | |||||
| dropdownMenu.style.overflow = ''; | |||||
| }; | |||||
| } | |||||
| /** | |||||
| * Close all open dropdowns | |||||
| */ | |||||
| private closeAllDropdowns(): void { | |||||
| if (!this.sidebarMenu) return; | |||||
| const openItems = this.sidebarMenu.querySelectorAll('li.open'); | |||||
| openItems.forEach(item => { | |||||
| const dropdownMenu = item.querySelector('.dropdown-menu') as HTMLElement; | |||||
| if (dropdownMenu) { | |||||
| this.closeDropdown(item as HTMLLIElement, dropdownMenu); | |||||
| } | |||||
| // Also remove the has-active-child class | |||||
| item.classList.remove('has-active-child'); | |||||
| }); | |||||
| } | |||||
| /** | |||||
| * Setup sidebar toggle functionality | |||||
| */ | |||||
| private setupSidebarToggle(): void { | |||||
| // Handle mobile sidebar toggle links (inside .sidebar-toggle divs) | |||||
| this.sidebarToggleLinks.forEach(link => { | |||||
| if (link && this.app) { | |||||
| link.addEventListener('click', this.handleSidebarToggle.bind(this)); | |||||
| } | |||||
| }); | |||||
| // Handle the main topbar sidebar toggle | |||||
| if (this.sidebarToggleById && this.app) { | |||||
| this.sidebarToggleById.addEventListener('click', this.handleSidebarToggle.bind(this)); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Handle sidebar toggle click | |||||
| */ | |||||
| private handleSidebarToggle(e: Event): void { | |||||
| e.preventDefault(); | |||||
| this.toggleSidebar(); | |||||
| } | |||||
| /** | |||||
| * Toggle sidebar and handle resize events properly | |||||
| */ | |||||
| private toggleSidebar(): void { | |||||
| if (!this.app) return; | |||||
| const wasCollapsed = this.state.isCollapsed; | |||||
| this.state.isCollapsed = !wasCollapsed; | |||||
| this.app.classList.toggle('is-collapsed'); | |||||
| // Dispatch custom event with proper typing | |||||
| setTimeout(() => { | |||||
| const event: SidebarToggleEvent = new CustomEvent('sidebar:toggle', { | |||||
| detail: { collapsed: this.state.isCollapsed }, | |||||
| }) as SidebarToggleEvent; | |||||
| window.dispatchEvent(event); | |||||
| // Still trigger resize for masonry but with a specific check | |||||
| if (window.EVENT) { | |||||
| window.dispatchEvent(window.EVENT); | |||||
| } | |||||
| }, this.options.animationDuration || 300); | |||||
| } | |||||
| /** | |||||
| * Set active link based on current URL | |||||
| */ | |||||
| private setActiveLink(): void { | |||||
| if (!this.sidebar) return; | |||||
| // Remove active class from all nav items (including dropdown items) | |||||
| const allNavItems = this.sidebar.querySelectorAll('.nav-item'); | |||||
| allNavItems.forEach(item => { | |||||
| item.classList.remove('actived'); | |||||
| }); | |||||
| // Close all dropdowns first | |||||
| this.closeAllDropdowns(); | |||||
| // Get current page filename | |||||
| const currentPath = window.location.pathname; | |||||
| const currentPage = currentPath.split('/').pop() || 'index.html'; | |||||
| // Find and activate the correct nav item | |||||
| const allLinks = this.sidebar.querySelectorAll('a[href]'); | |||||
| allLinks.forEach(link => { | |||||
| const href = link.getAttribute('href'); | |||||
| if (!href || href === 'javascript:void(0);' || href === 'javascript:void(0)') return; | |||||
| // Extract filename from href | |||||
| const linkPage = href.split('/').pop(); | |||||
| if (linkPage === currentPage) { | |||||
| const navItem = link.closest('.nav-item') as HTMLElement; | |||||
| if (navItem) { | |||||
| navItem.classList.add('actived'); | |||||
| this.state.activeMenu = linkPage || null; | |||||
| // If this is inside a dropdown, handle parent dropdown specially | |||||
| const parentDropdown = navItem.closest('.dropdown-menu') as HTMLElement; | |||||
| if (parentDropdown) { | |||||
| const parentDropdownItem = parentDropdown.closest('.nav-item.dropdown') as HTMLElement; | |||||
| if (parentDropdownItem) { | |||||
| // Open the parent dropdown | |||||
| parentDropdownItem.classList.add('open'); | |||||
| parentDropdown.style.display = 'block'; | |||||
| // Add special styling to indicate parent has active child | |||||
| parentDropdownItem.classList.add('has-active-child'); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| }); | |||||
| } | |||||
| /** | |||||
| * Handle window resize | |||||
| */ | |||||
| private handleResize(): void { | |||||
| this.state.isMobile = window.innerWidth <= (this.options.breakpoint || 768); | |||||
| if (this.options.autoHide && this.state.isMobile) { | |||||
| // Auto-hide logic for mobile | |||||
| this.collapse(); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Setup event listeners | |||||
| */ | |||||
| private setupEventListeners(): void { | |||||
| window.addEventListener('resize', this.handleResize.bind(this)); | |||||
| } | |||||
| /** | |||||
| * Remove event listeners | |||||
| */ | |||||
| private removeEventListeners(): void { | |||||
| window.removeEventListener('resize', this.handleResize.bind(this)); | |||||
| } | |||||
| /** | |||||
| * Public method to refresh active links (useful for SPA navigation) | |||||
| */ | |||||
| public refreshActiveLink(): void { | |||||
| this.setActiveLink(); | |||||
| } | |||||
| /** | |||||
| * Public method to toggle sidebar programmatically | |||||
| */ | |||||
| public toggle(): void { | |||||
| this.toggleSidebar(); | |||||
| } | |||||
| /** | |||||
| * Public method to collapse sidebar | |||||
| */ | |||||
| public collapse(): void { | |||||
| if (!this.app || this.state.isCollapsed) return; | |||||
| this.state.isCollapsed = true; | |||||
| this.app.classList.add('is-collapsed'); | |||||
| } | |||||
| /** | |||||
| * Public method to expand sidebar | |||||
| */ | |||||
| public expand(): void { | |||||
| if (!this.app || !this.state.isCollapsed) return; | |||||
| this.state.isCollapsed = false; | |||||
| this.app.classList.remove('is-collapsed'); | |||||
| } | |||||
| /** | |||||
| * Public method to check if sidebar is collapsed | |||||
| */ | |||||
| public isCollapsed(): boolean { | |||||
| return this.state.isCollapsed; | |||||
| } | |||||
| /** | |||||
| * Get current sidebar state | |||||
| */ | |||||
| public getState(): SidebarState { | |||||
| return { ...this.state }; | |||||
| } | |||||
| /** | |||||
| * Update sidebar options | |||||
| */ | |||||
| public updateOptions(newOptions: Partial<SidebarOptions>): void { | |||||
| this.options = { ...this.options, ...newOptions }; | |||||
| } | |||||
| /** | |||||
| * Get current options | |||||
| */ | |||||
| public getOptions(): SidebarOptions { | |||||
| return { ...this.options }; | |||||
| } | |||||
| } | |||||
| export default Sidebar; | |||||
| @ -1,707 +0,0 @@ | |||||
| /** | |||||
| * DataTable Implementation with TypeScript | |||||
| * Vanilla JavaScript DataTable with sorting, searching, and pagination | |||||
| */ | |||||
| import type { ComponentInterface } from '../../types'; | |||||
| // Type definitions for DataTable | |||||
| export interface DataTableOptions { | |||||
| sortable?: boolean; | |||||
| searchable?: boolean; | |||||
| pagination?: boolean; | |||||
| pageSize?: number; | |||||
| responsive?: boolean; | |||||
| striped?: boolean; | |||||
| bordered?: boolean; | |||||
| hover?: boolean; | |||||
| } | |||||
| export interface DataTableColumn { | |||||
| title: string; | |||||
| data: string | number; | |||||
| sortable?: boolean; | |||||
| searchable?: boolean; | |||||
| width?: string; | |||||
| className?: string; | |||||
| render?: (data: any, row: any[], index: number) => string; | |||||
| } | |||||
| export interface DataTableData { | |||||
| columns: DataTableColumn[]; | |||||
| rows: any[][]; | |||||
| } | |||||
| export interface DataTableState { | |||||
| currentPage: number; | |||||
| sortColumn: number | null; | |||||
| sortDirection: 'asc' | 'desc'; | |||||
| searchQuery: string; | |||||
| filteredData: any[][]; | |||||
| totalPages: number; | |||||
| } | |||||
| export type SortDirection = 'asc' | 'desc'; | |||||
| declare global { | |||||
| interface HTMLTableElement { | |||||
| dataTableInstance?: VanillaDataTable; | |||||
| } | |||||
| } | |||||
| // Enhanced DataTable implementation | |||||
| export class VanillaDataTable implements ComponentInterface { | |||||
| public name: string = 'VanillaDataTable'; | |||||
| public element: HTMLTableElement; | |||||
| public options: DataTableOptions; | |||||
| public isInitialized: boolean = false; | |||||
| private originalData: any[][] = []; | |||||
| private filteredData: any[][] = []; | |||||
| private state: DataTableState; | |||||
| private wrapper: HTMLElement | null = null; | |||||
| private searchInput: HTMLInputElement | null = null; | |||||
| private infoElement: HTMLElement | null = null; | |||||
| private paginationElement: HTMLElement | null = null; | |||||
| constructor(element: HTMLTableElement, options: DataTableOptions = {}) { | |||||
| this.element = element; | |||||
| this.options = { | |||||
| sortable: true, | |||||
| searchable: true, | |||||
| pagination: true, | |||||
| pageSize: 10, | |||||
| responsive: true, | |||||
| striped: true, | |||||
| bordered: true, | |||||
| hover: true, | |||||
| ...options, | |||||
| }; | |||||
| this.state = { | |||||
| currentPage: 1, | |||||
| sortColumn: null, | |||||
| sortDirection: 'asc', | |||||
| searchQuery: '', | |||||
| filteredData: [], | |||||
| totalPages: 0, | |||||
| }; | |||||
| this.init(); | |||||
| } | |||||
| public init(): void { | |||||
| this.extractData(); | |||||
| this.createControls(); | |||||
| this.applyStyles(); | |||||
| this.bindEvents(); | |||||
| this.render(); | |||||
| this.isInitialized = true; | |||||
| } | |||||
| public destroy(): void { | |||||
| if (this.wrapper && this.wrapper.parentNode) { | |||||
| this.wrapper.parentNode.replaceChild(this.element, this.wrapper); | |||||
| } | |||||
| this.isInitialized = false; | |||||
| } | |||||
| private extractData(): void { | |||||
| const tbody = this.element.querySelector('tbody'); | |||||
| if (!tbody) return; | |||||
| const rows = tbody.querySelectorAll('tr'); | |||||
| this.originalData = Array.from(rows).map(row => { | |||||
| const cells = row.querySelectorAll('td'); | |||||
| return Array.from(cells).map(cell => cell.textContent?.trim() || ''); | |||||
| }); | |||||
| this.filteredData = [...this.originalData]; | |||||
| this.state.filteredData = this.filteredData; | |||||
| } | |||||
| private createControls(): void { | |||||
| const wrapper = document.createElement('div'); | |||||
| wrapper.className = 'datatable-wrapper'; | |||||
| // Create top controls container | |||||
| const topControls = document.createElement('div'); | |||||
| topControls.className = 'datatable-top-controls'; | |||||
| // Create search input | |||||
| if (this.options.searchable) { | |||||
| const searchWrapper = document.createElement('div'); | |||||
| searchWrapper.className = 'datatable-search'; | |||||
| const searchLabel = document.createElement('label'); | |||||
| searchLabel.textContent = 'Search: '; | |||||
| this.searchInput = document.createElement('input'); | |||||
| this.searchInput.type = 'text'; | |||||
| this.searchInput.className = 'form-control'; | |||||
| this.searchInput.placeholder = 'Search...'; | |||||
| searchLabel.appendChild(this.searchInput); | |||||
| searchWrapper.appendChild(searchLabel); | |||||
| topControls.appendChild(searchWrapper); | |||||
| } | |||||
| // Create info display | |||||
| if (this.options.pagination) { | |||||
| this.infoElement = document.createElement('div'); | |||||
| this.infoElement.className = 'datatable-info'; | |||||
| topControls.appendChild(this.infoElement); | |||||
| } | |||||
| wrapper.appendChild(topControls); | |||||
| // Wrap the table | |||||
| if (this.element.parentNode) { | |||||
| this.element.parentNode.insertBefore(wrapper, this.element); | |||||
| } | |||||
| wrapper.appendChild(this.element); | |||||
| // Create pagination controls | |||||
| if (this.options.pagination) { | |||||
| this.paginationElement = document.createElement('div'); | |||||
| this.paginationElement.className = 'datatable-pagination'; | |||||
| wrapper.appendChild(this.paginationElement); | |||||
| } | |||||
| this.wrapper = wrapper; | |||||
| } | |||||
| private applyStyles(): void { | |||||
| // Apply Bootstrap-like styles | |||||
| const classes = ['table']; | |||||
| if (this.options.striped) classes.push('table-striped'); | |||||
| if (this.options.bordered) classes.push('table-bordered'); | |||||
| if (this.options.hover) classes.push('table-hover'); | |||||
| if (this.options.responsive) { | |||||
| const responsiveWrapper = document.createElement('div'); | |||||
| responsiveWrapper.className = 'table-responsive'; | |||||
| if (this.element.parentNode) { | |||||
| this.element.parentNode.insertBefore(responsiveWrapper, this.element); | |||||
| responsiveWrapper.appendChild(this.element); | |||||
| } | |||||
| } | |||||
| this.element.className = classes.join(' '); | |||||
| // Add custom styles | |||||
| this.injectStyles(); | |||||
| } | |||||
| private injectStyles(): void { | |||||
| const styleId = 'datatable-styles'; | |||||
| if (document.getElementById(styleId)) return; | |||||
| const style = document.createElement('style'); | |||||
| style.id = styleId; | |||||
| style.textContent = ` | |||||
| .datatable-wrapper { | |||||
| margin: 20px 0; | |||||
| } | |||||
| .datatable-top-controls { | |||||
| display: flex; | |||||
| justify-content: space-between; | |||||
| align-items: center; | |||||
| margin-bottom: 15px; | |||||
| flex-wrap: wrap; | |||||
| gap: 10px; | |||||
| } | |||||
| .datatable-search { | |||||
| display: flex; | |||||
| align-items: center; | |||||
| gap: 8px; | |||||
| } | |||||
| .datatable-search label { | |||||
| margin: 0; | |||||
| font-weight: 500; | |||||
| } | |||||
| .datatable-search input { | |||||
| width: 250px; | |||||
| padding: 6px 12px; | |||||
| border: 1px solid var(--c-border, #dee2e6); | |||||
| border-radius: 4px; | |||||
| font-size: 14px; | |||||
| } | |||||
| .datatable-info { | |||||
| color: var(--c-text-muted, #6c757d); | |||||
| font-size: 14px; | |||||
| margin: 0; | |||||
| } | |||||
| .datatable-pagination { | |||||
| margin-top: 15px; | |||||
| display: flex; | |||||
| justify-content: center; | |||||
| align-items: center; | |||||
| gap: 4px; | |||||
| flex-wrap: wrap; | |||||
| } | |||||
| .datatable-pagination button { | |||||
| background: var(--c-bkg-card, #fff); | |||||
| border: 1px solid var(--c-border, #dee2e6); | |||||
| color: var(--c-text-base, #333); | |||||
| padding: 8px 12px; | |||||
| cursor: pointer; | |||||
| border-radius: 4px; | |||||
| font-size: 14px; | |||||
| transition: all 0.2s ease; | |||||
| min-width: 40px; | |||||
| } | |||||
| .datatable-pagination button:hover:not(:disabled) { | |||||
| background: var(--c-primary, #007bff); | |||||
| border-color: var(--c-primary, #007bff); | |||||
| color: white; | |||||
| } | |||||
| .datatable-pagination button.active { | |||||
| background: var(--c-primary, #007bff); | |||||
| border-color: var(--c-primary, #007bff); | |||||
| color: white; | |||||
| } | |||||
| .datatable-pagination button:disabled { | |||||
| opacity: 0.6; | |||||
| cursor: not-allowed; | |||||
| background: var(--c-bkg-muted, #f8f9fa); | |||||
| } | |||||
| .datatable-sort { | |||||
| cursor: pointer; | |||||
| user-select: none; | |||||
| position: relative; | |||||
| padding-right: 20px !important; | |||||
| transition: background-color 0.2s ease; | |||||
| } | |||||
| .datatable-sort:hover { | |||||
| background: var(--c-bkg-hover, #f8f9fa); | |||||
| } | |||||
| .datatable-sort::after { | |||||
| content: '↕'; | |||||
| position: absolute; | |||||
| right: 8px; | |||||
| top: 50%; | |||||
| transform: translateY(-50%); | |||||
| opacity: 0.5; | |||||
| font-size: 12px; | |||||
| } | |||||
| .datatable-sort.asc::after { | |||||
| content: '↑'; | |||||
| opacity: 1; | |||||
| color: var(--c-primary, #007bff); | |||||
| } | |||||
| .datatable-sort.desc::after { | |||||
| content: '↓'; | |||||
| opacity: 1; | |||||
| color: var(--c-primary, #007bff); | |||||
| } | |||||
| .datatable-no-results { | |||||
| text-align: center; | |||||
| color: var(--c-text-muted, #6c757d); | |||||
| font-style: italic; | |||||
| padding: 20px; | |||||
| } | |||||
| @media (max-width: 768px) { | |||||
| .datatable-top-controls { | |||||
| flex-direction: column; | |||||
| align-items: stretch; | |||||
| } | |||||
| .datatable-search input { | |||||
| width: 100%; | |||||
| } | |||||
| .datatable-pagination { | |||||
| justify-content: center; | |||||
| } | |||||
| .datatable-pagination button { | |||||
| padding: 6px 10px; | |||||
| font-size: 13px; | |||||
| } | |||||
| } | |||||
| `; | |||||
| document.head.appendChild(style); | |||||
| } | |||||
| private bindEvents(): void { | |||||
| // Search functionality | |||||
| if (this.options.searchable && this.searchInput) { | |||||
| this.searchInput.addEventListener('input', (e) => { | |||||
| const target = e.target as HTMLInputElement; | |||||
| this.search(target.value); | |||||
| }); | |||||
| } | |||||
| // Sorting functionality | |||||
| if (this.options.sortable) { | |||||
| const headers = this.element.querySelectorAll<HTMLTableCellElement>('thead th'); | |||||
| headers.forEach((header, index) => { | |||||
| header.classList.add('datatable-sort'); | |||||
| header.addEventListener('click', () => { | |||||
| this.sort(index); | |||||
| }); | |||||
| header.setAttribute('tabindex', '0'); | |||||
| header.setAttribute('role', 'button'); | |||||
| header.setAttribute('aria-label', `Sort by ${header.textContent}`); | |||||
| }); | |||||
| } | |||||
| } | |||||
| public search(query: string): void { | |||||
| this.state.searchQuery = query; | |||||
| if (!query.trim()) { | |||||
| this.filteredData = [...this.originalData]; | |||||
| } else { | |||||
| const searchTerm = query.toLowerCase().trim(); | |||||
| this.filteredData = this.originalData.filter(row => | |||||
| row.some(cell => | |||||
| cell.toString().toLowerCase().includes(searchTerm) | |||||
| ) | |||||
| ); | |||||
| } | |||||
| this.state.filteredData = this.filteredData; | |||||
| this.state.currentPage = 1; | |||||
| this.render(); | |||||
| } | |||||
| public sort(columnIndex: number): void { | |||||
| if (this.state.sortColumn === columnIndex) { | |||||
| this.state.sortDirection = this.state.sortDirection === 'asc' ? 'desc' : 'asc'; | |||||
| } else { | |||||
| this.state.sortColumn = columnIndex; | |||||
| this.state.sortDirection = 'asc'; | |||||
| } | |||||
| this.filteredData.sort((a, b) => { | |||||
| const aVal = a[columnIndex]; | |||||
| const bVal = b[columnIndex]; | |||||
| // Try to parse as numbers | |||||
| const aNum = parseFloat(aVal); | |||||
| const bNum = parseFloat(bVal); | |||||
| let comparison = 0; | |||||
| if (!isNaN(aNum) && !isNaN(bNum)) { | |||||
| comparison = aNum - bNum; | |||||
| } else { | |||||
| // Try to parse as dates | |||||
| const aDate = new Date(aVal); | |||||
| const bDate = new Date(bVal); | |||||
| if (aDate.getTime() && bDate.getTime()) { | |||||
| comparison = aDate.getTime() - bDate.getTime(); | |||||
| } else { | |||||
| comparison = aVal.toString().localeCompare(bVal.toString()); | |||||
| } | |||||
| } | |||||
| return this.state.sortDirection === 'asc' ? comparison : -comparison; | |||||
| }); | |||||
| this.updateSortHeaders(); | |||||
| this.render(); | |||||
| } | |||||
| private updateSortHeaders(): void { | |||||
| const headers = this.element.querySelectorAll<HTMLTableCellElement>('thead th'); | |||||
| headers.forEach((header, index) => { | |||||
| header.classList.remove('asc', 'desc'); | |||||
| if (index === this.state.sortColumn) { | |||||
| header.classList.add(this.state.sortDirection); | |||||
| } | |||||
| }); | |||||
| } | |||||
| public render(): void { | |||||
| const tbody = this.element.querySelector('tbody'); | |||||
| if (!tbody) return; | |||||
| const startIndex = (this.state.currentPage - 1) * this.options.pageSize!; | |||||
| const endIndex = startIndex + this.options.pageSize!; | |||||
| const pageData = this.filteredData.slice(startIndex, endIndex); | |||||
| // Clear tbody | |||||
| tbody.innerHTML = ''; | |||||
| if (pageData.length === 0) { | |||||
| // Show no results message | |||||
| const noResultsRow = document.createElement('tr'); | |||||
| const noResultsCell = document.createElement('td'); | |||||
| noResultsCell.colSpan = this.getColumnCount(); | |||||
| noResultsCell.className = 'datatable-no-results'; | |||||
| noResultsCell.textContent = this.state.searchQuery ? | |||||
| 'No matching records found' : 'No data available'; | |||||
| noResultsRow.appendChild(noResultsCell); | |||||
| tbody.appendChild(noResultsRow); | |||||
| } else { | |||||
| // Add rows | |||||
| pageData.forEach((rowData, rowIndex) => { | |||||
| const row = document.createElement('tr'); | |||||
| rowData.forEach((cellData, colIndex) => { | |||||
| const cell = document.createElement('td'); | |||||
| cell.textContent = cellData.toString(); | |||||
| row.appendChild(cell); | |||||
| }); | |||||
| tbody.appendChild(row); | |||||
| }); | |||||
| } | |||||
| // Update pagination | |||||
| if (this.options.pagination) { | |||||
| this.updatePagination(); | |||||
| } | |||||
| // Update info | |||||
| this.updateInfo(); | |||||
| } | |||||
| private getColumnCount(): number { | |||||
| const headerRow = this.element.querySelector('thead tr'); | |||||
| return headerRow ? headerRow.querySelectorAll('th').length : 0; | |||||
| } | |||||
| private updatePagination(): void { | |||||
| if (!this.paginationElement) return; | |||||
| this.state.totalPages = Math.ceil(this.filteredData.length / this.options.pageSize!); | |||||
| this.paginationElement.innerHTML = ''; | |||||
| if (this.state.totalPages <= 1) return; | |||||
| // Previous button | |||||
| const prevBtn = this.createPaginationButton('Previous', () => { | |||||
| if (this.state.currentPage > 1) { | |||||
| this.state.currentPage--; | |||||
| this.render(); | |||||
| } | |||||
| }); | |||||
| prevBtn.disabled = this.state.currentPage === 1; | |||||
| this.paginationElement.appendChild(prevBtn); | |||||
| // Calculate page range to show | |||||
| const maxButtons = 5; | |||||
| let startPage = Math.max(1, this.state.currentPage - Math.floor(maxButtons / 2)); | |||||
| let endPage = Math.min(this.state.totalPages, startPage + maxButtons - 1); | |||||
| // Adjust if we're at the end | |||||
| if (endPage - startPage + 1 < maxButtons) { | |||||
| startPage = Math.max(1, endPage - maxButtons + 1); | |||||
| } | |||||
| // First page if not in range | |||||
| if (startPage > 1) { | |||||
| const firstBtn = this.createPaginationButton('1', () => { | |||||
| this.state.currentPage = 1; | |||||
| this.render(); | |||||
| }); | |||||
| this.paginationElement.appendChild(firstBtn); | |||||
| if (startPage > 2) { | |||||
| const ellipsis = document.createElement('span'); | |||||
| ellipsis.textContent = '...'; | |||||
| ellipsis.className = 'pagination-ellipsis'; | |||||
| this.paginationElement.appendChild(ellipsis); | |||||
| } | |||||
| } | |||||
| // Page numbers | |||||
| for (let i = startPage; i <= endPage; i++) { | |||||
| const pageBtn = this.createPaginationButton(i.toString(), () => { | |||||
| this.state.currentPage = i; | |||||
| this.render(); | |||||
| }); | |||||
| pageBtn.classList.toggle('active', i === this.state.currentPage); | |||||
| this.paginationElement.appendChild(pageBtn); | |||||
| } | |||||
| // Last page if not in range | |||||
| if (endPage < this.state.totalPages) { | |||||
| if (endPage < this.state.totalPages - 1) { | |||||
| const ellipsis = document.createElement('span'); | |||||
| ellipsis.textContent = '...'; | |||||
| ellipsis.className = 'pagination-ellipsis'; | |||||
| this.paginationElement.appendChild(ellipsis); | |||||
| } | |||||
| const lastBtn = this.createPaginationButton(this.state.totalPages.toString(), () => { | |||||
| this.state.currentPage = this.state.totalPages; | |||||
| this.render(); | |||||
| }); | |||||
| this.paginationElement.appendChild(lastBtn); | |||||
| } | |||||
| // Next button | |||||
| const nextBtn = this.createPaginationButton('Next', () => { | |||||
| if (this.state.currentPage < this.state.totalPages) { | |||||
| this.state.currentPage++; | |||||
| this.render(); | |||||
| } | |||||
| }); | |||||
| nextBtn.disabled = this.state.currentPage === this.state.totalPages; | |||||
| this.paginationElement.appendChild(nextBtn); | |||||
| } | |||||
| private createPaginationButton(text: string, onClick: () => void): HTMLButtonElement { | |||||
| const button = document.createElement('button'); | |||||
| button.textContent = text; | |||||
| button.addEventListener('click', onClick); | |||||
| return button; | |||||
| } | |||||
| private updateInfo(): void { | |||||
| if (!this.infoElement) return; | |||||
| const startIndex = (this.state.currentPage - 1) * this.options.pageSize! + 1; | |||||
| const endIndex = Math.min(startIndex + this.options.pageSize! - 1, this.filteredData.length); | |||||
| const total = this.filteredData.length; | |||||
| const originalTotal = this.originalData.length; | |||||
| if (total === 0) { | |||||
| this.infoElement.textContent = 'No entries to show'; | |||||
| } else if (total === originalTotal) { | |||||
| this.infoElement.textContent = `Showing ${startIndex} to ${endIndex} of ${total} entries`; | |||||
| } else { | |||||
| this.infoElement.textContent = `Showing ${startIndex} to ${endIndex} of ${total} entries (filtered from ${originalTotal} total entries)`; | |||||
| } | |||||
| } | |||||
| // Public API methods | |||||
| public goToPage(page: number): void { | |||||
| if (page >= 1 && page <= this.state.totalPages) { | |||||
| this.state.currentPage = page; | |||||
| this.render(); | |||||
| } | |||||
| } | |||||
| public setPageSize(size: number): void { | |||||
| this.options.pageSize = size; | |||||
| this.state.currentPage = 1; | |||||
| this.render(); | |||||
| } | |||||
| public getState(): Readonly<DataTableState> { | |||||
| return { ...this.state }; | |||||
| } | |||||
| public refresh(): void { | |||||
| this.extractData(); | |||||
| this.state.currentPage = 1; | |||||
| this.render(); | |||||
| } | |||||
| public clear(): void { | |||||
| this.originalData = []; | |||||
| this.filteredData = []; | |||||
| this.state.currentPage = 1; | |||||
| this.render(); | |||||
| } | |||||
| } | |||||
| // DataTable Manager | |||||
| export class DataTableManager { | |||||
| private instances: Map<string, VanillaDataTable> = new Map(); | |||||
| public initialize(selector: string = '#dataTable', options: DataTableOptions = {}): VanillaDataTable | null { | |||||
| const element = document.querySelector<HTMLTableElement>(selector); | |||||
| if (!element) { | |||||
| // Silently return null if element doesn't exist (normal for pages without tables) | |||||
| return null; | |||||
| } | |||||
| // Clean up existing instance | |||||
| if (element.dataTableInstance) { | |||||
| element.dataTableInstance.destroy(); | |||||
| } | |||||
| // Create new instance | |||||
| const dataTable = new VanillaDataTable(element, options); | |||||
| element.dataTableInstance = dataTable; | |||||
| // Store in manager | |||||
| this.instances.set(selector, dataTable); | |||||
| return dataTable; | |||||
| } | |||||
| public getInstance(selector: string): VanillaDataTable | undefined { | |||||
| return this.instances.get(selector); | |||||
| } | |||||
| public destroyInstance(selector: string): void { | |||||
| const instance = this.instances.get(selector); | |||||
| if (instance) { | |||||
| instance.destroy(); | |||||
| this.instances.delete(selector); | |||||
| } | |||||
| } | |||||
| public destroyAll(): void { | |||||
| this.instances.forEach((instance, selector) => { | |||||
| instance.destroy(); | |||||
| }); | |||||
| this.instances.clear(); | |||||
| } | |||||
| } | |||||
| // Create singleton manager | |||||
| const dataTableManager = new DataTableManager(); | |||||
| // Initialize DataTable | |||||
| const initializeDataTable = (): void => { | |||||
| // Only initialize if the table exists | |||||
| if (document.querySelector('#dataTable')) { | |||||
| dataTableManager.initialize('#dataTable', { | |||||
| sortable: true, | |||||
| searchable: true, | |||||
| pagination: true, | |||||
| pageSize: 10, | |||||
| responsive: true, | |||||
| striped: true, | |||||
| bordered: true, | |||||
| hover: true, | |||||
| }); | |||||
| } | |||||
| }; | |||||
| // Initialize on load | |||||
| if (document.readyState === 'loading') { | |||||
| document.addEventListener('DOMContentLoaded', initializeDataTable); | |||||
| } else { | |||||
| initializeDataTable(); | |||||
| } | |||||
| // Reinitialize on theme change | |||||
| window.addEventListener('adminator:themeChanged', () => { | |||||
| setTimeout(initializeDataTable, 100); | |||||
| }); | |||||
| // Cleanup on page unload | |||||
| window.addEventListener('beforeunload', () => { | |||||
| dataTableManager.destroyAll(); | |||||
| }); | |||||
| // Export default for compatibility | |||||
| export default { | |||||
| init: initializeDataTable, | |||||
| manager: dataTableManager, | |||||
| VanillaDataTable, | |||||
| DataTableManager, | |||||
| }; | |||||
| @ -1,699 +0,0 @@ | |||||
| /** | |||||
| * Enhanced HTML5 DatePicker with TypeScript | |||||
| * Modern date picker implementation using native HTML5 input[type="date"] | |||||
| */ | |||||
| import DateUtils from '../utils/date'; | |||||
| import type { ComponentInterface } from '../../types'; | |||||
| // Type definitions for DatePicker | |||||
| export interface DatePickerOptions { | |||||
| format?: string; | |||||
| autoclose?: boolean; | |||||
| todayHighlight?: boolean; | |||||
| minDate?: string; | |||||
| maxDate?: string; | |||||
| startDate?: string; | |||||
| endDate?: string; | |||||
| daysOfWeekDisabled?: number[]; | |||||
| datesDisabled?: string[]; | |||||
| weekStart?: number; | |||||
| language?: string; | |||||
| } | |||||
| export interface DatePickerEvent { | |||||
| date: string; | |||||
| formattedDate: string; | |||||
| dateObject: Date; | |||||
| isValid: boolean; | |||||
| } | |||||
| export interface DatePickerValidation { | |||||
| isValid: boolean; | |||||
| errors: string[]; | |||||
| } | |||||
| declare global { | |||||
| interface HTMLInputElement { | |||||
| vanillaDatePicker?: VanillaDatePicker; | |||||
| showPicker?: () => void; | |||||
| } | |||||
| } | |||||
| // Enhanced HTML5 date picker with vanilla JS | |||||
| export class VanillaDatePicker implements ComponentInterface { | |||||
| public name: string = 'VanillaDatePicker'; | |||||
| public element: HTMLInputElement; | |||||
| public options: DatePickerOptions; | |||||
| public isInitialized: boolean = false; | |||||
| private wrapper: HTMLElement | null = null; | |||||
| private todayIndicator: HTMLElement | null = null; | |||||
| private validationErrors: string[] = []; | |||||
| constructor(element: HTMLInputElement, options: DatePickerOptions = {}) { | |||||
| this.element = element; | |||||
| this.options = { | |||||
| format: 'yyyy-mm-dd', | |||||
| autoclose: true, | |||||
| todayHighlight: true, | |||||
| weekStart: 0, | |||||
| language: 'en', | |||||
| ...options, | |||||
| }; | |||||
| this.init(); | |||||
| } | |||||
| public init(): void { | |||||
| this.convertToHTML5(); | |||||
| this.enhanceInput(); | |||||
| this.applyStyles(); | |||||
| this.bindEvents(); | |||||
| this.validateConstraints(); | |||||
| this.addTodayHighlight(); | |||||
| this.isInitialized = true; | |||||
| } | |||||
| public destroy(): void { | |||||
| if (this.wrapper && this.wrapper.parentNode) { | |||||
| this.wrapper.parentNode.replaceChild(this.element, this.wrapper); | |||||
| } | |||||
| this.isInitialized = false; | |||||
| } | |||||
| private convertToHTML5(): void { | |||||
| // Convert input to HTML5 date type | |||||
| this.element.type = 'date'; | |||||
| this.element.classList.add('form-control', 'vanilla-datepicker'); | |||||
| // Remove placeholder since HTML5 date inputs don't need it | |||||
| this.element.removeAttribute('placeholder'); | |||||
| // Set constraints | |||||
| if (this.options.minDate) { | |||||
| this.element.min = this.options.minDate; | |||||
| } | |||||
| if (this.options.maxDate) { | |||||
| this.element.max = this.options.maxDate; | |||||
| } | |||||
| // Set default value if no value is set | |||||
| if (!this.element.value) { | |||||
| if (this.options.startDate) { | |||||
| this.element.value = this.options.startDate; | |||||
| } else if (this.options.todayHighlight) { | |||||
| this.element.value = DateUtils.formatters.inputDate(DateUtils.now()); | |||||
| } | |||||
| } | |||||
| // Ensure proper styling | |||||
| this.element.style.minHeight = '38px'; | |||||
| this.element.style.lineHeight = '1.5'; | |||||
| this.element.style.cursor = 'pointer'; | |||||
| // Add ARIA attributes | |||||
| this.element.setAttribute('aria-label', 'Select date'); | |||||
| this.element.setAttribute('role', 'textbox'); | |||||
| this.element.setAttribute('aria-expanded', 'false'); | |||||
| } | |||||
| private enhanceInput(): void { | |||||
| // Create wrapper for enhanced functionality | |||||
| const wrapper = document.createElement('div'); | |||||
| wrapper.className = 'vanilla-datepicker-wrapper'; | |||||
| wrapper.style.position = 'relative'; | |||||
| // Wrap the input | |||||
| const parent = this.element.parentNode; | |||||
| if (parent) { | |||||
| parent.insertBefore(wrapper, this.element); | |||||
| wrapper.appendChild(this.element); | |||||
| } | |||||
| // Add calendar icon if input is in an input group | |||||
| const inputGroup = this.element.closest('.input-group'); | |||||
| if (inputGroup) { | |||||
| const calendarIcon = inputGroup.querySelector<HTMLElement>('.input-group-text i.ti-calendar'); | |||||
| if (calendarIcon) { | |||||
| calendarIcon.addEventListener('click', this.handleIconClick.bind(this)); | |||||
| calendarIcon.style.cursor = 'pointer'; | |||||
| calendarIcon.setAttribute('tabindex', '0'); | |||||
| calendarIcon.setAttribute('role', 'button'); | |||||
| calendarIcon.setAttribute('aria-label', 'Open calendar'); | |||||
| } | |||||
| } | |||||
| this.wrapper = wrapper; | |||||
| } | |||||
| private applyStyles(): void { | |||||
| const styleId = 'vanilla-datepicker-styles'; | |||||
| if (document.getElementById(styleId)) return; | |||||
| const style = document.createElement('style'); | |||||
| style.id = styleId; | |||||
| style.textContent = ` | |||||
| .vanilla-datepicker-wrapper { | |||||
| position: relative; | |||||
| display: inline-block; | |||||
| width: 100%; | |||||
| } | |||||
| .vanilla-datepicker { | |||||
| width: 100%; | |||||
| padding: 8px 12px; | |||||
| border: 1px solid var(--c-border, #ced4da); | |||||
| border-radius: 4px; | |||||
| background-color: var(--c-bkg-card, #fff); | |||||
| color: var(--c-text-base, #495057); | |||||
| font-size: 14px; | |||||
| font-family: inherit; | |||||
| transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; | |||||
| } | |||||
| .vanilla-datepicker:focus { | |||||
| outline: none; | |||||
| border-color: var(--c-primary, #007bff); | |||||
| box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); | |||||
| } | |||||
| .vanilla-datepicker:invalid { | |||||
| border-color: var(--c-danger, #dc3545); | |||||
| } | |||||
| .vanilla-datepicker:invalid:focus { | |||||
| border-color: var(--c-danger, #dc3545); | |||||
| box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25); | |||||
| } | |||||
| .vanilla-datepicker::-webkit-calendar-picker-indicator { | |||||
| cursor: pointer; | |||||
| border-radius: 4px; | |||||
| margin-right: 2px; | |||||
| opacity: 0.6; | |||||
| transition: opacity 0.15s ease-in-out; | |||||
| filter: var(--datepicker-icon-filter, none); | |||||
| } | |||||
| .vanilla-datepicker::-webkit-calendar-picker-indicator:hover { | |||||
| opacity: 1; | |||||
| } | |||||
| .vanilla-datepicker::-webkit-datetime-edit-fields-wrapper { | |||||
| padding: 0; | |||||
| } | |||||
| .vanilla-datepicker::-webkit-datetime-edit-month-field, | |||||
| .vanilla-datepicker::-webkit-datetime-edit-day-field, | |||||
| .vanilla-datepicker::-webkit-datetime-edit-year-field { | |||||
| color: var(--c-text-base, #495057); | |||||
| } | |||||
| .vanilla-datepicker::-webkit-datetime-edit-text { | |||||
| color: var(--c-text-muted, #6c757d); | |||||
| } | |||||
| /* Dark mode support */ | |||||
| [data-theme="dark"] .vanilla-datepicker { | |||||
| background-color: var(--c-bkg-card, #1f2937); | |||||
| color: var(--c-text-base, #f9fafb); | |||||
| border-color: var(--c-border, #374151); | |||||
| --datepicker-icon-filter: invert(1); | |||||
| } | |||||
| .datepicker-today-indicator { | |||||
| position: absolute; | |||||
| top: 4px; | |||||
| right: 12px; | |||||
| width: 6px; | |||||
| height: 6px; | |||||
| background-color: var(--c-primary, #007bff); | |||||
| border-radius: 50%; | |||||
| opacity: 0.8; | |||||
| pointer-events: none; | |||||
| z-index: 1; | |||||
| } | |||||
| .datepicker-animation { | |||||
| animation: datepicker-highlight 0.3s ease-in-out; | |||||
| } | |||||
| @keyframes datepicker-highlight { | |||||
| 0% { transform: scale(1); } | |||||
| 50% { transform: scale(1.02); } | |||||
| 100% { transform: scale(1); } | |||||
| } | |||||
| .datepicker-error { | |||||
| border-color: var(--c-danger, #dc3545) !important; | |||||
| } | |||||
| .datepicker-error:focus { | |||||
| border-color: var(--c-danger, #dc3545) !important; | |||||
| box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25) !important; | |||||
| } | |||||
| .datepicker-validation-feedback { | |||||
| display: block; | |||||
| width: 100%; | |||||
| margin-top: 0.25rem; | |||||
| font-size: 0.875rem; | |||||
| color: var(--c-danger, #dc3545); | |||||
| } | |||||
| /* Responsive design */ | |||||
| @media (max-width: 768px) { | |||||
| .vanilla-datepicker { | |||||
| padding: 10px 12px; | |||||
| font-size: 16px; /* Prevent zoom on iOS */ | |||||
| } | |||||
| .vanilla-datepicker::-webkit-calendar-picker-indicator { | |||||
| width: 20px; | |||||
| height: 20px; | |||||
| } | |||||
| } | |||||
| `; | |||||
| document.head.appendChild(style); | |||||
| } | |||||
| private bindEvents(): void { | |||||
| // Handle click events | |||||
| this.element.addEventListener('click', this.handleClick.bind(this)); | |||||
| // Handle keyboard events | |||||
| this.element.addEventListener('keydown', this.handleKeydown.bind(this)); | |||||
| // Handle change events | |||||
| this.element.addEventListener('change', this.handleChange.bind(this)); | |||||
| // Handle focus events | |||||
| this.element.addEventListener('focus', this.handleFocus.bind(this)); | |||||
| // Handle blur events | |||||
| this.element.addEventListener('blur', this.handleBlur.bind(this)); | |||||
| // Handle input events for real-time validation | |||||
| this.element.addEventListener('input', this.handleInput.bind(this)); | |||||
| } | |||||
| private handleClick(): void { | |||||
| this.openPicker(); | |||||
| } | |||||
| private handleKeydown(e: KeyboardEvent): void { | |||||
| if (e.key === 'Enter' || e.key === ' ') { | |||||
| e.preventDefault(); | |||||
| this.openPicker(); | |||||
| } | |||||
| } | |||||
| private handleChange(e: Event): void { | |||||
| const target = e.target as HTMLInputElement; | |||||
| this.handleDateChange(target.value); | |||||
| } | |||||
| private handleFocus(): void { | |||||
| this.element.classList.add('datepicker-animation'); | |||||
| this.element.setAttribute('aria-expanded', 'true'); | |||||
| setTimeout(() => { | |||||
| this.element.classList.remove('datepicker-animation'); | |||||
| }, 300); | |||||
| } | |||||
| private handleBlur(): void { | |||||
| this.element.setAttribute('aria-expanded', 'false'); | |||||
| this.validateInput(); | |||||
| } | |||||
| private handleInput(): void { | |||||
| this.validateInput(); | |||||
| } | |||||
| private handleIconClick(e: Event): void { | |||||
| e.preventDefault(); | |||||
| e.stopPropagation(); | |||||
| this.openPicker(); | |||||
| } | |||||
| private openPicker(): void { | |||||
| this.element.focus(); | |||||
| // Try to open the native date picker | |||||
| if (this.element.showPicker && typeof this.element.showPicker === 'function') { | |||||
| try { | |||||
| this.element.showPicker(); | |||||
| } catch (error) { | |||||
| console.warn('DatePicker: showPicker not supported', error); | |||||
| } | |||||
| } | |||||
| } | |||||
| private handleDateChange(selectedDate: string): void { | |||||
| if (selectedDate) { | |||||
| // Add visual feedback | |||||
| this.element.classList.add('datepicker-animation'); | |||||
| setTimeout(() => { | |||||
| this.element.classList.remove('datepicker-animation'); | |||||
| }, 300); | |||||
| // Validate the date | |||||
| const validation = this.validateDate(selectedDate); | |||||
| // Create event data | |||||
| const eventData: DatePickerEvent = { | |||||
| date: selectedDate, | |||||
| formattedDate: this.formatDate(selectedDate), | |||||
| dateObject: new Date(selectedDate), | |||||
| isValid: validation.isValid, | |||||
| }; | |||||
| // Trigger custom event | |||||
| const changeEvent = new CustomEvent('datepicker:change', { | |||||
| detail: eventData, | |||||
| bubbles: true, | |||||
| }); | |||||
| this.element.dispatchEvent(changeEvent); | |||||
| // Update validation state | |||||
| this.updateValidationState(validation); | |||||
| } | |||||
| } | |||||
| private validateConstraints(): void { | |||||
| // Set up date constraints based on options | |||||
| if (this.options.datesDisabled && this.options.datesDisabled.length > 0) { | |||||
| this.element.addEventListener('input', (e) => { | |||||
| const target = e.target as HTMLInputElement; | |||||
| if (this.options.datesDisabled!.includes(target.value)) { | |||||
| this.addValidationError('This date is disabled'); | |||||
| } | |||||
| }); | |||||
| } | |||||
| if (this.options.daysOfWeekDisabled && this.options.daysOfWeekDisabled.length > 0) { | |||||
| this.element.addEventListener('input', (e) => { | |||||
| const target = e.target as HTMLInputElement; | |||||
| const date = new Date(target.value); | |||||
| const dayOfWeek = date.getDay(); | |||||
| if (this.options.daysOfWeekDisabled!.includes(dayOfWeek)) { | |||||
| this.addValidationError('This day of the week is disabled'); | |||||
| } | |||||
| }); | |||||
| } | |||||
| } | |||||
| private validateDate(dateString: string): DatePickerValidation { | |||||
| const errors: string[] = []; | |||||
| const date = new Date(dateString); | |||||
| // Check if date is valid | |||||
| if (isNaN(date.getTime())) { | |||||
| errors.push('Invalid date format'); | |||||
| } | |||||
| // Check min/max constraints | |||||
| if (this.options.minDate) { | |||||
| const minDate = new Date(this.options.minDate); | |||||
| if (date < minDate) { | |||||
| errors.push(`Date must be after ${this.formatDate(this.options.minDate)}`); | |||||
| } | |||||
| } | |||||
| if (this.options.maxDate) { | |||||
| const maxDate = new Date(this.options.maxDate); | |||||
| if (date > maxDate) { | |||||
| errors.push(`Date must be before ${this.formatDate(this.options.maxDate)}`); | |||||
| } | |||||
| } | |||||
| // Check disabled dates | |||||
| if (this.options.datesDisabled && this.options.datesDisabled.includes(dateString)) { | |||||
| errors.push('This date is disabled'); | |||||
| } | |||||
| // Check disabled days of week | |||||
| if (this.options.daysOfWeekDisabled && this.options.daysOfWeekDisabled.includes(date.getDay())) { | |||||
| errors.push('This day of the week is disabled'); | |||||
| } | |||||
| return { | |||||
| isValid: errors.length === 0, | |||||
| errors, | |||||
| }; | |||||
| } | |||||
| private validateInput(): void { | |||||
| const value = this.element.value; | |||||
| if (value) { | |||||
| const validation = this.validateDate(value); | |||||
| this.updateValidationState(validation); | |||||
| } else { | |||||
| this.clearValidationState(); | |||||
| } | |||||
| } | |||||
| private updateValidationState(validation: DatePickerValidation): void { | |||||
| this.validationErrors = validation.errors; | |||||
| // Remove existing validation feedback | |||||
| this.clearValidationFeedback(); | |||||
| if (!validation.isValid) { | |||||
| // Add error class | |||||
| this.element.classList.add('datepicker-error'); | |||||
| // Add validation feedback | |||||
| const feedback = document.createElement('div'); | |||||
| feedback.className = 'datepicker-validation-feedback'; | |||||
| feedback.textContent = validation.errors.join(', '); | |||||
| if (this.wrapper) { | |||||
| this.wrapper.appendChild(feedback); | |||||
| } | |||||
| // Set ARIA attributes | |||||
| this.element.setAttribute('aria-invalid', 'true'); | |||||
| this.element.setAttribute('aria-describedby', 'datepicker-error'); | |||||
| feedback.id = 'datepicker-error'; | |||||
| } else { | |||||
| this.clearValidationState(); | |||||
| } | |||||
| } | |||||
| private clearValidationState(): void { | |||||
| this.element.classList.remove('datepicker-error'); | |||||
| this.element.setAttribute('aria-invalid', 'false'); | |||||
| this.element.removeAttribute('aria-describedby'); | |||||
| this.validationErrors = []; | |||||
| this.clearValidationFeedback(); | |||||
| } | |||||
| private clearValidationFeedback(): void { | |||||
| if (this.wrapper) { | |||||
| const existingFeedback = this.wrapper.querySelector('.datepicker-validation-feedback'); | |||||
| if (existingFeedback) { | |||||
| existingFeedback.remove(); | |||||
| } | |||||
| } | |||||
| } | |||||
| private addValidationError(error: string): void { | |||||
| if (!this.validationErrors.includes(error)) { | |||||
| this.validationErrors.push(error); | |||||
| } | |||||
| } | |||||
| private addTodayHighlight(): void { | |||||
| if (this.options.todayHighlight) { | |||||
| const today = DateUtils.formatters.inputDate(DateUtils.now()); | |||||
| if (this.element.value === today) { | |||||
| this.todayIndicator = document.createElement('div'); | |||||
| this.todayIndicator.className = 'datepicker-today-indicator'; | |||||
| this.todayIndicator.title = 'Today'; | |||||
| if (this.wrapper) { | |||||
| this.wrapper.appendChild(this.todayIndicator); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| private formatDate(dateString: string): string { | |||||
| try { | |||||
| const date = new Date(dateString); | |||||
| return DateUtils.format(date, this.options.format || 'yyyy-mm-dd'); | |||||
| } catch (error) { | |||||
| return dateString; | |||||
| } | |||||
| } | |||||
| // Public API methods | |||||
| public setDate(dateString: string): void { | |||||
| this.element.value = dateString; | |||||
| this.handleDateChange(dateString); | |||||
| } | |||||
| public getDate(): string { | |||||
| return this.element.value; | |||||
| } | |||||
| public getFormattedDate(): string { | |||||
| return this.formatDate(this.element.value); | |||||
| } | |||||
| public getDateObject(): Date | null { | |||||
| return this.element.value ? new Date(this.element.value) : null; | |||||
| } | |||||
| public isValid(): boolean { | |||||
| return this.validationErrors.length === 0; | |||||
| } | |||||
| public getValidationErrors(): string[] { | |||||
| return [...this.validationErrors]; | |||||
| } | |||||
| public setMinDate(dateString: string): void { | |||||
| this.options.minDate = dateString; | |||||
| this.element.min = dateString; | |||||
| this.validateInput(); | |||||
| } | |||||
| public setMaxDate(dateString: string): void { | |||||
| this.options.maxDate = dateString; | |||||
| this.element.max = dateString; | |||||
| this.validateInput(); | |||||
| } | |||||
| public reset(): void { | |||||
| this.element.value = ''; | |||||
| this.clearValidationState(); | |||||
| if (this.todayIndicator) { | |||||
| this.todayIndicator.remove(); | |||||
| this.todayIndicator = null; | |||||
| } | |||||
| } | |||||
| public enable(): void { | |||||
| this.element.disabled = false; | |||||
| } | |||||
| public disable(): void { | |||||
| this.element.disabled = true; | |||||
| } | |||||
| public updateOptions(newOptions: Partial<DatePickerOptions>): void { | |||||
| this.options = { ...this.options, ...newOptions }; | |||||
| this.validateConstraints(); | |||||
| this.validateInput(); | |||||
| } | |||||
| } | |||||
| // DatePicker Manager | |||||
| export class DatePickerManager { | |||||
| private instances: Map<string, VanillaDatePicker> = new Map(); | |||||
| public initialize(selector: string, options: DatePickerOptions = {}): VanillaDatePicker[] { | |||||
| const elements = document.querySelectorAll<HTMLInputElement>(selector); | |||||
| const instances: VanillaDatePicker[] = []; | |||||
| elements.forEach((element, index) => { | |||||
| // Clean up existing instance | |||||
| if (element.vanillaDatePicker) { | |||||
| element.vanillaDatePicker.destroy(); | |||||
| } | |||||
| // Create new instance | |||||
| const datePicker = new VanillaDatePicker(element, options); | |||||
| element.vanillaDatePicker = datePicker; | |||||
| // Store in manager | |||||
| const key = `${selector}-${index}`; | |||||
| this.instances.set(key, datePicker); | |||||
| instances.push(datePicker); | |||||
| }); | |||||
| return instances; | |||||
| } | |||||
| public getInstances(selector: string): VanillaDatePicker[] { | |||||
| const instances: VanillaDatePicker[] = []; | |||||
| this.instances.forEach((instance, key) => { | |||||
| if (key.startsWith(selector)) { | |||||
| instances.push(instance); | |||||
| } | |||||
| }); | |||||
| return instances; | |||||
| } | |||||
| public destroyInstances(selector: string): void { | |||||
| const keysToDelete: string[] = []; | |||||
| this.instances.forEach((instance, key) => { | |||||
| if (key.startsWith(selector)) { | |||||
| instance.destroy(); | |||||
| keysToDelete.push(key); | |||||
| } | |||||
| }); | |||||
| keysToDelete.forEach(key => this.instances.delete(key)); | |||||
| } | |||||
| public destroyAll(): void { | |||||
| this.instances.forEach(instance => instance.destroy()); | |||||
| this.instances.clear(); | |||||
| } | |||||
| } | |||||
| // Create singleton manager | |||||
| const datePickerManager = new DatePickerManager(); | |||||
| // Initialize date pickers | |||||
| const initializeDatePickers = (): void => { | |||||
| // Start date pickers | |||||
| datePickerManager.initialize('.start-date', { | |||||
| format: 'yyyy-mm-dd', | |||||
| autoclose: true, | |||||
| todayHighlight: true, | |||||
| }); | |||||
| // End date pickers | |||||
| datePickerManager.initialize('.end-date', { | |||||
| format: 'yyyy-mm-dd', | |||||
| autoclose: true, | |||||
| todayHighlight: true, | |||||
| }); | |||||
| // Generic date pickers | |||||
| datePickerManager.initialize('input[type="date"]:not(.start-date):not(.end-date)', { | |||||
| format: 'yyyy-mm-dd', | |||||
| autoclose: true, | |||||
| todayHighlight: true, | |||||
| }); | |||||
| }; | |||||
| // Initialize on load | |||||
| if (document.readyState === 'loading') { | |||||
| document.addEventListener('DOMContentLoaded', initializeDatePickers); | |||||
| } else { | |||||
| initializeDatePickers(); | |||||
| } | |||||
| // Reinitialize on theme change | |||||
| window.addEventListener('adminator:themeChanged', () => { | |||||
| setTimeout(initializeDatePickers, 100); | |||||
| }); | |||||
| // Cleanup on page unload | |||||
| window.addEventListener('beforeunload', () => { | |||||
| datePickerManager.destroyAll(); | |||||
| }); | |||||
| // Export default for compatibility | |||||
| export default { | |||||
| init: initializeDatePickers, | |||||
| manager: datePickerManager, | |||||
| VanillaDatePicker, | |||||
| DatePickerManager, | |||||
| }; | |||||
| @ -1,740 +0,0 @@ | |||||
| /** | |||||
| * UI Bootstrap Components with TypeScript | |||||
| * Vanilla JavaScript implementations for Bootstrap components | |||||
| */ | |||||
| import type { ComponentInterface } from '../../types'; | |||||
| // Type definitions for UI components | |||||
| export interface UIComponentOptions { | |||||
| autoInit?: boolean; | |||||
| selector?: string; | |||||
| } | |||||
| export interface TooltipOptions { | |||||
| placement?: 'top' | 'bottom' | 'left' | 'right'; | |||||
| delay?: number; | |||||
| animation?: boolean; | |||||
| } | |||||
| export interface PopoverOptions { | |||||
| placement?: 'top' | 'bottom' | 'left' | 'right'; | |||||
| trigger?: 'click' | 'hover' | 'focus' | 'manual'; | |||||
| html?: boolean; | |||||
| animation?: boolean; | |||||
| } | |||||
| export interface ModalOptions { | |||||
| backdrop?: boolean | 'static'; | |||||
| keyboard?: boolean; | |||||
| focus?: boolean; | |||||
| show?: boolean; | |||||
| } | |||||
| export interface AccordionOptions { | |||||
| parent?: string; | |||||
| toggle?: boolean; | |||||
| } | |||||
| export interface DropdownOptions { | |||||
| offset?: [number, number]; | |||||
| flip?: boolean; | |||||
| boundary?: 'clippingParents' | 'viewport' | HTMLElement; | |||||
| } | |||||
| // Modal functionality | |||||
| export class VanillaModal implements ComponentInterface { | |||||
| public name: string = 'VanillaModal'; | |||||
| public element: HTMLElement; | |||||
| public options: ModalOptions; | |||||
| public isInitialized: boolean = false; | |||||
| private modal: HTMLElement | null = null; | |||||
| private backdrop: HTMLElement | null = null; | |||||
| private isOpen: boolean = false; | |||||
| private escapeHandler: ((e: KeyboardEvent) => void) | null = null; | |||||
| constructor(element: HTMLElement, options: ModalOptions = {}) { | |||||
| this.element = element; | |||||
| this.options = { | |||||
| backdrop: true, | |||||
| keyboard: true, | |||||
| focus: true, | |||||
| show: false, | |||||
| ...options, | |||||
| }; | |||||
| this.init(); | |||||
| } | |||||
| public init(): void { | |||||
| const targetSelector = this.element.getAttribute('data-bs-target'); | |||||
| if (!targetSelector) { | |||||
| console.warn('Modal: Missing data-bs-target attribute'); | |||||
| return; | |||||
| } | |||||
| this.modal = document.querySelector(targetSelector); | |||||
| if (!this.modal) { | |||||
| console.warn(`Modal: Target element ${targetSelector} not found`); | |||||
| return; | |||||
| } | |||||
| this.element.addEventListener('click', this.handleElementClick.bind(this)); | |||||
| // Close button functionality | |||||
| const closeButtons = this.modal.querySelectorAll<HTMLElement>('[data-bs-dismiss="modal"]'); | |||||
| closeButtons.forEach(btn => { | |||||
| btn.addEventListener('click', this.hide.bind(this)); | |||||
| }); | |||||
| // Close on backdrop click | |||||
| if (this.options.backdrop !== false) { | |||||
| this.modal.addEventListener('click', this.handleBackdropClick.bind(this)); | |||||
| } | |||||
| this.isInitialized = true; | |||||
| } | |||||
| public destroy(): void { | |||||
| if (this.escapeHandler) { | |||||
| document.removeEventListener('keydown', this.escapeHandler); | |||||
| this.escapeHandler = null; | |||||
| } | |||||
| this.hide(); | |||||
| this.isInitialized = false; | |||||
| } | |||||
| private handleElementClick(e: Event): void { | |||||
| e.preventDefault(); | |||||
| this.show(); | |||||
| } | |||||
| private handleBackdropClick(e: Event): void { | |||||
| if (e.target === this.modal && this.options.backdrop !== 'static') { | |||||
| this.hide(); | |||||
| } | |||||
| } | |||||
| public show(): void { | |||||
| if (this.isOpen || !this.modal) return; | |||||
| // Create backdrop | |||||
| if (this.options.backdrop !== false) { | |||||
| this.backdrop = document.createElement('div'); | |||||
| this.backdrop.className = 'modal-backdrop fade show'; | |||||
| document.body.appendChild(this.backdrop); | |||||
| } | |||||
| // Show modal | |||||
| this.modal.style.display = 'block'; | |||||
| this.modal.classList.add('show'); | |||||
| document.body.classList.add('modal-open'); | |||||
| this.isOpen = true; | |||||
| // Focus the modal | |||||
| if (this.options.focus) { | |||||
| this.modal.setAttribute('tabindex', '-1'); | |||||
| this.modal.focus(); | |||||
| } | |||||
| // Escape key handler | |||||
| if (this.options.keyboard) { | |||||
| this.escapeHandler = this.handleEscapeKey.bind(this); | |||||
| document.addEventListener('keydown', this.escapeHandler); | |||||
| } | |||||
| } | |||||
| public hide(): void { | |||||
| if (!this.isOpen || !this.modal) return; | |||||
| // Hide modal | |||||
| this.modal.classList.remove('show'); | |||||
| this.modal.style.display = 'none'; | |||||
| document.body.classList.remove('modal-open'); | |||||
| // Remove backdrop | |||||
| if (this.backdrop) { | |||||
| this.backdrop.remove(); | |||||
| this.backdrop = null; | |||||
| } | |||||
| this.isOpen = false; | |||||
| // Remove escape handler | |||||
| if (this.escapeHandler) { | |||||
| document.removeEventListener('keydown', this.escapeHandler); | |||||
| this.escapeHandler = null; | |||||
| } | |||||
| } | |||||
| private handleEscapeKey(e: KeyboardEvent): void { | |||||
| if (e.key === 'Escape') { | |||||
| this.hide(); | |||||
| } | |||||
| } | |||||
| public toggle(): void { | |||||
| if (this.isOpen) { | |||||
| this.hide(); | |||||
| } else { | |||||
| this.show(); | |||||
| } | |||||
| } | |||||
| public isVisible(): boolean { | |||||
| return this.isOpen; | |||||
| } | |||||
| } | |||||
| // Dropdown functionality | |||||
| export class VanillaDropdown implements ComponentInterface { | |||||
| public name: string = 'VanillaDropdown'; | |||||
| public element: HTMLElement; | |||||
| public options: DropdownOptions; | |||||
| public isInitialized: boolean = false; | |||||
| private menu: HTMLElement | null = null; | |||||
| private isOpen: boolean = false; | |||||
| private outsideClickHandler: ((e: Event) => void) | null = null; | |||||
| private escapeHandler: ((e: KeyboardEvent) => void) | null = null; | |||||
| constructor(element: HTMLElement, options: DropdownOptions = {}) { | |||||
| this.element = element; | |||||
| this.options = { | |||||
| offset: [0, 2], | |||||
| flip: true, | |||||
| boundary: 'clippingParents', | |||||
| ...options, | |||||
| }; | |||||
| this.init(); | |||||
| } | |||||
| public init(): void { | |||||
| const parent = this.element.parentNode as HTMLElement; | |||||
| if (!parent) return; | |||||
| this.menu = parent.querySelector('.dropdown-menu'); | |||||
| if (!this.menu) return; | |||||
| this.element.addEventListener('click', this.handleElementClick.bind(this)); | |||||
| // Setup event handlers | |||||
| this.outsideClickHandler = this.handleOutsideClick.bind(this); | |||||
| this.escapeHandler = this.handleEscapeKey.bind(this); | |||||
| this.isInitialized = true; | |||||
| } | |||||
| public destroy(): void { | |||||
| this.hide(); | |||||
| if (this.outsideClickHandler) { | |||||
| document.removeEventListener('click', this.outsideClickHandler); | |||||
| this.outsideClickHandler = null; | |||||
| } | |||||
| if (this.escapeHandler) { | |||||
| document.removeEventListener('keydown', this.escapeHandler); | |||||
| this.escapeHandler = null; | |||||
| } | |||||
| this.isInitialized = false; | |||||
| } | |||||
| private handleElementClick(e: Event): void { | |||||
| e.preventDefault(); | |||||
| e.stopPropagation(); | |||||
| this.toggle(); | |||||
| } | |||||
| private handleOutsideClick(e: Event): void { | |||||
| const parent = this.element.parentNode as HTMLElement; | |||||
| if (parent && !parent.contains(e.target as Node)) { | |||||
| this.hide(); | |||||
| } | |||||
| } | |||||
| private handleEscapeKey(e: KeyboardEvent): void { | |||||
| if (e.key === 'Escape' && this.isOpen) { | |||||
| this.hide(); | |||||
| } | |||||
| } | |||||
| public toggle(): void { | |||||
| if (this.isOpen) { | |||||
| this.hide(); | |||||
| } else { | |||||
| this.show(); | |||||
| } | |||||
| } | |||||
| public show(): void { | |||||
| if (this.isOpen || !this.menu) return; | |||||
| // Close other dropdowns | |||||
| document.querySelectorAll<HTMLElement>('.dropdown-menu.show').forEach(menu => { | |||||
| menu.classList.remove('show'); | |||||
| }); | |||||
| this.menu.classList.add('show'); | |||||
| this.element.setAttribute('aria-expanded', 'true'); | |||||
| this.isOpen = true; | |||||
| // Add event listeners | |||||
| if (this.outsideClickHandler) { | |||||
| document.addEventListener('click', this.outsideClickHandler); | |||||
| } | |||||
| if (this.escapeHandler) { | |||||
| document.addEventListener('keydown', this.escapeHandler); | |||||
| } | |||||
| } | |||||
| public hide(): void { | |||||
| if (!this.isOpen || !this.menu) return; | |||||
| this.menu.classList.remove('show'); | |||||
| this.element.setAttribute('aria-expanded', 'false'); | |||||
| this.isOpen = false; | |||||
| // Remove event listeners | |||||
| if (this.outsideClickHandler) { | |||||
| document.removeEventListener('click', this.outsideClickHandler); | |||||
| } | |||||
| if (this.escapeHandler) { | |||||
| document.removeEventListener('keydown', this.escapeHandler); | |||||
| } | |||||
| } | |||||
| } | |||||
| // Popover functionality | |||||
| export class VanillaPopover implements ComponentInterface { | |||||
| public name: string = 'VanillaPopover'; | |||||
| public element: HTMLElement; | |||||
| public options: PopoverOptions; | |||||
| public isInitialized: boolean = false; | |||||
| private popover: HTMLElement | null = null; | |||||
| private isOpen: boolean = false; | |||||
| private outsideClickHandler: ((e: Event) => void) | null = null; | |||||
| constructor(element: HTMLElement, options: PopoverOptions = {}) { | |||||
| this.element = element; | |||||
| this.options = { | |||||
| placement: 'top', | |||||
| trigger: 'click', | |||||
| html: false, | |||||
| animation: true, | |||||
| ...options, | |||||
| }; | |||||
| this.init(); | |||||
| } | |||||
| public init(): void { | |||||
| if (this.options.trigger === 'click') { | |||||
| this.element.addEventListener('click', this.handleElementClick.bind(this)); | |||||
| } else if (this.options.trigger === 'hover') { | |||||
| this.element.addEventListener('mouseenter', this.show.bind(this)); | |||||
| this.element.addEventListener('mouseleave', this.hide.bind(this)); | |||||
| } else if (this.options.trigger === 'focus') { | |||||
| this.element.addEventListener('focus', this.show.bind(this)); | |||||
| this.element.addEventListener('blur', this.hide.bind(this)); | |||||
| } | |||||
| this.outsideClickHandler = this.handleOutsideClick.bind(this); | |||||
| this.isInitialized = true; | |||||
| } | |||||
| public destroy(): void { | |||||
| this.hide(); | |||||
| if (this.outsideClickHandler) { | |||||
| document.removeEventListener('click', this.outsideClickHandler); | |||||
| this.outsideClickHandler = null; | |||||
| } | |||||
| this.isInitialized = false; | |||||
| } | |||||
| private handleElementClick(e: Event): void { | |||||
| e.preventDefault(); | |||||
| this.toggle(); | |||||
| } | |||||
| private handleOutsideClick(e: Event): void { | |||||
| if (!this.element.contains(e.target as Node) && | |||||
| (!this.popover || !this.popover.contains(e.target as Node))) { | |||||
| this.hide(); | |||||
| } | |||||
| } | |||||
| public toggle(): void { | |||||
| if (this.isOpen) { | |||||
| this.hide(); | |||||
| } else { | |||||
| this.show(); | |||||
| } | |||||
| } | |||||
| public show(): void { | |||||
| if (this.isOpen) return; | |||||
| // Close other popovers | |||||
| document.querySelectorAll<HTMLElement>('.popover').forEach(popover => { | |||||
| popover.remove(); | |||||
| }); | |||||
| const title = this.element.getAttribute('title') || this.element.getAttribute('data-bs-title'); | |||||
| const content = this.element.getAttribute('data-bs-content'); | |||||
| if (!content) return; | |||||
| this.popover = document.createElement('div'); | |||||
| this.popover.className = `popover bs-popover-${this.options.placement} show`; | |||||
| this.popover.style.position = 'absolute'; | |||||
| this.popover.style.zIndex = '1070'; | |||||
| this.popover.style.maxWidth = '276px'; | |||||
| this.popover.style.backgroundColor = '#fff'; | |||||
| this.popover.style.border = '1px solid rgba(0,0,0,.2)'; | |||||
| this.popover.style.borderRadius = '6px'; | |||||
| this.popover.style.boxShadow = '0 5px 10px rgba(0,0,0,.2)'; | |||||
| let popoverContent = ''; | |||||
| if (title) { | |||||
| popoverContent += `<div class="popover-header" style="padding: 8px 14px; margin-bottom: 0; font-size: 14px; background-color: #f7f7f7; border-bottom: 1px solid #ebebeb; border-radius: 5px 5px 0 0; font-weight: 600;">${title}</div>`; | |||||
| } | |||||
| popoverContent += `<div class="popover-body" style="padding: 9px 14px; word-wrap: break-word;">${content}</div>`; | |||||
| this.popover.innerHTML = popoverContent; | |||||
| document.body.appendChild(this.popover); | |||||
| // Position popover | |||||
| this.positionPopover(); | |||||
| this.isOpen = true; | |||||
| // Add outside click handler | |||||
| if (this.outsideClickHandler) { | |||||
| document.addEventListener('click', this.outsideClickHandler); | |||||
| } | |||||
| } | |||||
| public hide(): void { | |||||
| if (!this.isOpen) return; | |||||
| if (this.popover) { | |||||
| this.popover.remove(); | |||||
| this.popover = null; | |||||
| } | |||||
| this.isOpen = false; | |||||
| // Remove outside click handler | |||||
| if (this.outsideClickHandler) { | |||||
| document.removeEventListener('click', this.outsideClickHandler); | |||||
| } | |||||
| } | |||||
| private positionPopover(): void { | |||||
| if (!this.popover) return; | |||||
| const rect = this.element.getBoundingClientRect(); | |||||
| const popoverRect = this.popover.getBoundingClientRect(); | |||||
| switch (this.options.placement) { | |||||
| case 'top': | |||||
| this.popover.style.left = `${rect.left + (rect.width / 2) - (popoverRect.width / 2)}px`; | |||||
| this.popover.style.top = `${rect.top - popoverRect.height - 10}px`; | |||||
| break; | |||||
| case 'bottom': | |||||
| this.popover.style.left = `${rect.left + (rect.width / 2) - (popoverRect.width / 2)}px`; | |||||
| this.popover.style.top = `${rect.bottom + 10}px`; | |||||
| break; | |||||
| case 'left': | |||||
| this.popover.style.left = `${rect.left - popoverRect.width - 10}px`; | |||||
| this.popover.style.top = `${rect.top + (rect.height / 2) - (popoverRect.height / 2)}px`; | |||||
| break; | |||||
| case 'right': | |||||
| this.popover.style.left = `${rect.right + 10}px`; | |||||
| this.popover.style.top = `${rect.top + (rect.height / 2) - (popoverRect.height / 2)}px`; | |||||
| break; | |||||
| } | |||||
| } | |||||
| } | |||||
| // Tooltip functionality | |||||
| export class VanillaTooltip implements ComponentInterface { | |||||
| public name: string = 'VanillaTooltip'; | |||||
| public element: HTMLElement; | |||||
| public options: TooltipOptions; | |||||
| public isInitialized: boolean = false; | |||||
| private tooltip: HTMLElement | null = null; | |||||
| private showTimeout: number | null = null; | |||||
| private hideTimeout: number | null = null; | |||||
| constructor(element: HTMLElement, options: TooltipOptions = {}) { | |||||
| this.element = element; | |||||
| this.options = { | |||||
| placement: 'top', | |||||
| delay: 0, | |||||
| animation: true, | |||||
| ...options, | |||||
| }; | |||||
| this.init(); | |||||
| } | |||||
| public init(): void { | |||||
| this.element.addEventListener('mouseenter', this.handleMouseEnter.bind(this)); | |||||
| this.element.addEventListener('mouseleave', this.handleMouseLeave.bind(this)); | |||||
| this.element.addEventListener('focus', this.handleFocus.bind(this)); | |||||
| this.element.addEventListener('blur', this.handleBlur.bind(this)); | |||||
| this.isInitialized = true; | |||||
| } | |||||
| public destroy(): void { | |||||
| this.hide(); | |||||
| this.clearTimeouts(); | |||||
| this.isInitialized = false; | |||||
| } | |||||
| private handleMouseEnter(): void { | |||||
| this.clearTimeouts(); | |||||
| this.showTimeout = window.setTimeout(() => this.show(), this.options.delay); | |||||
| } | |||||
| private handleMouseLeave(): void { | |||||
| this.clearTimeouts(); | |||||
| this.hideTimeout = window.setTimeout(() => this.hide(), this.options.delay); | |||||
| } | |||||
| private handleFocus(): void { | |||||
| this.show(); | |||||
| } | |||||
| private handleBlur(): void { | |||||
| this.hide(); | |||||
| } | |||||
| private clearTimeouts(): void { | |||||
| if (this.showTimeout) { | |||||
| clearTimeout(this.showTimeout); | |||||
| this.showTimeout = null; | |||||
| } | |||||
| if (this.hideTimeout) { | |||||
| clearTimeout(this.hideTimeout); | |||||
| this.hideTimeout = null; | |||||
| } | |||||
| } | |||||
| public show(): void { | |||||
| if (this.tooltip) return; | |||||
| const title = this.element.getAttribute('title') || this.element.getAttribute('data-bs-title'); | |||||
| if (!title) return; | |||||
| this.tooltip = document.createElement('div'); | |||||
| this.tooltip.className = `tooltip bs-tooltip-${this.options.placement} show`; | |||||
| this.tooltip.style.position = 'absolute'; | |||||
| this.tooltip.style.zIndex = '1070'; | |||||
| this.tooltip.style.maxWidth = '200px'; | |||||
| this.tooltip.style.padding = '4px 8px'; | |||||
| this.tooltip.style.fontSize = '12px'; | |||||
| this.tooltip.style.backgroundColor = '#000'; | |||||
| this.tooltip.style.color = '#fff'; | |||||
| this.tooltip.style.borderRadius = '4px'; | |||||
| this.tooltip.style.pointerEvents = 'none'; | |||||
| this.tooltip.style.whiteSpace = 'nowrap'; | |||||
| this.tooltip.innerHTML = `<div class="tooltip-inner">${title}</div>`; | |||||
| document.body.appendChild(this.tooltip); | |||||
| // Position tooltip | |||||
| this.positionTooltip(); | |||||
| } | |||||
| public hide(): void { | |||||
| if (this.tooltip) { | |||||
| this.tooltip.remove(); | |||||
| this.tooltip = null; | |||||
| } | |||||
| } | |||||
| private positionTooltip(): void { | |||||
| if (!this.tooltip) return; | |||||
| const rect = this.element.getBoundingClientRect(); | |||||
| const tooltipRect = this.tooltip.getBoundingClientRect(); | |||||
| switch (this.options.placement) { | |||||
| case 'top': | |||||
| this.tooltip.style.left = `${rect.left + (rect.width / 2) - (tooltipRect.width / 2)}px`; | |||||
| this.tooltip.style.top = `${rect.top - tooltipRect.height - 5}px`; | |||||
| break; | |||||
| case 'bottom': | |||||
| this.tooltip.style.left = `${rect.left + (rect.width / 2) - (tooltipRect.width / 2)}px`; | |||||
| this.tooltip.style.top = `${rect.bottom + 5}px`; | |||||
| break; | |||||
| case 'left': | |||||
| this.tooltip.style.left = `${rect.left - tooltipRect.width - 5}px`; | |||||
| this.tooltip.style.top = `${rect.top + (rect.height / 2) - (tooltipRect.height / 2)}px`; | |||||
| break; | |||||
| case 'right': | |||||
| this.tooltip.style.left = `${rect.right + 5}px`; | |||||
| this.tooltip.style.top = `${rect.top + (rect.height / 2) - (tooltipRect.height / 2)}px`; | |||||
| break; | |||||
| } | |||||
| } | |||||
| } | |||||
| // Accordion functionality | |||||
| export class VanillaAccordion implements ComponentInterface { | |||||
| public name: string = 'VanillaAccordion'; | |||||
| public element: HTMLElement; | |||||
| public options: AccordionOptions; | |||||
| public isInitialized: boolean = false; | |||||
| private accordion: HTMLElement | null = null; | |||||
| private target: HTMLElement | null = null; | |||||
| private isOpen: boolean = false; | |||||
| constructor(element: HTMLElement, options: AccordionOptions = {}) { | |||||
| this.element = element; | |||||
| this.options = { | |||||
| toggle: true, | |||||
| ...options, | |||||
| }; | |||||
| this.init(); | |||||
| } | |||||
| public init(): void { | |||||
| this.accordion = this.element.closest('.accordion'); | |||||
| const targetSelector = this.element.getAttribute('data-bs-target'); | |||||
| if (!targetSelector) return; | |||||
| this.target = document.querySelector(targetSelector); | |||||
| if (!this.target) return; | |||||
| this.isOpen = !this.element.classList.contains('collapsed'); | |||||
| this.element.addEventListener('click', this.handleElementClick.bind(this)); | |||||
| this.isInitialized = true; | |||||
| } | |||||
| public destroy(): void { | |||||
| this.isInitialized = false; | |||||
| } | |||||
| private handleElementClick(e: Event): void { | |||||
| e.preventDefault(); | |||||
| this.toggle(); | |||||
| } | |||||
| public toggle(): void { | |||||
| if (this.isOpen) { | |||||
| this.hide(); | |||||
| } else { | |||||
| this.show(); | |||||
| } | |||||
| } | |||||
| public show(): void { | |||||
| if (this.isOpen || !this.target) return; | |||||
| // Close other accordion items in the same parent | |||||
| if (this.accordion) { | |||||
| const otherItems = this.accordion.querySelectorAll<HTMLElement>('.accordion-collapse.show'); | |||||
| otherItems.forEach(item => { | |||||
| if (item !== this.target) { | |||||
| item.classList.remove('show'); | |||||
| const button = this.accordion!.querySelector<HTMLElement>(`[data-bs-target="#${item.id}"]`); | |||||
| if (button) { | |||||
| button.classList.add('collapsed'); | |||||
| button.setAttribute('aria-expanded', 'false'); | |||||
| } | |||||
| } | |||||
| }); | |||||
| } | |||||
| // Show this item | |||||
| this.target.classList.add('show'); | |||||
| this.element.classList.remove('collapsed'); | |||||
| this.element.setAttribute('aria-expanded', 'true'); | |||||
| this.isOpen = true; | |||||
| } | |||||
| public hide(): void { | |||||
| if (!this.isOpen || !this.target) return; | |||||
| this.target.classList.remove('show'); | |||||
| this.element.classList.add('collapsed'); | |||||
| this.element.setAttribute('aria-expanded', 'false'); | |||||
| this.isOpen = false; | |||||
| } | |||||
| } | |||||
| // UI Manager Class | |||||
| export class UIManager { | |||||
| private components: Map<string, ComponentInterface> = new Map(); | |||||
| public initializeComponents(): void { | |||||
| // Initialize modals | |||||
| document.querySelectorAll<HTMLElement>('[data-bs-toggle="modal"]').forEach(element => { | |||||
| const modal = new VanillaModal(element); | |||||
| this.components.set(`modal-${element.id || Date.now()}`, modal); | |||||
| }); | |||||
| // Initialize dropdowns | |||||
| document.querySelectorAll<HTMLElement>('[data-bs-toggle="dropdown"]').forEach(element => { | |||||
| const dropdown = new VanillaDropdown(element); | |||||
| this.components.set(`dropdown-${element.id || Date.now()}`, dropdown); | |||||
| }); | |||||
| // Initialize popovers | |||||
| document.querySelectorAll<HTMLElement>('[data-bs-toggle="popover"]').forEach(element => { | |||||
| const popover = new VanillaPopover(element); | |||||
| this.components.set(`popover-${element.id || Date.now()}`, popover); | |||||
| }); | |||||
| // Initialize tooltips | |||||
| document.querySelectorAll<HTMLElement>('[data-bs-toggle="tooltip"]').forEach(element => { | |||||
| const tooltip = new VanillaTooltip(element); | |||||
| this.components.set(`tooltip-${element.id || Date.now()}`, tooltip); | |||||
| }); | |||||
| // Initialize accordions | |||||
| document.querySelectorAll<HTMLElement>('[data-bs-toggle="collapse"]').forEach(element => { | |||||
| const accordion = new VanillaAccordion(element); | |||||
| this.components.set(`accordion-${element.id || Date.now()}`, accordion); | |||||
| }); | |||||
| } | |||||
| public destroyComponents(): void { | |||||
| this.components.forEach(component => { | |||||
| component.destroy(); | |||||
| }); | |||||
| this.components.clear(); | |||||
| } | |||||
| public getComponent(id: string): ComponentInterface | undefined { | |||||
| return this.components.get(id); | |||||
| } | |||||
| } | |||||
| // Create and export singleton instance | |||||
| const uiManager = new UIManager(); | |||||
| // Initialize when DOM is ready | |||||
| const initializeUI = (): void => { | |||||
| uiManager.initializeComponents(); | |||||
| }; | |||||
| if (document.readyState === 'loading') { | |||||
| document.addEventListener('DOMContentLoaded', initializeUI); | |||||
| } else { | |||||
| initializeUI(); | |||||
| } | |||||
| // Export default object for compatibility | |||||
| export default { | |||||
| init: initializeUI, | |||||
| manager: uiManager, | |||||
| Modal: VanillaModal, | |||||
| Dropdown: VanillaDropdown, | |||||
| Popover: VanillaPopover, | |||||
| Tooltip: VanillaTooltip, | |||||
| Accordion: VanillaAccordion, | |||||
| }; | |||||
| @ -1,363 +0,0 @@ | |||||
| /** | |||||
| * Modern Date Utilities with TypeScript | |||||
| * Using Day.js (2KB) instead of Moment.js (67KB) - 97% size reduction | |||||
| * Provides consistent date formatting and manipulation across the application | |||||
| */ | |||||
| import dayjs, { Dayjs, ConfigType, UnitType, ManipulateType } from 'dayjs'; | |||||
| import utc from 'dayjs/plugin/utc'; | |||||
| import timezone from 'dayjs/plugin/timezone'; | |||||
| import relativeTime from 'dayjs/plugin/relativeTime'; | |||||
| import customParseFormat from 'dayjs/plugin/customParseFormat'; | |||||
| import advancedFormat from 'dayjs/plugin/advancedFormat'; | |||||
| import isBetween from 'dayjs/plugin/isBetween'; | |||||
| // Enable Day.js plugins | |||||
| dayjs.extend(utc); | |||||
| dayjs.extend(timezone); | |||||
| dayjs.extend(relativeTime); | |||||
| dayjs.extend(customParseFormat); | |||||
| dayjs.extend(advancedFormat); | |||||
| dayjs.extend(isBetween); | |||||
| // Type definitions | |||||
| export interface CalendarDay { | |||||
| date: string; | |||||
| day: number; | |||||
| isCurrentMonth: boolean; | |||||
| isToday: boolean; | |||||
| dayjs: Dayjs; | |||||
| } | |||||
| export interface CalendarMonth { | |||||
| month: string; | |||||
| year: number; | |||||
| monthIndex: number; | |||||
| days: CalendarDay[]; | |||||
| } | |||||
| export interface WeekDay { | |||||
| date: string; | |||||
| day: number; | |||||
| dayName: string; | |||||
| shortDayName: string; | |||||
| isToday: boolean; | |||||
| dayjs: Dayjs; | |||||
| } | |||||
| export interface WeekData { | |||||
| weekStart: string; | |||||
| weekEnd: string; | |||||
| days: WeekDay[]; | |||||
| } | |||||
| export interface ChartDatePoint { | |||||
| date: string; | |||||
| label: string; | |||||
| value: string; | |||||
| dayjs: Dayjs; | |||||
| } | |||||
| export type DateInput = ConfigType; | |||||
| export type DateUnit = UnitType; | |||||
| export type DateManipulateUnit = ManipulateType; | |||||
| export interface DateFormatters { | |||||
| shortDate: (date: DateInput) => string; | |||||
| longDate: (date: DateInput) => string; | |||||
| dateTime: (date: DateInput) => string; | |||||
| calendarDate: (date: DateInput) => string; | |||||
| calendarDateTime: (date: DateInput) => string; | |||||
| inputDate: (date: DateInput) => string; | |||||
| inputDateTime: (date: DateInput) => string; | |||||
| timeOnly: (date: DateInput) => string; | |||||
| monthYear: (date: DateInput) => string; | |||||
| dayMonth: (date: DateInput) => string; | |||||
| relative: (date: DateInput) => string; | |||||
| relativeCalendar: (date: DateInput) => string; | |||||
| } | |||||
| export interface DateCalendarUtils { | |||||
| getMonthData: (date?: DateInput) => CalendarMonth; | |||||
| getWeekData: (date?: DateInput) => WeekData; | |||||
| } | |||||
| export interface DateFormUtils { | |||||
| toInputValue: (date: DateInput) => string; | |||||
| toDateTimeInputValue: (date: DateInput) => string; | |||||
| fromInputValue: (value: string) => Dayjs; | |||||
| validateDateInput: (value: string) => boolean; | |||||
| } | |||||
| export interface DateChartUtils { | |||||
| generateDateRange: (start: DateInput, end: DateInput, interval?: DateManipulateUnit) => ChartDatePoint[]; | |||||
| getChartLabels: (period?: 'week' | 'month' | 'year') => string[]; | |||||
| } | |||||
| export interface DateTimezoneUtils { | |||||
| convert: (date: DateInput, tz: string) => Dayjs; | |||||
| utc: (date: DateInput) => Dayjs; | |||||
| local: (date: DateInput) => Dayjs; | |||||
| guess: () => string; | |||||
| } | |||||
| export interface DateUtilsInterface { | |||||
| now: () => Dayjs; | |||||
| parse: (input: DateInput, format?: string) => Dayjs; | |||||
| format: (date: DateInput, format?: string) => string; | |||||
| formatters: DateFormatters; | |||||
| add: (date: DateInput, amount: number, unit: DateManipulateUnit) => Dayjs; | |||||
| subtract: (date: DateInput, amount: number, unit: DateManipulateUnit) => Dayjs; | |||||
| startOf: (date: DateInput, unit: DateUnit) => Dayjs; | |||||
| endOf: (date: DateInput, unit: DateUnit) => Dayjs; | |||||
| isBefore: (date1: DateInput, date2: DateInput) => boolean; | |||||
| isAfter: (date1: DateInput, date2: DateInput) => boolean; | |||||
| isSame: (date1: DateInput, date2: DateInput, unit?: DateUnit) => boolean; | |||||
| isBetween: (date: DateInput, start: DateInput, end: DateInput) => boolean; | |||||
| isValid: (date: DateInput) => boolean; | |||||
| timezone: DateTimezoneUtils; | |||||
| calendar: DateCalendarUtils; | |||||
| form: DateFormUtils; | |||||
| charts: DateChartUtils; | |||||
| } | |||||
| export const DateUtils: DateUtilsInterface = { | |||||
| /** | |||||
| * Get current date/time | |||||
| */ | |||||
| now: (): Dayjs => dayjs(), | |||||
| /** | |||||
| * Parse date from string or Date object | |||||
| */ | |||||
| parse: (input: DateInput, format?: string): Dayjs => { | |||||
| return format ? dayjs(input, format) : dayjs(input); | |||||
| }, | |||||
| /** | |||||
| * Format date for display | |||||
| */ | |||||
| format: (date: DateInput, format: string = 'YYYY-MM-DD'): string => { | |||||
| return dayjs(date).format(format); | |||||
| }, | |||||
| /** | |||||
| * Common date formatting presets | |||||
| */ | |||||
| formatters: { | |||||
| // Dashboard display formats | |||||
| shortDate: (date: DateInput): string => dayjs(date).format('MMM DD, YYYY'), | |||||
| longDate: (date: DateInput): string => dayjs(date).format('MMMM DD, YYYY'), | |||||
| dateTime: (date: DateInput): string => dayjs(date).format('MMM DD, YYYY h:mm A'), | |||||
| // Calendar formats | |||||
| calendarDate: (date: DateInput): string => dayjs(date).format('YYYY-MM-DD'), | |||||
| calendarDateTime: (date: DateInput): string => dayjs(date).format('YYYY-MM-DD HH:mm:ss'), | |||||
| // Form input formats | |||||
| inputDate: (date: DateInput): string => dayjs(date).format('YYYY-MM-DD'), | |||||
| inputDateTime: (date: DateInput): string => dayjs(date).format('YYYY-MM-DDTHH:mm'), | |||||
| // Display formats | |||||
| timeOnly: (date: DateInput): string => dayjs(date).format('h:mm A'), | |||||
| monthYear: (date: DateInput): string => dayjs(date).format('MMMM YYYY'), | |||||
| dayMonth: (date: DateInput): string => dayjs(date).format('DD MMM'), | |||||
| // Relative time | |||||
| relative: (date: DateInput): string => dayjs(date).fromNow(), | |||||
| relativeCalendar: (date: DateInput): string => { | |||||
| const now = dayjs(); | |||||
| const target = dayjs(date); | |||||
| const diffDays = now.diff(target, 'day'); | |||||
| if (diffDays === 0) return 'Today'; | |||||
| if (diffDays === 1) return 'Yesterday'; | |||||
| if (diffDays === -1) return 'Tomorrow'; | |||||
| if (diffDays > 1 && diffDays < 7) return `${diffDays} days ago`; | |||||
| if (diffDays < -1 && diffDays > -7) return `In ${Math.abs(diffDays)} days`; | |||||
| return target.format('MMM DD, YYYY'); | |||||
| }, | |||||
| }, | |||||
| /** | |||||
| * Date manipulation | |||||
| */ | |||||
| add: (date: DateInput, amount: number, unit: DateManipulateUnit): Dayjs => | |||||
| dayjs(date).add(amount, unit), | |||||
| subtract: (date: DateInput, amount: number, unit: DateManipulateUnit): Dayjs => | |||||
| dayjs(date).subtract(amount, unit), | |||||
| startOf: (date: DateInput, unit: DateUnit): Dayjs => | |||||
| dayjs(date).startOf(unit), | |||||
| endOf: (date: DateInput, unit: DateUnit): Dayjs => | |||||
| dayjs(date).endOf(unit), | |||||
| /** | |||||
| * Date comparison | |||||
| */ | |||||
| isBefore: (date1: DateInput, date2: DateInput): boolean => | |||||
| dayjs(date1).isBefore(dayjs(date2)), | |||||
| isAfter: (date1: DateInput, date2: DateInput): boolean => | |||||
| dayjs(date1).isAfter(dayjs(date2)), | |||||
| isSame: (date1: DateInput, date2: DateInput, unit: DateUnit = 'day'): boolean => | |||||
| dayjs(date1).isSame(dayjs(date2), unit), | |||||
| isBetween: (date: DateInput, start: DateInput, end: DateInput): boolean => | |||||
| dayjs(date).isBetween(dayjs(start), dayjs(end)), | |||||
| /** | |||||
| * Date validation | |||||
| */ | |||||
| isValid: (date: DateInput): boolean => dayjs(date).isValid(), | |||||
| /** | |||||
| * Timezone utilities | |||||
| */ | |||||
| timezone: { | |||||
| convert: (date: DateInput, tz: string): Dayjs => dayjs(date).tz(tz), | |||||
| utc: (date: DateInput): Dayjs => dayjs(date).utc(), | |||||
| local: (date: DateInput): Dayjs => dayjs(date).local(), | |||||
| guess: (): string => dayjs.tz.guess(), | |||||
| }, | |||||
| /** | |||||
| * Calendar utilities | |||||
| */ | |||||
| calendar: { | |||||
| // Get calendar month data for building calendar views | |||||
| getMonthData: (date?: DateInput): CalendarMonth => { | |||||
| const target = date ? dayjs(date) : dayjs(); | |||||
| const startOfMonth = target.startOf('month'); | |||||
| const endOfMonth = target.endOf('month'); | |||||
| const startOfCalendar = startOfMonth.startOf('week'); | |||||
| const endOfCalendar = endOfMonth.endOf('week'); | |||||
| const days: CalendarDay[] = []; | |||||
| let current = startOfCalendar; | |||||
| while (current.isBefore(endOfCalendar) || current.isSame(endOfCalendar, 'day')) { | |||||
| days.push({ | |||||
| date: current.format('YYYY-MM-DD'), | |||||
| day: current.date(), | |||||
| isCurrentMonth: current.isSame(target, 'month'), | |||||
| isToday: current.isSame(dayjs(), 'day'), | |||||
| dayjs: current.clone(), | |||||
| }); | |||||
| current = current.add(1, 'day'); | |||||
| } | |||||
| return { | |||||
| month: target.format('MMMM YYYY'), | |||||
| year: target.year(), | |||||
| monthIndex: target.month(), | |||||
| days, | |||||
| }; | |||||
| }, | |||||
| // Get week data | |||||
| getWeekData: (date?: DateInput): WeekData => { | |||||
| const target = date ? dayjs(date) : dayjs(); | |||||
| const startOfWeek = target.startOf('week'); | |||||
| const endOfWeek = target.endOf('week'); | |||||
| const days: WeekDay[] = []; | |||||
| let current = startOfWeek; | |||||
| while (current.isBefore(endOfWeek) || current.isSame(endOfWeek, 'day')) { | |||||
| days.push({ | |||||
| date: current.format('YYYY-MM-DD'), | |||||
| day: current.date(), | |||||
| dayName: current.format('dddd'), | |||||
| shortDayName: current.format('ddd'), | |||||
| isToday: current.isSame(dayjs(), 'day'), | |||||
| dayjs: current.clone(), | |||||
| }); | |||||
| current = current.add(1, 'day'); | |||||
| } | |||||
| return { | |||||
| weekStart: startOfWeek.format('MMM DD'), | |||||
| weekEnd: endOfWeek.format('MMM DD, YYYY'), | |||||
| days, | |||||
| }; | |||||
| }, | |||||
| }, | |||||
| /** | |||||
| * Form utilities | |||||
| */ | |||||
| form: { | |||||
| // Convert date to HTML5 input format | |||||
| toInputValue: (date: DateInput): string => dayjs(date).format('YYYY-MM-DD'), | |||||
| toDateTimeInputValue: (date: DateInput): string => dayjs(date).format('YYYY-MM-DDTHH:mm'), | |||||
| // Parse from HTML5 input | |||||
| fromInputValue: (value: string): Dayjs => dayjs(value), | |||||
| // Validate date input | |||||
| validateDateInput: (value: string): boolean => { | |||||
| const parsed = dayjs(value); | |||||
| return parsed.isValid() && value.length >= 8; // Basic validation | |||||
| }, | |||||
| }, | |||||
| /** | |||||
| * Chart/Data utilities | |||||
| */ | |||||
| charts: { | |||||
| // Generate date ranges for charts | |||||
| generateDateRange: ( | |||||
| start: DateInput, | |||||
| end: DateInput, | |||||
| interval: DateManipulateUnit = 'day' | |||||
| ): ChartDatePoint[] => { | |||||
| const dates: ChartDatePoint[] = []; | |||||
| let current = dayjs(start); | |||||
| const endDate = dayjs(end); | |||||
| while (current.isBefore(endDate) || current.isSame(endDate, interval)) { | |||||
| dates.push({ | |||||
| date: current.format('YYYY-MM-DD'), | |||||
| label: current.format('MMM DD'), | |||||
| value: current.toISOString(), | |||||
| dayjs: current.clone(), | |||||
| }); | |||||
| current = current.add(1, interval); | |||||
| } | |||||
| return dates; | |||||
| }, | |||||
| // Get common chart date labels | |||||
| getChartLabels: (period: 'week' | 'month' | 'year' = 'week'): string[] => { | |||||
| const now = dayjs(); | |||||
| switch (period) { | |||||
| case 'week': | |||||
| return Array.from({ length: 7 }, (_, i) => | |||||
| now.subtract(6 - i, 'day').format('ddd') | |||||
| ); | |||||
| case 'month': | |||||
| return Array.from({ length: 30 }, (_, i) => | |||||
| now.subtract(29 - i, 'day').format('DD') | |||||
| ); | |||||
| case 'year': | |||||
| return Array.from({ length: 12 }, (_, i) => | |||||
| now.subtract(11 - i, 'month').format('MMM') | |||||
| ); | |||||
| default: | |||||
| return []; | |||||
| } | |||||
| }, | |||||
| }, | |||||
| }; | |||||
| // Export dayjs instance for direct use when needed | |||||
| export { dayjs }; | |||||
| // Default export | |||||
| export default DateUtils; | |||||
| @ -1,513 +0,0 @@ | |||||
| /** | |||||
| * DOM Utility Functions | |||||
| * Provides jQuery-like functionality using vanilla JavaScript with TypeScript support | |||||
| */ | |||||
| import type { DOMUtilities, AnimationOptions } from '../../../types'; | |||||
| export type ElementSelector = string | Element | null; | |||||
| interface ElementDimensions { | |||||
| width: number; | |||||
| height: number; | |||||
| top: number; | |||||
| left: number; | |||||
| bottom: number; | |||||
| right: number; | |||||
| } | |||||
| interface SlideAnimationKeyframes { | |||||
| height: string; | |||||
| } | |||||
| interface FadeAnimationKeyframes { | |||||
| opacity: number; | |||||
| } | |||||
| /** | |||||
| * Convert string selector to element or return element as-is | |||||
| */ | |||||
| function getElement(element: ElementSelector): Element | null { | |||||
| if (typeof element === 'string') { | |||||
| return document.querySelector(element); | |||||
| } | |||||
| return element; | |||||
| } | |||||
| /** | |||||
| * DOM Utility object with type-safe methods | |||||
| */ | |||||
| export const DOM: DOMUtilities = { | |||||
| /** | |||||
| * Document ready (replaces $(document).ready()) | |||||
| */ | |||||
| ready: (callback: () => void): void => { | |||||
| if (document.readyState === 'loading') { | |||||
| document.addEventListener('DOMContentLoaded', callback); | |||||
| } else { | |||||
| callback(); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Select single element (replaces $('selector')) | |||||
| */ | |||||
| select: (selector: string, context: Element | Document = document): HTMLElement | null => { | |||||
| return context.querySelector(selector); | |||||
| }, | |||||
| /** | |||||
| * Select multiple elements (replaces $('selector')) | |||||
| */ | |||||
| selectAll: (selector: string, context: Element | Document = document): HTMLElement[] => { | |||||
| return Array.from(context.querySelectorAll(selector)); | |||||
| }, | |||||
| /** | |||||
| * Check if element exists | |||||
| */ | |||||
| exists: (selector: string, context: Element | Document = document): boolean => { | |||||
| return context.querySelector(selector) !== null; | |||||
| }, | |||||
| /** | |||||
| * Add event listener (replaces $.on()) | |||||
| */ | |||||
| on: ( | |||||
| element: Element | Window | Document, | |||||
| event: string, | |||||
| handler: (event: Event) => void, | |||||
| options: AddEventListenerOptions = {} | |||||
| ): void => { | |||||
| if (element) { | |||||
| element.addEventListener(event, handler, options); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Remove event listener (replaces $.off()) | |||||
| */ | |||||
| off: ( | |||||
| element: Element | Window | Document, | |||||
| event: string, | |||||
| handler: (event: Event) => void | |||||
| ): void => { | |||||
| if (element) { | |||||
| element.removeEventListener(event, handler); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Add class (replaces $.addClass()) | |||||
| */ | |||||
| addClass: (element: Element, className: string): void => { | |||||
| const el = getElement(element); | |||||
| if (el) { | |||||
| el.classList.add(className); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Remove class (replaces $.removeClass()) | |||||
| */ | |||||
| removeClass: (element: Element, className: string): void => { | |||||
| const el = getElement(element); | |||||
| if (el) { | |||||
| el.classList.remove(className); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Toggle class (replaces $.toggleClass()) | |||||
| */ | |||||
| toggleClass: (element: Element, className: string): void => { | |||||
| const el = getElement(element); | |||||
| if (el) { | |||||
| el.classList.toggle(className); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Check if element has class (replaces $.hasClass()) | |||||
| */ | |||||
| hasClass: (element: Element, className: string): boolean => { | |||||
| const el = getElement(element); | |||||
| return el ? el.classList.contains(className) : false; | |||||
| }, | |||||
| /** | |||||
| * Get/Set attribute (replaces $.attr()) | |||||
| */ | |||||
| attr: (element: Element, name: string, value?: string): string | void => { | |||||
| const el = getElement(element); | |||||
| if (!el) return; | |||||
| if (value === undefined) { | |||||
| return el.getAttribute(name) || ''; | |||||
| } else { | |||||
| el.setAttribute(name, value); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Get/Set data attribute (replaces $.data()) | |||||
| */ | |||||
| data: (element: Element, name: string, value?: any): any => { | |||||
| const el = getElement(element); | |||||
| if (!el) return null; | |||||
| const dataName = `data-${name}`; | |||||
| if (value === undefined) { | |||||
| const attrValue = el.getAttribute(dataName); | |||||
| // Try to parse JSON for complex data | |||||
| if (attrValue) { | |||||
| try { | |||||
| return JSON.parse(attrValue); | |||||
| } catch { | |||||
| return attrValue; | |||||
| } | |||||
| } | |||||
| return null; | |||||
| } else { | |||||
| const stringValue = typeof value === 'object' ? JSON.stringify(value) : String(value); | |||||
| el.setAttribute(dataName, stringValue); | |||||
| } | |||||
| }, | |||||
| }; | |||||
| /** | |||||
| * Extended DOM utilities with additional functionality | |||||
| */ | |||||
| export const DOMExtended = { | |||||
| ...DOM, | |||||
| /** | |||||
| * Get/Set text content (replaces $.text()) | |||||
| */ | |||||
| text: (element: ElementSelector, content?: string): string | void => { | |||||
| const el = getElement(element); | |||||
| if (!el) return; | |||||
| if (content === undefined) { | |||||
| return el.textContent || ''; | |||||
| } else { | |||||
| el.textContent = content; | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Get/Set HTML content (replaces $.html()) | |||||
| */ | |||||
| html: (element: ElementSelector, content?: string): string | void => { | |||||
| const el = getElement(element); | |||||
| if (!el) return; | |||||
| if (content === undefined) { | |||||
| return (el as HTMLElement).innerHTML; | |||||
| } else { | |||||
| (el as HTMLElement).innerHTML = content; | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Hide element (replaces $.hide()) | |||||
| */ | |||||
| hide: (element: ElementSelector): void => { | |||||
| const el = getElement(element) as HTMLElement; | |||||
| if (el) { | |||||
| el.style.display = 'none'; | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Show element (replaces $.show()) | |||||
| */ | |||||
| show: (element: ElementSelector, display: string = 'block'): void => { | |||||
| const el = getElement(element) as HTMLElement; | |||||
| if (el) { | |||||
| el.style.display = display; | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Toggle visibility (replaces $.toggle()) | |||||
| */ | |||||
| toggle: (element: ElementSelector, display: string = 'block'): void => { | |||||
| const el = getElement(element) as HTMLElement; | |||||
| if (el) { | |||||
| if (el.style.display === 'none') { | |||||
| el.style.display = display; | |||||
| } else { | |||||
| el.style.display = 'none'; | |||||
| } | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Slide up animation (replaces $.slideUp()) | |||||
| */ | |||||
| slideUp: (element: ElementSelector, duration: number = 300): Promise<void> => { | |||||
| const el = getElement(element) as HTMLElement; | |||||
| if (!el) return Promise.resolve(); | |||||
| return new Promise((resolve) => { | |||||
| const height = el.scrollHeight; | |||||
| el.style.height = `${height}px`; | |||||
| el.style.overflow = 'hidden'; | |||||
| const animation = el.animate([ | |||||
| { height: `${height}px` } as SlideAnimationKeyframes, | |||||
| { height: '0px' } as SlideAnimationKeyframes, | |||||
| ], { | |||||
| duration, | |||||
| easing: 'ease-in-out', | |||||
| }); | |||||
| animation.onfinish = (): void => { | |||||
| el.style.display = 'none'; | |||||
| el.style.height = ''; | |||||
| el.style.overflow = ''; | |||||
| resolve(); | |||||
| }; | |||||
| }); | |||||
| }, | |||||
| /** | |||||
| * Slide down animation (replaces $.slideDown()) | |||||
| */ | |||||
| slideDown: (element: ElementSelector, duration: number = 300): Promise<void> => { | |||||
| const el = getElement(element) as HTMLElement; | |||||
| if (!el) return Promise.resolve(); | |||||
| return new Promise((resolve) => { | |||||
| el.style.display = 'block'; | |||||
| el.style.height = '0px'; | |||||
| el.style.overflow = 'hidden'; | |||||
| const height = el.scrollHeight; | |||||
| const animation = el.animate([ | |||||
| { height: '0px' } as SlideAnimationKeyframes, | |||||
| { height: `${height}px` } as SlideAnimationKeyframes, | |||||
| ], { | |||||
| duration, | |||||
| easing: 'ease-in-out', | |||||
| }); | |||||
| animation.onfinish = (): void => { | |||||
| el.style.height = 'auto'; | |||||
| el.style.overflow = 'visible'; | |||||
| resolve(); | |||||
| }; | |||||
| }); | |||||
| }, | |||||
| /** | |||||
| * Fade in animation (replaces $.fadeIn()) | |||||
| */ | |||||
| fadeIn: (element: ElementSelector, duration: number = 300): Promise<void> => { | |||||
| const el = getElement(element) as HTMLElement; | |||||
| if (!el) return Promise.resolve(); | |||||
| return new Promise((resolve) => { | |||||
| el.style.opacity = '0'; | |||||
| el.style.display = 'block'; | |||||
| const animation = el.animate([ | |||||
| { opacity: 0 } as FadeAnimationKeyframes, | |||||
| { opacity: 1 } as FadeAnimationKeyframes, | |||||
| ], { | |||||
| duration, | |||||
| easing: 'ease-in-out', | |||||
| }); | |||||
| animation.onfinish = (): void => { | |||||
| el.style.opacity = ''; | |||||
| resolve(); | |||||
| }; | |||||
| }); | |||||
| }, | |||||
| /** | |||||
| * Fade out animation (replaces $.fadeOut()) | |||||
| */ | |||||
| fadeOut: (element: ElementSelector, duration: number = 300): Promise<void> => { | |||||
| const el = getElement(element) as HTMLElement; | |||||
| if (!el) return Promise.resolve(); | |||||
| return new Promise((resolve) => { | |||||
| const animation = el.animate([ | |||||
| { opacity: 1 } as FadeAnimationKeyframes, | |||||
| { opacity: 0 } as FadeAnimationKeyframes, | |||||
| ], { | |||||
| duration, | |||||
| easing: 'ease-in-out', | |||||
| }); | |||||
| animation.onfinish = (): void => { | |||||
| el.style.display = 'none'; | |||||
| el.style.opacity = ''; | |||||
| resolve(); | |||||
| }; | |||||
| }); | |||||
| }, | |||||
| /** | |||||
| * Get element dimensions and position | |||||
| */ | |||||
| dimensions: (element: ElementSelector): ElementDimensions | null => { | |||||
| const el = getElement(element); | |||||
| if (!el) return null; | |||||
| const rect = el.getBoundingClientRect(); | |||||
| return { | |||||
| width: rect.width, | |||||
| height: rect.height, | |||||
| top: rect.top, | |||||
| left: rect.left, | |||||
| bottom: rect.bottom, | |||||
| right: rect.right, | |||||
| }; | |||||
| }, | |||||
| /** | |||||
| * Wait for DOM to be ready (replaces $(document).ready()) | |||||
| */ | |||||
| ready: (callback: () => void): void => { | |||||
| if (document.readyState === 'loading') { | |||||
| document.addEventListener('DOMContentLoaded', callback); | |||||
| } else { | |||||
| callback(); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Create element with attributes and content | |||||
| */ | |||||
| create: (tagName: string, attributes?: Record<string, string>, content?: string): HTMLElement => { | |||||
| const element = document.createElement(tagName); | |||||
| if (attributes) { | |||||
| Object.entries(attributes).forEach(([key, value]) => { | |||||
| element.setAttribute(key, value); | |||||
| }); | |||||
| } | |||||
| if (content) { | |||||
| element.textContent = content; | |||||
| } | |||||
| return element; | |||||
| }, | |||||
| /** | |||||
| * Append element to parent | |||||
| */ | |||||
| append: (parent: ElementSelector, child: Element): void => { | |||||
| const parentEl = getElement(parent); | |||||
| if (parentEl) { | |||||
| parentEl.appendChild(child); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Remove element from DOM | |||||
| */ | |||||
| remove: (element: ElementSelector): void => { | |||||
| const el = getElement(element); | |||||
| if (el && el.parentNode) { | |||||
| el.parentNode.removeChild(el); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Get/Set CSS styles | |||||
| */ | |||||
| css: (element: ElementSelector, property: string, value?: string): string | void => { | |||||
| const el = getElement(element) as HTMLElement; | |||||
| if (!el) return; | |||||
| if (value === undefined) { | |||||
| return window.getComputedStyle(el).getPropertyValue(property); | |||||
| } else { | |||||
| el.style.setProperty(property, value); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Get/Set element value (for form elements) | |||||
| */ | |||||
| val: (element: ElementSelector, value?: string): string | void => { | |||||
| const el = getElement(element) as HTMLInputElement; | |||||
| if (!el) return; | |||||
| if (value === undefined) { | |||||
| return el.value; | |||||
| } else { | |||||
| el.value = value; | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Trigger custom event | |||||
| */ | |||||
| trigger: (element: ElementSelector, eventName: string, detail?: any): void => { | |||||
| const el = getElement(element); | |||||
| if (el) { | |||||
| const event = new CustomEvent(eventName, { detail }); | |||||
| el.dispatchEvent(event); | |||||
| } | |||||
| }, | |||||
| /** | |||||
| * Check if element is visible | |||||
| */ | |||||
| isVisible: (element: ElementSelector): boolean => { | |||||
| const el = getElement(element) as HTMLElement; | |||||
| if (!el) return false; | |||||
| const style = window.getComputedStyle(el); | |||||
| return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0'; | |||||
| }, | |||||
| /** | |||||
| * Get element offset relative to document | |||||
| */ | |||||
| offset: (element: ElementSelector): { top: number; left: number } | null => { | |||||
| const el = getElement(element); | |||||
| if (!el) return null; | |||||
| const rect = el.getBoundingClientRect(); | |||||
| return { | |||||
| top: rect.top + window.pageYOffset, | |||||
| left: rect.left + window.pageXOffset, | |||||
| }; | |||||
| }, | |||||
| /** | |||||
| * Delegate event handling | |||||
| */ | |||||
| delegate: ( | |||||
| parent: ElementSelector, | |||||
| selector: string, | |||||
| event: string, | |||||
| handler: (event: Event) => void | |||||
| ): void => { | |||||
| const parentEl = getElement(parent); | |||||
| if (parentEl) { | |||||
| parentEl.addEventListener(event, (e) => { | |||||
| const target = e.target as Element; | |||||
| if (target && target.matches(selector)) { | |||||
| handler(e); | |||||
| } | |||||
| }); | |||||
| } | |||||
| }, | |||||
| }; | |||||
| // Export both the basic DOM utilities and extended version | |||||
| export { DOM as default, DOMExtended }; | |||||
| // Re-export types for convenience | |||||
| export type { DOMUtilities, ElementSelector, ElementDimensions }; | |||||
| @ -1,313 +0,0 @@ | |||||
| /** | |||||
| * Theme Management Utilities | |||||
| * Handles light/dark mode switching with Chart.js integration | |||||
| */ | |||||
| import type { Theme, ThemeConfig, ThemeColors, ThemeChangeEvent } from '../../../types'; | |||||
| declare global { | |||||
| interface Window { | |||||
| Chart?: any; // Chart.js global object | |||||
| } | |||||
| } | |||||
| interface VectorMapColors { | |||||
| backgroundColor: string; | |||||
| borderColor: string; | |||||
| regionColor: string; | |||||
| markerFill: string; | |||||
| markerStroke: string; | |||||
| hoverColor: string; | |||||
| selectedColor: string; | |||||
| scaleStart: string; | |||||
| scaleEnd: string; | |||||
| scaleLight: string; | |||||
| scaleDark: string; | |||||
| } | |||||
| interface SparklineColors { | |||||
| success: string; | |||||
| purple: string; | |||||
| info: string; | |||||
| danger: string; | |||||
| light: string; | |||||
| } | |||||
| interface ChartThemeColors { | |||||
| textColor: string; | |||||
| mutedColor: string; | |||||
| borderColor: string; | |||||
| gridColor: string; | |||||
| tooltipBg: string; | |||||
| } | |||||
| const THEME_KEY = 'adminator-theme'; | |||||
| /** | |||||
| * Theme Management Class | |||||
| */ | |||||
| class ThemeManager { | |||||
| private currentTheme: Theme = 'light'; | |||||
| private config: ThemeConfig; | |||||
| constructor(config?: Partial<ThemeConfig>) { | |||||
| this.config = { | |||||
| theme: 'light', | |||||
| autoDetect: true, | |||||
| persistChoice: true, | |||||
| ...config, | |||||
| }; | |||||
| } | |||||
| /** | |||||
| * Apply theme to the application | |||||
| */ | |||||
| apply(theme: Theme): void { | |||||
| const previousTheme = this.currentTheme; | |||||
| this.currentTheme = theme; | |||||
| // Set theme attribute on document element | |||||
| document.documentElement.setAttribute('data-theme', theme); | |||||
| // Update Chart.js defaults if Chart is available | |||||
| this.updateChartDefaults(theme); | |||||
| // Persist theme choice if enabled | |||||
| if (this.config.persistChoice) { | |||||
| this.persistTheme(theme); | |||||
| } | |||||
| // Dispatch theme change event | |||||
| this.dispatchThemeChange(theme, previousTheme); | |||||
| } | |||||
| /** | |||||
| * Toggle between light and dark themes | |||||
| */ | |||||
| toggle(): void { | |||||
| const nextTheme: Theme = this.currentTheme === 'dark' ? 'light' : 'dark'; | |||||
| this.apply(nextTheme); | |||||
| } | |||||
| /** | |||||
| * Get current theme | |||||
| */ | |||||
| current(): Theme { | |||||
| return this.currentTheme; | |||||
| } | |||||
| /** | |||||
| * Initialize theme system | |||||
| */ | |||||
| init(): void { | |||||
| let initialTheme: Theme = 'light'; | |||||
| // Try to load persisted theme | |||||
| if (this.config.persistChoice) { | |||||
| const persistedTheme = this.getPersistedTheme(); | |||||
| if (persistedTheme) { | |||||
| initialTheme = persistedTheme; | |||||
| } else if (this.config.autoDetect) { | |||||
| // Detect OS preference on first visit | |||||
| initialTheme = this.detectOSPreference(); | |||||
| } | |||||
| } | |||||
| this.apply(initialTheme); | |||||
| } | |||||
| /** | |||||
| * Get CSS custom property value | |||||
| */ | |||||
| getCSSVar(varName: string): string { | |||||
| return getComputedStyle(document.documentElement) | |||||
| .getPropertyValue(varName) | |||||
| .trim(); | |||||
| } | |||||
| /** | |||||
| * Get vector map theme colors | |||||
| */ | |||||
| getVectorMapColors(): VectorMapColors { | |||||
| return { | |||||
| backgroundColor: this.getCSSVar('--vmap-bg-color'), | |||||
| borderColor: this.getCSSVar('--vmap-border-color'), | |||||
| regionColor: this.getCSSVar('--vmap-region-color'), | |||||
| markerFill: this.getCSSVar('--vmap-marker-fill'), | |||||
| markerStroke: this.getCSSVar('--vmap-marker-stroke'), | |||||
| hoverColor: this.getCSSVar('--vmap-hover-color'), | |||||
| selectedColor: this.getCSSVar('--vmap-selected-color'), | |||||
| scaleStart: this.getCSSVar('--vmap-scale-start'), | |||||
| scaleEnd: this.getCSSVar('--vmap-scale-end'), | |||||
| scaleLight: this.getCSSVar('--vmap-scale-light'), | |||||
| scaleDark: this.getCSSVar('--vmap-scale-dark'), | |||||
| }; | |||||
| } | |||||
| /** | |||||
| * Get sparkline theme colors | |||||
| */ | |||||
| getSparklineColors(): SparklineColors { | |||||
| return { | |||||
| success: this.getCSSVar('--sparkline-success'), | |||||
| purple: this.getCSSVar('--sparkline-purple'), | |||||
| info: this.getCSSVar('--sparkline-info'), | |||||
| danger: this.getCSSVar('--sparkline-danger'), | |||||
| light: this.getCSSVar('--sparkline-light'), | |||||
| }; | |||||
| } | |||||
| /** | |||||
| * Get chart theme colors | |||||
| */ | |||||
| getChartColors(): ChartThemeColors { | |||||
| const isDark = this.currentTheme === 'dark'; | |||||
| return { | |||||
| textColor: isDark ? '#FFFFFF' : '#212529', | |||||
| mutedColor: isDark ? '#D1D5DB' : '#6C757D', | |||||
| borderColor: isDark ? '#374151' : '#E2E5E8', | |||||
| gridColor: isDark ? 'rgba(209, 213, 219, 0.15)' : 'rgba(0, 0, 0, 0.05)', | |||||
| tooltipBg: isDark ? '#1F2937' : 'rgba(255, 255, 255, 0.95)', | |||||
| }; | |||||
| } | |||||
| /** | |||||
| * Update configuration | |||||
| */ | |||||
| updateConfig(config: Partial<ThemeConfig>): void { | |||||
| this.config = { ...this.config, ...config }; | |||||
| } | |||||
| /** | |||||
| * Get current configuration | |||||
| */ | |||||
| getConfig(): ThemeConfig { | |||||
| return { ...this.config }; | |||||
| } | |||||
| /** | |||||
| * Private method: Update Chart.js defaults | |||||
| */ | |||||
| private updateChartDefaults(theme: Theme): void { | |||||
| if (!window.Chart || !window.Chart.defaults) { | |||||
| return; | |||||
| } | |||||
| const isDark = theme === 'dark'; | |||||
| const colors = this.getChartColors(); | |||||
| try { | |||||
| // Set global defaults | |||||
| window.Chart.defaults.color = colors.textColor; | |||||
| window.Chart.defaults.borderColor = colors.borderColor; | |||||
| window.Chart.defaults.backgroundColor = colors.tooltipBg; | |||||
| // Set plugin defaults | |||||
| if (window.Chart.defaults.plugins?.legend?.labels) { | |||||
| window.Chart.defaults.plugins.legend.labels.color = colors.textColor; | |||||
| } | |||||
| if (window.Chart.defaults.plugins?.tooltip) { | |||||
| window.Chart.defaults.plugins.tooltip.backgroundColor = colors.tooltipBg; | |||||
| window.Chart.defaults.plugins.tooltip.titleColor = colors.textColor; | |||||
| window.Chart.defaults.plugins.tooltip.bodyColor = colors.textColor; | |||||
| window.Chart.defaults.plugins.tooltip.borderColor = colors.borderColor; | |||||
| } | |||||
| // Set scale defaults | |||||
| const scaleDefaults = window.Chart.defaults.scales; | |||||
| if (scaleDefaults) { | |||||
| Object.keys(scaleDefaults).forEach(scaleType => { | |||||
| const scale = scaleDefaults[scaleType]; | |||||
| if (scale?.ticks) { | |||||
| scale.ticks.color = colors.mutedColor; | |||||
| } | |||||
| if (scale?.grid) { | |||||
| scale.grid.color = colors.gridColor; | |||||
| } | |||||
| if (scale?.pointLabels) { | |||||
| scale.pointLabels.color = colors.mutedColor; | |||||
| } | |||||
| if (scale?.angleLines) { | |||||
| scale.angleLines.color = colors.gridColor; | |||||
| } | |||||
| }); | |||||
| } | |||||
| } catch (error) { | |||||
| console.warn('Error updating Chart.js defaults:', error); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Private method: Persist theme to localStorage | |||||
| */ | |||||
| private persistTheme(theme: Theme): void { | |||||
| try { | |||||
| localStorage.setItem(THEME_KEY, theme); | |||||
| } catch (error) { | |||||
| console.warn('Unable to persist theme:', error); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Private method: Get persisted theme from localStorage | |||||
| */ | |||||
| private getPersistedTheme(): Theme | null { | |||||
| try { | |||||
| const theme = localStorage.getItem(THEME_KEY) as Theme; | |||||
| return ['light', 'dark'].includes(theme) ? theme : null; | |||||
| } catch (error) { | |||||
| console.warn('Unable to get persisted theme:', error); | |||||
| return null; | |||||
| } | |||||
| } | |||||
| /** | |||||
| * Private method: Detect OS color scheme preference | |||||
| */ | |||||
| private detectOSPreference(): Theme { | |||||
| if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { | |||||
| return 'dark'; | |||||
| } | |||||
| return 'light'; | |||||
| } | |||||
| /** | |||||
| * Private method: Dispatch theme change event | |||||
| */ | |||||
| private dispatchThemeChange(theme: Theme, previousTheme: Theme): void { | |||||
| const event: ThemeChangeEvent = new CustomEvent('adminator:themeChanged', { | |||||
| detail: { theme, previousTheme }, | |||||
| }) as ThemeChangeEvent; | |||||
| window.dispatchEvent(event); | |||||
| } | |||||
| } | |||||
| // Create singleton instance | |||||
| const themeManager = new ThemeManager(); | |||||
| // Export legacy object interface for compatibility | |||||
| export const Theme = { | |||||
| apply: (theme: Theme) => themeManager.apply(theme), | |||||
| toggle: () => themeManager.toggle(), | |||||
| current: () => themeManager.current(), | |||||
| init: () => themeManager.init(), | |||||
| getCSSVar: (varName: string) => themeManager.getCSSVar(varName), | |||||
| getVectorMapColors: () => themeManager.getVectorMapColors(), | |||||
| getSparklineColors: () => themeManager.getSparklineColors(), | |||||
| getChartColors: () => themeManager.getChartColors(), | |||||
| }; | |||||
| // Export both the manager instance and legacy interface | |||||
| export { themeManager as ThemeManager }; | |||||
| export default Theme; | |||||
| // Export types for external use | |||||
| export type { | |||||
| Theme as ThemeType, | |||||
| ThemeConfig, | |||||
| VectorMapColors, | |||||
| SparklineColors, | |||||
| ChartThemeColors, | |||||
| }; | |||||
| @ -1,542 +0,0 @@ | |||||
| /** | |||||
| * Vector Maps Implementation with TypeScript | |||||
| * Interactive world map using JSVectorMap with theme support | |||||
| */ | |||||
| import jsVectorMap from 'jsvectormap'; | |||||
| import 'jsvectormap/dist/jsvectormap.css'; | |||||
| import 'jsvectormap/dist/maps/world.js'; | |||||
| import { debounce } from 'lodash'; | |||||
| import { ThemeManager } from '../utils/theme'; | |||||
| import type { ComponentInterface } from '../../types'; | |||||
| // Type definitions for Vector Maps | |||||
| export interface VectorMapMarker { | |||||
| name: string; | |||||
| coords: [number, number]; | |||||
| data?: any; | |||||
| } | |||||
| export interface VectorMapColors { | |||||
| backgroundColor: string; | |||||
| regionColor: string; | |||||
| borderColor: string; | |||||
| hoverColor: string; | |||||
| selectedColor: string; | |||||
| markerFill: string; | |||||
| markerStroke: string; | |||||
| scaleStart: string; | |||||
| scaleEnd: string; | |||||
| textColor: string; | |||||
| } | |||||
| export interface VectorMapOptions { | |||||
| selector: string; | |||||
| map: string; | |||||
| backgroundColor?: string; | |||||
| regionStyle?: { | |||||
| initial?: Record<string, any>; | |||||
| hover?: Record<string, any>; | |||||
| selected?: Record<string, any>; | |||||
| }; | |||||
| markerStyle?: { | |||||
| initial?: Record<string, any>; | |||||
| hover?: Record<string, any>; | |||||
| }; | |||||
| markers?: VectorMapMarker[]; | |||||
| series?: { | |||||
| regions?: Array<{ | |||||
| attribute: string; | |||||
| scale: [string, string]; | |||||
| normalizeFunction?: string; | |||||
| values: Record<string, number>; | |||||
| }>; | |||||
| }; | |||||
| zoomOnScroll?: boolean; | |||||
| zoomButtons?: boolean; | |||||
| onMarkerTooltipShow?: (event: Event, tooltip: any, index: number) => void; | |||||
| onRegionTooltipShow?: (event: Event, tooltip: any, code: string) => void; | |||||
| onLoaded?: (map: any) => void; | |||||
| } | |||||
| export interface VectorMapInstance { | |||||
| destroy(): void; | |||||
| updateSeries(type: string, config: any): void; | |||||
| markers?: VectorMapMarker[]; | |||||
| mapData?: any; | |||||
| series?: any; | |||||
| } | |||||
| declare global { | |||||
| interface HTMLElement { | |||||
| mapInstance?: VectorMapInstance; | |||||
| } | |||||
| } | |||||
| // Enhanced Vector Map implementation | |||||
| export class VectorMapComponent implements ComponentInterface { | |||||
| public name: string = 'VectorMapComponent'; | |||||
| public element: HTMLElement; | |||||
| public options: VectorMapOptions; | |||||
| public isInitialized: boolean = false; | |||||
| private mapInstance: VectorMapInstance | null = null; | |||||
| private container: HTMLElement | null = null; | |||||
| private resizeObserver: ResizeObserver | null = null; | |||||
| private themeChangeHandler: (() => void) | null = null; | |||||
| private resizeHandler: (() => void) | null = null; | |||||
| private themeManager: typeof ThemeManager; | |||||
| constructor(element: HTMLElement, options: Partial<VectorMapOptions> = {}) { | |||||
| this.element = element; | |||||
| this.options = { | |||||
| selector: '#vmap', | |||||
| map: 'world', | |||||
| backgroundColor: 'transparent', | |||||
| zoomOnScroll: false, | |||||
| zoomButtons: false, | |||||
| markers: [ | |||||
| { | |||||
| name: 'INDIA : 350', | |||||
| coords: [21.00, 78.00], | |||||
| }, | |||||
| { | |||||
| name: 'Australia : 250', | |||||
| coords: [-33.00, 151.00], | |||||
| }, | |||||
| { | |||||
| name: 'USA : 250', | |||||
| coords: [36.77, -119.41], | |||||
| }, | |||||
| { | |||||
| name: 'UK : 250', | |||||
| coords: [55.37, -3.41], | |||||
| }, | |||||
| { | |||||
| name: 'UAE : 250', | |||||
| coords: [25.20, 55.27], | |||||
| }, | |||||
| ], | |||||
| ...options, | |||||
| }; | |||||
| this.themeManager = ThemeManager; | |||||
| this.init(); | |||||
| } | |||||
| public init(): void { | |||||
| this.setupContainer(); | |||||
| this.setupEventHandlers(); | |||||
| this.createMap(); | |||||
| this.isInitialized = true; | |||||
| } | |||||
| public destroy(): void { | |||||
| this.cleanup(); | |||||
| this.isInitialized = false; | |||||
| } | |||||
| private setupContainer(): void { | |||||
| // Remove existing map | |||||
| const existingMap = document.getElementById('vmap'); | |||||
| if (existingMap) { | |||||
| existingMap.remove(); | |||||
| } | |||||
| // Create new map container | |||||
| this.container = document.createElement('div'); | |||||
| this.container.id = 'vmap'; | |||||
| this.container.style.height = '490px'; | |||||
| this.container.style.position = 'relative'; | |||||
| this.container.style.overflow = 'hidden'; | |||||
| this.container.style.borderRadius = '8px'; | |||||
| this.container.style.border = '1px solid var(--c-border, #d3d9e3)'; | |||||
| this.container.style.backgroundColor = 'var(--c-bkg-card, #f9fafb)'; | |||||
| this.element.appendChild(this.container); | |||||
| } | |||||
| private setupEventHandlers(): void { | |||||
| // Theme change handler | |||||
| this.themeChangeHandler = debounce(this.updateMapTheme.bind(this), 150); | |||||
| window.addEventListener('adminator:themeChanged', this.themeChangeHandler); | |||||
| // Resize handler | |||||
| this.resizeHandler = debounce(this.handleResize.bind(this), 300); | |||||
| window.addEventListener('resize', this.resizeHandler); | |||||
| // Setup ResizeObserver if available | |||||
| if ('ResizeObserver' in window) { | |||||
| this.resizeObserver = new ResizeObserver( | |||||
| debounce(() => { | |||||
| if (this.mapInstance) { | |||||
| this.handleResize(); | |||||
| } | |||||
| }, 300) | |||||
| ); | |||||
| this.resizeObserver.observe(this.element); | |||||
| } | |||||
| } | |||||
| private createMap(): void { | |||||
| if (!this.container) return; | |||||
| // Destroy existing map instance | |||||
| this.destroyMapInstance(); | |||||
| const colors = this.getThemeColors(); | |||||
| const mapConfig = this.buildMapConfig(colors); | |||||
| try { | |||||
| this.mapInstance = jsVectorMap(mapConfig); | |||||
| this.element.mapInstance = this.mapInstance; | |||||
| } catch (error) { | |||||
| console.error('VectorMap: Failed to initialize map', error); | |||||
| this.showFallbackContent(colors); | |||||
| } | |||||
| } | |||||
| private getThemeColors(): VectorMapColors { | |||||
| const isDark = this.themeManager.current() === 'dark'; | |||||
| return { | |||||
| backgroundColor: isDark ? '#313644' : '#f9fafb', | |||||
| regionColor: isDark ? '#565a5c' : '#e6eaf0', | |||||
| borderColor: isDark ? '#72777a' : '#d3d9e3', | |||||
| hoverColor: isDark ? '#7774e7' : '#0f9aee', | |||||
| selectedColor: isDark ? '#37c936' : '#7774e7', | |||||
| markerFill: isDark ? '#0f9aee' : '#7774e7', | |||||
| markerStroke: isDark ? '#37c936' : '#0f9aee', | |||||
| scaleStart: isDark ? '#b9c2d0' : '#e6eaf0', | |||||
| scaleEnd: isDark ? '#0f9aee' : '#007bff', | |||||
| textColor: isDark ? '#99abb4' : '#72777a', | |||||
| }; | |||||
| } | |||||
| private buildMapConfig(colors: VectorMapColors): VectorMapOptions { | |||||
| return { | |||||
| selector: '#vmap', | |||||
| map: 'world', | |||||
| backgroundColor: this.options.backgroundColor || 'transparent', | |||||
| // Region styling | |||||
| regionStyle: { | |||||
| initial: { | |||||
| fill: colors.regionColor, | |||||
| stroke: colors.borderColor, | |||||
| 'stroke-width': 1, | |||||
| 'stroke-opacity': 0.4, | |||||
| }, | |||||
| hover: { | |||||
| fill: colors.hoverColor, | |||||
| cursor: 'pointer', | |||||
| }, | |||||
| selected: { | |||||
| fill: colors.selectedColor, | |||||
| }, | |||||
| ...this.options.regionStyle, | |||||
| }, | |||||
| // Marker styling | |||||
| markerStyle: { | |||||
| initial: { | |||||
| r: 7, | |||||
| fill: colors.markerFill, | |||||
| stroke: colors.markerStroke, | |||||
| 'stroke-width': 2, | |||||
| 'stroke-opacity': 0.4, | |||||
| }, | |||||
| hover: { | |||||
| r: 10, | |||||
| fill: colors.hoverColor, | |||||
| 'stroke-opacity': 0.8, | |||||
| cursor: 'pointer', | |||||
| }, | |||||
| ...this.options.markerStyle, | |||||
| }, | |||||
| // Markers data | |||||
| markers: this.options.markers || [], | |||||
| // Series configuration | |||||
| series: this.options.series, | |||||
| // Interaction options | |||||
| zoomOnScroll: this.options.zoomOnScroll || false, | |||||
| zoomButtons: this.options.zoomButtons || false, | |||||
| // Event handlers | |||||
| onMarkerTooltipShow: this.handleMarkerTooltip.bind(this), | |||||
| onRegionTooltipShow: this.handleRegionTooltip.bind(this), | |||||
| onLoaded: this.handleMapLoaded.bind(this), | |||||
| }; | |||||
| } | |||||
| private handleMarkerTooltip(event: Event, tooltip: any, index: number): void { | |||||
| try { | |||||
| const marker = this.mapInstance?.markers?.[index]; | |||||
| const markerName = marker?.name || `Marker ${index + 1}`; | |||||
| tooltip.text(markerName); | |||||
| } catch (error) { | |||||
| console.warn('VectorMap: Error in marker tooltip', error); | |||||
| } | |||||
| // Call custom handler if provided | |||||
| if (this.options.onMarkerTooltipShow) { | |||||
| this.options.onMarkerTooltipShow(event, tooltip, index); | |||||
| } | |||||
| } | |||||
| private handleRegionTooltip(event: Event, tooltip: any, code: string): void { | |||||
| try { | |||||
| const mapData = this.mapInstance?.mapData; | |||||
| const regionName = mapData?.paths?.[code]?.name || code; | |||||
| const series = this.mapInstance?.series?.regions?.[0]; | |||||
| const value = series?.values?.[code]; | |||||
| const text = value ? `${regionName}: ${value}` : regionName; | |||||
| tooltip.text(text); | |||||
| } catch (error) { | |||||
| console.warn('VectorMap: Error in region tooltip', error); | |||||
| tooltip.text(code); | |||||
| } | |||||
| // Call custom handler if provided | |||||
| if (this.options.onRegionTooltipShow) { | |||||
| this.options.onRegionTooltipShow(event, tooltip, code); | |||||
| } | |||||
| } | |||||
| private handleMapLoaded(map: any): void { | |||||
| console.log('VectorMap: Map loaded successfully'); | |||||
| // Call custom handler if provided | |||||
| if (this.options.onLoaded) { | |||||
| this.options.onLoaded(map); | |||||
| } | |||||
| } | |||||
| private showFallbackContent(colors: VectorMapColors): void { | |||||
| if (!this.container) return; | |||||
| this.container.innerHTML = ` | |||||
| <div style=" | |||||
| display: flex; | |||||
| align-items: center; | |||||
| justify-content: center; | |||||
| height: 100%; | |||||
| background: ${colors.backgroundColor}; | |||||
| border: 1px solid ${colors.borderColor}; | |||||
| border-radius: 8px; | |||||
| color: ${colors.textColor}; | |||||
| font-size: 14px; | |||||
| font-family: system-ui, -apple-system, sans-serif; | |||||
| "> | |||||
| <div style="text-align: center; padding: 20px;"> | |||||
| <div style="font-size: 32px; margin-bottom: 12px;">🗺️</div> | |||||
| <div style="font-size: 16px; font-weight: 500; margin-bottom: 8px;">World Map</div> | |||||
| <div style="font-size: 12px; opacity: 0.7;">Interactive map will load here</div> | |||||
| </div> | |||||
| </div> | |||||
| `; | |||||
| } | |||||
| private updateMapTheme(): void { | |||||
| if (!this.mapInstance || !this.container) { | |||||
| this.createMap(); | |||||
| return; | |||||
| } | |||||
| const colors = this.getThemeColors(); | |||||
| try { | |||||
| // Update container background | |||||
| this.container.style.backgroundColor = colors.backgroundColor; | |||||
| this.container.style.borderColor = colors.borderColor; | |||||
| // Update series if available | |||||
| if (this.mapInstance.series?.regions?.[0]) { | |||||
| this.mapInstance.updateSeries('regions', { | |||||
| attribute: 'fill', | |||||
| scale: [colors.scaleStart, colors.scaleEnd], | |||||
| values: this.mapInstance.series.regions[0].values || {}, | |||||
| }); | |||||
| } | |||||
| } catch (error) { | |||||
| console.warn('VectorMap: Theme update failed, reinitializing', error); | |||||
| this.createMap(); | |||||
| } | |||||
| } | |||||
| private handleResize(): void { | |||||
| if (this.mapInstance && this.container) { | |||||
| // Force a re-render by recreating the map | |||||
| this.createMap(); | |||||
| } | |||||
| } | |||||
| private destroyMapInstance(): void { | |||||
| if (this.mapInstance) { | |||||
| try { | |||||
| this.mapInstance.destroy(); | |||||
| } catch (error) { | |||||
| console.warn('VectorMap: Error destroying map instance', error); | |||||
| } | |||||
| this.mapInstance = null; | |||||
| } | |||||
| } | |||||
| private cleanup(): void { | |||||
| this.destroyMapInstance(); | |||||
| // Remove event listeners | |||||
| if (this.themeChangeHandler) { | |||||
| window.removeEventListener('adminator:themeChanged', this.themeChangeHandler); | |||||
| this.themeChangeHandler = null; | |||||
| } | |||||
| if (this.resizeHandler) { | |||||
| window.removeEventListener('resize', this.resizeHandler); | |||||
| this.resizeHandler = null; | |||||
| } | |||||
| // Disconnect ResizeObserver | |||||
| if (this.resizeObserver) { | |||||
| this.resizeObserver.disconnect(); | |||||
| this.resizeObserver = null; | |||||
| } | |||||
| // Clear container | |||||
| if (this.container && this.container.parentNode) { | |||||
| this.container.parentNode.removeChild(this.container); | |||||
| this.container = null; | |||||
| } | |||||
| } | |||||
| // Public API methods | |||||
| public updateMarkers(markers: VectorMapMarker[]): void { | |||||
| this.options.markers = markers; | |||||
| this.createMap(); | |||||
| } | |||||
| public updateSeries(type: string, config: any): void { | |||||
| if (this.mapInstance) { | |||||
| try { | |||||
| this.mapInstance.updateSeries(type, config); | |||||
| } catch (error) { | |||||
| console.warn('VectorMap: Error updating series', error); | |||||
| } | |||||
| } | |||||
| } | |||||
| public getMapInstance(): VectorMapInstance | null { | |||||
| return this.mapInstance; | |||||
| } | |||||
| public refresh(): void { | |||||
| this.createMap(); | |||||
| } | |||||
| public updateOptions(newOptions: Partial<VectorMapOptions>): void { | |||||
| this.options = { ...this.options, ...newOptions }; | |||||
| this.createMap(); | |||||
| } | |||||
| } | |||||
| // Vector Map Manager | |||||
| export class VectorMapManager { | |||||
| private instances: Map<string, VectorMapComponent> = new Map(); | |||||
| public initialize(selector: string = '#world-map-marker', options: Partial<VectorMapOptions> = {}): VectorMapComponent | null { | |||||
| const element = document.querySelector<HTMLElement>(selector); | |||||
| if (!element) { | |||||
| // Silently return null if element doesn't exist (normal for pages without maps) | |||||
| return null; | |||||
| } | |||||
| // Clean up existing instance | |||||
| const existingInstance = this.instances.get(selector); | |||||
| if (existingInstance) { | |||||
| existingInstance.destroy(); | |||||
| } | |||||
| // Create new instance | |||||
| const vectorMap = new VectorMapComponent(element, options); | |||||
| this.instances.set(selector, vectorMap); | |||||
| return vectorMap; | |||||
| } | |||||
| public getInstance(selector: string): VectorMapComponent | undefined { | |||||
| return this.instances.get(selector); | |||||
| } | |||||
| public destroyInstance(selector: string): void { | |||||
| const instance = this.instances.get(selector); | |||||
| if (instance) { | |||||
| instance.destroy(); | |||||
| this.instances.delete(selector); | |||||
| } | |||||
| } | |||||
| public destroyAll(): void { | |||||
| this.instances.forEach((instance) => { | |||||
| instance.destroy(); | |||||
| }); | |||||
| this.instances.clear(); | |||||
| } | |||||
| } | |||||
| // Create singleton manager | |||||
| const vectorMapManager = new VectorMapManager(); | |||||
| // Main initialization function | |||||
| const vectorMapInit = (): void => { | |||||
| // Only initialize if the map container exists | |||||
| if (document.querySelector('#world-map-marker')) { | |||||
| vectorMapManager.initialize('#world-map-marker', { | |||||
| markers: [ | |||||
| { | |||||
| name: 'INDIA : 350', | |||||
| coords: [21.00, 78.00], | |||||
| }, | |||||
| { | |||||
| name: 'Australia : 250', | |||||
| coords: [-33.00, 151.00], | |||||
| }, | |||||
| { | |||||
| name: 'USA : 250', | |||||
| coords: [36.77, -119.41], | |||||
| }, | |||||
| { | |||||
| name: 'UK : 250', | |||||
| coords: [55.37, -3.41], | |||||
| }, | |||||
| { | |||||
| name: 'UAE : 250', | |||||
| coords: [25.20, 55.27], | |||||
| }, | |||||
| ], | |||||
| }); | |||||
| } | |||||
| }; | |||||
| // Initialize map | |||||
| if (document.readyState === 'loading') { | |||||
| document.addEventListener('DOMContentLoaded', vectorMapInit); | |||||
| } else { | |||||
| vectorMapInit(); | |||||
| } | |||||
| // Cleanup on page unload | |||||
| window.addEventListener('beforeunload', () => { | |||||
| vectorMapManager.destroyAll(); | |||||
| }); | |||||
| // Export default for compatibility | |||||
| export default { | |||||
| init: vectorMapInit, | |||||
| manager: vectorMapManager, | |||||
| VectorMapComponent, | |||||
| VectorMapManager, | |||||
| }; | |||||
| @ -1,96 +0,0 @@ | |||||
| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="utf-8"> | |||||
| <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> | |||||
| <title>Test</title> | |||||
| <style> | |||||
| #loader { | |||||
| transition: all 0.3s ease-in-out; | |||||
| opacity: 1; | |||||
| visibility: visible; | |||||
| position: fixed; | |||||
| height: 100vh; | |||||
| width: 100%; | |||||
| background: #fff; | |||||
| z-index: 90000; | |||||
| } | |||||
| #loader.fadeOut { | |||||
| opacity: 0; | |||||
| visibility: hidden; | |||||
| } | |||||
| .spinner { | |||||
| width: 40px; | |||||
| height: 40px; | |||||
| position: absolute; | |||||
| top: calc(50% - 20px); | |||||
| left: calc(50% - 20px); | |||||
| background-color: #333; | |||||
| border-radius: 100%; | |||||
| -webkit-animation: sk-scaleout 1.0s infinite ease-in-out; | |||||
| animation: sk-scaleout 1.0s infinite ease-in-out; | |||||
| } | |||||
| @-webkit-keyframes sk-scaleout { | |||||
| 0% { -webkit-transform: scale(0) } | |||||
| 100% { | |||||
| -webkit-transform: scale(1.0); | |||||
| opacity: 0; | |||||
| } | |||||
| } | |||||
| @keyframes sk-scaleout { | |||||
| 0% { | |||||
| -webkit-transform: scale(0); | |||||
| transform: scale(0); | |||||
| } 100% { | |||||
| -webkit-transform: scale(1.0); | |||||
| transform: scale(1.0); | |||||
| opacity: 0; | |||||
| } | |||||
| } | |||||
| </style> | |||||
| </head> | |||||
| <body class="app"> | |||||
| <div id='loader'> | |||||
| <div class="spinner"></div> | |||||
| </div> | |||||
| <script> | |||||
| window.addEventListener('load', function load() { | |||||
| const loader = document.getElementById('loader'); | |||||
| setTimeout(function() { | |||||
| loader.classList.add('fadeOut'); | |||||
| }, 300); | |||||
| }); | |||||
| </script> | |||||
| <div class="page-container"> | |||||
| <!-- <div class="header navbar"> | |||||
| <div class="header-container"> | |||||
| </div> | |||||
| </div> --> | |||||
| <main class='main-content bgc-grey-100'> | |||||
| <div id='mainContent'> | |||||
| <div class="full-container"> | |||||
| <button type="button" class="btn btn-secondary" data-bs-toggle="tooltip" data-bs-placement="top" title="Tooltip on top"> | |||||
| Tooltip on top | |||||
| </button> | |||||
| <button type="button" class="btn btn-secondary" data-bs-toggle="tooltip" data-bs-placement="top" title="Tooltip on top"> | |||||
| Tooltip on top | |||||
| </button> | |||||
| </div> | |||||
| </div> | |||||
| </main> | |||||
| <footer class="bdT ta-c p-30 lh-0 fsz-sm c-grey-600"> | |||||
| <span>Copyright © 2025 Designed by <a href="https://colorlib.com" target="_blank" rel="nofollow noopener noreferrer" title="Colorlib">Colorlib</a>. All rights reserved.</span> | |||||
| </footer> | |||||
| </div> | |||||
| </body> | |||||
| </html> | |||||
| @ -1,236 +0,0 @@ | |||||
| /** | |||||
| * Core type definitions for Adminator Dashboard | |||||
| */ | |||||
| // Theme types | |||||
| export type Theme = 'light' | 'dark' | 'auto'; | |||||
| export interface ThemeConfig { | |||||
| theme: Theme; | |||||
| autoDetect: boolean; | |||||
| persistChoice: boolean; | |||||
| } | |||||
| // Component types | |||||
| export interface ComponentOptions { | |||||
| [key: string]: any; | |||||
| } | |||||
| export interface ComponentInterface { | |||||
| name: string; | |||||
| element: HTMLElement; | |||||
| options: ComponentOptions; | |||||
| isInitialized: boolean; | |||||
| init(): void; | |||||
| destroy(): void; | |||||
| } | |||||
| // Sidebar types | |||||
| export interface SidebarOptions { | |||||
| breakpoint?: number; | |||||
| collapsible?: boolean; | |||||
| autoHide?: boolean; | |||||
| animation?: boolean; | |||||
| animationDuration?: number; | |||||
| } | |||||
| export interface SidebarState { | |||||
| isCollapsed: boolean; | |||||
| isMobile: boolean; | |||||
| activeMenu: string | null; | |||||
| } | |||||
| // Chart types | |||||
| export type ChartType = 'line' | 'bar' | 'doughnut' | 'pie' | 'radar' | 'scatter' | 'bubble' | 'polarArea'; | |||||
| export interface ChartDataset { | |||||
| label?: string; | |||||
| data: number[]; | |||||
| backgroundColor?: string | string[]; | |||||
| borderColor?: string | string[]; | |||||
| borderWidth?: number; | |||||
| fill?: boolean; | |||||
| } | |||||
| export interface ChartData { | |||||
| labels: string[]; | |||||
| datasets: ChartDataset[]; | |||||
| } | |||||
| export interface ChartOptions { | |||||
| type: ChartType; | |||||
| data: ChartData; | |||||
| responsive?: boolean; | |||||
| maintainAspectRatio?: boolean; | |||||
| plugins?: any; | |||||
| scales?: any; | |||||
| } | |||||
| // DataTable types | |||||
| export interface DataTableColumn { | |||||
| key: string; | |||||
| title: string; | |||||
| sortable?: boolean; | |||||
| searchable?: boolean; | |||||
| render?: (value: any, row: any) => string; | |||||
| } | |||||
| export interface DataTableOptions { | |||||
| columns: DataTableColumn[]; | |||||
| data: any[]; | |||||
| pageSize?: number; | |||||
| sortable?: boolean; | |||||
| searchable?: boolean; | |||||
| pagination?: boolean; | |||||
| } | |||||
| export interface DataTableState { | |||||
| currentPage: number; | |||||
| pageSize: number; | |||||
| totalRows: number; | |||||
| sortColumn: string | null; | |||||
| sortDirection: 'asc' | 'desc'; | |||||
| searchQuery: string; | |||||
| filteredData: any[]; | |||||
| } | |||||
| // Date utilities types | |||||
| export interface DateRange { | |||||
| start: Date; | |||||
| end: Date; | |||||
| } | |||||
| export interface DateFormatOptions { | |||||
| locale?: string; | |||||
| format?: string; | |||||
| timeZone?: string; | |||||
| } | |||||
| // DOM utilities types | |||||
| export type DOMEventHandler = (event: Event) => void; | |||||
| export interface DOMUtilities { | |||||
| select: (selector: string, context?: Element | Document) => HTMLElement | null; | |||||
| selectAll: (selector: string, context?: Element | Document) => HTMLElement[]; | |||||
| on: (element: Element | Window | Document, event: string, handler: DOMEventHandler) => void; | |||||
| off: (element: Element | Window | Document, event: string, handler: DOMEventHandler) => void; | |||||
| addClass: (element: Element, className: string) => void; | |||||
| removeClass: (element: Element, className: string) => void; | |||||
| toggleClass: (element: Element, className: string) => void; | |||||
| hasClass: (element: Element, className: string) => boolean; | |||||
| attr: (element: Element, attribute: string, value?: string) => string | void; | |||||
| data: (element: Element, key: string, value?: any) => any; | |||||
| ready: (callback: () => void) => void; | |||||
| exists: (selector: string, context?: Element | Document) => boolean; | |||||
| } | |||||
| // Application state types | |||||
| export interface ApplicationState { | |||||
| theme: Theme; | |||||
| sidebar: SidebarState; | |||||
| components: Map<string, ComponentInterface>; | |||||
| isInitialized: boolean; | |||||
| } | |||||
| export interface ApplicationConfig { | |||||
| theme: ThemeConfig; | |||||
| sidebar: SidebarOptions; | |||||
| enableAnalytics?: boolean; | |||||
| debugMode?: boolean; | |||||
| } | |||||
| // Event types | |||||
| export interface CustomEventDetail { | |||||
| [key: string]: any; | |||||
| } | |||||
| export interface ThemeChangeEvent extends CustomEvent { | |||||
| detail: { | |||||
| theme: Theme; | |||||
| previousTheme: Theme; | |||||
| }; | |||||
| } | |||||
| export interface ComponentEvent extends CustomEvent { | |||||
| detail: { | |||||
| component: string; | |||||
| action: 'init' | 'destroy' | 'update'; | |||||
| data?: any; | |||||
| }; | |||||
| } | |||||
| // Utility types | |||||
| export type DeepPartial<T> = { | |||||
| [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]; | |||||
| }; | |||||
| export type RequiredKeys<T, K extends keyof T> = T & Required<Pick<T, K>>; | |||||
| export type OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>; | |||||
| // Color types | |||||
| export interface ColorPalette { | |||||
| primary: string; | |||||
| secondary: string; | |||||
| success: string; | |||||
| danger: string; | |||||
| warning: string; | |||||
| info: string; | |||||
| light: string; | |||||
| dark: string; | |||||
| } | |||||
| export interface ThemeColors { | |||||
| light: ColorPalette; | |||||
| dark: ColorPalette; | |||||
| } | |||||
| // Animation types | |||||
| export type AnimationEasing = 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'linear'; | |||||
| export interface AnimationOptions { | |||||
| duration?: number; | |||||
| easing?: AnimationEasing; | |||||
| delay?: number; | |||||
| fillMode?: 'none' | 'forwards' | 'backwards' | 'both'; | |||||
| } | |||||
| // Layout types | |||||
| export interface LayoutBreakpoints { | |||||
| xs: number; | |||||
| sm: number; | |||||
| md: number; | |||||
| lg: number; | |||||
| xl: number; | |||||
| xxl: number; | |||||
| } | |||||
| export interface ResponsiveConfig { | |||||
| breakpoints: LayoutBreakpoints; | |||||
| mobileFirst: boolean; | |||||
| } | |||||
| // Error types | |||||
| export class AdminatorError extends Error { | |||||
| constructor( | |||||
| message: string, | |||||
| public component?: string, | |||||
| public code?: string | |||||
| ) { | |||||
| super(message); | |||||
| this.name = 'AdminatorError'; | |||||
| } | |||||
| } | |||||
| // Plugin types | |||||
| export interface PluginInterface { | |||||
| name: string; | |||||
| version: string; | |||||
| dependencies?: string[]; | |||||
| init(app: any): void; | |||||
| destroy(): void; | |||||
| } | |||||
| export interface PluginRegistry { | |||||
| [key: string]: PluginInterface; | |||||
| } | |||||
| @ -1,51 +0,0 @@ | |||||
| { | |||||
| "compilerOptions": { | |||||
| "target": "ES2020", | |||||
| "lib": ["ES2020", "DOM", "DOM.Iterable"], | |||||
| "module": "ESNext", | |||||
| "moduleResolution": "node", | |||||
| "allowJs": true, | |||||
| "checkJs": false, | |||||
| "outDir": "./dist", | |||||
| "strict": true, | |||||
| "esModuleInterop": true, | |||||
| "skipLibCheck": true, | |||||
| "forceConsistentCasingInFileNames": true, | |||||
| "resolveJsonModule": true, | |||||
| "isolatedModules": true, | |||||
| "noEmit": true, | |||||
| "declaration": true, | |||||
| "declarationMap": true, | |||||
| "sourceMap": true, | |||||
| "removeComments": false, | |||||
| "importHelpers": true, | |||||
| "experimentalDecorators": true, | |||||
| "emitDecoratorMetadata": true, | |||||
| "allowSyntheticDefaultImports": true, | |||||
| "noImplicitAny": true, | |||||
| "noImplicitReturns": true, | |||||
| "noImplicitThis": true, | |||||
| "noUnusedLocals": true, | |||||
| "noUnusedParameters": true, | |||||
| "exactOptionalPropertyTypes": true, | |||||
| "noImplicitOverride": true, | |||||
| "noPropertyAccessFromIndexSignature": false, | |||||
| "noUncheckedIndexedAccess": false, | |||||
| "baseUrl": "./src", | |||||
| "paths": { | |||||
| "@/*": ["*"], | |||||
| "@/components/*": ["assets/scripts/components/*"], | |||||
| "@/utils/*": ["assets/scripts/utils/*"], | |||||
| "@/constants/*": ["assets/scripts/constants/*"] | |||||
| } | |||||
| }, | |||||
| "include": [ | |||||
| "src/**/*" | |||||
| ], | |||||
| "exclude": [ | |||||
| "node_modules", | |||||
| "dist", | |||||
| "**/*.spec.ts", | |||||
| "**/*.test.ts" | |||||
| ] | |||||
| } | |||||
| @ -1,10 +1,8 @@ | |||||
| module.exports = { | module.exports = { | ||||
| test : /\.(png|gif|jpg?g|svg)$/i, | test : /\.(png|gif|jpg?g|svg)$/i, | ||||
| exclude : /(node_modules)/, | exclude : /(node_modules)/, | ||||
| use : [{ | |||||
| loader: 'file-loader', | |||||
| options: { | |||||
| outputPath: 'assets', | |||||
| }, | |||||
| }], | |||||
| type : 'asset/resource', | |||||
| generator: { | |||||
| filename: 'assets/[name][ext]', | |||||
| }, | |||||
| }; | }; | ||||
| @ -1,16 +0,0 @@ | |||||
| module.exports = { | |||||
| test: /\.tsx?$/, | |||||
| exclude: /(node_modules|build|dist\/)/, | |||||
| use: [ | |||||
| { | |||||
| loader: 'babel-loader', | |||||
| }, | |||||
| { | |||||
| loader: 'ts-loader', | |||||
| options: { | |||||
| transpileOnly: true, | |||||
| experimentalWatchApi: true, | |||||
| }, | |||||
| }, | |||||
| ], | |||||
| }; | |||||