@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,344 @@
|
|
|
1
|
+
import {Modal} from "bootstrap";
|
|
2
|
+
import {type Context, cssClass as contextCssClass, icon as contextIcon} from "../Context.js";
|
|
3
|
+
import {DialogResult} from "../DialogResult.js";
|
|
4
|
+
import {html} from "../Tags.js";
|
|
5
|
+
import {Variant, cssClass as variantCssClass} from "../Variant.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Represents a dialog box button.
|
|
9
|
+
*/
|
|
10
|
+
export interface IDialogButton {
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The button icon.
|
|
14
|
+
*/
|
|
15
|
+
icon?: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The button label.
|
|
19
|
+
*/
|
|
20
|
+
label?: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The button value.
|
|
24
|
+
*/
|
|
25
|
+
value?: string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A tone variant.
|
|
29
|
+
*/
|
|
30
|
+
variant?: Variant;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Represents a message to display in a dialog box.
|
|
35
|
+
*/
|
|
36
|
+
export interface IDialogMessage {
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The child content displayed in the body.
|
|
40
|
+
*/
|
|
41
|
+
body: DocumentFragment;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The title displayed in the header.
|
|
45
|
+
*/
|
|
46
|
+
caption: string;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The child content displayed in the footer.
|
|
50
|
+
*/
|
|
51
|
+
footer?: DocumentFragment;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Displays a dialog box, which presents a message to the user.
|
|
56
|
+
*/
|
|
57
|
+
export class DialogBox extends HTMLElement {
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The list of observed attributes.
|
|
61
|
+
*/
|
|
62
|
+
static readonly observedAttributes = ["caption", "centered", "fade", "modal", "scrollable"];
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The underlying Bootstrap modal.
|
|
66
|
+
*/
|
|
67
|
+
#modal!: Modal;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The function invoked to resolve the dialog result.
|
|
71
|
+
*/
|
|
72
|
+
#resolve!: (value: string) => void;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The promise providing the dialog result.
|
|
76
|
+
*/
|
|
77
|
+
#result: string = DialogResult.None;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Creates a new dialog box.
|
|
81
|
+
*/
|
|
82
|
+
constructor() {
|
|
83
|
+
super();
|
|
84
|
+
this.firstElementChild!.addEventListener("hide.bs.modal", () => this.#resolve(this.#result));
|
|
85
|
+
this.querySelector(".btn-close")!.addEventListener("click", this.#close);
|
|
86
|
+
for (const button of this.querySelectorAll(".modal-footer button")) button.addEventListener("click", this.#close);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Registers the component.
|
|
91
|
+
*/
|
|
92
|
+
static {
|
|
93
|
+
customElements.define("dialog-box", this);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The child content displayed in the body.
|
|
98
|
+
*/
|
|
99
|
+
set body(value: DocumentFragment) { // eslint-disable-line accessor-pairs
|
|
100
|
+
this.querySelector(".modal-body")!.replaceChildren(...value.childNodes);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The title displayed in the header.
|
|
105
|
+
*/
|
|
106
|
+
get caption(): string {
|
|
107
|
+
return (this.getAttribute("caption") ?? "").trim();
|
|
108
|
+
}
|
|
109
|
+
set caption(value: string) {
|
|
110
|
+
this.setAttribute("caption", value);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Value indicating whether to vertically center this dialog box.
|
|
115
|
+
*/
|
|
116
|
+
get centered(): boolean {
|
|
117
|
+
return this.hasAttribute("centered");
|
|
118
|
+
}
|
|
119
|
+
set centered(value: boolean) {
|
|
120
|
+
this.toggleAttribute("centered", value);
|
|
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 child content displayed in the footer.
|
|
135
|
+
*/
|
|
136
|
+
set footer(value: DocumentFragment) { // eslint-disable-line accessor-pairs
|
|
137
|
+
const footer = this.querySelector<HTMLElement>(".modal-footer")!;
|
|
138
|
+
footer.hidden = !value.hasChildNodes();
|
|
139
|
+
footer.replaceChildren(...value.childNodes);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Value indicating whether to this dialog box will not close when clicking outside of it.
|
|
144
|
+
*/
|
|
145
|
+
get modal(): boolean {
|
|
146
|
+
return this.hasAttribute("modal");
|
|
147
|
+
}
|
|
148
|
+
set modal(value: boolean) {
|
|
149
|
+
this.toggleAttribute("modal", value);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Value indicating whether to initially show this component.
|
|
154
|
+
*/
|
|
155
|
+
get open(): boolean {
|
|
156
|
+
return this.hasAttribute("open");
|
|
157
|
+
}
|
|
158
|
+
set open(value: boolean) {
|
|
159
|
+
this.toggleAttribute("open", value);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The dialog result.
|
|
164
|
+
*/
|
|
165
|
+
get result(): string {
|
|
166
|
+
return this.#result;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Value indicating whether the body is scrollable.
|
|
171
|
+
*/
|
|
172
|
+
get scrollable(): boolean {
|
|
173
|
+
return this.hasAttribute("scrollable");
|
|
174
|
+
}
|
|
175
|
+
set scrollable(value: boolean) {
|
|
176
|
+
this.toggleAttribute("scrollable", value);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Method invoked when an attribute has been changed.
|
|
181
|
+
* @param attribute The attribute name.
|
|
182
|
+
* @param oldValue The previous attribute value.
|
|
183
|
+
* @param newValue The new attribute value.
|
|
184
|
+
*/
|
|
185
|
+
attributeChangedCallback(attribute: string, oldValue: string|null, newValue: string|null): void {
|
|
186
|
+
if (newValue != oldValue) switch (attribute) {
|
|
187
|
+
case "caption": this.#updateCaption(newValue ?? ""); break;
|
|
188
|
+
case "centered": this.#updateCentered(newValue != null); break;
|
|
189
|
+
case "fade": this.#updateFade(newValue != null); break;
|
|
190
|
+
case "modal": this.#updateModal(newValue != null); break;
|
|
191
|
+
case "scrollable": this.#updateScrollable(newValue != null); break;
|
|
192
|
+
// No default
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Shows an alert message with an "OK" button by default.
|
|
198
|
+
* @param context The contextual modifier.
|
|
199
|
+
* @param caption The title displayed in the header.
|
|
200
|
+
* @param message The child content displayed in the body.
|
|
201
|
+
* @param buttons The buttons displayed in the footer.
|
|
202
|
+
* @returns The dialog result.
|
|
203
|
+
*/
|
|
204
|
+
alert(context: Context, caption: string, message: DocumentFragment, buttons: IDialogButton[] = []): Promise<string> {
|
|
205
|
+
return this.show({
|
|
206
|
+
caption,
|
|
207
|
+
body: html`
|
|
208
|
+
<div class="d-flex gap-2">
|
|
209
|
+
<i class="icon icon-fill fs-1 text-${contextCssClass(context)}"> ${contextIcon(context)}</i>
|
|
210
|
+
<div class="flex-grow-1">${message}</div>
|
|
211
|
+
</div>
|
|
212
|
+
`,
|
|
213
|
+
footer: html`
|
|
214
|
+
${(buttons.length ? buttons : [{label: "OK", value: DialogResult.OK, variant: Variant.Primary}]).map(button => html`
|
|
215
|
+
<button class="btn btn-${variantCssClass(button.variant ?? Variant.Primary)}" type="button" value="${button.value ?? DialogResult.None}">
|
|
216
|
+
${button.icon ? html`<i class="icon ${button.label ? "me-1" : ""}">${button.icon}</i>` : ""}
|
|
217
|
+
${button.label}
|
|
218
|
+
</button>
|
|
219
|
+
`)}
|
|
220
|
+
`
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Closes this dialog box.
|
|
226
|
+
* @param result The dialog result.
|
|
227
|
+
*/
|
|
228
|
+
close(result: string = DialogResult.None): void {
|
|
229
|
+
this.#result = result;
|
|
230
|
+
this.#modal.hide();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Shows a confirmation message with two buttons, "OK" and "Cancel".
|
|
235
|
+
* @param context The contextual modifier.
|
|
236
|
+
* @param caption The title displayed in the header.
|
|
237
|
+
* @param message The child content displayed in the body.
|
|
238
|
+
* @returns The dialog result.
|
|
239
|
+
*/
|
|
240
|
+
confirm(context: Context, caption: string, message: DocumentFragment): Promise<string> {
|
|
241
|
+
return this.alert(context, caption, message, [
|
|
242
|
+
{label: "OK", value: DialogResult.OK, variant: Variant.Primary},
|
|
243
|
+
{label: "Annuler", value: DialogResult.Cancel, variant: Variant.Secondary}
|
|
244
|
+
]);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Method invoked when this component is connected.
|
|
249
|
+
*/
|
|
250
|
+
connectedCallback(): void {
|
|
251
|
+
this.#modal = new Modal(this.firstElementChild!);
|
|
252
|
+
if (this.open) void this.show();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Method invoked when this component is disconnected.
|
|
257
|
+
*/
|
|
258
|
+
disconnectedCallback(): void {
|
|
259
|
+
this.#modal.dispose();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Shows a message.
|
|
264
|
+
* @param message The message to show.
|
|
265
|
+
* @returns The dialog result.
|
|
266
|
+
*/
|
|
267
|
+
show(message: IDialogMessage|null = null): Promise<string> {
|
|
268
|
+
if (message) {
|
|
269
|
+
const footer = message.footer ?? document.createDocumentFragment();
|
|
270
|
+
for (const button of footer.querySelectorAll("button")) button.addEventListener("click", this.#close);
|
|
271
|
+
this.body = message.body;
|
|
272
|
+
this.caption = message.caption;
|
|
273
|
+
this.footer = footer;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const {promise, resolve} = Promise.withResolvers<string>();
|
|
277
|
+
this.#resolve = resolve;
|
|
278
|
+
this.#result = DialogResult.None;
|
|
279
|
+
this.#modal.show();
|
|
280
|
+
return promise;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Closes this dialog box.
|
|
285
|
+
* @param event The dispatched event.
|
|
286
|
+
*/
|
|
287
|
+
readonly #close: (event: Event) => void = event => {
|
|
288
|
+
event.preventDefault();
|
|
289
|
+
this.close((event.currentTarget as HTMLButtonElement).value);
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Updates the title displayed in the header.
|
|
294
|
+
* @param value The new value.
|
|
295
|
+
*/
|
|
296
|
+
#updateCaption(value: string): void {
|
|
297
|
+
this.querySelector(".modal-title")!.textContent = value.trim();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Updates the value indicating whether to vertically center this dialog box.
|
|
302
|
+
* @param value The new value.
|
|
303
|
+
*/
|
|
304
|
+
#updateCentered(value: boolean): void {
|
|
305
|
+
this.querySelector(".modal-dialog")!.classList.toggle("modal-dialog-centered", value);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Updates the value indicating whether to apply a transition.
|
|
310
|
+
* @param value The new value.
|
|
311
|
+
*/
|
|
312
|
+
#updateFade(value: boolean): void {
|
|
313
|
+
this.firstElementChild!.classList.toggle("fade", value);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Updates the value indicating whether to this dialog box will not close when clicking outside of it.
|
|
318
|
+
* @param value The new value.
|
|
319
|
+
*/
|
|
320
|
+
#updateModal(value: boolean): void {
|
|
321
|
+
(this.firstElementChild! as HTMLElement).dataset.bsBackdrop = value ? "static" : "true";
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Updates the value indicating whether the body is scrollable.
|
|
326
|
+
* @param value The new value.
|
|
327
|
+
*/
|
|
328
|
+
#updateScrollable(value: boolean): void {
|
|
329
|
+
this.querySelector(".modal-dialog")!.classList.toggle("modal-dialog-scrollable", value);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Declaration merging.
|
|
335
|
+
*/
|
|
336
|
+
declare global {
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The map of HTML tag names.
|
|
340
|
+
*/
|
|
341
|
+
interface HTMLElementTagNameMap {
|
|
342
|
+
"dialog-box": DialogBox;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A component for toggling an element to full-screen.
|
|
3
|
+
*/
|
|
4
|
+
export class FullScreenToggler extends HTMLElement {
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The abort controller used to remove the event listeners.
|
|
8
|
+
*/
|
|
9
|
+
#abortController: AbortController|null = null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The target element.
|
|
13
|
+
*/
|
|
14
|
+
#element: Element = document.body;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The handle to the underlying platform wake lock.
|
|
18
|
+
*/
|
|
19
|
+
#sentinel: WakeLockSentinel|null = null;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Registers the component.
|
|
23
|
+
*/
|
|
24
|
+
static {
|
|
25
|
+
customElements.define("fullscreen-toggler", this);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The CSS selector specifying the target element.
|
|
30
|
+
*/
|
|
31
|
+
get target(): string {
|
|
32
|
+
const value = this.getAttribute("target") ?? "";
|
|
33
|
+
return value.trim() || "body";
|
|
34
|
+
}
|
|
35
|
+
set target(value: string) {
|
|
36
|
+
this.setAttribute("target", value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Value indicating whether to prevent the device screen from dimming or locking when in full-screen mode.
|
|
41
|
+
*/
|
|
42
|
+
get wakeLock(): boolean {
|
|
43
|
+
return this.hasAttribute("wakeLock");
|
|
44
|
+
}
|
|
45
|
+
set wakeLock(value: boolean) {
|
|
46
|
+
this.toggleAttribute("wakeLock", value);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Method invoked when this component is connected.
|
|
51
|
+
*/
|
|
52
|
+
connectedCallback(): void {
|
|
53
|
+
this.#abortController = new AbortController;
|
|
54
|
+
document.addEventListener("visibilitychange", () => this.#onVisibilityChanged(), {signal: this.#abortController.signal});
|
|
55
|
+
this.#element = document.querySelector(this.target) ?? document.body;
|
|
56
|
+
this.#element.addEventListener("fullscreenchange", () => this.#onFullScreenChanged(), {signal: this.#abortController.signal});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Method invoked when this component is disconnected.
|
|
61
|
+
*/
|
|
62
|
+
disconnectedCallback(): void {
|
|
63
|
+
this.#abortController?.abort();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Toggles the full-screen mode of the associated element.
|
|
68
|
+
* @param event The dispatched event.
|
|
69
|
+
* @returns Completes when the full-screen mode has been toggled.
|
|
70
|
+
*/
|
|
71
|
+
async toggleFullScreen(event?: Event): Promise<void> {
|
|
72
|
+
event?.preventDefault();
|
|
73
|
+
if (document.fullscreenElement) await document.exitFullscreen();
|
|
74
|
+
else await this.#element.requestFullscreen();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Acquires a new wake lock.
|
|
79
|
+
* @returns Completes when the wake lock has been acquired.
|
|
80
|
+
*/
|
|
81
|
+
async #acquireWakeLock(): Promise<void> {
|
|
82
|
+
if (this.wakeLock && (!this.#sentinel || this.#sentinel.released)) this.#sentinel = await navigator.wakeLock.request();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Acquires or releases the wake lock when the document enters or exits the full-screen mode.
|
|
87
|
+
* @param event The dispatched event.
|
|
88
|
+
*/
|
|
89
|
+
#onFullScreenChanged(): void {
|
|
90
|
+
if (document.fullscreenElement) void this.#acquireWakeLock();
|
|
91
|
+
else void this.#releaseWakeLock();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Eventually acquires a new wake lock when the document visibility has changed.
|
|
96
|
+
* @param event The dispatched event.
|
|
97
|
+
*/
|
|
98
|
+
#onVisibilityChanged(): void {
|
|
99
|
+
if (document.fullscreenElement && !document.hidden) void this.#acquireWakeLock();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Releases the acquired wake lock.
|
|
104
|
+
* @returns Completes when the wake lock has been released.
|
|
105
|
+
*/
|
|
106
|
+
async #releaseWakeLock(): Promise<void> {
|
|
107
|
+
if (this.#sentinel && !this.#sentinel.released) await this.#sentinel.release();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Declaration merging.
|
|
113
|
+
*/
|
|
114
|
+
declare global {
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The map of HTML tag names.
|
|
118
|
+
*/
|
|
119
|
+
interface HTMLElementTagNameMap {
|
|
120
|
+
"fullscreen-toggler": FullScreenToggler;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import {KeyboardModifiers} from "../KeyboardModifiers.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Associates a shortcut key with its child content.
|
|
5
|
+
*/
|
|
6
|
+
export class KeyboardAccelerator extends HTMLElement {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The mapping between the modifier names and their values.
|
|
10
|
+
*/
|
|
11
|
+
static readonly #modifiers = new Map(Object.entries(KeyboardModifiers).filter(([key]) => key != "None"));
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The abort controller used to remove the event listeners.
|
|
15
|
+
*/
|
|
16
|
+
#abortController: AbortController|null = null;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Registers the component.
|
|
20
|
+
*/
|
|
21
|
+
static {
|
|
22
|
+
customElements.define("keyboard-accelerator", this);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Identifies the key for the keyboard accelerator.
|
|
27
|
+
*/
|
|
28
|
+
get key(): string {
|
|
29
|
+
return (this.getAttribute("key") ?? "").trim();
|
|
30
|
+
}
|
|
31
|
+
set key(value: string) {
|
|
32
|
+
this.setAttribute("key", value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Identifies the modifiers for the keyboard accelerator.
|
|
37
|
+
*/
|
|
38
|
+
get modifiers(): number {
|
|
39
|
+
return (this.getAttribute("modifiers") ?? "").split(",")
|
|
40
|
+
.map(value => value.trim())
|
|
41
|
+
.reduce<number>((modifiers, value) => modifiers | (KeyboardAccelerator.#modifiers.get(value) ?? 0), KeyboardModifiers.None);
|
|
42
|
+
}
|
|
43
|
+
set modifiers(value: number) {
|
|
44
|
+
this.setAttribute("modifiers", !value ? "None" : KeyboardAccelerator.#modifiers.entries()
|
|
45
|
+
.filter(([, flag]) => (value & flag) != 0)
|
|
46
|
+
.map(([key]) => key)
|
|
47
|
+
.toArray().join(", "));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Method invoked when this component is connected.
|
|
52
|
+
*/
|
|
53
|
+
connectedCallback(): void {
|
|
54
|
+
this.#abortController = new AbortController;
|
|
55
|
+
addEventListener("keyup", event => this.#activateChildContent(event), {signal: this.#abortController.signal});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Method invoked when this component is disconnected.
|
|
60
|
+
*/
|
|
61
|
+
disconnectedCallback(): void {
|
|
62
|
+
this.#abortController?.abort();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Activates the child content when the specified keyboard event designates the same key and modifiers as this keyboard accelerator.
|
|
67
|
+
* @param event The dispatched event.
|
|
68
|
+
*/
|
|
69
|
+
#activateChildContent(event: KeyboardEvent): void {
|
|
70
|
+
if (event.key != this.key) return;
|
|
71
|
+
|
|
72
|
+
const {activeElement} = document;
|
|
73
|
+
if (activeElement instanceof HTMLInputElement || activeElement instanceof HTMLTextAreaElement) return;
|
|
74
|
+
|
|
75
|
+
const {modifiers} = this;
|
|
76
|
+
if (!(modifiers & KeyboardModifiers.Ctrl) && event.ctrlKey) return;
|
|
77
|
+
if (!(modifiers & KeyboardModifiers.Shift) && event.shiftKey) return;
|
|
78
|
+
if (!(modifiers & KeyboardModifiers.Alt) && event.altKey) return;
|
|
79
|
+
if (!(modifiers & KeyboardModifiers.Meta) && event.metaKey) return;
|
|
80
|
+
|
|
81
|
+
event.preventDefault();
|
|
82
|
+
(this.firstElementChild as HTMLElement|null)?.click();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Declaration merging.
|
|
88
|
+
*/
|
|
89
|
+
declare global {
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The map of HTML tag names.
|
|
93
|
+
*/
|
|
94
|
+
interface HTMLElementTagNameMap {
|
|
95
|
+
"keyboard-accelerator": KeyboardAccelerator;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A component that shows up when an HTTP request starts, and hides when all concurrent HTTP requests are completed.
|
|
3
|
+
*/
|
|
4
|
+
export class LoadingIndicator extends HTMLElement {
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The list of observed attributes.
|
|
8
|
+
*/
|
|
9
|
+
static readonly observedAttributes = ["fade"];
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The number of concurrent HTTP requests.
|
|
13
|
+
*/
|
|
14
|
+
#requestCount = 0;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Registers the component.
|
|
18
|
+
*/
|
|
19
|
+
static {
|
|
20
|
+
customElements.define("loading-indicator", this);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Value indicating whether to apply a transition.
|
|
25
|
+
*/
|
|
26
|
+
get fade(): boolean {
|
|
27
|
+
return this.hasAttribute("fade");
|
|
28
|
+
}
|
|
29
|
+
set fade(value: boolean) {
|
|
30
|
+
this.toggleAttribute("fade", value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Value indicating whether this component is shown.
|
|
35
|
+
*/
|
|
36
|
+
get isShown(): boolean {
|
|
37
|
+
return this.classList.contains("show");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Value indicating whether to initially show this component.
|
|
42
|
+
*/
|
|
43
|
+
get open(): boolean {
|
|
44
|
+
return this.hasAttribute("open");
|
|
45
|
+
}
|
|
46
|
+
set open(value: boolean) {
|
|
47
|
+
this.toggleAttribute("open", value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Method invoked when an attribute has been changed.
|
|
52
|
+
* @param attribute The attribute name.
|
|
53
|
+
* @param oldValue The previous attribute value.
|
|
54
|
+
* @param newValue The new attribute value.
|
|
55
|
+
*/
|
|
56
|
+
attributeChangedCallback(attribute: string, oldValue: string|null, newValue: string|null): void {
|
|
57
|
+
if (newValue != oldValue) switch (attribute) {
|
|
58
|
+
case "fade": this.#updateFade(newValue != null); break;
|
|
59
|
+
// No default
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Method invoked when this component is connected.
|
|
65
|
+
*/
|
|
66
|
+
connectedCallback(): void {
|
|
67
|
+
this.#requestCount = 0;
|
|
68
|
+
if (this.open) this.show();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Hides this loading indicator.
|
|
73
|
+
* @param options Value indicating whether to force the loading indicator to hide.
|
|
74
|
+
*/
|
|
75
|
+
hide(options: {force?: boolean} = {}): void {
|
|
76
|
+
this.#requestCount--;
|
|
77
|
+
if (this.#requestCount <= 0 || options.force) {
|
|
78
|
+
this.#requestCount = 0;
|
|
79
|
+
this.classList.add("hide");
|
|
80
|
+
this.classList.remove("show");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Shows this loading indicator.
|
|
86
|
+
*/
|
|
87
|
+
show(): void {
|
|
88
|
+
this.#requestCount++;
|
|
89
|
+
this.classList.remove("hide");
|
|
90
|
+
this.classList.add("show");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Updates the value indicating whether to apply a transition.
|
|
95
|
+
* @param value The new value.
|
|
96
|
+
*/
|
|
97
|
+
#updateFade(value: boolean): void {
|
|
98
|
+
this.classList.toggle("fade", value);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Declaration merging.
|
|
104
|
+
*/
|
|
105
|
+
declare global {
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* The map of HTML tag names.
|
|
109
|
+
*/
|
|
110
|
+
interface HTMLElementTagNameMap {
|
|
111
|
+
"loading-indicator": LoadingIndicator;
|
|
112
|
+
}
|
|
113
|
+
}
|