@cedx/ui 0.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/License.md +20 -0
- package/ReadMe.md +17 -0
- package/lib/Alignment.d.ts +22 -0
- package/lib/Alignment.d.ts.map +1 -0
- package/lib/Alignment.js +17 -0
- package/lib/AppTheme.d.ts +34 -0
- package/lib/AppTheme.d.ts.map +1 -0
- package/lib/AppTheme.js +41 -0
- package/lib/Components/BackButton.d.ts +27 -0
- package/lib/Components/BackButton.d.ts.map +1 -0
- package/lib/Components/BackButton.js +29 -0
- package/lib/Components/DialogBox.d.ts +151 -0
- package/lib/Components/DialogBox.d.ts.map +1 -0
- package/lib/Components/DialogBox.js +268 -0
- package/lib/Components/FullScreenToggler.d.ts +42 -0
- package/lib/Components/FullScreenToggler.d.ts.map +1 -0
- package/lib/Components/FullScreenToggler.js +103 -0
- package/lib/Components/KeyboardAccelerator.d.ts +36 -0
- package/lib/Components/KeyboardAccelerator.d.ts.map +1 -0
- package/lib/Components/KeyboardAccelerator.js +78 -0
- package/lib/Components/LoadingIndicator.d.ts +58 -0
- package/lib/Components/LoadingIndicator.d.ts.map +1 -0
- package/lib/Components/LoadingIndicator.js +93 -0
- package/lib/Components/MenuActivator.d.ts +26 -0
- package/lib/Components/MenuActivator.d.ts.map +1 -0
- package/lib/Components/MenuActivator.js +42 -0
- package/lib/Components/OfflineIndicator.d.ts +59 -0
- package/lib/Components/OfflineIndicator.d.ts.map +1 -0
- package/lib/Components/OfflineIndicator.js +106 -0
- package/lib/Components/TabActivator.d.ts +49 -0
- package/lib/Components/TabActivator.d.ts.map +1 -0
- package/lib/Components/TabActivator.js +70 -0
- package/lib/Components/ThemeDropdown.d.ts +86 -0
- package/lib/Components/ThemeDropdown.d.ts.map +1 -0
- package/lib/Components/ThemeDropdown.js +207 -0
- package/lib/Components/Toast.d.ts +94 -0
- package/lib/Components/Toast.d.ts.map +1 -0
- package/lib/Components/Toast.js +284 -0
- package/lib/Components/Toaster.d.ts +119 -0
- package/lib/Components/Toaster.d.ts.map +1 -0
- package/lib/Components/Toaster.js +153 -0
- package/lib/Components/TypeAhead.d.ts +53 -0
- package/lib/Components/TypeAhead.d.ts.map +1 -0
- package/lib/Components/TypeAhead.js +138 -0
- package/lib/Context.d.ts +38 -0
- package/lib/Context.d.ts.map +1 -0
- package/lib/Context.js +42 -0
- package/lib/DialogResult.d.ts +30 -0
- package/lib/DialogResult.d.ts.map +1 -0
- package/lib/DialogResult.js +29 -0
- package/lib/File.d.ts +25 -0
- package/lib/File.d.ts.map +1 -0
- package/lib/File.js +66 -0
- package/lib/Form.d.ts +33 -0
- package/lib/Form.d.ts.map +1 -0
- package/lib/Form.js +50 -0
- package/lib/Htmx.d.ts +12 -0
- package/lib/Htmx.d.ts.map +1 -0
- package/lib/Htmx.js +2 -0
- package/lib/KeyboardModifiers.d.ts +26 -0
- package/lib/KeyboardModifiers.d.ts.map +1 -0
- package/lib/KeyboardModifiers.js +25 -0
- package/lib/Position.d.ts +52 -0
- package/lib/Position.d.ts.map +1 -0
- package/lib/Position.js +59 -0
- package/lib/Size.d.ts +40 -0
- package/lib/Size.d.ts.map +1 -0
- package/lib/Size.js +44 -0
- package/lib/StorageArea.d.ts +18 -0
- package/lib/StorageArea.d.ts.map +1 -0
- package/lib/StorageArea.js +13 -0
- package/lib/Tags.d.ts +15 -0
- package/lib/Tags.d.ts.map +1 -0
- package/lib/Tags.js +48 -0
- package/lib/Variant.d.ts +36 -0
- package/lib/Variant.d.ts.map +1 -0
- package/lib/Variant.js +31 -0
- package/lib/ViewportScroller.d.ts +49 -0
- package/lib/ViewportScroller.d.ts.map +1 -0
- package/lib/ViewportScroller.js +69 -0
- package/package.json +58 -0
- package/src/Client/Alignment.ts +25 -0
- package/src/Client/AppTheme.ts +51 -0
- package/src/Client/Components/BackButton.ts +45 -0
- package/src/Client/Components/DialogBox.ts +344 -0
- package/src/Client/Components/FullScreenToggler.ts +122 -0
- package/src/Client/Components/KeyboardAccelerator.ts +97 -0
- package/src/Client/Components/LoadingIndicator.ts +113 -0
- package/src/Client/Components/MenuActivator.ts +58 -0
- package/src/Client/Components/OfflineIndicator.ts +125 -0
- package/src/Client/Components/TabActivator.ts +93 -0
- package/src/Client/Components/ThemeDropdown.ts +235 -0
- package/src/Client/Components/Toast.ts +319 -0
- package/src/Client/Components/Toaster.ts +224 -0
- package/src/Client/Components/TypeAhead.ts +153 -0
- package/src/Client/Context.ts +53 -0
- package/src/Client/DialogResult.ts +35 -0
- package/src/Client/File.ts +73 -0
- package/src/Client/Form.ts +55 -0
- package/src/Client/Htmx.ts +13 -0
- package/src/Client/KeyboardModifiers.ts +30 -0
- package/src/Client/Position.ts +74 -0
- package/src/Client/Size.ts +56 -0
- package/src/Client/StorageArea.ts +20 -0
- package/src/Client/Tags.ts +58 -0
- package/src/Client/Variant.ts +42 -0
- package/src/Client/ViewportScroller.ts +89 -0
- package/src/Client/tsconfig.json +19 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import {Toast as BootstrapToast} from "bootstrap";
|
|
2
|
+
import {Context, cssClass, icon} from "../Context.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Represents a notification.
|
|
6
|
+
*/
|
|
7
|
+
export class Toast extends HTMLElement {
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The list of observed attributes.
|
|
11
|
+
*/
|
|
12
|
+
static readonly observedAttributes = ["autohide", "caption", "context", "culture", "delay", "fade", "icon"];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The time units.
|
|
16
|
+
*/
|
|
17
|
+
static readonly #timeUnits: Intl.RelativeTimeFormatUnit[] = ["second", "minute", "hour"];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The abort controller used to remove the event listeners.
|
|
21
|
+
*/
|
|
22
|
+
#abortController: AbortController|null = null;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The formatter used to format the relative time.
|
|
26
|
+
*/
|
|
27
|
+
#formatter!: Intl.RelativeTimeFormat;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The time at which this component was initially shown.
|
|
31
|
+
*/
|
|
32
|
+
#initialTime = Date.now();
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The timer identifier.
|
|
36
|
+
*/
|
|
37
|
+
#timer = 0;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The underlying Bootstrap toast.
|
|
41
|
+
*/
|
|
42
|
+
#toast!: BootstrapToast;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Registers the component.
|
|
46
|
+
*/
|
|
47
|
+
static {
|
|
48
|
+
customElements.define("toaster-item", this);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Value indicating whether to automatically hide this toast.
|
|
53
|
+
*/
|
|
54
|
+
get autoHide(): boolean {
|
|
55
|
+
return this.hasAttribute("autohide");
|
|
56
|
+
}
|
|
57
|
+
set autoHide(value: boolean) {
|
|
58
|
+
this.toggleAttribute("autohide", value);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The child content displayed in the body.
|
|
63
|
+
*/
|
|
64
|
+
set body(value: DocumentFragment) { // eslint-disable-line accessor-pairs
|
|
65
|
+
this.querySelector(".toast-body")!.replaceChildren(...value.childNodes);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The title displayed in the header.
|
|
70
|
+
*/
|
|
71
|
+
get caption(): string {
|
|
72
|
+
return (this.getAttribute("caption") ?? "").trim();
|
|
73
|
+
}
|
|
74
|
+
set caption(value: string) {
|
|
75
|
+
this.setAttribute("caption", value);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A contextual modifier.
|
|
80
|
+
*/
|
|
81
|
+
get context(): Context {
|
|
82
|
+
const value = this.getAttribute("context") as Context;
|
|
83
|
+
return Object.values(Context).includes(value) ? value : Context.Info;
|
|
84
|
+
}
|
|
85
|
+
set context(value: Context) {
|
|
86
|
+
this.setAttribute("context", value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The culture used to format the relative time.
|
|
91
|
+
*/
|
|
92
|
+
get culture(): Intl.Locale {
|
|
93
|
+
const value = this.getAttribute("culture") ?? "";
|
|
94
|
+
return new Intl.Locale(value.trim() || navigator.language);
|
|
95
|
+
}
|
|
96
|
+
set culture(value: Intl.Locale) {
|
|
97
|
+
this.setAttribute("culture", value.toString());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The delay, in milliseconds, to hide this toast.
|
|
102
|
+
*/
|
|
103
|
+
get delay(): number {
|
|
104
|
+
const value = Number(this.getAttribute("delay"));
|
|
105
|
+
return Math.max(0, Number.isNaN(value) ? 5_000 : value);
|
|
106
|
+
}
|
|
107
|
+
set delay(value: number) {
|
|
108
|
+
this.setAttribute("delay", value.toString());
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The time elapsed since this component was initially shown, in milliseconds.
|
|
113
|
+
*/
|
|
114
|
+
get elapsedTime(): number {
|
|
115
|
+
return Date.now() - this.#initialTime;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Value indicating whether to apply a transition.
|
|
120
|
+
*/
|
|
121
|
+
get fade(): boolean {
|
|
122
|
+
return this.hasAttribute("fade");
|
|
123
|
+
}
|
|
124
|
+
set fade(value: boolean) {
|
|
125
|
+
this.toggleAttribute("fade", value);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The icon displayed next to the caption.
|
|
130
|
+
*/
|
|
131
|
+
get icon(): string|null {
|
|
132
|
+
const value = this.getAttribute("icon") ?? "";
|
|
133
|
+
return value.trim() || null;
|
|
134
|
+
}
|
|
135
|
+
set icon(value: string|null) {
|
|
136
|
+
if (value) this.setAttribute("icon", value);
|
|
137
|
+
else this.removeAttribute("icon");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Value indicating whether to initially show this component.
|
|
142
|
+
*/
|
|
143
|
+
get open(): boolean {
|
|
144
|
+
return this.hasAttribute("open");
|
|
145
|
+
}
|
|
146
|
+
set open(value: boolean) {
|
|
147
|
+
this.toggleAttribute("open", value);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Method invoked when an attribute has been changed.
|
|
152
|
+
* @param attribute The attribute name.
|
|
153
|
+
* @param oldValue The previous attribute value.
|
|
154
|
+
* @param newValue The new attribute value.
|
|
155
|
+
*/
|
|
156
|
+
attributeChangedCallback(attribute: string, oldValue: string|null, newValue: string|null): void {
|
|
157
|
+
if (newValue != oldValue) switch (attribute) {
|
|
158
|
+
case "autohide": this.#updateAutoHide(newValue != null); break;
|
|
159
|
+
case "caption": this.#updateCaption(newValue ?? ""); break;
|
|
160
|
+
case "context": this.#updateContext(Object.values(Context).includes(newValue as Context) ? newValue as Context : Context.Info); break;
|
|
161
|
+
case "culture": this.#formatter = new Intl.RelativeTimeFormat((newValue ?? "").trim() || navigator.language, {style: "long"}); break;
|
|
162
|
+
case "delay": this.#updateDelay(Number(newValue)); break;
|
|
163
|
+
case "fade": this.#updateFade(newValue != null); break;
|
|
164
|
+
case "icon": this.#updateIcon(newValue); break;
|
|
165
|
+
// No default
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Closes this toast.
|
|
171
|
+
*/
|
|
172
|
+
close(): void {
|
|
173
|
+
this.#toast.hide();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Method invoked when this component is connected.
|
|
178
|
+
*/
|
|
179
|
+
connectedCallback(): void {
|
|
180
|
+
this.#abortController = new AbortController;
|
|
181
|
+
|
|
182
|
+
const root = this.firstElementChild!;
|
|
183
|
+
root.addEventListener("hide.bs.toast", () => this.#stopTimer(), {signal: this.#abortController.signal});
|
|
184
|
+
root.addEventListener("show.bs.toast", () => this.#startTimer(), {signal: this.#abortController.signal});
|
|
185
|
+
|
|
186
|
+
this.#toast = new BootstrapToast(root);
|
|
187
|
+
if (this.open) this.show();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Method invoked when this component is disconnected.
|
|
192
|
+
*/
|
|
193
|
+
disconnectedCallback(): void {
|
|
194
|
+
this.#stopTimer();
|
|
195
|
+
this.#abortController?.abort();
|
|
196
|
+
this.#toast.dispose();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Shows this toast.
|
|
201
|
+
*/
|
|
202
|
+
show(): void {
|
|
203
|
+
if (!this.#toast.isShown()) {
|
|
204
|
+
this.#initialTime = Date.now();
|
|
205
|
+
this.#updateElapsedTime();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
this.#toast.show();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Formats the specified elapsed time.
|
|
213
|
+
* @param elapsed The elapsed time, in seconds.
|
|
214
|
+
* @returns The formated time.
|
|
215
|
+
*/
|
|
216
|
+
#formatTime(elapsed: number): string {
|
|
217
|
+
let index = 0;
|
|
218
|
+
while (elapsed > 60 && index < Toast.#timeUnits.length) {
|
|
219
|
+
elapsed /= 60;
|
|
220
|
+
index++;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return this.#formatter.format(Math.ceil(-elapsed), Toast.#timeUnits[index]);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Starts the timer.
|
|
228
|
+
*/
|
|
229
|
+
#startTimer(): void {
|
|
230
|
+
this.#timer = window.setInterval(() => this.#updateElapsedTime(), 1_000);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Stops the timer.
|
|
235
|
+
*/
|
|
236
|
+
#stopTimer(): void {
|
|
237
|
+
clearInterval(this.#timer);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Updates the value indicating whether to automatically hide this toast.
|
|
242
|
+
* @param value The new value.
|
|
243
|
+
*/
|
|
244
|
+
#updateAutoHide(value: boolean): void {
|
|
245
|
+
(this.firstElementChild! as HTMLElement).dataset.bsAutohide = value ? "true" : "false";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Updates the title displayed in the header.
|
|
250
|
+
* @param value The new value.
|
|
251
|
+
*/
|
|
252
|
+
#updateCaption(value: string): void {
|
|
253
|
+
this.querySelector(".toast-header b")!.textContent = value.trim();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Updates the contextual modifier.
|
|
258
|
+
* @param value The new value.
|
|
259
|
+
*/
|
|
260
|
+
#updateContext(value: Context): void {
|
|
261
|
+
const contexts = Object.values(Context);
|
|
262
|
+
|
|
263
|
+
let {classList} = this.querySelector(".toast-header")!;
|
|
264
|
+
classList.remove(...contexts.map(context => `toast-header-${cssClass(context)}`));
|
|
265
|
+
classList.add(`toast-header-${cssClass(value)}`);
|
|
266
|
+
|
|
267
|
+
({classList} = this.querySelector(".toast-header .icon")!);
|
|
268
|
+
classList.remove(...contexts.map(context => `text-${cssClass(context)}`));
|
|
269
|
+
classList.add(`text-${cssClass(value)}`);
|
|
270
|
+
|
|
271
|
+
if (!this.icon) this.#updateIcon(icon(value));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Updates the delay to hide the toast.
|
|
276
|
+
* @param value The new value.
|
|
277
|
+
*/
|
|
278
|
+
#updateDelay(value: number): void {
|
|
279
|
+
const delay = Math.max(0, Number.isNaN(value) ? 5_000 : value);
|
|
280
|
+
(this.firstElementChild! as HTMLElement).dataset.bsDelay = delay.toString();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Updates the label corresponding to the elapsed time.
|
|
285
|
+
*/
|
|
286
|
+
#updateElapsedTime(): void {
|
|
287
|
+
const {elapsedTime} = this;
|
|
288
|
+
this.querySelector(".toast-header small")!.textContent = elapsedTime > 0 ? this.#formatTime(elapsedTime / 1_000) : "";
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Updates the value indicating whether to apply a transition.
|
|
293
|
+
* @param value The new value.
|
|
294
|
+
*/
|
|
295
|
+
#updateFade(value: boolean): void {
|
|
296
|
+
(this.firstElementChild! as HTMLElement).dataset.bsAnimation = value ? "true" : "false";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Updates the icon displayed next to the caption.
|
|
301
|
+
* @param value The new value.
|
|
302
|
+
*/
|
|
303
|
+
#updateIcon(value: string|null): void {
|
|
304
|
+
this.querySelector(".toast-header .icon")!.textContent = (value ?? "").trim() || icon(this.context);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Declaration merging.
|
|
310
|
+
*/
|
|
311
|
+
declare global {
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* The map of HTML tag names.
|
|
315
|
+
*/
|
|
316
|
+
interface HTMLElementTagNameMap {
|
|
317
|
+
"toaster-item": Toast;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import {Context} from "../Context.js";
|
|
2
|
+
import {Position, cssClass} from "../Position.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Represents a notification.
|
|
6
|
+
*/
|
|
7
|
+
export interface IToast {
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Value indicating whether to automatically hide the toast.
|
|
11
|
+
*/
|
|
12
|
+
autoHide?: boolean;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The child content displayed in the body.
|
|
16
|
+
*/
|
|
17
|
+
body: DocumentFragment;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The title displayed in the header.
|
|
21
|
+
*/
|
|
22
|
+
caption: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The default contextual modifier.
|
|
26
|
+
*/
|
|
27
|
+
context?: Context;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The culture used to format the relative time.
|
|
31
|
+
*/
|
|
32
|
+
culture?: Intl.Locale;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The delay, in milliseconds, to hide the toast.
|
|
36
|
+
*/
|
|
37
|
+
delay?: number;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Value indicating whether to apply a transition.
|
|
41
|
+
*/
|
|
42
|
+
fade?: boolean;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The icon displayed next to the caption.
|
|
46
|
+
*/
|
|
47
|
+
icon?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Manages the notifications.
|
|
52
|
+
*/
|
|
53
|
+
export class Toaster extends HTMLElement {
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The list of observed attributes.
|
|
57
|
+
*/
|
|
58
|
+
static readonly observedAttributes = ["position"];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The template for a toast.
|
|
62
|
+
*/
|
|
63
|
+
readonly #toastTemplate = this.querySelector("template")!.content;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Creates a new toaster.
|
|
67
|
+
*/
|
|
68
|
+
constructor() {
|
|
69
|
+
super();
|
|
70
|
+
for (const toast of this.querySelectorAll("toaster-item")) toast.addEventListener("hidden.bs.toast", () => toast.remove());
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Registers the component.
|
|
75
|
+
*/
|
|
76
|
+
static {
|
|
77
|
+
customElements.define("toaster-container", this);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Value indicating whether to automatically hide the toasts.
|
|
82
|
+
*/
|
|
83
|
+
get autoHide(): boolean {
|
|
84
|
+
return this.hasAttribute("autohide");
|
|
85
|
+
}
|
|
86
|
+
set autoHide(value: boolean) {
|
|
87
|
+
this.toggleAttribute("autohide", value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The default contextual modifier.
|
|
92
|
+
*/
|
|
93
|
+
get context(): Context {
|
|
94
|
+
const value = this.getAttribute("context") as Context;
|
|
95
|
+
return Object.values(Context).includes(value) ? value : Context.Info;
|
|
96
|
+
}
|
|
97
|
+
set context(value: Context) {
|
|
98
|
+
this.setAttribute("context", value);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The default culture used to format the relative times.
|
|
103
|
+
*/
|
|
104
|
+
get culture(): Intl.Locale {
|
|
105
|
+
const value = this.getAttribute("culture") ?? "";
|
|
106
|
+
return new Intl.Locale(value.trim() || navigator.language);
|
|
107
|
+
}
|
|
108
|
+
set culture(value: Intl.Locale) {
|
|
109
|
+
this.setAttribute("culture", value.toString());
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The default delay, in milliseconds, to hide the toasts.
|
|
114
|
+
*/
|
|
115
|
+
get delay(): number {
|
|
116
|
+
const value = Number(this.getAttribute("delay"));
|
|
117
|
+
return Math.max(0, Number.isNaN(value) ? 5_000 : value);
|
|
118
|
+
}
|
|
119
|
+
set delay(value: number) {
|
|
120
|
+
this.setAttribute("delay", value.toString());
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Value indicating whether to apply a transition.
|
|
125
|
+
*/
|
|
126
|
+
get fade(): boolean {
|
|
127
|
+
return this.hasAttribute("fade");
|
|
128
|
+
}
|
|
129
|
+
set fade(value: boolean) {
|
|
130
|
+
this.toggleAttribute("fade", value);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The default icon displayed next to the captions.
|
|
135
|
+
*/
|
|
136
|
+
get icon(): string|null {
|
|
137
|
+
const value = this.getAttribute("icon") ?? "";
|
|
138
|
+
return value.trim() || null;
|
|
139
|
+
}
|
|
140
|
+
set icon(value: string|null) {
|
|
141
|
+
if (value) this.setAttribute("icon", value);
|
|
142
|
+
else this.removeAttribute("icon");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* The toaster placement.
|
|
147
|
+
*/
|
|
148
|
+
get position(): Position {
|
|
149
|
+
const value = this.getAttribute("position") as Position;
|
|
150
|
+
return Object.values(Position).includes(value) ? value : Position.BottomEnd;
|
|
151
|
+
}
|
|
152
|
+
set position(value: Position) {
|
|
153
|
+
this.setAttribute("position", value);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Method invoked when an attribute has been changed.
|
|
158
|
+
* @param attribute The attribute name.
|
|
159
|
+
* @param oldValue The previous attribute value.
|
|
160
|
+
* @param newValue The new attribute value.
|
|
161
|
+
*/
|
|
162
|
+
attributeChangedCallback(attribute: string, oldValue: string|null, newValue: string|null): void {
|
|
163
|
+
if (newValue != oldValue) switch (attribute) {
|
|
164
|
+
case "position": this.#updatePosition(Object.values(Position).includes(newValue as Position) ? newValue as Position : Position.BottomEnd); break;
|
|
165
|
+
// No default
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Shows a toast.
|
|
171
|
+
* @param context The contextual modifier.
|
|
172
|
+
* @param caption The title displayed in the toast header.
|
|
173
|
+
* @param message The child content displayed in the toast body.
|
|
174
|
+
*/
|
|
175
|
+
notify(context: Context, caption: string, message: DocumentFragment): void {
|
|
176
|
+
return this.show({body: message, caption, context});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Shows a toast.
|
|
181
|
+
* @param toast The toast to show.
|
|
182
|
+
*/
|
|
183
|
+
show(toast: IToast): void {
|
|
184
|
+
const item = document.createElement("toaster-item");
|
|
185
|
+
const childContent = (this.#toastTemplate.cloneNode(true) as DocumentFragment).querySelector(".toast")!;
|
|
186
|
+
childContent.addEventListener("hidden.bs.toast", () => item.remove());
|
|
187
|
+
item.appendChild(childContent);
|
|
188
|
+
|
|
189
|
+
item.autoHide = toast.autoHide ?? this.autoHide;
|
|
190
|
+
item.body = toast.body;
|
|
191
|
+
item.caption = toast.caption;
|
|
192
|
+
item.context = toast.context ?? this.context;
|
|
193
|
+
item.culture = toast.culture ?? this.culture;
|
|
194
|
+
item.delay = toast.delay ?? this.delay;
|
|
195
|
+
item.fade = toast.fade ?? this.fade;
|
|
196
|
+
item.icon = toast.icon ?? this.icon;
|
|
197
|
+
|
|
198
|
+
this.firstElementChild!.appendChild(item);
|
|
199
|
+
item.show();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Updates the toaster placement.
|
|
204
|
+
* @param value The new value.
|
|
205
|
+
*/
|
|
206
|
+
#updatePosition(value: Position): void {
|
|
207
|
+
const {classList} = this.firstElementChild!;
|
|
208
|
+
classList.remove(...Object.values(Position).flatMap(position => cssClass(position).split(" ")));
|
|
209
|
+
classList.add(...cssClass(value).split(" "));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Declaration merging.
|
|
215
|
+
*/
|
|
216
|
+
declare global {
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* The map of HTML tag names.
|
|
220
|
+
*/
|
|
221
|
+
interface HTMLElementTagNameMap {
|
|
222
|
+
"toaster-container": Toaster;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A dynamic list of suggested values for an associated control.
|
|
3
|
+
*/
|
|
4
|
+
export class TypeAhead extends HTMLElement {
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The list of observed attributes.
|
|
8
|
+
*/
|
|
9
|
+
static readonly observedAttributes = ["list", "query"];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The debounced handler used to look up the query.
|
|
13
|
+
*/
|
|
14
|
+
#debounced: (value: string) => void = () => { /* Noop */ };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The previous query.
|
|
18
|
+
*/
|
|
19
|
+
#previousQuery = "";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Registers the component.
|
|
23
|
+
*/
|
|
24
|
+
static {
|
|
25
|
+
customElements.define("type-ahead", this);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The delay in milliseconds to wait before triggering autocomplete suggestions.
|
|
30
|
+
*/
|
|
31
|
+
get delay(): number {
|
|
32
|
+
const value = Number(this.getAttribute("delay"));
|
|
33
|
+
return Math.max(0, Number.isNaN(value) ? 300 : value);
|
|
34
|
+
}
|
|
35
|
+
set delay(value: number) {
|
|
36
|
+
this.setAttribute("delay", value.toString());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The function invoked when the query has been changed.
|
|
41
|
+
*/
|
|
42
|
+
set handler(callback: (query: string) => Promise<string[]>) { // eslint-disable-line accessor-pairs
|
|
43
|
+
this.#debounced = this.#debounce(async query => { // eslint-disable-line @typescript-eslint/no-misused-promises
|
|
44
|
+
try { this.#updateItems(await callback(query)); }
|
|
45
|
+
catch { this.#updateItems([]); }
|
|
46
|
+
}, this.delay);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The data list identifier.
|
|
51
|
+
*/
|
|
52
|
+
get list(): string {
|
|
53
|
+
return (this.getAttribute("list") ?? "").trim();
|
|
54
|
+
}
|
|
55
|
+
set list(value: string) {
|
|
56
|
+
this.setAttribute("list", value);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The minimum character length needed before triggering autocomplete suggestions.
|
|
61
|
+
*/
|
|
62
|
+
get minLength(): number {
|
|
63
|
+
const value = Number(this.getAttribute("minlength"));
|
|
64
|
+
return Math.max(1, Number.isNaN(value) ? 1 : value);
|
|
65
|
+
}
|
|
66
|
+
set minLength(value: number) {
|
|
67
|
+
this.setAttribute("minlength", value.toString());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The query to look up.
|
|
72
|
+
*/
|
|
73
|
+
get query(): string {
|
|
74
|
+
return (this.getAttribute("query") ?? "").trim();
|
|
75
|
+
}
|
|
76
|
+
set query(value: string) {
|
|
77
|
+
this.setAttribute("query", value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Method invoked when an attribute has been changed.
|
|
82
|
+
* @param attribute The attribute name.
|
|
83
|
+
* @param oldValue The previous attribute value.
|
|
84
|
+
* @param newValue The new attribute value.
|
|
85
|
+
*/
|
|
86
|
+
attributeChangedCallback(attribute: string, oldValue: string|null, newValue: string|null): void {
|
|
87
|
+
if (newValue != oldValue) switch (attribute) {
|
|
88
|
+
case "list": this.#updateList(newValue ?? ""); break;
|
|
89
|
+
case "query": this.#updateQuery(newValue ?? ""); break;
|
|
90
|
+
// No default
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Postpones the invocation of the specified callback until after a given delay has elapsed since the last time it was invoked.
|
|
96
|
+
* @param callback The function to debounce.
|
|
97
|
+
* @param delay The delay to wait, in milliseconds.
|
|
98
|
+
* @returns The debounced function.
|
|
99
|
+
*/
|
|
100
|
+
#debounce(callback: (value: string) => void, delay: number): (value: string) => void {
|
|
101
|
+
let timer = 0;
|
|
102
|
+
return value => {
|
|
103
|
+
if (timer) clearTimeout(timer);
|
|
104
|
+
timer = window.setTimeout(callback, delay, value);
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Updates the identifier of the underlying data list.
|
|
110
|
+
* @param value The new value.
|
|
111
|
+
*/
|
|
112
|
+
#updateList(value: string): void {
|
|
113
|
+
this.firstElementChild!.id = value.trim();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Updates the query to look up.
|
|
118
|
+
* @param value The new value.
|
|
119
|
+
*/
|
|
120
|
+
#updateQuery(value: string): void {
|
|
121
|
+
const query = value.trim();
|
|
122
|
+
if (query != this.#previousQuery) {
|
|
123
|
+
this.#previousQuery = query;
|
|
124
|
+
if (query.length < this.minLength) this.#updateItems([]);
|
|
125
|
+
else this.#debounced(query);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Updates the items of the underlying data list.
|
|
131
|
+
* @param values The new values.
|
|
132
|
+
*/
|
|
133
|
+
#updateItems(values: string[]): void {
|
|
134
|
+
this.firstElementChild!.replaceChildren(...values.map(value => {
|
|
135
|
+
const option = document.createElement("option");
|
|
136
|
+
option.value = value;
|
|
137
|
+
return option;
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Declaration merging.
|
|
144
|
+
*/
|
|
145
|
+
declare global {
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The map of HTML tag names.
|
|
149
|
+
*/
|
|
150
|
+
interface HTMLElementTagNameMap {
|
|
151
|
+
"type-ahead": TypeAhead;
|
|
152
|
+
}
|
|
153
|
+
}
|