admin-lte 4.1.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +57 -1
  2. package/README.md +23 -7
  3. package/dist/css/adminlte-docs.css +1 -1
  4. package/dist/css/adminlte-docs.css.map +1 -1
  5. package/dist/css/adminlte-docs.min.css +1 -1
  6. package/dist/css/adminlte-docs.min.css.map +1 -1
  7. package/dist/css/adminlte-docs.rtl.css +1 -1
  8. package/dist/css/adminlte-docs.rtl.css.map +1 -1
  9. package/dist/css/adminlte-docs.rtl.min.css +1 -1
  10. package/dist/css/adminlte-docs.rtl.min.css.map +1 -1
  11. package/dist/css/adminlte-select2.css +329 -0
  12. package/dist/css/adminlte-select2.css.map +1 -0
  13. package/dist/css/adminlte-select2.min.css +12 -0
  14. package/dist/css/adminlte-select2.min.css.map +1 -0
  15. package/dist/css/adminlte-select2.rtl.css +328 -0
  16. package/dist/css/adminlte-select2.rtl.css.map +1 -0
  17. package/dist/css/adminlte-select2.rtl.min.css +12 -0
  18. package/dist/css/adminlte-select2.rtl.min.css.map +1 -0
  19. package/dist/css/adminlte.css +332 -131
  20. package/dist/css/adminlte.css.map +1 -1
  21. package/dist/css/adminlte.min.css +2 -2
  22. package/dist/css/adminlte.min.css.map +1 -1
  23. package/dist/css/adminlte.rtl.css +332 -131
  24. package/dist/css/adminlte.rtl.css.map +1 -1
  25. package/dist/css/adminlte.rtl.min.css +2 -2
  26. package/dist/css/adminlte.rtl.min.css.map +1 -1
  27. package/dist/js/adminlte.esm.js +300 -109
  28. package/dist/js/adminlte.esm.js.map +1 -1
  29. package/dist/js/adminlte.esm.min.js +2 -2
  30. package/dist/js/adminlte.esm.min.js.map +1 -1
  31. package/dist/js/adminlte.js +302 -108
  32. package/dist/js/adminlte.js.map +1 -1
  33. package/dist/js/adminlte.min.js +2 -2
  34. package/dist/js/adminlte.min.js.map +1 -1
  35. package/dist/js/types/adminlte.d.ts +3 -1
  36. package/dist/js/types/base-component.d.ts +24 -0
  37. package/dist/js/types/color-mode.d.ts +24 -6
  38. package/dist/js/types/push-menu.d.ts +21 -0
  39. package/dist/js/types/sidebar-search.d.ts +46 -0
  40. package/dist/js/types/util/index.d.ts +38 -1
  41. package/package.json +24 -23
  42. package/src/scss/_app-sidebar.scss +12 -0
  43. package/src/scss/_ribbon.scss +75 -0
  44. package/src/scss/_variables.scss +28 -0
  45. package/src/scss/_widgets.scss +192 -0
  46. package/src/scss/adminlte-docs.scss +1 -1
  47. package/src/scss/adminlte-select2.scss +31 -0
  48. package/src/scss/adminlte.scss +1 -1
  49. package/src/scss/compat/_select2.scss +476 -0
  50. package/src/scss/parts/_extra-components.scss +2 -0
  51. package/src/ts/accessibility.ts +31 -15
  52. package/src/ts/adminlte.ts +7 -3
  53. package/src/ts/base-component.ts +30 -3
  54. package/src/ts/color-mode.ts +100 -20
  55. package/src/ts/push-menu.ts +37 -7
  56. package/src/ts/sidebar-search.ts +252 -0
  57. package/src/ts/util/index.ts +79 -15
@@ -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
- * Persists the choice in localStorage, follows the OS preference in
6
- * "auto" mode, and keeps [data-bs-theme-value] toggles and
7
- * [data-lte-theme-icon] indicator icons in sync.
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 SELECTOR_TOGGLE = '[data-bs-theme-value]'
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 && ['light', 'dark', 'auto'].includes(stored) ? stored as Theme : null
101
+ return stored && isValidTheme(stored) ? stored : null
48
102
  } catch {
49
103
  return null
50
104
  }
51
105
  }
52
106
 
53
107
  /**
54
- * The user's effective choice: the stored theme, falling back to the OS
55
- * preference.
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 stored = this.getStoredTheme()
59
- if (stored) {
60
- return stored
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('data-bs-theme', resolved)
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('data-bs-theme-value') === theme
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('data-bs-theme-value') as Theme | null
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 no explicit choice (or "auto") is stored.
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
- const stored = colorMode.getStoredTheme()
167
- if (!stored || stored === 'auto') {
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(stored ?? 'auto')
249
+ colorMode._showActiveTheme(preferred ?? 'auto')
170
250
  }
171
251
  }, { signal: getLifecycleSignal() })
172
252
  })
@@ -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. On low screen sizes, the sidebar should
345
- // always be collapsed by default unless explicitly opened.
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
@@ -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