nexus-shared 3.0.0 → 3.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/CHANGELOG.md +446 -262
- package/LICENSE +21 -21
- package/README.md +1084 -1000
- package/dist/Client.Index.d.ts +3 -0
- package/dist/Client.Index.js +3 -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 +177 -58
- package/dist/Components/Forms/CrudParts.d.ts +5 -1
- package/dist/Components/Forms/CrudParts.js +37 -5
- package/dist/Components/Forms/ExcelImport.d.ts +23 -7
- package/dist/Components/Forms/ExcelImport.js +189 -60
- package/dist/Components/Forms/Form.js +5 -13
- package/dist/Components/Forms/SubmitForm.d.ts +11 -2
- package/dist/Components/Forms/SubmitForm.js +23 -12
- package/dist/Components/Inputs/DropdownParts.d.ts +1 -1
- package/dist/Components/Inputs/DropdownParts.js +4 -6
- package/dist/Components/Inputs/NumberBox.js +24 -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/TimePanel.js +4 -3
- package/dist/Components/Layouts/Popup.js +1 -4
- package/dist/Components/Layouts/ThemePicker.d.ts +20 -3
- package/dist/Components/Layouts/ThemePicker.js +77 -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 +192 -44
- 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 +2 -2
- package/dist/Components/Viewers/DataTableParts.js +3 -3
- package/dist/Helpers/AnimationHelpers.d.ts +11 -0
- package/dist/Helpers/AnimationHelpers.js +20 -2
- package/dist/Helpers/CrudBackend.d.ts +7 -4
- package/dist/Helpers/CrudBackend.js +4 -2
- 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/MessageBuilder.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/RowsStore.d.ts +7 -2
- package/dist/Helpers/RowsStore.js +15 -3
- package/dist/Helpers/TableExport.js +12 -12
- package/dist/Helpers/TableFilters.d.ts +25 -0
- package/dist/Helpers/TableFilters.js +162 -0
- package/dist/Helpers/TableHelpers.d.ts +6 -3
- package/dist/Helpers/TableHelpers.js +8 -4
- package/dist/Helpers/TableStore.d.ts +11 -3
- package/dist/Helpers/TableStore.js +16 -5
- package/dist/Helpers/ThemeHelpers.d.ts +21 -4
- package/dist/Helpers/ThemeHelpers.js +46 -9
- package/dist/Helpers/UserPreferences.d.ts +14 -0
- package/dist/Helpers/UserPreferences.js +99 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +20 -1
- package/dist/Interfaces/ExcelInterfaces.d.ts +12 -0
- package/dist/Interfaces/FormInterfaces.d.ts +33 -5
- package/dist/Interfaces/PreferenceInterfaces.d.ts +43 -0
- package/dist/Interfaces/PreferenceInterfaces.js +28 -0
- package/dist/Interfaces/TableInterfaces.d.ts +59 -12
- package/dist/Interfaces/ThemeInterfaces.d.ts +25 -0
- package/dist/Interfaces/ThemeInterfaces.js +15 -0
- package/dist/Services/ExcelApi.d.ts +3 -1
- package/dist/Services/ExcelApi.js +1 -1
- package/dist/Services/ThemeService.d.ts +18 -2
- package/dist/Services/ThemeService.js +80 -16
- package/dist/Shared.Index.d.ts +2 -0
- package/dist/Shared.Index.js +2 -0
- package/package.json +85 -85
- package/src/Styles/Nexus.Avatar.css +79 -0
- package/src/Styles/Nexus.Base.css +117 -70
- package/src/Styles/Nexus.Button.css +180 -153
- package/src/Styles/Nexus.Calendar.css +273 -275
- package/src/Styles/Nexus.Chat.css +1443 -1489
- package/src/Styles/Nexus.Choice.css +343 -345
- package/src/Styles/Nexus.Crud.css +168 -161
- package/src/Styles/Nexus.DateTime.css +81 -81
- package/src/Styles/Nexus.Dropdown.css +725 -774
- package/src/Styles/Nexus.Editor.css +295 -297
- package/src/Styles/Nexus.Excel.css +389 -370
- package/src/Styles/Nexus.Form.css +279 -321
- package/src/Styles/Nexus.Index.css +37 -36
- package/src/Styles/Nexus.Input.css +602 -637
- package/src/Styles/Nexus.Markdown.css +237 -265
- package/src/Styles/Nexus.Menu.css +237 -259
- package/src/Styles/Nexus.Popup.css +724 -731
- package/src/Styles/Nexus.RichText.css +2 -32
- package/src/Styles/Nexus.Rows.css +637 -596
- package/src/Styles/Nexus.Slider.css +276 -278
- package/src/Styles/Nexus.Tab.Buttons.css +432 -419
- package/src/Styles/Nexus.Table.css +1386 -1296
- package/src/Styles/Nexus.Theme.Picker.css +286 -232
- package/src/Styles/Nexus.Themes.css +194 -194
- package/src/Styles/Nexus.Time.css +144 -147
- package/src/Styles/Nexus.Toast.css +263 -265
- package/src/Styles/Nexus.Tokens.css +89 -72
- package/src/Styles/Nexus.Tree.css +431 -435
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ENumberScales } from "../Interfaces/PreferenceInterfaces.js";
|
|
1
2
|
/** Default locale for number text. Fixed rather than the browser's, so server-rendered text matches the browser's. */
|
|
2
3
|
export const DEFAULT_NUMBER_LOCALE = "en-US";
|
|
3
4
|
/** Most digits a number field takes. A JavaScript number holds 15 significant digits exactly; beyond that, digits change. */
|
|
@@ -9,9 +10,43 @@ const BIDI_MARKS = /[\u061c\u200e\u200f]/g;
|
|
|
9
10
|
// U+2212, the typographic minus sign some locales use.
|
|
10
11
|
const MINUS_SIGN = "\u2212";
|
|
11
12
|
const SPACES = /\s/g;
|
|
12
|
-
|
|
13
|
+
/*
|
|
14
|
+
* A person's number settings as a locale: the scale picks where the groups fall (en-US, or en-IN for lakh and crore)
|
|
15
|
+
* and the separator what marks them. A pair Intl already has is that locale; any other is a key of three parts,
|
|
16
|
+
* "en-IN|.|,", that the helpers here read as the base locale's grouping with these two marks. Everything that takes
|
|
17
|
+
* a `locale` (a number box, a table cell, a message) therefore takes the person's settings without knowing of them.
|
|
18
|
+
*/
|
|
19
|
+
const SEPARATOR_MARKS = {
|
|
20
|
+
period: { group: ".", decimal: "," },
|
|
21
|
+
space: { group: "\u00a0", decimal: "." },
|
|
22
|
+
none: { group: "", decimal: "." },
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* The locale for a number scale and a thousands separator: `en-US` (1,234,567.5) and `en-IN` (12,34,567.5) as they are,
|
|
26
|
+
* else a key such as "en-US|.|," (1.234.567,5) that `formatNumber`, `parseNumber` and `getNumberSymbols` read.
|
|
27
|
+
*/
|
|
28
|
+
export function numberLocale(scale, thousands) {
|
|
29
|
+
const base = scale === ENumberScales.SouthAsian ? "en-IN" : DEFAULT_NUMBER_LOCALE;
|
|
30
|
+
if (thousands === "comma" || !(thousands in SEPARATOR_MARKS))
|
|
31
|
+
return base;
|
|
32
|
+
const { group, decimal } = SEPARATOR_MARKS[thousands];
|
|
33
|
+
return `${base}|${group}|${decimal}`;
|
|
34
|
+
}
|
|
35
|
+
/** A locale key from `numberLocale`: its base locale and its own marks, or `null` for a plain locale. */
|
|
36
|
+
function customMarks(locale) {
|
|
37
|
+
if (!locale.includes("|"))
|
|
38
|
+
return null;
|
|
39
|
+
const [base = DEFAULT_NUMBER_LOCALE, group = ",", decimal = "."] = locale.split("|");
|
|
40
|
+
return { base, group, decimal };
|
|
41
|
+
}
|
|
42
|
+
/** The decimal and group separators of a locale (or of a key from `numberLocale`). */
|
|
13
43
|
export function getNumberSymbols(locale = DEFAULT_NUMBER_LOCALE) {
|
|
14
44
|
let symbols = symbolsByLocale.get(locale);
|
|
45
|
+
const custom = symbols ? null : customMarks(locale);
|
|
46
|
+
if (custom) {
|
|
47
|
+
symbols = { decimal: custom.decimal, group: custom.group };
|
|
48
|
+
symbolsByLocale.set(locale, symbols);
|
|
49
|
+
}
|
|
15
50
|
if (!symbols) {
|
|
16
51
|
const parts = new Intl.NumberFormat(locale).formatToParts(1234567.5);
|
|
17
52
|
symbols = {
|
|
@@ -33,9 +68,10 @@ export function formatNumber(value, options = {}) {
|
|
|
33
68
|
const maxDecimals = decimals ?? MAX_NUMBER_DIGITS;
|
|
34
69
|
const minDecimals = fixedDecimals ? maxDecimals : 0;
|
|
35
70
|
const key = `${locale}|${minDecimals}|${maxDecimals}|${grouping}`;
|
|
71
|
+
const custom = customMarks(locale);
|
|
36
72
|
let formatter = formatters.get(key);
|
|
37
73
|
if (!formatter) {
|
|
38
|
-
formatter = new Intl.NumberFormat(locale, {
|
|
74
|
+
formatter = new Intl.NumberFormat(custom?.base ?? locale, {
|
|
39
75
|
numberingSystem: "latn",
|
|
40
76
|
useGrouping: grouping,
|
|
41
77
|
minimumFractionDigits: minDecimals,
|
|
@@ -44,7 +80,9 @@ export function formatNumber(value, options = {}) {
|
|
|
44
80
|
formatters.set(key, formatter);
|
|
45
81
|
}
|
|
46
82
|
// Rounded first, the same way as roundDecimal, which also turns -0.001 into 0 rather than "-0".
|
|
47
|
-
|
|
83
|
+
const text = formatter.format(roundDecimal(value, maxDecimals)).replace(BIDI_MARKS, "").replace(MINUS_SIGN, "-");
|
|
84
|
+
// The base locales write "," between groups and "." before decimals; a key's own marks take their places.
|
|
85
|
+
return custom ? text.replace(/[,.]/g, mark => (mark === "," ? custom.group : custom.decimal)) : text;
|
|
48
86
|
}
|
|
49
87
|
/**
|
|
50
88
|
* Reads a number written in the locale's format: "1,250.50" gives 1250.5, and in `de-DE` "1.250,50" does.
|
|
@@ -20,8 +20,9 @@ export interface PlaceUnderAnchorOptions {
|
|
|
20
20
|
arrowAt?: DOMRect;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
|
-
* Places a panel under an anchor, or above it when
|
|
24
|
-
* edge and kept inside the window, with an arrow aimed at the anchor.
|
|
23
|
+
* Places a panel under an anchor, or above it when it fits only there, or on the side with more room when it fits on
|
|
24
|
+
* neither; aligned to the anchor's start or end edge and kept inside the window, with an arrow aimed at the anchor. A
|
|
25
|
+
* panel that scrolls (`.nx-popover__scroll`) is never taller than the room on its side. Returns whether it went above.
|
|
25
26
|
*/
|
|
26
27
|
export declare function placeUnderAnchor(panel: HTMLElement, anchor: DOMRect, options?: PlaceUnderAnchorOptions): boolean;
|
|
27
28
|
export interface PlaceListPanelOptions {
|
|
@@ -57,11 +58,22 @@ export interface PlaceAtPointOptions {
|
|
|
57
58
|
/**
|
|
58
59
|
* Places a panel at a point, such as where the user right-clicked: to the right of and below the point when there
|
|
59
60
|
* is room, otherwise to the left of or above it, and never past the window's edges. With an arrow, the panel is set
|
|
60
|
-
* back far enough along both axes for the arrow's tip to land on the point.
|
|
61
|
+
* back far enough along both axes for the arrow's tip to land on the point. A panel that scrolls is capped to the
|
|
62
|
+
* room on its side, as under an anchor.
|
|
61
63
|
*/
|
|
62
64
|
export declare function placeAtPoint(panel: HTMLElement, x: number, y: number, options?: PlaceAtPointOptions): void;
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Runs `place` again, once per frame at most, while the page scrolls, the window changes size, or `panel` does (its
|
|
67
|
+
* content grew). Returns the stop function. A frame later, never inside the observer, so a panel that `place` resizes
|
|
68
|
+
* is measured once more and settles instead of looping.
|
|
69
|
+
*/
|
|
70
|
+
export declare function followAnchor(place: () => void, panel?: Element): () => void;
|
|
71
|
+
/**
|
|
72
|
+
* Shows a panel with its scroller (`.nx-popover__scroll`) at the top. The browser keeps where a panel was scrolled
|
|
73
|
+
* while it was shut, and a list that opens part-way down has lost its heading and its first rows. Does nothing on
|
|
74
|
+
* browsers without popover support.
|
|
75
|
+
*/
|
|
76
|
+
export declare function showPanel(panel: HTMLElement): void;
|
|
65
77
|
/** Whether the popover is shown. Safe on browsers without popover support, where it is never "open". */
|
|
66
78
|
export declare function isPopoverOpen(panel: HTMLElement | null): boolean;
|
|
67
79
|
/** How far the arrow reaches past the panel's edge: its depth, less the panel's border it is drawn over. */
|
|
@@ -8,19 +8,16 @@
|
|
|
8
8
|
* knows both rectangles; `.nx-popover` draws it from what they write (see "The arrow" below).
|
|
9
9
|
*/
|
|
10
10
|
/**
|
|
11
|
-
* Places a panel under an anchor, or above it when
|
|
12
|
-
* edge and kept inside the window, with an arrow aimed at the anchor.
|
|
11
|
+
* Places a panel under an anchor, or above it when it fits only there, or on the side with more room when it fits on
|
|
12
|
+
* neither; aligned to the anchor's start or end edge and kept inside the window, with an arrow aimed at the anchor. A
|
|
13
|
+
* panel that scrolls (`.nx-popover__scroll`) is never taller than the room on its side. Returns whether it went above.
|
|
13
14
|
*/
|
|
14
15
|
export function placeUnderAnchor(panel, anchor, options = {}) {
|
|
15
16
|
const { align = "start", margin = 8, arrow = true } = options;
|
|
16
17
|
// The arrow needs its reach between the panel and the anchor, or it would be drawn over the anchor.
|
|
17
18
|
const gap = Math.max(options.gap ?? 4, arrow ? ARROW_REACH : 0);
|
|
18
|
-
const width = panel.
|
|
19
|
-
const
|
|
20
|
-
const below = anchor.bottom + gap;
|
|
21
|
-
const above = anchor.top - gap - height;
|
|
22
|
-
const fitsBelow = below + height <= window.innerHeight - margin;
|
|
23
|
-
const top = fitsBelow || above < margin ? below : above;
|
|
19
|
+
const { side, width, height } = fitToRoom(panel, window.innerHeight - anchor.bottom - gap - margin, anchor.top - gap - margin);
|
|
20
|
+
const top = side === "below" ? anchor.bottom + gap : anchor.top - gap - height;
|
|
24
21
|
const wanted = align === "end" ? anchor.right - width : anchor.left;
|
|
25
22
|
// The arrow points at the middle of what it belongs to. The panel's own style is read once: this runs again on
|
|
26
23
|
// every animation frame while the page scrolls under an open panel.
|
|
@@ -30,7 +27,6 @@ export function placeUnderAnchor(panel, anchor, options = {}) {
|
|
|
30
27
|
const left = Math.max(margin, Math.min(arrow ? centred(panel, target, wanted, width, middle, style) : wanted, window.innerWidth - width - margin));
|
|
31
28
|
panel.style.top = `${Math.round(top)}px`;
|
|
32
29
|
panel.style.left = `${Math.round(left)}px`;
|
|
33
|
-
const side = top === below ? "below" : "above";
|
|
34
30
|
const box = { left: Math.round(left), top: Math.round(top), width, height };
|
|
35
31
|
aimArrow(panel, side, box, arrow ? { at: middle, facing: side === "below" ? target.bottom : target.top, room: gap } : null, style);
|
|
36
32
|
return side === "above";
|
|
@@ -71,24 +67,42 @@ function setStyle(element, property, value) {
|
|
|
71
67
|
if (element.style[property] !== value)
|
|
72
68
|
element.style[property] = value;
|
|
73
69
|
}
|
|
70
|
+
/** The room a panel was given on its side, which a panel with a scroller reads as its tallest (`.nx-popover`). */
|
|
71
|
+
const ROOM = "--nx-popover-room";
|
|
72
|
+
/**
|
|
73
|
+
* Picks a panel's side by its whole height: below while it fits there, above while it fits only there, else the side
|
|
74
|
+
* with more room. That side's room is written on the panel, which a panel with `.nx-popover__scroll` takes as its
|
|
75
|
+
* tallest, so it ends inside the window and scrolls inside itself. Returns the side and the size it has.
|
|
76
|
+
*
|
|
77
|
+
* The whole height is the panel's own plus what its scroller hides, read without changing anything: this runs on every
|
|
78
|
+
* scroll while the panel is open, its scroller's own included, and lifting the cap to measure would throw the list
|
|
79
|
+
* back to its top.
|
|
80
|
+
*/
|
|
81
|
+
function fitToRoom(panel, roomBelow, roomAbove) {
|
|
82
|
+
const scroller = panel.querySelector(":scope > .nx-popover__scroll");
|
|
83
|
+
const whole = panel.offsetHeight + (scroller ? scroller.scrollHeight - scroller.clientHeight : 0);
|
|
84
|
+
const side = whole <= roomBelow || (whole > roomAbove && roomBelow >= roomAbove) ? "below" : "above";
|
|
85
|
+
const room = `${Math.max(0, Math.floor(side === "below" ? roomBelow : roomAbove))}px`;
|
|
86
|
+
if (panel.style.getPropertyValue(ROOM) !== room)
|
|
87
|
+
panel.style.setProperty(ROOM, room);
|
|
88
|
+
return { side, width: panel.offsetWidth, height: panel.offsetHeight };
|
|
89
|
+
}
|
|
74
90
|
/**
|
|
75
91
|
* Places a panel at a point, such as where the user right-clicked: to the right of and below the point when there
|
|
76
92
|
* is room, otherwise to the left of or above it, and never past the window's edges. With an arrow, the panel is set
|
|
77
|
-
* back far enough along both axes for the arrow's tip to land on the point.
|
|
93
|
+
* back far enough along both axes for the arrow's tip to land on the point. A panel that scrolls is capped to the
|
|
94
|
+
* room on its side, as under an anchor.
|
|
78
95
|
*/
|
|
79
96
|
export function placeAtPoint(panel, x, y, options = {}) {
|
|
80
97
|
const { margin = 8, arrow = true } = options;
|
|
81
|
-
const width = panel.offsetWidth;
|
|
82
|
-
const height = panel.offsetHeight;
|
|
83
98
|
// The tip sits `lead` along the panel's edge and `ARROW_REACH` out from it, so the panel starts that much back.
|
|
84
99
|
const lead = arrow ? arrowLead(panel) : 0;
|
|
85
100
|
const reach = arrow ? ARROW_REACH : 0;
|
|
101
|
+
const { side: opens, width, height } = fitToRoom(panel, window.innerHeight - y - reach - margin, y - reach - margin);
|
|
86
102
|
let left = x - lead;
|
|
87
|
-
|
|
103
|
+
const top = opens === "below" ? y + reach : y - reach - height;
|
|
88
104
|
if (left + width > window.innerWidth - margin)
|
|
89
105
|
left = x + lead - width;
|
|
90
|
-
if (top + height > window.innerHeight - margin)
|
|
91
|
-
top = y - reach - height;
|
|
92
106
|
const placed = {
|
|
93
107
|
left: Math.round(Math.max(margin, Math.min(left, window.innerWidth - width - margin))),
|
|
94
108
|
top: Math.round(Math.max(margin, Math.min(top, window.innerHeight - height - margin))),
|
|
@@ -100,8 +114,12 @@ export function placeAtPoint(panel, x, y, options = {}) {
|
|
|
100
114
|
const side = placed.top >= y ? "below" : "above";
|
|
101
115
|
aimArrow(panel, side, placed, arrow ? { at: x, facing: y, room: reach } : null);
|
|
102
116
|
}
|
|
103
|
-
/**
|
|
104
|
-
|
|
117
|
+
/**
|
|
118
|
+
* Runs `place` again, once per frame at most, while the page scrolls, the window changes size, or `panel` does (its
|
|
119
|
+
* content grew). Returns the stop function. A frame later, never inside the observer, so a panel that `place` resizes
|
|
120
|
+
* is measured once more and settles instead of looping.
|
|
121
|
+
*/
|
|
122
|
+
export function followAnchor(place, panel) {
|
|
105
123
|
let frame = 0;
|
|
106
124
|
const reposition = () => {
|
|
107
125
|
cancelAnimationFrame(frame);
|
|
@@ -109,12 +127,29 @@ export function followAnchor(place) {
|
|
|
109
127
|
};
|
|
110
128
|
window.addEventListener("scroll", reposition, { capture: true, passive: true });
|
|
111
129
|
window.addEventListener("resize", reposition);
|
|
130
|
+
const observer = new ResizeObserver(reposition);
|
|
131
|
+
if (panel)
|
|
132
|
+
observer.observe(panel);
|
|
112
133
|
return () => {
|
|
113
134
|
cancelAnimationFrame(frame);
|
|
114
135
|
window.removeEventListener("scroll", reposition, { capture: true });
|
|
115
136
|
window.removeEventListener("resize", reposition);
|
|
137
|
+
observer.disconnect();
|
|
116
138
|
};
|
|
117
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Shows a panel with its scroller (`.nx-popover__scroll`) at the top. The browser keeps where a panel was scrolled
|
|
142
|
+
* while it was shut, and a list that opens part-way down has lost its heading and its first rows. Does nothing on
|
|
143
|
+
* browsers without popover support.
|
|
144
|
+
*/
|
|
145
|
+
export function showPanel(panel) {
|
|
146
|
+
if (typeof panel.showPopover !== "function")
|
|
147
|
+
return;
|
|
148
|
+
panel.showPopover();
|
|
149
|
+
const scroller = panel.querySelector(":scope > .nx-popover__scroll");
|
|
150
|
+
if (scroller)
|
|
151
|
+
scroller.scrollTop = 0;
|
|
152
|
+
}
|
|
118
153
|
/** Whether the popover is shown. Safe on browsers without popover support, where it is never "open". */
|
|
119
154
|
export function isPopoverOpen(panel) {
|
|
120
155
|
return Boolean(panel && typeof panel.matches === "function" && panel.matches(":popover-open"));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type Ref, type RefObject, type ToggleEvent } from "react";
|
|
2
|
+
/** What opens a panel from elsewhere, such as an item of a menu. */
|
|
3
|
+
export interface PopoverPanelHandle {
|
|
4
|
+
open: () => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* A `popover="auto"` panel that `open()` shows under `anchor`, aligned to its end, with the arrow: its height capped to
|
|
8
|
+
* the room on its side (its `.nx-popover__scroll` scrolls), placed again while the page moves or the panel grows, and
|
|
9
|
+
* the focus moved in as it opens (`focus` moves it, else the panel takes it). The anchor holds the focus as the panel
|
|
10
|
+
* opens, so the browser gives it back there when Esc closes the panel. `shown` turns true on the first open and stays,
|
|
11
|
+
* for content drawn only from then on.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const { panelRef, open, onToggle } = usePopoverPanel(anchor, ref);
|
|
15
|
+
* <div ref={panelRef} popover="auto" role="dialog" tabIndex={-1} className="nx-popover" onToggle={onToggle}>
|
|
16
|
+
* <div className="nx-popover__scroll">…</div>
|
|
17
|
+
* </div>
|
|
18
|
+
*/
|
|
19
|
+
export declare function usePopoverPanel(anchor: RefObject<HTMLElement | null>, ref?: Ref<PopoverPanelHandle>, focus?: () => void): {
|
|
20
|
+
panelRef: RefObject<HTMLDivElement | null>;
|
|
21
|
+
open: boolean;
|
|
22
|
+
shown: boolean;
|
|
23
|
+
onToggle: (event: ToggleEvent<HTMLDivElement>) => void;
|
|
24
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { flushSync } from "react-dom";
|
|
4
|
+
import { followAnchor, isPopoverOpen, placeUnderAnchor, showPanel } from "./PopoverHelpers.js";
|
|
5
|
+
/**
|
|
6
|
+
* A `popover="auto"` panel that `open()` shows under `anchor`, aligned to its end, with the arrow: its height capped to
|
|
7
|
+
* the room on its side (its `.nx-popover__scroll` scrolls), placed again while the page moves or the panel grows, and
|
|
8
|
+
* the focus moved in as it opens (`focus` moves it, else the panel takes it). The anchor holds the focus as the panel
|
|
9
|
+
* opens, so the browser gives it back there when Esc closes the panel. `shown` turns true on the first open and stays,
|
|
10
|
+
* for content drawn only from then on.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const { panelRef, open, onToggle } = usePopoverPanel(anchor, ref);
|
|
14
|
+
* <div ref={panelRef} popover="auto" role="dialog" tabIndex={-1} className="nx-popover" onToggle={onToggle}>
|
|
15
|
+
* <div className="nx-popover__scroll">…</div>
|
|
16
|
+
* </div>
|
|
17
|
+
*/
|
|
18
|
+
export function usePopoverPanel(anchor, ref, focus) {
|
|
19
|
+
const panelRef = useRef(null);
|
|
20
|
+
const [open, setOpen] = useState(false);
|
|
21
|
+
const [shown, setShown] = useState(false);
|
|
22
|
+
// A long panel keeps its head and its foot in the window, and its scroller takes what is left (`placeUnderAnchor`).
|
|
23
|
+
function place() {
|
|
24
|
+
const panel = panelRef.current;
|
|
25
|
+
const trigger = anchor.current;
|
|
26
|
+
if (panel && trigger)
|
|
27
|
+
placeUnderAnchor(panel, trigger.getBoundingClientRect(), { align: "end" });
|
|
28
|
+
}
|
|
29
|
+
useImperativeHandle(ref, () => ({
|
|
30
|
+
open() {
|
|
31
|
+
const panel = panelRef.current;
|
|
32
|
+
if (!panel || isPopoverOpen(panel))
|
|
33
|
+
return;
|
|
34
|
+
// Drawn first, so the panel is placed at its real size and the focus finds what it shows.
|
|
35
|
+
flushSync(() => setShown(true));
|
|
36
|
+
anchor.current?.focus({ preventScroll: true });
|
|
37
|
+
showPanel(panel);
|
|
38
|
+
place();
|
|
39
|
+
focus?.();
|
|
40
|
+
if (!panel.contains(document.activeElement))
|
|
41
|
+
panel.focus();
|
|
42
|
+
},
|
|
43
|
+
}));
|
|
44
|
+
// The panel is fixed, so it follows the anchor while it is open.
|
|
45
|
+
useLayoutEffect(() => {
|
|
46
|
+
const panel = panelRef.current;
|
|
47
|
+
if (!open || !panel)
|
|
48
|
+
return;
|
|
49
|
+
return followAnchor(place, panel);
|
|
50
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
51
|
+
}, [open]);
|
|
52
|
+
return {
|
|
53
|
+
panelRef,
|
|
54
|
+
open,
|
|
55
|
+
shown,
|
|
56
|
+
// A list or a calendar inside the panel toggles too, and React passes its event up: only the panel's own counts.
|
|
57
|
+
onToggle: (event) => {
|
|
58
|
+
if (event.target === event.currentTarget)
|
|
59
|
+
setOpen(event.newState === "open");
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -109,8 +109,13 @@ export declare class RowsStore<TRow extends RowValue = RowValue> {
|
|
|
109
109
|
add(values?: Partial<TRow>, index?: number): string;
|
|
110
110
|
/** Adds a copy of a row after it, without its id and with its radios off. Returns the copy's key. */
|
|
111
111
|
duplicate(key: string): string | null;
|
|
112
|
-
/**
|
|
113
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Removes a row. A radio it had on moves to the first row left. `undoRemove` puts it back - unless `undo` is false: a
|
|
114
|
+
* row the code drops, such as one nothing was typed in, is not offered back.
|
|
115
|
+
*/
|
|
116
|
+
remove(key: string, { undo }?: {
|
|
117
|
+
undo?: boolean;
|
|
118
|
+
}): void;
|
|
114
119
|
/** Puts the last removed row back where it was. Returns its key, or `null` when there is nothing to undo. */
|
|
115
120
|
undoRemove(): string | null;
|
|
116
121
|
/** Moves a row to a new position. */
|
|
@@ -226,8 +226,11 @@ export class RowsStore {
|
|
|
226
226
|
values[params.name] = false;
|
|
227
227
|
return this.add(values, index + 1);
|
|
228
228
|
}
|
|
229
|
-
/**
|
|
230
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Removes a row. A radio it had on moves to the first row left. `undoRemove` puts it back - unless `undo` is false: a
|
|
231
|
+
* row the code drops, such as one nothing was typed in, is not offered back.
|
|
232
|
+
*/
|
|
233
|
+
remove(key, { undo = true } = {}) {
|
|
231
234
|
const index = this.positions.get(key);
|
|
232
235
|
if (index === undefined)
|
|
233
236
|
return;
|
|
@@ -245,7 +248,16 @@ export class RowsStore {
|
|
|
245
248
|
this.write(first.key, params.name, true, "code", changed, 1);
|
|
246
249
|
moved.push({ key: first.key, name: params.name });
|
|
247
250
|
}
|
|
248
|
-
|
|
251
|
+
if (undo)
|
|
252
|
+
this.removed = { entry, index, moved };
|
|
253
|
+
// The person's own last removal stays theirs to undo, in its place: a row dropped before it moves it up one. One
|
|
254
|
+
// that handed a radio to another row could leave two rows with it on after that undo, so then nothing is undone.
|
|
255
|
+
else if (this.removed) {
|
|
256
|
+
if (moved.length)
|
|
257
|
+
this.removed = null;
|
|
258
|
+
else if (index < this.removed.index)
|
|
259
|
+
this.removed = { ...this.removed, index: this.removed.index - 1 };
|
|
260
|
+
}
|
|
249
261
|
this.messageShown = true;
|
|
250
262
|
this.markUniqueDirty();
|
|
251
263
|
this.settle(changed);
|
|
@@ -38,18 +38,18 @@ export function exportTableCsv(output) {
|
|
|
38
38
|
}
|
|
39
39
|
const escapeHtml = (text) => text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
40
40
|
/* Plain black on white, since the paper has no theme, and a header row that repeats on every page. */
|
|
41
|
-
const PRINT_STYLE = `
|
|
42
|
-
* { box-sizing: border-box; }
|
|
43
|
-
body { margin: 16mm 12mm; color: #111; font: 11pt/1.45 system-ui, "Segoe UI", sans-serif; }
|
|
44
|
-
h1 { margin: 0 0 2mm; font-size: 15pt; }
|
|
45
|
-
p.nx-note { margin: 0 0 5mm; color: #555; font-size: 9pt; }
|
|
46
|
-
table { width: 100%; border-collapse: collapse; font-size: 9.5pt; }
|
|
47
|
-
thead { display: table-header-group; }
|
|
48
|
-
th, td { padding: 4px 6px; border-bottom: 1px solid #bbb; text-align: left; vertical-align: top; }
|
|
49
|
-
th { border-bottom: 1.5px solid #555; font-weight: 600; white-space: nowrap; }
|
|
50
|
-
td.nx-end, th.nx-end { text-align: right; }
|
|
51
|
-
td.nx-center, th.nx-center { text-align: center; }
|
|
52
|
-
tr { page-break-inside: avoid; }
|
|
41
|
+
const PRINT_STYLE = `
|
|
42
|
+
* { box-sizing: border-box; }
|
|
43
|
+
body { margin: 16mm 12mm; color: #111; font: 11pt/1.45 system-ui, "Segoe UI", sans-serif; }
|
|
44
|
+
h1 { margin: 0 0 2mm; font-size: 15pt; }
|
|
45
|
+
p.nx-note { margin: 0 0 5mm; color: #555; font-size: 9pt; }
|
|
46
|
+
table { width: 100%; border-collapse: collapse; font-size: 9.5pt; }
|
|
47
|
+
thead { display: table-header-group; }
|
|
48
|
+
th, td { padding: 4px 6px; border-bottom: 1px solid #bbb; text-align: left; vertical-align: top; }
|
|
49
|
+
th { border-bottom: 1.5px solid #555; font-weight: 600; white-space: nowrap; }
|
|
50
|
+
td.nx-end, th.nx-end { text-align: right; }
|
|
51
|
+
td.nx-center, th.nx-center { text-align: center; }
|
|
52
|
+
tr { page-break-inside: avoid; }
|
|
53
53
|
`;
|
|
54
54
|
/** The print view's HTML: a heading, a note, and the table. */
|
|
55
55
|
export function tableToPrintHtml({ columns, rows, cellOptions = {}, name = "Table", note }) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { InputParams } from "../Interfaces/FormInterfaces.ts";
|
|
2
|
+
import type { TableColumn, TableFilters, TableRow } from "../Interfaces/TableInterfaces.ts";
|
|
3
|
+
/** The params as a filter: the same object without `required`, `defaultValue`, `defaultChecked`. */
|
|
4
|
+
export declare function asFilterInput(params: InputParams): InputParams;
|
|
5
|
+
/**
|
|
6
|
+
* The values that filter, in input order, or `undefined` when none does. Drops a value that `isEmptyInputValue` calls
|
|
7
|
+
* empty or that equals `emptyInputValue(params)` (an unticked box, a slider at its min, a range slider at [min, max]),
|
|
8
|
+
* and any name no input declares.
|
|
9
|
+
*/
|
|
10
|
+
export declare function activeFilters(inputs: readonly InputParams[], values: Readonly<Record<string, unknown>> | undefined): TableFilters | undefined;
|
|
11
|
+
/** The labels of the active filters, for a menu line or an export note: ["Status", "Check-in"]. */
|
|
12
|
+
export declare function filterNames(inputs: readonly InputParams[], filters: TableFilters | undefined): string[];
|
|
13
|
+
/**
|
|
14
|
+
* A client table's test of a row for the active filters, or `null` when nothing filters. Each input reads the column
|
|
15
|
+
* whose `key` is its name; a `number` input named `{key}From` / `{key}To` is a bound of column `{key}`; any other name
|
|
16
|
+
* is a path into the row. A blank cell never matches an active filter.
|
|
17
|
+
*/
|
|
18
|
+
export declare function filterMatcher<TRow extends TableRow>(columns: readonly TableColumn<TRow>[], inputs: readonly InputParams[], filters: TableFilters | undefined): ((row: TRow) => boolean) | null;
|
|
19
|
+
/**
|
|
20
|
+
* The active filters in the flat names a server takes: scalars and lists as they are (never an empty list), a date
|
|
21
|
+
* range as `{name}From` / `{name}Before` (the day after its last day), a date on or a date range over a `date-time`
|
|
22
|
+
* column as the UTC moments of those local days, a range slider as `{name}From` / `{name}To`.
|
|
23
|
+
* `timeZone` defaults to the zone the table shows date-times in.
|
|
24
|
+
*/
|
|
25
|
+
export declare function filterRequest<TRow extends TableRow>(filters: TableFilters | undefined, inputs: readonly InputParams[], columns: readonly TableColumn<TRow>[], timeZone?: string): Record<string, unknown>;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { addDays } from "./DateHelpers.js";
|
|
2
|
+
import { fromLocalDateTime, toLocalDateTime, toUtcDateTime } from "./DateTimeHelpers.js";
|
|
3
|
+
import { foldSearchText, searchWords } from "./DropdownHelpers.js";
|
|
4
|
+
import { emptyInputValue, inputFieldName, inputValuesEqual, isEmptyInputValue } from "./InputParamsHelpers.js";
|
|
5
|
+
import { columnValue, readPath, searchTextOf } from "./TableHelpers.js";
|
|
6
|
+
/*
|
|
7
|
+
* A data table's advanced filter, with no React: input params as filters, the values that filter, a client table's
|
|
8
|
+
* test of a row, and the names a server takes.
|
|
9
|
+
*/
|
|
10
|
+
// Inputs whose value cannot filter a row, and names a Nexus page request already uses for its paging.
|
|
11
|
+
const UNSUPPORTED = new Set(["markdown", "rich-text", "tree", "tabular", "group", "password"]);
|
|
12
|
+
const RESERVED = new Set(["searchKeyword", "skipCount", "maxCount", "sorting", "sortType"]);
|
|
13
|
+
const warned = new Set();
|
|
14
|
+
function filterable(params) {
|
|
15
|
+
const reserved = RESERVED.has(params.name);
|
|
16
|
+
if (!reserved && !UNSUPPORTED.has(params.type))
|
|
17
|
+
return true;
|
|
18
|
+
if (!warned.has(params.name)) {
|
|
19
|
+
warned.add(params.name);
|
|
20
|
+
console.warn(`DataTable: the filter "${params.name}" is ignored: ${reserved ? "a page request already sends that name" : `a ${params.type} input cannot filter rows`}.`);
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
/** The params as a filter: the same object without `required`, `defaultValue`, `defaultChecked`. */
|
|
25
|
+
export function asFilterInput(params) {
|
|
26
|
+
// Descriptors rather than a spread, so a getter (a `hidden` that follows `dependsOn`) is still read as the form draws.
|
|
27
|
+
const { required: _required, defaultValue: _value, defaultChecked: _checked, ...rest } = Object.getOwnPropertyDescriptors(params);
|
|
28
|
+
// An input that cannot filter stays out of the panel, as its value stays out of the filters.
|
|
29
|
+
if (!filterable(params))
|
|
30
|
+
rest.hidden = { value: true, enumerable: true };
|
|
31
|
+
return Object.defineProperties({}, rest);
|
|
32
|
+
}
|
|
33
|
+
/** Each input that filters, with its value: in input order, text trimmed, empty values left out. */
|
|
34
|
+
function activeInputs(inputs, values) {
|
|
35
|
+
const active = [];
|
|
36
|
+
if (!values)
|
|
37
|
+
return active;
|
|
38
|
+
for (const params of inputs) {
|
|
39
|
+
const given = values[params.name];
|
|
40
|
+
const value = typeof given === "string" ? given.trim() : given;
|
|
41
|
+
if (!isEmptyInputValue(value) && !inputValuesEqual(value, emptyInputValue(params)) && filterable(params))
|
|
42
|
+
active.push([params, value]);
|
|
43
|
+
}
|
|
44
|
+
return active;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The values that filter, in input order, or `undefined` when none does. Drops a value that `isEmptyInputValue` calls
|
|
48
|
+
* empty or that equals `emptyInputValue(params)` (an unticked box, a slider at its min, a range slider at [min, max]),
|
|
49
|
+
* and any name no input declares.
|
|
50
|
+
*/
|
|
51
|
+
export function activeFilters(inputs, values) {
|
|
52
|
+
const active = activeInputs(inputs, values);
|
|
53
|
+
return active.length ? Object.fromEntries(active.map(([params, value]) => [params.name, value])) : undefined;
|
|
54
|
+
}
|
|
55
|
+
/** The labels of the active filters, for a menu line or an export note: ["Status", "Check-in"]. */
|
|
56
|
+
export function filterNames(inputs, filters) {
|
|
57
|
+
return activeInputs(inputs, filters).map(([params]) => inputFieldName(params) ?? (typeof params.label === "string" ? params.label : params.name));
|
|
58
|
+
}
|
|
59
|
+
const isBlank = (cell) => cell === undefined || cell === null || cell === "";
|
|
60
|
+
/** One filter's test of a row's cell (never blank here). */
|
|
61
|
+
function cellTest(params, value, column, bound) {
|
|
62
|
+
// A moment's day is the one the table shows it on; any other value is read as an ISO date.
|
|
63
|
+
const dayOf = (cell) => (column?.format === "date-time" ? toLocalDateTime(cell)?.date : String(cell).slice(0, 10));
|
|
64
|
+
switch (params.type) {
|
|
65
|
+
case "text":
|
|
66
|
+
case "email":
|
|
67
|
+
case "search":
|
|
68
|
+
case "tel":
|
|
69
|
+
case "url":
|
|
70
|
+
case "textarea": {
|
|
71
|
+
// Every word, folded as the search folds it, in the text the cell shows (an option's label, Yes or No).
|
|
72
|
+
const words = searchWords(String(value));
|
|
73
|
+
return (cell, row) => {
|
|
74
|
+
const text = foldSearchText(column ? searchTextOf(column, row) : String(cell));
|
|
75
|
+
return words.every(word => text.includes(word));
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
case "checkbox":
|
|
79
|
+
case "switch":
|
|
80
|
+
case "radio":
|
|
81
|
+
return cell => cell === true || cell === "true" || cell === 1;
|
|
82
|
+
case "number":
|
|
83
|
+
case "slider": {
|
|
84
|
+
const number = Number(value);
|
|
85
|
+
return cell => (bound === "From" ? Number(cell) >= number : bound === "To" ? Number(cell) <= number : Number(cell) === number);
|
|
86
|
+
}
|
|
87
|
+
case "range-slider": {
|
|
88
|
+
const [low, high] = value;
|
|
89
|
+
return cell => Number(cell) >= low && Number(cell) <= high;
|
|
90
|
+
}
|
|
91
|
+
case "date":
|
|
92
|
+
return cell => dayOf(cell) === value;
|
|
93
|
+
case "date-range": {
|
|
94
|
+
const [from, to] = value;
|
|
95
|
+
return cell => {
|
|
96
|
+
const day = dayOf(cell);
|
|
97
|
+
return day !== undefined && (!from || day >= from) && (!to || day <= to);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
case "time":
|
|
101
|
+
return cell => String(cell).slice(0, 5) === String(value).slice(0, 5);
|
|
102
|
+
case "date-time": {
|
|
103
|
+
// The same minute, however each side writes the moment ("…Z", "+05:45", or no zone for UTC).
|
|
104
|
+
const minute = (moment) => toUtcDateTime(moment)?.slice(0, 16);
|
|
105
|
+
const wanted = minute(value);
|
|
106
|
+
return cell => minute(cell) === wanted;
|
|
107
|
+
}
|
|
108
|
+
default: {
|
|
109
|
+
// A choice, or several: compared as text, so 2 matches "2"; a cell holding a list matches when any item does.
|
|
110
|
+
const wanted = new Set((Array.isArray(value) ? value : [value]).map(String));
|
|
111
|
+
return cell => (Array.isArray(cell) ? cell : [cell]).some(item => wanted.has(String(item)));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* A client table's test of a row for the active filters, or `null` when nothing filters. Each input reads the column
|
|
117
|
+
* whose `key` is its name; a `number` input named `{key}From` / `{key}To` is a bound of column `{key}`; any other name
|
|
118
|
+
* is a path into the row. A blank cell never matches an active filter.
|
|
119
|
+
*/
|
|
120
|
+
export function filterMatcher(columns, inputs, filters) {
|
|
121
|
+
const tests = activeInputs(inputs, filters).map(([params, value]) => {
|
|
122
|
+
const { name } = params;
|
|
123
|
+
let column = columns.find(candidate => candidate.key === name);
|
|
124
|
+
const [, key, bound] = (!column && params.type === "number" && /^(.+)(From|To)$/.exec(name)) || [];
|
|
125
|
+
column ??= columns.find(candidate => candidate.key === key);
|
|
126
|
+
const test = cellTest(params, value, column, column ? bound : undefined);
|
|
127
|
+
return (row) => {
|
|
128
|
+
const cell = column ? columnValue(column, row) : readPath(row, name);
|
|
129
|
+
return !isBlank(cell) && test(cell, row);
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
return tests.length ? row => tests.every(test => test(row)) : null;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* The active filters in the flat names a server takes: scalars and lists as they are (never an empty list), a date
|
|
136
|
+
* range as `{name}From` / `{name}Before` (the day after its last day), a date on or a date range over a `date-time`
|
|
137
|
+
* column as the UTC moments of those local days, a range slider as `{name}From` / `{name}To`.
|
|
138
|
+
* `timeZone` defaults to the zone the table shows date-times in.
|
|
139
|
+
*/
|
|
140
|
+
export function filterRequest(filters, inputs, columns, timeZone) {
|
|
141
|
+
const request = {};
|
|
142
|
+
for (const [params, value] of activeInputs(inputs, filters)) {
|
|
143
|
+
const { name } = params;
|
|
144
|
+
const moments = columns.find(column => column.key === name)?.format === "date-time";
|
|
145
|
+
// Days are half-open, [From, Before), so the last day is never lost to a time of day.
|
|
146
|
+
const days = (from, to) => {
|
|
147
|
+
if (from)
|
|
148
|
+
request[`${name}From`] = moments ? fromLocalDateTime(from, "00:00", timeZone) : from;
|
|
149
|
+
if (to)
|
|
150
|
+
request[`${name}Before`] = moments ? fromLocalDateTime(addDays(to, 1), "00:00", timeZone) : addDays(to, 1);
|
|
151
|
+
};
|
|
152
|
+
if (params.type === "date-range")
|
|
153
|
+
days(...value);
|
|
154
|
+
else if (params.type === "date" && moments)
|
|
155
|
+
days(value, value);
|
|
156
|
+
else if (params.type === "range-slider")
|
|
157
|
+
[request[`${name}From`], request[`${name}To`]] = value;
|
|
158
|
+
else
|
|
159
|
+
request[name] = value;
|
|
160
|
+
}
|
|
161
|
+
return request;
|
|
162
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DatePreferences } from "../Interfaces/DateInterfaces.ts";
|
|
2
|
+
import type { InputParams } from "../Interfaces/FormInterfaces.ts";
|
|
2
3
|
import type { TableColumn, TableColumnOption, TableHeadBand, TableKey, TablePage, TablePinSide, TableQuery, TableRow, TableSort, TableViewColumn } from "../Interfaces/TableInterfaces.ts";
|
|
3
4
|
/** Rows per page when nothing says otherwise. */
|
|
4
5
|
export declare const DEFAULT_PAGE_SIZE = 10;
|
|
@@ -23,6 +24,8 @@ export interface CellTextOptions {
|
|
|
23
24
|
preferences?: Partial<DatePreferences>;
|
|
24
25
|
/** IANA zone for `date-time` values. Default: the browser's. */
|
|
25
26
|
timeZone?: string;
|
|
27
|
+
/** How numbers are written: a locale, or the person's settings (`numberLocale`, `useNumberLocale`). Default `en-US`. */
|
|
28
|
+
locale?: string;
|
|
26
29
|
}
|
|
27
30
|
/**
|
|
28
31
|
* A value as the cell writes it, in the column's format: "1,250.00", "Rs. 4,500.00", "12.5%", the date and time in the
|
|
@@ -54,10 +57,10 @@ export declare function readTablePage<TRow extends TableRow>(answer: TablePage<T
|
|
|
54
57
|
matches: number;
|
|
55
58
|
};
|
|
56
59
|
/**
|
|
57
|
-
* Searches, sorts, and pages rows the way the table shows them. `texts` caches each row's folded search text,
|
|
58
|
-
* search of 10,000 rows looks at each row once per keystroke.
|
|
60
|
+
* Searches, filters, sorts, and pages rows the way the table shows them. `texts` caches each row's folded search text,
|
|
61
|
+
* so a search of 10,000 rows looks at each row once per keystroke. `query.filters` are matched by `filterInputs`.
|
|
59
62
|
*/
|
|
60
|
-
export declare function queryRows<TRow extends TableRow>(rows: readonly TRow[], columns: readonly TableColumn<TRow>[], query: TableQuery, texts?: WeakMap<TRow, string
|
|
63
|
+
export declare function queryRows<TRow extends TableRow>(rows: readonly TRow[], columns: readonly TableColumn<TRow>[], query: TableQuery, texts?: WeakMap<TRow, string>, filterInputs?: readonly InputParams[]): {
|
|
61
64
|
rows: TRow[];
|
|
62
65
|
matches: number;
|
|
63
66
|
page: number;
|