@cedx/base 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 +21 -0
- package/lib/Abstractions/ILoadingIndicator.d.ts +17 -0
- package/lib/Abstractions/ILoadingIndicator.d.ts.map +1 -0
- package/lib/Abstractions/ILoadingIndicator.js +1 -0
- package/lib/DependencyInjection/Container.d.ts +43 -0
- package/lib/DependencyInjection/Container.d.ts.map +1 -0
- package/lib/DependencyInjection/Container.js +65 -0
- package/lib/Html/AppTheme.d.ts +34 -0
- package/lib/Html/AppTheme.d.ts.map +1 -0
- package/lib/Html/AppTheme.js +41 -0
- package/lib/Html/MenuAlignment.d.ts +18 -0
- package/lib/Html/MenuAlignment.d.ts.map +1 -0
- package/lib/Html/MenuAlignment.js +13 -0
- package/lib/Html/ViewportScroller.d.ts +49 -0
- package/lib/Html/ViewportScroller.d.ts.map +1 -0
- package/lib/Html/ViewportScroller.js +69 -0
- package/lib/Http/HttpClient.d.ts +68 -0
- package/lib/Http/HttpClient.d.ts.map +1 -0
- package/lib/Http/HttpClient.js +102 -0
- package/lib/Http/HttpError.d.ts +33 -0
- package/lib/Http/HttpError.d.ts.map +1 -0
- package/lib/Http/HttpError.js +66 -0
- package/lib/Http/StatusCodes.d.ts +114 -0
- package/lib/Http/StatusCodes.d.ts.map +1 -0
- package/lib/Http/StatusCodes.js +109 -0
- package/lib/Number.d.ts +8 -0
- package/lib/Number.d.ts.map +1 -0
- package/lib/Number.js +10 -0
- package/lib/UI/LoadingIndicator.d.ts +40 -0
- package/lib/UI/LoadingIndicator.d.ts.map +1 -0
- package/lib/UI/LoadingIndicator.js +50 -0
- package/lib/UI/OfflineIndicator.d.ts +39 -0
- package/lib/UI/OfflineIndicator.d.ts.map +1 -0
- package/lib/UI/OfflineIndicator.js +48 -0
- package/lib/UI/ThemeDropdown.d.ts +68 -0
- package/lib/UI/ThemeDropdown.d.ts.map +1 -0
- package/lib/UI/ThemeDropdown.js +123 -0
- package/package.json +59 -0
- package/src/Client/Abstractions/ILoadingIndicator.ts +16 -0
- package/src/Client/Abstractions/tsconfig.json +13 -0
- package/src/Client/Base/tsconfig.json +13 -0
- package/src/Client/DependencyInjection/Container.ts +75 -0
- package/src/Client/DependencyInjection/tsconfig.json +13 -0
- package/src/Client/Html/AppTheme.ts +51 -0
- package/src/Client/Html/MenuAlignment.ts +20 -0
- package/src/Client/Html/ViewportScroller.ts +89 -0
- package/src/Client/Html/tsconfig.json +13 -0
- package/src/Client/Http/HttpClient.ts +127 -0
- package/src/Client/Http/HttpError.ts +75 -0
- package/src/Client/Http/StatusCodes.ts +140 -0
- package/src/Client/Http/tsconfig.json +16 -0
- package/src/Client/Number.ts +10 -0
- package/src/Client/UI/LoadingIndicator.ts +66 -0
- package/src/Client/UI/OfflineIndicator.ts +61 -0
- package/src/Client/UI/ThemeDropdown.ts +134 -0
- package/src/Client/UI/tsconfig.json +19 -0
- package/src/Client/tsconfig.json +11 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { __decorate } from "tslib";
|
|
2
|
+
import { AppTheme, themeIcon, themeLabel } from "#Html/AppTheme.js";
|
|
3
|
+
import { MenuAlignment } from "#Html/MenuAlignment.js";
|
|
4
|
+
import { html, LitElement } from "lit";
|
|
5
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
6
|
+
import { classMap } from "lit/directives/class-map.js";
|
|
7
|
+
import { when } from "lit/directives/when.js";
|
|
8
|
+
/**
|
|
9
|
+
* A dropdown menu for switching the color mode.
|
|
10
|
+
*/
|
|
11
|
+
let ThemeDropdown = class ThemeDropdown extends LitElement {
|
|
12
|
+
/**
|
|
13
|
+
* The media query used to check the system theme.
|
|
14
|
+
*/
|
|
15
|
+
#mediaQuery;
|
|
16
|
+
/**
|
|
17
|
+
* Creates a new theme dropdown.
|
|
18
|
+
*/
|
|
19
|
+
constructor() {
|
|
20
|
+
super();
|
|
21
|
+
/**
|
|
22
|
+
* The alignment of the dropdown menu.
|
|
23
|
+
*/
|
|
24
|
+
this.alignment = MenuAlignment.End;
|
|
25
|
+
/**
|
|
26
|
+
* The label of the dropdown menu.
|
|
27
|
+
*/
|
|
28
|
+
this.label = "";
|
|
29
|
+
/**
|
|
30
|
+
* The key of the storage entry providing the saved theme.
|
|
31
|
+
*/
|
|
32
|
+
this.storageKey = "AppTheme";
|
|
33
|
+
/**
|
|
34
|
+
* The media query used to check the system theme.
|
|
35
|
+
*/
|
|
36
|
+
this.#mediaQuery = matchMedia("(prefers-color-scheme: dark)");
|
|
37
|
+
const theme = localStorage.getItem(this.storageKey);
|
|
38
|
+
this.appTheme = Object.values(AppTheme).includes(theme) ? theme : AppTheme.System;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The current theme mode.
|
|
42
|
+
*/
|
|
43
|
+
get theme() { return this.appTheme; }
|
|
44
|
+
set theme(value) {
|
|
45
|
+
localStorage.setItem(this.storageKey, this.appTheme = value);
|
|
46
|
+
this.#applyTheme();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Method invoked when this component is connected.
|
|
50
|
+
*/
|
|
51
|
+
connectedCallback() {
|
|
52
|
+
super.connectedCallback();
|
|
53
|
+
this.#applyTheme();
|
|
54
|
+
this.#mediaQuery.addEventListener("change", this);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Method invoked when this component is disconnected.
|
|
58
|
+
*/
|
|
59
|
+
disconnectedCallback() {
|
|
60
|
+
this.#mediaQuery.removeEventListener("change", this);
|
|
61
|
+
super.disconnectedCallback();
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Handles the events.
|
|
65
|
+
*/
|
|
66
|
+
handleEvent() {
|
|
67
|
+
this.#applyTheme();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Returns the node into which this component should render.
|
|
71
|
+
* @returns The node into which this component should render.
|
|
72
|
+
*/
|
|
73
|
+
createRenderRoot() {
|
|
74
|
+
return this;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Renders this component.
|
|
78
|
+
* @returns The view template.
|
|
79
|
+
*/
|
|
80
|
+
render() {
|
|
81
|
+
return html `
|
|
82
|
+
<li class="nav-item dropdown">
|
|
83
|
+
<a class="dropdown-toggle nav-link" data-bs-toggle="dropdown" href="#">
|
|
84
|
+
<i class="icon icon-fill">${themeIcon(this.appTheme)}</i>
|
|
85
|
+
${when(this.label, () => html `<span class="ms-1">${this.label}</span>`)}
|
|
86
|
+
</a>
|
|
87
|
+
<ul class="dropdown-menu ${classMap({ "dropdown-menu-end": this.alignment == MenuAlignment.End })}">
|
|
88
|
+
${Object.values(AppTheme).map(value => html `
|
|
89
|
+
<li>
|
|
90
|
+
<button class="dropdown-item d-flex align-items-center justify-content-between" @click=${() => this.theme = value}>
|
|
91
|
+
<span><i class="icon icon-fill me-1">${themeIcon(value)}</i> ${themeLabel(value)}</span>
|
|
92
|
+
${when(value == this.appTheme, () => html `<i class="icon ms-2">check</i>`)}
|
|
93
|
+
</button>
|
|
94
|
+
</li>
|
|
95
|
+
`)}
|
|
96
|
+
</ul>
|
|
97
|
+
</li>
|
|
98
|
+
`;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Applies the theme to the document.
|
|
102
|
+
*/
|
|
103
|
+
#applyTheme() {
|
|
104
|
+
const theme = this.appTheme == AppTheme.System ? (this.#mediaQuery.matches ? AppTheme.Dark : AppTheme.Light) : this.appTheme;
|
|
105
|
+
document.documentElement.dataset.bsTheme = theme.toLowerCase();
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
__decorate([
|
|
109
|
+
property()
|
|
110
|
+
], ThemeDropdown.prototype, "alignment", void 0);
|
|
111
|
+
__decorate([
|
|
112
|
+
property()
|
|
113
|
+
], ThemeDropdown.prototype, "label", void 0);
|
|
114
|
+
__decorate([
|
|
115
|
+
property()
|
|
116
|
+
], ThemeDropdown.prototype, "storageKey", void 0);
|
|
117
|
+
__decorate([
|
|
118
|
+
state()
|
|
119
|
+
], ThemeDropdown.prototype, "appTheme", void 0);
|
|
120
|
+
ThemeDropdown = __decorate([
|
|
121
|
+
customElement("theme-dropdown")
|
|
122
|
+
], ThemeDropdown);
|
|
123
|
+
export { ThemeDropdown };
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"author": "Cédric Belin <cedx@outlook.com>",
|
|
3
|
+
"bugs": "https://github.com/cedx/base/issues",
|
|
4
|
+
"description": "Base library by Cédric Belin, full stack developer.",
|
|
5
|
+
"homepage": "https://github.com/cedx/base",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"name": "@cedx/base",
|
|
8
|
+
"repository": "cedx/base",
|
|
9
|
+
"type": "module",
|
|
10
|
+
"version": "0.1.0",
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@playwright/browser-chromium": "^1.54.2",
|
|
13
|
+
"@types/bootstrap": "^5.2.10",
|
|
14
|
+
"@types/chai": "^5.2.2",
|
|
15
|
+
"@types/mocha": "^10.0.10",
|
|
16
|
+
"@types/node": "^24.2.0",
|
|
17
|
+
"@types/serve-handler": "^6.1.4",
|
|
18
|
+
"chai": "^5.2.1",
|
|
19
|
+
"esbuild": "^0.25.8",
|
|
20
|
+
"globals": "^16.3.0",
|
|
21
|
+
"mocha": "^11.7.1",
|
|
22
|
+
"playwright": "^1.54.2",
|
|
23
|
+
"serve-handler": "^6.1.6",
|
|
24
|
+
"typedoc": "^0.28.9",
|
|
25
|
+
"typescript": "^5.9.2",
|
|
26
|
+
"typescript-eslint": "^8.39.0"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=24.0.0"
|
|
30
|
+
},
|
|
31
|
+
"exports": {
|
|
32
|
+
"./*.js": {
|
|
33
|
+
"types": "./lib/*.d.ts",
|
|
34
|
+
"default": "./lib/*.js"
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"lib/",
|
|
39
|
+
"src/Client/"
|
|
40
|
+
],
|
|
41
|
+
"imports": {
|
|
42
|
+
"#Abstractions/*.js": "./lib/Abstractions/*.js",
|
|
43
|
+
"#Html/*.js": "./lib/Html/*.js"
|
|
44
|
+
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"belin",
|
|
47
|
+
"core",
|
|
48
|
+
"framework",
|
|
49
|
+
"library"
|
|
50
|
+
],
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"bootstrap": ">=5.3.0",
|
|
53
|
+
"lit": ">=3.3.0",
|
|
54
|
+
"tslib": ">=2.8.0"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A component that shows up when an HTTP request starts, and hides when all concurrent HTTP requests are completed.
|
|
3
|
+
*/
|
|
4
|
+
export interface ILoadingIndicator {
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Starts the loading indicator.
|
|
8
|
+
*/
|
|
9
|
+
start: () => void;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Stops the loading indicator.
|
|
13
|
+
* @param options Value indicating whether to force the loading indicator to stop.
|
|
14
|
+
*/
|
|
15
|
+
stop: (options?: {force?: boolean}) => void;
|
|
16
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.json",
|
|
3
|
+
"include": ["*.ts"],
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"composite": true,
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"noEmit": false,
|
|
9
|
+
"outDir": "../../../lib/Abstractions",
|
|
10
|
+
"rootDir": ".",
|
|
11
|
+
"tsBuildInfoFile": "../../../var/Abstractions.tsbuildinfo"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.json",
|
|
3
|
+
"include": ["../*.ts"],
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"composite": true,
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"noEmit": false,
|
|
9
|
+
"outDir": "../../../lib",
|
|
10
|
+
"rootDir": "..",
|
|
11
|
+
"tsBuildInfoFile": "../../../var/Base.tsbuildinfo"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provides a dependency container.
|
|
3
|
+
*/
|
|
4
|
+
export class Container {
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The registered factories.
|
|
8
|
+
*/
|
|
9
|
+
readonly #factories = new Map<ContainerToken, () => any>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The registered services.
|
|
13
|
+
*/
|
|
14
|
+
readonly #services = new Map<ContainerToken, any>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Removes the service registered with the specified identifier.
|
|
18
|
+
* @param id The identification token.
|
|
19
|
+
*/
|
|
20
|
+
delete(id: ContainerToken): void {
|
|
21
|
+
this.#factories.delete(id);
|
|
22
|
+
this.#services.delete(id);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Gets the service registered with the specified identifier.
|
|
27
|
+
* @param id The identification token.
|
|
28
|
+
* @returns The instance of the service registered with the specified identifier.
|
|
29
|
+
* @throws `Error` if there is no factory registered with the specified identifier.
|
|
30
|
+
*/
|
|
31
|
+
get<T>(id: ContainerToken): T { // eslint-disable-line @typescript-eslint/no-unnecessary-type-parameters
|
|
32
|
+
if (!this.#services.has(id))
|
|
33
|
+
if (this.#factories.has(id)) this.set(id, this.#factories.get(id)!());
|
|
34
|
+
else if (typeof id == "function") this.set(id, Reflect.construct(id, []));
|
|
35
|
+
else throw Error("There is no factory registered with the specified identifier.");
|
|
36
|
+
|
|
37
|
+
return this.#services.get(id) as T;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Gets a value indicating whether this container has a service registered with the specified identifier.
|
|
42
|
+
* @param id The identification token.
|
|
43
|
+
* @returns `true` if a service registered with the specified identifier exists in this container, otherwise `false`.
|
|
44
|
+
*/
|
|
45
|
+
has(id: ContainerToken): boolean {
|
|
46
|
+
return this.#factories.has(id) || this.#services.has(id);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Registers a service factory with this container.
|
|
51
|
+
* @param id The identification token.
|
|
52
|
+
* @param factory The service factory.
|
|
53
|
+
* @returns This instance.
|
|
54
|
+
*/
|
|
55
|
+
register(id: ContainerToken, factory: () => unknown): this {
|
|
56
|
+
this.#factories.set(id, factory);
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Registers a service instance with this container.
|
|
62
|
+
* @param id The identification token.
|
|
63
|
+
* @param service The service instance.
|
|
64
|
+
* @returns This instance.
|
|
65
|
+
*/
|
|
66
|
+
set(id: ContainerToken, service: unknown): this {
|
|
67
|
+
this.#services.set(id, service);
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A token identifying a service.
|
|
74
|
+
*/
|
|
75
|
+
export type ContainerToken = string|symbol|(new(...args: any[]) => any);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.json",
|
|
3
|
+
"include": ["*.ts"],
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"composite": true,
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"noEmit": false,
|
|
9
|
+
"outDir": "../../../lib/DependencyInjection",
|
|
10
|
+
"rootDir": ".",
|
|
11
|
+
"tsBuildInfoFile": "../../../var/DependencyInjection.tsbuildinfo"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enumerates different themes an operating system or application can show.
|
|
3
|
+
*/
|
|
4
|
+
export const AppTheme = Object.freeze({
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The system predefined theme mode.
|
|
8
|
+
*/
|
|
9
|
+
System: "System",
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The light predefined theme mode.
|
|
13
|
+
*/
|
|
14
|
+
Light: "Light",
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The dark predefined theme mode.
|
|
18
|
+
*/
|
|
19
|
+
Dark: "Dark"
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Enumerates different themes an operating system or application can show.
|
|
24
|
+
*/
|
|
25
|
+
export type AppTheme = typeof AppTheme[keyof typeof AppTheme];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Gets the icon corresponding to the specified theme.
|
|
29
|
+
* @param theme The theme mode.
|
|
30
|
+
* @returns The icon corresponding to the specified theme.
|
|
31
|
+
*/
|
|
32
|
+
export function themeIcon(theme: AppTheme): string {
|
|
33
|
+
switch (theme) {
|
|
34
|
+
case AppTheme.Dark: return "dark_mode";
|
|
35
|
+
case AppTheme.Light: return "light_mode";
|
|
36
|
+
default: return "contrast";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Gets the label corresponding to the specified theme.
|
|
42
|
+
* @param theme The theme mode.
|
|
43
|
+
* @returns The label corresponding to the specified theme.
|
|
44
|
+
*/
|
|
45
|
+
export function themeLabel(theme: AppTheme): string {
|
|
46
|
+
switch (theme) {
|
|
47
|
+
case AppTheme.Dark: return "Sombre";
|
|
48
|
+
case AppTheme.Light: return "Clair";
|
|
49
|
+
default: return "Auto";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -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 right aligned.
|
|
8
|
+
*/
|
|
9
|
+
End: "End",
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The dropdown menu is left aligned.
|
|
13
|
+
*/
|
|
14
|
+
Start: "Start"
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Defines the alignment of a dropdown menu.
|
|
19
|
+
*/
|
|
20
|
+
export type MenuAlignment = typeof MenuAlignment[keyof typeof MenuAlignment];
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defines the scrolling options.
|
|
3
|
+
*/
|
|
4
|
+
export type ScrollOptions = Partial<{
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Value indicating whether scrolling is instant or animates smoothly.
|
|
8
|
+
*/
|
|
9
|
+
behavior: ScrollBehavior
|
|
10
|
+
}>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Manages the scroll position.
|
|
14
|
+
*/
|
|
15
|
+
export class ViewportScroller {
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The top offset used when scrolling to an element.
|
|
19
|
+
*/
|
|
20
|
+
#scrollOffset = -1;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The function returning the element used as viewport.
|
|
24
|
+
*/
|
|
25
|
+
readonly #viewport: () => Element;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Creates a new viewport scroller.
|
|
29
|
+
* @param viewport A function that returns the element used as viewport.
|
|
30
|
+
*/
|
|
31
|
+
constructor(viewport: () => Element = () => document.scrollingElement ?? document.documentElement) {
|
|
32
|
+
this.#viewport = viewport;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The top offset used when scrolling to an element.
|
|
37
|
+
*/
|
|
38
|
+
get scrollOffset(): number {
|
|
39
|
+
if (this.#scrollOffset < 0) {
|
|
40
|
+
const fontSize = parseInt(getComputedStyle(document.body).fontSize);
|
|
41
|
+
this.#scrollOffset = Number.isNaN(fontSize) ? 0 : fontSize * 2;
|
|
42
|
+
|
|
43
|
+
const navbarHeight = parseInt(getComputedStyle(document.documentElement).getPropertyValue("--navbar-height"));
|
|
44
|
+
this.#scrollOffset += Number.isNaN(navbarHeight) ? 0 : navbarHeight;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const actionBar: HTMLElement|null = document.body.querySelector("action-bar");
|
|
48
|
+
return this.#scrollOffset + (actionBar?.offsetHeight ?? 0);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Scrolls to the specified anchor.
|
|
53
|
+
* @param anchor The identifier or name of an elment.
|
|
54
|
+
* @param options Value indicating whether scrolling is instant or animates smoothly.
|
|
55
|
+
*/
|
|
56
|
+
scrollToAnchor(anchor: string, options: ScrollOptions = {}): void {
|
|
57
|
+
const element = document.getElementById(anchor) ?? document.body.querySelector(`[name="${anchor}"]`);
|
|
58
|
+
if (element) this.scrollToElement(element, options);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Scrolls to the specified element.
|
|
63
|
+
* @param element The element to scroll to.
|
|
64
|
+
* @param options Value indicating whether scrolling is instant or animates smoothly.
|
|
65
|
+
*/
|
|
66
|
+
scrollToElement(element: Element, options: ScrollOptions = {}): void {
|
|
67
|
+
const {left, top} = element.getBoundingClientRect();
|
|
68
|
+
const {scrollLeft, scrollTop} = this.#viewport();
|
|
69
|
+
this.scrollToPosition(left + scrollLeft, top + scrollTop - this.scrollOffset, options);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Scrolls to the specified position.
|
|
74
|
+
* @param x The pixel along the horizontal axis.
|
|
75
|
+
* @param y The pixel along the vertical axis.
|
|
76
|
+
* @param options Value indicating whether scrolling is instant or animates smoothly.
|
|
77
|
+
*/
|
|
78
|
+
scrollToPosition(x: number, y: number, options: ScrollOptions = {}): void {
|
|
79
|
+
this.#viewport().scrollTo({left: x, top: y, behavior: options.behavior ?? "auto"});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Scrolls to the top.
|
|
84
|
+
* @param options Value indicating whether scrolling is instant or animates smoothly.
|
|
85
|
+
*/
|
|
86
|
+
scrollToTop(options: ScrollOptions = {}): void {
|
|
87
|
+
this.scrollToPosition(0, 0, options);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.json",
|
|
3
|
+
"include": ["*.ts"],
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"composite": true,
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"noEmit": false,
|
|
9
|
+
"outDir": "../../../lib/Html",
|
|
10
|
+
"rootDir": ".",
|
|
11
|
+
"tsBuildInfoFile": "../../../var/Html.tsbuildinfo"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type {ILoadingIndicator} from "#Abstractions/ILoadingIndicator.js";
|
|
2
|
+
import {HttpError} from "./HttpError.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Performs HTTP requests.
|
|
6
|
+
*/
|
|
7
|
+
export class HttpClient {
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The base URL of the remote service.
|
|
11
|
+
*/
|
|
12
|
+
readonly baseUrl: URL;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The function returning the component used as loading indicator.
|
|
16
|
+
*/
|
|
17
|
+
readonly #loadingIndicator: () => ILoadingIndicator|null;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates a new HTTP client.
|
|
21
|
+
* @param options An object providing values to initialize this instance.
|
|
22
|
+
*/
|
|
23
|
+
constructor(options: HttpClientOptions = {}) {
|
|
24
|
+
const url = options.baseUrl ? (options.baseUrl instanceof URL ? options.baseUrl.href : options.baseUrl) : document.baseURI;
|
|
25
|
+
this.baseUrl = new URL(url.endsWith("/") ? url : `${url}/`);
|
|
26
|
+
this.#loadingIndicator = options.loadingIndicator ?? (() => document.body.querySelector("loading-indicator") as ILoadingIndicator|null);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Performs a DELETE request.
|
|
31
|
+
* @param url The URL of the resource to fetch.
|
|
32
|
+
* @param options The request options.
|
|
33
|
+
* @returns The server response.
|
|
34
|
+
*/
|
|
35
|
+
delete(url?: string|URL, options?: RequestInit): Promise<Response> {
|
|
36
|
+
return this.#fetch("DELETE", url, null, options);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Performs a GET request.
|
|
41
|
+
* @param url The URL of the resource to fetch.
|
|
42
|
+
* @param options The request options.
|
|
43
|
+
* @returns The server response.
|
|
44
|
+
*/
|
|
45
|
+
get(url?: string|URL, options?: RequestInit): Promise<Response> {
|
|
46
|
+
return this.#fetch("GET", url, null, options);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Performs a PATCH request.
|
|
51
|
+
* @param url The URL of the resource to fetch.
|
|
52
|
+
* @param body The request body.
|
|
53
|
+
* @param options The request options.
|
|
54
|
+
* @returns The server response.
|
|
55
|
+
*/
|
|
56
|
+
patch(url?: string|URL, body?: unknown, options?: RequestInit): Promise<Response> {
|
|
57
|
+
return this.#fetch("PATCH", url, body, options);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Performs a POST request.
|
|
62
|
+
* @param url The URL of the resource to fetch.
|
|
63
|
+
* @param body The request body.
|
|
64
|
+
* @param options The request options.
|
|
65
|
+
* @returns The server response.
|
|
66
|
+
*/
|
|
67
|
+
post(url?: string|URL, body?: unknown, options?: RequestInit): Promise<Response> {
|
|
68
|
+
return this.#fetch("POST", url, body, options);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Performs a PUT request.
|
|
73
|
+
* @param url The URL of the resource to fetch.
|
|
74
|
+
* @param body The request body.
|
|
75
|
+
* @param options The request options.
|
|
76
|
+
* @returns The server response.
|
|
77
|
+
*/
|
|
78
|
+
put(url?: string|URL, body?: unknown, options?: RequestInit): Promise<Response> {
|
|
79
|
+
return this.#fetch("PUT", url, body, options);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Performs a custom HTTP request.
|
|
84
|
+
* @param method The HTTP method.
|
|
85
|
+
* @param url The URL of the resource to fetch.
|
|
86
|
+
* @param body The request body.
|
|
87
|
+
* @param options The request options.
|
|
88
|
+
* @returns The server response.
|
|
89
|
+
*/
|
|
90
|
+
async #fetch(method: string, url: string|URL = "", body: unknown = null, options: RequestInit = {}): Promise<Response> {
|
|
91
|
+
const headers = new Headers(options.headers);
|
|
92
|
+
if (!headers.has("accept")) headers.set("accept", "application/json");
|
|
93
|
+
|
|
94
|
+
if (body && !(body instanceof Blob || body instanceof FormData || body instanceof URLSearchParams)) {
|
|
95
|
+
if (typeof body != "string") body = JSON.stringify(body);
|
|
96
|
+
if (!headers.has("content-type")) headers.set("content-type", "application/json");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const loadingIndicator = this.#loadingIndicator();
|
|
100
|
+
try {
|
|
101
|
+
loadingIndicator?.start();
|
|
102
|
+
const request = new Request(new URL(url, this.baseUrl), {...options, method, headers, body} as RequestInit);
|
|
103
|
+
const response = await fetch(request);
|
|
104
|
+
if (!response.ok) throw new HttpError(response);
|
|
105
|
+
return response;
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
loadingIndicator?.stop();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Defines the options of a {@link HttpClient} instance.
|
|
115
|
+
*/
|
|
116
|
+
export type HttpClientOptions = Partial<{
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The base URL of the remote service.
|
|
120
|
+
*/
|
|
121
|
+
baseUrl: string|URL;
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The function returning the component used as loading indicator.
|
|
125
|
+
*/
|
|
126
|
+
loadingIndicator: () => ILoadingIndicator|null;
|
|
127
|
+
}>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {StatusCodes} from "./StatusCodes.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* An object thrown when an HTTP error occurs.
|
|
5
|
+
*/
|
|
6
|
+
export class HttpError extends globalThis.Error {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The validation errors.
|
|
10
|
+
*/
|
|
11
|
+
#validationErrors: Map<string, string>|null = null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates a new HTTP error.
|
|
15
|
+
* @param response The server response.
|
|
16
|
+
*/
|
|
17
|
+
constructor(response: Response) {
|
|
18
|
+
super(`${response.status} ${response.statusText}`, {cause: response});
|
|
19
|
+
this.name = "HttpError";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The server response.
|
|
24
|
+
*/
|
|
25
|
+
override get cause(): Response {
|
|
26
|
+
return super.cause as Response;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Value indicating whether the response's status code is between 400 and 499.
|
|
31
|
+
*/
|
|
32
|
+
get isClientError(): boolean {
|
|
33
|
+
const {status} = this;
|
|
34
|
+
return status >= 400 && status < 500;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Value indicating whether the response's status code is between 500 and 599.
|
|
39
|
+
*/
|
|
40
|
+
get isServerError(): boolean {
|
|
41
|
+
const {status} = this;
|
|
42
|
+
return status >= 500 && status < 600;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The response's status code.
|
|
47
|
+
*/
|
|
48
|
+
get status(): StatusCodes {
|
|
49
|
+
return this.cause.status as StatusCodes;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The validation errors.
|
|
54
|
+
*/
|
|
55
|
+
get validationErrors(): Promise<Map<string, string>> {
|
|
56
|
+
return this.#validationErrors
|
|
57
|
+
? Promise.resolve(this.#validationErrors)
|
|
58
|
+
: this.#parseValidationErrors().then(errors => this.#validationErrors = errors);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parses the validation errors returned in the body of the specified response.
|
|
63
|
+
* @returns The validation errors provided by the response body.
|
|
64
|
+
*/
|
|
65
|
+
async #parseValidationErrors(): Promise<Map<string, string>> {
|
|
66
|
+
try {
|
|
67
|
+
const statuses = new Set<StatusCodes>([StatusCodes.BadRequest, StatusCodes.UnprocessableContent]);
|
|
68
|
+
const ignoreBody = this.cause.bodyUsed || !statuses.has(this.status);
|
|
69
|
+
return new Map(ignoreBody ? [] : Object.entries(await this.cause.json() as Record<string, string>));
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return new Map;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|