@cedx/base 0.6.0 → 0.7.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.
Files changed (125) hide show
  1. package/ReadMe.md +1 -1
  2. package/lib/Data/Pagination.d.ts +30 -13
  3. package/lib/Data/Pagination.d.ts.map +1 -1
  4. package/lib/Data/Pagination.js +39 -12
  5. package/lib/Data/Sort.d.ts +25 -23
  6. package/lib/Data/Sort.d.ts.map +1 -1
  7. package/lib/Data/Sort.js +37 -33
  8. package/lib/{Date.d.ts → DateExtensions.d.ts} +1 -1
  9. package/lib/DateExtensions.d.ts.map +1 -0
  10. package/lib/{Html/File.d.ts → FileExtensions.d.ts} +4 -11
  11. package/lib/FileExtensions.d.ts.map +1 -0
  12. package/lib/{Html/File.js → FileExtensions.js} +3 -12
  13. package/lib/{Http → Net/Http}/HttpClient.d.ts +17 -2
  14. package/lib/Net/Http/HttpClient.d.ts.map +1 -0
  15. package/lib/{Http → Net/Http}/HttpClient.js +17 -15
  16. package/lib/Net/Http/HttpMethod.d.ts +46 -0
  17. package/lib/Net/Http/HttpMethod.d.ts.map +1 -0
  18. package/lib/Net/Http/HttpMethod.js +41 -0
  19. package/lib/Net/Http/HttpRequestError.d.ts +33 -0
  20. package/lib/Net/Http/HttpRequestError.d.ts.map +1 -0
  21. package/lib/{Http/HttpError.js → Net/Http/HttpRequestError.js} +16 -16
  22. package/lib/Net/Http/StatusCode.d.ts +122 -0
  23. package/lib/Net/Http/StatusCode.d.ts.map +1 -0
  24. package/lib/Net/Http/StatusCode.js +117 -0
  25. package/lib/Net/Mime/DispositionType.d.ts +18 -0
  26. package/lib/Net/Mime/DispositionType.d.ts.map +1 -0
  27. package/lib/Net/Mime/DispositionType.js +13 -0
  28. package/lib/Net/Mime/MediaType.d.ts +151 -0
  29. package/lib/Net/Mime/MediaType.d.ts.map +1 -0
  30. package/lib/Net/Mime/MediaType.js +150 -0
  31. package/lib/{Number.d.ts → NumberExtensions.d.ts} +1 -1
  32. package/lib/NumberExtensions.d.ts.map +1 -0
  33. package/lib/{String.d.ts → StringExtensions.d.ts} +1 -1
  34. package/lib/StringExtensions.d.ts.map +1 -0
  35. package/lib/{Html → UI}/AppTheme.d.ts +3 -3
  36. package/lib/UI/AppTheme.d.ts.map +1 -0
  37. package/lib/{Html → UI}/AppTheme.js +3 -3
  38. package/lib/UI/{Component.d.ts → Components/ComponentBase.d.ts} +5 -5
  39. package/lib/UI/Components/ComponentBase.d.ts.map +1 -0
  40. package/lib/UI/Components/ComponentBase.js +29 -0
  41. package/lib/UI/{LoadingIndicator.d.ts → Components/LoadingIndicator.d.ts} +1 -2
  42. package/lib/UI/Components/LoadingIndicator.d.ts.map +1 -0
  43. package/lib/UI/Components/MenuActivator.d.ts.map +1 -0
  44. package/lib/UI/Components/OfflineIndicator.d.ts.map +1 -0
  45. package/lib/UI/Components/ThemeDropdown.d.ts +67 -0
  46. package/lib/UI/Components/ThemeDropdown.d.ts.map +1 -0
  47. package/lib/UI/Components/ThemeDropdown.js +140 -0
  48. package/lib/UI/Context.d.ts.map +1 -0
  49. package/lib/UI/MenuAlignment.d.ts +18 -0
  50. package/lib/UI/MenuAlignment.d.ts.map +1 -0
  51. package/lib/UI/MenuAlignment.js +13 -0
  52. package/lib/{Html → UI}/ViewportScroller.d.ts +1 -1
  53. package/lib/UI/ViewportScroller.d.ts.map +1 -0
  54. package/lib/{Html → UI}/ViewportScroller.js +6 -6
  55. package/package.json +5 -7
  56. package/src/Client/Data/Pagination.ts +59 -13
  57. package/src/Client/Data/Sort.ts +40 -35
  58. package/src/Client/Data/tsconfig.json +1 -1
  59. package/src/Client/{Html/File.ts → FileExtensions.ts} +3 -13
  60. package/src/Client/{Http → Net/Http}/HttpClient.ts +32 -14
  61. package/src/Client/Net/Http/HttpMethod.ts +55 -0
  62. package/src/Client/{Http/HttpError.ts → Net/Http/HttpRequestError.ts} +17 -17
  63. package/src/Client/Net/Http/StatusCode.ts +150 -0
  64. package/src/Client/Net/Mime/DispositionType.ts +20 -0
  65. package/src/Client/Net/Mime/MediaType.ts +185 -0
  66. package/src/Client/{Abstractions → Net}/tsconfig.json +3 -3
  67. package/src/Client/{Html → UI}/AppTheme.ts +3 -3
  68. package/src/Client/UI/Components/ComponentBase.ts +34 -0
  69. package/src/Client/UI/{LoadingIndicator.ts → Components/LoadingIndicator.ts} +1 -3
  70. package/src/Client/UI/Components/ThemeDropdown.ts +163 -0
  71. package/src/Client/UI/MenuAlignment.ts +20 -0
  72. package/src/Client/{Html → UI}/ViewportScroller.ts +6 -6
  73. package/src/Client/UI/tsconfig.json +2 -3
  74. package/src/Client/tsconfig.json +1 -4
  75. package/lib/Abstractions/ILoadingIndicator.d.ts +0 -17
  76. package/lib/Abstractions/ILoadingIndicator.d.ts.map +0 -1
  77. package/lib/Abstractions/ILoadingIndicator.js +0 -1
  78. package/lib/Date.d.ts.map +0 -1
  79. package/lib/DependencyInjection/Container.d.ts +0 -43
  80. package/lib/DependencyInjection/Container.d.ts.map +0 -1
  81. package/lib/DependencyInjection/Container.js +0 -65
  82. package/lib/Html/AppTheme.d.ts.map +0 -1
  83. package/lib/Html/Context.d.ts.map +0 -1
  84. package/lib/Html/File.d.ts.map +0 -1
  85. package/lib/Html/ViewportScroller.d.ts.map +0 -1
  86. package/lib/Http/HttpClient.d.ts.map +0 -1
  87. package/lib/Http/HttpError.d.ts +0 -33
  88. package/lib/Http/HttpError.d.ts.map +0 -1
  89. package/lib/Http/StatusCodes.d.ts +0 -114
  90. package/lib/Http/StatusCodes.d.ts.map +0 -1
  91. package/lib/Http/StatusCodes.js +0 -109
  92. package/lib/Number.d.ts.map +0 -1
  93. package/lib/String.d.ts.map +0 -1
  94. package/lib/UI/Component.d.ts.map +0 -1
  95. package/lib/UI/Component.js +0 -29
  96. package/lib/UI/LoadingIndicator.d.ts.map +0 -1
  97. package/lib/UI/MenuActivator.d.ts.map +0 -1
  98. package/lib/UI/OfflineIndicator.d.ts.map +0 -1
  99. package/lib/UI/ThemeDropdown.d.ts +0 -40
  100. package/lib/UI/ThemeDropdown.d.ts.map +0 -1
  101. package/lib/UI/ThemeDropdown.js +0 -80
  102. package/src/Client/Abstractions/ILoadingIndicator.ts +0 -16
  103. package/src/Client/DependencyInjection/Container.ts +0 -75
  104. package/src/Client/DependencyInjection/tsconfig.json +0 -13
  105. package/src/Client/Html/tsconfig.json +0 -16
  106. package/src/Client/Http/StatusCodes.ts +0 -140
  107. package/src/Client/Http/tsconfig.json +0 -16
  108. package/src/Client/UI/Component.ts +0 -34
  109. package/src/Client/UI/ThemeDropdown.ts +0 -104
  110. /package/lib/{Date.js → DateExtensions.js} +0 -0
  111. /package/lib/{Number.js → NumberExtensions.js} +0 -0
  112. /package/lib/{String.js → StringExtensions.js} +0 -0
  113. /package/lib/UI/{LoadingIndicator.js → Components/LoadingIndicator.js} +0 -0
  114. /package/lib/UI/{MenuActivator.d.ts → Components/MenuActivator.d.ts} +0 -0
  115. /package/lib/UI/{MenuActivator.js → Components/MenuActivator.js} +0 -0
  116. /package/lib/UI/{OfflineIndicator.d.ts → Components/OfflineIndicator.d.ts} +0 -0
  117. /package/lib/UI/{OfflineIndicator.js → Components/OfflineIndicator.js} +0 -0
  118. /package/lib/{Html → UI}/Context.d.ts +0 -0
  119. /package/lib/{Html → UI}/Context.js +0 -0
  120. /package/src/Client/{Date.ts → DateExtensions.ts} +0 -0
  121. /package/src/Client/{Number.ts → NumberExtensions.ts} +0 -0
  122. /package/src/Client/{String.ts → StringExtensions.ts} +0 -0
  123. /package/src/Client/UI/{MenuActivator.ts → Components/MenuActivator.ts} +0 -0
  124. /package/src/Client/UI/{OfflineIndicator.ts → Components/OfflineIndicator.ts} +0 -0
  125. /package/src/Client/{Html → UI}/Context.ts +0 -0
