admin-lte 4.0.4 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/css/adminlte-docs.css +878 -0
  3. package/dist/css/adminlte-docs.css.map +1 -0
  4. package/dist/css/adminlte-docs.min.css +7 -0
  5. package/dist/css/adminlte-docs.min.css.map +1 -0
  6. package/dist/css/adminlte-docs.rtl.css +877 -0
  7. package/dist/css/adminlte-docs.rtl.css.map +1 -0
  8. package/dist/css/adminlte-docs.rtl.min.css +7 -0
  9. package/dist/css/adminlte-docs.rtl.min.css.map +1 -0
  10. package/dist/css/adminlte.css +2 -782
  11. package/dist/css/adminlte.css.map +1 -1
  12. package/dist/css/adminlte.min.css +2 -2
  13. package/dist/css/adminlte.min.css.map +1 -1
  14. package/dist/css/adminlte.rtl.css +2 -782
  15. package/dist/css/adminlte.rtl.css.map +1 -1
  16. package/dist/css/adminlte.rtl.min.css +2 -2
  17. package/dist/css/adminlte.rtl.min.css.map +1 -1
  18. package/dist/js/adminlte.esm.js +1365 -0
  19. package/dist/js/adminlte.esm.js.map +1 -0
  20. package/dist/js/adminlte.esm.min.js +7 -0
  21. package/dist/js/adminlte.esm.min.js.map +1 -0
  22. package/dist/js/adminlte.js +391 -200
  23. package/dist/js/adminlte.js.map +1 -1
  24. package/dist/js/adminlte.min.js +2 -2
  25. package/dist/js/adminlte.min.js.map +1 -1
  26. package/dist/js/types/accessibility.d.ts +46 -0
  27. package/dist/js/types/adminlte.d.ts +9 -0
  28. package/dist/js/types/base-component.d.ts +36 -0
  29. package/dist/js/types/card-widget.d.ts +30 -0
  30. package/dist/js/types/color-mode.d.ts +56 -0
  31. package/dist/js/types/direct-chat.d.ts +19 -0
  32. package/dist/js/types/fullscreen.d.ts +21 -0
  33. package/dist/js/types/layout.d.ts +19 -0
  34. package/dist/js/types/push-menu.d.ts +102 -0
  35. package/dist/js/types/treeview.d.ts +27 -0
  36. package/dist/js/types/util/index.d.ts +42 -0
  37. package/package.json +32 -6
  38. package/src/scss/_app-header.scss +3 -1
  39. package/src/scss/_app-main.scss +4 -0
  40. package/src/scss/_app-sidebar.scss +3 -1
  41. package/src/scss/_bootstrap-overrides.scss +60 -0
  42. package/src/scss/_info-box.scss +3 -1
  43. package/src/scss/_progress-bars.scss +3 -1
  44. package/src/scss/_table.scss +4 -2
  45. package/src/scss/adminlte-docs.scss +25 -0
  46. package/src/scss/adminlte.scss +5 -4
  47. package/src/scss/pages/_login_and_register.scss +3 -1
  48. package/src/ts/adminlte.ts +3 -1
  49. package/src/ts/base-component.ts +84 -0
  50. package/src/ts/card-widget.ts +124 -100
  51. package/src/ts/color-mode.ts +174 -0
  52. package/src/ts/direct-chat.ts +38 -32
  53. package/src/ts/fullscreen.ts +32 -23
  54. package/src/ts/push-menu.ts +69 -36
  55. package/src/ts/treeview.ts +84 -50
  56. package/src/ts/util/index.ts +1 -1
  57. package/src/scss/_bootstrap-variables.scss +0 -1766
@@ -6,6 +6,7 @@
6
6
  * --------------------------------------------
7
7
  */
8
8
 
