Browse Source

Add TypeScript migration and jQuery-free implementation files

This commit includes all the core files from the jQuery-free migration:

### TypeScript Files Added:
- tsconfig.json - TypeScript configuration
- src/types/index.ts - Comprehensive type definitions
- src/assets/scripts/app.ts - Main application TypeScript version
- src/assets/scripts/components/Chart.ts - Chart component TypeScript
- src/assets/scripts/components/Sidebar.ts - Sidebar component TypeScript
- src/assets/scripts/datatable/index.ts - DataTable TypeScript implementation
- src/assets/scripts/datepicker/index.ts - DatePicker TypeScript implementation
- src/assets/scripts/ui/index.ts - UI components TypeScript
- src/assets/scripts/utils/date.ts - Date utilities TypeScript
- src/assets/scripts/utils/dom.ts - DOM utilities TypeScript
- src/assets/scripts/utils/theme.ts - Theme utilities TypeScript
- src/assets/scripts/vectorMaps/index.ts - Vector maps TypeScript
- webpack/rules/ts.js - TypeScript webpack rules

### Updated JavaScript Files:
- src/assets/scripts/app.js - Updated main application
- src/assets/scripts/datatable/index.js - Updated DataTable implementation
- src/assets/scripts/datepicker/index.js - Updated DatePicker implementation
- src/assets/scripts/ui/index.js - Updated UI components
- src/assets/scripts/utils/theme.js - Updated theme utilities

### Configuration Files:
- .npmignore - NPM package ignore rules
- package-lock.json - Updated dependencies lock file
- webpack/config.js - Updated webpack configuration
- webpack/rules/index.js - Updated webpack rules
- webpack/rules/js.js - Updated JavaScript rules

These files complete the jQuery-free migration with modern TypeScript
implementations and maintain full backward compatibility.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
pull/317/head
Aigars Silkalns 5 months ago
parent
commit
f20baf588e
23 changed files with 7171 additions and 38 deletions
  1. +44
    -0
      .npmignore
  2. +413
    -7
      package-lock.json
  3. +8
    -7
      src/assets/scripts/app.js
  4. +757
    -0
      src/assets/scripts/app.ts
  5. +1350
    -0
      src/assets/scripts/components/Chart.ts
  6. +388
    -0
      src/assets/scripts/components/Sidebar.ts
  7. +1
    -1
      src/assets/scripts/datatable/index.js
  8. +707
    -0
      src/assets/scripts/datatable/index.ts
  9. +3
    -4
      src/assets/scripts/datepicker/index.js
  10. +699
    -0
      src/assets/scripts/datepicker/index.ts
  11. +17
    -17
      src/assets/scripts/ui/index.js
  12. +740
    -0
      src/assets/scripts/ui/index.ts
  13. +363
    -0
      src/assets/scripts/utils/date.ts
  14. +513
    -0
      src/assets/scripts/utils/dom.ts
  15. +1
    -0
      src/assets/scripts/utils/theme.js
  16. +313
    -0
      src/assets/scripts/utils/theme.ts
  17. +542
    -0
      src/assets/scripts/vectorMaps/index.ts
  18. +236
    -0
      src/types/index.ts
  19. +51
    -0
      tsconfig.json
  20. +7
    -1
      webpack/config.js
  21. +1
    -0
      webpack/rules/index.js
  22. +1
    -1
      webpack/rules/js.js
  23. +16
    -0
      webpack/rules/ts.js

+ 44
- 0
.npmignore View File

@ -0,0 +1,44 @@
# Development files
node_modules/
.git/
.gitignore
.eslintrc.js
.stylelintrc.json
# Build artifacts that aren't needed
webpack.config.js
babel.config.js
# Documentation that's not essential for npm users
.github/
docs/
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# OS files
.DS_Store
Thumbs.db
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
# Temporary folders
tmp/
temp/

+ 413
- 7
package-lock.json View File