@@ -1,9 +1,9 @@
1
- import {StatusCodes} from "./StatusCodes.js";
1
+ import {StatusCode} from "./StatusCode.js";
2
2
 
3
3
  /**
4
- * An object thrown when an HTTP error occurs.
4
+ * An error thrown by the HTTP client.
5
5
  */
6
- export class HttpError extends globalThis.Error {
6
+ export class HttpRequestError extends globalThis.Error {
7
7
 
8
8
  /**
9
9
  * The validation errors.
@@ -12,41 +12,41 @@ export class HttpError extends globalThis.Error {
12
12
 
13
13
  /**
14
14
  * Creates a new HTTP error.
15
- * @param response The server response.
15
+ * @param response The HTTP response.
16
16
  */
17
17
  constructor(response: Response) {
18
18
  super(`${response.status} ${response.statusText}`, {cause: response});
19
- this.name = "HttpError";
19
+ this.name = "HttpRequestError";
20
20
  }
21
21
 
22
22
  /**
23
- * The server response.
23
+ * The HTTP response.
24
24
  */
25
25
  override get cause(): Response {
26
26
  return super.cause as Response;
27
27
  }
28
28
 
29
29
  /**
30
- * Value indicating whether the response's status code is between 400 and 499.
30
+ * Value indicating whether the HTTP status code is between 400 and 499.
31
31
  */
32
32
  get isClientError(): boolean {
33
- const {status} = this;
34
- return status >= 400 && status < 500;
33
+ const {statusCode} = this;
34
+ return statusCode >= 400 && statusCode < 500;
35
35
  }
36
36
 
37
37
  /**
38
- * Value indicating whether the response's status code is between 500 and 599.
38
+ * Value indicating whether the HTTP status code is between 500 and 599.
39
39
  */
40
40
  get isServerError(): boolean {
41
- const {status} = this;
42
- return status >= 500 && status < 600;
41
+ const {statusCode} = this;
42
+ return statusCode >= 500 && statusCode < 600;
43
43
  }
44
44
 
45
45
  /**
46
- * The response's status code.
46
+ * The HTTP status code.
47
47
  */
48
- get status(): StatusCodes {
49
- return this.cause.status as StatusCodes;
48
+ get statusCode(): StatusCode {
49
+ return this.cause.status as StatusCode;
50
50
  }
51
51
 
52
52
  /**
@@ -64,8 +64,8 @@ export class HttpError extends globalThis.Error {
64
64
  */
65
65
  async #parseValidationErrors(): Promise<Map<string, string>> {
66
66
  try {
67
- const statuses = new Set<StatusCodes>([StatusCodes.BadRequest, StatusCodes.UnprocessableContent]);
68
- const ignoreBody = this.cause.bodyUsed || !statuses.has(this.status);
67
+ const statuses: StatusCode[] = [StatusCode.BadRequest, StatusCode.UnprocessableContent];
68
+ const ignoreBody = this.cause.bodyUsed || !statuses.includes(this.statusCode);
69
69
  return new Map(ignoreBody ? [] : Object.entries(await this.cause.json() as Record<string, string>));
70
70
  }
71
71
  catch {
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Provides common HTTP status codes.
3
+ */
4
+ export const StatusCode = Object.freeze({
5
+
6
+ /**
7
+ * The `OK` status code.
8
+ */
9
+ OK: 200,
10
+
11
+ /**
12
+ * The `Created` status code.
13
+ */
14
+ Created: 201,
15
+
16
+ /**
17
+ * The `No Content` status code.
18
+ */
19
+ NoContent: 204,
20
+
21
+ /**
22
+ * The `Moved Permanently` status code.
23
+ */
24
+ MovedPermanently: 301,
25
+
26
+ /**
27
+ * The `Found` status code.
28
+ */
29
+ Found: 302,
30
+
31
+ /**
32
+ * The `Not Modified` status code.
33
+ */
34
+ NotModified: 304,
35
+
36
+ /**
37
+ * The `Temporary Redirect` status code.
38
+ */
39
+ TemporaryRedirect: 307,
40
+
41
+ /**
42
+ * The `Permanent Redirect` status code.
43
+ */
44
+ PermanentRedirect: 308,
45
+
46
+ /**
47
+ * The `Bad Request` status code.
48
+ */
49
+ BadRequest: 400,
50
+
51
+ /**
52
+ * The `Unauthorized` status code.
53
+ */
54
+ Unauthorized: 401,
55
+
56
+ /**
57
+ * The `Payment Required` status code.
58
+ */
59
+ PaymentRequired: 402,
60
+
61
+ /**
62
+ * The `Forbidden` status code.
63
+ */
64
+ Forbidden: 403,
65
+
66
+ /**
67
+ * The `Not Found` status code.
68
+ */
69
+ NotFound: 404,
70
+
71
+ /**
72
+ * The `Method Not Allowed` status code.
73
+ */
74
+ MethodNotAllowed: 405,
75
+
76
+ /**
77
+ * The `Not Acceptable` status code.
78
+ */
79
+ NotAcceptable: 406,
80
+
81
+ /**
82
+ * The `Request Timeout` status code.
83
+ */
84
+ RequestTimeout: 408,
85
+
86
+ /**
87
+ * The `Conflict` status code.
88
+ */
89
+ Conflict: 409,
90
+
91
+ /**
92
+ * The `Payload Too Large` status code.
93
+ */
94
+ PayloadTooLarge: 413,
95
+
96
+ /**
97
+ * The `Unsupported Media Type` status code.
98
+ */
99
+ UnsupportedMediaType: 415,
100
+
101
+ /**
102
+ * The `Authentication Timeout` status code.
103
+ */
104
+ AuthenticationTimeout: 419,
105
+
106
+ /**
107
+ * The `Unprocessable Content` status code.
108
+ */
109
+ UnprocessableContent: 422,
110
+
111
+ /**
112
+ * The `Too Many Requests` status code.
113
+ */
114
+ TooManyRequests: 429,
115
+
116
+ /**
117
+ * The `Internal Server Error` status code.
118
+ */
119
+ InternalServerError: 500,
120
+
121
+ /**
122
+ * The `Not Implemented` status code.
123
+ */
124
+ NotImplemented: 501,
125
+
126
+ /**
127
+ * The `Bad Gateway` status code.
128
+ */
129
+ BadGateway: 502,
130
+
131
+ /**
132
+ * The `Service Unavailable` status code.
133
+ */
134
+ ServiceUnavailable: 503,
135
+
136
+ /**
137
+ * The `Gateway Timeout` status code.
138
+ */
139
+ GatewayTimeout: 504,
140
+
141
+ /**
142
+ * The `Bandwidth Limit Exceeded` status
143
+ */
144
+ BandwidthLimitExceeded: 509
145
+ });
146
+
147
+ /**
148
+ * Provides common HTTP status codes.
149
+ */
150
+ export type StatusCode = typeof StatusCode[keyof typeof StatusCode];
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Provides the strings used to specify the disposition type.
3
+ */
4
+ export const DispositionType = Object.freeze({
5
+
6
+ /**
7
+ * The `attachment` disposition type.
8
+ */
9
+ Attachment: "attachment",
10
+
11
+ /**
12
+ * The `inline` disposition type.
13
+ */
14
+ Inline: "inline"
15
+ });
16
+
17
+ /**
18
+ * Provides the strings used to specify the disposition type.
19
+ */
20
+ export type DispositionType = typeof DispositionType[keyof typeof DispositionType];
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Provides the strings used to specify the media type.
3
+ */
4
+ export const MediaType = Object.freeze({
5
+
6
+ /**
7
+ * Specifies the kind of application data.
8
+ */
9
+ Application: Object.freeze({
10
+
11
+ /**
12
+ * Specifies that the application data consists of url-encoded key-value pairs.
13
+ */
14
+ FormUrlEncoded: "application/x-www-form-urlencoded",
15
+
16
+ /**
17
+ * Specifies that the application data is in gzip format.
18
+ */
19
+ GZip: "application/gzip",
20
+
21
+ /**
22
+ * Specifies that the application data is in JSON format.
23
+ */
24
+ Json: "application/json",
25
+
26
+ /**
27
+ * Specifies that the application data is in Web Application Manifest.
28
+ */
29
+ Manifest: "application/manifest+json",
30
+
31
+ /**
32
+ * Specifies that the application data is not interpreted.
33
+ */
34
+ Octet: "application/octet-stream",
35
+
36
+ /**
37
+ * Specifies that the application data is in Portable Document Format (PDF).
38
+ */
39
+ Pdf: "application/pdf",
40
+
41
+ /**
42
+ * Specifies that the application data is a SOAP document.
43
+ */
44
+ Soap: "application/soap+xml",
45
+
46
+ /**
47
+ * Specifies that the application data is in WASM format.
48
+ */
49
+ Wasm: "application/wasm",
50
+
51
+ /**
52
+ * Specifies that the application data is in XML format.
53
+ */
54
+ Xml: "application/xml",
55
+
56
+ /**
57
+ * Specifies that the application data is compressed.
58
+ */
59
+ Zip: "application/zip"
60
+ }),
61
+
62
+ /**
63
+ * Specifies the kind of font data.
64
+ */
65
+ Font: Object.freeze({
66
+
67
+ /**
68
+ * Specifies that the font data is in TrueType font (TTF) format.
69
+ */
70
+ Ttf: "font/ttf",
71
+
72
+ /**
73
+ * Specifies that the font data is in WOFF format.
74
+ */
75
+ Woff: "font/woff",
76
+
77
+ /**
78
+ * Specifies that the font data is in WOFF2 format.
79
+ */
80
+ Woff2: "font/woff2"
81
+ }),
82
+
83
+ /**
84
+ * Specifies the kind of image data.
85
+ */
86
+ Image: Object.freeze({
87
+
88
+ /**
89
+ * Specifies that the image data is in AVIF format.
90
+ */
91
+ Avif: "image/avif",
92
+
93
+ /**
94
+ * Specifies that the image data is in GIF format.
95
+ */
96
+ Gif: "image/gif",
97
+
98
+ /**
99
+ * Specifies that the image data is in ICO format.
100
+ */
101
+ Icon: "image/x-icon",
102
+
103
+ /**
104
+ * Specifies that the image data is in JPEG format.
105
+ */
106
+ Jpeg: "image/jpeg",
107
+
108
+ /**
109
+ * Specifies that the image data is in PNG format.
110
+ */
111
+ Png: "image/png",
112
+
113
+ /**
114
+ * Specifies that the image data is in SVG format.
115
+ */
116
+ Svg: "image/svg+xml",
117
+
118
+ /**
119
+ * Specifies that the image data is in WEBP format.
120
+ */
121
+ WebP: "image/webp"
122
+ }),
123
+
124
+ /**
125
+ * Specifies the kind of multipart data.
126
+ */
127
+ Multipart: Object.freeze({
128
+
129
+ /**
130
+ * Specifies that the multipart data is in form data format.
131
+ */
132
+ FormData: "multipart/form-data",
133
+
134
+ /**
135
+ * Specifies that the multipart data is in mixed format.
136
+ */
137
+ Mixed: "multipart/mixed"
138
+ }),
139
+
140
+ /**
141
+ * Specifies the kind of text data.
142
+ */
143
+ Text: Object.freeze({
144
+
145
+ /**
146
+ * Specifies that the text data is in CSS format.
147
+ */
148
+ Css: "text/css",
149
+
150
+ /**
151
+ * Specifies that the text data is in CSV format.
152
+ */
153
+ Csv: "text/csv",
154
+
155
+ /**
156
+ * Specifies that the text data is in event stream format.
157
+ */
158
+ EventStream: "text/event-stream",
159
+
160
+ /**
161
+ * Specifies that the text data is in HTML format.
162
+ */
163
+ Html: "text/html",
164
+
165
+ /**
166
+ * Specifies that the text data is in JavaScript format.
167
+ */
168
+ JavaScript: "text/javascript",
169
+
170
+ /**
171
+ * Specifies that the text data is in Markdown format.
172
+ */
173
+ Markdown: "text/markdown",
174
+
175
+ /**
176
+ * Specifies that the text data is in plain text format.
177
+ */
178
+ Plain: "text/plain",
179
+
180
+ /**
181
+ * Specifies that the text data is in XML format.
182
+ */
183
+ Xml: "text/xml"
184
+ })
185
+ });
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "extends": "../../../tsconfig.json",
3
- "include": ["*.ts"],
3
+ "include": ["**/*.ts"],
4
4
  "compilerOptions": {
5
5
  "composite": true,
6
6
  "declaration": true,
7
7
  "declarationMap": true,
8
8
  "noEmit": false,
9
- "outDir": "../../../lib/Abstractions",
9
+ "outDir": "../../../lib/Net",
10
10
  "rootDir": ".",
11
- "tsBuildInfoFile": "../../../var/Abstractions.tsbuildinfo"
11
+ "tsBuildInfoFile": "../../../var/Net.tsbuildinfo"
12
12
  }
13
13
  }
@@ -4,17 +4,17 @@
4
4
  export const AppTheme = Object.freeze({
5
5
 
6
6
  /**
7
- * The system predefined theme mode.
7
+ * The system theme.
8
8
  */
9
9
  System: "System",
10
10
 
11
11
  /**
12
- * The light predefined theme mode.
12
+ * The light theme.
13
13
  */
14
14
  Light: "Light",
15
15
 
16
16
  /**
17
- * The dark predefined theme mode.
17
+ * The dark theme.
18
18
  */
19
19
  Dark: "Dark"
20
20
  });
@@ -0,0 +1,34 @@
1
+ import {LitElement, type CSSResultGroup} from "lit";
2
+
3
+ /**
4
+ * Optional base class for UI components. Alternatively, components may extend {@link LitElement} directly.
5
+ */
6
+ export abstract class ComponentBase extends LitElement {
7
+
8
+ /**
9
+ * The component styles.
10
+ */
11
+ static override styles: CSSResultGroup = [document.adoptedStyleSheets];
12
+
13
+ /**
14
+ * Value indicating whether a shadow DOM tree should be attached to this component.
15
+ */
16
+ readonly #attachShadow: boolean;
17
+
18
+ /**
19
+ * Creates a new component.
20
+ * @param options Value indicating whether a shadow DOM tree should be attached to this component.
21
+ */
22
+ constructor(options: {attachShadow?: boolean} = {}) {
23
+ super();
24
+ this.#attachShadow = options.attachShadow ?? false;
25
+ }
26
+
27
+ /**
28
+ * Returns the node into which this component should render.
29
+ * @returns The node into which this component should render.
30
+ */
31
+ protected override createRenderRoot(): DocumentFragment|HTMLElement {
32
+ return this.#attachShadow ? super.createRenderRoot() : this;
33
+ }
34
+ }
@@ -1,9 +1,7 @@
1
- import type {ILoadingIndicator} from "#Abstractions/ILoadingIndicator.js";
2
-
3
1
  /**
4
2
  * A component that shows up when an HTTP request starts, and hides when all concurrent HTTP requests are completed.
5
3
  */
6
- export class LoadingIndicator extends HTMLElement implements ILoadingIndicator {
4
+ export class LoadingIndicator extends HTMLElement {
7
5
 
8
6
  /**
9
7
  * The number of concurrent HTTP requests.
@@ -0,0 +1,163 @@
1
+ import {AppTheme, getIcon} from "../AppTheme.js";
2
+ import {MenuAlignment} from "../MenuAlignment.js";
3
+
4
+ /**
5
+ * A dropdown menu for switching the application theme.
6
+ */
7
+ export class ThemeDropdown extends HTMLElement {
8
+
9
+ /**
10
+ * The list of observed attributes.
11
+ */
12
+ static readonly observedAttributes = ["alignment", "apptheme", "label"];
13
+
14
+ /**
15
+ * The media query used to check the application theme.
16
+ */
17
+ readonly #mediaQuery = matchMedia("(prefers-color-scheme: dark)");
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
+ /**
28
+ * Registers the component.
29
+ */
30
+ static {
31
+ customElements.define("theme-dropdown", this);
32
+ }
33
+
34
+ /**
35
+ * The alignment of the dropdown menu.
36
+ */
37
+ get alignment(): MenuAlignment {
38
+ const value = this.getAttribute("alignment") as MenuAlignment;
39
+ return Object.values(MenuAlignment).includes(value) ? value : MenuAlignment.End;
40
+ }
41
+ set alignment(value: MenuAlignment) {
42
+ this.setAttribute("alignment", value);
43
+ }
44
+
45
+ /**
46
+ * The current application theme.
47
+ */
48
+ get appTheme(): AppTheme {
49
+ const value = this.getAttribute("apptheme") as AppTheme;
50
+ return Object.values(AppTheme).includes(value) ? value : AppTheme.System;
51
+ }
52
+ set appTheme(value: AppTheme) {
53
+ this.setAttribute("apptheme", value);
54
+ localStorage.setItem(this.storageKey, this.appTheme);
55
+ }
56
+
57
+ /**
58
+ * The label of the dropdown menu.
59
+ */
60
+ get label(): string {
61
+ const value = this.getAttribute("label") ?? "";
62
+ return value.trim() || "Thème";
63
+ }
64
+ set label(value: string) {
65
+ this.setAttribute("label", value);
66
+ }
67
+
68
+ /**
69
+ * The key of the storage entry providing the saved application theme.
70
+ */
71
+ get storageKey(): string {
72
+ const value = this.getAttribute("storagekey") ?? "";
73
+ return value.trim() || "AppTheme";
74
+ }
75
+ set storageKey(value: string) {
76
+ this.setAttribute("storagekey", value);
77
+ }
78
+
79
+ /**
80
+ * Method invoked when an attribute has been changed.
81
+ * @param attribute The attribute name.
82
+ * @param oldValue The previous attribute value.
83
+ * @param newValue The new attribute value.
84
+ */
85
+ attributeChangedCallback(attribute: string, oldValue: string|null, newValue: string|null): void {
86
+ 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
+ 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();
99
+ break;
100
+ }
101
+ case "label": {
102
+ this.querySelector(".dropdown-toggle > span")!.textContent = (newValue ?? "").trim() || "Thème";
103
+ break;
104
+ }
105
+ // No default
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Method invoked when this component is connected.
111
+ */
112
+ connectedCallback(): void {
113
+ this.#mediaQuery.addEventListener("change", this);
114
+ const appTheme = localStorage.getItem(this.storageKey) as AppTheme|null;
115
+ if (appTheme) this.setAttribute("apptheme", appTheme);
116
+ }
117
+
118
+ /**
119
+ * Method invoked when this component is disconnected.
120
+ */
121
+ disconnectedCallback(): void {
122
+ this.#mediaQuery.removeEventListener("change", this);
123
+ }
124
+
125
+ /**
126
+ * Handles the events.
127
+ */
128
+ handleEvent(): void {
129
+ this.#applyTheme();
130
+ }
131
+
132
+ /**
133
+ * Applies the application theme to the document.
134
+ */
135
+ #applyTheme(): void {
136
+ const {appTheme} = this;
137
+ const bsTheme = appTheme == AppTheme.System ? (this.#mediaQuery.matches ? AppTheme.Dark : AppTheme.Light) : appTheme;
138
+ document.documentElement.dataset.bsTheme = bsTheme.toLowerCase();
139
+ }
140
+
141
+ /**
142
+ * Changes the current application theme.
143
+ * @param event The dispatched event.
144
+ */
145
+ #setTheme(event: Event): void {
146
+ event.preventDefault();
147
+ const button = (event.target as HTMLElement).closest("button")!;
148
+ this.appTheme = button.dataset.theme! as AppTheme;
149
+ }
150
+ }
151
+
152
+ /**
153
+ * Declaration merging.
154
+ */
155
+ declare global {
156
+
157
+ /**
158
+ * The map of HTML tag names.
159
+ */
160
+ interface HTMLElementTagNameMap {
161
+ "theme-dropdown": ThemeDropdown;
162
+ }
163
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Defines the alignment of a dropdown menu.
3
+ */
4
+ export const MenuAlignment = Object.freeze({
5
+
6
+ /**
7
+ * The dropdown menu is left aligned.
8
+ */
9
+ Start: "Start",
10
+
11
+ /**
12
+ * The dropdown menu is right aligned.
13
+ */
14
+ End: "End"
15
+ });
16
+
17
+ /**
18
+ * Defines the alignment of a dropdown menu.
19
+ */
20
+ export type MenuAlignment = typeof MenuAlignment[keyof typeof MenuAlignment];