@cedx/base 0.11.0 → 0.12.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/ReadMe.md CHANGED
@@ -1,5 +1,5 @@
1
1
  # Belin.io Base
2
- ![.NET](https://badgen.net/badge/.net/%3E%3D9.0/green) ![Version](https://badgen.net/badge/project/v0.11.0/blue) ![Licence](https://badgen.net/badge/licence/MIT/blue)
2
+ ![.NET](https://badgen.net/badge/.net/%3E%3D9.0/green) ![Version](https://badgen.net/badge/project/v0.12.0/blue) ![Licence](https://badgen.net/badge/licence/MIT/blue)
3
3
 
4
4
  Base library by [Cédric Belin](https://belin.io), full stack developer,
5
5
  implemented in [C#](https://learn.microsoft.com/en-us/dotnet/csharp) and [TypeScript](https://www.typescriptlang.org).
@@ -1 +1 @@
1
- {"version":3,"file":"OfflineIndicator.d.ts","sourceRoot":"","sources":["../../../src/Client/UI/Components/OfflineIndicator.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,WAAW;;IAShD;;OAEG;IACH,iBAAiB,IAAI,IAAI;IAKzB;;OAEG;IACH,oBAAoB,IAAI,IAAI;CAS5B;AAED;;GAEG;AACH,OAAO,CAAC,MAAM,CAAC;IAEd;;OAEG;IACH,UAAU,qBAAqB;QAC9B,mBAAmB,EAAE,gBAAgB,CAAC;KACtC;CACD"}
1
+ {"version":3,"file":"OfflineIndicator.d.ts","sourceRoot":"","sources":["../../../src/Client/UI/Components/OfflineIndicator.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,WAAW;;IAShD;;OAEG;IACH,iBAAiB,IAAI,IAAI;IAKzB;;OAEG;IACH,oBAAoB,IAAI,IAAI;CAQ5B;AAED;;GAEG;AACH,OAAO,CAAC,MAAM,CAAC;IAEd;;OAEG;IACH,UAAU,qBAAqB;QAC9B,mBAAmB,EAAE,gBAAgB,CAAC;KACtC;CACD"}
@@ -12,19 +12,19 @@ export class OfflineIndicator extends HTMLElement {
12
12
  * Method invoked when this component is connected.
13
13
  */
14
14
  connectedCallback() {
15
- this.#updateHiddenState();
15
+ this.#update();
16
16
  for (const event of ["online", "offline"])
17
- addEventListener(event, this.#updateHiddenState);
17
+ addEventListener(event, this.#update);
18
18
  }
19
19
  /**
20
20
  * Method invoked when this component is disconnected.
21
21
  */
22
22
  disconnectedCallback() {
23
23
  for (const event of ["online", "offline"])
24
- removeEventListener(event, this.#updateHiddenState);
24
+ removeEventListener(event, this.#update);
25
25
  }
26
26
  /**
27
- * Updates the hidden state of this component according to the {@link navigator.onLine} property.
27
+ * Updates this component.
28
28
  */
29
- #updateHiddenState = () => this.hidden = navigator.onLine;
29
+ #update = () => this.hidden = navigator.onLine;
30
30
  }
@@ -25,7 +25,7 @@ export declare class TabActivator extends HTMLElement {
25
25
  /**
26
26
  * The tab list.
27
27
  */
28
- get tabs(): NodeListOf<HTMLButtonElement>;
28
+ get tabs(): NodeListOf<HTMLElement>;
29
29
  /**
30
30
  * Method invoked when this component is connected.
31
31
  */
@@ -1 +1 @@
1
- {"version":3,"file":"TabActivator.d.ts","sourceRoot":"","sources":["../../../src/Client/UI/Components/TabActivator.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,WAAW,EAAC,MAAM,mBAAmB,CAAC;AAE9C;;GAEG;AACH,qBAAa,YAAa,SAAQ,WAAW;IAS5C;;OAEG;IACH,IAAI,cAAc,IAAI,MAAM,CAG3B;IACD,IAAI,cAAc,CAAC,KAAK,EAAE,MAAM,EAE/B;IAED;;OAEG;IACH,IAAI,OAAO,IAAI,UAAU,CAAC,OAAO,CAEhC;IAED;;OAEG;IACH,IAAI,WAAW,IAAI,WAAW,CAG7B;IACD,IAAI,WAAW,CAAC,KAAK,EAAE,WAAW,EAEjC;IAED;;OAEG;IACH,IAAI,UAAU,IAAI,MAAM,CAGvB;IACD,IAAI,UAAU,CAAC,KAAK,EAAE,MAAM,EAE3B;IAED;;OAEG;IACH,IAAI,IAAI,IAAI,UAAU,CAAC,iBAAiB,CAAC,CAExC;IAED;;OAEG;IACH,iBAAiB,IAAI,IAAI;CAQzB;AAED;;GAEG;AACH,OAAO,CAAC,MAAM,CAAC;IAEd;;OAEG;IACH,UAAU,qBAAqB;QAC9B,eAAe,EAAE,YAAY,CAAC;KAC9B;CACD"}
1
+ {"version":3,"file":"TabActivator.d.ts","sourceRoot":"","sources":["../../../src/Client/UI/Components/TabActivator.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,WAAW,EAAC,MAAM,mBAAmB,CAAC;AAE9C;;GAEG;AACH,qBAAa,YAAa,SAAQ,WAAW;IAS5C;;OAEG;IACH,IAAI,cAAc,IAAI,MAAM,CAG3B;IACD,IAAI,cAAc,CAAC,KAAK,EAAE,MAAM,EAE/B;IAED;;OAEG;IACH,IAAI,OAAO,IAAI,UAAU,CAAC,OAAO,CAEhC;IAED;;OAEG;IACH,IAAI,WAAW,IAAI,WAAW,CAG7B;IACD,IAAI,WAAW,CAAC,KAAK,EAAE,WAAW,EAEjC;IAED;;OAEG;IACH,IAAI,UAAU,IAAI,MAAM,CAGvB;IACD,IAAI,UAAU,CAAC,KAAK,EAAE,MAAM,EAE3B;IAED;;OAEG;IACH,IAAI,IAAI,IAAI,UAAU,CAAC,WAAW,CAAC,CAElC;IAED;;OAEG;IACH,iBAAiB,IAAI,IAAI;CAQzB;AAED;;GAEG;AACH,OAAO,CAAC,MAAM,CAAC;IAEd;;OAEG;IACH,UAAU,qBAAqB;QAC9B,eAAe,EAAE,YAAY,CAAC;KAC9B;CACD"}
@@ -40,6 +40,10 @@ export declare class ThemeDropdown extends HTMLElement {
40
40
  * @param newValue The new attribute value.
41
41
  */
42
42
  attributeChangedCallback(attribute: string, oldValue: string | null, newValue: string | null): void;
43
+ /**
44
+ * Closes the dropdown menu.
45
+ */
46
+ close(): void;
43
47
  /**
44
48
  * Method invoked when this component is connected.
45
49
  */
@@ -48,6 +52,14 @@ export declare class ThemeDropdown extends HTMLElement {
48
52
  * Method invoked when this component is disconnected.
49
53
  */
50
54
  disconnectedCallback(): void;
55
+ /**
56
+ * Opens the dropdown menu.
57
+ */
58
+ open(): void;
59
+ /**
60
+ * Saves the current application theme into the local storage.
61
+ */
62
+ save(): void;
51
63
  }
52
64
  /**
53
65
  * Declaration merging.
@@ -1 +1 @@
1
- {"version":3,"file":"ThemeDropdown.d.ts","sourceRoot":"","sources":["../../../src/Client/UI/Components/ThemeDropdown.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,QAAQ,EAAU,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAElD;;GAEG;AACH,qBAAa,aAAc,SAAQ,WAAW;;IAE7C;;OAEG;IACH,MAAM,CAAC,QAAQ,CAAC,kBAAkB,WAAsC;IAOxE;;OAEG;;IAaH;;OAEG;IACH,IAAI,SAAS,IAAI,aAAa,CAG7B;IACD,IAAI,SAAS,CAAC,KAAK,EAAE,aAAa,EAEjC;IAED;;OAEG;IACH,IAAI,QAAQ,IAAI,QAAQ,CAGvB;IACD,IAAI,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAG3B;IAED;;OAEG;IACH,IAAI,KAAK,IAAI,MAAM,CAGlB;IACD,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,EAEtB;IAED;;OAEG;IACH,IAAI,UAAU,IAAI,MAAM,CAGvB;IACD,IAAI,UAAU,CAAC,KAAK,EAAE,MAAM,EAE3B;IAED;;;;;OAKG;IACH,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAC,IAAI,GAAG,IAAI;IAwB/F;;OAEG;IACH,iBAAiB,IAAI,IAAI;IAMzB;;OAEG;IACH,oBAAoB,IAAI,IAAI;CAsB5B;AAED;;GAEG;AACH,OAAO,CAAC,MAAM,CAAC;IAEd;;OAEG;IACH,UAAU,qBAAqB;QAC9B,gBAAgB,EAAE,aAAa,CAAC;KAChC;CACD"}
1
+ {"version":3,"file":"ThemeDropdown.d.ts","sourceRoot":"","sources":["../../../src/Client/UI/Components/ThemeDropdown.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,QAAQ,EAAU,MAAM,gBAAgB,CAAC;AACjD,OAAO,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAElD;;GAEG;AACH,qBAAa,aAAc,SAAQ,WAAW;;IAE7C;;OAEG;IACH,MAAM,CAAC,QAAQ,CAAC,kBAAkB,WAAsC;IAYxE;;OAEG;;IAaH;;OAEG;IACH,IAAI,SAAS,IAAI,aAAa,CAG7B;IACD,IAAI,SAAS,CAAC,KAAK,EAAE,aAAa,EAEjC;IAED;;OAEG;IACH,IAAI,QAAQ,IAAI,QAAQ,CAGvB;IACD,IAAI,QAAQ,CAAC,KAAK,EAAE,QAAQ,EAE3B;IAED;;OAEG;IACH,IAAI,KAAK,IAAI,MAAM,CAGlB;IACD,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,EAEtB;IAED;;OAEG;IACH,IAAI,UAAU,IAAI,MAAM,CAGvB;IACD,IAAI,UAAU,CAAC,KAAK,EAAE,MAAM,EAE3B;IAED;;;;;OAKG;IACH,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAC,IAAI,GAAG,IAAI;IAe/F;;OAEG;IACH,KAAK,IAAI,IAAI;IAIb;;OAEG;IACH,iBAAiB,IAAI,IAAI;IAQzB;;OAEG;IACH,oBAAoB,IAAI,IAAI;IAK5B;;OAEG;IACH,IAAI,IAAI,IAAI;IAIZ;;OAEG;IACH,IAAI,IAAI,IAAI;CAkDZ;AAED;;GAEG;AACH,OAAO,CAAC,MAAM,CAAC;IAEd;;OAEG;IACH,UAAU,qBAAqB;QAC9B,gBAAgB,EAAE,aAAa,CAAC;KAChC;CACD"}
@@ -1,3 +1,4 @@
1
+ import { Dropdown } from "bootstrap";
1
2
  import { AppTheme, getIcon } from "../AppTheme.js";
2
3
  import { MenuAlignment } from "../MenuAlignment.js";
3
4
  /**
@@ -8,6 +9,10 @@ export class ThemeDropdown extends HTMLElement {
8
9
  * The list of observed attributes.
9
10
  */
10
11
  static observedAttributes = ["alignment", "apptheme", "label"];
12
+ /**
13
+ * The dropdown menu.
14
+ */
15
+ #dropdown;
11
16
  /**
12
17
  * The media query used to check the application theme.
13
18
  */
@@ -18,7 +23,7 @@ export class ThemeDropdown extends HTMLElement {
18
23
  constructor() {
19
24
  super();
20
25
  for (const button of this.querySelectorAll("button"))
21
- button.addEventListener("click", this.#setTheme);
26
+ button.addEventListener("click", this.#setAppTheme);
22
27
  }
23
28
  /**
24
29
  * Registers the component.
@@ -45,7 +50,6 @@ export class ThemeDropdown extends HTMLElement {
45
50
  }
46
51
  set appTheme(value) {
47
52
  this.setAttribute("apptheme", value);
48
- localStorage.setItem(this.storageKey, this.appTheme);
49
53
  }
50
54
  /**
51
55
  * The label of the dropdown menu.
@@ -76,48 +80,57 @@ export class ThemeDropdown extends HTMLElement {
76
80
  attributeChangedCallback(attribute, oldValue, newValue) {
77
81
  if (newValue != oldValue)
78
82
  switch (attribute) {
79
- case "alignment": {
80
- const alignment = Object.values(MenuAlignment).includes(newValue) ? newValue : MenuAlignment.End;
81
- const { classList } = this.querySelector(".dropdown-menu");
82
- if (alignment == MenuAlignment.End)
83
- classList.add("dropdown-menu-end");
84
- else
85
- classList.remove("dropdown-menu-end");
83
+ case "alignment":
84
+ this.#updateAlignment(Object.values(MenuAlignment).includes(newValue) ? newValue : MenuAlignment.End);
86
85
  break;
87
- }
88
- case "apptheme": {
89
- const appTheme = Object.values(AppTheme).includes(newValue) ? newValue : AppTheme.System;
90
- this.querySelector(".dropdown-toggle > .icon").textContent = getIcon(appTheme);
91
- this.querySelector(`button[data-theme="${appTheme}"]`).appendChild(this.querySelector(".dropdown-item > .icon"));
92
- this.#applyTheme();
86
+ case "apptheme":
87
+ this.#updateAppTheme(Object.values(AppTheme).includes(newValue) ? newValue : AppTheme.System);
93
88
  break;
94
- }
95
- case "label": {
96
- this.querySelector(".dropdown-toggle > span").textContent = (newValue ?? "").trim() || "Thème";
89
+ case "label":
90
+ this.#updateLabel(newValue ?? "");
97
91
  break;
98
- }
99
92
  // No default
100
93
  }
101
94
  }
95
+ /**
96
+ * Closes the dropdown menu.
97
+ */
98
+ close() {
99
+ this.#dropdown.hide();
100
+ }
102
101
  /**
103
102
  * Method invoked when this component is connected.
104
103
  */
105
104
  connectedCallback() {
106
105
  const appTheme = localStorage.getItem(this.storageKey);
107
106
  if (appTheme)
108
- this.setAttribute("apptheme", appTheme);
109
- this.#mediaQuery.addEventListener("change", this.#applyTheme);
107
+ this.appTheme = appTheme;
108
+ this.#dropdown = new Dropdown(this.querySelector(".dropdown-toggle"));
109
+ this.#mediaQuery.addEventListener("change", this.#applyToDocument);
110
110
  }
111
111
  /**
112
112
  * Method invoked when this component is disconnected.
113
113
  */
114
114
  disconnectedCallback() {
115
- this.#mediaQuery.removeEventListener("change", this.#applyTheme);
115
+ this.#dropdown.dispose();
116
+ this.#mediaQuery.removeEventListener("change", this.#applyToDocument);
117
+ }
118
+ /**
119
+ * Opens the dropdown menu.
120
+ */
121
+ open() {
122
+ this.#dropdown.show();
123
+ }
124
+ /**
125
+ * Saves the current application theme into the local storage.
126
+ */
127
+ save() {
128
+ localStorage.setItem(this.storageKey, this.appTheme);
116
129
  }
117
130
  /**
118
131
  * Applies the application theme to the document.
119
132
  */
120
- #applyTheme = () => {
133
+ #applyToDocument = () => {
121
134
  const { appTheme } = this;
122
135
  const bsTheme = appTheme == AppTheme.System ? (this.#mediaQuery.matches ? AppTheme.Dark : AppTheme.Light) : appTheme;
123
136
  document.documentElement.dataset.bsTheme = bsTheme.toLowerCase();
@@ -126,9 +139,36 @@ export class ThemeDropdown extends HTMLElement {
126
139
  * Changes the current application theme.
127
140
  * @param event The dispatched event.
128
141
  */
129
- #setTheme = event => {
142
+ #setAppTheme = event => {
130
143
  event.preventDefault();
131
- const button = event.target.closest("button");
132
- this.appTheme = button.dataset.theme;
144
+ this.appTheme = event.target.closest("button").dataset.theme;
145
+ this.save();
133
146
  };
147
+ /**
148
+ * Updates the alignment of the dropdown menu.
149
+ * @param value The new value.
150
+ */
151
+ #updateAlignment(value) {
152
+ const { classList } = this.querySelector(".dropdown-menu");
153
+ if (value == MenuAlignment.End)
154
+ classList.add("dropdown-menu-end");
155
+ else
156
+ classList.remove("dropdown-menu-end");
157
+ }
158
+ /**
159
+ * Updates the application theme.
160
+ * @param value The new value.
161
+ */
162
+ #updateAppTheme(value) {
163
+ this.querySelector(".dropdown-toggle > .icon").textContent = getIcon(value);
164
+ this.querySelector(`button[data-theme="${value}"]`).appendChild(this.querySelector(".dropdown-item > .icon"));
165
+ this.#applyToDocument();
166
+ }
167
+ /**
168
+ * Updates the label of the dropdown menu.
169
+ * @param value The new value.
170
+ */
171
+ #updateLabel(value) {
172
+ this.querySelector(".dropdown-toggle > span").textContent = value.trim() || "Thème";
173
+ }
134
174
  }
@@ -0,0 +1,74 @@
1
+ import { Context } from "@cedx/base/UI/Context.js";
2
+ /**
3
+ * Manages the notification messages.
4
+ */
5
+ export declare class Toast extends HTMLElement {
6
+ #private;
7
+ /**
8
+ * The list of observed attributes.
9
+ */
10
+ static readonly observedAttributes: string[];
11
+ /**
12
+ * Value indicating whether to apply a fade transition.
13
+ */
14
+ get animation(): boolean;
15
+ set animation(value: boolean);
16
+ /**
17
+ * Value indicating whether to automatically hide the notification.
18
+ */
19
+ get autoHide(): boolean;
20
+ set autoHide(value: boolean);
21
+ /**
22
+ * The title displayed in the header.
23
+ */
24
+ get caption(): string;
25
+ set caption(value: string);
26
+ /**
27
+ * A contextual modifier.
28
+ */
29
+ get context(): Context;
30
+ set context(value: Context);
31
+ /**
32
+ * The culture used to format the relative time.
33
+ */
34
+ get culture(): Intl.Locale;
35
+ set culture(value: Intl.Locale);
36
+ /**
37
+ * The delay, in milliseconds, to hide the notification.
38
+ */
39
+ get delay(): number;
40
+ set delay(value: number);
41
+ /**
42
+ * The time elapsed since this component was initially shown, in milliseconds.
43
+ */
44
+ get elapsedTime(): number;
45
+ /**
46
+ * The icon displayed next to the caption.
47
+ */
48
+ get icon(): string;
49
+ set icon(value: string);
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
+ /**
58
+ * Method invoked when this component is connected.
59
+ */
60
+ connectedCallback(): void;
61
+ /**
62
+ * Method invoked when this component is disconnected.
63
+ */
64
+ disconnectedCallback(): void;
65
+ /**
66
+ * Hides this toast.
67
+ */
68
+ hide(): void;
69
+ /**
70
+ * Shows this toast.
71
+ */
72
+ show(): void;
73
+ }
74
+ //# sourceMappingURL=Toast.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Toast.d.ts","sourceRoot":"","sources":["../../../src/Client/UI/Components/Toast.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,OAAO,EAAU,MAAM,0BAA0B,CAAC;AAG1D;;GAEG;AACH,qBAAa,KAAM,SAAQ,WAAW;;IAErC;;OAEG;IACH,MAAM,CAAC,QAAQ,CAAC,kBAAkB,WAA6C;IAuC/E;;OAEG;IACH,IAAI,SAAS,IAAI,OAAO,CAEvB;IACD,IAAI,SAAS,CAAC,KAAK,EAAE,OAAO,EAG3B;IAED;;OAEG;IACH,IAAI,QAAQ,IAAI,OAAO,CAEtB;IACD,IAAI,QAAQ,CAAC,KAAK,EAAE,OAAO,EAG1B;IAED;;OAEG;IACH,IAAI,OAAO,IAAI,MAAM,CAEpB;IACD,IAAI,OAAO,CAAC,KAAK,EAAE,MAAM,EAExB;IAED;;OAEG;IACH,IAAI,OAAO,IAAI,OAAO,CAGrB;IACD,IAAI,OAAO,CAAC,KAAK,EAAE,OAAO,EAEzB;IAED;;OAEG;IACH,IAAI,OAAO,IAAI,IAAI,CAAC,MAAM,CAGzB;IACD,IAAI,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,MAAM,EAE7B;IAED;;OAEG;IACH,IAAI,KAAK,IAAI,MAAM,CAGlB;IACD,IAAI,KAAK,CAAC,KAAK,EAAE,MAAM,EAEtB;IAED;;OAEG;IACH,IAAI,WAAW,IAAI,MAAM,CAExB;IAED;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAGjB;IACD,IAAI,IAAI,CAAC,KAAK,EAAE,MAAM,EAErB;IAED;;;;;OAKG;IACH,wBAAwB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAC,IAAI,GAAG,IAAI;IAkB/F;;OAEG;IACH,iBAAiB,IAAI,IAAI;IASzB;;OAEG;IACH,oBAAoB,IAAI,IAAI;IAK5B;;OAEG;IACH,IAAI,IAAI,IAAI;IAIZ;;OAEG;IACH,IAAI,IAAI,IAAI;CA+DZ"}
@@ -0,0 +1,225 @@
1
+ import { Duration } from "@cedx/base/Duration.js";
2
+ import { Context, getIcon } from "@cedx/base/UI/Context.js";
3
+ import { Toast as BootstrapToast } from "bootstrap";
4
+ /**
5
+ * Manages the notification messages.
6
+ */
7
+ export class Toast extends HTMLElement {
8
+ /**
9
+ * The list of observed attributes.
10
+ */
11
+ static observedAttributes = ["caption", "context", "culture", "icon"];
12
+ /**
13
+ * The time units.
14
+ */
15
+ static #timeUnits = ["second", "minute", "hour"];
16
+ /**
17
+ * The formatter used to format the relative time.
18
+ */
19
+ #formatter;
20
+ /**
21
+ * The toast header.
22
+ */
23
+ #header = this.querySelector(".toast-header");
24
+ /**
25
+ * The time at which this component was initially shown.
26
+ */
27
+ #initialTime = Date.now();
28
+ /**
29
+ * The timer identifier.
30
+ */
31
+ #timer = 0;
32
+ /**
33
+ * The underlying Bootstrap toast.
34
+ */
35
+ #toast;
36
+ /**
37
+ * Registers the component.
38
+ */
39
+ static {
40
+ customElements.define("toaster-item", this);
41
+ }
42
+ /**
43
+ * Value indicating whether to apply a fade transition.
44
+ */
45
+ get animation() {
46
+ return this.hasAttribute("animation");
47
+ }
48
+ set animation(value) {
49
+ if (value)
50
+ this.setAttribute("animation", "");
51
+ else
52
+ this.removeAttribute("animation");
53
+ }
54
+ /**
55
+ * Value indicating whether to automatically hide the notification.
56
+ */
57
+ get autoHide() {
58
+ return this.hasAttribute("autohide");
59
+ }
60
+ set autoHide(value) {
61
+ if (value)
62
+ this.setAttribute("autohide", "");
63
+ else
64
+ this.removeAttribute("autohide");
65
+ }
66
+ /**
67
+ * The title displayed in the header.
68
+ */
69
+ get caption() {
70
+ return (this.getAttribute("caption") ?? "").trim();
71
+ }
72
+ set caption(value) {
73
+ this.setAttribute("caption", value);
74
+ }
75
+ /**
76
+ * A contextual modifier.
77
+ */
78
+ get context() {
79
+ const value = this.getAttribute("context");
80
+ return Object.values(Context).includes(value) ? value : Context.Info;
81
+ }
82
+ set context(value) {
83
+ this.setAttribute("context", value);
84
+ }
85
+ /**
86
+ * The culture used to format the relative time.
87
+ */
88
+ get culture() {
89
+ const value = this.getAttribute("culture") ?? "";
90
+ return new Intl.Locale(value.trim() || navigator.language);
91
+ }
92
+ set culture(value) {
93
+ this.setAttribute("culture", value.toString());
94
+ }
95
+ /**
96
+ * The delay, in milliseconds, to hide the notification.
97
+ */
98
+ get delay() {
99
+ const value = Number(this.getAttribute("delay"));
100
+ return Math.max(1, Number.isNaN(value) ? 5_000 : value);
101
+ }
102
+ set delay(value) {
103
+ this.setAttribute("delay", value.toString());
104
+ }
105
+ /**
106
+ * The time elapsed since this component was initially shown, in milliseconds.
107
+ */
108
+ get elapsedTime() {
109
+ return Date.now() - this.#initialTime;
110
+ }
111
+ /**
112
+ * The icon displayed next to the caption.
113
+ */
114
+ get icon() {
115
+ const value = this.getAttribute("icon") ?? "";
116
+ return value.trim() || getIcon(Context.Info);
117
+ }
118
+ set icon(value) {
119
+ this.setAttribute("icon", value);
120
+ }
121
+ /**
122
+ * Method invoked when an attribute has been changed.
123
+ * @param attribute The attribute name.
124
+ * @param oldValue The previous attribute value.
125
+ * @param newValue The new attribute value.
126
+ */
127
+ attributeChangedCallback(attribute, oldValue, newValue) {
128
+ if (newValue != oldValue)
129
+ switch (attribute) {
130
+ case "caption":
131
+ this.#updateCaption(newValue ?? "");
132
+ break;
133
+ case "context":
134
+ this.#updateContext(Object.values(Context).includes(newValue) ? newValue : Context.Info);
135
+ break;
136
+ case "culture":
137
+ this.#formatter = new Intl.RelativeTimeFormat((newValue ?? "").trim() || navigator.language, { style: "long" });
138
+ break;
139
+ case "icon":
140
+ this.#updateIcon(newValue ?? "");
141
+ break;
142
+ // No default
143
+ }
144
+ }
145
+ /**
146
+ * Method invoked when this component is connected.
147
+ */
148
+ connectedCallback() {
149
+ const toast = this.querySelector(".toast");
150
+ toast.addEventListener("hidden.bs.toast", () => clearInterval(this.#timer));
151
+ toast.addEventListener("show.bs.toast", () => this.#timer = window.setInterval(this.#updateElapsedTime, Duration.Second));
152
+ const { animation, autoHide: autohide, delay } = this;
153
+ this.#toast = new BootstrapToast(toast, { animation, autohide, delay });
154
+ }
155
+ /**
156
+ * Method invoked when this component is disconnected.
157
+ */
158
+ disconnectedCallback() {
159
+ clearInterval(this.#timer);
160
+ this.#toast.dispose();
161
+ }
162
+ /**
163
+ * Hides this toast.
164
+ */
165
+ hide() {
166
+ this.#toast.hide();
167
+ }
168
+ /**
169
+ * Shows this toast.
170
+ */
171
+ show() {
172
+ if (!this.#toast.isShown()) {
173
+ this.#initialTime = Date.now();
174
+ this.#updateElapsedTime();
175
+ }
176
+ this.#toast.show();
177
+ }
178
+ /**
179
+ * Formats the specified elapsed time.
180
+ * @param elapsed The elapsed time, in seconds.
181
+ * @returns The formated time.
182
+ */
183
+ #formatTime(elapsed) {
184
+ let index = 0;
185
+ while (elapsed > 60 && index < Toast.#timeUnits.length) {
186
+ elapsed /= 60;
187
+ index++;
188
+ }
189
+ return this.#formatter.format(Math.ceil(-elapsed), Toast.#timeUnits[index]);
190
+ }
191
+ /**
192
+ * Updates the title displayed in the header.
193
+ * @param value The new value.
194
+ */
195
+ #updateCaption(value) {
196
+ this.#header.querySelector("b").textContent = value.trim();
197
+ }
198
+ /**
199
+ * Updates the title displayed in the header.
200
+ * @param value The new value.
201
+ */
202
+ #updateContext(value) {
203
+ const contexts = Object.values(Context);
204
+ let { classList } = this.#header;
205
+ classList.remove(...contexts.map(context => `toast-header-${context}`));
206
+ classList.add(`toast-header-${value}`);
207
+ ({ classList } = this.#header.querySelector(".icon"));
208
+ classList.remove(...contexts.map(context => `text-${context}`));
209
+ classList.add(`text-${value}`);
210
+ }
211
+ /**
212
+ * Updates the label corresponding to the elapsed time.
213
+ */
214
+ #updateElapsedTime = () => {
215
+ const { elapsedTime } = this;
216
+ this.#header.querySelector("small").textContent = elapsedTime > 0 ? this.#formatTime(elapsedTime / Duration.Second) : "";
217
+ };
218
+ /**
219
+ * Updates the icon displayed next to the caption.
220
+ * @param value The new value.
221
+ */
222
+ #updateIcon(value) {
223
+ this.#header.querySelector(".icon").textContent = value.trim() || getIcon(Context.Info);
224
+ }
225
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Defines the placement of an element.
3
+ */
4
+ export declare const Position: Readonly<{
5
+ /**
6
+ * Top left.
7
+ */
8
+ TopLeft: "TopLeft";
9
+ /**
10
+ * Top center.
11
+ */
12
+ TopCenter: "TopCenter";
13
+ /**
14
+ * Top right.
15
+ */
16
+ TopRight: "TopRight";
17
+ /**
18
+ * Middle left.
19
+ */
20
+ MiddleLeft: "MiddleLeft";
21
+ /**
22
+ * Middle center.
23
+ */
24
+ MiddleCenter: "MiddleCenter";
25
+ /**
26
+ * Middle right.
27
+ */
28
+ MiddleRight: "MiddleRight";
29
+ /**
30
+ * Bottom left.
31
+ */
32
+ BottomLeft: "BottomLeft";
33
+ /**
34
+ * Bottom center.
35
+ */
36
+ BottomCenter: "BottomCenter";
37
+ /**
38
+ * Bottom right.
39
+ */
40
+ BottomRight: "BottomRight";
41
+ }>;
42
+ /**
43
+ * Defines the placement of an element.
44
+ */
45
+ export type Position = typeof Position[keyof typeof Position];
46
+ //# sourceMappingURL=Position.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Position.d.ts","sourceRoot":"","sources":["../../src/Client/UI/Position.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,QAAQ;IAEpB;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;IAGH;;OAEG;;EAEF,CAAC;AAEH;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,QAAQ,CAAC,MAAM,OAAO,QAAQ,CAAC,CAAC"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Defines the placement of an element.
3
+ */
4
+ export const Position = Object.freeze({
5
+ /**
6
+ * Top left.
7
+ */
8
+ TopLeft: "TopLeft",
9
+ /**
10
+ * Top center.
11
+ */
12
+ TopCenter: "TopCenter",
13
+ /**
14
+ * Top right.
15
+ */
16
+ TopRight: "TopRight",
17
+ /**
18
+ * Middle left.
19
+ */
20
+ MiddleLeft: "MiddleLeft",
21
+ /**
22
+ * Middle center.
23
+ */
24
+ MiddleCenter: "MiddleCenter",
25
+ /**
26
+ * Middle right.
27
+ */
28
+ MiddleRight: "MiddleRight",
29
+ /**
30
+ * Bottom left.
31
+ */
32
+ BottomLeft: "BottomLeft",
33
+ /**
34
+ * Bottom center.
35
+ */
36
+ BottomCenter: "BottomCenter",
37
+ /**
38
+ * Bottom right.
39
+ */
40
+ BottomRight: "BottomRight"
41
+ });
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "name": "@cedx/base",
8
8
  "repository": "cedx/base",
9
9
  "type": "module",
10
- "version": "0.11.0",
10
+ "version": "0.12.0",
11
11
  "devDependencies": {
12
12
  "@playwright/browser-chromium": "^1.55.0",
13
13
  "@types/bootstrap": "^5.2.10",
@@ -15,7 +15,7 @@
15
15
  "@types/mocha": "^10.0.10",
16
16
  "@types/node": "^24.3.0",
17
17
  "@types/serve-handler": "^6.1.4",
18
- "chai": "^6.0.0",
18
+ "chai": "^6.0.1",
19
19
  "esbuild": "^0.25.9",
20
20
  "globals": "^16.3.0",
21
21
  "mocha": "^11.7.1",
@@ -14,22 +14,21 @@ export class OfflineIndicator extends HTMLElement {
14
14
  * Method invoked when this component is connected.
15
15
  */
16
16
  connectedCallback(): void {
17
- this.#updateHiddenState();
18
- for (const event of ["online", "offline"]) addEventListener(event, this.#updateHiddenState);
17
+ this.#update();
18
+ for (const event of ["online", "offline"]) addEventListener(event, this.#update);
19
19
  }
20
20
 
21
21
  /**
22
22
  * Method invoked when this component is disconnected.
23
23
  */
24
24
  disconnectedCallback(): void {
25
- for (const event of ["online", "offline"]) removeEventListener(event, this.#updateHiddenState);
25
+ for (const event of ["online", "offline"]) removeEventListener(event, this.#update);
26
26
  }
27
27
 
28
28
  /**
29
- * Updates the hidden state of this component according to the {@link navigator.onLine} property.
29
+ * Updates this component.
30
30
  */
31
- readonly #updateHiddenState: () => void = () =>
32
- this.hidden = navigator.onLine;
31
+ readonly #update: () => void = () => this.hidden = navigator.onLine;
33
32
  }
34
33
 
35
34
  /**
@@ -56,7 +56,7 @@ export class TabActivator extends HTMLElement {
56
56
  /**
57
57
  * The tab list.
58
58
  */
59
- get tabs(): NodeListOf<HTMLButtonElement> {
59
+ get tabs(): NodeListOf<HTMLElement> {
60
60
  return this.querySelectorAll('[data-bs-toggle="tab"]');
61
61
  }
62
62
 
@@ -1,3 +1,4 @@
1
+ import {Dropdown} from "bootstrap";
1
2
  import {AppTheme, getIcon} from "../AppTheme.js";
2
3
  import {MenuAlignment} from "../MenuAlignment.js";
3
4
 
@@ -11,6 +12,11 @@ export class ThemeDropdown extends HTMLElement {
11
12
  */
12
13
  static readonly observedAttributes = ["alignment", "apptheme", "label"];
13
14
 
15
+ /**
16
+ * The dropdown menu.
17
+ */
18
+ #dropdown!: Dropdown;
19
+
14
20
  /**
15
21
  * The media query used to check the application theme.
16
22
  */
@@ -21,7 +27,7 @@ export class ThemeDropdown extends HTMLElement {
21
27
  */
22
28
  constructor() {
23
29
  super();
24
- for (const button of this.querySelectorAll("button")) button.addEventListener("click", this.#setTheme);
30
+ for (const button of this.querySelectorAll("button")) button.addEventListener("click", this.#setAppTheme);
25
31
  }
26
32
 
27
33
  /**
@@ -51,7 +57,6 @@ export class ThemeDropdown extends HTMLElement {
51
57
  }
52
58
  set appTheme(value: AppTheme) {
53
59
  this.setAttribute("apptheme", value);
54
- localStorage.setItem(this.storageKey, this.appTheme);
55
60
  }
56
61
 
57
62
  /**
@@ -84,48 +89,63 @@ export class ThemeDropdown extends HTMLElement {
84
89
  */
85
90
  attributeChangedCallback(attribute: string, oldValue: string|null, newValue: string|null): void {
86
91
  if (newValue != oldValue) switch (attribute) {
87
- case "alignment": {
88
- const alignment = Object.values(MenuAlignment).includes(newValue as MenuAlignment) ? newValue as MenuAlignment : MenuAlignment.End;
89
- const {classList} = this.querySelector(".dropdown-menu")!;
90
- if (alignment == MenuAlignment.End) classList.add("dropdown-menu-end");
91
- else classList.remove("dropdown-menu-end");
92
+ case "alignment":
93
+ this.#updateAlignment(Object.values(MenuAlignment).includes(newValue as MenuAlignment) ? newValue as MenuAlignment : MenuAlignment.End);
92
94
  break;
93
- }
94
- case "apptheme": {
95
- const appTheme = Object.values(AppTheme).includes(newValue as AppTheme) ? newValue as AppTheme : AppTheme.System;
96
- this.querySelector(".dropdown-toggle > .icon")!.textContent = getIcon(appTheme);
97
- this.querySelector(`button[data-theme="${appTheme}"]`)!.appendChild(this.querySelector(".dropdown-item > .icon")!);
98
- this.#applyTheme();
95
+ case "apptheme":
96
+ this.#updateAppTheme(Object.values(AppTheme).includes(newValue as AppTheme) ? newValue as AppTheme : AppTheme.System);
99
97
  break;
100
- }
101
- case "label": {
102
- this.querySelector(".dropdown-toggle > span")!.textContent = (newValue ?? "").trim() || "Thème";
98
+ case "label":
99
+ this.#updateLabel(newValue ?? "");
103
100
  break;
104
- }
105
101
  // No default
106
102
  }
107
103
  }
108
104
 
105
+ /**
106
+ * Closes the dropdown menu.
107
+ */
108
+ close(): void {
109
+ this.#dropdown.hide();
110
+ }
111
+
109
112
  /**
110
113
  * Method invoked when this component is connected.
111
114
  */
112
115
  connectedCallback(): void {
113
116
  const appTheme = localStorage.getItem(this.storageKey) as AppTheme|null;
114
- if (appTheme) this.setAttribute("apptheme", appTheme);
115
- this.#mediaQuery.addEventListener("change", this.#applyTheme);
117
+ if (appTheme) this.appTheme = appTheme;
118
+
119
+ this.#dropdown = new Dropdown(this.querySelector(".dropdown-toggle")!);
120
+ this.#mediaQuery.addEventListener("change", this.#applyToDocument);
116
121
  }
117
122
 
118
123
  /**
119
124
  * Method invoked when this component is disconnected.
120
125
  */
121
126
  disconnectedCallback(): void {
122
- this.#mediaQuery.removeEventListener("change", this.#applyTheme);
127
+ this.#dropdown.dispose();
128
+ this.#mediaQuery.removeEventListener("change", this.#applyToDocument);
129
+ }
130
+
131
+ /**
132
+ * Opens the dropdown menu.
133
+ */
134
+ open(): void {
135
+ this.#dropdown.show();
136
+ }
137
+
138
+ /**
139
+ * Saves the current application theme into the local storage.
140
+ */
141
+ save(): void {
142
+ localStorage.setItem(this.storageKey, this.appTheme);
123
143
  }
124
144
 
125
145
  /**
126
146
  * Applies the application theme to the document.
127
147
  */
128
- readonly #applyTheme: () => void = () => {
148
+ readonly #applyToDocument: () => void = () => {
129
149
  const {appTheme} = this;
130
150
  const bsTheme = appTheme == AppTheme.System ? (this.#mediaQuery.matches ? AppTheme.Dark : AppTheme.Light) : appTheme;
131
151
  document.documentElement.dataset.bsTheme = bsTheme.toLowerCase();
@@ -135,11 +155,39 @@ export class ThemeDropdown extends HTMLElement {
135
155
  * Changes the current application theme.
136
156
  * @param event The dispatched event.
137
157
  */
138
- readonly #setTheme: (event: Event) => void = event => {
158
+ readonly #setAppTheme: (event: Event) => void = event => {
139
159
  event.preventDefault();
140
- const button = (event.target as HTMLElement).closest("button")!;
141
- this.appTheme = button.dataset.theme! as AppTheme;
160
+ this.appTheme = (event.target as Element).closest("button")!.dataset.theme! as AppTheme;
161
+ this.save();
142
162
  };
163
+
164
+ /**
165
+ * Updates the alignment of the dropdown menu.
166
+ * @param value The new value.
167
+ */
168
+ #updateAlignment(value: MenuAlignment): void {
169
+ const {classList} = this.querySelector(".dropdown-menu")!;
170
+ if (value == MenuAlignment.End) classList.add("dropdown-menu-end");
171
+ else classList.remove("dropdown-menu-end");
172
+ }
173
+
174
+ /**
175
+ * Updates the application theme.
176
+ * @param value The new value.
177
+ */
178
+ #updateAppTheme(value: AppTheme): void {
179
+ this.querySelector(".dropdown-toggle > .icon")!.textContent = getIcon(value);
180
+ this.querySelector(`button[data-theme="${value}"]`)!.appendChild(this.querySelector(".dropdown-item > .icon")!);
181
+ this.#applyToDocument();
182
+ }
183
+
184
+ /**
185
+ * Updates the label of the dropdown menu.
186
+ * @param value The new value.
187
+ */
188
+ #updateLabel(value: string): void {
189
+ this.querySelector(".dropdown-toggle > span")!.textContent = value.trim() || "Thème";
190
+ }
143
191
  }
144
192
 
145
193
  /**
@@ -0,0 +1,252 @@
1
+ import {Duration} from "@cedx/base/Duration.js";
2
+ import {Context, getIcon} from "@cedx/base/UI/Context.js";
3
+ import {Toast as BootstrapToast} from "bootstrap";
4
+
5
+ /**
6
+ * Manages the notification messages.
7
+ */
8
+ export class Toast extends HTMLElement {
9
+
10
+ /**
11
+ * The list of observed attributes.
12
+ */
13
+ static readonly observedAttributes = ["caption", "context", "culture", "icon"];
14
+
15
+ /**
16
+ * The time units.
17
+ */
18
+ static readonly #timeUnits: Intl.RelativeTimeFormatUnit[] = ["second", "minute", "hour"];
19
+
20
+ /**
21
+ * The formatter used to format the relative time.
22
+ */
23
+ #formatter!: Intl.RelativeTimeFormat;
24
+
25
+ /**
26
+ * The toast header.
27
+ */
28
+ readonly #header = this.querySelector(".toast-header")!;
29
+
30
+ /**
31
+ * The time at which this component was initially shown.
32
+ */
33
+ #initialTime = Date.now();
34
+
35
+ /**
36
+ * The timer identifier.
37
+ */
38
+ #timer = 0;
39
+
40
+ /**
41
+ * The underlying Bootstrap toast.
42
+ */
43
+ #toast!: BootstrapToast;
44
+
45
+ /**
46
+ * Registers the component.
47
+ */
48
+ static {
49
+ customElements.define("toaster-item", this);
50
+ }
51
+
52
+ /**
53
+ * Value indicating whether to apply a fade transition.
54
+ */
55
+ get animation(): boolean {
56
+ return this.hasAttribute("animation");
57
+ }
58
+ set animation(value: boolean) {
59
+ if (value) this.setAttribute("animation", "");
60
+ else this.removeAttribute("animation");
61
+ }
62
+
63
+ /**
64
+ * Value indicating whether to automatically hide the notification.
65
+ */
66
+ get autoHide(): boolean {
67
+ return this.hasAttribute("autohide");
68
+ }
69
+ set autoHide(value: boolean) {
70
+ if (value) this.setAttribute("autohide", "");
71
+ else this.removeAttribute("autohide");
72
+ }
73
+
74
+ /**
75
+ * The title displayed in the header.
76
+ */
77
+ get caption(): string {
78
+ return (this.getAttribute("caption") ?? "").trim();
79
+ }
80
+ set caption(value: string) {
81
+ this.setAttribute("caption", value);
82
+ }
83
+
84
+ /**
85
+ * A contextual modifier.
86
+ */
87
+ get context(): Context {
88
+ const value = this.getAttribute("context") as Context;
89
+ return Object.values(Context).includes(value) ? value : Context.Info;
90
+ }
91
+ set context(value: Context) {
92
+ this.setAttribute("context", value);
93
+ }
94
+
95
+ /**
96
+ * The culture used to format the relative time.
97
+ */
98
+ get culture(): Intl.Locale {
99
+ const value = this.getAttribute("culture") ?? "";
100
+ return new Intl.Locale(value.trim() || navigator.language);
101
+ }
102
+ set culture(value: Intl.Locale) {
103
+ this.setAttribute("culture", value.toString());
104
+ }
105
+
106
+ /**
107
+ * The delay, in milliseconds, to hide the notification.
108
+ */
109
+ get delay(): number {
110
+ const value = Number(this.getAttribute("delay"));
111
+ return Math.max(1, Number.isNaN(value) ? 5_000 : value);
112
+ }
113
+ set delay(value: number) {
114
+ this.setAttribute("delay", value.toString());
115
+ }
116
+
117
+ /**
118
+ * The time elapsed since this component was initially shown, in milliseconds.
119
+ */
120
+ get elapsedTime(): number {
121
+ return Date.now() - this.#initialTime;
122
+ }
123
+
124
+ /**
125
+ * The icon displayed next to the caption.
126
+ */
127
+ get icon(): string {
128
+ const value = this.getAttribute("icon") ?? "";
129
+ return value.trim() || getIcon(Context.Info);
130
+ }
131
+ set icon(value: string) {
132
+ this.setAttribute("icon", value);
133
+ }
134
+
135
+ /**
136
+ * Method invoked when an attribute has been changed.
137
+ * @param attribute The attribute name.
138
+ * @param oldValue The previous attribute value.
139
+ * @param newValue The new attribute value.
140
+ */
141
+ attributeChangedCallback(attribute: string, oldValue: string|null, newValue: string|null): void {
142
+ if (newValue != oldValue) switch (attribute) {
143
+ case "caption":
144
+ this.#updateCaption(newValue ?? "");
145
+ break;
146
+ case "context":
147
+ this.#updateContext(Object.values(Context).includes(newValue as Context) ? newValue as Context : Context.Info);
148
+ break;
149
+ case "culture":
150
+ this.#formatter = new Intl.RelativeTimeFormat((newValue ?? "").trim() || navigator.language, {style: "long"});
151
+ break;
152
+ case "icon":
153
+ this.#updateIcon(newValue ?? "")
154
+ break;
155
+ // No default
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Method invoked when this component is connected.
161
+ */
162
+ connectedCallback(): void {
163
+ const toast = this.querySelector(".toast")!;
164
+ toast.addEventListener("hidden.bs.toast", () => clearInterval(this.#timer));
165
+ toast.addEventListener("show.bs.toast", () => this.#timer = window.setInterval(this.#updateElapsedTime, Duration.Second));
166
+
167
+ const {animation, autoHide: autohide, delay} = this;
168
+ this.#toast = new BootstrapToast(toast, {animation, autohide, delay});
169
+ }
170
+
171
+ /**
172
+ * Method invoked when this component is disconnected.
173
+ */
174
+ disconnectedCallback(): void {
175
+ clearInterval(this.#timer);
176
+ this.#toast.dispose();
177
+ }
178
+
179
+ /**
180
+ * Hides this toast.
181
+ */
182
+ hide(): void {
183
+ this.#toast.hide();
184
+ }
185
+
186
+ /**
187
+ * Shows this toast.
188
+ */
189
+ show(): void {
190
+ if (!this.#toast.isShown()) {
191
+ this.#initialTime = Date.now();
192
+ this.#updateElapsedTime();
193
+ }
194
+
195
+ this.#toast.show();
196
+ }
197
+
198
+ /**
199
+ * Formats the specified elapsed time.
200
+ * @param elapsed The elapsed time, in seconds.
201
+ * @returns The formated time.
202
+ */
203
+ #formatTime(elapsed: number): string {
204
+ let index = 0;
205
+ while (elapsed > 60 && index < Toast.#timeUnits.length) {
206
+ elapsed /= 60;
207
+ index++;
208
+ }
209
+
210
+ return this.#formatter.format(Math.ceil(-elapsed), Toast.#timeUnits[index]);
211
+ }
212
+
213
+ /**
214
+ * Updates the title displayed in the header.
215
+ * @param value The new value.
216
+ */
217
+ #updateCaption(value: string): void {
218
+ this.#header.querySelector("b")!.textContent = value.trim();
219
+ }
220
+
221
+ /**
222
+ * Updates the title displayed in the header.
223
+ * @param value The new value.
224
+ */
225
+ #updateContext(value: Context): void {
226
+ const contexts = Object.values(Context);
227
+
228
+ let {classList} = this.#header;
229
+ classList.remove(...contexts.map(context => `toast-header-${context}`));
230
+ classList.add(`toast-header-${value}`);
231
+
232
+ ({classList} = this.#header.querySelector(".icon")!);
233
+ classList.remove(...contexts.map(context => `text-${context}`));
234
+ classList.add(`text-${value}`);
235
+ }
236
+
237
+ /**
238
+ * Updates the label corresponding to the elapsed time.
239
+ */
240
+ readonly #updateElapsedTime: () => void = () => {
241
+ const {elapsedTime} = this;
242
+ this.#header.querySelector("small")!.textContent = elapsedTime > 0 ? this.#formatTime(elapsedTime / Duration.Second) : "";
243
+ };
244
+
245
+ /**
246
+ * Updates the icon displayed next to the caption.
247
+ * @param value The new value.
248
+ */
249
+ #updateIcon(value: string): void {
250
+ this.#header.querySelector(".icon")!.textContent = value.trim() || getIcon(Context.Info);
251
+ }
252
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Defines the placement of an element.
3
+ */
4
+ export const Position = Object.freeze({
5
+
6
+ /**
7
+ * Top left.
8
+ */
9
+ TopLeft: "TopLeft",
10
+
11
+ /**
12
+ * Top center.
13
+ */
14
+ TopCenter: "TopCenter",
15
+
16
+ /**
17
+ * Top right.
18
+ */
19
+ TopRight: "TopRight",
20
+
21
+ /**
22
+ * Middle left.
23
+ */
24
+ MiddleLeft: "MiddleLeft",
25
+
26
+ /**
27
+ * Middle center.
28
+ */
29
+ MiddleCenter: "MiddleCenter",
30
+
31
+ /**
32
+ * Middle right.
33
+ */
34
+ MiddleRight: "MiddleRight",
35
+
36
+ /**
37
+ * Bottom left.
38
+ */
39
+ BottomLeft: "BottomLeft",
40
+
41
+ /**
42
+ * Bottom center.
43
+ */
44
+ BottomCenter: "BottomCenter",
45
+
46
+ /**
47
+ * Bottom right.
48
+ */
49
+ BottomRight: "BottomRight"
50
+ });
51
+
52
+ /**
53
+ * Defines the placement of an element.
54
+ */
55
+ export type Position = typeof Position[keyof typeof Position];