@cedx/base 0.4.1 → 0.6.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 +1 -1
- package/lib/Data/PaginatedList.d.ts +35 -0
- package/lib/Data/PaginatedList.d.ts.map +1 -0
- package/lib/Data/PaginatedList.js +28 -0
- package/lib/Data/Pagination.d.ts +57 -0
- package/lib/Data/Pagination.d.ts.map +1 -0
- package/lib/Data/Pagination.js +74 -0
- package/lib/Data/Sort.d.ts +144 -0
- package/lib/Data/Sort.d.ts.map +1 -0
- package/lib/Data/Sort.js +209 -0
- package/lib/DependencyInjection/Container.js +1 -1
- package/lib/Html/Context.d.ts +1 -1
- package/lib/Html/Context.d.ts.map +1 -1
- package/lib/Html/Context.js +1 -1
- package/lib/UI/Component.d.ts +24 -0
- package/lib/UI/Component.d.ts.map +1 -0
- package/lib/UI/Component.js +29 -0
- package/lib/UI/MenuActivator.d.ts +1 -1
- package/lib/UI/MenuActivator.js +2 -2
- package/lib/UI/ThemeDropdown.d.ts +6 -0
- package/lib/UI/ThemeDropdown.d.ts.map +1 -1
- package/lib/UI/ThemeDropdown.js +14 -12
- package/package.json +2 -2
- package/src/Client/Data/PaginatedList.ts +47 -0
- package/src/Client/Data/Pagination.ts +90 -0
- package/src/Client/Data/Sort.ts +237 -0
- package/src/Client/Data/tsconfig.json +13 -0
- package/src/Client/DependencyInjection/Container.ts +1 -1
- package/src/Client/Html/Context.ts +1 -1
- package/src/Client/UI/Component.ts +34 -0
- package/src/Client/UI/MenuActivator.ts +2 -2
- package/src/Client/UI/ThemeDropdown.ts +15 -14
- package/src/Client/UI/tsconfig.json +1 -3
- package/src/Client/tsconfig.json +1 -0
package/lib/UI/MenuActivator.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A component that activates the items of a menu based on the current document URL.
|
|
2
|
+
* A component that activates the items of a menu, based on the current document URL.
|
|
3
3
|
*/
|
|
4
4
|
export class MenuActivator extends HTMLElement {
|
|
5
5
|
/**
|
|
@@ -12,7 +12,7 @@ export class MenuActivator extends HTMLElement {
|
|
|
12
12
|
* Method invoked when this component is connected.
|
|
13
13
|
*/
|
|
14
14
|
connectedCallback() {
|
|
15
|
-
for (const anchor of this.
|
|
15
|
+
for (const anchor of this.querySelectorAll("a"))
|
|
16
16
|
if (anchor.href != location.href)
|
|
17
17
|
anchor.classList.remove("active");
|
|
18
18
|
else {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AppTheme } from "#Html/AppTheme.js";
|
|
1
2
|
/**
|
|
2
3
|
* A dropdown menu for switching the application theme.
|
|
3
4
|
*/
|
|
@@ -7,6 +8,11 @@ export declare class ThemeDropdown extends HTMLElement {
|
|
|
7
8
|
* Creates a new theme dropdown.
|
|
8
9
|
*/
|
|
9
10
|
constructor();
|
|
11
|
+
/**
|
|
12
|
+
* The current application theme.
|
|
13
|
+
*/
|
|
14
|
+
get theme(): AppTheme;
|
|
15
|
+
set theme(value: AppTheme);
|
|
10
16
|
/**
|
|
11
17
|
* Method invoked when this component is connected.
|
|
12
18
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThemeDropdown.d.ts","sourceRoot":"","sources":["../../src/Client/UI/ThemeDropdown.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ThemeDropdown.d.ts","sourceRoot":"","sources":["../../src/Client/UI/ThemeDropdown.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,QAAQ,EAAU,MAAM,mBAAmB,CAAC;AAEpD;;GAEG;AACH,qBAAa,aAAc,SAAQ,WAAW;;IAiB7C;;OAEG;;IAeH;;OAEG;IACH,IAAI,KAAK,IAAI,QAAQ,CAEpB;IACD,IAAI,KAAK,CAAC,KAAK,EAAE,QAAQ,EAGxB;IAED;;OAEG;IACH,iBAAiB,IAAI,IAAI;IAKzB;;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"}
|
package/lib/UI/ThemeDropdown.js
CHANGED
|
@@ -7,10 +7,6 @@ export class ThemeDropdown extends HTMLElement {
|
|
|
7
7
|
* The media query used to check the application theme.
|
|
8
8
|
*/
|
|
9
9
|
#mediaQuery = matchMedia("(prefers-color-scheme: dark)");
|
|
10
|
-
/**
|
|
11
|
-
* The root element.
|
|
12
|
-
*/
|
|
13
|
-
#root = this.firstElementChild;
|
|
14
10
|
/**
|
|
15
11
|
* The key of the storage entry providing the saved theme mode.
|
|
16
12
|
*/
|
|
@@ -26,7 +22,7 @@ export class ThemeDropdown extends HTMLElement {
|
|
|
26
22
|
super();
|
|
27
23
|
const theme = localStorage.getItem(this.#storageKey);
|
|
28
24
|
this.#theme = Object.values(AppTheme).includes(theme) ? theme : AppTheme.System;
|
|
29
|
-
for (const button of this
|
|
25
|
+
for (const button of this.querySelectorAll("button"))
|
|
30
26
|
button.addEventListener("click", this.#setTheme.bind(this));
|
|
31
27
|
}
|
|
32
28
|
/**
|
|
@@ -35,6 +31,16 @@ export class ThemeDropdown extends HTMLElement {
|
|
|
35
31
|
static {
|
|
36
32
|
customElements.define("theme-dropdown", this);
|
|
37
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* The current application theme.
|
|
36
|
+
*/
|
|
37
|
+
get theme() {
|
|
38
|
+
return this.#theme;
|
|
39
|
+
}
|
|
40
|
+
set theme(value) {
|
|
41
|
+
localStorage.setItem(this.#storageKey, this.#theme = value);
|
|
42
|
+
this.#applyTheme();
|
|
43
|
+
}
|
|
38
44
|
/**
|
|
39
45
|
* Method invoked when this component is connected.
|
|
40
46
|
*/
|
|
@@ -60,11 +66,8 @@ export class ThemeDropdown extends HTMLElement {
|
|
|
60
66
|
#applyTheme() {
|
|
61
67
|
const theme = this.#theme == AppTheme.System ? (this.#mediaQuery.matches ? AppTheme.Dark : AppTheme.Light) : this.#theme;
|
|
62
68
|
document.documentElement.dataset.bsTheme = theme.toLowerCase();
|
|
63
|
-
this
|
|
64
|
-
|
|
65
|
-
checkIcon.remove();
|
|
66
|
-
const activeButton = this.#root.querySelector(`button[data-theme="${this.#theme}"]`);
|
|
67
|
-
activeButton.appendChild(checkIcon);
|
|
69
|
+
this.querySelector(".dropdown-toggle > .icon").textContent = getIcon(this.#theme);
|
|
70
|
+
this.querySelector(`button[data-theme="${this.#theme}"]`).appendChild(this.querySelector(".dropdown-item > .icon"));
|
|
68
71
|
}
|
|
69
72
|
/**
|
|
70
73
|
* Changes the current theme.
|
|
@@ -72,7 +75,6 @@ export class ThemeDropdown extends HTMLElement {
|
|
|
72
75
|
*/
|
|
73
76
|
#setTheme(event) {
|
|
74
77
|
const button = event.target.closest("button");
|
|
75
|
-
|
|
76
|
-
this.#applyTheme();
|
|
78
|
+
this.theme = button.dataset.theme;
|
|
77
79
|
}
|
|
78
80
|
}
|
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.
|
|
10
|
+
"version": "0.6.0",
|
|
11
11
|
"devDependencies": {
|
|
12
12
|
"@playwright/browser-chromium": "^1.54.2",
|
|
13
13
|
"@types/bootstrap": "^5.2.10",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
],
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"bootstrap": ">=5.3.0",
|
|
54
|
-
"
|
|
54
|
+
"lit": ">=3.3.0"
|
|
55
55
|
},
|
|
56
56
|
"publishConfig": {
|
|
57
57
|
"access": "public"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {Pagination} from "./Pagination.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A list with information relevant to the pagination of its items.
|
|
5
|
+
*/
|
|
6
|
+
export class PaginatedList<T> extends Array<T> {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The information relevant to the pagination of the list items.
|
|
10
|
+
*/
|
|
11
|
+
pagination: Pagination;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates a new paginated list.
|
|
15
|
+
* @param options An object providing values to initialize this instance.
|
|
16
|
+
*/
|
|
17
|
+
constructor(options: PaginatedListOptions<T> = {}) {
|
|
18
|
+
super();
|
|
19
|
+
for (const item of options.items ?? []) this.push(item);
|
|
20
|
+
this.pagination = options.pagination ?? new Pagination;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates an empty paginated list.
|
|
25
|
+
* @param itemsPerPage The number of items per page.
|
|
26
|
+
* @returns An empty paginated list with the specified number of items per page.
|
|
27
|
+
*/
|
|
28
|
+
static empty<T>(itemsPerPage: number): PaginatedList<T> {
|
|
29
|
+
return new this({pagination: new Pagination({itemsPerPage})});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Defines the options of a {@link PaginatedList} instance.
|
|
35
|
+
*/
|
|
36
|
+
export type PaginatedListOptions<T> = Partial<{
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The list items.
|
|
40
|
+
*/
|
|
41
|
+
items: T[];
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The information relevant to the pagination of the list items.
|
|
45
|
+
*/
|
|
46
|
+
pagination: Pagination;
|
|
47
|
+
}>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents information relevant to the pagination of data items.
|
|
3
|
+
*/
|
|
4
|
+
export class Pagination {
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The one-based current page number.
|
|
8
|
+
*/
|
|
9
|
+
currentPageIndex: number;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The number of items per page.
|
|
13
|
+
*/
|
|
14
|
+
itemsPerPage: number;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The total number of items.
|
|
18
|
+
*/
|
|
19
|
+
totalItemCount: number;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Creates a new pagination.
|
|
23
|
+
* @param options An object providing values to initialize this instance.
|
|
24
|
+
*/
|
|
25
|
+
constructor(options: PaginationOptions = {}) {
|
|
26
|
+
this.currentPageIndex = Math.max(1, options.currentPageIndex ?? 1);
|
|
27
|
+
this.itemsPerPage = Math.max(1, Math.min(1000, options.itemsPerPage ?? 25));
|
|
28
|
+
this.totalItemCount = Math.max(0, options.totalItemCount ?? 0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Value indicating whether a next page exists.
|
|
33
|
+
*/
|
|
34
|
+
get hasNextPage(): boolean {
|
|
35
|
+
return this.currentPageIndex < this.totalItemCount;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Value indicating whether a previous page exists.
|
|
40
|
+
*/
|
|
41
|
+
get hasPreviousPage(): boolean {
|
|
42
|
+
return this.currentPageIndex > 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The one-based last page number.
|
|
47
|
+
*/
|
|
48
|
+
get lastPageIndex(): number {
|
|
49
|
+
return Math.ceil(this.totalItemCount / this.itemsPerPage);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* The data limit.
|
|
54
|
+
*/
|
|
55
|
+
get limit(): number {
|
|
56
|
+
return this.itemsPerPage;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The data offset.
|
|
61
|
+
*/
|
|
62
|
+
get offset(): number {
|
|
63
|
+
return (this.currentPageIndex - 1) * this.itemsPerPage;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The search parameters corresponding to this object.
|
|
68
|
+
*/
|
|
69
|
+
get searchParams(): URLSearchParams {
|
|
70
|
+
return new URLSearchParams({page: this.currentPageIndex.toString(), perPage: this.itemsPerPage.toString()});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Creates a new pagination from the HTTP headers of the specified response.
|
|
75
|
+
* @param response A server response.
|
|
76
|
+
* @returns The pagination corresponding to the HTTP headers of the specified response.
|
|
77
|
+
*/
|
|
78
|
+
static fromResponse(response: Response): Pagination {
|
|
79
|
+
return new this({
|
|
80
|
+
currentPageIndex: Number(response.headers.get("X-Pagination-Current-Page") ?? "1"),
|
|
81
|
+
itemsPerPage: Number(response.headers.get("X-Pagination-Per-Page") ?? "25"),
|
|
82
|
+
totalItemCount: Number(response.headers.get("X-Pagination-Total-Count") ?? "0")
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Defines the options of a {@link Pagination} instance.
|
|
89
|
+
*/
|
|
90
|
+
export type PaginationOptions = Partial<Pick<Pagination, "currentPageIndex"|"itemsPerPage"|"totalItemCount">>;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Specifies the order of a sorted property.
|
|
3
|
+
*/
|
|
4
|
+
export const SortOrder = Object.freeze({
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The sort is ascending.
|
|
8
|
+
*/
|
|
9
|
+
Ascending: "ASC",
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The sort is descending.
|
|
13
|
+
*/
|
|
14
|
+
Descending: "DESC"
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Specifies the order of a sorted property.
|
|
19
|
+
*/
|
|
20
|
+
export type SortOrder = typeof SortOrder[keyof typeof SortOrder];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Holds the name of a property and the order to sort by.
|
|
24
|
+
*/
|
|
25
|
+
export type SortProperty = [string, SortOrder];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Represents information relevant to the sorting of data items.
|
|
29
|
+
*/
|
|
30
|
+
export class Sort implements Iterable<SortProperty> {
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The list of sorted properties.
|
|
34
|
+
*/
|
|
35
|
+
#properties: SortProperty[];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Creates new sort.
|
|
39
|
+
* @param properties The list of properties to be sorted.
|
|
40
|
+
*/
|
|
41
|
+
constructor(properties: SortProperty[] = []) {
|
|
42
|
+
this.#properties = properties;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The number of properties.
|
|
47
|
+
*/
|
|
48
|
+
get length(): number {
|
|
49
|
+
return this.#properties.length;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Creates a new sort from the specified property and order.
|
|
54
|
+
* @param property The property name.
|
|
55
|
+
* @param order The sort order.
|
|
56
|
+
* @returns The sort corresponding to the property and order.
|
|
57
|
+
*/
|
|
58
|
+
static of(property: string, order: SortOrder = SortOrder.Ascending): Sort {
|
|
59
|
+
return new this([[property, order]]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Creates a new sort from the specified string.
|
|
64
|
+
* @param value A string representing a sort.
|
|
65
|
+
* @returns The sort corresponding to the specified string.
|
|
66
|
+
*/
|
|
67
|
+
static parse(value: string): Sort {
|
|
68
|
+
return new this((value ? value.split(",") : []).map(item => {
|
|
69
|
+
const order = item.startsWith("-") ? SortOrder.Descending : SortOrder.Ascending;
|
|
70
|
+
return [order == SortOrder.Ascending ? item : item.slice(1), order];
|
|
71
|
+
}));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Returns a new iterator that allows iterating the entries of this sort.
|
|
76
|
+
* @returns An iterator over the sorted properties.
|
|
77
|
+
*/
|
|
78
|
+
*[Symbol.iterator](): Generator<SortProperty, void, void> {
|
|
79
|
+
for (const entry of this.#properties) yield entry;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Appends the specified property to this sort.
|
|
84
|
+
* @param property The property name.
|
|
85
|
+
* @param order The sort order.
|
|
86
|
+
* @returns This instance.
|
|
87
|
+
*/
|
|
88
|
+
append(property: string, order: SortOrder): this {
|
|
89
|
+
this.delete(property);
|
|
90
|
+
this.#properties.push([property, order]);
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Gets the sorted property at the specified index.
|
|
96
|
+
* @param index The position in this sort.
|
|
97
|
+
* @returns The sorted property at the specified index, or `null` if it doesn't exist.
|
|
98
|
+
*/
|
|
99
|
+
at(index: number): SortProperty|null {
|
|
100
|
+
return this.#properties.at(index) ?? null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Compares the specified objects, according to the current sort properties.
|
|
105
|
+
* @param x The first object to compare.
|
|
106
|
+
* @param y The second object to compare.
|
|
107
|
+
* @returns A value indicating the relationship between the two objects.
|
|
108
|
+
*/
|
|
109
|
+
compare(x: object, y: object): number {
|
|
110
|
+
for (const [property, order] of this.#properties) {
|
|
111
|
+
const xAttr = Reflect.get(x, property); // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
|
112
|
+
const yAttr = Reflect.get(y, property); // eslint-disable-line @typescript-eslint/no-unsafe-assignment
|
|
113
|
+
const value = xAttr > yAttr ? 1 : (xAttr < yAttr ? -1 : 0);
|
|
114
|
+
if (value) return order == SortOrder.Ascending ? value : -value;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Removes the specified property from this sort.
|
|
122
|
+
* @param property The property name.
|
|
123
|
+
*/
|
|
124
|
+
delete(property: string): void {
|
|
125
|
+
this.#properties = this.#properties.filter(([key]) => key != property);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Gets the order associated with the specified property.
|
|
130
|
+
* @param property The property name.
|
|
131
|
+
* @returns The order associated with the specified property, or `null` if the property doesn't exist.
|
|
132
|
+
*/
|
|
133
|
+
get(property: string): SortOrder|null {
|
|
134
|
+
for (const [key, order] of this.#properties) if (key == property) return order;
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Gets the icon corresponding to the specified property.
|
|
140
|
+
* @param property The property name.
|
|
141
|
+
* @returns The icon corresponding to the specified property.
|
|
142
|
+
*/
|
|
143
|
+
getIcon(property: string): string {
|
|
144
|
+
switch (this.get(property)) {
|
|
145
|
+
case SortOrder.Ascending: return "arrow_upward";
|
|
146
|
+
case SortOrder.Descending: return "arrow_downward";
|
|
147
|
+
default: return "swap_vert";
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Returns a value indicating whether the specified property exists in this sort.
|
|
153
|
+
* @param property The property name.
|
|
154
|
+
* @returns `true` if the specified property exists in this sort, otherwise `false`.
|
|
155
|
+
*/
|
|
156
|
+
has(property: string): boolean {
|
|
157
|
+
return this.#properties.some(([key]) => key == property);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Gets the index of the specified property in the underlying list.
|
|
162
|
+
* @param property The property name.
|
|
163
|
+
* @returns The index of the specified property, or `-1` if the property is not found.
|
|
164
|
+
*/
|
|
165
|
+
indexOf(property: string): number {
|
|
166
|
+
for (const [index, [key]] of this.#properties.entries()) if (key == property) return index;
|
|
167
|
+
return -1;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Prepends the specified property to this sort.
|
|
172
|
+
* @param property The property name.
|
|
173
|
+
* @param order The sort order.
|
|
174
|
+
* @returns This instance.
|
|
175
|
+
*/
|
|
176
|
+
prepend(property: string, order: SortOrder): this {
|
|
177
|
+
this.delete(property);
|
|
178
|
+
this.#properties.unshift([property, order]);
|
|
179
|
+
return this;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Returns a value indicating whether the current sort satisfies the specified conditions.
|
|
184
|
+
* @param conditions The conditions to satisfy.
|
|
185
|
+
* @returns `true` if the current sort satisfies the specified conditions, otherwise `false`.
|
|
186
|
+
*/
|
|
187
|
+
satisfies(conditions: Partial<{min: number, max: number, properties: string[]}> = {}): boolean {
|
|
188
|
+
const min = conditions.min ?? -1;
|
|
189
|
+
if (min >= 0) return this.length >= min;
|
|
190
|
+
|
|
191
|
+
const max = conditions.max ?? -1;
|
|
192
|
+
if (max >= 0) return this.length <= max;
|
|
193
|
+
|
|
194
|
+
const properties = conditions.properties ?? [];
|
|
195
|
+
return properties.length ? this.#properties.every(([key]) => properties.includes(key)) : true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Sets the order of the specified property.
|
|
200
|
+
* @param property The property name.
|
|
201
|
+
* @param order The sort order.
|
|
202
|
+
* @returns This instance.
|
|
203
|
+
*/
|
|
204
|
+
set(property: string, order: SortOrder): this {
|
|
205
|
+
for (const [index, [key]] of this.#properties.entries()) if (key == property) {
|
|
206
|
+
this.#properties[index] = [key, order];
|
|
207
|
+
return this;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return this.append(property, order);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Returns a JSON representation of this object.
|
|
215
|
+
* @returns The JSON representation of this object.
|
|
216
|
+
*/
|
|
217
|
+
toJSON(): string {
|
|
218
|
+
return this.toString();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Converts this sort to an SQL clause.
|
|
223
|
+
* @param escape A function used to escape the SQL identifiers.
|
|
224
|
+
* @returns The SQL clause corresponding to this object.
|
|
225
|
+
*/
|
|
226
|
+
toSql(escape: (identifier: string) => string = id => id): string {
|
|
227
|
+
return this.#properties.map(([property, order]) => `${escape(property)} ${order}`).join(", ");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Returns a string representation of this object.
|
|
232
|
+
* @returns The string representation of this object.
|
|
233
|
+
*/
|
|
234
|
+
toString(): string {
|
|
235
|
+
return this.#properties.map(([property, order]) => `${order == SortOrder.Descending ? "-" : ""}${property}`).join();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -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/Data",
|
|
10
|
+
"rootDir": ".",
|
|
11
|
+
"tsBuildInfoFile": "../../../var/Data.tsbuildinfo"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -30,7 +30,7 @@ export class Container {
|
|
|
30
30
|
*/
|
|
31
31
|
get<T>(id: ContainerToken): T { // eslint-disable-line @typescript-eslint/no-unnecessary-type-parameters
|
|
32
32
|
if (!this.#services.has(id))
|
|
33
|
-
if (this.#factories.has(id)) this.set(id, this.#factories.get(id)
|
|
33
|
+
if (this.#factories.has(id)) this.set(id, this.#factories.get(id)!.call(this));
|
|
34
34
|
else if (typeof id == "function") this.set(id, Reflect.construct(id, []));
|
|
35
35
|
else throw Error("There is no factory registered with the specified identifier.");
|
|
36
36
|
|
|
@@ -34,7 +34,7 @@ export type Context = typeof Context[keyof typeof Context];
|
|
|
34
34
|
* @param context The context.
|
|
35
35
|
* @returns The icon corresponding to the specified context.
|
|
36
36
|
*/
|
|
37
|
-
export function
|
|
37
|
+
export function getIcon(context: Context): string {
|
|
38
38
|
switch (context) {
|
|
39
39
|
case Context.Danger: return "error";
|
|
40
40
|
case Context.Success: return "check_circle";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {LitElement, type CSSResultGroup} from "lit";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The optional base class for UI components. Alternatively, components may extend {@link LitElement} directly.
|
|
5
|
+
*/
|
|
6
|
+
export abstract class Component extends LitElement {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The component styles.
|
|
10
|
+
*/
|
|
11
|
+
static override styles: CSSResultGroup = [document.adoptedStyleSheets];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Value indicating whether this component uses a shadow root.
|
|
15
|
+
*/
|
|
16
|
+
readonly #useShadowRoot: boolean;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Creates a new component.
|
|
20
|
+
* @param options Value indicating whether this component uses a shadow root.
|
|
21
|
+
*/
|
|
22
|
+
constructor(options: {shadowRoot?: boolean} = {}) {
|
|
23
|
+
super();
|
|
24
|
+
this.#useShadowRoot = options.shadowRoot ?? 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.#useShadowRoot ? super.createRenderRoot() : this;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A component that activates the items of a menu based on the current document URL.
|
|
2
|
+
* A component that activates the items of a menu, based on the current document URL.
|
|
3
3
|
*/
|
|
4
4
|
export class MenuActivator extends HTMLElement {
|
|
5
5
|
|
|
@@ -14,7 +14,7 @@ export class MenuActivator extends HTMLElement {
|
|
|
14
14
|
* Method invoked when this component is connected.
|
|
15
15
|
*/
|
|
16
16
|
connectedCallback(): void {
|
|
17
|
-
for (const anchor of this.
|
|
17
|
+
for (const anchor of this.querySelectorAll("a"))
|
|
18
18
|
if (anchor.href != location.href) anchor.classList.remove("active");
|
|
19
19
|
else {
|
|
20
20
|
anchor.classList.add("active");
|
|
@@ -10,11 +10,6 @@ export class ThemeDropdown extends HTMLElement {
|
|
|
10
10
|
*/
|
|
11
11
|
readonly #mediaQuery = matchMedia("(prefers-color-scheme: dark)");
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
* The root element.
|
|
15
|
-
*/
|
|
16
|
-
readonly #root = this.firstElementChild!;
|
|
17
|
-
|
|
18
13
|
/**
|
|
19
14
|
* The key of the storage entry providing the saved theme mode.
|
|
20
15
|
*/
|
|
@@ -32,7 +27,7 @@ export class ThemeDropdown extends HTMLElement {
|
|
|
32
27
|
super();
|
|
33
28
|
const theme = localStorage.getItem(this.#storageKey) as AppTheme;
|
|
34
29
|
this.#theme = Object.values(AppTheme).includes(theme) ? theme : AppTheme.System;
|
|
35
|
-
for (const button of this
|
|
30
|
+
for (const button of this.querySelectorAll("button")) button.addEventListener("click", this.#setTheme.bind(this));
|
|
36
31
|
}
|
|
37
32
|
|
|
38
33
|
/**
|
|
@@ -42,6 +37,17 @@ export class ThemeDropdown extends HTMLElement {
|
|
|
42
37
|
customElements.define("theme-dropdown", this);
|
|
43
38
|
}
|
|
44
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The current application theme.
|
|
42
|
+
*/
|
|
43
|
+
get theme(): AppTheme {
|
|
44
|
+
return this.#theme;
|
|
45
|
+
}
|
|
46
|
+
set theme(value: AppTheme) {
|
|
47
|
+
localStorage.setItem(this.#storageKey, this.#theme = value);
|
|
48
|
+
this.#applyTheme();
|
|
49
|
+
}
|
|
50
|
+
|
|
45
51
|
/**
|
|
46
52
|
* Method invoked when this component is connected.
|
|
47
53
|
*/
|
|
@@ -70,12 +76,8 @@ export class ThemeDropdown extends HTMLElement {
|
|
|
70
76
|
#applyTheme(): void {
|
|
71
77
|
const theme = this.#theme == AppTheme.System ? (this.#mediaQuery.matches ? AppTheme.Dark : AppTheme.Light) : this.#theme;
|
|
72
78
|
document.documentElement.dataset.bsTheme = theme.toLowerCase();
|
|
73
|
-
this
|
|
74
|
-
|
|
75
|
-
const checkIcon = this.#root.querySelector(".dropdown-item > .icon")!;
|
|
76
|
-
checkIcon.remove();
|
|
77
|
-
const activeButton = this.#root.querySelector(`button[data-theme="${this.#theme}"]`)!
|
|
78
|
-
activeButton.appendChild(checkIcon);
|
|
79
|
+
this.querySelector(".dropdown-toggle > .icon")!.textContent = getIcon(this.#theme);
|
|
80
|
+
this.querySelector(`button[data-theme="${this.#theme}"]`)!.appendChild(this.querySelector(".dropdown-item > .icon")!);
|
|
79
81
|
}
|
|
80
82
|
|
|
81
83
|
/**
|
|
@@ -84,8 +86,7 @@ export class ThemeDropdown extends HTMLElement {
|
|
|
84
86
|
*/
|
|
85
87
|
#setTheme(event: Event): void {
|
|
86
88
|
const button = (event.target as HTMLElement).closest("button")!;
|
|
87
|
-
|
|
88
|
-
this.#applyTheme();
|
|
89
|
+
this.theme = button.dataset.theme as AppTheme;
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
92
|
|
|
@@ -5,12 +5,10 @@
|
|
|
5
5
|
"composite": true,
|
|
6
6
|
"declaration": true,
|
|
7
7
|
"declarationMap": true,
|
|
8
|
-
"importHelpers": true,
|
|
9
8
|
"noEmit": false,
|
|
10
9
|
"outDir": "../../../lib/UI",
|
|
11
10
|
"rootDir": ".",
|
|
12
|
-
"tsBuildInfoFile": "../../../var/UI.tsbuildinfo"
|
|
13
|
-
"useDefineForClassFields": false
|
|
11
|
+
"tsBuildInfoFile": "../../../var/UI.tsbuildinfo"
|
|
14
12
|
},
|
|
15
13
|
"references": [
|
|
16
14
|
{"path": "../Abstractions/tsconfig.json"},
|
package/src/Client/tsconfig.json
CHANGED