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
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* AdminLTE v4.0.4 — documentation styles
|
|
3
|
+
* Styles for the documentation and FAQ pages only (body.docs-page /
|
|
4
|
+
* body.faq-page). Applications never need this file; it is loaded by the
|
|
5
|
+
* docs pages in addition to adminlte.css.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Bootstrap Configuration
|
|
9
|
+
// ---------------------------------------------------
|
|
10
|
+
@import "bootstrap/scss/functions";
|
|
11
|
+
@import "bootstrap-overrides";
|
|
12
|
+
@import "bootstrap/scss/variables";
|
|
13
|
+
@import "bootstrap/scss/variables-dark";
|
|
14
|
+
@import "bootstrap/scss/maps";
|
|
15
|
+
@import "bootstrap/scss/mixins";
|
|
16
|
+
|
|
17
|
+
// AdminLTE Configuration
|
|
18
|
+
// ---------------------------------------------------
|
|
19
|
+
@import "variables";
|
|
20
|
+
@import "variables-dark";
|
|
21
|
+
@import "mixins";
|
|
22
|
+
|
|
23
|
+
// Documentation styles
|
|
24
|
+
// ---------------------------------------------------
|
|
25
|
+
@import "docs";
|
package/src/scss/adminlte.scss
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
* AdminLTE v4.
|
|
2
|
+
* AdminLTE v4.1.0
|
|
3
3
|
* Author: Colorlib
|
|
4
4
|
* Website: AdminLTE.io <https://adminlte.io>
|
|
5
5
|
* License: Open source - MIT <https://opensource.org/licenses/MIT>
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
// AdminLTE Configuration
|
|
13
13
|
// ---------------------------------------------------
|
|
14
|
-
@import "bootstrap-
|
|
14
|
+
@import "bootstrap-overrides"; // AdminLTE's ~10 changes to Bootstrap defaults
|
|
15
15
|
|
|
16
16
|
// Bootstrap Configuration
|
|
17
17
|
// ---------------------------------------------------
|
|
@@ -77,5 +77,6 @@
|
|
|
77
77
|
// AdminLTE Accessibility Styles - WCAG 2.1 AA Compliance
|
|
78
78
|
@import "accessibility";
|
|
79
79
|
|
|
80
|
-
//
|
|
81
|
-
|
|
80
|
+
// Documentation styles live in their own bundle (adminlte-docs.scss) so
|
|
81
|
+
// applications don't ship them — the docs pages load it in addition to
|
|
82
|
+
// this file.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@use "sass:map";
|
|
2
|
+
|
|
1
3
|
//
|
|
2
4
|
// Pages: Login & Register
|
|
3
5
|
//
|
|
@@ -28,7 +30,7 @@
|
|
|
28
30
|
.register-box {
|
|
29
31
|
width: 400px;
|
|
30
32
|
|
|
31
|
-
@media (max-width: map
|
|
33
|
+
@media (max-width: map.get($grid-breakpoints, sm)) {
|
|
32
34
|
width: 90%;
|
|
33
35
|
margin-top: .5rem;
|
|
34
36
|
}
|
package/src/ts/accessibility.ts
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* WCAG 2.1 AA Compliance Features
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { getLifecycleSignal } from './util/index'
|
|
7
|
+
|
|
6
8
|
export interface AccessibilityConfig {
|
|
7
9
|
announcements: boolean
|
|
8
10
|
skipLinks: boolean
|
|
@@ -15,6 +17,9 @@ export class AccessibilityManager {
|
|
|
15
17
|
private config: AccessibilityConfig
|
|
16
18
|
private liveRegion: HTMLElement | null = null
|
|
17
19
|
private focusHistory: HTMLElement[] = []
|
|
20
|
+
// Listeners bound to `document` survive Turbo's <body> swap, so they are
|
|
21
|
+
// registered with this signal and torn down before the next re-init.
|
|
22
|
+
private readonly signal: AbortSignal = getLifecycleSignal()
|
|
18
23
|
|
|
19
24
|
constructor(config: Partial<AccessibilityConfig> = {}) {
|
|
20
25
|
this.config = {
|
|
@@ -59,18 +64,34 @@ export class AccessibilityManager {
|
|
|
59
64
|
private createLiveRegion(): void {
|
|
60
65
|
if (this.liveRegion) return
|
|
61
66
|
|
|
67
|
+
// Adopt an existing region instead of appending a duplicate: Turbo caches
|
|
68
|
+
// the <body> including JS-injected nodes, so a region created before a
|
|
69
|
+
// navigation is restored along with the snapshot.
|
|
70
|
+
const existingRegion = document.getElementById('live-region')
|
|
71
|
+
if (existingRegion) {
|
|
72
|
+
this.liveRegion = existingRegion
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
62
76
|
this.liveRegion = document.createElement('div')
|
|
63
77
|
this.liveRegion.id = 'live-region'
|
|
64
78
|
this.liveRegion.className = 'live-region'
|
|
65
79
|
this.liveRegion.setAttribute('aria-live', 'polite')
|
|
66
80
|
this.liveRegion.setAttribute('aria-atomic', 'true')
|
|
67
81
|
this.liveRegion.setAttribute('role', 'status')
|
|
68
|
-
|
|
82
|
+
|
|
69
83
|
document.body.append(this.liveRegion)
|
|
70
84
|
}
|
|
71
85
|
|
|
72
86
|
// WCAG 2.4.1: Bypass Blocks
|
|
73
87
|
private addSkipLinks(): void {
|
|
88
|
+
// Same Turbo snapshot concern as createLiveRegion: skip links restored
|
|
89
|
+
// with a cached <body> must not be injected a second time.
|
|
90
|
+
if (document.querySelector('.skip-links')) {
|
|
91
|
+
this.ensureSkipTargets()
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
74
95
|
const skipLinksContainer = document.createElement('div')
|
|
75
96
|
skipLinksContainer.className = 'skip-links'
|
|
76
97
|
|
|
@@ -113,51 +134,22 @@ export class AccessibilityManager {
|
|
|
113
134
|
|
|
114
135
|
// WCAG 2.4.3: Focus Order & 2.4.7: Focus Visible
|
|
115
136
|
private initFocusManagement(): void {
|
|
137
|
+
// Note: focus must never be wrapped at the document edges — trapping Tab
|
|
138
|
+
// inside the page prevents keyboard users from reaching browser chrome
|
|
139
|
+
// and is itself a WCAG 2.1.2 (No Keyboard Trap) failure. Focus trapping
|
|
140
|
+
// is only legitimate inside active modal dialogs, which Bootstrap already
|
|
141
|
+
// handles.
|
|
116
142
|
document.addEventListener('keydown', (event) => {
|
|
117
|
-
if (event.key === 'Tab') {
|
|
118
|
-
this.handleTabNavigation(event)
|
|
119
|
-
}
|
|
120
143
|
if (event.key === 'Escape') {
|
|
121
144
|
this.handleEscapeKey(event)
|
|
122
145
|
}
|
|
123
|
-
})
|
|
146
|
+
}, { signal: this.signal })
|
|
124
147
|
|
|
125
148
|
// Focus management for modals and dropdowns
|
|
126
149
|
this.initModalFocusManagement()
|
|
127
150
|
this.initDropdownFocusManagement()
|
|
128
151
|
}
|
|
129
152
|
|
|
130
|
-
private handleTabNavigation(event: KeyboardEvent): void {
|
|
131
|
-
const focusableElements = this.getFocusableElements()
|
|
132
|
-
const currentIndex = focusableElements.indexOf(document.activeElement as HTMLElement)
|
|
133
|
-
|
|
134
|
-
if (event.shiftKey) {
|
|
135
|
-
// Shift+Tab (backward)
|
|
136
|
-
if (currentIndex <= 0) {
|
|
137
|
-
event.preventDefault()
|
|
138
|
-
focusableElements.at(-1)?.focus()
|
|
139
|
-
}
|
|
140
|
-
} else if (currentIndex >= focusableElements.length - 1) {
|
|
141
|
-
// Tab (forward)
|
|
142
|
-
event.preventDefault()
|
|
143
|
-
focusableElements[0]?.focus()
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
private getFocusableElements(): HTMLElement[] {
|
|
148
|
-
const selector = [
|
|
149
|
-
'a[href]',
|
|
150
|
-
'button:not([disabled])',
|
|
151
|
-
'input:not([disabled])',
|
|
152
|
-
'select:not([disabled])',
|
|
153
|
-
'textarea:not([disabled])',
|
|
154
|
-
'[tabindex]:not([tabindex="-1"])',
|
|
155
|
-
'[contenteditable="true"]'
|
|
156
|
-
].join(', ')
|
|
157
|
-
|
|
158
|
-
return Array.from(document.querySelectorAll(selector)) as HTMLElement[]
|
|
159
|
-
}
|
|
160
|
-
|
|
161
153
|
private handleEscapeKey(event: KeyboardEvent): void {
|
|
162
154
|
// Close dropdowns, but let Bootstrap handle its own modal keyboard behavior
|
|
163
155
|
const activeModal = document.querySelector('.modal.show')
|
|
@@ -181,18 +173,24 @@ export class AccessibilityManager {
|
|
|
181
173
|
// Add keyboard support for custom components
|
|
182
174
|
document.addEventListener('keydown', (event) => {
|
|
183
175
|
const target = event.target as HTMLElement
|
|
184
|
-
|
|
176
|
+
|
|
177
|
+
// Never intercept keys inside editable controls: arrow keys must keep
|
|
178
|
+
// moving the caret in e.g. a navbar search input.
|
|
179
|
+
if (target.matches('input, textarea, select, [contenteditable], [contenteditable] *')) {
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
185
183
|
// Handle arrow key navigation for menus
|
|
186
184
|
if (target.closest('.nav, .navbar-nav, .dropdown-menu')) {
|
|
187
185
|
this.handleMenuNavigation(event)
|
|
188
186
|
}
|
|
189
|
-
|
|
187
|
+
|
|
190
188
|
// Handle Enter and Space for custom buttons
|
|
191
189
|
if ((event.key === 'Enter' || event.key === ' ') && target.hasAttribute('role') && target.getAttribute('role') === 'button' && !target.matches('button, input[type="button"], input[type="submit"]')) {
|
|
192
190
|
event.preventDefault()
|
|
193
191
|
target.click()
|
|
194
192
|
}
|
|
195
|
-
})
|
|
193
|
+
}, { signal: this.signal })
|
|
196
194
|
}
|
|
197
195
|
|
|
198
196
|
private handleMenuNavigation(event: KeyboardEvent): void {
|
|
@@ -201,9 +199,18 @@ export class AccessibilityManager {
|
|
|
201
199
|
}
|
|
202
200
|
|
|
203
201
|
const currentElement = event.target as HTMLElement
|
|
204
|
-
const menuItems = Array.from(currentElement.closest('.nav, .navbar-nav, .dropdown-menu')?.querySelectorAll('a, button') || []) as HTMLElement[]
|
|
202
|
+
const menuItems = (Array.from(currentElement.closest('.nav, .navbar-nav, .dropdown-menu')?.querySelectorAll('a, button') || []) as HTMLElement[])
|
|
203
|
+
// Skip items hidden inside collapsed submenus or closed dropdowns.
|
|
204
|
+
.filter(item => item.offsetParent !== null)
|
|
205
205
|
const currentIndex = menuItems.indexOf(currentElement)
|
|
206
|
-
|
|
206
|
+
|
|
207
|
+
// Only take over navigation when focus is actually on a menu item;
|
|
208
|
+
// otherwise stealing the arrow keys would yank focus out of unrelated
|
|
209
|
+
// controls that merely sit inside a nav container.
|
|
210
|
+
if (currentIndex === -1) {
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
207
214
|
let nextIndex: number
|
|
208
215
|
|
|
209
216
|
switch (event.key) {
|
|
@@ -244,16 +251,20 @@ export class AccessibilityManager {
|
|
|
244
251
|
// Disable smooth scrolling
|
|
245
252
|
document.documentElement.style.scrollBehavior = 'auto'
|
|
246
253
|
|
|
247
|
-
// Reduce animation duration
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
254
|
+
// Reduce animation duration. The <head> survives Turbo navigations, so
|
|
255
|
+
// only inject the override stylesheet once to avoid stacking duplicates.
|
|
256
|
+
if (!document.getElementById('adminlte-reduce-motion')) {
|
|
257
|
+
const style = document.createElement('style')
|
|
258
|
+
style.id = 'adminlte-reduce-motion'
|
|
259
|
+
style.textContent = `
|
|
260
|
+
*, *::before, *::after {
|
|
261
|
+
animation-duration: 0.01ms !important;
|
|
262
|
+
animation-iteration-count: 1 !important;
|
|
263
|
+
transition-duration: 0.01ms !important;
|
|
264
|
+
}
|
|
265
|
+
`
|
|
266
|
+
document.head.append(style)
|
|
267
|
+
}
|
|
257
268
|
}
|
|
258
269
|
}
|
|
259
270
|
|
|
@@ -283,6 +294,11 @@ export class AccessibilityManager {
|
|
|
283
294
|
childList: true,
|
|
284
295
|
subtree: true
|
|
285
296
|
})
|
|
297
|
+
|
|
298
|
+
// Stop observing the old <body> before Turbo swaps in a new one.
|
|
299
|
+
this.signal.addEventListener('abort', () => {
|
|
300
|
+
observer.disconnect()
|
|
301
|
+
}, { once: true })
|
|
286
302
|
}
|
|
287
303
|
|
|
288
304
|
// WCAG 1.3.1: Info and Relationships
|
|
@@ -351,6 +367,14 @@ export class AccessibilityManager {
|
|
|
351
367
|
}
|
|
352
368
|
|
|
353
369
|
private handleFormError(input: HTMLInputElement): void {
|
|
370
|
+
// Inputs lacking both an id and a name would otherwise collide on a
|
|
371
|
+
// single "-error" id. Assign a stable generated id once and persist it on
|
|
372
|
+
// the element so repeated `invalid` events reuse the same error node
|
|
373
|
+
// instead of appending a new orphaned one each time.
|
|
374
|
+
if (!input.id && !input.name) {
|
|
375
|
+
input.id = accessibilityUtils.generateId('field')
|
|
376
|
+
}
|
|
377
|
+
|
|
354
378
|
const errorId = `${input.id || input.name}-error`
|
|
355
379
|
let errorElement = document.getElementById(errorId)
|
|
356
380
|
|
|
@@ -368,7 +392,14 @@ export class AccessibilityManager {
|
|
|
368
392
|
}
|
|
369
393
|
|
|
370
394
|
errorElement.textContent = input.validationMessage
|
|
371
|
-
|
|
395
|
+
// Append to any existing aria-describedby (e.g. help text) instead of
|
|
396
|
+
// clobbering it.
|
|
397
|
+
const describedBy = (input.getAttribute('aria-describedby') || '').split(/\s+/).filter(Boolean)
|
|
398
|
+
if (!describedBy.includes(errorId)) {
|
|
399
|
+
describedBy.push(errorId)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
input.setAttribute('aria-describedby', describedBy.join(' '))
|
|
372
403
|
input.classList.add('is-invalid')
|
|
373
404
|
|
|
374
405
|
this.announce(`Error in ${input.labels?.[0]?.textContent || input.name}: ${input.validationMessage}`, 'assertive')
|
|
@@ -376,25 +407,33 @@ export class AccessibilityManager {
|
|
|
376
407
|
|
|
377
408
|
// Modal focus management
|
|
378
409
|
private initModalFocusManagement(): void {
|
|
410
|
+
// Capture the trigger on `show` — before Bootstrap (or the `shown`
|
|
411
|
+
// handler below) moves focus into the modal. Capturing on `shown` would
|
|
412
|
+
// store an element inside the modal itself, so closing it would "restore"
|
|
413
|
+
// focus to a now-hidden node and drop focus to <body>.
|
|
414
|
+
document.addEventListener('show.bs.modal', () => {
|
|
415
|
+
this.focusHistory.push(document.activeElement as HTMLElement)
|
|
416
|
+
}, { signal: this.signal })
|
|
417
|
+
|
|
379
418
|
document.addEventListener('shown.bs.modal', (event) => {
|
|
380
419
|
const modal = event.target as HTMLElement
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
})
|
|
420
|
+
// Respect an explicit [autofocus] before falling back to the first
|
|
421
|
+
// focusable control.
|
|
422
|
+
const autofocusElement = modal.querySelector('[autofocus]') as HTMLElement | null
|
|
423
|
+
const firstFocusable = autofocusElement ||
|
|
424
|
+
(modal.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])') as HTMLElement | null)
|
|
425
|
+
|
|
426
|
+
firstFocusable?.focus()
|
|
427
|
+
}, { signal: this.signal })
|
|
390
428
|
|
|
391
429
|
document.addEventListener('hidden.bs.modal', () => {
|
|
392
|
-
// Restore previous focus
|
|
430
|
+
// Restore previous focus, unless the trigger has since been removed
|
|
431
|
+
// from the document (e.g. a delete-confirmation modal removing its row).
|
|
393
432
|
const previousElement = this.focusHistory.pop()
|
|
394
|
-
if (previousElement) {
|
|
433
|
+
if (previousElement?.isConnected) {
|
|
395
434
|
previousElement.focus()
|
|
396
435
|
}
|
|
397
|
-
})
|
|
436
|
+
}, { signal: this.signal })
|
|
398
437
|
}
|
|
399
438
|
|
|
400
439
|
// Dropdown focus management
|
|
@@ -407,7 +446,7 @@ export class AccessibilityManager {
|
|
|
407
446
|
if (firstItem) {
|
|
408
447
|
firstItem.focus()
|
|
409
448
|
}
|
|
410
|
-
})
|
|
449
|
+
}, { signal: this.signal })
|
|
411
450
|
}
|
|
412
451
|
|
|
413
452
|
// Public API methods
|
|
@@ -460,7 +499,7 @@ export class AccessibilityManager {
|
|
|
460
499
|
event.preventDefault()
|
|
461
500
|
}
|
|
462
501
|
}
|
|
463
|
-
})
|
|
502
|
+
}, { signal: this.signal })
|
|
464
503
|
}
|
|
465
504
|
|
|
466
505
|
public addLandmarks(): void {
|
|
@@ -470,7 +509,9 @@ export class AccessibilityManager {
|
|
|
470
509
|
const appMain = document.querySelector('.app-main')
|
|
471
510
|
if (appMain) {
|
|
472
511
|
appMain.setAttribute('role', 'main')
|
|
473
|
-
appMain.id
|
|
512
|
+
if (!appMain.id) {
|
|
513
|
+
appMain.id = 'main'
|
|
514
|
+
}
|
|
474
515
|
}
|
|
475
516
|
}
|
|
476
517
|
|
|
@@ -505,10 +546,29 @@ export const initAccessibility = (config?: Partial<AccessibilityConfig>): Access
|
|
|
505
546
|
return new AccessibilityManager(config)
|
|
506
547
|
}
|
|
507
548
|
|
|
549
|
+
// Parse a CSS color into RGB channels. Supports rgb()/rgba() strings and
|
|
550
|
+
// 3- or 6-digit hex notation; anything else falls back to black.
|
|
551
|
+
const parseColorChannels = (color: string): number[] => {
|
|
552
|
+
const hexMatch = /^#([\da-f]{3}|[\da-f]{6})$/i.exec(color.trim())
|
|
553
|
+
if (hexMatch) {
|
|
554
|
+
let hex = hexMatch[1]
|
|
555
|
+
if (hex.length === 3) {
|
|
556
|
+
hex = [...hex].map(character => character + character).join('')
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return [
|
|
560
|
+
Number.parseInt(hex.slice(0, 2), 16),
|
|
561
|
+
Number.parseInt(hex.slice(2, 4), 16),
|
|
562
|
+
Number.parseInt(hex.slice(4, 6), 16)
|
|
563
|
+
]
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return color.match(/\d+/g)?.map(Number) || [0, 0, 0]
|
|
567
|
+
}
|
|
568
|
+
|
|
508
569
|
// Utility function for luminance calculation
|
|
509
570
|
const getLuminance = (color: string): number => {
|
|
510
|
-
const
|
|
511
|
-
const [r, g, b] = rgb.map(c => {
|
|
571
|
+
const [r, g, b] = parseColorChannels(color).map(c => {
|
|
512
572
|
c = c / 255
|
|
513
573
|
return c <= 0.039_28 ? c / 12.92 : (c + 0.055) ** 2.4 / (1.055 ** 2.4)
|
|
514
574
|
})
|
package/src/ts/adminlte.ts
CHANGED
|
@@ -5,10 +5,11 @@ import Treeview from './treeview.js'
|
|
|
5
5
|
import DirectChat from './direct-chat.js'
|
|
6
6
|
import FullScreen from './fullscreen.js'
|
|
7
7
|
import PushMenu from './push-menu.js'
|
|
8
|
+
import ColorMode from './color-mode.js'
|
|
8
9
|
import { initAccessibility } from './accessibility.js'
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
|
-
* AdminLTE v4.
|
|
12
|
+
* AdminLTE v4.1.0
|
|
12
13
|
* Author: Colorlib
|
|
13
14
|
* Website: AdminLTE.io <https://adminlte.io>
|
|
14
15
|
* License: Open source - MIT <https://opensource.org/licenses/MIT>
|
|
@@ -38,5 +39,6 @@ export {
|
|
|
38
39
|
DirectChat,
|
|
39
40
|
FullScreen,
|
|
40
41
|
PushMenu,
|
|
42
|
+
ColorMode,
|
|
41
43
|
initAccessibility
|
|
42
44
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* --------------------------------------------
|
|
3
|
+
* @file AdminLTE base-component.ts
|
|
4
|
+
* @description Shared component lifecycle for AdminLTE plugins: a per-element
|
|
5
|
+
* instance registry (getInstance / getOrCreateInstance / dispose) and a
|
|
6
|
+
* consistent custom-event contract, mirroring Bootstrap's component API.
|
|
7
|
+
* @license MIT
|
|
8
|
+
* --------------------------------------------
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* element -> (data key -> component instance). WeakMap keys don't prevent
|
|
13
|
+
* garbage collection, so instances die with their elements — important under
|
|
14
|
+
* Hotwired Turbo, which swaps the whole <body> on navigation.
|
|
15
|
+
*/
|
|
16
|
+
const componentRegistry = new WeakMap<Element, Map<string, BaseComponent>>()
|
|
17
|
+
|
|
18
|
+
class BaseComponent {
|
|
19
|
+
static get NAME(): string {
|
|
20
|
+
throw new Error('Component subclasses must override the static NAME getter.')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static get DATA_KEY(): string {
|
|
24
|
+
return `lte.${this.NAME}`
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Untyped registry lookup. Every component exposes a typed wrapper
|
|
29
|
+
* (e.g. CardWidget.getInstance()) built on top of this.
|
|
30
|
+
*/
|
|
31
|
+
protected static _getInstance(element: Element | null | undefined): BaseComponent | null {
|
|
32
|
+
if (!element) {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return componentRegistry.get(element)?.get(this.DATA_KEY) ?? null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_element: HTMLElement
|
|
40
|
+
|
|
41
|
+
constructor(element: HTMLElement) {
|
|
42
|
+
this._element = element
|
|
43
|
+
|
|
44
|
+
const instances = componentRegistry.get(element) ?? new Map<string, BaseComponent>()
|
|
45
|
+
componentRegistry.set(element, instances)
|
|
46
|
+
instances.set((this.constructor as typeof BaseComponent).DATA_KEY, this)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Remove this instance from the registry so getInstance() no longer
|
|
51
|
+
* returns it. Subclasses release their own resources, then call
|
|
52
|
+
* super.dispose().
|
|
53
|
+
*/
|
|
54
|
+
dispose(): void {
|
|
55
|
+
const instances = componentRegistry.get(this._element)
|
|
56
|
+
instances?.delete((this.constructor as typeof BaseComponent).DATA_KEY)
|
|
57
|
+
|
|
58
|
+
if (instances?.size === 0) {
|
|
59
|
+
componentRegistry.delete(this._element)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Dispatch a namespaced custom event that bubbles — so applications can
|
|
66
|
+
* listen once on `document` — and can optionally carry a payload or be
|
|
67
|
+
* canceled. Returns the event so callers can check `defaultPrevented`.
|
|
68
|
+
*/
|
|
69
|
+
const dispatchCustomEvent = <T = undefined>(
|
|
70
|
+
element: Element,
|
|
71
|
+
name: string,
|
|
72
|
+
options: { cancelable?: boolean; detail?: T } = {}
|
|
73
|
+
): CustomEvent<T | undefined> => {
|
|
74
|
+
const event = new CustomEvent<T | undefined>(name, {
|
|
75
|
+
bubbles: true,
|
|
76
|
+
cancelable: options.cancelable ?? false,
|
|
77
|
+
detail: options.detail
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
element.dispatchEvent(event)
|
|
81
|
+
return event
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export { BaseComponent, dispatchCustomEvent }
|