@ -1,12 +1,13 @@
{
"name": "adminator",
"version": "2.6.1",
"name": "adminator-admin-dashboard",
"version": "2.7.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "adminator",
"version": "2.6.1",
"name": "adminator-admin-dashboard",
"version": "2.7.0",
"license": "MIT",
"dependencies": {
"@fullcalendar/core": "^6.1.17",
"@fullcalendar/daygrid": "^6.1.17",
@ -33,6 +34,11 @@
"@babel/preset-env": "^7.27.2",
"@babel/runtime": "^7.27.6",
"@eslint/js": "^9.29.0",
"@types/lodash": "^4.17.20",
"@types/masonry-layout": "^4.2.8",
"@types/node": "^24.0.12",
"@typescript-eslint/eslint-plugin": "^8.36.0",
"@typescript-eslint/parser": "^8.36.0",
"babel-loader": "^10.0.0",
"case-sensitive-paths-webpack-plugin": "^2.4.0",
"copy-webpack-plugin": "^13.0.0",
@ -53,10 +59,15 @@
"style-loader": "^4.0.0",
"stylelint": "^16.21.0",
"stylelint-config-standard": "^38.0.0",
"ts-loader": "^9.5.2",
"typescript": "^5.8.3",
"webpack": "^5.99.9",
"webpack-cli": "^6.0.1",
"webpack-dashboard": "^3.3.8",
"webpack-dev-server": "^5.2.2"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@ampproject/remapping": {
@ -4293,12 +4304,39 @@
"@types/istanbul-lib-report": "*"
}
},
"node_modules/@types/jquery": {
"version": "3.5.32",
"resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.32.tgz",
"integrity": "sha512-b9Xbf4CkMqS02YH8zACqN1xzdxc3cO735Qe5AbSUFmyOiaWAbcpqh9Wna+Uk0vgACvoQHpWDg2rGdHkYPLmCiQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/sizzle": "*"
}
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/masonry-layout": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/@types/masonry-layout/-/masonry-layout-4.2.8.tgz",
"integrity": "sha512-Et2to22C31FG1UFaHRBL6BznMOhrur3Ckr9gvR7fRVmPgxqiwCEKZtV8GpFscHyNAKhZ0QlkwXJRPnJvxZUKQw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/jquery": "*"
}
},
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@ -4307,9 +4345,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.0.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.1.tgz",
"integrity": "sha512-MX4Zioh39chHlDJbKmEgydJDS3tspMP/lnQC67G3SWsTnb9NeYVWOjkxpOSy4oMfPs4StcWHwBrvUb4ybfnuaw==",
"version": "24.0.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.12.tgz",
"integrity": "sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g==",
"license": "MIT",
"dependencies": {
"undici-types": "~7.8.0"
@ -4379,6 +4417,13 @@
"@types/send": "*"
}
},
"node_modules/@types/sizzle": {
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.9.tgz",
"integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/sockjs": {
"version": "0.3.36",
"resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz",
@ -4416,6 +4461,289 @@
"dev": true,
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz",
"integrity": "sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.36.0",
"@typescript-eslint/type-utils": "8.36.0",
"@typescript-eslint/utils": "8.36.0",
"@typescript-eslint/visitor-keys": "8.36.0",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.36.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.36.0.tgz",
"integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/scope-manager": "8.36.0",
"@typescript-eslint/types": "8.36.0",
"@typescript-eslint/typescript-estree": "8.36.0",
"@typescript-eslint/visitor-keys": "8.36.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz",
"integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.36.0",
"@typescript-eslint/types": "^8.36.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz",
"integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.36.0",
"@typescript-eslint/visitor-keys": "8.36.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz",
"integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.36.0.tgz",
"integrity": "sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "8.36.0",
"@typescript-eslint/utils": "8.36.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz",
"integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz",
"integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.36.0",
"@typescript-eslint/tsconfig-utils": "8.36.0",
"@typescript-eslint/types": "8.36.0",
"@typescript-eslint/visitor-keys": "8.36.0",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.36.0.tgz",
"integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.36.0",
"@typescript-eslint/types": "8.36.0",
"@typescript-eslint/typescript-estree": "8.36.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <5.9.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.36.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz",
"integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.36.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@webassemblyjs/ast": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
@ -7657,6 +7985,13 @@
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
"license": "ISC"
},
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true,
"license": "MIT"
},
"node_modules/handle-thing": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz",
@ -12921,6 +13256,63 @@
"tslib": "2"
}
},
"node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
},
"peerDependencies": {
"typescript": ">=4.8.4"
}
},
"node_modules/ts-loader": {
"version": "9.5.2",
"resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.2.tgz",
"integrity": "sha512-Qo4piXvOTWcMGIgRiuFa6nHNm+54HbYaZCKqc9eeZCLRy3XqafQgwX2F7mofrbJG3g7EEb+lkiR+z2Lic2s3Zw==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "^4.1.0",
"enhanced-resolve": "^5.0.0",
"micromatch": "^4.0.0",
"semver": "^7.3.4",
"source-map": "^0.7.4"
},
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"typescript": "*",
"webpack": "^5.0.0"
}
},
"node_modules/ts-loader/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/ts-loader/node_modules/source-map": {
"version": "0.7.4",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz",
"integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">= 8"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@ -12955,6 +13347,20 @@
"node": ">= 0.6"
}
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/uglify-js": {
"version": "3.19.3",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",