9
+ import { BaseComponent, dispatchCustomEvent } from './base-component'
9
10
  import {
10
11
  getLifecycleSignal,
11
12
  onDOMContentLoaded
@@ -15,8 +16,8 @@ import {
15
16
  * Constants
16
17
  * ============================================================================
17
18
  */
18
- const DATA_KEY = 'lte.fullscreen'
19
- const EVENT_KEY = `.${DATA_KEY}`
19
+ const NAME = 'fullscreen'
20
+ const EVENT_KEY = `.lte.${NAME}`
20
21
  const EVENT_MAXIMIZED = `maximized${EVENT_KEY}`
21
22
  const EVENT_MINIMIZED = `minimized${EVENT_KEY}`
22
23
 
@@ -48,7 +49,7 @@ function syncFullScreenState(): void {
48
49
  const eventName = isFullScreen ? EVENT_MAXIMIZED : EVENT_MINIMIZED
49
50
 
50
51
  document.querySelectorAll(SELECTOR_FULLSCREEN_TOGGLE).forEach(button => {
51
- button.dispatchEvent(new Event(eventName))
52
+ dispatchCustomEvent(button, eventName)
52
53
  })
53
54
  }
54
55
 
@@ -56,13 +57,17 @@ function syncFullScreenState(): void {
56
57
  * Class Definition.
57
58
  * ============================================================================
58
59
  */
59
- class FullScreen {
60
- _element: HTMLElement
61
- _config: undefined
60
+ class FullScreen extends BaseComponent {
61
+ static get NAME(): string {
62
+ return NAME
63
+ }
64
+
65
+ static getInstance(element: Element | null | undefined): FullScreen | null {
66
+ return this._getInstance(element) as FullScreen | null
67
+ }
62
68
 
63
- constructor(element: HTMLElement, config?: undefined) {
64
- this._element = element
65
- this._config = config
69
+ static getOrCreateInstance(element: HTMLElement): FullScreen {
70
+ return this.getInstance(element) ?? new this(element)
66
71
  }
67
72
 
68
73
  inFullScreen(): void {
@@ -96,25 +101,29 @@ class FullScreen {
96
101
  /**
97
102
  * Data Api implementation
98
103
  * ============================================================================
104
+ * Toggle clicks are delegated on `document`; the fullscreenchange listener is
105
+ * re-registered per page load with the lifecycle signal.
99
106
  */
100
- onDOMContentLoaded(() => {
101
- document.addEventListener('fullscreenchange', syncFullScreenState, { signal: getLifecycleSignal() })
102
107
 
103
- const buttons = document.querySelectorAll(SELECTOR_FULLSCREEN_TOGGLE)
108
+ document.addEventListener('click', event => {
109
+ const target = event.target
104
110
 
105
- buttons.forEach(btn => {
106
- btn.addEventListener('click', event => {
107
- event.preventDefault()
111
+ if (!(target instanceof Element)) {
112
+ return
113
+ }
108
114
 
109
- const target = event.target as HTMLElement
110
- const button = target.closest(SELECTOR_FULLSCREEN_TOGGLE) as HTMLElement | undefined
115
+ const button = target.closest(SELECTOR_FULLSCREEN_TOGGLE) as HTMLElement | null
111
116
 
112
- if (button) {
113
- const data = new FullScreen(button, undefined)
114
- data.toggleFullScreen()
115
- }
116
- })
117
- })
117
+ if (!button) {
118
+ return
119
+ }
120
+
121
+ event.preventDefault()
122
+ FullScreen.getOrCreateInstance(button).toggleFullScreen()
123
+ })
124
+
125
+ onDOMContentLoaded(() => {
126
+ document.addEventListener('fullscreenchange', syncFullScreenState, { signal: getLifecycleSignal() })
118
127
  })
119
128
 
120
129
  export default FullScreen
@@ -6,6 +6,7 @@
6
6
  * ----------------------------------------------------------------------------
7
7
  */
8
8
 
9
+ import { BaseComponent, dispatchCustomEvent } from './base-component'
9
10
  import {
10
11
  getLifecycleSignal,
11
12
  onDOMContentLoaded
@@ -17,11 +18,17 @@ import {
17
18
  * ----------------------------------------------------------------------------
18
19
  */
19
20
 
20
- const DATA_KEY = 'lte.push-menu'
21
- const EVENT_KEY = `.${DATA_KEY}`
21
+ const NAME = 'push-menu'
22
+ const EVENT_KEY = `.lte.${NAME}`
23
+
24
+ // Cancelable "before" events: preventDefault() aborts the state change.
22
25
  const EVENT_OPEN = `open${EVENT_KEY}`
23
26
  const EVENT_COLLAPSE = `collapse${EVENT_KEY}`
24
27
 
28
+ // "After" events, dispatched once the classes have been applied.
29
+ const EVENT_OPENED = `opened${EVENT_KEY}`
30
+ const EVENT_COLLAPSED = `collapsed${EVENT_KEY}`
31
+
25
32
  const CLASS_NAME_SIDEBAR_MINI = 'sidebar-mini'
26
33
  const CLASS_NAME_SIDEBAR_EXPAND = 'sidebar-expand'
27
34
  const CLASS_NAME_SIDEBAR_OVERLAY = 'sidebar-overlay'
@@ -68,12 +75,23 @@ const Defaults: Config = {
68
75
  * ----------------------------------------------------------------------------
69
76
  */
70
77
 
71
- class PushMenu {
72
- _element: HTMLElement
78
+ class PushMenu extends BaseComponent {
79
+ static get NAME(): string {
80
+ return NAME
81
+ }
82
+
83
+ static getInstance(element: Element | null | undefined): PushMenu | null {
84
+ return this._getInstance(element) as PushMenu | null
85
+ }
86
+
87
+ static getOrCreateInstance(element: HTMLElement, config: Partial<Config> = {}): PushMenu {
88
+ return this.getInstance(element) ?? new this(element, config)
89
+ }
90
+
73
91
  _config: Config
74
92
 
75
- constructor(element: HTMLElement, config: Config) {
76
- this._element = element
93
+ constructor(element: HTMLElement, config: Partial<Config> = {}) {
94
+ super(element)
77
95
  this._config = { ...Defaults, ...config }
78
96
  }
79
97
 
@@ -118,6 +136,12 @@ class PushMenu {
118
136
  * Expand the sidebar menu.
119
137
  */
120
138
  expand(): void {
139
+ // The "open" event is cancelable: preventDefault() keeps the sidebar
140
+ // in its current state.
141
+ if (dispatchCustomEvent(this._element, EVENT_OPEN, { cancelable: true }).defaultPrevented) {
142
+ return
143
+ }
144
+
121
145
  // Remove the sidebar-collapse class. Only on mobile, add the sidebar-open
122
146
  // class to indicate the sidebar is explicitly open.
123
147
 
@@ -127,24 +151,24 @@ class PushMenu {
127
151
  document.body.classList.add(CLASS_NAME_SIDEBAR_OPEN)
128
152
  }
129
153
 
130
- // Dispatch the expand event.
131
-
132
- this._element.dispatchEvent(new Event(EVENT_OPEN))
154
+ dispatchCustomEvent(this._element, EVENT_OPENED)
133
155
  }
134
156
 
135
157
  /**
136
158
  * Collapse the sidebar menu.
137
159
  */
138
160
  collapse(): void {
161
+ if (dispatchCustomEvent(this._element, EVENT_COLLAPSE, { cancelable: true }).defaultPrevented) {
162
+ return
163
+ }
164
+
139
165
  // Remove the sidebar-open class (if present), and add the sidebar-collapse
140
166
  // class.
141
167
 
142
168
  document.body.classList.remove(CLASS_NAME_SIDEBAR_OPEN)
143
169
  document.body.classList.add(CLASS_NAME_SIDEBAR_COLLAPSE)
144
170
 
145
- // Dispatch the collapse event.
146
-
147
- this._element.dispatchEvent(new Event(EVENT_COLLAPSE))
171
+ dispatchCustomEvent(this._element, EVENT_COLLAPSED)
148
172
  }
149
173
 
150
174
  /**
@@ -332,12 +356,40 @@ class PushMenu {
332
356
  * ----------------------------------------------------------------------------
333
357
  * Data Api implementation
334
358
  * ----------------------------------------------------------------------------
359
+ * Toggle clicks are handled by one delegated listener on `document`, so
360
+ * toggle buttons added later work and the listener survives Turbo's <body>
361
+ * swaps. The instance itself is created per page load below and can be
362
+ * retrieved anywhere with:
363
+ *
364
+ * PushMenu.getInstance(document.querySelector('.app-sidebar'))
335
365
  */
336
366
 
367
+ document.addEventListener('click', event => {
368
+ const target = event.target
369
+
370
+ if (!(target instanceof Element)) {
371
+ return
372
+ }
373
+
374
+ const button = target.closest(SELECTOR_SIDEBAR_TOGGLE)
375
+
376
+ if (!button) {
377
+ return
378
+ }
379
+
380
+ event.preventDefault()
381
+
382
+ const sidebar = document.querySelector(SELECTOR_APP_SIDEBAR) as HTMLElement | null
383
+
384
+ if (sidebar) {
385
+ PushMenu.getOrCreateInstance(sidebar).toggle()
386
+ }
387
+ })
388
+
337
389
  onDOMContentLoaded(() => {
338
390
  // Find the sidebar element in the DOM.
339
391
 
340
- const sidebar = document?.querySelector(SELECTOR_APP_SIDEBAR) as HTMLElement | undefined
392
+ const sidebar = document.querySelector(SELECTOR_APP_SIDEBAR) as HTMLElement | null
341
393
 
342
394
  if (!sidebar) {
343
395
  return
@@ -358,9 +410,11 @@ onDOMContentLoaded(() => {
358
410
  enablePersistenceAttr === 'true'
359
411
  }
360
412
 
361
- // Initialize the PushMenu plugin (a unique instance).
413
+ // Initialize the PushMenu plugin (a unique instance per sidebar element;
414
+ // Turbo navigations swap in a fresh element, so a fresh instance is
415
+ // created and the old one is garbage-collected with the old <body>).
362
416
 
363
- const pushMenu = new PushMenu(sidebar, config)
417
+ const pushMenu = PushMenu.getOrCreateInstance(sidebar, config)
364
418
  pushMenu.init()
365
419
 
366
420
  // Update the sidebar state only when the viewport crosses the sidebar
@@ -416,27 +470,6 @@ onDOMContentLoaded(() => {
416
470
  event.preventDefault()
417
471
  pushMenu.collapse()
418
472
  })
419
-
420
- // Handle click events on sidebar toggle buttons.
421
-
422
- const fullBtn = document.querySelectorAll(SELECTOR_SIDEBAR_TOGGLE)
423
-
424
- fullBtn.forEach(btn => {
425
- btn.addEventListener('click', event => {
426
- event.preventDefault()
427
-
428
- let button = event.currentTarget as HTMLElement | undefined
429
-
430
- if (button?.dataset.lteToggle !== 'sidebar') {
431
- button = button?.closest(SELECTOR_SIDEBAR_TOGGLE) as HTMLElement | undefined
432
- }
433
-
434
- if (button) {
435
- event?.preventDefault()
436
- pushMenu.toggle()
437
- }
438
- })
439
- })
440
473
  })
441
474
 
442
475
  export default PushMenu
@@ -6,6 +6,7 @@
6
6
  * --------------------------------------------
7
7
  */
8
8
 
9
+ import { BaseComponent, dispatchCustomEvent } from './base-component'
9
10
  import {
10
11
  onDOMContentLoaded,
11
12
  slideDown,
@@ -18,10 +19,14 @@ import {
18
19
  * ------------------------------------------------------------------------
19
20
  */
20
21
 
21
- // const NAME = 'Treeview'
22
- const DATA_KEY = 'lte.treeview'
23
- const EVENT_KEY = `.${DATA_KEY}`
22
+ const NAME = 'treeview'
23
+ const EVENT_KEY = `.lte.${NAME}`
24
24
 
25
+ // Cancelable "before" events (dispatched on the nav-item as an action starts)
26
+ const EVENT_EXPAND = `expand${EVENT_KEY}`
27
+ const EVENT_COLLAPSE = `collapse${EVENT_KEY}`
28
+
29
+ // "After" events (dispatched on the nav-item when the animation finished)
25
30
  const EVENT_EXPANDED = `expanded${EVENT_KEY}`
26
31
  const EVENT_COLLAPSED = `collapsed${EVENT_KEY}`
27
32
  const EVENT_LOAD_DATA_API = `load${EVENT_KEY}`
@@ -56,17 +61,30 @@ const setAriaExpanded = (navItem: Element, expanded: boolean): void => {
56
61
  * ====================================================
57
62
  */
58
63
 
59
- class Treeview {
60
- _element: HTMLElement
64
+ class Treeview extends BaseComponent {
65
+ static get NAME(): string {
66
+ return NAME
67
+ }
68
+
69
+ static getInstance(element: Element | null | undefined): Treeview | null {
70
+ return this._getInstance(element) as Treeview | null
71
+ }
72
+
73
+ static getOrCreateInstance(element: HTMLElement, config: Partial<Config> = {}): Treeview {
74
+ return this.getInstance(element) ?? new this(element, config)
75
+ }
76
+
61
77
  _config: Config
62
78
 
63
- constructor(element: HTMLElement, config: Config) {
64
- this._element = element
79
+ constructor(element: HTMLElement, config: Partial<Config> = {}) {
80
+ super(element)
65
81
  this._config = { ...Default, ...config }
66
82
  }
67
83
 
68
84
  open(): void {
69
- const event = new Event(EVENT_EXPANDED)
85
+ if (dispatchCustomEvent(this._element, EVENT_EXPAND, { cancelable: true }).defaultPrevented) {
86
+ return
87
+ }
70
88
 
71
89
  if (this._config.accordion) {
72
90
  const openMenuList = this._element.parentElement?.querySelectorAll(`${SELECTOR_NAV_ITEM}.${CLASS_NAME_MENU_OPEN}`)
@@ -90,26 +108,36 @@ class Treeview {
90
108
  this._element.classList.add(CLASS_NAME_MENU_OPEN)
91
109
  setAriaExpanded(this._element, true)
92
110
 
93
- const childElement = this._element?.querySelector(SELECTOR_TREEVIEW_MENU) as HTMLElement | undefined
111
+ const childElement = this._element.querySelector(SELECTOR_TREEVIEW_MENU) as HTMLElement | undefined
94
112
  if (childElement) {
95
113
  slideDown(childElement, this._config.animationSpeed)
96
114
  }
97
115
 
98
- this._element.dispatchEvent(event)
116
+ setTimeout(() => {
117
+ if (this._element.classList.contains(CLASS_NAME_MENU_OPEN)) {
118
+ dispatchCustomEvent(this._element, EVENT_EXPANDED)
119
+ }
120
+ }, this._config.animationSpeed)
99
121
  }
100
122
 
101
123
  close(): void {
102
- const event = new Event(EVENT_COLLAPSED)
124
+ if (dispatchCustomEvent(this._element, EVENT_COLLAPSE, { cancelable: true }).defaultPrevented) {
125
+ return
126
+ }
103
127
 
104
128
  this._element.classList.remove(CLASS_NAME_MENU_OPEN)
105
129
  setAriaExpanded(this._element, false)
106
130
 
107
- const childElement = this._element?.querySelector(SELECTOR_TREEVIEW_MENU) as HTMLElement | undefined
131
+ const childElement = this._element.querySelector(SELECTOR_TREEVIEW_MENU) as HTMLElement | undefined
108
132
  if (childElement) {
109
133
  slideUp(childElement, this._config.animationSpeed)
110
134
  }
111
135
 
112
- this._element.dispatchEvent(event)
136
+ setTimeout(() => {
137
+ if (!this._element.classList.contains(CLASS_NAME_MENU_OPEN)) {
138
+ dispatchCustomEvent(this._element, EVENT_COLLAPSED)
139
+ }
140
+ }, this._config.animationSpeed)
113
141
  }
114
142
 
115
143
  toggle(): void {
@@ -125,8 +153,50 @@ class Treeview {
125
153
  * ------------------------------------------------------------------------
126
154
  * Data Api implementation
127
155
  * ------------------------------------------------------------------------
156
+ * Clicks are handled by one delegated listener on `document`, so nav items
157
+ * added after load (dynamic menus, Turbo Frames) work without
158
+ * re-initialisation. Initial state (pre-opened menus, ARIA stamping) is
159
+ * applied per page load below.
128
160
  */
129
161
 
162
+ document.addEventListener('click', event => {
163
+ const target = event.target
164
+
165
+ if (!(target instanceof Element)) {
166
+ return
167
+ }
168
+
169
+ const toggleRoot = target.closest(SELECTOR_DATA_TOGGLE) as HTMLElement | null
170
+
171
+ if (!toggleRoot) {
172
+ return
173
+ }
174
+
175
+ const targetItem = target.closest(SELECTOR_NAV_ITEM) as HTMLElement | null
176
+ const targetLink = target.closest(SELECTOR_NAV_LINK)
177
+
178
+ // Avoid creating Treeview instances on non menu elements
179
+ if (!targetItem?.querySelector(SELECTOR_TREEVIEW_MENU)) {
180
+ return
181
+ }
182
+
183
+ if (target.getAttribute('href') === '#' || targetLink?.getAttribute('href') === '#') {
184
+ event.preventDefault()
185
+ }
186
+
187
+ // Read config from data attributes on the treeview root, falling back to
188
+ // Default. The config of the first interaction sticks to the instance.
189
+ const accordionAttr = toggleRoot.dataset.accordion
190
+ const animationSpeedAttr = toggleRoot.dataset.animationSpeed
191
+
192
+ const config: Config = {
193
+ accordion: accordionAttr === undefined ? Default.accordion : accordionAttr === 'true',
194
+ animationSpeed: animationSpeedAttr === undefined ? Default.animationSpeed : Number(animationSpeedAttr)
195
+ }
196
+
197
+ Treeview.getOrCreateInstance(targetItem, config).toggle()
198
+ })
199
+
130
200
  onDOMContentLoaded(() => {
131
201
  const openMenuItems = document.querySelectorAll(`${SELECTOR_NAV_ITEM}.${CLASS_NAME_MENU_OPEN}`)
132
202
 
@@ -149,42 +219,6 @@ onDOMContentLoaded(() => {
149
219
  }
150
220
  })
151
221
  })
152
-
153
- const button = document.querySelectorAll(SELECTOR_DATA_TOGGLE)
154
-
155
- button.forEach(btn => {
156
- btn.addEventListener('click', event => {
157
- const target = event.target as HTMLElement
158
- const targetItem = target.closest(SELECTOR_NAV_ITEM) as HTMLElement | undefined
159
- const targetLink = target.closest(SELECTOR_NAV_LINK) as HTMLAnchorElement | undefined
160
- const targetTreeviewMenu = targetItem?.querySelector(SELECTOR_TREEVIEW_MENU) as HTMLElement | undefined
161
- const lteToggleElement = event.currentTarget as HTMLElement
162
-
163
- // Avoid creating Treeview instances on non menu elements
164
- if (!targetTreeviewMenu) {
165
- return
166
- }
167
-
168
- if (target?.getAttribute('href') === '#' || targetLink?.getAttribute('href') === '#') {
169
- event.preventDefault()
170
- }
171
-
172
- if (targetItem) {
173
- // Read data attributes
174
- const accordionAttr = lteToggleElement.dataset.accordion
175
- const animationSpeedAttr = lteToggleElement.dataset.animationSpeed
176
-
177
- // Build config from data attributes, fallback to Default
178
- const config: Config = {
179
- accordion: accordionAttr === undefined ? Default.accordion : accordionAttr === 'true',
180
- animationSpeed: animationSpeedAttr === undefined ? Default.animationSpeed : Number(animationSpeedAttr)
181
- }
182
-
183
- const data = new Treeview(targetItem, config)
184
- data.toggle()
185
- }
186
- })
187
- })
188
222
  })
189
223
 
190
- export default Treeview
224
+ export default Treeview
@@ -109,7 +109,7 @@ const safePropertyAccess = (obj: Record<string, unknown>, property: string): unk
109
109
  * the previous animation's steps: without this, rapidly toggling a treeview or
110
110
  * card leaves a stale cleanup timer that strips height/transition mid-animation
111
111
  * and desyncs the element's display state from its component's classes. */
112
- const slideTimers = new WeakMap<HTMLElement, number[]>()
112
+ const slideTimers = new WeakMap<HTMLElement, Array<ReturnType<typeof globalThis.setTimeout>>>()
113
113
 
114
114
  const cancelSlide = (target: HTMLElement): void => {
115
115
  const timers = slideTimers.get(target) ?? []