admin-lte 4.0.4 → 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/CHANGELOG.md +23 -0
- 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 +2 -782
- 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 +2 -782
- 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 +391 -200
- 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 +32 -6
- 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/_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/ts/adminlte.ts +3 -1
- package/src/ts/base-component.ts +84 -0
- package/src/ts/card-widget.ts +124 -100
- package/src/ts/color-mode.ts +174 -0
- package/src/ts/direct-chat.ts +38 -32
- package/src/ts/fullscreen.ts +32 -23
- package/src/ts/push-menu.ts +69 -36
- package/src/ts/treeview.ts +84 -50
- package/src/ts/util/index.ts +1 -1
- package/src/scss/_bootstrap-variables.scss +0 -1766
|
@@ -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 }
|
package/src/ts/card-widget.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* --------------------------------------------
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { BaseComponent, dispatchCustomEvent } from './base-component'
|
|
9
10
|
import {
|
|
10
|
-
onDOMContentLoaded,
|
|
11
11
|
slideUp,
|
|
12
12
|
slideDown
|
|
13
13
|
} from './util/index'
|
|
@@ -17,11 +17,18 @@ import {
|
|
|
17
17
|
* ====================================================
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
const
|
|
21
|
-
const EVENT_KEY =
|
|
20
|
+
const NAME = 'card-widget'
|
|
21
|
+
const EVENT_KEY = `.lte.${NAME}`
|
|
22
|
+
|
|
23
|
+
// Cancelable "before" events, dispatched on the card when an action starts.
|
|
24
|
+
const EVENT_COLLAPSE = `collapse${EVENT_KEY}`
|
|
25
|
+
const EVENT_EXPAND = `expand${EVENT_KEY}`
|
|
26
|
+
const EVENT_REMOVE = `remove${EVENT_KEY}`
|
|
27
|
+
|
|
28
|
+
// "After" events, dispatched on the card when the action has completed.
|
|
22
29
|
const EVENT_COLLAPSED = `collapsed${EVENT_KEY}`
|
|
23
30
|
const EVENT_EXPANDED = `expanded${EVENT_KEY}`
|
|
24
|
-
const
|
|
31
|
+
const EVENT_REMOVED = `removed${EVENT_KEY}`
|
|
25
32
|
const EVENT_MAXIMIZED = `maximized${EVENT_KEY}`
|
|
26
33
|
const EVENT_MINIMIZED = `minimized${EVENT_KEY}`
|
|
27
34
|
|
|
@@ -53,14 +60,24 @@ const Default: Config = {
|
|
|
53
60
|
maximizeTrigger: SELECTOR_DATA_MAXIMIZE
|
|
54
61
|
}
|
|
55
62
|
|
|
56
|
-
class CardWidget {
|
|
57
|
-
|
|
63
|
+
class CardWidget extends BaseComponent {
|
|
64
|
+
static get NAME(): string {
|
|
65
|
+
return NAME
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static getInstance(element: Element | null | undefined): CardWidget | null {
|
|
69
|
+
return this._getInstance(element) as CardWidget | null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static getOrCreateInstance(element: HTMLElement, config: Partial<Config> = {}): CardWidget {
|
|
73
|
+
return this.getInstance(element) ?? new this(element, config)
|
|
74
|
+
}
|
|
75
|
+
|
|
58
76
|
_parent: HTMLElement | undefined
|
|
59
|
-
_clone: HTMLElement | undefined
|
|
60
77
|
_config: Config
|
|
61
78
|
|
|
62
|
-
constructor(element: HTMLElement, config: Config) {
|
|
63
|
-
|
|
79
|
+
constructor(element: HTMLElement, config: Partial<Config> = {}) {
|
|
80
|
+
super(element)
|
|
64
81
|
this._parent = element.closest(SELECTOR_CARD) as HTMLElement | undefined
|
|
65
82
|
|
|
66
83
|
if (element.classList.contains(CLASS_NAME_CARD)) {
|
|
@@ -71,76 +88,87 @@ class CardWidget {
|
|
|
71
88
|
}
|
|
72
89
|
|
|
73
90
|
collapse() {
|
|
74
|
-
|
|
91
|
+
if (!this._parent) {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
75
94
|
|
|
76
|
-
if (this._parent) {
|
|
77
|
-
|
|
78
|
-
|
|
95
|
+
if (dispatchCustomEvent(this._parent, EVENT_COLLAPSE, { cancelable: true }).defaultPrevented) {
|
|
96
|
+
return
|
|
97
|
+
}
|
|
79
98
|
|
|
80
|
-
|
|
81
|
-
|
|
99
|
+
this._parent.classList.add(CLASS_NAME_COLLAPSING)
|
|
100
|
+
this._parent.classList.remove(CLASS_NAME_EXPANDING)
|
|
82
101
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
slideUp(el, this._config.animationSpeed)
|
|
86
|
-
}
|
|
87
|
-
})
|
|
102
|
+
// Only target direct children to avoid affecting nested cards
|
|
103
|
+
const elm = this._parent.querySelectorAll(`:scope > ${SELECTOR_CARD_BODY}, :scope > ${SELECTOR_CARD_FOOTER}`)
|
|
88
104
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
this._parent.classList.remove(CLASS_NAME_COLLAPSING)
|
|
95
|
-
}
|
|
96
|
-
}, this._config.animationSpeed)
|
|
97
|
-
}
|
|
105
|
+
elm.forEach(el => {
|
|
106
|
+
if (el instanceof HTMLElement) {
|
|
107
|
+
slideUp(el, this._config.animationSpeed)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
98
110
|
|
|
99
|
-
|
|
111
|
+
setTimeout(() => {
|
|
112
|
+
// Guard against a stale timer: if expand() ran while this collapse
|
|
113
|
+
// was still animating, the card must not end up marked collapsed.
|
|
114
|
+
if (this._parent?.classList.contains(CLASS_NAME_COLLAPSING)) {
|
|
115
|
+
this._parent.classList.add(CLASS_NAME_COLLAPSED)
|
|
116
|
+
this._parent.classList.remove(CLASS_NAME_COLLAPSING)
|
|
117
|
+
dispatchCustomEvent(this._parent, EVENT_COLLAPSED)
|
|
118
|
+
}
|
|
119
|
+
}, this._config.animationSpeed)
|
|
100
120
|
}
|
|
101
121
|
|
|
102
122
|
expand() {
|
|
103
|
-
|
|
123
|
+
if (!this._parent) {
|
|
124
|
+
return
|
|
125
|
+
}
|
|
104
126
|
|
|
105
|
-
if (this._parent) {
|
|
106
|
-
|
|
107
|
-
|
|
127
|
+
if (dispatchCustomEvent(this._parent, EVENT_EXPAND, { cancelable: true }).defaultPrevented) {
|
|
128
|
+
return
|
|
129
|
+
}
|
|
108
130
|
|
|
109
|
-
|
|
110
|
-
|
|
131
|
+
this._parent.classList.add(CLASS_NAME_EXPANDING)
|
|
132
|
+
this._parent.classList.remove(CLASS_NAME_COLLAPSING, CLASS_NAME_COLLAPSED)
|
|
111
133
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
slideDown(el, this._config.animationSpeed)
|
|
115
|
-
}
|
|
116
|
-
})
|
|
134
|
+
// Only target direct children to avoid affecting nested cards
|
|
135
|
+
const elm = this._parent.querySelectorAll(`:scope > ${SELECTOR_CARD_BODY}, :scope > ${SELECTOR_CARD_FOOTER}`)
|
|
117
136
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}, this._config.animationSpeed)
|
|
124
|
-
}
|
|
137
|
+
elm.forEach(el => {
|
|
138
|
+
if (el instanceof HTMLElement) {
|
|
139
|
+
slideDown(el, this._config.animationSpeed)
|
|
140
|
+
}
|
|
141
|
+
})
|
|
125
142
|
|
|
126
|
-
|
|
143
|
+
setTimeout(() => {
|
|
144
|
+
// Same stale-timer guard as collapse(), in the other direction.
|
|
145
|
+
if (this._parent?.classList.contains(CLASS_NAME_EXPANDING)) {
|
|
146
|
+
this._parent.classList.remove(CLASS_NAME_EXPANDING)
|
|
147
|
+
dispatchCustomEvent(this._parent, EVENT_EXPANDED)
|
|
148
|
+
}
|
|
149
|
+
}, this._config.animationSpeed)
|
|
127
150
|
}
|
|
128
151
|
|
|
129
152
|
remove() {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const parent = this._parent
|
|
134
|
-
slideUp(parent, this._config.animationSpeed)
|
|
153
|
+
if (!this._parent) {
|
|
154
|
+
return
|
|
155
|
+
}
|
|
135
156
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
setTimeout(() => {
|
|
139
|
-
parent.remove()
|
|
140
|
-
}, this._config.animationSpeed)
|
|
157
|
+
if (dispatchCustomEvent(this._parent, EVENT_REMOVE, { cancelable: true }).defaultPrevented) {
|
|
158
|
+
return
|
|
141
159
|
}
|
|
142
160
|
|
|
143
|
-
this.
|
|
161
|
+
const parent = this._parent
|
|
162
|
+
slideUp(parent, this._config.animationSpeed)
|
|
163
|
+
|
|
164
|
+
// Actually remove the card once the animation finishes — hiding it
|
|
165
|
+
// would keep its form fields submitting and its ids in the document.
|
|
166
|
+
// `removed` is dispatched just before detachment so it can still bubble.
|
|
167
|
+
setTimeout(() => {
|
|
168
|
+
dispatchCustomEvent(parent, EVENT_REMOVED)
|
|
169
|
+
parent.remove()
|
|
170
|
+
this.dispose()
|
|
171
|
+
}, this._config.animationSpeed)
|
|
144
172
|
}
|
|
145
173
|
|
|
146
174
|
toggle() {
|
|
@@ -155,8 +183,6 @@ class CardWidget {
|
|
|
155
183
|
}
|
|
156
184
|
|
|
157
185
|
maximize() {
|
|
158
|
-
const event = new Event(EVENT_MAXIMIZED)
|
|
159
|
-
|
|
160
186
|
if (this._parent) {
|
|
161
187
|
this._parent.style.height = `${this._parent.offsetHeight}px`
|
|
162
188
|
this._parent.style.width = `${this._parent.offsetWidth}px`
|
|
@@ -175,16 +201,14 @@ class CardWidget {
|
|
|
175
201
|
if (this._parent.classList.contains(CLASS_NAME_COLLAPSED)) {
|
|
176
202
|
this._parent.classList.add(CLASS_NAME_WAS_COLLAPSED)
|
|
177
203
|
}
|
|
204
|
+
|
|
205
|
+
dispatchCustomEvent(this._parent, EVENT_MAXIMIZED)
|
|
178
206
|
}
|
|
179
207
|
}, 150)
|
|
180
208
|
}
|
|
181
|
-
|
|
182
|
-
this._element?.dispatchEvent(event)
|
|
183
209
|
}
|
|
184
210
|
|
|
185
211
|
minimize() {
|
|
186
|
-
const event = new Event(EVENT_MINIMIZED)
|
|
187
|
-
|
|
188
212
|
if (this._parent) {
|
|
189
213
|
this._parent.style.height = 'auto'
|
|
190
214
|
this._parent.style.width = 'auto'
|
|
@@ -204,6 +228,8 @@ class CardWidget {
|
|
|
204
228
|
this._parent.classList.remove(CLASS_NAME_WAS_COLLAPSED)
|
|
205
229
|
}
|
|
206
230
|
|
|
231
|
+
dispatchCustomEvent(this._parent, EVENT_MINIMIZED)
|
|
232
|
+
|
|
207
233
|
// Drop the inline sizing/transition once the restore transition has
|
|
208
234
|
// finished so the card doesn't carry stale styles forever.
|
|
209
235
|
setTimeout(() => {
|
|
@@ -214,8 +240,6 @@ class CardWidget {
|
|
|
214
240
|
}
|
|
215
241
|
}, 10)
|
|
216
242
|
}
|
|
217
|
-
|
|
218
|
-
this._element?.dispatchEvent(event)
|
|
219
243
|
}
|
|
220
244
|
|
|
221
245
|
toggleMaximize() {
|
|
@@ -232,44 +256,44 @@ class CardWidget {
|
|
|
232
256
|
*
|
|
233
257
|
* Data Api implementation
|
|
234
258
|
* ====================================================
|
|
259
|
+
* A single delegated listener on `document` handles all card toggles — so
|
|
260
|
+
* cards inserted later (AJAX, Turbo Frames) work without re-initialisation,
|
|
261
|
+
* and the listener itself survives Turbo's <body> swaps.
|
|
235
262
|
*/
|
|
236
263
|
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
// Use currentTarget (the [data-lte-toggle] button itself), not target:
|
|
241
|
-
// clicking the <i> icon inside the button would otherwise make the widget's
|
|
242
|
-
// events fire on the icon instead of the button.
|
|
243
|
-
collapseBtn.forEach(btn => {
|
|
244
|
-
btn.addEventListener('click', event => {
|
|
245
|
-
event.preventDefault()
|
|
246
|
-
const target = event.currentTarget as HTMLElement
|
|
247
|
-
const data = new CardWidget(target, Default)
|
|
248
|
-
data.toggle()
|
|
249
|
-
})
|
|
250
|
-
})
|
|
264
|
+
document.addEventListener('click', event => {
|
|
265
|
+
const target = event.target
|
|
251
266
|
|
|
252
|
-
|
|
267
|
+
if (!(target instanceof Element)) {
|
|
268
|
+
return
|
|
269
|
+
}
|
|
253
270
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const data = new CardWidget(target, Default)
|
|
259
|
-
data.remove()
|
|
260
|
-
})
|
|
261
|
-
})
|
|
271
|
+
const collapseTrigger = target.closest(SELECTOR_DATA_COLLAPSE)
|
|
272
|
+
const removeTrigger = target.closest(SELECTOR_DATA_REMOVE)
|
|
273
|
+
const maximizeTrigger = target.closest(SELECTOR_DATA_MAXIMIZE)
|
|
274
|
+
const trigger = collapseTrigger ?? removeTrigger ?? maximizeTrigger
|
|
262
275
|
|
|
263
|
-
|
|
276
|
+
if (!trigger) {
|
|
277
|
+
return
|
|
278
|
+
}
|
|
264
279
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
280
|
+
event.preventDefault()
|
|
281
|
+
|
|
282
|
+
const card = trigger.closest(SELECTOR_CARD) as HTMLElement | null
|
|
283
|
+
|
|
284
|
+
if (!card) {
|
|
285
|
+
return
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const widget = CardWidget.getOrCreateInstance(card)
|
|
289
|
+
|
|
290
|
+
if (collapseTrigger) {
|
|
291
|
+
widget.toggle()
|
|
292
|
+
} else if (removeTrigger) {
|
|
293
|
+
widget.remove()
|
|
294
|
+
} else {
|
|
295
|
+
widget.toggleMaximize()
|
|
296
|
+
}
|
|
273
297
|
})
|
|
274
298
|
|
|
275
299
|
export default CardWidget
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* --------------------------------------------
|
|
3
|
+
* @file AdminLTE color-mode.ts
|
|
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.
|
|
8
|
+
*
|
|
9
|
+
* Ships in the bundle so applications no longer need to copy the demo's
|
|
10
|
+
* inline script. The tiny no-flash snippet in <head> (see _head.astro)
|
|
11
|
+
* remains inline by design — it must run before first paint.
|
|
12
|
+
* @license MIT
|
|
13
|
+
* --------------------------------------------
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { getLifecycleSignal, onDOMContentLoaded } from './util/index'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Constants
|
|
20
|
+
* ====================================================
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const DATA_KEY = 'lte.color-mode'
|
|
24
|
+
const EVENT_KEY = `.${DATA_KEY}`
|
|
25
|
+
const EVENT_CHANGED = `changed${EVENT_KEY}`
|
|
26
|
+
|
|
27
|
+
const STORAGE_KEY = 'lte-theme'
|
|
28
|
+
|
|
29
|
+
const SELECTOR_TOGGLE = '[data-bs-theme-value]'
|
|
30
|
+
const SELECTOR_ICON = '[data-lte-theme-icon]'
|
|
31
|
+
|
|
32
|
+
type Theme = 'light' | 'dark' | 'auto'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Class Definition
|
|
36
|
+
* ====================================================
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
class ColorMode {
|
|
40
|
+
/**
|
|
41
|
+
* Read the persisted theme choice, or null when nothing was stored or
|
|
42
|
+
* localStorage is unavailable (private mode, sandboxed iframe).
|
|
43
|
+
*/
|
|
44
|
+
getStoredTheme(): Theme | null {
|
|
45
|
+
try {
|
|
46
|
+
const stored = localStorage.getItem(STORAGE_KEY)
|
|
47
|
+
return stored && ['light', 'dark', 'auto'].includes(stored) ? stored as Theme : null
|
|
48
|
+
} catch {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The user's effective choice: the stored theme, falling back to the OS
|
|
55
|
+
* preference.
|
|
56
|
+
*/
|
|
57
|
+
getPreferredTheme(): Theme {
|
|
58
|
+
const stored = this.getStoredTheme()
|
|
59
|
+
if (stored) {
|
|
60
|
+
return stored
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return this._prefersDark() ? 'dark' : 'light'
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve "auto" against the OS preference.
|
|
68
|
+
*/
|
|
69
|
+
resolveTheme(theme: Theme): 'light' | 'dark' {
|
|
70
|
+
if (theme === 'auto') {
|
|
71
|
+
return this._prefersDark() ? 'dark' : 'light'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return theme
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Apply a theme and persist the choice. Dispatches
|
|
79
|
+
* `changed.lte.color-mode` on the document with { theme, resolved }.
|
|
80
|
+
*/
|
|
81
|
+
setTheme(theme: Theme): void {
|
|
82
|
+
try {
|
|
83
|
+
localStorage.setItem(STORAGE_KEY, theme)
|
|
84
|
+
} catch {
|
|
85
|
+
// localStorage may be unavailable; the theme still applies for this page.
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this._applyTheme(theme)
|
|
89
|
+
this._showActiveTheme(theme)
|
|
90
|
+
|
|
91
|
+
document.dispatchEvent(new CustomEvent(EVENT_CHANGED, {
|
|
92
|
+
detail: { theme, resolved: this.resolveTheme(theme) }
|
|
93
|
+
}))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Apply without persisting — used on init and when the OS preference
|
|
98
|
+
* changes while in "auto" mode.
|
|
99
|
+
*/
|
|
100
|
+
_applyTheme(theme: Theme): void {
|
|
101
|
+
const resolved = this.resolveTheme(theme)
|
|
102
|
+
document.documentElement.setAttribute('data-bs-theme', resolved)
|
|
103
|
+
document.documentElement.style.colorScheme = resolved
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_prefersDark(): boolean {
|
|
107
|
+
return globalThis.matchMedia('(prefers-color-scheme: dark)').matches
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Sync the [data-bs-theme-value] toggles (active state, pressed state,
|
|
112
|
+
* check mark) and the [data-lte-theme-icon] indicator icons.
|
|
113
|
+
*/
|
|
114
|
+
_showActiveTheme(theme: Theme): void {
|
|
115
|
+
document.querySelectorAll(SELECTOR_TOGGLE).forEach(toggle => {
|
|
116
|
+
const isActive = toggle.getAttribute('data-bs-theme-value') === theme
|
|
117
|
+
toggle.classList.toggle('active', isActive)
|
|
118
|
+
toggle.setAttribute('aria-pressed', String(isActive))
|
|
119
|
+
toggle.querySelector('.bi-check-lg')?.classList.toggle('d-none', !isActive)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
document.querySelectorAll(SELECTOR_ICON).forEach(icon => {
|
|
123
|
+
icon.classList.toggle('d-none', (icon as HTMLElement).dataset.lteThemeIcon !== theme)
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Apply the preferred theme and sync the UI without persisting anything.
|
|
129
|
+
*/
|
|
130
|
+
init(): void {
|
|
131
|
+
const theme = this.getPreferredTheme()
|
|
132
|
+
this._applyTheme(theme)
|
|
133
|
+
this._showActiveTheme(theme)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Data API implementation
|
|
139
|
+
* ====================================================
|
|
140
|
+
* Toggle clicks are delegated on `document`, so switcher buttons added after
|
|
141
|
+
* load work and the listener survives Turbo's <body> swaps. The class is
|
|
142
|
+
* stateless — everything lives in localStorage and the DOM.
|
|
143
|
+
*/
|
|
144
|
+
|
|
145
|
+
document.addEventListener('click', event => {
|
|
146
|
+
const target = event.target
|
|
147
|
+
|
|
148
|
+
if (!(target instanceof Element)) {
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const toggle = target.closest(SELECTOR_TOGGLE)
|
|
153
|
+
const theme = toggle?.getAttribute('data-bs-theme-value') as Theme | null
|
|
154
|
+
|
|
155
|
+
if (theme) {
|
|
156
|
+
new ColorMode().setTheme(theme)
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
onDOMContentLoaded(() => {
|
|
161
|
+
const colorMode = new ColorMode()
|
|
162
|
+
colorMode.init()
|
|
163
|
+
|
|
164
|
+
// Follow the OS while no explicit choice (or "auto") is stored.
|
|
165
|
+
globalThis.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
|
166
|
+
const stored = colorMode.getStoredTheme()
|
|
167
|
+
if (!stored || stored === 'auto') {
|
|
168
|
+
colorMode._applyTheme('auto')
|
|
169
|
+
colorMode._showActiveTheme(stored ?? 'auto')
|
|
170
|
+
}
|
|
171
|
+
}, { signal: getLifecycleSignal() })
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
export default ColorMode
|
package/src/ts/direct-chat.ts
CHANGED
|
@@ -6,17 +6,15 @@
|
|
|
6
6
|
* --------------------------------------------
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
10
|
-
onDOMContentLoaded
|
|
11
|
-
} from './util/index'
|
|
9
|
+
import { BaseComponent, dispatchCustomEvent } from './base-component'
|
|
12
10
|
|
|
13
11
|
/**
|
|
14
12
|
* Constants
|
|
15
13
|
* ====================================================
|
|
16
14
|
*/
|
|
17
15
|
|
|
18
|
-
const
|
|
19
|
-
const EVENT_KEY =
|
|
16
|
+
const NAME = 'direct-chat'
|
|
17
|
+
const EVENT_KEY = `.lte.${NAME}`
|
|
20
18
|
const EVENT_EXPANDED = `expanded${EVENT_KEY}`
|
|
21
19
|
const EVENT_COLLAPSED = `collapsed${EVENT_KEY}`
|
|
22
20
|
|
|
@@ -30,25 +28,26 @@ const CLASS_NAME_DIRECT_CHAT_OPEN = 'direct-chat-contacts-open'
|
|
|
30
28
|
* ====================================================
|
|
31
29
|
*/
|
|
32
30
|
|
|
33
|
-
class DirectChat {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
class DirectChat extends BaseComponent {
|
|
32
|
+
static get NAME(): string {
|
|
33
|
+
return NAME
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static getInstance(element: Element | null | undefined): DirectChat | null {
|
|
37
|
+
return this._getInstance(element) as DirectChat | null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
static getOrCreateInstance(element: HTMLElement): DirectChat {
|
|
41
|
+
return this.getInstance(element) ?? new this(element)
|
|
37
42
|
}
|
|
38
43
|
|
|
39
44
|
toggle(): void {
|
|
40
45
|
if (this._element.classList.contains(CLASS_NAME_DIRECT_CHAT_OPEN)) {
|
|
41
|
-
const event = new Event(EVENT_COLLAPSED)
|
|
42
|
-
|
|
43
46
|
this._element.classList.remove(CLASS_NAME_DIRECT_CHAT_OPEN)
|
|
44
|
-
|
|
45
|
-
this._element.dispatchEvent(event)
|
|
47
|
+
dispatchCustomEvent(this._element, EVENT_COLLAPSED)
|
|
46
48
|
} else {
|
|
47
|
-
const event = new Event(EVENT_EXPANDED)
|
|
48
|
-
|
|
49
49
|
this._element.classList.add(CLASS_NAME_DIRECT_CHAT_OPEN)
|
|
50
|
-
|
|
51
|
-
this._element.dispatchEvent(event)
|
|
50
|
+
dispatchCustomEvent(this._element, EVENT_EXPANDED)
|
|
52
51
|
}
|
|
53
52
|
}
|
|
54
53
|
}
|
|
@@ -57,23 +56,30 @@ class DirectChat {
|
|
|
57
56
|
*
|
|
58
57
|
* Data Api implementation
|
|
59
58
|
* ====================================================
|
|
59
|
+
* One delegated listener on `document`: chat panes inserted later work, and
|
|
60
|
+
* the listener survives Turbo's <body> swaps.
|
|
60
61
|
*/
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
63
|
+
document.addEventListener('click', event => {
|
|
64
|
+
const target = event.target
|
|
65
|
+
|
|
66
|
+
if (!(target instanceof Element)) {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const trigger = target.closest(SELECTOR_DATA_TOGGLE)
|
|
71
|
+
|
|
72
|
+
if (!trigger) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
event.preventDefault()
|
|
77
|
+
|
|
78
|
+
const chatPane = trigger.closest(SELECTOR_DIRECT_CHAT) as HTMLElement | null
|
|
79
|
+
|
|
80
|
+
if (chatPane) {
|
|
81
|
+
DirectChat.getOrCreateInstance(chatPane).toggle()
|
|
82
|
+
}
|
|
77
83
|
})
|
|
78
84
|
|
|
79
85
|
export default DirectChat
|