admin-lte 4.1.0 → 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 +18 -1
- package/README.md +6 -6
- package/dist/css/adminlte-docs.css +1 -1
- package/dist/css/adminlte-docs.css.map +1 -1
- package/dist/css/adminlte-docs.min.css +1 -1
- package/dist/css/adminlte-docs.min.css.map +1 -1
- package/dist/css/adminlte-docs.rtl.css +1 -1
- package/dist/css/adminlte-docs.rtl.css.map +1 -1
- package/dist/css/adminlte-docs.rtl.min.css +1 -1
- package/dist/css/adminlte-docs.rtl.min.css.map +1 -1
- package/dist/css/adminlte.css +131 -131
- 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 +131 -131
- 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 +71 -30
- package/dist/js/adminlte.esm.js.map +1 -1
- package/dist/js/adminlte.esm.min.js +2 -2
- package/dist/js/adminlte.esm.min.js.map +1 -1
- package/dist/js/adminlte.js +72 -29
- 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/adminlte.d.ts +2 -1
- package/dist/js/types/base-component.d.ts +24 -0
- package/dist/js/types/color-mode.d.ts +24 -6
- package/dist/js/types/push-menu.d.ts +21 -0
- package/dist/js/types/util/index.d.ts +38 -1
- package/package.json +24 -23
- package/src/scss/adminlte-docs.scss +1 -1
- package/src/scss/adminlte.scss +1 -1
- package/src/ts/accessibility.ts +6 -2
- package/src/ts/adminlte.ts +5 -3
- package/src/ts/base-component.ts +30 -3
- package/src/ts/color-mode.ts +100 -20
- package/src/ts/push-menu.ts +37 -7
- package/src/ts/util/index.ts +79 -15
package/src/ts/adminlte.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { onDOMContentLoaded } from './util/index.js'
|
|
1
|
+
import { initialize, teardown, onDOMContentLoaded } from './util/index.js'
|
|
2
2
|
import Layout from './layout.js'
|
|
3
3
|
import CardWidget from './card-widget.js'
|
|
4
4
|
import Treeview from './treeview.js'
|
|
@@ -9,7 +9,7 @@ import ColorMode from './color-mode.js'
|
|
|
9
9
|
import { initAccessibility } from './accessibility.js'
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* AdminLTE v4.
|
|
12
|
+
* AdminLTE v4.2.0
|
|
13
13
|
* Author: Colorlib
|
|
14
14
|
* Website: AdminLTE.io <https://adminlte.io>
|
|
15
15
|
* License: Open source - MIT <https://opensource.org/licenses/MIT>
|
|
@@ -40,5 +40,7 @@ export {
|
|
|
40
40
|
FullScreen,
|
|
41
41
|
PushMenu,
|
|
42
42
|
ColorMode,
|
|
43
|
-
initAccessibility
|
|
43
|
+
initAccessibility,
|
|
44
|
+
initialize,
|
|
45
|
+
teardown
|
|
44
46
|
}
|
package/src/ts/base-component.ts
CHANGED
|
@@ -9,17 +9,25 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* element -> (data key -> component instance).
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* Component registry: element -> (data key -> component instance). A single
|
|
13
|
+
* element can host several components at once, each stored under its own
|
|
14
|
+
* DATA_KEY. WeakMap keys don't prevent garbage collection, so instances die
|
|
15
|
+
* with their elements — important under Hotwired Turbo, which swaps the whole
|
|
16
|
+
* <body> on navigation.
|
|
15
17
|
*/
|
|
16
18
|
const componentRegistry = new WeakMap<Element, Map<string, BaseComponent>>()
|
|
17
19
|
|
|
18
20
|
class BaseComponent {
|
|
21
|
+
/**
|
|
22
|
+
* Subclasses must override this getter to declare their own name.
|
|
23
|
+
*/
|
|
19
24
|
static get NAME(): string {
|
|
20
25
|
throw new Error('Component subclasses must override the static NAME getter.')
|
|
21
26
|
}
|
|
22
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Key this component is registered under: `lte.<name>`.
|
|
30
|
+
*/
|
|
23
31
|
static get DATA_KEY(): string {
|
|
24
32
|
return `lte.${this.NAME}`
|
|
25
33
|
}
|
|
@@ -27,6 +35,9 @@ class BaseComponent {
|
|
|
27
35
|
/**
|
|
28
36
|
* Untyped registry lookup. Every component exposes a typed wrapper
|
|
29
37
|
* (e.g. CardWidget.getInstance()) built on top of this.
|
|
38
|
+
*
|
|
39
|
+
* @param element The element to look up.
|
|
40
|
+
* @returns The instance for this component, or null if there is none.
|
|
30
41
|
*/
|
|
31
42
|
protected static _getInstance(element: Element | null | undefined): BaseComponent | null {
|
|
32
43
|
if (!element) {
|
|
@@ -36,8 +47,17 @@ class BaseComponent {
|
|
|
36
47
|
return componentRegistry.get(element)?.get(this.DATA_KEY) ?? null
|
|
37
48
|
}
|
|
38
49
|
|
|
50
|
+
/**
|
|
51
|
+
* The element this instance is attached to.
|
|
52
|
+
*/
|
|
39
53
|
_element: HTMLElement
|
|
40
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Attach a new instance to the given element and register it under the
|
|
57
|
+
* subclass's DATA_KEY.
|
|
58
|
+
*
|
|
59
|
+
* @param element The element to attach this instance to.
|
|
60
|
+
*/
|
|
41
61
|
constructor(element: HTMLElement) {
|
|
42
62
|
this._element = element
|
|
43
63
|
|
|
@@ -55,6 +75,7 @@ class BaseComponent {
|
|
|
55
75
|
const instances = componentRegistry.get(this._element)
|
|
56
76
|
instances?.delete((this.constructor as typeof BaseComponent).DATA_KEY)
|
|
57
77
|
|
|
78
|
+
// Drop the element's entry once it holds no components at all.
|
|
58
79
|
if (instances?.size === 0) {
|
|
59
80
|
componentRegistry.delete(this._element)
|
|
60
81
|
}
|
|
@@ -65,6 +86,12 @@ class BaseComponent {
|
|
|
65
86
|
* Dispatch a namespaced custom event that bubbles — so applications can
|
|
66
87
|
* listen once on `document` — and can optionally carry a payload or be
|
|
67
88
|
* canceled. Returns the event so callers can check `defaultPrevented`.
|
|
89
|
+
*
|
|
90
|
+
* @param element The element to dispatch the event on.
|
|
91
|
+
* @param name The namespaced event name, e.g. `collapse.lte.push-menu`.
|
|
92
|
+
* @param options `cancelable` opts the event into preventDefault(); `detail`
|
|
93
|
+
* is the payload handed to listeners.
|
|
94
|
+
* @returns The dispatched event, after listeners have run.
|
|
68
95
|
*/
|
|
69
96
|
const dispatchCustomEvent = <T = undefined>(
|
|
70
97
|
element: Element,
|
package/src/ts/color-mode.ts
CHANGED
|
@@ -2,13 +2,19 @@
|
|
|
2
2
|
* --------------------------------------------
|
|
3
3
|
* @file AdminLTE color-mode.ts
|
|
4
4
|
* @description Color mode (light/dark/auto) switcher for AdminLTE.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* [data-
|
|
5
|
+
* Resolves the theme from, in order: the visitor's stored choice, the theme
|
|
6
|
+
* the page itself declared in <html data-bs-theme="…">, and finally the OS
|
|
7
|
+
* preference. Keeps [data-bs-theme-value] toggles and [data-lte-theme-icon]
|
|
8
|
+
* indicator icons in sync.
|
|
8
9
|
*
|
|
9
10
|
* Ships in the bundle so applications no longer need to copy the demo's
|
|
10
11
|
* inline script. The tiny no-flash snippet in <head> (see _head.astro)
|
|
11
|
-
* remains inline by design — it must run before first paint.
|
|
12
|
+
* remains inline by design — it must run before first paint. That snippet
|
|
13
|
+
* flags the values it computes itself with [data-lte-theme-resolved], so a
|
|
14
|
+
* theme authored in the markup can be told apart from one it resolved.
|
|
15
|
+
*
|
|
16
|
+
* Applications with their own theming opt out entirely with
|
|
17
|
+
* <html data-lte-color-mode="off">.
|
|
12
18
|
* @license MIT
|
|
13
19
|
* --------------------------------------------
|
|
14
20
|
*/
|
|
@@ -26,11 +32,59 @@ const EVENT_CHANGED = `changed${EVENT_KEY}`
|
|
|
26
32
|
|
|
27
33
|
const STORAGE_KEY = 'lte-theme'
|
|
28
34
|
|
|
29
|
-
const
|
|
35
|
+
const ATTRIBUTE_THEME = 'data-bs-theme'
|
|
36
|
+
const ATTRIBUTE_TOGGLE = 'data-bs-theme-value'
|
|
37
|
+
const ATTRIBUTE_DISABLED = 'data-lte-color-mode'
|
|
38
|
+
const ATTRIBUTE_RESOLVED = 'data-lte-theme-resolved'
|
|
39
|
+
|
|
40
|
+
const SELECTOR_TOGGLE = `[${ATTRIBUTE_TOGGLE}]`
|
|
30
41
|
const SELECTOR_ICON = '[data-lte-theme-icon]'
|
|
31
42
|
|
|
32
43
|
type Theme = 'light' | 'dark' | 'auto'
|
|
33
44
|
|
|
45
|
+
const THEMES = new Set<string>(['light', 'dark', 'auto'])
|
|
46
|
+
|
|
47
|
+
const isValidTheme = (value: string): value is Theme => THEMES.has(value)
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Applications with their own theming take over by adding
|
|
51
|
+
* `data-lte-color-mode="off"` to <html>: ColorMode then never writes
|
|
52
|
+
* `data-bs-theme` — not on load, not on a toggle click, not when the OS
|
|
53
|
+
* preference changes. This is also the escape hatch for custom Bootstrap
|
|
54
|
+
* themes, whose names ColorMode cannot resolve (#6084).
|
|
55
|
+
*
|
|
56
|
+
* Read live rather than captured, so it can be flipped at runtime.
|
|
57
|
+
*/
|
|
58
|
+
const isDisabled = (): boolean =>
|
|
59
|
+
document.documentElement.getAttribute(ATTRIBUTE_DISABLED) === 'off'
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The theme the page itself declared in <html data-bs-theme="…">, or null when
|
|
63
|
+
* it declared none.
|
|
64
|
+
*
|
|
65
|
+
* Captured once, at module evaluation, because the attribute is both an input
|
|
66
|
+
* and an output: after the first `_applyTheme()` it holds ColorMode's own
|
|
67
|
+
* write, which must not be mistaken for the page's intent on a later lifecycle
|
|
68
|
+
* pass (Turbo, `initialize()`). Reading it here runs before any of those.
|
|
69
|
+
*
|
|
70
|
+
* The pre-paint snippet in <head> writes before this module is even fetched,
|
|
71
|
+
* so it marks the values it computed itself with [data-lte-theme-resolved] —
|
|
72
|
+
* those are not authored, and are ignored.
|
|
73
|
+
*/
|
|
74
|
+
const readMarkupTheme = (): Theme | null => {
|
|
75
|
+
const { documentElement } = document
|
|
76
|
+
|
|
77
|
+
if (documentElement.hasAttribute(ATTRIBUTE_RESOLVED)) {
|
|
78
|
+
return null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const declared = documentElement.getAttribute(ATTRIBUTE_THEME)
|
|
82
|
+
|
|
83
|
+
return declared && isValidTheme(declared) ? declared : null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const MARKUP_THEME = readMarkupTheme()
|
|
87
|
+
|
|
34
88
|
/**
|
|
35
89
|
* Class Definition
|
|
36
90
|
* ====================================================
|
|
@@ -44,20 +98,32 @@ class ColorMode {
|
|
|
44
98
|
getStoredTheme(): Theme | null {
|
|
45
99
|
try {
|
|
46
100
|
const stored = localStorage.getItem(STORAGE_KEY)
|
|
47
|
-
return stored &&
|
|
101
|
+
return stored && isValidTheme(stored) ? stored : null
|
|
48
102
|
} catch {
|
|
49
103
|
return null
|
|
50
104
|
}
|
|
51
105
|
}
|
|
52
106
|
|
|
53
107
|
/**
|
|
54
|
-
* The
|
|
55
|
-
*
|
|
108
|
+
* The theme declared in the markup, for applications that render it
|
|
109
|
+
* server-side from a cookie or a user record. Null when the page declared
|
|
110
|
+
* none, or when the value is a custom Bootstrap theme ColorMode cannot
|
|
111
|
+
* resolve — see `isDisabled` for those.
|
|
112
|
+
*/
|
|
113
|
+
getMarkupTheme(): Theme | null {
|
|
114
|
+
return MARKUP_THEME
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The user's effective choice: the stored theme, then the theme declared in
|
|
119
|
+
* the markup, falling back to the OS preference. Storage comes first because
|
|
120
|
+
* it is the visitor's own click on this device; markup is only the default
|
|
121
|
+
* the page shipped with.
|
|
56
122
|
*/
|
|
57
123
|
getPreferredTheme(): Theme {
|
|
58
|
-
const
|
|
59
|
-
if (
|
|
60
|
-
return
|
|
124
|
+
const preferred = this.getStoredTheme() ?? this.getMarkupTheme()
|
|
125
|
+
if (preferred) {
|
|
126
|
+
return preferred
|
|
61
127
|
}
|
|
62
128
|
|
|
63
129
|
return this._prefersDark() ? 'dark' : 'light'
|
|
@@ -99,10 +165,13 @@ class ColorMode {
|
|
|
99
165
|
*/
|
|
100
166
|
_applyTheme(theme: Theme): void {
|
|
101
167
|
const resolved = this.resolveTheme(theme)
|
|
102
|
-
document.documentElement.setAttribute(
|
|
168
|
+
document.documentElement.setAttribute(ATTRIBUTE_THEME, resolved)
|
|
103
169
|
document.documentElement.style.colorScheme = resolved
|
|
104
170
|
}
|
|
105
171
|
|
|
172
|
+
/**
|
|
173
|
+
* Whether the OS preference is currently dark.
|
|
174
|
+
*/
|
|
106
175
|
_prefersDark(): boolean {
|
|
107
176
|
return globalThis.matchMedia('(prefers-color-scheme: dark)').matches
|
|
108
177
|
}
|
|
@@ -113,7 +182,7 @@ class ColorMode {
|
|
|
113
182
|
*/
|
|
114
183
|
_showActiveTheme(theme: Theme): void {
|
|
115
184
|
document.querySelectorAll(SELECTOR_TOGGLE).forEach(toggle => {
|
|
116
|
-
const isActive = toggle.getAttribute(
|
|
185
|
+
const isActive = toggle.getAttribute(ATTRIBUTE_TOGGLE) === theme
|
|
117
186
|
toggle.classList.toggle('active', isActive)
|
|
118
187
|
toggle.setAttribute('aria-pressed', String(isActive))
|
|
119
188
|
toggle.querySelector('.bi-check-lg')?.classList.toggle('d-none', !isActive)
|
|
@@ -128,6 +197,10 @@ class ColorMode {
|
|
|
128
197
|
* Apply the preferred theme and sync the UI without persisting anything.
|
|
129
198
|
*/
|
|
130
199
|
init(): void {
|
|
200
|
+
if (isDisabled()) {
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
131
204
|
const theme = this.getPreferredTheme()
|
|
132
205
|
this._applyTheme(theme)
|
|
133
206
|
this._showActiveTheme(theme)
|
|
@@ -145,14 +218,14 @@ class ColorMode {
|
|
|
145
218
|
document.addEventListener('click', event => {
|
|
146
219
|
const target = event.target
|
|
147
220
|
|
|
148
|
-
if (!(target instanceof Element)) {
|
|
221
|
+
if (!(target instanceof Element) || isDisabled()) {
|
|
149
222
|
return
|
|
150
223
|
}
|
|
151
224
|
|
|
152
225
|
const toggle = target.closest(SELECTOR_TOGGLE)
|
|
153
|
-
const theme = toggle?.getAttribute(
|
|
226
|
+
const theme = toggle?.getAttribute(ATTRIBUTE_TOGGLE)
|
|
154
227
|
|
|
155
|
-
if (theme) {
|
|
228
|
+
if (theme && isValidTheme(theme)) {
|
|
156
229
|
new ColorMode().setTheme(theme)
|
|
157
230
|
}
|
|
158
231
|
})
|
|
@@ -161,12 +234,19 @@ onDOMContentLoaded(() => {
|
|
|
161
234
|
const colorMode = new ColorMode()
|
|
162
235
|
colorMode.init()
|
|
163
236
|
|
|
164
|
-
// Follow the OS while
|
|
237
|
+
// Follow the OS only while the OS *is* the effective choice: nothing stored
|
|
238
|
+
// and nothing declared in the markup, or an explicit "auto". A theme the
|
|
239
|
+
// page declared is a preference too, and outlives an OS change (#6093).
|
|
165
240
|
globalThis.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
|
166
|
-
|
|
167
|
-
|
|
241
|
+
if (isDisabled()) {
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const preferred = colorMode.getStoredTheme() ?? colorMode.getMarkupTheme()
|
|
246
|
+
|
|
247
|
+
if (!preferred || preferred === 'auto') {
|
|
168
248
|
colorMode._applyTheme('auto')
|
|
169
|
-
colorMode._showActiveTheme(
|
|
249
|
+
colorMode._showActiveTheme(preferred ?? 'auto')
|
|
170
250
|
}
|
|
171
251
|
}, { signal: getLifecycleSignal() })
|
|
172
252
|
})
|
package/src/ts/push-menu.ts
CHANGED
|
@@ -80,16 +80,37 @@ class PushMenu extends BaseComponent {
|
|
|
80
80
|
return NAME
|
|
81
81
|
}
|
|
82
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
|
+
*/
|
|
83
89
|
static getInstance(element: Element | null | undefined): PushMenu | null {
|
|
84
90
|
return this._getInstance(element) as PushMenu | null
|
|
85
91
|
}
|
|
86
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
|
+
*/
|
|
87
101
|
static getOrCreateInstance(element: HTMLElement, config: Partial<Config> = {}): PushMenu {
|
|
88
102
|
return this.getInstance(element) ?? new this(element, config)
|
|
89
103
|
}
|
|
90
104
|
|
|
105
|
+
/**
|
|
106
|
+
* The defaults merged with the overrides this instance was created with.
|
|
107
|
+
*/
|
|
91
108
|
_config: Config
|
|
92
109
|
|
|
110
|
+
/**
|
|
111
|
+
* @param element The sidebar element to attach to.
|
|
112
|
+
* @param config Overrides merged over the defaults.
|
|
113
|
+
*/
|
|
93
114
|
constructor(element: HTMLElement, config: Partial<Config> = {}) {
|
|
94
115
|
super(element)
|
|
95
116
|
this._config = { ...Defaults, ...config }
|
|
@@ -158,6 +179,8 @@ class PushMenu extends BaseComponent {
|
|
|
158
179
|
* Collapse the sidebar menu.
|
|
159
180
|
*/
|
|
160
181
|
collapse(): void {
|
|
182
|
+
// The "collapse" event is cancelable: preventDefault() keeps the sidebar
|
|
183
|
+
// in its current state.
|
|
161
184
|
if (dispatchCustomEvent(this._element, EVENT_COLLAPSE, { cancelable: true }).defaultPrevented) {
|
|
162
185
|
return
|
|
163
186
|
}
|
|
@@ -341,12 +364,12 @@ class PushMenu extends BaseComponent {
|
|
|
341
364
|
|
|
342
365
|
// When persistence is enabled and screen size is above the breakpoint, load
|
|
343
366
|
// the saved sidebar state from local storage. Otherwise, use responsive
|
|
344
|
-
// logic to set the initial state
|
|
345
|
-
//
|
|
367
|
+
// logic to set the initial state (unless explicitly set as collapsed at
|
|
368
|
+
// initialization).
|
|
346
369
|
|
|
347
370
|
if (this._config.enablePersistence && !this.isMobileSize()) {
|
|
348
371
|
this.loadSidebarState()
|
|
349
|
-
} else {
|
|
372
|
+
} else if (!this.isCollapsed()) {
|
|
350
373
|
this.updateStateByResponsiveLogic()
|
|
351
374
|
}
|
|
352
375
|
}
|
|
@@ -446,16 +469,23 @@ onDOMContentLoaded(() => {
|
|
|
446
469
|
// Handle touch events on overlay (area outside sidebar), usually we want to
|
|
447
470
|
// close the sidebar when the user taps outside the sidebar on mobile
|
|
448
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()
|
|
449
479
|
|
|
450
480
|
let overlayTouchMoved = false
|
|
451
481
|
|
|
452
482
|
sidebarOverlay.addEventListener('touchstart', () => {
|
|
453
483
|
overlayTouchMoved = false
|
|
454
|
-
}, { passive: true })
|
|
484
|
+
}, { passive: true, signal: overlaySignal })
|
|
455
485
|
|
|
456
486
|
sidebarOverlay.addEventListener('touchmove', () => {
|
|
457
487
|
overlayTouchMoved = true
|
|
458
|
-
}, { passive: true })
|
|
488
|
+
}, { passive: true, signal: overlaySignal })
|
|
459
489
|
|
|
460
490
|
sidebarOverlay.addEventListener('touchend', event => {
|
|
461
491
|
if (!overlayTouchMoved) {
|
|
@@ -464,12 +494,12 @@ onDOMContentLoaded(() => {
|
|
|
464
494
|
}
|
|
465
495
|
|
|
466
496
|
overlayTouchMoved = false
|
|
467
|
-
}, { passive: false })
|
|
497
|
+
}, { passive: false, signal: overlaySignal })
|
|
468
498
|
|
|
469
499
|
sidebarOverlay.addEventListener('click', event => {
|
|
470
500
|
event.preventDefault()
|
|
471
501
|
pushMenu.collapse()
|
|
472
|
-
})
|
|
502
|
+
}, { signal: overlaySignal })
|
|
473
503
|
})
|
|
474
504
|
|
|
475
505
|
export default PushMenu
|
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()
|
|
@@ -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,
|