admin-lte 4.0.2 → 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.
- package/ACCESSIBILITY-COMPLIANCE.md +60 -339
- package/CHANGELOG.md +66 -0
- package/README.md +15 -2
- 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 +1176 -2718
- 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 +1176 -2718
- 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 +1365 -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 +606 -310
- 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 +9 -0
- package/dist/js/types/base-component.d.ts +36 -0
- package/dist/js/types/card-widget.d.ts +30 -0
- package/dist/js/types/color-mode.d.ts +56 -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 +102 -0
- package/dist/js/types/treeview.d.ts +27 -0
- package/dist/js/types/util/index.d.ts +42 -0
- package/package.json +60 -21
- 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/_callouts.scss +13 -3
- package/src/scss/_dropdown.scss +3 -1
- 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/scss/parts/_core.scss +0 -1
- package/src/ts/accessibility.ts +129 -69
- package/src/ts/adminlte.ts +3 -1
- package/src/ts/base-component.ts +84 -0
- package/src/ts/card-widget.ts +134 -85
- package/src/ts/color-mode.ts +174 -0
- package/src/ts/direct-chat.ts +38 -32
- package/src/ts/fullscreen.ts +78 -63
- package/src/ts/layout.ts +2 -1
- package/src/ts/push-menu.ts +93 -44
- package/src/ts/treeview.ts +107 -47
- package/src/ts/util/index.ts +114 -29
- package/.browserslistrc +0 -17
- package/.prettierrc +0 -6
- package/CODE_OF_CONDUCT.md +0 -43
- package/SECURITY.md +0 -99
- package/dist/UI/general.html +0 -2317
- package/dist/UI/icons.html +0 -1126
- package/dist/UI/timeline.html +0 -1212
- package/dist/docs/browser-support.html +0 -826
- package/dist/docs/color-mode.html +0 -1142
- package/dist/docs/components/main-header.html +0 -1209
- package/dist/docs/components/main-sidebar.html +0 -924
- package/dist/docs/customization.html +0 -1080
- package/dist/docs/deployment.html +0 -1145
- package/dist/docs/faq.html +0 -1392
- package/dist/docs/getting-started.html +0 -991
- package/dist/docs/how-to-contribute.html +0 -890
- package/dist/docs/index.html +0 -16
- package/dist/docs/integrations.html +0 -1231
- package/dist/docs/introduction.html +0 -1008
- package/dist/docs/javascript/accessibility.html +0 -949
- package/dist/docs/javascript/card-widget.html +0 -1019
- package/dist/docs/javascript/direct-chat.html +0 -896
- package/dist/docs/javascript/fullscreen.html +0 -905
- package/dist/docs/javascript/layout.html +0 -908
- package/dist/docs/javascript/plugins-overview.html +0 -1144
- package/dist/docs/javascript/pushmenu.html +0 -1045
- package/dist/docs/javascript/treeview.html +0 -1024
- package/dist/docs/layout-blueprint.html +0 -1087
- package/dist/docs/layout.html +0 -842
- package/dist/docs/license.html +0 -807
- package/dist/docs/migration.html +0 -1149
- package/dist/docs/recipes.html +0 -1185
- package/dist/docs/rtl.html +0 -957
- package/dist/examples/lockscreen.html +0 -249
- package/dist/examples/login-v2.html +0 -282
- package/dist/examples/login.html +0 -272
- package/dist/examples/register-v2.html +0 -291
- package/dist/examples/register.html +0 -277
- package/dist/forms/elements.html +0 -1405
- package/dist/forms/layout.html +0 -1267
- package/dist/forms/validation.html +0 -1275
- package/dist/forms/wizard.html +0 -1410
- package/dist/generate/theme.html +0 -1082
- package/dist/index.html +0 -1745
- package/dist/index2.html +0 -2277
- package/dist/index3.html +0 -1532
- package/dist/layout/collapsed-sidebar-without-hover.html +0 -1144
- package/dist/layout/collapsed-sidebar.html +0 -1137
- package/dist/layout/fixed-complete.html +0 -1138
- package/dist/layout/fixed-footer.html +0 -1136
- package/dist/layout/fixed-header.html +0 -1136
- package/dist/layout/fixed-sidebar.html +0 -1138
- package/dist/layout/layout-custom-area.html +0 -1177
- package/dist/layout/layout-rtl.html +0 -1137
- package/dist/layout/logo-switch.html +0 -683
- package/dist/layout/sidebar-mini.html +0 -1137
- package/dist/layout/unfixed-sidebar.html +0 -1136
- package/dist/mailbox/compose.html +0 -1151
- package/dist/mailbox/inbox.html +0 -1509
- package/dist/mailbox/read.html +0 -1202
- package/dist/pages/404.html +0 -239
- package/dist/pages/500.html +0 -227
- package/dist/pages/calendar.html +0 -1266
- package/dist/pages/chat.html +0 -1427
- package/dist/pages/faq.html +0 -1383
- package/dist/pages/file-manager.html +0 -1835
- package/dist/pages/invoice.html +0 -1199
- package/dist/pages/kanban.html +0 -1439
- package/dist/pages/maintenance.html +0 -230
- package/dist/pages/pricing.html +0 -1279
- package/dist/pages/profile.html +0 -1382
- package/dist/pages/projects.html +0 -1644
- package/dist/pages/settings.html +0 -1387
- package/dist/tables/data.html +0 -1319
- package/dist/tables/simple.html +0 -1435
- package/dist/widgets/cards.html +0 -1356
- package/dist/widgets/info-box.html +0 -1429
- package/dist/widgets/small-box.html +0 -1225
- package/eslint.config.js +0 -82
- package/github-issue-response.md +0 -40
- package/index.html +0 -12
- package/netlify.toml +0 -16
- package/src/scss/_bootstrap-variables.scss +0 -1766
package/src/ts/fullscreen.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* --------------------------------------------
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { BaseComponent, dispatchCustomEvent } from './base-component'
|
|
9
10
|
import {
|
|
11
|
+
getLifecycleSignal,
|
|
10
12
|
onDOMContentLoaded
|
|
11
13
|
} from './util/index'
|
|
12
14
|
|
|
@@ -14,8 +16,8 @@ import {
|
|
|
14
16
|
* Constants
|
|
15
17
|
* ============================================================================
|
|
16
18
|
*/
|
|
17
|
-
const
|
|
18
|
-
const EVENT_KEY =
|
|
19
|
+
const NAME = 'fullscreen'
|
|
20
|
+
const EVENT_KEY = `.lte.${NAME}`
|
|
19
21
|
const EVENT_MAXIMIZED = `maximized${EVENT_KEY}`
|
|
20
22
|
const EVENT_MINIMIZED = `minimized${EVENT_KEY}`
|
|
21
23
|
|
|
@@ -23,68 +25,75 @@ const SELECTOR_FULLSCREEN_TOGGLE = '[data-lte-toggle="fullscreen"]'
|
|
|
23
25
|
const SELECTOR_MAXIMIZE_ICON = '[data-lte-icon="maximize"]'
|
|
24
26
|
const SELECTOR_MINIMIZE_ICON = '[data-lte-icon="minimize"]'
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Keep the icons and custom events in sync with the browser's actual
|
|
30
|
+
* fullscreen state. Driving this from the `fullscreenchange` event (rather
|
|
31
|
+
* than from the request/exit calls) means the UI stays correct no matter how
|
|
32
|
+
* the transition happened — including the cases the imperative approach could
|
|
33
|
+
* never catch: a request that was denied (permissions policy, missing
|
|
34
|
+
* `allowfullscreen` on an iframe, lost user gesture) and an exit triggered by
|
|
35
|
+
* the user pressing ESC or F11.
|
|
36
|
+
*/
|
|
37
|
+
function syncFullScreenState(): void {
|
|
38
|
+
const iconMaximize = document.querySelector<HTMLElement>(SELECTOR_MAXIMIZE_ICON)
|
|
39
|
+
const iconMinimize = document.querySelector<HTMLElement>(SELECTOR_MINIMIZE_ICON)
|
|
40
|
+
const isFullScreen = Boolean(document.fullscreenElement)
|
|
41
|
+
|
|
42
|
+
// Toggle Bootstrap's .d-none utility instead of hardcoding inline
|
|
43
|
+
// display:block. The previous approach overrode the icon library's
|
|
44
|
+
// natural display value (eg. some icon fonts use inline-block) and
|
|
45
|
+
// caused the icon to shift its position. Fixes #6021.
|
|
46
|
+
iconMaximize?.classList.toggle('d-none', isFullScreen)
|
|
47
|
+
iconMinimize?.classList.toggle('d-none', !isFullScreen)
|
|
48
|
+
|
|
49
|
+
const eventName = isFullScreen ? EVENT_MAXIMIZED : EVENT_MINIMIZED
|
|
50
|
+
|
|
51
|
+
document.querySelectorAll(SELECTOR_FULLSCREEN_TOGGLE).forEach(button => {
|
|
52
|
+
dispatchCustomEvent(button, eventName)
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
26
56
|
/**
|
|
27
57
|
* Class Definition.
|
|
28
58
|
* ============================================================================
|
|
29
59
|
*/
|
|
30
|
-
class FullScreen {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
constructor(element: HTMLElement, config?: undefined) {
|
|
35
|
-
this._element = element
|
|
36
|
-
this._config = config
|
|
60
|
+
class FullScreen extends BaseComponent {
|
|
61
|
+
static get NAME(): string {
|
|
62
|
+
return NAME
|
|
37
63
|
}
|
|
38
64
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const iconMaximize = document.querySelector<HTMLElement>(SELECTOR_MAXIMIZE_ICON)
|
|
43
|
-
const iconMinimize = document.querySelector<HTMLElement>(SELECTOR_MINIMIZE_ICON)
|
|
44
|
-
|
|
45
|
-
void document.documentElement.requestFullscreen()
|
|
46
|
-
|
|
47
|
-
// Toggle Bootstrap's .d-none utility instead of hardcoding inline
|
|
48
|
-
// display:block. The previous approach overrode the icon library's
|
|
49
|
-
// natural display value (eg. some icon fonts use inline-block) and
|
|
50
|
-
// caused the icon to shift its position. Fixes #6021.
|
|
51
|
-
if (iconMaximize) {
|
|
52
|
-
iconMaximize.classList.add('d-none')
|
|
53
|
-
}
|
|
65
|
+
static getInstance(element: Element | null | undefined): FullScreen | null {
|
|
66
|
+
return this._getInstance(element) as FullScreen | null
|
|
67
|
+
}
|
|
54
68
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
69
|
+
static getOrCreateInstance(element: HTMLElement): FullScreen {
|
|
70
|
+
return this.getInstance(element) ?? new this(element)
|
|
71
|
+
}
|
|
58
72
|
|
|
59
|
-
|
|
73
|
+
inFullScreen(): void {
|
|
74
|
+
// Fire-and-forget: the resulting `fullscreenchange` event updates the
|
|
75
|
+
// icons and dispatches `maximized.lte.fullscreen`. If the request is
|
|
76
|
+
// denied the event never fires, so the UI correctly stays untouched.
|
|
77
|
+
void document.documentElement.requestFullscreen().catch(() => {
|
|
78
|
+
// Request denied — nothing to undo.
|
|
79
|
+
})
|
|
60
80
|
}
|
|
61
81
|
|
|
62
82
|
outFullscreen(): void {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const iconMinimize = document.querySelector<HTMLElement>(SELECTOR_MINIMIZE_ICON)
|
|
67
|
-
|
|
68
|
-
void document.exitFullscreen()
|
|
69
|
-
|
|
70
|
-
if (iconMaximize) {
|
|
71
|
-
iconMaximize.classList.remove('d-none')
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (iconMinimize) {
|
|
75
|
-
iconMinimize.classList.add('d-none')
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
this._element.dispatchEvent(event)
|
|
83
|
+
void document.exitFullscreen().catch(() => {
|
|
84
|
+
// Exit failed — nothing to undo.
|
|
85
|
+
})
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
toggleFullScreen(): void {
|
|
82
|
-
if (document.fullscreenEnabled) {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
if (!document.fullscreenEnabled) {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (document.fullscreenElement) {
|
|
94
|
+
this.outFullscreen()
|
|
95
|
+
} else {
|
|
96
|
+
this.inFullScreen()
|
|
88
97
|
}
|
|
89
98
|
}
|
|
90
99
|
}
|
|
@@ -92,23 +101,29 @@ class FullScreen {
|
|
|
92
101
|
/**
|
|
93
102
|
* Data Api implementation
|
|
94
103
|
* ============================================================================
|
|
104
|
+
* Toggle clicks are delegated on `document`; the fullscreenchange listener is
|
|
105
|
+
* re-registered per page load with the lifecycle signal.
|
|
95
106
|
*/
|
|
96
|
-
onDOMContentLoaded(() => {
|
|
97
|
-
const buttons = document.querySelectorAll(SELECTOR_FULLSCREEN_TOGGLE)
|
|
98
107
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
event.preventDefault()
|
|
108
|
+
document.addEventListener('click', event => {
|
|
109
|
+
const target = event.target
|
|
102
110
|
|
|
103
|
-
|
|
104
|
-
|
|
111
|
+
if (!(target instanceof Element)) {
|
|
112
|
+
return
|
|
113
|
+
}
|
|
105
114
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
const button = target.closest(SELECTOR_FULLSCREEN_TOGGLE) as HTMLElement | null
|
|
116
|
+
|
|
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() })
|
|
112
127
|
})
|
|
113
128
|
|
|
114
129
|
export default FullScreen
|
package/src/ts/layout.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
|
+
getLifecycleSignal,
|
|
10
11
|
onDOMContentLoaded
|
|
11
12
|
} from './util/index'
|
|
12
13
|
|
|
@@ -62,7 +63,7 @@ class Layout {
|
|
|
62
63
|
|
|
63
64
|
onDOMContentLoaded(() => {
|
|
64
65
|
const layout = new Layout(document.body)
|
|
65
|
-
window.addEventListener('resize', () => layout.holdTransition(200))
|
|
66
|
+
window.addEventListener('resize', () => layout.holdTransition(200), { signal: getLifecycleSignal() })
|
|
66
67
|
|
|
67
68
|
setTimeout(() => {
|
|
68
69
|
document.body.classList.add(CLASS_NAME_APP_LOADED)
|
package/src/ts/push-menu.ts
CHANGED
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
* ----------------------------------------------------------------------------
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { BaseComponent, dispatchCustomEvent } from './base-component'
|
|
9
10
|
import {
|
|
11
|
+
getLifecycleSignal,
|
|
10
12
|
onDOMContentLoaded
|
|
11
13
|
} from './util/index'
|
|
12
14
|
|
|
@@ -16,11 +18,17 @@ import {
|
|
|
16
18
|
* ----------------------------------------------------------------------------
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
|
-
const
|
|
20
|
-
const EVENT_KEY =
|
|
21
|
+
const NAME = 'push-menu'
|
|
22
|
+
const EVENT_KEY = `.lte.${NAME}`
|
|
23
|
+
|
|
24
|
+
// Cancelable "before" events: preventDefault() aborts the state change.
|
|
21
25
|
const EVENT_OPEN = `open${EVENT_KEY}`
|
|
22
26
|
const EVENT_COLLAPSE = `collapse${EVENT_KEY}`
|
|
23
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
|
+
|
|
24
32
|
const CLASS_NAME_SIDEBAR_MINI = 'sidebar-mini'
|
|
25
33
|
const CLASS_NAME_SIDEBAR_EXPAND = 'sidebar-expand'
|
|
26
34
|
const CLASS_NAME_SIDEBAR_OVERLAY = 'sidebar-overlay'
|
|
@@ -55,7 +63,9 @@ type Config = {
|
|
|
55
63
|
}
|
|
56
64
|
|
|
57
65
|
const Defaults: Config = {
|
|
58
|
-
|
|
66
|
+
// Matches the CSS convention (breakpoint-max = breakpoint - .02) so that a
|
|
67
|
+
// window of exactly 992px is classified as desktop by both JS and CSS.
|
|
68
|
+
sidebarBreakpoint: 991.98,
|
|
59
69
|
enablePersistence: false
|
|
60
70
|
}
|
|
61
71
|
|
|
@@ -65,12 +75,23 @@ const Defaults: Config = {
|
|
|
65
75
|
* ----------------------------------------------------------------------------
|
|
66
76
|
*/
|
|
67
77
|
|
|
68
|
-
class PushMenu {
|
|
69
|
-
|
|
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
|
+
|
|
70
91
|
_config: Config
|
|
71
92
|
|
|
72
|
-
constructor(element: HTMLElement, config: Config) {
|
|
73
|
-
|
|
93
|
+
constructor(element: HTMLElement, config: Partial<Config> = {}) {
|
|
94
|
+
super(element)
|
|
74
95
|
this._config = { ...Defaults, ...config }
|
|
75
96
|
}
|
|
76
97
|
|
|
@@ -115,6 +136,12 @@ class PushMenu {
|
|
|
115
136
|
* Expand the sidebar menu.
|
|
116
137
|
*/
|
|
117
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
|
+
|
|
118
145
|
// Remove the sidebar-collapse class. Only on mobile, add the sidebar-open
|
|
119
146
|
// class to indicate the sidebar is explicitly open.
|
|
120
147
|
|
|
@@ -124,24 +151,24 @@ class PushMenu {
|
|
|
124
151
|
document.body.classList.add(CLASS_NAME_SIDEBAR_OPEN)
|
|
125
152
|
}
|
|
126
153
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
this._element.dispatchEvent(new Event(EVENT_OPEN))
|
|
154
|
+
dispatchCustomEvent(this._element, EVENT_OPENED)
|
|
130
155
|
}
|
|
131
156
|
|
|
132
157
|
/**
|
|
133
158
|
* Collapse the sidebar menu.
|
|
134
159
|
*/
|
|
135
160
|
collapse(): void {
|
|
161
|
+
if (dispatchCustomEvent(this._element, EVENT_COLLAPSE, { cancelable: true }).defaultPrevented) {
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
136
165
|
// Remove the sidebar-open class (if present), and add the sidebar-collapse
|
|
137
166
|
// class.
|
|
138
167
|
|
|
139
168
|
document.body.classList.remove(CLASS_NAME_SIDEBAR_OPEN)
|
|
140
169
|
document.body.classList.add(CLASS_NAME_SIDEBAR_COLLAPSE)
|
|
141
170
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
this._element.dispatchEvent(new Event(EVENT_COLLAPSE))
|
|
171
|
+
dispatchCustomEvent(this._element, EVENT_COLLAPSED)
|
|
145
172
|
}
|
|
146
173
|
|
|
147
174
|
/**
|
|
@@ -329,12 +356,40 @@ class PushMenu {
|
|
|
329
356
|
* ----------------------------------------------------------------------------
|
|
330
357
|
* Data Api implementation
|
|
331
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'))
|
|
332
365
|
*/
|
|
333
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
|
+
|
|
334
389
|
onDOMContentLoaded(() => {
|
|
335
390
|
// Find the sidebar element in the DOM.
|
|
336
391
|
|
|
337
|
-
const sidebar = document
|
|
392
|
+
const sidebar = document.querySelector(SELECTOR_APP_SIDEBAR) as HTMLElement | null
|
|
338
393
|
|
|
339
394
|
if (!sidebar) {
|
|
340
395
|
return
|
|
@@ -355,23 +410,38 @@ onDOMContentLoaded(() => {
|
|
|
355
410
|
enablePersistenceAttr === 'true'
|
|
356
411
|
}
|
|
357
412
|
|
|
358
|
-
// 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>).
|
|
359
416
|
|
|
360
|
-
const pushMenu =
|
|
417
|
+
const pushMenu = PushMenu.getOrCreateInstance(sidebar, config)
|
|
361
418
|
pushMenu.init()
|
|
362
419
|
|
|
363
|
-
// Update the sidebar state
|
|
420
|
+
// Update the sidebar state only when the viewport crosses the sidebar
|
|
421
|
+
// breakpoint. matchMedia fires exactly on crossings, so height-only resizes
|
|
422
|
+
// (mobile URL bar / keyboard show-hide) and same-side width changes never
|
|
423
|
+
// disturb a sidebar state the user chose. init() has already read the
|
|
424
|
+
// effective breakpoint from the CSS, so the query uses the final value.
|
|
425
|
+
|
|
426
|
+
const breakpointQuery = globalThis.matchMedia(`(max-width: ${pushMenu._config.sidebarBreakpoint}px)`)
|
|
364
427
|
|
|
365
|
-
|
|
366
|
-
pushMenu.setupSidebarBreakPoint()
|
|
428
|
+
breakpointQuery.addEventListener('change', () => {
|
|
367
429
|
pushMenu.updateStateByResponsiveLogic()
|
|
368
|
-
})
|
|
430
|
+
}, { signal: getLifecycleSignal() })
|
|
369
431
|
|
|
370
432
|
// Create the sidebar overlay element and append it to the app wrapper.
|
|
433
|
+
// Reuse an existing overlay if present: Turbo restores cached <body>
|
|
434
|
+
// snapshots that already contain the injected node (listeners are gone
|
|
435
|
+
// after the restore — the snapshot is a clone — so they are rebound below).
|
|
371
436
|
|
|
372
|
-
const
|
|
373
|
-
sidebarOverlay
|
|
374
|
-
|
|
437
|
+
const appWrapper = document.querySelector(SELECTOR_APP_WRAPPER)
|
|
438
|
+
let sidebarOverlay = appWrapper?.querySelector(`:scope > .${CLASS_NAME_SIDEBAR_OVERLAY}`) as HTMLElement | null
|
|
439
|
+
|
|
440
|
+
if (!sidebarOverlay) {
|
|
441
|
+
sidebarOverlay = document.createElement('div')
|
|
442
|
+
sidebarOverlay.className = CLASS_NAME_SIDEBAR_OVERLAY
|
|
443
|
+
appWrapper?.append(sidebarOverlay)
|
|
444
|
+
}
|
|
375
445
|
|
|
376
446
|
// Handle touch events on overlay (area outside sidebar), usually we want to
|
|
377
447
|
// close the sidebar when the user taps outside the sidebar on mobile
|
|
@@ -400,27 +470,6 @@ onDOMContentLoaded(() => {
|
|
|
400
470
|
event.preventDefault()
|
|
401
471
|
pushMenu.collapse()
|
|
402
472
|
})
|
|
403
|
-
|
|
404
|
-
// Handle click events on sidebar toggle buttons.
|
|
405
|
-
|
|
406
|
-
const fullBtn = document.querySelectorAll(SELECTOR_SIDEBAR_TOGGLE)
|
|
407
|
-
|
|
408
|
-
fullBtn.forEach(btn => {
|
|
409
|
-
btn.addEventListener('click', event => {
|
|
410
|
-
event.preventDefault()
|
|
411
|
-
|
|
412
|
-
let button = event.currentTarget as HTMLElement | undefined
|
|
413
|
-
|
|
414
|
-
if (button?.dataset.lteToggle !== 'sidebar') {
|
|
415
|
-
button = button?.closest(SELECTOR_SIDEBAR_TOGGLE) as HTMLElement | undefined
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (button) {
|
|
419
|
-
event?.preventDefault()
|
|
420
|
-
pushMenu.toggle()
|
|
421
|
-
}
|
|
422
|
-
})
|
|
423
|
-
})
|
|
424
473
|
})
|
|
425
474
|
|
|
426
475
|
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}`
|
|
@@ -42,29 +47,56 @@ type Config = {
|
|
|
42
47
|
accordion: boolean;
|
|
43
48
|
}
|
|
44
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Reflect a submenu's open state on its toggle link so screen readers can
|
|
52
|
+
* announce the treeview as expandable/expanded (WCAG 4.1.2).
|
|
53
|
+
*/
|
|
54
|
+
const setAriaExpanded = (navItem: Element, expanded: boolean): void => {
|
|
55
|
+
const link = navItem.querySelector(`:scope > ${SELECTOR_NAV_LINK}`)
|
|
56
|
+
link?.setAttribute('aria-expanded', String(expanded))
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
/**
|
|
46
60
|
* Class Definition
|
|
47
61
|
* ====================================================
|
|
48
62
|
*/
|
|
49
63
|
|
|
50
|
-
class Treeview {
|
|
51
|
-
|
|
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
|
+
|
|
52
77
|
_config: Config
|
|
53
78
|
|
|
54
|
-
constructor(element: HTMLElement, config: Config) {
|
|
55
|
-
|
|
79
|
+
constructor(element: HTMLElement, config: Partial<Config> = {}) {
|
|
80
|
+
super(element)
|
|
56
81
|
this._config = { ...Default, ...config }
|
|
57
82
|
}
|
|
58
83
|
|
|
59
84
|
open(): void {
|
|
60
|
-
|
|
85
|
+
if (dispatchCustomEvent(this._element, EVENT_EXPAND, { cancelable: true }).defaultPrevented) {
|
|
86
|
+
return
|
|
87
|
+
}
|
|
61
88
|
|
|
62
89
|
if (this._config.accordion) {
|
|
63
90
|
const openMenuList = this._element.parentElement?.querySelectorAll(`${SELECTOR_NAV_ITEM}.${CLASS_NAME_MENU_OPEN}`)
|
|
64
91
|
|
|
65
92
|
openMenuList?.forEach(openMenu => {
|
|
66
|
-
|
|
93
|
+
// Skip the item being opened and anything nested inside it. The old
|
|
94
|
+
// guard compared against the parent <ul> — which never matches a
|
|
95
|
+
// nav-item — so calling open() on an already-open item slid its own
|
|
96
|
+
// menu shut.
|
|
97
|
+
if (!this._element.contains(openMenu)) {
|
|
67
98
|
openMenu.classList.remove(CLASS_NAME_MENU_OPEN)
|
|
99
|
+
setAriaExpanded(openMenu, false)
|
|
68
100
|
const childElement = openMenu?.querySelector(SELECTOR_TREEVIEW_MENU) as HTMLElement | undefined
|
|
69
101
|
if (childElement) {
|
|
70
102
|
slideUp(childElement, this._config.animationSpeed)
|
|
@@ -74,26 +106,38 @@ class Treeview {
|
|
|
74
106
|
}
|
|
75
107
|
|
|
76
108
|
this._element.classList.add(CLASS_NAME_MENU_OPEN)
|
|
109
|
+
setAriaExpanded(this._element, true)
|
|
77
110
|
|
|
78
|
-
const childElement = this._element
|
|
111
|
+
const childElement = this._element.querySelector(SELECTOR_TREEVIEW_MENU) as HTMLElement | undefined
|
|
79
112
|
if (childElement) {
|
|
80
113
|
slideDown(childElement, this._config.animationSpeed)
|
|
81
114
|
}
|
|
82
115
|
|
|
83
|
-
|
|
116
|
+
setTimeout(() => {
|
|
117
|
+
if (this._element.classList.contains(CLASS_NAME_MENU_OPEN)) {
|
|
118
|
+
dispatchCustomEvent(this._element, EVENT_EXPANDED)
|
|
119
|
+
}
|
|
120
|
+
}, this._config.animationSpeed)
|
|
84
121
|
}
|
|
85
122
|
|
|
86
123
|
close(): void {
|
|
87
|
-
|
|
124
|
+
if (dispatchCustomEvent(this._element, EVENT_COLLAPSE, { cancelable: true }).defaultPrevented) {
|
|
125
|
+
return
|
|
126
|
+
}
|
|
88
127
|
|
|
89
128
|
this._element.classList.remove(CLASS_NAME_MENU_OPEN)
|
|
129
|
+
setAriaExpanded(this._element, false)
|
|
90
130
|
|
|
91
|
-
const childElement = this._element
|
|
131
|
+
const childElement = this._element.querySelector(SELECTOR_TREEVIEW_MENU) as HTMLElement | undefined
|
|
92
132
|
if (childElement) {
|
|
93
133
|
slideUp(childElement, this._config.animationSpeed)
|
|
94
134
|
}
|
|
95
135
|
|
|
96
|
-
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
if (!this._element.classList.contains(CLASS_NAME_MENU_OPEN)) {
|
|
138
|
+
dispatchCustomEvent(this._element, EVENT_COLLAPSED)
|
|
139
|
+
}
|
|
140
|
+
}, this._config.animationSpeed)
|
|
97
141
|
}
|
|
98
142
|
|
|
99
143
|
toggle(): void {
|
|
@@ -109,8 +153,50 @@ class Treeview {
|
|
|
109
153
|
* ------------------------------------------------------------------------
|
|
110
154
|
* Data Api implementation
|
|
111
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.
|
|
112
160
|
*/
|
|
113
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
|
+
|
|
114
200
|
onDOMContentLoaded(() => {
|
|
115
201
|
const openMenuItems = document.querySelectorAll(`${SELECTOR_NAV_ITEM}.${CLASS_NAME_MENU_OPEN}`)
|
|
116
202
|
|
|
@@ -124,41 +210,15 @@ onDOMContentLoaded(() => {
|
|
|
124
210
|
}
|
|
125
211
|
})
|
|
126
212
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const targetLink = target.closest(SELECTOR_NAV_LINK) as HTMLAnchorElement | undefined
|
|
134
|
-
const targetTreeviewMenu = targetItem?.querySelector(SELECTOR_TREEVIEW_MENU) as HTMLElement | undefined
|
|
135
|
-
const lteToggleElement = event.currentTarget as HTMLElement
|
|
136
|
-
|
|
137
|
-
// Avoid creating Treeview instances on non menu elements
|
|
138
|
-
if (!targetTreeviewMenu) {
|
|
139
|
-
return
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (target?.getAttribute('href') === '#' || targetLink?.getAttribute('href') === '#') {
|
|
143
|
-
event.preventDefault()
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
if (targetItem) {
|
|
147
|
-
// Read data attributes
|
|
148
|
-
const accordionAttr = lteToggleElement.dataset.accordion
|
|
149
|
-
const animationSpeedAttr = lteToggleElement.dataset.animationSpeed
|
|
150
|
-
|
|
151
|
-
// Build config from data attributes, fallback to Default
|
|
152
|
-
const config: Config = {
|
|
153
|
-
accordion: accordionAttr === undefined ? Default.accordion : accordionAttr === 'true',
|
|
154
|
-
animationSpeed: animationSpeedAttr === undefined ? Default.animationSpeed : Number(animationSpeedAttr)
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const data = new Treeview(targetItem, config)
|
|
158
|
-
data.toggle()
|
|
213
|
+
// Stamp the initial ARIA state on every submenu toggle so assistive tech
|
|
214
|
+
// knows the items are expandable before any interaction.
|
|
215
|
+
document.querySelectorAll(SELECTOR_DATA_TOGGLE).forEach(root => {
|
|
216
|
+
root.querySelectorAll(SELECTOR_NAV_ITEM).forEach(item => {
|
|
217
|
+
if (item.querySelector(`:scope > ${SELECTOR_TREEVIEW_MENU}`)) {
|
|
218
|
+
setAriaExpanded(item, item.classList.contains(CLASS_NAME_MENU_OPEN))
|
|
159
219
|
}
|
|
160
220
|
})
|
|
161
221
|
})
|
|
162
222
|
})
|
|
163
223
|
|
|
164
|
-
export default Treeview
|
|
224
|
+
export default Treeview
|