@cedx/base 0.8.0 → 0.9.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.8.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.9.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).
@@ -3,6 +3,14 @@
3
3
  */
4
4
  export declare class LoadingIndicator extends HTMLElement {
5
5
  #private;
6
+ /**
7
+ * Hides this component.
8
+ */
9
+ hide(): void;
10
+ /**
11
+ * Shows this component.
12
+ */
13
+ show(): void;
6
14
  /**
7
15
  * Starts the loading indicator.
8
16
  */
@@ -1 +1 @@
1
- {"version":3,"file":"LoadingIndicator.d.ts","sourceRoot":"","sources":["../../../src/Client/UI/Components/LoadingIndicator.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,WAAW;;IAchD;;OAEG;IACH,KAAK,IAAI,IAAI;IAMb;;;OAGG;IACH,IAAI,CAAC,OAAO,GAAE;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAM,GAAG,IAAI;CAQ3C;AAED;;GAEG;AACH,OAAO,CAAC,MAAM,CAAC;IAEd;;OAEG;IACH,UAAU,qBAAqB;QAC9B,mBAAmB,EAAE,gBAAgB,CAAC;KACtC;CACD"}
1
+ {"version":3,"file":"LoadingIndicator.d.ts","sourceRoot":"","sources":["../../../src/Client/UI/Components/LoadingIndicator.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,gBAAiB,SAAQ,WAAW;;IAchD;;OAEG;IACH,IAAI,IAAI,IAAI;IAKZ;;OAEG;IACH,IAAI,IAAI,IAAI;IAKZ;;OAEG;IACH,KAAK,IAAI,IAAI;IAKb;;;OAGG;IACH,IAAI,CAAC,OAAO,GAAE;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAM,GAAG,IAAI;CAO3C;AAED;;GAEG;AACH,OAAO,CAAC,MAAM,CAAC;IAEd;;OAEG;IACH,UAAU,qBAAqB;QAC9B,mBAAmB,EAAE,gBAAgB,CAAC;KACtC;CACD"}
@@ -12,13 +12,26 @@ export class LoadingIndicator extends HTMLElement {
12
12
  static {
13
13
  customElements.define("loading-indicator", this);
14
14
  }
15
+ /**
16
+ * Hides this component.
17
+ */
18
+ hide() {
19
+ this.hidden = true;
20
+ document.body.classList.remove("loading");
21
+ }
22
+ /**
23
+ * Shows this component.
24
+ */
25
+ show() {
26
+ this.hidden = false;
27
+ document.body.classList.add("loading");
28
+ }
15
29
  /**
16
30
  * Starts the loading indicator.
17
31
  */
18
32
  start() {
19
33
  this.#requestCount++;
20
- this.hidden = false;
21
- document.body.classList.add("loading");
34
+ this.show();
22
35
  }
23
36
  /**
24
37
  * Stops the loading indicator.
@@ -28,8 +41,7 @@ export class LoadingIndicator extends HTMLElement {
28
41
  this.#requestCount--;
29
42
  if (options.force || this.#requestCount <= 0) {
30
43
  this.#requestCount = 0;
31
- this.hidden = true;
32
- document.body.classList.remove("loading");
44
+ this.hide();
33
45
  }
34
46
  }
35
47
  }
@@ -2,6 +2,7 @@
2
2
  * A component that shows up when the network is unavailable, and hides when connectivity is restored.
3
3
  */
4
4
  export declare class OfflineIndicator extends HTMLElement {
5
+ #private;
5
6
  /**
6
7
  * Creates a new offline indicator.
7
8
  */
@@ -15,9 +16,13 @@ export declare class OfflineIndicator extends HTMLElement {
15
16
  */
16
17
  disconnectedCallback(): void;
17
18
  /**
18
- * Handles the events.
19
+ * Hides this component.
19
20
  */
20
- handleEvent(): void;
21
+ hide(): void;
22
+ /**
23
+ * Shows this component.
24
+ */
25
+ show(): void;
21
26
  }
22
27
  /**
23
28
  * Declaration merging.
@@ -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;IAEhD;;OAEG;;IAaH;;OAEG;IACH,iBAAiB,IAAI,IAAI;IAIzB;;OAEG;IACH,oBAAoB,IAAI,IAAI;IAI5B;;OAEG;IACH,WAAW,IAAI,IAAI;CAGnB;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;;IAEhD;;OAEG;;IAaH;;OAEG;IACH,iBAAiB,IAAI,IAAI;IAIzB;;OAEG;IACH,oBAAoB,IAAI,IAAI;IAI5B;;OAEG;IACH,IAAI,IAAI,IAAI;IAIZ;;OAEG;IACH,IAAI,IAAI,IAAI;CASZ;AAED;;GAEG;AACH,OAAO,CAAC,MAAM,CAAC;IAEd;;OAEG;IACH,UAAU,qBAAqB;QAC9B,mBAAmB,EAAE,gBAAgB,CAAC;KACtC;CACD"}
@@ -7,7 +7,7 @@ export class OfflineIndicator extends HTMLElement {
7
7
  */
8
8
  constructor() {
9
9
  super();
10
- this.hidden = navigator.onLine;
10
+ this.#updateHiddenState();
11
11
  }
12
12
  /**
13
13
  * Registers the component.
@@ -20,19 +20,29 @@ export class OfflineIndicator extends HTMLElement {
20
20
  */
21
21
  connectedCallback() {
22
22
  for (const event of ["online", "offline"])
23
- addEventListener(event, this);
23
+ addEventListener(event, this.#updateHiddenState);
24
24
  }
25
25
  /**
26
26
  * Method invoked when this component is disconnected.
27
27
  */
28
28
  disconnectedCallback() {
29
29
  for (const event of ["online", "offline"])
30
- removeEventListener(event, this);
30
+ removeEventListener(event, this.#updateHiddenState);
31
31
  }
32
32
  /**
33
- * Handles the events.
33
+ * Hides this component.
34
34
  */
35
- handleEvent() {
36
- this.hidden = navigator.onLine;
35
+ hide() {
36
+ this.hidden = true;
37
37
  }
38
+ /**
39
+ * Shows this component.
40
+ */
41
+ show() {
42
+ this.hidden = false;
43
+ }
44
+ /**
45
+ * Updates the hidden state of this component according to the {@link navigator.onLine} property.
46
+ */
47
+ #updateHiddenState = () => this.hidden = navigator.onLine;
38
48
  }
@@ -0,0 +1,45 @@
1
+ import { StorageArea } from "../StorageArea.js";
2
+ /**
3
+ * A component that activates a tab, based on its index saved in the web storage.
4
+ */
5
+ export declare class TabActivator extends HTMLElement {
6
+ /**
7
+ * The one-based index of the active tab.
8
+ */
9
+ get activeTabIndex(): number;
10
+ set activeTabIndex(value: number);
11
+ /**
12
+ * The storage object corresponding to the current {@link storageArea}.
13
+ */
14
+ get storage(): globalThis.Storage;
15
+ /**
16
+ * The storage area to use.
17
+ */
18
+ get storageArea(): StorageArea;
19
+ set storageArea(value: StorageArea);
20
+ /**
21
+ * The key of the storage entry providing the active tab index.
22
+ */
23
+ get storageKey(): string;
24
+ set storageKey(value: string);
25
+ /**
26
+ * The tab list.
27
+ */
28
+ get tabs(): NodeListOf<HTMLButtonElement>;
29
+ /**
30
+ * Method invoked when this component is connected.
31
+ */
32
+ connectedCallback(): void;
33
+ }
34
+ /**
35
+ * Declaration merging.
36
+ */
37
+ declare global {
38
+ /**
39
+ * The map of HTML tag names.
40
+ */
41
+ interface HTMLElementTagNameMap {
42
+ "tab-activator": TabActivator;
43
+ }
44
+ }
45
+ //# sourceMappingURL=TabActivator.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,67 @@
1
+ import { Tab } from "bootstrap";
2
+ import { StorageArea } from "../StorageArea.js";
3
+ /**
4
+ * A component that activates a tab, based on its index saved in the web storage.
5
+ */
6
+ export class TabActivator extends HTMLElement {
7
+ /**
8
+ * Registers the component.
9
+ */
10
+ static {
11
+ customElements.define("tab-activator", this);
12
+ }
13
+ /**
14
+ * The one-based index of the active tab.
15
+ */
16
+ get activeTabIndex() {
17
+ const index = Number.parseInt(this.storage.getItem(this.storageKey) ?? "1");
18
+ return Math.max(1, Math.min(this.tabs.length, Number.isNaN(index) ? 1 : index));
19
+ }
20
+ set activeTabIndex(value) {
21
+ this.storage.setItem(this.storageKey, value.toString());
22
+ }
23
+ /**
24
+ * The storage object corresponding to the current {@link storageArea}.
25
+ */
26
+ get storage() {
27
+ return this.storageArea == StorageArea.Local ? localStorage : sessionStorage;
28
+ }
29
+ /**
30
+ * The storage area to use.
31
+ */
32
+ get storageArea() {
33
+ const value = this.getAttribute("storagearea");
34
+ return Object.values(StorageArea).includes(value) ? value : StorageArea.Session;
35
+ }
36
+ set storageArea(value) {
37
+ this.setAttribute("storagearea", value);
38
+ }
39
+ /**
40
+ * The key of the storage entry providing the active tab index.
41
+ */
42
+ get storageKey() {
43
+ const value = this.getAttribute("storagekey") ?? "";
44
+ return value.trim() || "ActiveTabIndex";
45
+ }
46
+ set storageKey(value) {
47
+ this.setAttribute("storagekey", value);
48
+ }
49
+ /**
50
+ * The tab list.
51
+ */
52
+ get tabs() {
53
+ return this.querySelectorAll(".nav-tabs button");
54
+ }
55
+ /**
56
+ * Method invoked when this component is connected.
57
+ */
58
+ connectedCallback() {
59
+ const { activeTabIndex, tabs } = this;
60
+ for (let index = 1; index <= tabs.length; index++) {
61
+ const tab = tabs.item(index - 1);
62
+ tab.addEventListener("click", () => this.activeTabIndex = index);
63
+ if (index == activeTabIndex)
64
+ Tab.getOrCreateInstance(tab).show();
65
+ }
66
+ }
67
+ }
@@ -9,10 +9,6 @@ export declare class ThemeDropdown extends HTMLElement {
9
9
  * The list of observed attributes.
10
10
  */
11
11
  static readonly observedAttributes: string[];
12
- /**
13
- * Creates a new theme dropdown.
14
- */
15
- constructor();
16
12
  /**
17
13
  * The alignment of the dropdown menu.
18
14
  */
@@ -48,10 +44,6 @@ export declare class ThemeDropdown extends HTMLElement {
48
44
  * Method invoked when this component is disconnected.
49
45
  */
50
46
  disconnectedCallback(): void;
51
- /**
52
- * Handles the events.
53
- */
54
- handleEvent(): void;
55
47
  }
56
48
  /**
57
49
  * 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;IAI5B;;OAEG;IACH,WAAW,IAAI,IAAI;CAsBnB;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":"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;IAcxE;;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;IAQzB;;OAEG;IACH,oBAAoB,IAAI,IAAI;CAuB5B;AAED;;GAEG;AACH,OAAO,CAAC,MAAM,CAAC;IAEd;;OAEG;IACH,UAAU,qBAAqB;QAC9B,gBAAgB,EAAE,aAAa,CAAC;KAChC;CACD"}
@@ -12,14 +12,6 @@ export class ThemeDropdown extends HTMLElement {
12
12
  * The media query used to check the application theme.
13
13
  */
14
14
  #mediaQuery = matchMedia("(prefers-color-scheme: dark)");
15
- /**
16
- * Creates a new theme dropdown.
17
- */
18
- constructor() {
19
- super();
20
- for (const button of this.querySelectorAll("button"))
21
- button.addEventListener("click", this.#setTheme.bind(this));
22
- }
23
15
  /**
24
16
  * Registers the component.
25
17
  */
@@ -103,7 +95,9 @@ export class ThemeDropdown extends HTMLElement {
103
95
  * Method invoked when this component is connected.
104
96
  */
105
97
  connectedCallback() {
106
- this.#mediaQuery.addEventListener("change", this);
98
+ for (const button of this.querySelectorAll("button"))
99
+ button.addEventListener("click", this.#setTheme);
100
+ this.#mediaQuery.addEventListener("change", this.#applyTheme);
107
101
  const appTheme = localStorage.getItem(this.storageKey);
108
102
  if (appTheme)
109
103
  this.setAttribute("apptheme", appTheme);
@@ -112,29 +106,25 @@ export class ThemeDropdown extends HTMLElement {
112
106
  * Method invoked when this component is disconnected.
113
107
  */
114
108
  disconnectedCallback() {
115
- this.#mediaQuery.removeEventListener("change", this);
116
- }
117
- /**
118
- * Handles the events.
119
- */
120
- handleEvent() {
121
- this.#applyTheme();
109
+ for (const button of this.querySelectorAll("button"))
110
+ button.removeEventListener("click", this.#setTheme);
111
+ this.#mediaQuery.removeEventListener("change", this.#applyTheme);
122
112
  }
123
113
  /**
124
114
  * Applies the application theme to the document.
125
115
  */
126
- #applyTheme() {
116
+ #applyTheme = () => {
127
117
  const { appTheme } = this;
128
118
  const bsTheme = appTheme == AppTheme.System ? (this.#mediaQuery.matches ? AppTheme.Dark : AppTheme.Light) : appTheme;
129
119
  document.documentElement.dataset.bsTheme = bsTheme.toLowerCase();
130
- }
120
+ };
131
121
  /**
132
122
  * Changes the current application theme.
133
123
  * @param event The dispatched event.
134
124
  */
135
- #setTheme(event) {
125
+ #setTheme = event => {
136
126
  event.preventDefault();
137
127
  const button = event.target.closest("button");
138
128
  this.appTheme = button.dataset.theme;
139
- }
129
+ };
140
130
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Identifies the web storage area.
3
+ */
4
+ export declare const StorageArea: Readonly<{
5
+ /**
6
+ * Indicates the local storage.
7
+ */
8
+ Local: "Local";
9
+ /**
10
+ * Indicates the session storage.
11
+ */
12
+ Session: "Session";
13
+ }>;
14
+ /**
15
+ * Identifies the web storage area.
16
+ */
17
+ export type StorageArea = typeof StorageArea[keyof typeof StorageArea];
18
+ //# sourceMappingURL=StorageArea.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StorageArea.d.ts","sourceRoot":"","sources":["../../src/Client/UI/StorageArea.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,WAAW;IAEvB;;OAEG;;IAGH;;OAEG;;EAEF,CAAC;AAEH;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,WAAW,CAAC,MAAM,OAAO,WAAW,CAAC,CAAC"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Identifies the web storage area.
3
+ */
4
+ export const StorageArea = Object.freeze({
5
+ /**
6
+ * Indicates the local storage.
7
+ */
8
+ Local: "Local",
9
+ /**
10
+ * Indicates the session storage.
11
+ */
12
+ Session: "Session"
13
+ });
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.8.0",
10
+ "version": "0.9.0",
11
11
  "devDependencies": {
12
12
  "@playwright/browser-chromium": "^1.54.2",
13
13
  "@types/bootstrap": "^5.2.10",
@@ -15,13 +15,28 @@ export class LoadingIndicator extends HTMLElement {
15
15
  customElements.define("loading-indicator", this);
16
16
  }
17
17
 
18
+ /**
19
+ * Hides this component.
20
+ */
21
+ hide(): void {
22
+ this.hidden = true;
23
+ document.body.classList.remove("loading");
24
+ }
25
+
26
+ /**
27
+ * Shows this component.
28
+ */
29
+ show(): void {
30
+ this.hidden = false;
31
+ document.body.classList.add("loading");
32
+ }
33
+
18
34
  /**
19
35
  * Starts the loading indicator.
20
36
  */
21
37
  start(): void {
22
38
  this.#requestCount++;
23
- this.hidden = false;
24
- document.body.classList.add("loading");
39
+ this.show();
25
40
  }
26
41
 
27
42
  /**
@@ -32,8 +47,7 @@ export class LoadingIndicator extends HTMLElement {
32
47
  this.#requestCount--;
33
48
  if (options.force || this.#requestCount <= 0) {
34
49
  this.#requestCount = 0;
35
- this.hidden = true;
36
- document.body.classList.remove("loading");
50
+ this.hide();
37
51
  }
38
52
  }
39
53
  }
@@ -8,7 +8,7 @@ export class OfflineIndicator extends HTMLElement {
8
8
  */
9
9
  constructor() {
10
10
  super();
11
- this.hidden = navigator.onLine;
11
+ this.#updateHiddenState();
12
12
  }
13
13
 
14
14
  /**
@@ -22,22 +22,35 @@ export class OfflineIndicator extends HTMLElement {
22
22
  * Method invoked when this component is connected.
23
23
  */
24
24
  connectedCallback(): void {
25
- for (const event of ["online", "offline"]) addEventListener(event, this);
25
+ for (const event of ["online", "offline"]) addEventListener(event, this.#updateHiddenState);
26
26
  }
27
27
 
28
28
  /**
29
29
  * Method invoked when this component is disconnected.
30
30
  */
31
31
  disconnectedCallback(): void {
32
- for (const event of ["online", "offline"]) removeEventListener(event, this);
32
+ for (const event of ["online", "offline"]) removeEventListener(event, this.#updateHiddenState);
33
33
  }
34
34
 
35
35
  /**
36
- * Handles the events.
36
+ * Hides this component.
37
37
  */
38
- handleEvent(): void {
39
- this.hidden = navigator.onLine;
38
+ hide(): void {
39
+ this.hidden = true;
40
+ }
41
+
42
+ /**
43
+ * Shows this component.
44
+ */
45
+ show(): void {
46
+ this.hidden = false;
40
47
  }
48
+
49
+ /**
50
+ * Updates the hidden state of this component according to the {@link navigator.onLine} property.
51
+ */
52
+ readonly #updateHiddenState: () => void = () =>
53
+ this.hidden = navigator.onLine;
41
54
  }
42
55
 
43
56
  /**
@@ -0,0 +1,87 @@
1
+ import {Tab} from "bootstrap";
2
+ import {StorageArea} from "../StorageArea.js";
3
+
4
+ /**
5
+ * A component that activates a tab, based on its index saved in the web storage.
6
+ */
7
+ export class TabActivator extends HTMLElement {
8
+
9
+ /**
10
+ * Registers the component.
11
+ */
12
+ static {
13
+ customElements.define("tab-activator", this);
14
+ }
15
+
16
+ /**
17
+ * The one-based index of the active tab.
18
+ */
19
+ get activeTabIndex(): number {
20
+ const index = Number.parseInt(this.storage.getItem(this.storageKey) ?? "1");
21
+ return Math.max(1, Math.min(this.tabs.length, Number.isNaN(index) ? 1 : index));
22
+ }
23
+ set activeTabIndex(value: number) {
24
+ this.storage.setItem(this.storageKey, value.toString());
25
+ }
26
+
27
+ /**
28
+ * The storage object corresponding to the current {@link storageArea}.
29
+ */
30
+ get storage(): globalThis.Storage {
31
+ return this.storageArea == StorageArea.Local ? localStorage : sessionStorage;
32
+ }
33
+
34
+ /**
35
+ * The storage area to use.
36
+ */
37
+ get storageArea(): StorageArea {
38
+ const value = this.getAttribute("storagearea") as StorageArea;
39
+ return Object.values(StorageArea).includes(value) ? value : StorageArea.Session;
40
+ }
41
+ set storageArea(value: StorageArea) {
42
+ this.setAttribute("storagearea", value);
43
+ }
44
+
45
+ /**
46
+ * The key of the storage entry providing the active tab index.
47
+ */
48
+ get storageKey(): string {
49
+ const value = this.getAttribute("storagekey") ?? "";
50
+ return value.trim() || "ActiveTabIndex";
51
+ }
52
+ set storageKey(value: string) {
53
+ this.setAttribute("storagekey", value);
54
+ }
55
+
56
+ /**
57
+ * The tab list.
58
+ */
59
+ get tabs(): NodeListOf<HTMLButtonElement> {
60
+ return this.querySelectorAll(".nav-tabs button");
61
+ }
62
+
63
+ /**
64
+ * Method invoked when this component is connected.
65
+ */
66
+ connectedCallback(): void {
67
+ const {activeTabIndex, tabs} = this;
68
+ for (let index = 1; index <= tabs.length; index++) {
69
+ const tab = tabs.item(index - 1);
70
+ tab.addEventListener("click", () => this.activeTabIndex = index);
71
+ if (index == activeTabIndex) Tab.getOrCreateInstance(tab).show();
72
+ }
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Declaration merging.
78
+ */
79
+ declare global {
80
+
81
+ /**
82
+ * The map of HTML tag names.
83
+ */
84
+ interface HTMLElementTagNameMap {
85
+ "tab-activator": TabActivator;
86
+ }
87
+ }
@@ -16,14 +16,6 @@ export class ThemeDropdown extends HTMLElement {
16
16
  */
17
17
  readonly #mediaQuery = matchMedia("(prefers-color-scheme: dark)");
18
18
 
19
- /**
20
- * Creates a new theme dropdown.
21
- */
22
- constructor() {
23
- super();
24
- for (const button of this.querySelectorAll("button")) button.addEventListener("click", this.#setTheme.bind(this));
25
- }
26
-
27
19
  /**
28
20
  * Registers the component.
29
21
  */
@@ -110,7 +102,9 @@ export class ThemeDropdown extends HTMLElement {
110
102
  * Method invoked when this component is connected.
111
103
  */
112
104
  connectedCallback(): void {
113
- this.#mediaQuery.addEventListener("change", this);
105
+ for (const button of this.querySelectorAll("button")) button.addEventListener("click", this.#setTheme);
106
+ this.#mediaQuery.addEventListener("change", this.#applyTheme);
107
+
114
108
  const appTheme = localStorage.getItem(this.storageKey) as AppTheme|null;
115
109
  if (appTheme) this.setAttribute("apptheme", appTheme);
116
110
  }
@@ -119,34 +113,28 @@ export class ThemeDropdown extends HTMLElement {
119
113
  * Method invoked when this component is disconnected.
120
114
  */
121
115
  disconnectedCallback(): void {
122
- this.#mediaQuery.removeEventListener("change", this);
123
- }
124
-
125
- /**
126
- * Handles the events.
127
- */
128
- handleEvent(): void {
129
- this.#applyTheme();
116
+ for (const button of this.querySelectorAll("button")) button.removeEventListener("click", this.#setTheme);
117
+ this.#mediaQuery.removeEventListener("change", this.#applyTheme);
130
118
  }
131
119
 
132
120
  /**
133
121
  * Applies the application theme to the document.
134
122
  */
135
- #applyTheme(): void {
123
+ readonly #applyTheme: () => void = () => {
136
124
  const {appTheme} = this;
137
125
  const bsTheme = appTheme == AppTheme.System ? (this.#mediaQuery.matches ? AppTheme.Dark : AppTheme.Light) : appTheme;
138
126
  document.documentElement.dataset.bsTheme = bsTheme.toLowerCase();
139
- }
127
+ };
140
128
 
141
129
  /**
142
130
  * Changes the current application theme.
143
131
  * @param event The dispatched event.
144
132
  */
145
- #setTheme(event: Event): void {
133
+ readonly #setTheme: (event: Event) => void = event => {
146
134
  event.preventDefault();
147
135
  const button = (event.target as HTMLElement).closest("button")!;
148
136
  this.appTheme = button.dataset.theme! as AppTheme;
149
- }
137
+ };
150
138
  }
151
139
 
152
140
  /**
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Identifies the web storage area.
3
+ */
4
+ export const StorageArea = Object.freeze({
5
+
6
+ /**
7
+ * Indicates the local storage.
8
+ */
9
+ Local: "Local",
10
+
11
+ /**
12
+ * Indicates the session storage.
13
+ */
14
+ Session: "Session"
15
+ });
16
+
17
+ /**
18
+ * Identifies the web storage area.
19
+ */
20
+ export type StorageArea = typeof StorageArea[keyof typeof StorageArea];