admin-lte 4.2.0 → 4.3.1
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 +39 -0
- package/README.md +17 -1
- 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-select2.css +329 -0
- package/dist/css/adminlte-select2.css.map +1 -0
- package/dist/css/adminlte-select2.min.css +12 -0
- package/dist/css/adminlte-select2.min.css.map +1 -0
- package/dist/css/adminlte-select2.rtl.css +328 -0
- package/dist/css/adminlte-select2.rtl.css.map +1 -0
- package/dist/css/adminlte-select2.rtl.min.css +12 -0
- package/dist/css/adminlte-select2.rtl.min.css.map +1 -0
- package/dist/css/adminlte.css +202 -1
- 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 +202 -1
- 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 +231 -81
- 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 +231 -80
- 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/sidebar-search.d.ts +46 -0
- package/package.json +1 -1
- package/src/scss/_app-sidebar.scss +12 -0
- package/src/scss/_ribbon.scss +75 -0
- package/src/scss/_variables.scss +28 -0
- package/src/scss/_widgets.scss +192 -0
- package/src/scss/adminlte-docs.scss +1 -1
- package/src/scss/adminlte-select2.scss +31 -0
- package/src/scss/adminlte.scss +1 -1
- package/src/scss/compat/_select2.scss +476 -0
- package/src/scss/parts/_extra-components.scss +2 -0
- package/src/ts/accessibility.ts +25 -13
- package/src/ts/adminlte.ts +3 -1
- package/src/ts/sidebar-search.ts +252 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* --------------------------------------------
|
|
3
|
+
* @file AdminLTE sidebar-search.ts
|
|
4
|
+
* @description Live filter for the sidebar menu.
|
|
5
|
+
* @license MIT
|
|
6
|
+
* --------------------------------------------
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { BaseComponent, dispatchCustomEvent } from './base-component'
|
|
10
|
+
import { onDOMContentLoaded } from './util/index'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Constants
|
|
14
|
+
* ============================================================================
|
|
15
|
+
*/
|
|
16
|
+
const NAME = 'sidebar-search'
|
|
17
|
+
const EVENT_KEY = `.lte.${NAME}`
|
|
18
|
+
const EVENT_FILTERED = `filtered${EVENT_KEY}`
|
|
19
|
+
|
|
20
|
+
const CLASS_NAME_MENU_OPEN = 'menu-open'
|
|
21
|
+
|
|
22
|
+
const SELECTOR_DATA_TOGGLE = '[data-lte-toggle="sidebar-search"]'
|
|
23
|
+
const SELECTOR_SIDEBAR = '.app-sidebar'
|
|
24
|
+
const SELECTOR_MENU = '.sidebar-menu'
|
|
25
|
+
const SELECTOR_NAV_ITEM = '.nav-item'
|
|
26
|
+
const SELECTOR_NAV_HEADER = '.nav-header'
|
|
27
|
+
const SELECTOR_NAV_LINK = ':scope > .nav-link'
|
|
28
|
+
const SELECTOR_TREEVIEW_MENU = ':scope > .nav-treeview'
|
|
29
|
+
const SELECTOR_EMPTY_STATE = '[data-lte-search-empty]'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A submenu's resting state, captured before the first keystroke so clearing
|
|
33
|
+
* the field can put the menu back exactly as the user left it. The inline
|
|
34
|
+
* `display` matters as well as the class: Treeview's slide animation writes
|
|
35
|
+
* one, and it would otherwise win over `.menu-open > .nav-treeview`.
|
|
36
|
+
*/
|
|
37
|
+
type SubmenuState = {
|
|
38
|
+
open: boolean;
|
|
39
|
+
display: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const submenuOf = (item: HTMLElement): HTMLElement | null =>
|
|
43
|
+
item.querySelector<HTMLElement>(SELECTOR_TREEVIEW_MENU)
|
|
44
|
+
|
|
45
|
+
const setOpen = (item: HTMLElement, submenu: HTMLElement, open: boolean, display: string): void => {
|
|
46
|
+
item.classList.toggle(CLASS_NAME_MENU_OPEN, open)
|
|
47
|
+
item.querySelector(SELECTOR_NAV_LINK)?.setAttribute('aria-expanded', String(open))
|
|
48
|
+
submenu.style.display = display
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Class Definition
|
|
53
|
+
* ============================================================================
|
|
54
|
+
*/
|
|
55
|
+
class SidebarSearch extends BaseComponent {
|
|
56
|
+
static get NAME(): string {
|
|
57
|
+
return NAME
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
static getInstance(element: Element | null | undefined): SidebarSearch | null {
|
|
61
|
+
return this._getInstance(element) as SidebarSearch | null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
static getOrCreateInstance(element: HTMLElement): SidebarSearch {
|
|
65
|
+
return this.getInstance(element) ?? new this(element)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
_menu: HTMLElement | null
|
|
69
|
+
_emptyState: HTMLElement | null
|
|
70
|
+
_snapshot: Map<HTMLElement, SubmenuState> | null = null
|
|
71
|
+
|
|
72
|
+
constructor(element: HTMLElement) {
|
|
73
|
+
super(element)
|
|
74
|
+
|
|
75
|
+
// `data-lte-target` names the menu explicitly; without it, the nearest
|
|
76
|
+
// sidebar's menu is used, which covers the single-sidebar default.
|
|
77
|
+
const target = element.dataset.lteTarget
|
|
78
|
+
const scope = element.closest(SELECTOR_SIDEBAR) ?? document
|
|
79
|
+
|
|
80
|
+
this._menu = target ?
|
|
81
|
+
document.querySelector<HTMLElement>(target) :
|
|
82
|
+
scope.querySelector<HTMLElement>(SELECTOR_MENU)
|
|
83
|
+
this._emptyState = scope.querySelector<HTMLElement>(SELECTOR_EMPTY_STATE)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Show only the menu entries matching `term`, expanding whatever has to be
|
|
88
|
+
* expanded to reveal them. An empty term restores the menu.
|
|
89
|
+
*
|
|
90
|
+
* @param term The text to match against nav-link labels, case-insensitively.
|
|
91
|
+
*/
|
|
92
|
+
search(term: string): void {
|
|
93
|
+
const menu = this._menu
|
|
94
|
+
|
|
95
|
+
if (!menu) {
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const query = term.trim().toLowerCase()
|
|
100
|
+
|
|
101
|
+
if (!query) {
|
|
102
|
+
this.clear()
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
this._snapshot ??= this._takeSnapshot(menu)
|
|
107
|
+
|
|
108
|
+
const items = [...menu.querySelectorAll<HTMLElement>(SELECTOR_NAV_ITEM)]
|
|
109
|
+
const matched = new Set<HTMLElement>()
|
|
110
|
+
|
|
111
|
+
for (const item of items) {
|
|
112
|
+
const label = item.querySelector(SELECTOR_NAV_LINK)?.textContent?.replace(/\s+/g, ' ').trim().toLowerCase()
|
|
113
|
+
|
|
114
|
+
if (label?.includes(query)) {
|
|
115
|
+
matched.add(item)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Deepest-first, so by the time a parent is reached its children have
|
|
120
|
+
// already been hidden or kept and it can simply look for a survivor.
|
|
121
|
+
for (let index = items.length - 1; index >= 0; index--) {
|
|
122
|
+
const item = items[index]
|
|
123
|
+
item.hidden = !(matched.has(item) || item.querySelector(`${SELECTOR_NAV_ITEM}:not([hidden])`))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// A group whose own name matched keeps its whole subtree — otherwise
|
|
127
|
+
// expanding it would lead to an empty list.
|
|
128
|
+
for (const item of matched) {
|
|
129
|
+
for (const descendant of item.querySelectorAll<HTMLElement>(SELECTOR_NAV_ITEM)) {
|
|
130
|
+
descendant.hidden = false
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let visible = 0
|
|
135
|
+
|
|
136
|
+
for (const item of items) {
|
|
137
|
+
if (!item.hidden) {
|
|
138
|
+
visible++
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const submenu = submenuOf(item)
|
|
142
|
+
|
|
143
|
+
if (submenu) {
|
|
144
|
+
const expand = !item.hidden && Boolean(item.querySelector(`${SELECTOR_NAV_ITEM}:not([hidden])`))
|
|
145
|
+
setOpen(item, submenu, expand, expand ? 'block' : 'none')
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Section headings label groups that are no longer all present.
|
|
150
|
+
for (const header of menu.querySelectorAll<HTMLElement>(SELECTOR_NAV_HEADER)) {
|
|
151
|
+
header.hidden = true
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (this._emptyState) {
|
|
155
|
+
this._emptyState.hidden = visible > 0
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
dispatchCustomEvent(this._element, EVENT_FILTERED, { detail: { query, matches: visible } })
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Drop the filter: every entry becomes visible again and each submenu goes
|
|
163
|
+
* back to the open/closed state it had before the search started.
|
|
164
|
+
*/
|
|
165
|
+
clear(): void {
|
|
166
|
+
const menu = this._menu
|
|
167
|
+
|
|
168
|
+
if (!menu) {
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (const item of menu.querySelectorAll<HTMLElement>(`${SELECTOR_NAV_ITEM}, ${SELECTOR_NAV_HEADER}`)) {
|
|
173
|
+
item.hidden = false
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (this._snapshot) {
|
|
177
|
+
for (const [item, state] of this._snapshot) {
|
|
178
|
+
const submenu = submenuOf(item)
|
|
179
|
+
|
|
180
|
+
if (submenu) {
|
|
181
|
+
setOpen(item, submenu, state.open, state.display)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this._snapshot = null
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (this._emptyState) {
|
|
189
|
+
this._emptyState.hidden = true
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
dispatchCustomEvent(this._element, EVENT_FILTERED, { detail: { query: '', matches: -1 } })
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
dispose(): void {
|
|
196
|
+
this.clear()
|
|
197
|
+
super.dispose()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
_takeSnapshot(menu: HTMLElement): Map<HTMLElement, SubmenuState> {
|
|
201
|
+
const snapshot = new Map<HTMLElement, SubmenuState>()
|
|
202
|
+
|
|
203
|
+
for (const item of menu.querySelectorAll<HTMLElement>(SELECTOR_NAV_ITEM)) {
|
|
204
|
+
const submenu = submenuOf(item)
|
|
205
|
+
|
|
206
|
+
if (submenu) {
|
|
207
|
+
snapshot.set(item, {
|
|
208
|
+
open: item.classList.contains(CLASS_NAME_MENU_OPEN),
|
|
209
|
+
display: submenu.style.display
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return snapshot
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Data Api implementation
|
|
220
|
+
* ============================================================================
|
|
221
|
+
* Both listeners are delegated on `document`, so a sidebar rendered after load
|
|
222
|
+
* (Turbo Frame, client-side router) needs no re-initialisation.
|
|
223
|
+
*/
|
|
224
|
+
|
|
225
|
+
document.addEventListener('input', event => {
|
|
226
|
+
const target = event.target
|
|
227
|
+
|
|
228
|
+
if (target instanceof HTMLInputElement && target.matches(SELECTOR_DATA_TOGGLE)) {
|
|
229
|
+
SidebarSearch.getOrCreateInstance(target).search(target.value)
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
document.addEventListener('keydown', event => {
|
|
234
|
+
const target = event.target
|
|
235
|
+
|
|
236
|
+
if (event.key === 'Escape' && target instanceof HTMLInputElement && target.matches(SELECTOR_DATA_TOGGLE)) {
|
|
237
|
+
target.value = ''
|
|
238
|
+
SidebarSearch.getOrCreateInstance(target).clear()
|
|
239
|
+
}
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
// A field restored with a value by the browser (bfcache, back navigation)
|
|
243
|
+
// should show its filtered menu rather than a menu that disagrees with it.
|
|
244
|
+
onDOMContentLoaded(() => {
|
|
245
|
+
document.querySelectorAll<HTMLInputElement>(SELECTOR_DATA_TOGGLE).forEach(input => {
|
|
246
|
+
if (input.value) {
|
|
247
|
+
SidebarSearch.getOrCreateInstance(input).search(input.value)
|
|
248
|
+
}
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
export default SidebarSearch
|