adminlte.js 51 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260
  1. /*!
  2. * AdminLTE v4.0.0-rc6 (https://adminlte.io)
  3. * Copyright 2014-2025 Colorlib <https://colorlib.com>
  4. * Licensed under MIT (https://github.com/ColorlibHQ/AdminLTE/blob/master/LICENSE)
  5. */
  6. (function (global, factory) {
  7. typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
  8. typeof define === 'function' && define.amd ? define(['exports'], factory) :
  9. (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.adminlte = {}));
  10. })(this, (function (exports) { 'use strict';
  11. const domContentLoadedCallbacks = [];
  12. const onDOMContentLoaded = (callback) => {
  13. if (document.readyState === 'loading') {
  14. // add listener on the first call when the document is in loading state
  15. if (!domContentLoadedCallbacks.length) {
  16. document.addEventListener('DOMContentLoaded', () => {
  17. for (const callback of domContentLoadedCallbacks) {
  18. callback();
  19. }
  20. });
  21. }
  22. domContentLoadedCallbacks.push(callback);
  23. }
  24. else {
  25. callback();
  26. }
  27. };
  28. /* SLIDE UP */
  29. const slideUp = (target, duration = 500) => {
  30. target.style.transitionProperty = 'height, margin, padding';
  31. target.style.transitionDuration = `${duration}ms`;
  32. target.style.boxSizing = 'border-box';
  33. target.style.height = `${target.offsetHeight}px`;
  34. target.style.overflow = 'hidden';
  35. globalThis.setTimeout(() => {
  36. target.style.height = '0';
  37. target.style.paddingTop = '0';
  38. target.style.paddingBottom = '0';
  39. target.style.marginTop = '0';
  40. target.style.marginBottom = '0';
  41. }, 1);
  42. globalThis.setTimeout(() => {
  43. target.style.display = 'none';
  44. target.style.removeProperty('height');
  45. target.style.removeProperty('padding-top');
  46. target.style.removeProperty('padding-bottom');
  47. target.style.removeProperty('margin-top');
  48. target.style.removeProperty('margin-bottom');
  49. target.style.removeProperty('overflow');
  50. target.style.removeProperty('transition-duration');
  51. target.style.removeProperty('transition-property');
  52. }, duration);
  53. };
  54. /* SLIDE DOWN */
  55. const slideDown = (target, duration = 500) => {
  56. target.style.removeProperty('display');
  57. let { display } = globalThis.getComputedStyle(target);
  58. if (display === 'none') {
  59. display = 'block';
  60. }
  61. target.style.display = display;
  62. const height = target.offsetHeight;
  63. target.style.overflow = 'hidden';
  64. target.style.height = '0';
  65. target.style.paddingTop = '0';
  66. target.style.paddingBottom = '0';
  67. target.style.marginTop = '0';
  68. target.style.marginBottom = '0';
  69. globalThis.setTimeout(() => {
  70. target.style.boxSizing = 'border-box';
  71. target.style.transitionProperty = 'height, margin, padding';
  72. target.style.transitionDuration = `${duration}ms`;
  73. target.style.height = `${height}px`;
  74. target.style.removeProperty('padding-top');
  75. target.style.removeProperty('padding-bottom');
  76. target.style.removeProperty('margin-top');
  77. target.style.removeProperty('margin-bottom');
  78. }, 1);
  79. globalThis.setTimeout(() => {
  80. target.style.removeProperty('height');
  81. target.style.removeProperty('overflow');
  82. target.style.removeProperty('transition-duration');
  83. target.style.removeProperty('transition-property');
  84. }, duration);
  85. };
  86. /**
  87. * --------------------------------------------
  88. * @file AdminLTE layout.ts
  89. * @description Layout for AdminLTE.
  90. * @license MIT
  91. * --------------------------------------------
  92. */
  93. /**
  94. * ------------------------------------------------------------------------
  95. * Constants
  96. * ------------------------------------------------------------------------
  97. */
  98. const CLASS_NAME_HOLD_TRANSITIONS = 'hold-transition';
  99. const CLASS_NAME_APP_LOADED = 'app-loaded';
  100. /**
  101. * Class Definition
  102. * ====================================================
  103. */
  104. class Layout {
  105. _element;
  106. constructor(element) {
  107. this._element = element;
  108. }
  109. holdTransition() {
  110. let resizeTimer;
  111. window.addEventListener('resize', () => {
  112. document.body.classList.add(CLASS_NAME_HOLD_TRANSITIONS);
  113. clearTimeout(resizeTimer);
  114. resizeTimer = setTimeout(() => {
  115. document.body.classList.remove(CLASS_NAME_HOLD_TRANSITIONS);
  116. }, 400);
  117. });
  118. }
  119. }
  120. onDOMContentLoaded(() => {
  121. const data = new Layout(document.body);
  122. data.holdTransition();
  123. setTimeout(() => {
  124. document.body.classList.add(CLASS_NAME_APP_LOADED);
  125. }, 400);
  126. });
  127. /**
  128. * --------------------------------------------
  129. * @file AdminLTE card-widget.ts
  130. * @description Card widget for AdminLTE.
  131. * @license MIT
  132. * --------------------------------------------
  133. */
  134. /**
  135. * Constants
  136. * ====================================================
  137. */
  138. const DATA_KEY$4 = 'lte.card-widget';
  139. const EVENT_KEY$4 = `.${DATA_KEY$4}`;
  140. const EVENT_COLLAPSED$2 = `collapsed${EVENT_KEY$4}`;
  141. const EVENT_EXPANDED$2 = `expanded${EVENT_KEY$4}`;
  142. const EVENT_REMOVE = `remove${EVENT_KEY$4}`;
  143. const EVENT_MAXIMIZED$1 = `maximized${EVENT_KEY$4}`;
  144. const EVENT_MINIMIZED$1 = `minimized${EVENT_KEY$4}`;
  145. const CLASS_NAME_CARD = 'card';
  146. const CLASS_NAME_COLLAPSED = 'collapsed-card';
  147. const CLASS_NAME_COLLAPSING = 'collapsing-card';
  148. const CLASS_NAME_EXPANDING = 'expanding-card';
  149. const CLASS_NAME_WAS_COLLAPSED = 'was-collapsed';
  150. const CLASS_NAME_MAXIMIZED = 'maximized-card';
  151. const SELECTOR_DATA_REMOVE = '[data-lte-toggle="card-remove"]';
  152. const SELECTOR_DATA_COLLAPSE = '[data-lte-toggle="card-collapse"]';
  153. const SELECTOR_DATA_MAXIMIZE = '[data-lte-toggle="card-maximize"]';
  154. const SELECTOR_CARD = `.${CLASS_NAME_CARD}`;
  155. const SELECTOR_CARD_BODY = '.card-body';
  156. const SELECTOR_CARD_FOOTER = '.card-footer';
  157. const Default$1 = {
  158. animationSpeed: 500,
  159. collapseTrigger: SELECTOR_DATA_COLLAPSE,
  160. removeTrigger: SELECTOR_DATA_REMOVE,
  161. maximizeTrigger: SELECTOR_DATA_MAXIMIZE
  162. };
  163. class CardWidget {
  164. _element;
  165. _parent;
  166. _clone;
  167. _config;
  168. constructor(element, config) {
  169. this._element = element;
  170. this._parent = element.closest(SELECTOR_CARD);
  171. if (element.classList.contains(CLASS_NAME_CARD)) {
  172. this._parent = element;
  173. }
  174. this._config = { ...Default$1, ...config };
  175. }
  176. collapse() {
  177. const event = new Event(EVENT_COLLAPSED$2);
  178. if (this._parent) {
  179. this._parent.classList.add(CLASS_NAME_COLLAPSING);
  180. // Only target direct children to avoid affecting nested cards
  181. const elm = this._parent?.querySelectorAll(`:scope > ${SELECTOR_CARD_BODY}, :scope > ${SELECTOR_CARD_FOOTER}`);
  182. elm.forEach(el => {
  183. if (el instanceof HTMLElement) {
  184. slideUp(el, this._config.animationSpeed);
  185. }
  186. });
  187. setTimeout(() => {
  188. if (this._parent) {
  189. this._parent.classList.add(CLASS_NAME_COLLAPSED);
  190. this._parent.classList.remove(CLASS_NAME_COLLAPSING);
  191. }
  192. }, this._config.animationSpeed);
  193. }
  194. this._element?.dispatchEvent(event);
  195. }
  196. expand() {
  197. const event = new Event(EVENT_EXPANDED$2);
  198. if (this._parent) {
  199. this._parent.classList.add(CLASS_NAME_EXPANDING);
  200. // Only target direct children to avoid affecting nested cards
  201. const elm = this._parent?.querySelectorAll(`:scope > ${SELECTOR_CARD_BODY}, :scope > ${SELECTOR_CARD_FOOTER}`);
  202. elm.forEach(el => {
  203. if (el instanceof HTMLElement) {
  204. slideDown(el, this._config.animationSpeed);
  205. }
  206. });
  207. setTimeout(() => {
  208. if (this._parent) {
  209. this._parent.classList.remove(CLASS_NAME_COLLAPSED, CLASS_NAME_EXPANDING);
  210. }
  211. }, this._config.animationSpeed);
  212. }
  213. this._element?.dispatchEvent(event);
  214. }
  215. remove() {
  216. const event = new Event(EVENT_REMOVE);
  217. if (this._parent) {
  218. slideUp(this._parent, this._config.animationSpeed);
  219. }
  220. this._element?.dispatchEvent(event);
  221. }
  222. toggle() {
  223. if (this._parent?.classList.contains(CLASS_NAME_COLLAPSED)) {
  224. this.expand();
  225. return;
  226. }
  227. this.collapse();
  228. }
  229. maximize() {
  230. const event = new Event(EVENT_MAXIMIZED$1);
  231. if (this._parent) {
  232. this._parent.style.height = `${this._parent.offsetHeight}px`;
  233. this._parent.style.width = `${this._parent.offsetWidth}px`;
  234. this._parent.style.transition = 'all .15s';
  235. setTimeout(() => {
  236. const htmlTag = document.querySelector('html');
  237. if (htmlTag) {
  238. htmlTag.classList.add(CLASS_NAME_MAXIMIZED);
  239. }
  240. if (this._parent) {
  241. this._parent.classList.add(CLASS_NAME_MAXIMIZED);
  242. if (this._parent.classList.contains(CLASS_NAME_COLLAPSED)) {
  243. this._parent.classList.add(CLASS_NAME_WAS_COLLAPSED);
  244. }
  245. }
  246. }, 150);
  247. }
  248. this._element?.dispatchEvent(event);
  249. }
  250. minimize() {
  251. const event = new Event(EVENT_MINIMIZED$1);
  252. if (this._parent) {
  253. this._parent.style.height = 'auto';
  254. this._parent.style.width = 'auto';
  255. this._parent.style.transition = 'all .15s';
  256. setTimeout(() => {
  257. const htmlTag = document.querySelector('html');
  258. if (htmlTag) {
  259. htmlTag.classList.remove(CLASS_NAME_MAXIMIZED);
  260. }
  261. if (this._parent) {
  262. this._parent.classList.remove(CLASS_NAME_MAXIMIZED);
  263. if (this._parent?.classList.contains(CLASS_NAME_WAS_COLLAPSED)) {
  264. this._parent.classList.remove(CLASS_NAME_WAS_COLLAPSED);
  265. }
  266. }
  267. }, 10);
  268. }
  269. this._element?.dispatchEvent(event);
  270. }
  271. toggleMaximize() {
  272. if (this._parent?.classList.contains(CLASS_NAME_MAXIMIZED)) {
  273. this.minimize();
  274. return;
  275. }
  276. this.maximize();
  277. }
  278. }
  279. /**
  280. *
  281. * Data Api implementation
  282. * ====================================================
  283. */
  284. onDOMContentLoaded(() => {
  285. const collapseBtn = document.querySelectorAll(SELECTOR_DATA_COLLAPSE);
  286. collapseBtn.forEach(btn => {
  287. btn.addEventListener('click', event => {
  288. event.preventDefault();
  289. const target = event.target;
  290. const data = new CardWidget(target, Default$1);
  291. data.toggle();
  292. });
  293. });
  294. const removeBtn = document.querySelectorAll(SELECTOR_DATA_REMOVE);
  295. removeBtn.forEach(btn => {
  296. btn.addEventListener('click', event => {
  297. event.preventDefault();
  298. const target = event.target;
  299. const data = new CardWidget(target, Default$1);
  300. data.remove();
  301. });
  302. });
  303. const maxBtn = document.querySelectorAll(SELECTOR_DATA_MAXIMIZE);
  304. maxBtn.forEach(btn => {
  305. btn.addEventListener('click', event => {
  306. event.preventDefault();
  307. const target = event.target;
  308. const data = new CardWidget(target, Default$1);
  309. data.toggleMaximize();
  310. });
  311. });
  312. });
  313. /**
  314. * --------------------------------------------
  315. * @file AdminLTE treeview.ts
  316. * @description Treeview plugin for AdminLTE.
  317. * @license MIT
  318. * --------------------------------------------
  319. */
  320. /**
  321. * ------------------------------------------------------------------------
  322. * Constants
  323. * ------------------------------------------------------------------------
  324. */
  325. // const NAME = 'Treeview'
  326. const DATA_KEY$3 = 'lte.treeview';
  327. const EVENT_KEY$3 = `.${DATA_KEY$3}`;
  328. const EVENT_EXPANDED$1 = `expanded${EVENT_KEY$3}`;
  329. const EVENT_COLLAPSED$1 = `collapsed${EVENT_KEY$3}`;
  330. const EVENT_LOAD_DATA_API = `load${EVENT_KEY$3}`;
  331. const CLASS_NAME_MENU_OPEN$1 = 'menu-open';
  332. const SELECTOR_NAV_ITEM$1 = '.nav-item';
  333. const SELECTOR_NAV_LINK = '.nav-link';
  334. const SELECTOR_TREEVIEW_MENU = '.nav-treeview';
  335. const SELECTOR_DATA_TOGGLE$1 = '[data-lte-toggle="treeview"]';
  336. const Default = {
  337. animationSpeed: 300,
  338. accordion: true
  339. };
  340. /**
  341. * Class Definition
  342. * ====================================================
  343. */
  344. class Treeview {
  345. _element;
  346. _config;
  347. constructor(element, config) {
  348. this._element = element;
  349. this._config = { ...Default, ...config };
  350. }
  351. open() {
  352. const event = new Event(EVENT_EXPANDED$1);
  353. if (this._config.accordion) {
  354. const openMenuList = this._element.parentElement?.querySelectorAll(`${SELECTOR_NAV_ITEM$1}.${CLASS_NAME_MENU_OPEN$1}`);
  355. openMenuList?.forEach(openMenu => {
  356. if (openMenu !== this._element.parentElement) {
  357. openMenu.classList.remove(CLASS_NAME_MENU_OPEN$1);
  358. const childElement = openMenu?.querySelector(SELECTOR_TREEVIEW_MENU);
  359. if (childElement) {
  360. slideUp(childElement, this._config.animationSpeed);
  361. }
  362. }
  363. });
  364. }
  365. this._element.classList.add(CLASS_NAME_MENU_OPEN$1);
  366. const childElement = this._element?.querySelector(SELECTOR_TREEVIEW_MENU);
  367. if (childElement) {
  368. slideDown(childElement, this._config.animationSpeed);
  369. }
  370. this._element.dispatchEvent(event);
  371. }
  372. close() {
  373. const event = new Event(EVENT_COLLAPSED$1);
  374. this._element.classList.remove(CLASS_NAME_MENU_OPEN$1);
  375. const childElement = this._element?.querySelector(SELECTOR_TREEVIEW_MENU);
  376. if (childElement) {
  377. slideUp(childElement, this._config.animationSpeed);
  378. }
  379. this._element.dispatchEvent(event);
  380. }
  381. toggle() {
  382. if (this._element.classList.contains(CLASS_NAME_MENU_OPEN$1)) {
  383. this.close();
  384. }
  385. else {
  386. this.open();
  387. }
  388. }
  389. }
  390. /**
  391. * ------------------------------------------------------------------------
  392. * Data Api implementation
  393. * ------------------------------------------------------------------------
  394. */
  395. onDOMContentLoaded(() => {
  396. const openMenuItems = document.querySelectorAll(`${SELECTOR_NAV_ITEM$1}.${CLASS_NAME_MENU_OPEN$1}`);
  397. openMenuItems.forEach(menuItem => {
  398. const childElement = menuItem.querySelector(SELECTOR_TREEVIEW_MENU);
  399. if (childElement) {
  400. slideDown(childElement, 0);
  401. const event = new Event(EVENT_LOAD_DATA_API);
  402. menuItem.dispatchEvent(event);
  403. }
  404. });
  405. const button = document.querySelectorAll(SELECTOR_DATA_TOGGLE$1);
  406. button.forEach(btn => {
  407. btn.addEventListener('click', event => {
  408. const target = event.target;
  409. const targetItem = target.closest(SELECTOR_NAV_ITEM$1);
  410. const targetLink = target.closest(SELECTOR_NAV_LINK);
  411. const targetTreeviewMenu = targetItem?.querySelector(SELECTOR_TREEVIEW_MENU);
  412. const lteToggleElement = event.currentTarget;
  413. // Avoid creating Treeview instances on non menu elements
  414. if (!targetTreeviewMenu) {
  415. return;
  416. }
  417. if (target?.getAttribute('href') === '#' || targetLink?.getAttribute('href') === '#') {
  418. event.preventDefault();
  419. }
  420. if (targetItem) {
  421. // Read data attributes
  422. const accordionAttr = lteToggleElement.dataset.accordion;
  423. const animationSpeedAttr = lteToggleElement.dataset.animationSpeed;
  424. // Build config from data attributes, fallback to Default
  425. const config = {
  426. accordion: accordionAttr === undefined ? Default.accordion : accordionAttr === 'true',
  427. animationSpeed: animationSpeedAttr === undefined ? Default.animationSpeed : Number(animationSpeedAttr)
  428. };
  429. const data = new Treeview(targetItem, config);
  430. data.toggle();
  431. }
  432. });
  433. });
  434. });
  435. /**
  436. * --------------------------------------------
  437. * @file AdminLTE direct-chat.ts
  438. * @description Direct chat for AdminLTE.
  439. * @license MIT
  440. * --------------------------------------------
  441. */
  442. /**
  443. * Constants
  444. * ====================================================
  445. */
  446. const DATA_KEY$2 = 'lte.direct-chat';
  447. const EVENT_KEY$2 = `.${DATA_KEY$2}`;
  448. const EVENT_EXPANDED = `expanded${EVENT_KEY$2}`;
  449. const EVENT_COLLAPSED = `collapsed${EVENT_KEY$2}`;
  450. const SELECTOR_DATA_TOGGLE = '[data-lte-toggle="chat-pane"]';
  451. const SELECTOR_DIRECT_CHAT = '.direct-chat';
  452. const CLASS_NAME_DIRECT_CHAT_OPEN = 'direct-chat-contacts-open';
  453. /**
  454. * Class Definition
  455. * ====================================================
  456. */
  457. class DirectChat {
  458. _element;
  459. constructor(element) {
  460. this._element = element;
  461. }
  462. toggle() {
  463. if (this._element.classList.contains(CLASS_NAME_DIRECT_CHAT_OPEN)) {
  464. const event = new Event(EVENT_COLLAPSED);
  465. this._element.classList.remove(CLASS_NAME_DIRECT_CHAT_OPEN);
  466. this._element.dispatchEvent(event);
  467. }
  468. else {
  469. const event = new Event(EVENT_EXPANDED);
  470. this._element.classList.add(CLASS_NAME_DIRECT_CHAT_OPEN);
  471. this._element.dispatchEvent(event);
  472. }
  473. }
  474. }
  475. /**
  476. *
  477. * Data Api implementation
  478. * ====================================================
  479. */
  480. onDOMContentLoaded(() => {
  481. const button = document.querySelectorAll(SELECTOR_DATA_TOGGLE);
  482. button.forEach(btn => {
  483. btn.addEventListener('click', event => {
  484. event.preventDefault();
  485. const target = event.target;
  486. const chatPane = target.closest(SELECTOR_DIRECT_CHAT);
  487. if (chatPane) {
  488. const data = new DirectChat(chatPane);
  489. data.toggle();
  490. }
  491. });
  492. });
  493. });
  494. /**
  495. * --------------------------------------------
  496. * @file AdminLTE fullscreen.ts
  497. * @description Fullscreen plugin for AdminLTE.
  498. * @license MIT
  499. * --------------------------------------------
  500. */
  501. /**
  502. * Constants
  503. * ============================================================================
  504. */
  505. const DATA_KEY$1 = 'lte.fullscreen';
  506. const EVENT_KEY$1 = `.${DATA_KEY$1}`;
  507. const EVENT_MAXIMIZED = `maximized${EVENT_KEY$1}`;
  508. const EVENT_MINIMIZED = `minimized${EVENT_KEY$1}`;
  509. const SELECTOR_FULLSCREEN_TOGGLE = '[data-lte-toggle="fullscreen"]';
  510. const SELECTOR_MAXIMIZE_ICON = '[data-lte-icon="maximize"]';
  511. const SELECTOR_MINIMIZE_ICON = '[data-lte-icon="minimize"]';
  512. /**
  513. * Class Definition.
  514. * ============================================================================
  515. */
  516. class FullScreen {
  517. _element;
  518. _config;
  519. constructor(element, config) {
  520. this._element = element;
  521. this._config = config;
  522. }
  523. inFullScreen() {
  524. const event = new Event(EVENT_MAXIMIZED);
  525. const iconMaximize = document.querySelector(SELECTOR_MAXIMIZE_ICON);
  526. const iconMinimize = document.querySelector(SELECTOR_MINIMIZE_ICON);
  527. void document.documentElement.requestFullscreen();
  528. if (iconMaximize) {
  529. iconMaximize.style.display = 'none';
  530. }
  531. if (iconMinimize) {
  532. iconMinimize.style.display = 'block';
  533. }
  534. this._element.dispatchEvent(event);
  535. }
  536. outFullscreen() {
  537. const event = new Event(EVENT_MINIMIZED);
  538. const iconMaximize = document.querySelector(SELECTOR_MAXIMIZE_ICON);
  539. const iconMinimize = document.querySelector(SELECTOR_MINIMIZE_ICON);
  540. void document.exitFullscreen();
  541. if (iconMaximize) {
  542. iconMaximize.style.display = 'block';
  543. }
  544. if (iconMinimize) {
  545. iconMinimize.style.display = 'none';
  546. }
  547. this._element.dispatchEvent(event);
  548. }
  549. toggleFullScreen() {
  550. if (document.fullscreenEnabled) {
  551. if (document.fullscreenElement) {
  552. this.outFullscreen();
  553. }
  554. else {
  555. this.inFullScreen();
  556. }
  557. }
  558. }
  559. }
  560. /**
  561. * Data Api implementation
  562. * ============================================================================
  563. */
  564. onDOMContentLoaded(() => {
  565. const buttons = document.querySelectorAll(SELECTOR_FULLSCREEN_TOGGLE);
  566. buttons.forEach(btn => {
  567. btn.addEventListener('click', event => {
  568. event.preventDefault();
  569. const target = event.target;
  570. const button = target.closest(SELECTOR_FULLSCREEN_TOGGLE);
  571. if (button) {
  572. const data = new FullScreen(button, undefined);
  573. data.toggleFullScreen();
  574. }
  575. });
  576. });
  577. });
  578. /**
  579. * --------------------------------------------
  580. * @file AdminLTE push-menu.ts
  581. * @description Push menu for AdminLTE.
  582. * @license MIT
  583. * --------------------------------------------
  584. */
  585. /**
  586. * ------------------------------------------------------------------------
  587. * Constants
  588. * ------------------------------------------------------------------------
  589. */
  590. const DATA_KEY = 'lte.push-menu';
  591. const EVENT_KEY = `.${DATA_KEY}`;
  592. const EVENT_OPEN = `open${EVENT_KEY}`;
  593. const EVENT_COLLAPSE = `collapse${EVENT_KEY}`;
  594. const CLASS_NAME_SIDEBAR_MINI = 'sidebar-mini';
  595. const CLASS_NAME_SIDEBAR_COLLAPSE = 'sidebar-collapse';
  596. const CLASS_NAME_SIDEBAR_OPEN = 'sidebar-open';
  597. const CLASS_NAME_SIDEBAR_EXPAND = 'sidebar-expand';
  598. const CLASS_NAME_SIDEBAR_OVERLAY = 'sidebar-overlay';
  599. const CLASS_NAME_MENU_OPEN = 'menu-open';
  600. const SELECTOR_APP_SIDEBAR = '.app-sidebar';
  601. const SELECTOR_SIDEBAR_MENU = '.sidebar-menu';
  602. const SELECTOR_NAV_ITEM = '.nav-item';
  603. const SELECTOR_NAV_TREEVIEW = '.nav-treeview';
  604. const SELECTOR_APP_WRAPPER = '.app-wrapper';
  605. const SELECTOR_SIDEBAR_EXPAND = `[class*="${CLASS_NAME_SIDEBAR_EXPAND}"]`;
  606. const SELECTOR_SIDEBAR_TOGGLE = '[data-lte-toggle="sidebar"]';
  607. const STORAGE_KEY_SIDEBAR_STATE = 'lte.sidebar.state';
  608. const Defaults = {
  609. sidebarBreakpoint: 992,
  610. enablePersistence: true
  611. };
  612. /**
  613. * Class Definition
  614. * ====================================================
  615. */
  616. class PushMenu {
  617. _element;
  618. _config;
  619. constructor(element, config) {
  620. this._element = element;
  621. this._config = { ...Defaults, ...config };
  622. }
  623. menusClose() {
  624. const navTreeview = document.querySelectorAll(SELECTOR_NAV_TREEVIEW);
  625. navTreeview.forEach(navTree => {
  626. navTree.style.removeProperty('display');
  627. navTree.style.removeProperty('height');
  628. });
  629. const navSidebar = document.querySelector(SELECTOR_SIDEBAR_MENU);
  630. const navItem = navSidebar?.querySelectorAll(SELECTOR_NAV_ITEM);
  631. if (navItem) {
  632. navItem.forEach(navI => {
  633. navI.classList.remove(CLASS_NAME_MENU_OPEN);
  634. });
  635. }
  636. }
  637. expand() {
  638. const event = new Event(EVENT_OPEN);
  639. document.body.classList.remove(CLASS_NAME_SIDEBAR_COLLAPSE);
  640. document.body.classList.add(CLASS_NAME_SIDEBAR_OPEN);
  641. this._element.dispatchEvent(event);
  642. }
  643. collapse() {
  644. const event = new Event(EVENT_COLLAPSE);
  645. document.body.classList.remove(CLASS_NAME_SIDEBAR_OPEN);
  646. document.body.classList.add(CLASS_NAME_SIDEBAR_COLLAPSE);
  647. this._element.dispatchEvent(event);
  648. }
  649. addSidebarBreakPoint() {
  650. const sidebarExpandList = document.querySelector(SELECTOR_SIDEBAR_EXPAND)?.classList ?? [];
  651. const sidebarExpand = Array.from(sidebarExpandList).find(className => className.startsWith(CLASS_NAME_SIDEBAR_EXPAND)) ?? '';
  652. const sidebar = document.getElementsByClassName(sidebarExpand)[0];
  653. const sidebarContent = globalThis.getComputedStyle(sidebar, '::before').getPropertyValue('content');
  654. this._config = { ...this._config, sidebarBreakpoint: Number(sidebarContent.replace(/[^\d.-]/g, '')) };
  655. // FIXED: Don't auto-collapse on mobile if sidebar is currently open
  656. // This prevents resize events (triggered by scrolling) from closing the sidebar
  657. const isCurrentlyOpen = document.body.classList.contains(CLASS_NAME_SIDEBAR_OPEN);
  658. if (window.innerWidth <= this._config.sidebarBreakpoint) {
  659. // Only collapse if not currently open (prevents scroll-triggered closes)
  660. if (!isCurrentlyOpen) {
  661. this.collapse();
  662. }
  663. }
  664. else {
  665. if (!document.body.classList.contains(CLASS_NAME_SIDEBAR_MINI)) {
  666. this.expand();
  667. }
  668. if (document.body.classList.contains(CLASS_NAME_SIDEBAR_MINI) && document.body.classList.contains(CLASS_NAME_SIDEBAR_COLLAPSE)) {
  669. this.collapse();
  670. }
  671. }
  672. }
  673. toggle() {
  674. if (document.body.classList.contains(CLASS_NAME_SIDEBAR_COLLAPSE)) {
  675. this.expand();
  676. }
  677. else {
  678. this.collapse();
  679. }
  680. this.saveSidebarState();
  681. }
  682. /**
  683. * Save sidebar state to localStorage
  684. */
  685. saveSidebarState() {
  686. if (!this._config.enablePersistence) {
  687. return;
  688. }
  689. // Check for SSR environment
  690. if (globalThis.window === undefined || globalThis.localStorage === undefined) {
  691. return;
  692. }
  693. try {
  694. const state = document.body.classList.contains(CLASS_NAME_SIDEBAR_COLLAPSE) ?
  695. CLASS_NAME_SIDEBAR_COLLAPSE :
  696. CLASS_NAME_SIDEBAR_OPEN;
  697. localStorage.setItem(STORAGE_KEY_SIDEBAR_STATE, state);
  698. }
  699. catch {
  700. // localStorage may be unavailable (private browsing, quota exceeded, etc.)
  701. }
  702. }
  703. /**
  704. * Load sidebar state from localStorage
  705. * Only applies on desktop; mobile always starts collapsed
  706. */
  707. loadSidebarState() {
  708. if (!this._config.enablePersistence) {
  709. return;
  710. }
  711. // Check for SSR environment
  712. if (globalThis.window === undefined || globalThis.localStorage === undefined) {
  713. return;
  714. }
  715. // Don't restore state on mobile - let responsive behavior handle it
  716. if (globalThis.innerWidth <= this._config.sidebarBreakpoint) {
  717. return;
  718. }
  719. try {
  720. const storedState = localStorage.getItem(STORAGE_KEY_SIDEBAR_STATE);
  721. if (storedState === CLASS_NAME_SIDEBAR_COLLAPSE) {
  722. this.collapse();
  723. }
  724. else if (storedState === CLASS_NAME_SIDEBAR_OPEN) {
  725. this.expand();
  726. }
  727. // If null (never saved), let default behavior apply
  728. }
  729. catch {
  730. // localStorage may be unavailable
  731. }
  732. }
  733. init() {
  734. this.addSidebarBreakPoint();
  735. this.loadSidebarState();
  736. }
  737. }
  738. /**
  739. * ------------------------------------------------------------------------
  740. * Data Api implementation
  741. * ------------------------------------------------------------------------
  742. */
  743. onDOMContentLoaded(() => {
  744. const sidebar = document?.querySelector(SELECTOR_APP_SIDEBAR);
  745. if (sidebar) {
  746. const data = new PushMenu(sidebar, Defaults);
  747. data.init();
  748. window.addEventListener('resize', () => {
  749. data.init();
  750. });
  751. }
  752. const sidebarOverlay = document.createElement('div');
  753. sidebarOverlay.className = CLASS_NAME_SIDEBAR_OVERLAY;
  754. document.querySelector(SELECTOR_APP_WRAPPER)?.append(sidebarOverlay);
  755. let overlayTouchMoved = false;
  756. // Handle touch events on overlay (area outside sidebar)
  757. sidebarOverlay.addEventListener('touchstart', () => {
  758. overlayTouchMoved = false;
  759. }, { passive: true });
  760. sidebarOverlay.addEventListener('touchmove', () => {
  761. overlayTouchMoved = true;
  762. }, { passive: true });
  763. sidebarOverlay.addEventListener('touchend', event => {
  764. if (!overlayTouchMoved) {
  765. event.preventDefault();
  766. const target = event.currentTarget;
  767. const data = new PushMenu(target, Defaults);
  768. data.collapse();
  769. }
  770. overlayTouchMoved = false;
  771. }, { passive: false });
  772. sidebarOverlay.addEventListener('click', event => {
  773. event.preventDefault();
  774. const target = event.currentTarget;
  775. const data = new PushMenu(target, Defaults);
  776. data.collapse();
  777. });
  778. const fullBtn = document.querySelectorAll(SELECTOR_SIDEBAR_TOGGLE);
  779. fullBtn.forEach(btn => {
  780. btn.addEventListener('click', event => {
  781. event.preventDefault();
  782. let button = event.currentTarget;
  783. if (button?.dataset.lteToggle !== 'sidebar') {
  784. button = button?.closest(SELECTOR_SIDEBAR_TOGGLE);
  785. }
  786. if (button) {
  787. event?.preventDefault();
  788. const data = new PushMenu(button, Defaults);
  789. data.toggle();
  790. }
  791. });
  792. });
  793. });
  794. /**
  795. * AdminLTE Accessibility Module
  796. * WCAG 2.1 AA Compliance Features
  797. */
  798. class AccessibilityManager {
  799. config;
  800. liveRegion = null;
  801. focusHistory = [];
  802. constructor(config = {}) {
  803. this.config = {
  804. announcements: true,
  805. skipLinks: true,
  806. focusManagement: true,
  807. keyboardNavigation: true,
  808. reducedMotion: true,
  809. ...config
  810. };
  811. this.init();
  812. }
  813. init() {
  814. if (this.config.announcements) {
  815. this.createLiveRegion();
  816. }
  817. if (this.config.skipLinks) {
  818. this.addSkipLinks();
  819. }
  820. if (this.config.focusManagement) {
  821. this.initFocusManagement();
  822. }
  823. if (this.config.keyboardNavigation) {
  824. this.initKeyboardNavigation();
  825. }
  826. if (this.config.reducedMotion) {
  827. this.respectReducedMotion();
  828. }
  829. this.initErrorAnnouncements();
  830. this.initTableAccessibility();
  831. this.initFormAccessibility();
  832. }
  833. // WCAG 4.1.3: Status Messages
  834. createLiveRegion() {
  835. if (this.liveRegion)
  836. return;
  837. this.liveRegion = document.createElement('div');
  838. this.liveRegion.id = 'live-region';
  839. this.liveRegion.className = 'live-region';
  840. this.liveRegion.setAttribute('aria-live', 'polite');
  841. this.liveRegion.setAttribute('aria-atomic', 'true');
  842. this.liveRegion.setAttribute('role', 'status');
  843. document.body.append(this.liveRegion);
  844. }
  845. // WCAG 2.4.1: Bypass Blocks
  846. addSkipLinks() {
  847. const skipLinksContainer = document.createElement('div');
  848. skipLinksContainer.className = 'skip-links';
  849. const skipToMain = document.createElement('a');
  850. skipToMain.href = '#main';
  851. skipToMain.className = 'skip-link';
  852. skipToMain.textContent = 'Skip to main content';
  853. const skipToNav = document.createElement('a');
  854. skipToNav.href = '#navigation';
  855. skipToNav.className = 'skip-link';
  856. skipToNav.textContent = 'Skip to navigation';
  857. skipLinksContainer.append(skipToMain);
  858. skipLinksContainer.append(skipToNav);
  859. document.body.insertBefore(skipLinksContainer, document.body.firstChild);
  860. // Ensure targets exist and are focusable
  861. this.ensureSkipTargets();
  862. }
  863. ensureSkipTargets() {
  864. const main = document.querySelector('#main, main, [role="main"]');
  865. if (main && !main.id) {
  866. main.id = 'main';
  867. }
  868. if (main && !main.hasAttribute('tabindex')) {
  869. main.setAttribute('tabindex', '-1');
  870. }
  871. const nav = document.querySelector('#navigation, nav, [role="navigation"]');
  872. if (nav && !nav.id) {
  873. nav.id = 'navigation';
  874. }
  875. if (nav && !nav.hasAttribute('tabindex')) {
  876. nav.setAttribute('tabindex', '-1');
  877. }
  878. }
  879. // WCAG 2.4.3: Focus Order & 2.4.7: Focus Visible
  880. initFocusManagement() {
  881. document.addEventListener('keydown', (event) => {
  882. if (event.key === 'Tab') {
  883. this.handleTabNavigation(event);
  884. }
  885. if (event.key === 'Escape') {
  886. this.handleEscapeKey(event);
  887. }
  888. });
  889. // Focus management for modals and dropdowns
  890. this.initModalFocusManagement();
  891. this.initDropdownFocusManagement();
  892. }
  893. handleTabNavigation(event) {
  894. const focusableElements = this.getFocusableElements();
  895. const currentIndex = focusableElements.indexOf(document.activeElement);
  896. if (event.shiftKey) {
  897. // Shift+Tab (backward)
  898. if (currentIndex <= 0) {
  899. event.preventDefault();
  900. focusableElements.at(-1)?.focus();
  901. }
  902. }
  903. else if (currentIndex >= focusableElements.length - 1) {
  904. // Tab (forward)
  905. event.preventDefault();
  906. focusableElements[0]?.focus();
  907. }
  908. }
  909. getFocusableElements() {
  910. const selector = [
  911. 'a[href]',
  912. 'button:not([disabled])',
  913. 'input:not([disabled])',
  914. 'select:not([disabled])',
  915. 'textarea:not([disabled])',
  916. '[tabindex]:not([tabindex="-1"])',
  917. '[contenteditable="true"]'
  918. ].join(', ');
  919. return Array.from(document.querySelectorAll(selector));
  920. }
  921. handleEscapeKey(event) {
  922. // Close modals, dropdowns, etc.
  923. const activeModal = document.querySelector('.modal.show');
  924. const activeDropdown = document.querySelector('.dropdown-menu.show');
  925. if (activeModal) {
  926. const closeButton = activeModal.querySelector('[data-bs-dismiss="modal"]');
  927. closeButton?.click();
  928. event.preventDefault();
  929. }
  930. else if (activeDropdown) {
  931. const toggleButton = document.querySelector('[data-bs-toggle="dropdown"][aria-expanded="true"]');
  932. toggleButton?.click();
  933. event.preventDefault();
  934. }
  935. }
  936. // WCAG 2.1.1: Keyboard Access
  937. initKeyboardNavigation() {
  938. // Add keyboard support for custom components
  939. document.addEventListener('keydown', (event) => {
  940. const target = event.target;
  941. // Handle arrow key navigation for menus
  942. if (target.closest('.nav, .navbar-nav, .dropdown-menu')) {
  943. this.handleMenuNavigation(event);
  944. }
  945. // Handle Enter and Space for custom buttons
  946. if ((event.key === 'Enter' || event.key === ' ') && target.hasAttribute('role') && target.getAttribute('role') === 'button' && !target.matches('button, input[type="button"], input[type="submit"]')) {
  947. event.preventDefault();
  948. target.click();
  949. }
  950. });
  951. }
  952. handleMenuNavigation(event) {
  953. if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) {
  954. return;
  955. }
  956. const currentElement = event.target;
  957. const menuItems = Array.from(currentElement.closest('.nav, .navbar-nav, .dropdown-menu')?.querySelectorAll('a, button') || []);
  958. const currentIndex = menuItems.indexOf(currentElement);
  959. let nextIndex;
  960. switch (event.key) {
  961. case 'ArrowDown':
  962. case 'ArrowRight': {
  963. nextIndex = currentIndex < menuItems.length - 1 ? currentIndex + 1 : 0;
  964. break;
  965. }
  966. case 'ArrowUp':
  967. case 'ArrowLeft': {
  968. nextIndex = currentIndex > 0 ? currentIndex - 1 : menuItems.length - 1;
  969. break;
  970. }
  971. case 'Home': {
  972. nextIndex = 0;
  973. break;
  974. }
  975. case 'End': {
  976. nextIndex = menuItems.length - 1;
  977. break;
  978. }
  979. default: {
  980. return;
  981. }
  982. }
  983. event.preventDefault();
  984. menuItems[nextIndex]?.focus();
  985. }
  986. // WCAG 2.3.3: Animation from Interactions
  987. respectReducedMotion() {
  988. const prefersReducedMotion = globalThis.matchMedia('(prefers-reduced-motion: reduce)').matches;
  989. if (prefersReducedMotion) {
  990. document.body.classList.add('reduce-motion');
  991. // Disable smooth scrolling
  992. document.documentElement.style.scrollBehavior = 'auto';
  993. // Reduce animation duration
  994. const style = document.createElement('style');
  995. style.textContent = `
  996. *, *::before, *::after {
  997. animation-duration: 0.01ms !important;
  998. animation-iteration-count: 1 !important;
  999. transition-duration: 0.01ms !important;
  1000. }
  1001. `;
  1002. document.head.append(style);
  1003. }
  1004. }
  1005. // WCAG 3.3.1: Error Identification
  1006. initErrorAnnouncements() {
  1007. const observer = new MutationObserver((mutations) => {
  1008. mutations.forEach((mutation) => {
  1009. mutation.addedNodes.forEach((node) => {
  1010. if (node.nodeType === Node.ELEMENT_NODE) {
  1011. const element = node;
  1012. // Check for error messages
  1013. if (element.matches('.alert-danger, .invalid-feedback, .error')) {
  1014. this.announce(element.textContent || 'Error occurred', 'assertive');
  1015. }
  1016. // Check for success messages
  1017. if (element.matches('.alert-success, .success')) {
  1018. this.announce(element.textContent || 'Success', 'polite');
  1019. }
  1020. }
  1021. });
  1022. });
  1023. });
  1024. observer.observe(document.body, {
  1025. childList: true,
  1026. subtree: true
  1027. });
  1028. }
  1029. // WCAG 1.3.1: Info and Relationships
  1030. initTableAccessibility() {
  1031. document.querySelectorAll('table').forEach((table) => {
  1032. // Add table role if missing
  1033. if (!table.hasAttribute('role')) {
  1034. table.setAttribute('role', 'table');
  1035. }
  1036. // Ensure headers have proper scope
  1037. table.querySelectorAll('th').forEach((th) => {
  1038. if (!th.hasAttribute('scope')) {
  1039. const isInThead = th.closest('thead');
  1040. const isFirstColumn = th.cellIndex === 0;
  1041. if (isInThead) {
  1042. th.setAttribute('scope', 'col');
  1043. }
  1044. else if (isFirstColumn) {
  1045. th.setAttribute('scope', 'row');
  1046. }
  1047. }
  1048. });
  1049. // Add caption if missing but title exists
  1050. if (!table.querySelector('caption') && table.hasAttribute('title')) {
  1051. const caption = document.createElement('caption');
  1052. caption.textContent = table.getAttribute('title') || '';
  1053. table.insertBefore(caption, table.firstChild);
  1054. }
  1055. });
  1056. }
  1057. // WCAG 3.3.2: Labels or Instructions
  1058. initFormAccessibility() {
  1059. document.querySelectorAll('input, select, textarea').forEach((input) => {
  1060. const htmlInput = input;
  1061. // Ensure all inputs have labels
  1062. if (!htmlInput.labels?.length && !htmlInput.hasAttribute('aria-label') && !htmlInput.hasAttribute('aria-labelledby')) {
  1063. const placeholder = htmlInput.getAttribute('placeholder');
  1064. if (placeholder) {
  1065. htmlInput.setAttribute('aria-label', placeholder);
  1066. }
  1067. }
  1068. // Add required indicators
  1069. if (htmlInput.hasAttribute('required')) {
  1070. const label = htmlInput.labels?.[0];
  1071. if (label && !label.querySelector('.required-indicator')) {
  1072. const indicator = document.createElement('span');
  1073. indicator.className = 'required-indicator sr-only';
  1074. indicator.textContent = ' (required)';
  1075. label.append(indicator);
  1076. }
  1077. }
  1078. // Handle invalid state unless the element explicitly opts out via the
  1079. // 'disable-adminlte-validations' class.
  1080. if (!htmlInput.classList.contains('disable-adminlte-validations')) {
  1081. htmlInput.addEventListener('invalid', () => {
  1082. this.handleFormError(htmlInput);
  1083. });
  1084. }
  1085. });
  1086. }
  1087. handleFormError(input) {
  1088. const errorId = `${input.id || input.name}-error`;
  1089. let errorElement = document.getElementById(errorId);
  1090. if (!errorElement) {
  1091. errorElement = document.createElement('div');
  1092. errorElement.id = errorId;
  1093. errorElement.className = 'invalid-feedback';
  1094. errorElement.setAttribute('role', 'alert');
  1095. // Always append the error element as the last child of the parent.
  1096. // This prevents breaking layouts where inputs use Bootstrap's
  1097. // `.input-group-text` decorators, ensuring the error stays below
  1098. // the entire input group.
  1099. input.parentNode?.append(errorElement);
  1100. }
  1101. errorElement.textContent = input.validationMessage;
  1102. input.setAttribute('aria-describedby', errorId);
  1103. input.classList.add('is-invalid');
  1104. this.announce(`Error in ${input.labels?.[0]?.textContent || input.name}: ${input.validationMessage}`, 'assertive');
  1105. }
  1106. // Modal focus management
  1107. initModalFocusManagement() {
  1108. document.addEventListener('shown.bs.modal', (event) => {
  1109. const modal = event.target;
  1110. const focusableElements = modal.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
  1111. if (focusableElements.length > 0) {
  1112. focusableElements[0].focus();
  1113. }
  1114. // Store previous focus
  1115. this.focusHistory.push(document.activeElement);
  1116. });
  1117. document.addEventListener('hidden.bs.modal', () => {
  1118. // Restore previous focus
  1119. const previousElement = this.focusHistory.pop();
  1120. if (previousElement) {
  1121. previousElement.focus();
  1122. }
  1123. });
  1124. }
  1125. // Dropdown focus management
  1126. initDropdownFocusManagement() {
  1127. document.addEventListener('shown.bs.dropdown', (event) => {
  1128. const dropdown = event.target;
  1129. const menu = dropdown.querySelector('.dropdown-menu');
  1130. const firstItem = menu?.querySelector('a, button');
  1131. if (firstItem) {
  1132. firstItem.focus();
  1133. }
  1134. });
  1135. }
  1136. // Public API methods
  1137. announce(message, priority = 'polite') {
  1138. if (!this.liveRegion) {
  1139. this.createLiveRegion();
  1140. }
  1141. if (this.liveRegion) {
  1142. this.liveRegion.setAttribute('aria-live', priority);
  1143. this.liveRegion.textContent = message;
  1144. // Clear after announcement
  1145. setTimeout(() => {
  1146. if (this.liveRegion) {
  1147. this.liveRegion.textContent = '';
  1148. }
  1149. }, 1000);
  1150. }
  1151. }
  1152. focusElement(selector) {
  1153. const element = document.querySelector(selector);
  1154. if (element) {
  1155. element.focus();
  1156. // Ensure element is visible
  1157. element.scrollIntoView({ behavior: 'smooth', block: 'center' });
  1158. }
  1159. }
  1160. trapFocus(container) {
  1161. const focusableElements = container.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
  1162. const focusableArray = Array.from(focusableElements);
  1163. const firstElement = focusableArray[0];
  1164. const lastElement = focusableArray.at(-1);
  1165. container.addEventListener('keydown', (event) => {
  1166. if (event.key === 'Tab') {
  1167. if (event.shiftKey) {
  1168. if (document.activeElement === firstElement) {
  1169. lastElement?.focus();
  1170. event.preventDefault();
  1171. }
  1172. }
  1173. else if (document.activeElement === lastElement) {
  1174. firstElement.focus();
  1175. event.preventDefault();
  1176. }
  1177. }
  1178. });
  1179. }
  1180. addLandmarks() {
  1181. // Add main landmark if missing
  1182. const main = document.querySelector('main');
  1183. if (!main) {
  1184. const appMain = document.querySelector('.app-main');
  1185. if (appMain) {
  1186. appMain.setAttribute('role', 'main');
  1187. appMain.id = 'main';
  1188. }
  1189. }
  1190. // Add navigation landmarks
  1191. document.querySelectorAll('.navbar-nav, .nav').forEach((nav, index) => {
  1192. if (!nav.hasAttribute('role')) {
  1193. nav.setAttribute('role', 'navigation');
  1194. }
  1195. if (!nav.hasAttribute('aria-label')) {
  1196. nav.setAttribute('aria-label', `Navigation ${index + 1}`);
  1197. }
  1198. });
  1199. // Add search landmark
  1200. const searchForm = document.querySelector('form[role="search"], .navbar-search');
  1201. if (searchForm && !searchForm.hasAttribute('role')) {
  1202. searchForm.setAttribute('role', 'search');
  1203. }
  1204. }
  1205. }
  1206. // Initialize accessibility when DOM is ready
  1207. const initAccessibility = (config) => {
  1208. return new AccessibilityManager(config);
  1209. };
  1210. /**
  1211. * AdminLTE v4.0.0-rc5
  1212. * Author: Colorlib
  1213. * Website: AdminLTE.io <https://adminlte.io>
  1214. * License: Open source - MIT <https://opensource.org/licenses/MIT>
  1215. */
  1216. onDOMContentLoaded(() => {
  1217. /**
  1218. * Initialize AdminLTE Core Components
  1219. * -------------------------------
  1220. */
  1221. const layout = new Layout(document.body);
  1222. layout.holdTransition();
  1223. /**
  1224. * Initialize Accessibility Features - WCAG 2.1 AA Compliance
  1225. * --------------------------------------------------------
  1226. */
  1227. const accessibilityManager = initAccessibility({
  1228. announcements: true,
  1229. skipLinks: true,
  1230. focusManagement: true,
  1231. keyboardNavigation: true,
  1232. reducedMotion: true
  1233. });
  1234. // Add semantic landmarks
  1235. accessibilityManager.addLandmarks();
  1236. // Mark app as loaded after initialization
  1237. setTimeout(() => {
  1238. document.body.classList.add('app-loaded');
  1239. }, 400);
  1240. });
  1241. exports.CardWidget = CardWidget;
  1242. exports.DirectChat = DirectChat;
  1243. exports.FullScreen = FullScreen;
  1244. exports.Layout = Layout;
  1245. exports.PushMenu = PushMenu;
  1246. exports.Treeview = Treeview;
  1247. exports.initAccessibility = initAccessibility;
  1248. }));
  1249. //# sourceMappingURL=adminlte.js.map