admin-lte 4.0.4 → 4.2.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.
- package/CHANGELOG.md +41 -1
- package/README.md +6 -6
- package/dist/css/adminlte-docs.css +878 -0
- package/dist/css/adminlte-docs.css.map +1 -0
- package/dist/css/adminlte-docs.min.css +7 -0
- package/dist/css/adminlte-docs.min.css.map +1 -0
- package/dist/css/adminlte-docs.rtl.css +877 -0
- package/dist/css/adminlte-docs.rtl.css.map +1 -0
- package/dist/css/adminlte-docs.rtl.min.css +7 -0
- package/dist/css/adminlte-docs.rtl.min.css.map +1 -0
- package/dist/css/adminlte.css +132 -912
- package/dist/css/adminlte.css.map +1 -1
- package/dist/css/adminlte.min.css +2 -2
- package/dist/css/adminlte.min.css.map +1 -1
- package/dist/css/adminlte.rtl.css +132 -912
- package/dist/css/adminlte.rtl.css.map +1 -1
- package/dist/css/adminlte.rtl.min.css +2 -2
- package/dist/css/adminlte.rtl.min.css.map +1 -1
- package/dist/js/adminlte.esm.js +1406 -0
- package/dist/js/adminlte.esm.js.map +1 -0
- package/dist/js/adminlte.esm.min.js +7 -0
- package/dist/js/adminlte.esm.min.js.map +1 -0
- package/dist/js/adminlte.js +449 -215
- package/dist/js/adminlte.js.map +1 -1
- package/dist/js/adminlte.min.js +2 -2
- package/dist/js/adminlte.min.js.map +1 -1
- package/dist/js/types/accessibility.d.ts +46 -0
- package/dist/js/types/adminlte.d.ts +10 -0
- package/dist/js/types/base-component.d.ts +60 -0
- package/dist/js/types/card-widget.d.ts +30 -0
- package/dist/js/types/color-mode.d.ts +74 -0
- package/dist/js/types/direct-chat.d.ts +19 -0
- package/dist/js/types/fullscreen.d.ts +21 -0
- package/dist/js/types/layout.d.ts +19 -0
- package/dist/js/types/push-menu.d.ts +123 -0
- package/dist/js/types/treeview.d.ts +27 -0
- package/dist/js/types/util/index.d.ts +79 -0
- package/package.json +52 -25
- package/src/scss/_app-header.scss +3 -1
- package/src/scss/_app-main.scss +4 -0
- package/src/scss/_app-sidebar.scss +3 -1
- package/src/scss/_bootstrap-overrides.scss +60 -0
- package/src/scss/_info-box.scss +3 -1
- package/src/scss/_progress-bars.scss +3 -1
- package/src/scss/_table.scss +4 -2
- package/src/scss/adminlte-docs.scss +25 -0
- package/src/scss/adminlte.scss +5 -4
- package/src/scss/pages/_login_and_register.scss +3 -1
- package/src/ts/accessibility.ts +6 -2
- package/src/ts/adminlte.ts +7 -3
- package/src/ts/base-component.ts +111 -0
- package/src/ts/card-widget.ts +124 -100
- package/src/ts/color-mode.ts +254 -0
- package/src/ts/direct-chat.ts +38 -32
- package/src/ts/fullscreen.ts +32 -23
- package/src/ts/push-menu.ts +106 -43
- package/src/ts/treeview.ts +84 -50
- package/src/ts/util/index.ts +80 -16
- package/src/scss/_bootstrap-variables.scss +0 -1766
package/src/ts/push-menu.ts
CHANGED
|
@@ -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
|
|
21
|
-
const EVENT_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,44 @@ const Defaults: Config = {
|
|
|
68
75
|
* ----------------------------------------------------------------------------
|
|
69
76
|
*/
|
|
70
77
|
|
|
71
|
-
class PushMenu {
|
|
72
|
-
|
|
78
|
+
class PushMenu extends BaseComponent {
|
|
79
|
+
static get NAME(): string {
|
|
80
|
+
return NAME
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Look up the PushMenu already attached to the given element.
|
|
85
|
+
*
|
|
86
|
+
* @param element The sidebar element to look up.
|
|
87
|
+
* @returns The existing instance, or null if the sidebar has none yet.
|
|
88
|
+
*/
|
|
89
|
+
static getInstance(element: Element | null | undefined): PushMenu | null {
|
|
90
|
+
return this._getInstance(element) as PushMenu | null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Look up the PushMenu attached to the given element, creating one when the
|
|
95
|
+
* element has none. `config` is ignored if an instance already exists.
|
|
96
|
+
*
|
|
97
|
+
* @param element The sidebar element.
|
|
98
|
+
* @param config Overrides merged over the defaults for a new instance.
|
|
99
|
+
* @returns The existing or newly created instance.
|
|
100
|
+
*/
|
|
101
|
+
static getOrCreateInstance(element: HTMLElement, config: Partial<Config> = {}): PushMenu {
|
|
102
|
+
return this.getInstance(element) ?? new this(element, config)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The defaults merged with the overrides this instance was created with.
|
|
107
|
+
*/
|
|
73
108
|
_config: Config
|
|
74
109
|
|
|
75
|
-
|
|
76
|
-
|
|
110
|
+
/**
|
|
111
|
+
* @param element The sidebar element to attach to.
|
|
112
|
+
* @param config Overrides merged over the defaults.
|
|
113
|
+
*/
|
|
114
|
+
constructor(element: HTMLElement, config: Partial<Config> = {}) {
|
|
115
|
+
super(element)
|
|
77
116
|
this._config = { ...Defaults, ...config }
|
|
78
117
|
}
|
|
79
118
|
|
|
@@ -118,6 +157,12 @@ class PushMenu {
|
|
|
118
157
|
* Expand the sidebar menu.
|
|
119
158
|
*/
|
|
120
159
|
expand(): void {
|
|
160
|
+
// The "open" event is cancelable: preventDefault() keeps the sidebar
|
|
161
|
+
// in its current state.
|
|
162
|
+
if (dispatchCustomEvent(this._element, EVENT_OPEN, { cancelable: true }).defaultPrevented) {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
121
166
|
// Remove the sidebar-collapse class. Only on mobile, add the sidebar-open
|
|
122
167
|
// class to indicate the sidebar is explicitly open.
|
|
123
168
|
|
|
@@ -127,24 +172,26 @@ class PushMenu {
|
|
|
127
172
|
document.body.classList.add(CLASS_NAME_SIDEBAR_OPEN)
|
|
128
173
|
}
|
|
129
174
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
this._element.dispatchEvent(new Event(EVENT_OPEN))
|
|
175
|
+
dispatchCustomEvent(this._element, EVENT_OPENED)
|
|
133
176
|
}
|
|
134
177
|
|
|
135
178
|
/**
|
|
136
179
|
* Collapse the sidebar menu.
|
|
137
180
|
*/
|
|
138
181
|
collapse(): void {
|
|
182
|
+
// The "collapse" event is cancelable: preventDefault() keeps the sidebar
|
|
183
|
+
// in its current state.
|
|
184
|
+
if (dispatchCustomEvent(this._element, EVENT_COLLAPSE, { cancelable: true }).defaultPrevented) {
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
139
188
|
// Remove the sidebar-open class (if present), and add the sidebar-collapse
|
|
140
189
|
// class.
|
|
141
190
|
|
|
142
191
|
document.body.classList.remove(CLASS_NAME_SIDEBAR_OPEN)
|
|
143
192
|
document.body.classList.add(CLASS_NAME_SIDEBAR_COLLAPSE)
|
|
144
193
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
this._element.dispatchEvent(new Event(EVENT_COLLAPSE))
|
|
194
|
+
dispatchCustomEvent(this._element, EVENT_COLLAPSED)
|
|
148
195
|
}
|
|
149
196
|
|
|
150
197
|
/**
|
|
@@ -317,12 +364,12 @@ class PushMenu {
|
|
|
317
364
|
|
|
318
365
|
// When persistence is enabled and screen size is above the breakpoint, load
|
|
319
366
|
// the saved sidebar state from local storage. Otherwise, use responsive
|
|
320
|
-
// logic to set the initial state
|
|
321
|
-
//
|
|
367
|
+
// logic to set the initial state (unless explicitly set as collapsed at
|
|
368
|
+
// initialization).
|
|
322
369
|
|
|
323
370
|
if (this._config.enablePersistence && !this.isMobileSize()) {
|
|
324
371
|
this.loadSidebarState()
|
|
325
|
-
} else {
|
|
372
|
+
} else if (!this.isCollapsed()) {
|
|
326
373
|
this.updateStateByResponsiveLogic()
|
|
327
374
|
}
|
|
328
375
|
}
|
|
@@ -332,12 +379,40 @@ class PushMenu {
|
|
|
332
379
|
* ----------------------------------------------------------------------------
|
|
333
380
|
* Data Api implementation
|
|
334
381
|
* ----------------------------------------------------------------------------
|
|
382
|
+
* Toggle clicks are handled by one delegated listener on `document`, so
|
|
383
|
+
* toggle buttons added later work and the listener survives Turbo's <body>
|
|
384
|
+
* swaps. The instance itself is created per page load below and can be
|
|
385
|
+
* retrieved anywhere with:
|
|
386
|
+
*
|
|
387
|
+
* PushMenu.getInstance(document.querySelector('.app-sidebar'))
|
|
335
388
|
*/
|
|
336
389
|
|
|
390
|
+
document.addEventListener('click', event => {
|
|
391
|
+
const target = event.target
|
|
392
|
+
|
|
393
|
+
if (!(target instanceof Element)) {
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const button = target.closest(SELECTOR_SIDEBAR_TOGGLE)
|
|
398
|
+
|
|
399
|
+
if (!button) {
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
event.preventDefault()
|
|
404
|
+
|
|
405
|
+
const sidebar = document.querySelector(SELECTOR_APP_SIDEBAR) as HTMLElement | null
|
|
406
|
+
|
|
407
|
+
if (sidebar) {
|
|
408
|
+
PushMenu.getOrCreateInstance(sidebar).toggle()
|
|
409
|
+
}
|
|
410
|
+
})
|
|
411
|
+
|
|
337
412
|
onDOMContentLoaded(() => {
|
|
338
413
|
// Find the sidebar element in the DOM.
|
|
339
414
|
|
|
340
|
-
const sidebar = document
|
|
415
|
+
const sidebar = document.querySelector(SELECTOR_APP_SIDEBAR) as HTMLElement | null
|
|
341
416
|
|
|
342
417
|
if (!sidebar) {
|
|
343
418
|
return
|
|
@@ -358,9 +433,11 @@ onDOMContentLoaded(() => {
|
|
|
358
433
|
enablePersistenceAttr === 'true'
|
|
359
434
|
}
|
|
360
435
|
|
|
361
|
-
// Initialize the PushMenu plugin (a unique instance
|
|
436
|
+
// Initialize the PushMenu plugin (a unique instance per sidebar element;
|
|
437
|
+
// Turbo navigations swap in a fresh element, so a fresh instance is
|
|
438
|
+
// created and the old one is garbage-collected with the old <body>).
|
|
362
439
|
|
|
363
|
-
const pushMenu =
|
|
440
|
+
const pushMenu = PushMenu.getOrCreateInstance(sidebar, config)
|
|
364
441
|
pushMenu.init()
|
|
365
442
|
|
|
366
443
|
// Update the sidebar state only when the viewport crosses the sidebar
|
|
@@ -392,16 +469,23 @@ onDOMContentLoaded(() => {
|
|
|
392
469
|
// Handle touch events on overlay (area outside sidebar), usually we want to
|
|
393
470
|
// close the sidebar when the user taps outside the sidebar on mobile
|
|
394
471
|
// devices.
|
|
472
|
+
//
|
|
473
|
+
// These are bound with the lifecycle signal even though the overlay lives
|
|
474
|
+
// inside <body>: the node above is reused when it already exists, so under a
|
|
475
|
+
// framework that re-initialises against a persistent <body>, an unsignalled
|
|
476
|
+
// binding would stack another set of handlers on the same element per cycle.
|
|
477
|
+
|
|
478
|
+
const overlaySignal = getLifecycleSignal()
|
|
395
479
|
|
|
396
480
|
let overlayTouchMoved = false
|
|
397
481
|
|
|
398
482
|
sidebarOverlay.addEventListener('touchstart', () => {
|
|
399
483
|
overlayTouchMoved = false
|
|
400
|
-
}, { passive: true })
|
|
484
|
+
}, { passive: true, signal: overlaySignal })
|
|
401
485
|
|
|
402
486
|
sidebarOverlay.addEventListener('touchmove', () => {
|
|
403
487
|
overlayTouchMoved = true
|
|
404
|
-
}, { passive: true })
|
|
488
|
+
}, { passive: true, signal: overlaySignal })
|
|
405
489
|
|
|
406
490
|
sidebarOverlay.addEventListener('touchend', event => {
|
|
407
491
|
if (!overlayTouchMoved) {
|
|
@@ -410,33 +494,12 @@ onDOMContentLoaded(() => {
|
|
|
410
494
|
}
|
|
411
495
|
|
|
412
496
|
overlayTouchMoved = false
|
|
413
|
-
}, { passive: false })
|
|
497
|
+
}, { passive: false, signal: overlaySignal })
|
|
414
498
|
|
|
415
499
|
sidebarOverlay.addEventListener('click', event => {
|
|
416
500
|
event.preventDefault()
|
|
417
501
|
pushMenu.collapse()
|
|
418
|
-
})
|
|
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
|
-
})
|
|
502
|
+
}, { signal: overlaySignal })
|
|
440
503
|
})
|
|
441
504
|
|
|
442
505
|
export default PushMenu
|
package/src/ts/treeview.ts
CHANGED
|
@@ -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
|
-
|
|
22
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
package/src/ts/util/index.ts
CHANGED
|
@@ -16,6 +16,18 @@
|
|
|
16
16
|
* cycle's listeners before the callbacks run again. Listeners bound to elements
|
|
17
17
|
* inside <body> don't need the signal — Turbo discards the old <body>, so they
|
|
18
18
|
* are cleaned up automatically.
|
|
19
|
+
*
|
|
20
|
+
* Turbo is not the only environment that renders after `DOMContentLoaded`:
|
|
21
|
+
* client-side frameworks that build the layout themselves (GWT, and other
|
|
22
|
+
* imperative widget toolkits) have an empty <body> when the initial batch runs,
|
|
23
|
+
* so the per-page init pass finds no sidebar and no menu. Those consumers call
|
|
24
|
+
* the exported `initialize()` once the layout is attached — it performs the same
|
|
25
|
+
* reset-then-replay cycle Turbo gets, without faking Turbo events.
|
|
26
|
+
*
|
|
27
|
+
* Unlike Turbo, such frameworks keep the same <body> across a re-init, so
|
|
28
|
+
* element-level listeners are NOT discarded for them. Callbacks should therefore
|
|
29
|
+
* pass `getLifecycleSignal()` to every `addEventListener` they make — including
|
|
30
|
+
* ones on elements — whenever the element can outlive the cycle.
|
|
19
31
|
*/
|
|
20
32
|
|
|
21
33
|
const lifecycleCallbacks: Array<() => void> = []
|
|
@@ -24,7 +36,10 @@ const lifecycleCallbacks: Array<() => void> = []
|
|
|
24
36
|
// without reassigning top-level bindings.
|
|
25
37
|
const lifecycleState = {
|
|
26
38
|
controller: new AbortController(),
|
|
27
|
-
hasInitialized: false
|
|
39
|
+
hasInitialized: false,
|
|
40
|
+
// True while the callback batch is executing — lets initialize() refuse
|
|
41
|
+
// re-entrant calls made from inside a lifecycle callback.
|
|
42
|
+
isReplaying: false
|
|
28
43
|
}
|
|
29
44
|
|
|
30
45
|
/**
|
|
@@ -40,9 +55,14 @@ const runLifecycleCallbacks = (): void => {
|
|
|
40
55
|
}
|
|
41
56
|
|
|
42
57
|
lifecycleState.hasInitialized = true
|
|
58
|
+
lifecycleState.isReplaying = true
|
|
43
59
|
|
|
44
|
-
|
|
45
|
-
callback
|
|
60
|
+
try {
|
|
61
|
+
for (const callback of lifecycleCallbacks) {
|
|
62
|
+
callback()
|
|
63
|
+
}
|
|
64
|
+
} finally {
|
|
65
|
+
lifecycleState.isReplaying = false
|
|
46
66
|
}
|
|
47
67
|
}
|
|
48
68
|
|
|
@@ -58,24 +78,66 @@ const onDOMContentLoaded = (callback: () => void): void => {
|
|
|
58
78
|
}
|
|
59
79
|
}
|
|
60
80
|
|
|
61
|
-
|
|
81
|
+
/**
|
|
82
|
+
* End the current lifecycle: abort the cycle's signal so listeners registered
|
|
83
|
+
* with it are removed, then arm a fresh cycle for the next replay.
|
|
84
|
+
*
|
|
85
|
+
* Exported for SPA containers that unmount the AdminLTE layout: calling it
|
|
86
|
+
* drops the window/document listeners the current cycle added without
|
|
87
|
+
* immediately re-initialising. Internally it is also the first half of
|
|
88
|
+
* `initialize()` and the `turbo:before-render` handler.
|
|
89
|
+
*/
|
|
90
|
+
const teardown = (): void => {
|
|
91
|
+
lifecycleState.controller.abort()
|
|
92
|
+
lifecycleState.controller = new AbortController()
|
|
93
|
+
lifecycleState.hasInitialized = false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Re-run every plugin's initialisation against the DOM as it stands right now.
|
|
98
|
+
*
|
|
99
|
+
* Intended for frameworks that render the layout after `DOMContentLoaded` has
|
|
100
|
+
* already fired — call it once the sidebar and menu are attached, and PushMenu,
|
|
101
|
+
* Treeview and ColorMode pick them up as if they had been in the initial HTML.
|
|
102
|
+
* Delegated click handling never needs this; only the per-page init pass does.
|
|
103
|
+
*
|
|
104
|
+
* The previous cycle is torn down first, so calling it repeatedly does not stack
|
|
105
|
+
* listeners registered with `getLifecycleSignal()`. Calling it before the
|
|
106
|
+
* initial batch has run (while `document.readyState === 'loading'`) runs that
|
|
107
|
+
* batch early, against whatever DOM exists at the time — the initial
|
|
108
|
+
* `DOMContentLoaded` pass below still replays against the complete DOM.
|
|
109
|
+
*/
|
|
110
|
+
const initialize = (): void => {
|
|
111
|
+
// Re-entrancy guard: a lifecycle callback calling initialize() would tear
|
|
112
|
+
// down its own cycle mid-replay and recurse without end.
|
|
113
|
+
if (lifecycleState.isReplaying) {
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
teardown()
|
|
118
|
+
runLifecycleCallbacks()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Initial page load. Routed through initialize() so that an early initialize()
|
|
122
|
+
// call (a framework initialising before the document finished loading) cannot
|
|
123
|
+
// mark the cycle as done and suppress this pass — the replay tears the early
|
|
124
|
+
// cycle down and re-runs every callback against the complete DOM.
|
|
62
125
|
if (document.readyState === 'loading') {
|
|
63
|
-
document.addEventListener('DOMContentLoaded',
|
|
126
|
+
document.addEventListener('DOMContentLoaded', initialize, { once: true })
|
|
64
127
|
} else {
|
|
65
128
|
runLifecycleCallbacks()
|
|
66
129
|
}
|
|
67
130
|
|
|
68
131
|
// Hotwired Turbo: drop the previous cycle's window/document listeners, then
|
|
69
|
-
// re-run initialisation against the freshly rendered <body>.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
})
|
|
132
|
+
// re-run initialisation against the freshly rendered <body>. The teardown has to
|
|
133
|
+
// happen at `before-render` rather than as part of the replay, so the outgoing
|
|
134
|
+
// <body>'s listeners are gone before Turbo swaps in the new one — which is why
|
|
135
|
+
// this is the two-step form of `initialize()` rather than a call to it.
|
|
136
|
+
document.addEventListener('turbo:before-render', teardown)
|
|
75
137
|
|
|
76
138
|
document.addEventListener('turbo:load', runLifecycleCallbacks)
|
|
77
139
|
|
|
78
|
-
|
|
140
|
+
// ES2022 UTILITY FUNCTIONS
|
|
79
141
|
|
|
80
142
|
/**
|
|
81
143
|
* Check if an element has a specific data attribute using ES2022 Object.hasOwn()
|
|
@@ -109,7 +171,7 @@ const safePropertyAccess = (obj: Record<string, unknown>, property: string): unk
|
|
|
109
171
|
* the previous animation's steps: without this, rapidly toggling a treeview or
|
|
110
172
|
* card leaves a stale cleanup timer that strips height/transition mid-animation
|
|
111
173
|
* and desyncs the element's display state from its component's classes. */
|
|
112
|
-
const slideTimers = new WeakMap<HTMLElement,
|
|
174
|
+
const slideTimers = new WeakMap<HTMLElement, Array<ReturnType<typeof globalThis.setTimeout>>>()
|
|
113
175
|
|
|
114
176
|
const cancelSlide = (target: HTMLElement): void => {
|
|
115
177
|
const timers = slideTimers.get(target) ?? []
|
|
@@ -126,7 +188,7 @@ const clearSlideStyles = (target: HTMLElement): void => {
|
|
|
126
188
|
}
|
|
127
189
|
}
|
|
128
190
|
|
|
129
|
-
|
|
191
|
+
// SLIDE UP
|
|
130
192
|
const slideUp = (target: HTMLElement, duration = 500) => {
|
|
131
193
|
cancelSlide(target)
|
|
132
194
|
|
|
@@ -159,7 +221,7 @@ const slideUp = (target: HTMLElement, duration = 500) => {
|
|
|
159
221
|
slideTimers.set(target, [stepTimer, cleanupTimer])
|
|
160
222
|
}
|
|
161
223
|
|
|
162
|
-
|
|
224
|
+
// SLIDE DOWN
|
|
163
225
|
const slideDown = (target: HTMLElement, duration = 500) => {
|
|
164
226
|
cancelSlide(target)
|
|
165
227
|
// Drop inline styles a cancelled slideUp may have left behind (height: 0,
|
|
@@ -206,7 +268,7 @@ const slideDown = (target: HTMLElement, duration = 500) => {
|
|
|
206
268
|
slideTimers.set(target, [stepTimer, cleanupTimer])
|
|
207
269
|
}
|
|
208
270
|
|
|
209
|
-
|
|
271
|
+
// TOGGLE
|
|
210
272
|
const slideToggle = (target: HTMLElement, duration = 500) => {
|
|
211
273
|
if (globalThis.getComputedStyle(target).display === 'none') {
|
|
212
274
|
slideDown(target, duration)
|
|
@@ -219,6 +281,8 @@ const slideToggle = (target: HTMLElement, duration = 500) => {
|
|
|
219
281
|
export {
|
|
220
282
|
onDOMContentLoaded,
|
|
221
283
|
getLifecycleSignal,
|
|
284
|
+
initialize,
|
|
285
|
+
teardown,
|
|
222
286
|
slideUp,
|
|
223
287
|
slideDown,
|
|
224
288
|
slideToggle,
|