nexus-shared 3.0.0 → 3.2.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/CHANGELOG.md +442 -0
- package/README.md +1123 -1000
- package/dist/Client.Index.d.ts +6 -0
- package/dist/Client.Index.js +6 -0
- package/dist/Components/Chats/ChatParts.d.ts +2 -2
- package/dist/Components/Chats/ChatParts.js +3 -7
- package/dist/Components/Chats/ChatThread.js +3 -3
- package/dist/Components/Documents/Button.d.ts +6 -1
- package/dist/Components/Documents/Button.js +2 -2
- package/dist/Components/Documents/Menu.js +21 -9
- package/dist/Components/Documents/SplitButton.d.ts +3 -1
- package/dist/Components/Documents/SplitButton.js +7 -5
- package/dist/Components/Documents/TabButtons.d.ts +5 -2
- package/dist/Components/Documents/TabButtons.js +2 -2
- package/dist/Components/Forms/ApiForm.d.ts +1 -1
- package/dist/Components/Forms/ApiForm.js +1 -1
- package/dist/Components/Forms/Crud.js +486 -206
- package/dist/Components/Forms/CrudHistory.d.ts +66 -0
- package/dist/Components/Forms/CrudHistory.js +140 -0
- package/dist/Components/Forms/CrudParts.d.ts +5 -1
- package/dist/Components/Forms/CrudParts.js +38 -5
- package/dist/Components/Forms/ExcelImport.d.ts +23 -7
- package/dist/Components/Forms/ExcelImport.js +189 -60
- package/dist/Components/Forms/Form.js +9 -14
- package/dist/Components/Forms/SubmitForm.d.ts +11 -2
- package/dist/Components/Forms/SubmitForm.js +23 -12
- package/dist/Components/Inputs/CodeBox.d.ts +69 -0
- package/dist/Components/Inputs/CodeBox.js +316 -0
- package/dist/Components/Inputs/DatePicker.js +4 -1
- package/dist/Components/Inputs/DateRangePicker.js +4 -1
- package/dist/Components/Inputs/DateTimePicker.js +4 -1
- package/dist/Components/Inputs/Dropdown.js +4 -1
- package/dist/Components/Inputs/DropdownParts.d.ts +1 -1
- package/dist/Components/Inputs/DropdownParts.js +4 -6
- package/dist/Components/Inputs/InputRenderer.js +3 -0
- package/dist/Components/Inputs/NumberBox.js +26 -2
- package/dist/Components/Inputs/RowsInput.js +2 -2
- package/dist/Components/Inputs/Slider.js +4 -1
- package/dist/Components/Inputs/TabularForm.js +42 -10
- package/dist/Components/Inputs/TextArea.js +4 -1
- package/dist/Components/Inputs/TextBox.js +4 -1
- package/dist/Components/Inputs/TimePanel.js +4 -3
- package/dist/Components/Inputs/TimePicker.js +4 -1
- package/dist/Components/Inputs/TreeView.d.ts +55 -5
- package/dist/Components/Inputs/TreeView.js +372 -23
- package/dist/Components/Layouts/Popup.js +1 -4
- package/dist/Components/Layouts/ThemePicker.d.ts +21 -3
- package/dist/Components/Layouts/ThemePicker.js +89 -6
- package/dist/Components/Layouts/ThemeScript.d.ts +4 -2
- package/dist/Components/Layouts/ThemeScript.js +4 -2
- package/dist/Components/Layouts/ThemeSwitcher.d.ts +6 -3
- package/dist/Components/Layouts/ThemeSwitcher.js +9 -8
- package/dist/Components/Viewers/Avatar.d.ts +21 -0
- package/dist/Components/Viewers/Avatar.js +21 -0
- package/dist/Components/Viewers/DataTable.js +252 -62
- package/dist/Components/Viewers/DataTableColumns.d.ts +7 -3
- package/dist/Components/Viewers/DataTableColumns.js +25 -68
- package/dist/Components/Viewers/DataTableFilters.d.ts +21 -0
- package/dist/Components/Viewers/DataTableFilters.js +68 -0
- package/dist/Components/Viewers/DataTableParts.d.ts +28 -10
- package/dist/Components/Viewers/DataTableParts.js +42 -13
- package/dist/Helpers/AnimationHelpers.d.ts +11 -0
- package/dist/Helpers/AnimationHelpers.js +20 -2
- package/dist/Helpers/ApiModules.d.ts +3 -34
- package/dist/Helpers/ApiModules.js +3 -3
- package/dist/Helpers/ApiRoutes.d.ts +68 -26
- package/dist/Helpers/ApiRoutes.js +31 -27
- package/dist/Helpers/ArrayHelpers.d.ts +0 -0
- package/dist/Helpers/ArrayHelpers.js +2 -0
- package/dist/Helpers/CodeHelpers.d.ts +97 -0
- package/dist/Helpers/CodeHelpers.js +332 -0
- package/dist/Helpers/CrudBackend.d.ts +101 -8
- package/dist/Helpers/CrudBackend.js +172 -11
- package/dist/Helpers/CrudColumns.d.ts +51 -0
- package/dist/Helpers/CrudColumns.js +69 -0
- package/dist/Helpers/CrudHelpers.d.ts +50 -5
- package/dist/Helpers/CrudHelpers.js +109 -12
- package/dist/Helpers/ExcelBackend.d.ts +3 -3
- package/dist/Helpers/ExcelBackend.js +8 -2
- package/dist/Helpers/ExcelHelpers.d.ts +36 -8
- package/dist/Helpers/ExcelHelpers.js +55 -13
- package/dist/Helpers/FormStore.d.ts +9 -5
- package/dist/Helpers/FormStore.js +25 -5
- package/dist/Helpers/InputParamsHelpers.js +8 -0
- package/dist/Helpers/MessageBuilder.js +3 -0
- package/dist/Helpers/NexusHelpers.d.ts +0 -0
- package/dist/Helpers/NexusHelpers.js +1 -0
- package/dist/Helpers/NumberHelpers.d.ts +7 -1
- package/dist/Helpers/NumberHelpers.js +41 -3
- package/dist/Helpers/PopoverHelpers.d.ts +17 -5
- package/dist/Helpers/PopoverHelpers.js +52 -17
- package/dist/Helpers/PopoverHooks.d.ts +24 -0
- package/dist/Helpers/PopoverHooks.js +62 -0
- package/dist/Helpers/RequestCache.d.ts +35 -0
- package/dist/Helpers/RequestCache.js +113 -0
- package/dist/Helpers/RowsStore.d.ts +7 -2
- package/dist/Helpers/RowsStore.js +15 -3
- package/dist/Helpers/TableFilters.d.ts +25 -0
- package/dist/Helpers/TableFilters.js +162 -0
- package/dist/Helpers/TableHelpers.d.ts +7 -4
- package/dist/Helpers/TableHelpers.js +15 -6
- package/dist/Helpers/TableJoins.d.ts +70 -0
- package/dist/Helpers/TableJoins.js +250 -0
- package/dist/Helpers/TableStore.d.ts +16 -3
- package/dist/Helpers/TableStore.js +25 -5
- package/dist/Helpers/ThemeHelpers.d.ts +21 -4
- package/dist/Helpers/ThemeHelpers.js +46 -9
- package/dist/Helpers/TreeStore.d.ts +225 -11
- package/dist/Helpers/TreeStore.js +683 -90
- package/dist/Helpers/UserPreferences.d.ts +22 -0
- package/dist/Helpers/UserPreferences.js +116 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +67 -10
- package/dist/Interfaces/ExcelInterfaces.d.ts +12 -0
- package/dist/Interfaces/FormInterfaces.d.ts +37 -6
- package/dist/Interfaces/InputInterfaces.d.ts +24 -0
- package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
- package/dist/Interfaces/PreferenceInterfaces.d.ts +51 -0
- package/dist/Interfaces/PreferenceInterfaces.js +25 -0
- package/dist/Interfaces/TableInterfaces.d.ts +145 -16
- package/dist/Interfaces/ThemeInterfaces.d.ts +25 -0
- package/dist/Interfaces/ThemeInterfaces.js +15 -0
- package/dist/Services/ApiProxy.d.ts +36 -1
- package/dist/Services/ApiProxy.js +182 -52
- package/dist/Services/ApiProxyRules.d.ts +203 -0
- package/dist/Services/ApiProxyRules.js +426 -0
- package/dist/Services/ExcelApi.d.ts +3 -1
- package/dist/Services/ExcelApi.js +1 -1
- package/dist/Services/JoinSources.d.ts +48 -0
- package/dist/Services/JoinSources.js +74 -0
- package/dist/Services/ServerApi.d.ts +13 -2
- package/dist/Services/ServerApi.js +8 -3
- package/dist/Services/ThemeService.d.ts +18 -2
- package/dist/Services/ThemeService.js +80 -16
- package/dist/Shared.Index.d.ts +6 -0
- package/dist/Shared.Index.js +6 -0
- package/package.json +4 -2
- package/src/Styles/Nexus.Avatar.css +79 -0
- package/src/Styles/Nexus.Base.css +51 -4
- package/src/Styles/Nexus.Button.css +27 -0
- package/src/Styles/Nexus.Calendar.css +4 -6
- package/src/Styles/Nexus.Chat.css +5 -51
- package/src/Styles/Nexus.Choice.css +4 -6
- package/src/Styles/Nexus.Crud.css +16 -9
- package/src/Styles/Nexus.Dropdown.css +12 -61
- package/src/Styles/Nexus.Editor.css +4 -6
- package/src/Styles/Nexus.Excel.css +34 -15
- package/src/Styles/Nexus.Form.css +25 -67
- package/src/Styles/Nexus.Index.css +1 -0
- package/src/Styles/Nexus.Input.css +794 -637
- package/src/Styles/Nexus.Markdown.css +0 -28
- package/src/Styles/Nexus.Menu.css +8 -30
- package/src/Styles/Nexus.Popup.css +22 -29
- package/src/Styles/Nexus.RichText.css +2 -32
- package/src/Styles/Nexus.Rows.css +51 -10
- package/src/Styles/Nexus.Slider.css +3 -5
- package/src/Styles/Nexus.Tab.Buttons.css +20 -7
- package/src/Styles/Nexus.Table.css +1387 -1296
- package/src/Styles/Nexus.Theme.Picker.css +342 -232
- package/src/Styles/Nexus.Time.css +9 -12
- package/src/Styles/Nexus.Toast.css +6 -8
- package/src/Styles/Nexus.Tokens.css +17 -0
- package/src/Styles/Nexus.Tree.css +76 -11
|
@@ -2,6 +2,8 @@ import { formatDate } from "./DateHelpers.js";
|
|
|
2
2
|
import { formatDateTime } from "./DateTimeHelpers.js";
|
|
3
3
|
import { foldSearchText, searchWords } from "./DropdownHelpers.js";
|
|
4
4
|
import { formatNumber } from "./NumberHelpers.js";
|
|
5
|
+
import { filterMatcher } from "./TableFilters.js";
|
|
6
|
+
import { joinedValue } from "./TableJoins.js";
|
|
5
7
|
import { formatTime } from "./TimeHelpers.js";
|
|
6
8
|
/*
|
|
7
9
|
* Data table helpers, with no React: reading a column's value, its text, searching, sorting, and paging rows, and the
|
|
@@ -32,9 +34,13 @@ export function rowKeyText(row, rowKey) {
|
|
|
32
34
|
const key = typeof rowKey === "function" ? rowKey(row) : readPath(row, rowKey);
|
|
33
35
|
return key === undefined || key === null ? "" : String(key);
|
|
34
36
|
}
|
|
35
|
-
/** A column's value in a row. */
|
|
37
|
+
/** A column's value in a row: its own `value`, the record a `join` looked up, or the field. */
|
|
36
38
|
export function columnValue(column, row) {
|
|
37
|
-
|
|
39
|
+
if (column.value)
|
|
40
|
+
return column.value(row);
|
|
41
|
+
if (column.join)
|
|
42
|
+
return joinedValue(column.join, column.key, row);
|
|
43
|
+
return readPath(row, column.key);
|
|
38
44
|
}
|
|
39
45
|
/** The option a value stands for, if the column has options. Values compare as text, so 2 matches "2". */
|
|
40
46
|
export function columnOption(column, value) {
|
|
@@ -83,7 +89,7 @@ export function formatCellText(column, value, options = {}) {
|
|
|
83
89
|
if (number === null)
|
|
84
90
|
return String(value);
|
|
85
91
|
const decimals = column.decimals ?? (column.format === "money" ? 2 : undefined);
|
|
86
|
-
const text = formatNumber(number, { decimals, fixedDecimals: column.format === "money" || column.decimals !== undefined, grouping: true });
|
|
92
|
+
const text = formatNumber(number, { decimals, fixedDecimals: column.format === "money" || column.decimals !== undefined, grouping: true, locale: options.locale });
|
|
87
93
|
return `${prefix}${text}${column.format === "percent" && !suffix ? "%" : suffix}`;
|
|
88
94
|
}
|
|
89
95
|
case "date":
|
|
@@ -176,10 +182,10 @@ export function readTablePage(answer) {
|
|
|
176
182
|
return { rows, total, matches };
|
|
177
183
|
}
|
|
178
184
|
/**
|
|
179
|
-
* Searches, sorts, and pages rows the way the table shows them. `texts` caches each row's folded search text,
|
|
180
|
-
* search of 10,000 rows looks at each row once per keystroke.
|
|
185
|
+
* Searches, filters, sorts, and pages rows the way the table shows them. `texts` caches each row's folded search text,
|
|
186
|
+
* so a search of 10,000 rows looks at each row once per keystroke. `query.filters` are matched by `filterInputs`.
|
|
181
187
|
*/
|
|
182
|
-
export function queryRows(rows, columns, query, texts) {
|
|
188
|
+
export function queryRows(rows, columns, query, texts, filterInputs = []) {
|
|
183
189
|
let list = rows;
|
|
184
190
|
const words = searchWords(query.search);
|
|
185
191
|
if (words.length) {
|
|
@@ -193,6 +199,9 @@ export function queryRows(rows, columns, query, texts) {
|
|
|
193
199
|
return words.every(word => text.includes(word));
|
|
194
200
|
});
|
|
195
201
|
}
|
|
202
|
+
const test = filterMatcher(columns, filterInputs, query.filters);
|
|
203
|
+
if (test)
|
|
204
|
+
list = list.filter(test);
|
|
196
205
|
const sortColumn = query.sort ? columns.find(column => column.key === query.sort.key) : undefined;
|
|
197
206
|
if (sortColumn && query.sort)
|
|
198
207
|
list = sortRows(list, sortColumn, query.sort.direction);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ApiEndpoint } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
+
import type { TableJoin, TableJoinSource, TableJoinedRecord, TableRow } from "../Interfaces/TableInterfaces.ts";
|
|
3
|
+
/** How long an answer is kept before the next request asks again: the CRUD's own cache lifetime. */
|
|
4
|
+
export declare const JOIN_LIFETIME: number;
|
|
5
|
+
/** What is known of one key: its record, that its source has none, that it is on its way, or that the call failed. */
|
|
6
|
+
export type JoinedState = {
|
|
7
|
+
status: "ready";
|
|
8
|
+
record: TableJoinedRecord;
|
|
9
|
+
} | {
|
|
10
|
+
status: "missing";
|
|
11
|
+
} | {
|
|
12
|
+
status: "loading";
|
|
13
|
+
} | {
|
|
14
|
+
status: "failed";
|
|
15
|
+
};
|
|
16
|
+
export declare class TableJoins {
|
|
17
|
+
private sources;
|
|
18
|
+
private lists;
|
|
19
|
+
private listeners;
|
|
20
|
+
private version;
|
|
21
|
+
/** Called after every answer, and after answers are marked old. For `useSyncExternalStore`. */
|
|
22
|
+
subscribe: (listener: () => void) => (() => void);
|
|
23
|
+
/** A number that changes with every answer, so a table knows to draw again. */
|
|
24
|
+
getVersion: () => number;
|
|
25
|
+
private notify;
|
|
26
|
+
private cacheOf;
|
|
27
|
+
/**
|
|
28
|
+
* What is known of `key` in the source called `name`, for drawing: its record - an aged one too, until the new one is
|
|
29
|
+
* in - or that it is missing, on its way, or could not be read. A key nobody has asked for yet reads as on its way.
|
|
30
|
+
*/
|
|
31
|
+
peek(name: string, key: string): JoinedState;
|
|
32
|
+
/**
|
|
33
|
+
* Asks for every key the cache cannot answer fresh. The keys asked for in one moment - by every table and input on
|
|
34
|
+
* the page - go in one call per source, after the moment is over; a key already on its way is not asked again.
|
|
35
|
+
*/
|
|
36
|
+
request(source: TableJoinSource, keys: Iterable<string>): void;
|
|
37
|
+
private queue;
|
|
38
|
+
private send;
|
|
39
|
+
/**
|
|
40
|
+
* The records behind these keys, once they are in: what a picker shows for the values a form already holds. The
|
|
41
|
+
* calls and the answers are the ones the tables share, so a form opened from a table costs nothing more.
|
|
42
|
+
*/
|
|
43
|
+
records(source: TableJoinSource, keys: Iterable<string>): Promise<TableJoinedRecord[]>;
|
|
44
|
+
/**
|
|
45
|
+
* A whole list kept under `name` - a picker's options - loaded once per lifetime; a second ask while it loads waits for
|
|
46
|
+
* the same call. A list that failed is not kept, so the next ask tries again.
|
|
47
|
+
*/
|
|
48
|
+
list(name: string, load: (signal: AbortSignal) => Promise<readonly TableJoinedRecord[]>, lifetime?: number): Promise<readonly TableJoinedRecord[]>;
|
|
49
|
+
/**
|
|
50
|
+
* Marks answers as old - a source's, some of its keys, or every one - after the records behind them may have changed:
|
|
51
|
+
* a save, a move to the trash, a Reload. They are still drawn, and asked for again at the next request.
|
|
52
|
+
*/
|
|
53
|
+
expire(name?: string, keys?: Iterable<string>): void;
|
|
54
|
+
/** Drops a kept list (or every one), so the next ask loads it again. */
|
|
55
|
+
expireList(name?: string): void;
|
|
56
|
+
/** Forgets everything. For tests. */
|
|
57
|
+
clear(): void;
|
|
58
|
+
}
|
|
59
|
+
/** The page's one cache of looked-up records: every table and form shares it. */
|
|
60
|
+
export declare const tableJoins: TableJoins;
|
|
61
|
+
/** The name a source is kept under: its own, or an endpoint's module, method and path. */
|
|
62
|
+
export declare function joinSourceName(source: ApiEndpoint | TableJoinSource): string;
|
|
63
|
+
/** The key a row holds for a join, as text; null when it holds none. */
|
|
64
|
+
export declare function joinKey(join: TableJoin, row: TableRow): string | null;
|
|
65
|
+
/** What is known of the record a row's join names; null when the row names none. */
|
|
66
|
+
export declare function joinedState(join: TableJoin, row: TableRow): JoinedState | null;
|
|
67
|
+
/** A join column's value: the looked-up record's field, or undefined until it is in. */
|
|
68
|
+
export declare function joinedValue(join: TableJoin, columnKey: string, row: TableRow): unknown;
|
|
69
|
+
/** The keys rows hold for every join among these columns, by source: what one request per source asks for. */
|
|
70
|
+
export declare function joinKeys(joins: readonly TableJoin[], rows: readonly TableRow[]): Map<ApiEndpoint | TableJoinSource, Set<string>>;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { isApiEndpoint } from "./ApiRoutes.js";
|
|
2
|
+
// A cycle, and a harmless one: TableHelpers reads a join column through this module, and only calls are made across it.
|
|
3
|
+
import { readPath } from "./TableHelpers.js";
|
|
4
|
+
/*
|
|
5
|
+
* The records a page looks up by key, kept once for every table and form on it: a country's sub-region, named from the
|
|
6
|
+
* sub-regions' FindListing. It is the client side of a join - the database answers the row with the key alone, and the
|
|
7
|
+
* page names it - and these are its rules (the owner's caching rules, Project-Skills/README.RULES.md):
|
|
8
|
+
*
|
|
9
|
+
* - one call per source for every key wanted in the same moment, however many tables and inputs ask;
|
|
10
|
+
* - a call already out is waited for, never made twice;
|
|
11
|
+
* - an answer is kept for a lifetime, and one that has aged is still drawn while the new one is asked for, so a
|
|
12
|
+
* refresh never blanks a cell;
|
|
13
|
+
* - a key the source does not answer is remembered as missing ("Not found"), and a call that failed is not made
|
|
14
|
+
* again for a short while, so a backend that is down is not asked once per render.
|
|
15
|
+
*
|
|
16
|
+
* Nothing here knows about React or the API: a source loads, and whoever draws subscribes. Kept in memory, for the life
|
|
17
|
+
* of the page.
|
|
18
|
+
*/
|
|
19
|
+
/** How long an answer is kept before the next request asks again: the CRUD's own cache lifetime. */
|
|
20
|
+
export const JOIN_LIFETIME = 5 * 60 * 1000;
|
|
21
|
+
/** How long a key whose call failed waits before it is asked again. */
|
|
22
|
+
const RETRY_AFTER = 30 * 1000;
|
|
23
|
+
/** The most keys one call carries; more are sent in turn. A Nexus mass action takes 500, and so does a FindListing. */
|
|
24
|
+
const KEYS_PER_CALL = 500;
|
|
25
|
+
const LOADING = { status: "loading" };
|
|
26
|
+
const MISSING = { status: "missing" };
|
|
27
|
+
const FAILED = { status: "failed" };
|
|
28
|
+
export class TableJoins {
|
|
29
|
+
sources = new Map();
|
|
30
|
+
lists = new Map();
|
|
31
|
+
listeners = new Set();
|
|
32
|
+
version = 0;
|
|
33
|
+
/** Called after every answer, and after answers are marked old. For `useSyncExternalStore`. */
|
|
34
|
+
subscribe = (listener) => {
|
|
35
|
+
this.listeners.add(listener);
|
|
36
|
+
return () => {
|
|
37
|
+
this.listeners.delete(listener);
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
/** A number that changes with every answer, so a table knows to draw again. */
|
|
41
|
+
getVersion = () => this.version;
|
|
42
|
+
notify() {
|
|
43
|
+
this.version++;
|
|
44
|
+
for (const listener of this.listeners)
|
|
45
|
+
listener();
|
|
46
|
+
}
|
|
47
|
+
cacheOf(name) {
|
|
48
|
+
let cache = this.sources.get(name);
|
|
49
|
+
if (!cache) {
|
|
50
|
+
cache = { entries: new Map(), pending: new Map(), failedAt: new Map(), queued: null };
|
|
51
|
+
this.sources.set(name, cache);
|
|
52
|
+
}
|
|
53
|
+
return cache;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* What is known of `key` in the source called `name`, for drawing: its record - an aged one too, until the new one is
|
|
57
|
+
* in - or that it is missing, on its way, or could not be read. A key nobody has asked for yet reads as on its way.
|
|
58
|
+
*/
|
|
59
|
+
peek(name, key) {
|
|
60
|
+
const cache = this.sources.get(name);
|
|
61
|
+
const entry = cache?.entries.get(key);
|
|
62
|
+
if (entry)
|
|
63
|
+
return entry.record ? { status: "ready", record: entry.record } : MISSING;
|
|
64
|
+
if (cache?.failedAt.has(key) && !cache.pending.has(key))
|
|
65
|
+
return FAILED;
|
|
66
|
+
return LOADING;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Asks for every key the cache cannot answer fresh. The keys asked for in one moment - by every table and input on
|
|
70
|
+
* the page - go in one call per source, after the moment is over; a key already on its way is not asked again.
|
|
71
|
+
*/
|
|
72
|
+
request(source, keys) {
|
|
73
|
+
const cache = this.cacheOf(source.name);
|
|
74
|
+
const lifetime = source.lifetime ?? JOIN_LIFETIME;
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
for (const key of keys) {
|
|
77
|
+
if (!key || cache.pending.has(key))
|
|
78
|
+
continue;
|
|
79
|
+
const entry = cache.entries.get(key);
|
|
80
|
+
if (entry && now - entry.at < lifetime)
|
|
81
|
+
continue;
|
|
82
|
+
const failed = cache.failedAt.get(key);
|
|
83
|
+
if (!entry && failed !== undefined && now - failed < RETRY_AFTER)
|
|
84
|
+
continue;
|
|
85
|
+
this.queue(source, cache, key);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
queue(source, cache, key) {
|
|
89
|
+
let batch = cache.queued;
|
|
90
|
+
if (!batch) {
|
|
91
|
+
let finish = () => { };
|
|
92
|
+
const done = new Promise(resolve => (finish = resolve));
|
|
93
|
+
const queued = { keys: new Set(), done, finish };
|
|
94
|
+
cache.queued = batch = queued;
|
|
95
|
+
queueMicrotask(() => void this.send(source, cache, queued));
|
|
96
|
+
}
|
|
97
|
+
batch.keys.add(key);
|
|
98
|
+
cache.pending.set(key, batch.done);
|
|
99
|
+
}
|
|
100
|
+
async send(source, cache, batch) {
|
|
101
|
+
if (cache.queued === batch)
|
|
102
|
+
cache.queued = null;
|
|
103
|
+
const keys = [...batch.keys];
|
|
104
|
+
const keyField = source.key ?? "id";
|
|
105
|
+
let answered = 0;
|
|
106
|
+
try {
|
|
107
|
+
for (let start = 0; start < keys.length; start += KEYS_PER_CALL) {
|
|
108
|
+
const part = keys.slice(start, start + KEYS_PER_CALL);
|
|
109
|
+
const records = await source.load(part, new AbortController().signal);
|
|
110
|
+
const found = new Map();
|
|
111
|
+
for (const record of records) {
|
|
112
|
+
const key = record[keyField];
|
|
113
|
+
if (key !== undefined && key !== null)
|
|
114
|
+
found.set(String(key), record);
|
|
115
|
+
}
|
|
116
|
+
const at = Date.now();
|
|
117
|
+
for (const key of part) {
|
|
118
|
+
cache.entries.set(key, { record: found.get(key) ?? null, at });
|
|
119
|
+
cache.failedAt.delete(key);
|
|
120
|
+
}
|
|
121
|
+
answered += part.length;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// A key with an older answer keeps it; one with none is "could not be read", and waits before it is asked again.
|
|
126
|
+
const at = Date.now();
|
|
127
|
+
for (const key of keys.slice(answered))
|
|
128
|
+
if (!cache.entries.has(key))
|
|
129
|
+
cache.failedAt.set(key, at);
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
for (const key of keys)
|
|
133
|
+
if (cache.pending.get(key) === batch.done)
|
|
134
|
+
cache.pending.delete(key);
|
|
135
|
+
batch.finish();
|
|
136
|
+
this.notify();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The records behind these keys, once they are in: what a picker shows for the values a form already holds. The
|
|
141
|
+
* calls and the answers are the ones the tables share, so a form opened from a table costs nothing more.
|
|
142
|
+
*/
|
|
143
|
+
async records(source, keys) {
|
|
144
|
+
const wanted = [...new Set([...keys].filter(Boolean))];
|
|
145
|
+
this.request(source, wanted);
|
|
146
|
+
const cache = this.cacheOf(source.name);
|
|
147
|
+
await Promise.all(wanted.map(key => cache.pending.get(key)).filter(Boolean));
|
|
148
|
+
const found = [];
|
|
149
|
+
for (const key of wanted) {
|
|
150
|
+
const record = cache.entries.get(key)?.record;
|
|
151
|
+
if (record)
|
|
152
|
+
found.push(record);
|
|
153
|
+
}
|
|
154
|
+
return found;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* A whole list kept under `name` - a picker's options - loaded once per lifetime; a second ask while it loads waits for
|
|
158
|
+
* the same call. A list that failed is not kept, so the next ask tries again.
|
|
159
|
+
*/
|
|
160
|
+
list(name, load, lifetime = JOIN_LIFETIME) {
|
|
161
|
+
const kept = this.lists.get(name);
|
|
162
|
+
if (kept?.pending)
|
|
163
|
+
return kept.pending;
|
|
164
|
+
if (kept?.rows && Date.now() - kept.at < lifetime)
|
|
165
|
+
return Promise.resolve(kept.rows);
|
|
166
|
+
const pending = load(new AbortController().signal).then(rows => {
|
|
167
|
+
this.lists.set(name, { at: Date.now(), rows });
|
|
168
|
+
return rows;
|
|
169
|
+
}, (error) => {
|
|
170
|
+
this.lists.delete(name);
|
|
171
|
+
throw error;
|
|
172
|
+
});
|
|
173
|
+
this.lists.set(name, { ...kept, at: kept?.at ?? 0, pending });
|
|
174
|
+
return pending;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Marks answers as old - a source's, some of its keys, or every one - after the records behind them may have changed:
|
|
178
|
+
* a save, a move to the trash, a Reload. They are still drawn, and asked for again at the next request.
|
|
179
|
+
*/
|
|
180
|
+
expire(name, keys) {
|
|
181
|
+
const caches = name === undefined ? [...this.sources.values()] : [this.sources.get(name)].filter((cache) => Boolean(cache));
|
|
182
|
+
for (const cache of caches) {
|
|
183
|
+
if (keys) {
|
|
184
|
+
for (const key of keys) {
|
|
185
|
+
const entry = cache.entries.get(key);
|
|
186
|
+
if (entry)
|
|
187
|
+
entry.at = 0;
|
|
188
|
+
cache.failedAt.delete(key);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
for (const entry of cache.entries.values())
|
|
193
|
+
entry.at = 0;
|
|
194
|
+
cache.failedAt.clear();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (caches.length)
|
|
198
|
+
this.notify();
|
|
199
|
+
}
|
|
200
|
+
/** Drops a kept list (or every one), so the next ask loads it again. */
|
|
201
|
+
expireList(name) {
|
|
202
|
+
if (name === undefined)
|
|
203
|
+
this.lists.clear();
|
|
204
|
+
else
|
|
205
|
+
this.lists.delete(name);
|
|
206
|
+
}
|
|
207
|
+
/** Forgets everything. For tests. */
|
|
208
|
+
clear() {
|
|
209
|
+
this.sources.clear();
|
|
210
|
+
this.lists.clear();
|
|
211
|
+
this.notify();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/** The page's one cache of looked-up records: every table and form shares it. */
|
|
215
|
+
export const tableJoins = new TableJoins();
|
|
216
|
+
/** The name a source is kept under: its own, or an endpoint's module, method and path. */
|
|
217
|
+
export function joinSourceName(source) {
|
|
218
|
+
return isApiEndpoint(source) ? `${String(source.module)} ${source.method} ${source.path}` : source.name;
|
|
219
|
+
}
|
|
220
|
+
/** The key a row holds for a join, as text; null when it holds none. */
|
|
221
|
+
export function joinKey(join, row) {
|
|
222
|
+
const key = readPath(row, join.from);
|
|
223
|
+
return key === undefined || key === null || key === "" ? null : String(key);
|
|
224
|
+
}
|
|
225
|
+
/** What is known of the record a row's join names; null when the row names none. */
|
|
226
|
+
export function joinedState(join, row) {
|
|
227
|
+
const key = joinKey(join, row);
|
|
228
|
+
return key === null ? null : tableJoins.peek(joinSourceName(join.source), key);
|
|
229
|
+
}
|
|
230
|
+
/** A join column's value: the looked-up record's field, or undefined until it is in. */
|
|
231
|
+
export function joinedValue(join, columnKey, row) {
|
|
232
|
+
const state = joinedState(join, row);
|
|
233
|
+
return state?.status === "ready" ? readPath(state.record, join.field ?? columnKey) : undefined;
|
|
234
|
+
}
|
|
235
|
+
/** The keys rows hold for every join among these columns, by source: what one request per source asks for. */
|
|
236
|
+
export function joinKeys(joins, rows) {
|
|
237
|
+
const bySource = new Map();
|
|
238
|
+
for (const join of joins) {
|
|
239
|
+
const name = joinSourceName(join.source);
|
|
240
|
+
let entry = bySource.get(name);
|
|
241
|
+
if (!entry)
|
|
242
|
+
bySource.set(name, (entry = { source: join.source, keys: new Set() }));
|
|
243
|
+
for (const row of rows) {
|
|
244
|
+
const key = joinKey(join, row);
|
|
245
|
+
if (key !== null)
|
|
246
|
+
entry.keys.add(key);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return new Map([...bySource.values()].map(entry => [entry.source, entry.keys]));
|
|
250
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { InputParams } from "../Interfaces/FormInterfaces.ts";
|
|
1
2
|
import type { TableColumn, TableKey, TableLoader, TablePaging, TablePinSide, TableRow, TableSort, TableState } from "../Interfaces/TableInterfaces.ts";
|
|
2
3
|
/** What the table component tells its store on every render. */
|
|
3
4
|
export interface TableStoreOptions<TRow extends TableRow = TableRow> {
|
|
@@ -9,6 +10,8 @@ export interface TableStoreOptions<TRow extends TableRow = TableRow> {
|
|
|
9
10
|
paging: TablePaging;
|
|
10
11
|
maxSelection?: number;
|
|
11
12
|
onSelectionChange?: (rows: TRow[]) => void;
|
|
13
|
+
/** The advanced filter's inputs, as the table prepared them (`asFilterInput`). */
|
|
14
|
+
filterInputs?: readonly InputParams[];
|
|
12
15
|
}
|
|
13
16
|
/** How long a changed or added row stays highlighted. */
|
|
14
17
|
export declare const TABLE_FLASH_MS = 1600;
|
|
@@ -52,8 +55,8 @@ export declare class TableStore<TRow extends TableRow = TableRow> {
|
|
|
52
55
|
getRow(key: TableKey): TRow | undefined;
|
|
53
56
|
getSelected(): TRow[];
|
|
54
57
|
/**
|
|
55
|
-
* Every row the table would show for its query, in order: the whole searched and sorted list for a client
|
|
56
|
-
* page a server sent for a server table. An export and a print write these.
|
|
58
|
+
* Every row the table would show for its query, in order: the whole searched, filtered and sorted list for a client
|
|
59
|
+
* table, the page a server sent for a server table. An export and a print write these.
|
|
57
60
|
*/
|
|
58
61
|
getViewRows(): TRow[];
|
|
59
62
|
isSelected(row: TRow): boolean;
|
|
@@ -78,11 +81,21 @@ export declare class TableStore<TRow extends TableRow = TableRow> {
|
|
|
78
81
|
reload({ quiet, reset }?: TableReloadOptions): Promise<void>;
|
|
79
82
|
private take;
|
|
80
83
|
private fail;
|
|
81
|
-
/** Searches, sorts, and pages every row (client tables), then tells the table. */
|
|
84
|
+
/** Searches, filters, sorts, and pages every row (client tables), then tells the table. */
|
|
82
85
|
private view;
|
|
86
|
+
/**
|
|
87
|
+
* Some columns' values changed while the rows did not: the names a `join` column reads came in. A client table reads
|
|
88
|
+
* its search text again and searches, filters, sorts and pages anew; a server table draws again. Nothing loads.
|
|
89
|
+
*/
|
|
90
|
+
refreshValues(): void;
|
|
83
91
|
/** Changes the query: a client table shows it at once, a server table loads it. */
|
|
84
92
|
private query;
|
|
85
93
|
setSearch(search: string): void;
|
|
94
|
+
/**
|
|
95
|
+
* Filters the rows by these values, from the first page; `{}` clears. Values are normalized with `activeFilters`;
|
|
96
|
+
* the same filters change nothing. A client table shows the result at once; a server table loads it.
|
|
97
|
+
*/
|
|
98
|
+
setFilters(values: Readonly<Record<string, unknown>>): void;
|
|
86
99
|
setSort(sort: TableSort | null): void;
|
|
87
100
|
/** Sorts by a column: ascending, then descending, then back to no sort. */
|
|
88
101
|
toggleSort(key: string): void;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { errorMessage, isAbortError } from "./ApiResponses.js";
|
|
2
|
+
import { activeFilters } from "./TableFilters.js";
|
|
2
3
|
import { DEFAULT_MAX_SELECTION, DEFAULT_PAGE_SIZE, MIN_COLUMN_WIDTH, clampPage, moveColumnKey, nextSort, queryRows, readTablePage, rowKeyText } from "./TableHelpers.js";
|
|
3
4
|
/** How long a changed or added row stays highlighted. */
|
|
4
5
|
export const TABLE_FLASH_MS = 1600;
|
|
@@ -76,13 +77,13 @@ export class TableStore {
|
|
|
76
77
|
return [...this.state.selected.values()];
|
|
77
78
|
}
|
|
78
79
|
/**
|
|
79
|
-
* Every row the table would show for its query, in order: the whole searched and sorted list for a client
|
|
80
|
-
* page a server sent for a server table. An export and a print write these.
|
|
80
|
+
* Every row the table would show for its query, in order: the whole searched, filtered and sorted list for a client
|
|
81
|
+
* table, the page a server sent for a server table. An export and a print write these.
|
|
81
82
|
*/
|
|
82
83
|
getViewRows() {
|
|
83
84
|
if (this.options.paging === "server")
|
|
84
85
|
return [...this.state.rows];
|
|
85
|
-
return queryRows(this.all, this.options.columns, { ...this.state.query, page: 1, pageSize: Number.MAX_SAFE_INTEGER }, this.texts).rows;
|
|
86
|
+
return queryRows(this.all, this.options.columns, { ...this.state.query, page: 1, pageSize: Number.MAX_SAFE_INTEGER }, this.texts, this.options.filterInputs).rows;
|
|
86
87
|
}
|
|
87
88
|
isSelected(row) {
|
|
88
89
|
return this.state.selected.has(this.keyOf(row));
|
|
@@ -220,14 +221,14 @@ export class TableStore {
|
|
|
220
221
|
this.controller = null;
|
|
221
222
|
this.set({ status: "error", refreshing: false, error: errorMessage(error, "The rows could not be loaded.") });
|
|
222
223
|
}
|
|
223
|
-
/** Searches, sorts, and pages every row (client tables), then tells the table. */
|
|
224
|
+
/** Searches, filters, sorts, and pages every row (client tables), then tells the table. */
|
|
224
225
|
view(extra = {}) {
|
|
225
226
|
if (this.options.paging === "server") {
|
|
226
227
|
this.set(extra);
|
|
227
228
|
return;
|
|
228
229
|
}
|
|
229
230
|
const query = { ...this.state.query, ...extra.query };
|
|
230
|
-
const result = queryRows(this.all, this.options.columns, query, this.texts);
|
|
231
|
+
const result = queryRows(this.all, this.options.columns, query, this.texts, this.options.filterInputs);
|
|
231
232
|
const page = result.page;
|
|
232
233
|
this.set({
|
|
233
234
|
...extra,
|
|
@@ -238,6 +239,15 @@ export class TableStore {
|
|
|
238
239
|
query: page === query.page ? query : { ...query, page },
|
|
239
240
|
});
|
|
240
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Some columns' values changed while the rows did not: the names a `join` column reads came in. A client table reads
|
|
244
|
+
* its search text again and searches, filters, sorts and pages anew; a server table draws again. Nothing loads.
|
|
245
|
+
*/
|
|
246
|
+
refreshValues() {
|
|
247
|
+
this.texts = new WeakMap();
|
|
248
|
+
if (this.state.status === "ready")
|
|
249
|
+
this.view();
|
|
250
|
+
}
|
|
241
251
|
/** Changes the query: a client table shows it at once, a server table loads it. */
|
|
242
252
|
query(changes) {
|
|
243
253
|
const query = { ...this.state.query, ...changes };
|
|
@@ -254,6 +264,16 @@ export class TableStore {
|
|
|
254
264
|
return;
|
|
255
265
|
this.query({ search: text, page: 1 });
|
|
256
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Filters the rows by these values, from the first page; `{}` clears. Values are normalized with `activeFilters`;
|
|
269
|
+
* the same filters change nothing. A client table shows the result at once; a server table loads it.
|
|
270
|
+
*/
|
|
271
|
+
setFilters(values) {
|
|
272
|
+
const filters = activeFilters(this.options.filterInputs ?? [], values);
|
|
273
|
+
if (JSON.stringify(filters) === JSON.stringify(this.state.query.filters))
|
|
274
|
+
return;
|
|
275
|
+
this.query({ filters, page: 1 });
|
|
276
|
+
}
|
|
257
277
|
setSort(sort) {
|
|
258
278
|
this.query({ sort, page: 1 });
|
|
259
279
|
}
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
import { type ThemeMode, type ThemeOption } from "../Interfaces/ThemeInterfaces.ts";
|
|
1
|
+
import { type AnimationMode, type Density, type ThemeMode, type ThemeOption } from "../Interfaces/ThemeInterfaces.ts";
|
|
2
2
|
export declare const THEME_ID_PATTERN: RegExp;
|
|
3
|
+
/** The operating system's "less motion" setting, which `system` animations follow. */
|
|
4
|
+
export declare const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
|
|
5
|
+
/** What `data-nx-motion` says for an animations choice, given whether the operating system asks for less motion. */
|
|
6
|
+
export declare function motionFor(animations: AnimationMode, systemReduces: boolean): "reduce" | "full";
|
|
7
|
+
/**
|
|
8
|
+
* Whether this is a development build. Bundlers (Next.js, Vite, webpack) write the build's mode in place of
|
|
9
|
+
* `process.env.NODE_ENV`, and a server reads its own; a page with neither counts as production.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isDevelopmentBuild(): boolean;
|
|
3
12
|
export interface ThemeScriptOptions {
|
|
4
13
|
/** Themes a saved choice may name. Default: every built-in theme. */
|
|
5
14
|
themes?: readonly ThemeOption[];
|
|
@@ -7,9 +16,17 @@ export interface ThemeScriptOptions {
|
|
|
7
16
|
defaultTheme?: string;
|
|
8
17
|
/** Mode before the visitor chooses one. Default `system`. */
|
|
9
18
|
defaultMode?: ThemeMode;
|
|
19
|
+
/** Density before the visitor chooses one. Default `compact`. */
|
|
20
|
+
defaultDensity?: Density;
|
|
21
|
+
/** Animations before the visitor chooses. Default `system`, which follows `prefers-reduced-motion`. */
|
|
22
|
+
defaultAnimations?: AnimationMode;
|
|
23
|
+
/** Shows the saved scale grid. Default: in a development build only, so a grid left on never reaches production. */
|
|
24
|
+
grid?: boolean;
|
|
10
25
|
}
|
|
11
26
|
/**
|
|
12
|
-
* JavaScript that sets the saved theme and
|
|
13
|
-
* the
|
|
27
|
+
* JavaScript that sets the saved theme, mode, density, animations and scale grid on `<html>` before the page paints,
|
|
28
|
+
* so a page never flashes the defaults first. It also works out `data-nx-motion` from the animations choice and the
|
|
29
|
+
* system setting, and keeps it right when the system setting changes. `ThemeScript` renders it; use this directly
|
|
30
|
+
* only outside React.
|
|
14
31
|
*/
|
|
15
|
-
export declare function themeScript({ themes, defaultTheme, defaultMode }?: ThemeScriptOptions): string;
|
|
32
|
+
export declare function themeScript({ themes, defaultTheme, defaultMode, defaultDensity, defaultAnimations, grid, }?: ThemeScriptOptions): string;
|
|
@@ -1,16 +1,53 @@
|
|
|
1
|
-
import { DEFAULT_THEME, NEXUS_THEMES, THEME_ATTRIBUTE, THEME_MODE_ATTRIBUTE, THEME_MODE_STORAGE_KEY, THEME_STORAGE_KEY, } from "../Interfaces/ThemeInterfaces.js";
|
|
1
|
+
import { ANIMATIONS_ATTRIBUTE, ANIMATIONS_STORAGE_KEY, DEFAULT_THEME, DENSITY_ATTRIBUTE, DENSITY_STORAGE_KEY, GRID_ATTRIBUTE, GRID_STORAGE_KEY, MOTION_ATTRIBUTE, NEXUS_THEMES, THEME_ATTRIBUTE, THEME_MODE_ATTRIBUTE, THEME_MODE_STORAGE_KEY, THEME_STORAGE_KEY, } from "../Interfaces/ThemeInterfaces.js";
|
|
2
2
|
export const THEME_ID_PATTERN = /^[a-z0-9-]+$/;
|
|
3
|
+
/** The operating system's "less motion" setting, which `system` animations follow. */
|
|
4
|
+
export const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
|
|
5
|
+
/** What `data-nx-motion` says for an animations choice, given whether the operating system asks for less motion. */
|
|
6
|
+
export function motionFor(animations, systemReduces) {
|
|
7
|
+
return animations === "off" || (animations !== "on" && systemReduces) ? "reduce" : "full";
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Whether this is a development build. Bundlers (Next.js, Vite, webpack) write the build's mode in place of
|
|
11
|
+
* `process.env.NODE_ENV`, and a server reads its own; a page with neither counts as production.
|
|
12
|
+
*/
|
|
13
|
+
export function isDevelopmentBuild() {
|
|
14
|
+
try {
|
|
15
|
+
return process.env.NODE_ENV !== "production";
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
3
21
|
/**
|
|
4
|
-
* JavaScript that sets the saved theme and
|
|
5
|
-
* the
|
|
22
|
+
* JavaScript that sets the saved theme, mode, density, animations and scale grid on `<html>` before the page paints,
|
|
23
|
+
* so a page never flashes the defaults first. It also works out `data-nx-motion` from the animations choice and the
|
|
24
|
+
* system setting, and keeps it right when the system setting changes. `ThemeScript` renders it; use this directly
|
|
25
|
+
* only outside React.
|
|
6
26
|
*/
|
|
7
|
-
export function themeScript({ themes = NEXUS_THEMES, defaultTheme = DEFAULT_THEME, defaultMode = "system" } = {}) {
|
|
8
|
-
const
|
|
9
|
-
|
|
27
|
+
export function themeScript({ themes = NEXUS_THEMES, defaultTheme = DEFAULT_THEME, defaultMode = "system", defaultDensity = "compact", defaultAnimations = "system", grid = isDevelopmentBuild(), } = {}) {
|
|
28
|
+
const config = {
|
|
29
|
+
ids: themes.map(theme => theme.id).filter(id => THEME_ID_PATTERN.test(id)),
|
|
30
|
+
theme: defaultTheme,
|
|
31
|
+
mode: defaultMode,
|
|
32
|
+
density: defaultDensity,
|
|
33
|
+
animations: defaultAnimations,
|
|
34
|
+
grid,
|
|
35
|
+
keys: [THEME_STORAGE_KEY, THEME_MODE_STORAGE_KEY, DENSITY_STORAGE_KEY, ANIMATIONS_STORAGE_KEY, GRID_STORAGE_KEY],
|
|
36
|
+
attributes: [THEME_ATTRIBUTE, THEME_MODE_ATTRIBUTE, DENSITY_ATTRIBUTE, ANIMATIONS_ATTRIBUTE, GRID_ATTRIBUTE, MOTION_ATTRIBUTE],
|
|
37
|
+
reduce: REDUCED_MOTION_QUERY,
|
|
38
|
+
};
|
|
10
39
|
// "<" is escaped so no value can close the <script> element.
|
|
11
40
|
const json = JSON.stringify(config).replace(/</g, "\\u003c");
|
|
12
|
-
return ("(function(c){var d=document.documentElement,t=c
|
|
13
|
-
"try{
|
|
14
|
-
"if(c
|
|
41
|
+
return ("(function(c){var d=document.documentElement,k=c.keys,a=c.attributes,t=c.theme,m=c.mode,n=c.density,o=c.animations,g='off',v;" +
|
|
42
|
+
"function r(i){try{return localStorage.getItem(k[i])}catch(e){return null}}" +
|
|
43
|
+
"v=r(0);if(c.ids.indexOf(v)>-1)t=v;v=r(1);if(v==='light'||v==='dark'||v==='system')m=v;" +
|
|
44
|
+
"v=r(2);if(v==='compact'||v==='cozy')n=v;v=r(3);if(v==='on'||v==='off'||v==='system')o=v;" +
|
|
45
|
+
"if(c.grid){v=r(4);if(v==='px'||v==='cm')g=v}" +
|
|
46
|
+
"if(c.ids.indexOf(t)>-1)d.setAttribute(a[0],t);if(m==='light'||m==='dark')d.setAttribute(a[1],m);" +
|
|
47
|
+
"d.setAttribute(a[2],n==='cozy'?n:'compact');if(o==='on'||o==='off')d.setAttribute(a[3],o);if(g!=='off')d.setAttribute(a[4],g);" +
|
|
48
|
+
// Motion follows the system setting live, read against whatever the animations choice is by then.
|
|
49
|
+
"var q=window.matchMedia?matchMedia(c.reduce):null;" +
|
|
50
|
+
"function f(){var x=d.getAttribute(a[3]);d.setAttribute(a[5],x==='off'||(x!=='on'&&q&&q.matches)?'reduce':'full')}" +
|
|
51
|
+
"f();if(q&&q.addEventListener)q.addEventListener('change',f)" +
|
|
15
52
|
`})(${json})`);
|
|
16
53
|
}
|