admin-lte 4.2.0 → 4.4.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/README.md +18 -1
  3. package/dist/css/adminlte-colors.css +1261 -0
  4. package/dist/css/adminlte-colors.css.map +1 -0
  5. package/dist/css/adminlte-colors.min.css +15 -0
  6. package/dist/css/adminlte-colors.min.css.map +1 -0
  7. package/dist/css/adminlte-colors.rtl.css +1260 -0
  8. package/dist/css/adminlte-colors.rtl.css.map +1 -0
  9. package/dist/css/adminlte-colors.rtl.min.css +15 -0
  10. package/dist/css/adminlte-colors.rtl.min.css.map +1 -0
  11. package/dist/css/adminlte-docs.css +1 -1
  12. package/dist/css/adminlte-docs.css.map +1 -1
  13. package/dist/css/adminlte-docs.min.css +1 -1
  14. package/dist/css/adminlte-docs.min.css.map +1 -1
  15. package/dist/css/adminlte-docs.rtl.css +1 -1
  16. package/dist/css/adminlte-docs.rtl.css.map +1 -1
  17. package/dist/css/adminlte-docs.rtl.min.css +1 -1
  18. package/dist/css/adminlte-docs.rtl.min.css.map +1 -1
  19. package/dist/css/adminlte-select2.css +329 -0
  20. package/dist/css/adminlte-select2.css.map +1 -0
  21. package/dist/css/adminlte-select2.min.css +12 -0
  22. package/dist/css/adminlte-select2.min.css.map +1 -0
  23. package/dist/css/adminlte-select2.rtl.css +328 -0
  24. package/dist/css/adminlte-select2.rtl.css.map +1 -0
  25. package/dist/css/adminlte-select2.rtl.min.css +12 -0
  26. package/dist/css/adminlte-select2.rtl.min.css.map +1 -0
  27. package/dist/css/adminlte.css +323 -19
  28. package/dist/css/adminlte.css.map +1 -1
  29. package/dist/css/adminlte.min.css +2 -2
  30. package/dist/css/adminlte.min.css.map +1 -1
  31. package/dist/css/adminlte.rtl.css +323 -19
  32. package/dist/css/adminlte.rtl.css.map +1 -1
  33. package/dist/css/adminlte.rtl.min.css +2 -2
  34. package/dist/css/adminlte.rtl.min.css.map +1 -1
  35. package/dist/js/adminlte.esm.js +231 -81
  36. package/dist/js/adminlte.esm.js.map +1 -1
  37. package/dist/js/adminlte.esm.min.js +2 -2
  38. package/dist/js/adminlte.esm.min.js.map +1 -1
  39. package/dist/js/adminlte.js +231 -80
  40. package/dist/js/adminlte.js.map +1 -1
  41. package/dist/js/adminlte.min.js +2 -2
  42. package/dist/js/adminlte.min.js.map +1 -1
  43. package/dist/js/types/adminlte.d.ts +2 -1
  44. package/dist/js/types/sidebar-search.d.ts +46 -0
  45. package/package.json +2 -1
  46. package/src/scss/_app-header.scss +49 -1
  47. package/src/scss/_app-sidebar.scss +22 -2
  48. package/src/scss/_mixins.scss +1 -0
  49. package/src/scss/_ribbon.scss +75 -0
  50. package/src/scss/_variables.scss +36 -0
  51. package/src/scss/_widgets.scss +192 -0
  52. package/src/scss/adminlte-colors.scss +35 -0
  53. package/src/scss/adminlte-docs.scss +1 -1
  54. package/src/scss/adminlte-select2.scss +31 -0
  55. package/src/scss/adminlte.scss +1 -1
  56. package/src/scss/colors/_colors.scss +159 -0
  57. package/src/scss/colors/_variables.scss +85 -0
  58. package/src/scss/compat/_select2.scss +476 -0
  59. package/src/scss/mixins/_search-field.scss +46 -0
  60. package/src/scss/parts/_extra-components.scss +2 -0
  61. package/src/ts/accessibility.ts +25 -13
  62. package/src/ts/adminlte.ts +3 -1
  63. 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