+ 8
- 7
src/assets/scripts/app.js View File

@ -5,11 +5,11 @@
// Note: Bootstrap 5 CSS is still available via SCSS imports
// Bootstrap JS components removed to eliminate jQuery dependency
import DOM from './utils/dom';
import { DOM } from './utils/dom';
import DateUtils from './utils/date';
import Theme from './utils/theme';
import Sidebar from './components/Sidebar';
import ChartComponent from './components/Chart';
import { ThemeManager } from './utils/theme';
import { Sidebar } from './components/Sidebar';
import { ChartComponent } from './components/Chart';
// Import styles
import '../styles/index.scss';
@ -31,6 +31,7 @@ class AdminatorApp {
constructor() {
this.components = new Map();
this.isInitialized = false;
this.themeManager = ThemeManager;
// Initialize when DOM is ready
DOM.ready(() => {
@ -212,7 +213,7 @@ class AdminatorApp {
// Initializing theme system
// Initialize theme system first
Theme.init();
this.themeManager.init();
// Inject theme toggle if missing - with retry mechanism
setTimeout(() => {
@ -249,11 +250,11 @@ class AdminatorApp {
const toggle = DOM.select('#theme-toggle');
if (toggle) {
// Set initial state
const currentTheme = Theme.current();
const currentTheme = this.themeManager.current();
toggle.checked = currentTheme === 'dark';
DOM.on(toggle, 'change', () => {
Theme.apply(toggle.checked ? 'dark' : 'light');
this.themeManager.apply(toggle.checked ? 'dark' : 'light');
});
// Listen for theme changes from other sources


+ 757
- 0
src/assets/scripts/app.ts View File

@ -0,0 +1,757 @@
/**
* 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;

+ 1350
- 0
src/assets/scripts/components/Chart.ts
File diff suppressed because it is too large
View File


+ 388
- 0
src/assets/scripts/components/Sidebar.ts View File

@ -0,0 +1,388 @@
/**
* 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
- 1
src/assets/scripts/datatable/index.js View File

@ -1,4 +1,4 @@
import Theme from '../utils/theme.js';
// DataTable implementation
export default (function () {


+ 707
- 0
src/assets/scripts/datatable/index.ts View File

@ -0,0 +1,707 @@
/**
* 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,
};

+ 3
- 4
src/assets/scripts/datepicker/index.js View File

@ -1,5 +1,4 @@
import DateUtils from '../utils/date.js';
import Theme from '../utils/theme.js';
export default (function () {
@ -163,7 +162,7 @@ export default (function () {
bindEvents() {
// Handle click events
this.element.addEventListener('click', (e) => {
this.element.addEventListener('click', () => {
this.openPicker();
});
@ -180,7 +179,7 @@ export default (function () {
});
// Handle focus events
this.element.addEventListener('focus', (e) => {
this.element.addEventListener('focus', () => {
this.element.classList.add('datepicker-animation');
setTimeout(() => {
this.element.classList.remove('datepicker-animation');
@ -195,7 +194,7 @@ export default (function () {
if (this.element.showPicker && typeof this.element.showPicker === 'function') {
try {
this.element.showPicker();
} catch (e) {
} catch {
// Fallback for browsers that don't support showPicker
}
}


+ 699
- 0
src/assets/scripts/datepicker/index.ts View File

@ -0,0 +1,699 @@
/**
* 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,
};

+ 17
- 17
src/assets/scripts/ui/index.js View File

@ -277,22 +277,22 @@ export default (function () {
const rect = this.element.getBoundingClientRect();
switch (placement) {
case 'top':
this.tooltip.style.left = `${rect.left + (rect.width / 2) - (this.tooltip.offsetWidth / 2)}px`;
this.tooltip.style.top = `${rect.top - this.tooltip.offsetHeight - 5}px`;
break;
case 'bottom':
this.tooltip.style.left = `${rect.left + (rect.width / 2) - (this.tooltip.offsetWidth / 2)}px`;
this.tooltip.style.top = `${rect.bottom + 5}px`;
break;
case 'left':
this.tooltip.style.left = `${rect.left - this.tooltip.offsetWidth - 5}px`;
this.tooltip.style.top = `${rect.top + (rect.height / 2) - (this.tooltip.offsetHeight / 2)}px`;
break;
case 'right':
this.tooltip.style.left = `${rect.right + 5}px`;
this.tooltip.style.top = `${rect.top + (rect.height / 2) - (this.tooltip.offsetHeight / 2)}px`;
break;
case 'top':
this.tooltip.style.left = `${rect.left + (rect.width / 2) - (this.tooltip.offsetWidth / 2)}px`;
this.tooltip.style.top = `${rect.top - this.tooltip.offsetHeight - 5}px`;
break;
case 'bottom':
this.tooltip.style.left = `${rect.left + (rect.width / 2) - (this.tooltip.offsetWidth / 2)}px`;
this.tooltip.style.top = `${rect.bottom + 5}px`;
break;
case 'left':
this.tooltip.style.left = `${rect.left - this.tooltip.offsetWidth - 5}px`;
this.tooltip.style.top = `${rect.top + (rect.height / 2) - (this.tooltip.offsetHeight / 2)}px`;
break;
case 'right':
this.tooltip.style.left = `${rect.right + 5}px`;
this.tooltip.style.top = `${rect.top + (rect.height / 2) - (this.tooltip.offsetHeight / 2)}px`;
break;
}
}
@ -407,6 +407,6 @@ export default (function () {
Dropdown: VanillaDropdown,
Popover: VanillaPopover,
Tooltip: VanillaTooltip,
Accordion: VanillaAccordion
Accordion: VanillaAccordion,
};
}());

+ 740
- 0
src/assets/scripts/ui/index.ts View File

@ -0,0 +1,740 @@
/**
* 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,
};

+ 363
- 0
src/assets/scripts/utils/date.ts View File

@ -0,0 +1,363 @@
/**
* 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;

+ 513
- 0
src/assets/scripts/utils/dom.ts View File

@ -0,0 +1,513 @@
/**
* 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
- 0
src/assets/scripts/utils/theme.js View File

@ -1,3 +1,4 @@
/* global Chart */
const THEME_KEY = 'adminator-theme';
const Theme = {


+ 313
- 0
src/assets/scripts/utils/theme.ts View File

@ -0,0 +1,313 @@
/**
* 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,
};

+ 542
- 0
src/assets/scripts/vectorMaps/index.ts View File

@ -0,0 +1,542 @@
/**
* 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,
};

+ 236
- 0
src/types/index.ts View File

@ -0,0 +1,236 @@
/**
* 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;
}

+ 51
- 0
tsconfig.json View File

@ -0,0 +1,51 @@
{
"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"
]
}

+ 7
- 1
webpack/config.js View File

@ -39,11 +39,17 @@ const
// ---------------
const resolve = {
extensions: ['.webpack-loader.js', '.web-loader.js', '.loader.js', '.js'],
extensions: ['.tsx', '.ts', '.webpack-loader.js', '.web-loader.js', '.loader.js', '.js', '.jsx'],
modules: [
path.join(__dirname, '../node_modules'),
path.join(manifest.paths.src, ''),
],
alias: {
'@': path.join(manifest.paths.src),
'@/components': path.join(manifest.paths.src, 'assets', 'scripts', 'components'),
'@/utils': path.join(manifest.paths.src, 'assets', 'scripts', 'utils'),
'@/constants': path.join(manifest.paths.src, 'assets', 'scripts', 'constants'),
},
};
const optimization = {


+ 1
- 0
webpack/rules/index.js View File

@ -1,5 +1,6 @@
module.exports = [
require('./js'),
require('./ts'),
require('./images'),
require('./css'),
require('./sass'),


+ 1
- 1
webpack/rules/js.js View File

@ -1,5 +1,5 @@
module.exports = {
test : /\.(js)$/,
test : /\.(js|jsx)$/,
exclude : /(node_modules|build|dist\/)/,
use : ['babel-loader'],
};

+ 16
- 0
webpack/rules/ts.js View File

@ -0,0 +1,16 @@
module.exports = {
test: /\.tsx?$/,
exclude: /(node_modules|build|dist\/)/,
use: [
{
loader: 'babel-loader',
},
{
loader: 'ts-loader',
options: {
transpileOnly: true,
experimentalWatchApi: true,
},
},
],
};

Loading…
Cancel
Save