nexus-shared 2.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 -0
- package/LICENSE +21 -21
- package/README.md +1084 -903
- package/dist/Client.Index.d.ts +14 -0
- package/dist/Client.Index.js +14 -0
- package/dist/Components/Chats/Chat.d.ts +28 -0
- package/dist/Components/Chats/Chat.js +19 -0
- package/dist/Components/Chats/ChatButton.d.ts +26 -0
- package/dist/Components/Chats/ChatButton.js +41 -0
- package/dist/Components/Chats/ChatComposer.d.ts +41 -0
- package/dist/Components/Chats/ChatComposer.js +177 -0
- package/dist/Components/Chats/ChatConversations.d.ts +35 -0
- package/dist/Components/Chats/ChatConversations.js +38 -0
- package/dist/Components/Chats/ChatPanel.d.ts +66 -0
- package/dist/Components/Chats/ChatPanel.js +94 -0
- package/dist/Components/Chats/ChatParts.d.ts +87 -0
- package/dist/Components/Chats/ChatParts.js +96 -0
- package/dist/Components/Chats/ChatThread.d.ts +22 -0
- package/dist/Components/Chats/ChatThread.js +96 -0
- package/dist/Components/Documents/Button.d.ts +6 -1
- package/dist/Components/Documents/Button.js +2 -2
- package/dist/Components/Documents/Menu.js +44 -28
- package/dist/Components/Documents/SplitButton.d.ts +3 -1
- package/dist/Components/Documents/SplitButton.js +12 -8
- package/dist/Components/Documents/TabButtons.d.ts +29 -8
- package/dist/Components/Documents/TabButtons.js +24 -5
- package/dist/Components/Forms/ApiForm.d.ts +6 -4
- package/dist/Components/Forms/ApiForm.js +15 -14
- package/dist/Components/Forms/Crud.js +342 -71
- package/dist/Components/Forms/CrudParts.d.ts +5 -1
- package/dist/Components/Forms/CrudParts.js +37 -5
- package/dist/Components/Forms/ExcelImport.d.ts +58 -0
- package/dist/Components/Forms/ExcelImport.js +319 -0
- package/dist/Components/Forms/Form.js +9 -13
- package/dist/Components/Forms/SubmitForm.d.ts +11 -2
- package/dist/Components/Forms/SubmitForm.js +23 -12
- package/dist/Components/Inputs/DateTimePicker.js +2 -1
- package/dist/Components/Inputs/DropdownParts.d.ts +1 -1
- package/dist/Components/Inputs/DropdownParts.js +4 -6
- package/dist/Components/Inputs/GroupForm.js +4 -2
- package/dist/Components/Inputs/InputRenderer.d.ts +2 -0
- package/dist/Components/Inputs/InputRenderer.js +1 -1
- package/dist/Components/Inputs/NumberBox.js +24 -2
- package/dist/Components/Inputs/ReadOnlyNotice.js +2 -1
- package/dist/Components/Inputs/RowsInput.d.ts +12 -1
- package/dist/Components/Inputs/RowsInput.js +55 -6
- package/dist/Components/Inputs/Slider.js +4 -1
- package/dist/Components/Inputs/TabularForm.d.ts +7 -3
- package/dist/Components/Inputs/TabularForm.js +117 -20
- package/dist/Components/Inputs/TimePanel.js +4 -3
- package/dist/Components/Inputs/TimePicker.js +2 -1
- 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 +39 -18
- package/dist/Components/Viewers/Avatar.d.ts +21 -0
- package/dist/Components/Viewers/Avatar.js +21 -0
- package/dist/Components/Viewers/DataTable.js +262 -60
- package/dist/Components/Viewers/DataTableColumns.d.ts +32 -0
- package/dist/Components/Viewers/DataTableColumns.js +200 -0
- package/dist/Components/Viewers/DataTableFilters.d.ts +21 -0
- package/dist/Components/Viewers/DataTableFilters.js +68 -0
- package/dist/Components/Viewers/DataTableParts.d.ts +8 -4
- package/dist/Components/Viewers/DataTableParts.js +24 -12
- package/dist/Helpers/AnimationHelpers.d.ts +11 -0
- package/dist/Helpers/AnimationHelpers.js +20 -2
- package/dist/Helpers/ApiClient.d.ts +22 -0
- package/dist/Helpers/ApiClient.js +4 -1
- package/dist/Helpers/ApiFormHelpers.d.ts +27 -4
- package/dist/Helpers/ApiFormHelpers.js +73 -9
- package/dist/Helpers/ApiModules.d.ts +0 -4
- package/dist/Helpers/ApiModules.js +1 -0
- package/dist/Helpers/ApiResponses.d.ts +14 -4
- package/dist/Helpers/ApiResponses.js +116 -25
- package/dist/Helpers/ApiRoutes.d.ts +0 -10
- package/dist/Helpers/ApiRoutes.js +2 -10
- package/dist/Helpers/ChatBackend.d.ts +134 -0
- package/dist/Helpers/ChatBackend.js +332 -0
- package/dist/Helpers/ChatHelpers.d.ts +189 -0
- package/dist/Helpers/ChatHelpers.js +486 -0
- package/dist/Helpers/ChatHooks.d.ts +81 -0
- package/dist/Helpers/ChatHooks.js +175 -0
- package/dist/Helpers/ChatStore.d.ts +132 -0
- package/dist/Helpers/ChatStore.js +394 -0
- package/dist/Helpers/ChatSummaries.d.ts +39 -0
- package/dist/Helpers/ChatSummaries.js +118 -0
- package/dist/Helpers/CrudBackend.d.ts +54 -9
- package/dist/Helpers/CrudBackend.js +103 -6
- package/dist/Helpers/CrudHelpers.d.ts +39 -4
- package/dist/Helpers/CrudHelpers.js +76 -11
- package/dist/Helpers/ExcelBackend.d.ts +106 -0
- package/dist/Helpers/ExcelBackend.js +296 -0
- package/dist/Helpers/ExcelHelpers.d.ts +126 -0
- package/dist/Helpers/ExcelHelpers.js +344 -0
- package/dist/Helpers/FormStore.d.ts +11 -5
- package/dist/Helpers/FormStore.js +44 -6
- package/dist/Helpers/MessageBuilder.js +1 -2
- package/dist/Helpers/NumberHelpers.d.ts +7 -1
- package/dist/Helpers/NumberHelpers.js +41 -3
- package/dist/Helpers/PopoverHelpers.d.ts +46 -9
- package/dist/Helpers/PopoverHelpers.js +163 -24
- package/dist/Helpers/PopoverHooks.d.ts +24 -0
- package/dist/Helpers/PopoverHooks.js +62 -0
- package/dist/Helpers/RowsStore.d.ts +14 -3
- package/dist/Helpers/RowsStore.js +37 -3
- package/dist/Helpers/TableColumns.d.ts +7 -0
- package/dist/Helpers/TableColumns.js +41 -13
- 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 +15 -4
- package/dist/Helpers/TableHelpers.js +28 -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/ApiInterfaces.d.ts +29 -4
- package/dist/Interfaces/ApiInterfaces.js +16 -10
- package/dist/Interfaces/ChatInterfaces.d.ts +240 -0
- package/dist/Interfaces/ChatInterfaces.js +1 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +92 -12
- package/dist/Interfaces/ExcelInterfaces.d.ts +227 -0
- package/dist/Interfaces/ExcelInterfaces.js +45 -0
- package/dist/Interfaces/FormInterfaces.d.ts +85 -7
- package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
- package/dist/Interfaces/PreferenceInterfaces.d.ts +43 -0
- package/dist/Interfaces/PreferenceInterfaces.js +28 -0
- package/dist/Interfaces/TableInterfaces.d.ts +116 -9
- package/dist/Interfaces/ThemeInterfaces.d.ts +25 -0
- package/dist/Interfaces/ThemeInterfaces.js +15 -0
- package/dist/Services/BrowserApi.d.ts +18 -1
- package/dist/Services/BrowserApi.js +18 -0
- package/dist/Services/ChatApi.d.ts +37 -0
- package/dist/Services/ChatApi.js +98 -0
- package/dist/Services/ExcelApi.d.ts +48 -0
- package/dist/Services/ExcelApi.js +196 -0
- package/dist/Services/ServerApi.d.ts +6 -1
- package/dist/Services/ServerApi.js +3 -0
- package/dist/Services/ThemeService.d.ts +18 -2
- package/dist/Services/ThemeService.js +80 -16
- package/dist/Shared.Index.d.ts +10 -0
- package/dist/Shared.Index.js +10 -0
- package/package.json +85 -82
- package/src/Styles/Nexus.Avatar.css +79 -0
- package/src/Styles/Nexus.Base.css +117 -70
- package/src/Styles/Nexus.Button.css +180 -151
- package/src/Styles/Nexus.Calendar.css +273 -275
- package/src/Styles/Nexus.Chat.css +1443 -0
- package/src/Styles/Nexus.Choice.css +343 -345
- package/src/Styles/Nexus.Crud.css +168 -84
- 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 -0
- package/src/Styles/Nexus.Form.css +279 -298
- package/src/Styles/Nexus.Index.css +37 -34
- package/src/Styles/Nexus.Input.css +602 -637
- package/src/Styles/Nexus.Markdown.css +237 -265
- package/src/Styles/Nexus.Menu.css +237 -249
- package/src/Styles/Nexus.Popup.css +724 -731
- package/src/Styles/Nexus.RichText.css +2 -32
- package/src/Styles/Nexus.Rows.css +637 -566
- package/src/Styles/Nexus.Slider.css +276 -278
- package/src/Styles/Nexus.Tab.Buttons.css +432 -364
- package/src/Styles/Nexus.Table.css +1386 -1014
- package/src/Styles/Nexus.Theme.Picker.css +286 -159
- 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,19 +1,30 @@
|
|
|
1
|
+
/** Which side of its anchor a panel opened on: `below` carries its arrow on its top edge, `above` on its bottom edge. */
|
|
2
|
+
export type PanelSide = "below" | "above";
|
|
1
3
|
export type PopoverAlign = "start" | "end";
|
|
2
4
|
export interface PlaceUnderAnchorOptions {
|
|
3
5
|
/** Which edge of the anchor the panel lines up with. Default `start` (the left edge). */
|
|
4
6
|
align?: PopoverAlign;
|
|
5
|
-
/** Space between the anchor and the panel, in pixels. Default 4. */
|
|
7
|
+
/** Space between the anchor and the panel, in pixels. Default 4, or the arrow's reach while it is drawn. */
|
|
6
8
|
gap?: number;
|
|
7
9
|
/** Space kept from the window's edges, in pixels. Default 8. */
|
|
8
10
|
margin?: number;
|
|
11
|
+
/**
|
|
12
|
+
* Draws an arrow on the panel's edge, aimed at the middle of the anchor. Default true: a panel opened from a button
|
|
13
|
+
* says which button. Pass false for a panel that opens flush under a field and is read as part of it.
|
|
14
|
+
*/
|
|
15
|
+
arrow?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* What the arrow points at, when the panel lines up with more than that: the half of a split button that opened it.
|
|
18
|
+
* Default: the anchor itself.
|
|
19
|
+
*/
|
|
20
|
+
arrowAt?: DOMRect;
|
|
9
21
|
}
|
|
10
22
|
/**
|
|
11
|
-
* Places a panel under an anchor, or above it when
|
|
12
|
-
* edge and kept inside the window
|
|
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.
|
|
13
26
|
*/
|
|
14
27
|
export declare function placeUnderAnchor(panel: HTMLElement, anchor: DOMRect, options?: PlaceUnderAnchorOptions): boolean;
|
|
15
|
-
/** Which side of its anchor a list panel opened on. */
|
|
16
|
-
export type PanelSide = "below" | "above";
|
|
17
28
|
export interface PlaceListPanelOptions {
|
|
18
29
|
/** The side of an earlier call, kept while it has `minHeight` of room, so a list that loads more items does not jump across its anchor. */
|
|
19
30
|
side?: PanelSide;
|
|
@@ -30,14 +41,40 @@ export interface PlaceListPanelOptions {
|
|
|
30
41
|
* Places a list panel under its anchor, at least as wide as the anchor, or above it when the room below is short and
|
|
31
42
|
* there is more above. The panel's height is capped to the room on its side, so a long list scrolls inside the window
|
|
32
43
|
* instead of running past it. Returns the side, to pass back while the panel stays open.
|
|
44
|
+
*
|
|
45
|
+
* It draws no arrow: the panel is as wide as the field and sits flush under it, so it is already read as that field's
|
|
46
|
+
* list, and an arrow over a field's own border would look like a fault.
|
|
33
47
|
*/
|
|
34
48
|
export declare function placeListPanel(panel: HTMLElement, anchor: DOMRect, options?: PlaceListPanelOptions): PanelSide;
|
|
49
|
+
export interface PlaceAtPointOptions {
|
|
50
|
+
/** Space kept from the window's edges, in pixels. Default 8. */
|
|
51
|
+
margin?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Draws an arrow with its tip on the point, so the menu says where it was opened. Default true. The panel then
|
|
54
|
+
* stands the arrow's reach away from the point, and its corner no longer sits on it.
|
|
55
|
+
*/
|
|
56
|
+
arrow?: boolean;
|
|
57
|
+
}
|
|
35
58
|
/**
|
|
36
59
|
* Places a panel at a point, such as where the user right-clicked: to the right of and below the point when there
|
|
37
|
-
* is room, otherwise to the left of or above it, and never past the window's edges.
|
|
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
|
|
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.
|
|
63
|
+
*/
|
|
64
|
+
export declare function placeAtPoint(panel: HTMLElement, x: number, y: number, options?: PlaceAtPointOptions): void;
|
|
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.
|
|
38
75
|
*/
|
|
39
|
-
export declare function
|
|
40
|
-
/** Runs `place` again, once per frame at most, while the page scrolls or the window changes size. Returns the stop function. */
|
|
41
|
-
export declare function followAnchor(place: () => void): () => void;
|
|
76
|
+
export declare function showPanel(panel: HTMLElement): void;
|
|
42
77
|
/** Whether the popover is shown. Safe on browsers without popover support, where it is never "open". */
|
|
43
78
|
export declare function isPopoverOpen(panel: HTMLElement | null): boolean;
|
|
79
|
+
/** How far the arrow reaches past the panel's edge: its depth, less the panel's border it is drawn over. */
|
|
80
|
+
export declare const ARROW_REACH: number;
|
|
@@ -2,29 +2,42 @@
|
|
|
2
2
|
* Placing native popovers. A panel is a fixed [popover] element; these helpers set its top and left once it is shown,
|
|
3
3
|
* keep it inside the window, and move it again while the page scrolls. They measure offsetWidth and offsetHeight,
|
|
4
4
|
* which ignore the panel's opening transform.
|
|
5
|
+
*
|
|
6
|
+
* A panel that belongs to a button or to a place also gets an arrow, so the panel is read as that button's panel and
|
|
7
|
+
* not as a dialog from nowhere. These helpers work out where the arrow goes, because they are the only code that
|
|
8
|
+
* knows both rectangles; `.nx-popover` draws it from what they write (see "The arrow" below).
|
|
5
9
|
*/
|
|
6
10
|
/**
|
|
7
|
-
* Places a panel under an anchor, or above it when
|
|
8
|
-
* edge and kept inside the window
|
|
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.
|
|
9
14
|
*/
|
|
10
15
|
export function placeUnderAnchor(panel, anchor, options = {}) {
|
|
11
|
-
const { align = "start",
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const fitsBelow = below + height <= window.innerHeight - margin;
|
|
17
|
-
const top = fitsBelow || above < margin ? below : above;
|
|
16
|
+
const { align = "start", margin = 8, arrow = true } = options;
|
|
17
|
+
// The arrow needs its reach between the panel and the anchor, or it would be drawn over the anchor.
|
|
18
|
+
const gap = Math.max(options.gap ?? 4, arrow ? ARROW_REACH : 0);
|
|
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;
|
|
18
21
|
const wanted = align === "end" ? anchor.right - width : anchor.left;
|
|
19
|
-
|
|
22
|
+
// The arrow points at the middle of what it belongs to. The panel's own style is read once: this runs again on
|
|
23
|
+
// every animation frame while the page scrolls under an open panel.
|
|
24
|
+
const target = options.arrowAt ?? anchor;
|
|
25
|
+
const middle = (target.left + target.right) / 2;
|
|
26
|
+
const style = getComputedStyle(panel);
|
|
27
|
+
const left = Math.max(margin, Math.min(arrow ? centred(panel, target, wanted, width, middle, style) : wanted, window.innerWidth - width - margin));
|
|
20
28
|
panel.style.top = `${Math.round(top)}px`;
|
|
21
29
|
panel.style.left = `${Math.round(left)}px`;
|
|
22
|
-
|
|
30
|
+
const box = { left: Math.round(left), top: Math.round(top), width, height };
|
|
31
|
+
aimArrow(panel, side, box, arrow ? { at: middle, facing: side === "below" ? target.bottom : target.top, room: gap } : null, style);
|
|
32
|
+
return side === "above";
|
|
23
33
|
}
|
|
24
34
|
/**
|
|
25
35
|
* Places a list panel under its anchor, at least as wide as the anchor, or above it when the room below is short and
|
|
26
36
|
* there is more above. The panel's height is capped to the room on its side, so a long list scrolls inside the window
|
|
27
37
|
* instead of running past it. Returns the side, to pass back while the panel stays open.
|
|
38
|
+
*
|
|
39
|
+
* It draws no arrow: the panel is as wide as the field and sits flush under it, so it is already read as that field's
|
|
40
|
+
* list, and an arrow over a field's own border would look like a fault.
|
|
28
41
|
*/
|
|
29
42
|
export function placeListPanel(panel, anchor, options = {}) {
|
|
30
43
|
const { maxHeight = 360, minHeight = 180, gap = 4, margin = 8 } = options;
|
|
@@ -54,24 +67,59 @@ function setStyle(element, property, value) {
|
|
|
54
67
|
if (element.style[property] !== value)
|
|
55
68
|
element.style[property] = value;
|
|
56
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
|
+
}
|
|
57
90
|
/**
|
|
58
91
|
* Places a panel at a point, such as where the user right-clicked: to the right of and below the point when there
|
|
59
|
-
* is room, otherwise to the left of or above it, and never past the window's edges.
|
|
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
|
|
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.
|
|
60
95
|
*/
|
|
61
|
-
export function placeAtPoint(panel, x, y,
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
96
|
+
export function placeAtPoint(panel, x, y, options = {}) {
|
|
97
|
+
const { margin = 8, arrow = true } = options;
|
|
98
|
+
// The tip sits `lead` along the panel's edge and `ARROW_REACH` out from it, so the panel starts that much back.
|
|
99
|
+
const lead = arrow ? arrowLead(panel) : 0;
|
|
100
|
+
const reach = arrow ? ARROW_REACH : 0;
|
|
101
|
+
const { side: opens, width, height } = fitToRoom(panel, window.innerHeight - y - reach - margin, y - reach - margin);
|
|
102
|
+
let left = x - lead;
|
|
103
|
+
const top = opens === "below" ? y + reach : y - reach - height;
|
|
66
104
|
if (left + width > window.innerWidth - margin)
|
|
67
|
-
left = x - width;
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
105
|
+
left = x + lead - width;
|
|
106
|
+
const placed = {
|
|
107
|
+
left: Math.round(Math.max(margin, Math.min(left, window.innerWidth - width - margin))),
|
|
108
|
+
top: Math.round(Math.max(margin, Math.min(top, window.innerHeight - height - margin))),
|
|
109
|
+
width,
|
|
110
|
+
height,
|
|
111
|
+
};
|
|
112
|
+
panel.style.left = `${placed.left}px`;
|
|
113
|
+
panel.style.top = `${placed.top}px`;
|
|
114
|
+
const side = placed.top >= y ? "below" : "above";
|
|
115
|
+
aimArrow(panel, side, placed, arrow ? { at: x, facing: y, room: reach } : null);
|
|
72
116
|
}
|
|
73
|
-
/**
|
|
74
|
-
|
|
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) {
|
|
75
123
|
let frame = 0;
|
|
76
124
|
const reposition = () => {
|
|
77
125
|
cancelAnimationFrame(frame);
|
|
@@ -79,13 +127,104 @@ export function followAnchor(place) {
|
|
|
79
127
|
};
|
|
80
128
|
window.addEventListener("scroll", reposition, { capture: true, passive: true });
|
|
81
129
|
window.addEventListener("resize", reposition);
|
|
130
|
+
const observer = new ResizeObserver(reposition);
|
|
131
|
+
if (panel)
|
|
132
|
+
observer.observe(panel);
|
|
82
133
|
return () => {
|
|
83
134
|
cancelAnimationFrame(frame);
|
|
84
135
|
window.removeEventListener("scroll", reposition, { capture: true });
|
|
85
136
|
window.removeEventListener("resize", reposition);
|
|
137
|
+
observer.disconnect();
|
|
86
138
|
};
|
|
87
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
|
+
}
|
|
88
153
|
/** Whether the popover is shown. Safe on browsers without popover support, where it is never "open". */
|
|
89
154
|
export function isPopoverOpen(panel) {
|
|
90
155
|
return Boolean(panel && typeof panel.matches === "function" && panel.matches(":popover-open"));
|
|
91
156
|
}
|
|
157
|
+
/* ---------------------------------------------------------------------------------------------------------------
|
|
158
|
+
* The arrow
|
|
159
|
+
*
|
|
160
|
+
* An isosceles tail on the panel's edge, drawn in the panel's own border and background, so the two read as one
|
|
161
|
+
* shape in every theme: `.nx-popover[data-nx-arrow]::before` and `::after` in Nexus.Theme.Picker.css. These
|
|
162
|
+
* functions write the two things only the placement knows: which edge carries the tail (`data-nx-arrow`) and where
|
|
163
|
+
* along that edge its tip sits (`--nx-popover-arrow-x`). Both are cleared when it would point at nothing, and no
|
|
164
|
+
* component has to take part.
|
|
165
|
+
* ------------------------------------------------------------------------------------------------------------- */
|
|
166
|
+
/** The arrow's base, across the panel's edge. Keep in step with `--nx-popover-arrow` in the `.nx-popover` rule. */
|
|
167
|
+
const ARROW_BASE = 26;
|
|
168
|
+
/** The arrow's depth, out from the panel's edge. Keep in step with `--nx-popover-arrow-depth`. */
|
|
169
|
+
const ARROW_DEPTH = 11;
|
|
170
|
+
/** Half the arrow's base: how far from its tip each foot stands. */
|
|
171
|
+
const ARROW_HALF = ARROW_BASE / 2;
|
|
172
|
+
/** How far the arrow reaches past the panel's edge: its depth, less the panel's border it is drawn over. */
|
|
173
|
+
export const ARROW_REACH = ARROW_DEPTH - 1;
|
|
174
|
+
/**
|
|
175
|
+
* How far along the panel's edge the arrow's tip has to stay: its own half base, the panel's rounded corner, and two
|
|
176
|
+
* pixels more, so a foot never stands on the curve and the tail is never read as a torn-off corner.
|
|
177
|
+
*/
|
|
178
|
+
function arrowLead(panel) {
|
|
179
|
+
const style = panel instanceof HTMLElement ? getComputedStyle(panel) : panel;
|
|
180
|
+
return (parseFloat(style.borderTopLeftRadius) || 0) + ARROW_HALF + 2;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Where a panel starts along its edge so that its arrow stands on the middle of the anchor.
|
|
184
|
+
*
|
|
185
|
+
* The tip may not stand on a rounded corner, so it keeps `lead` from each end of the panel. An **icon button** is
|
|
186
|
+
* narrower than two leads, so a panel lined up with it would hold the tip short of its middle: the panel slides
|
|
187
|
+
* along its edge instead, past the end of the button, by half the button's width at most.
|
|
188
|
+
*
|
|
189
|
+
* A **wide anchor keeps its alignment**, whatever the arrow can reach. A menu belongs at the start of the row,
|
|
190
|
+
* the cell, or the field it opened from - sliding it to the middle of a 900px row would move it right across the
|
|
191
|
+
* page - so the arrow aims as far as it can there and is left out when that is more than half a base off, which is
|
|
192
|
+
* what it did before any of this. The window's edges still win over both.
|
|
193
|
+
*/
|
|
194
|
+
function centred(panel, anchor, wanted, width, at, style) {
|
|
195
|
+
const lead = arrowLead(style);
|
|
196
|
+
// Only a button too small to be aimed at while the panel lines up with it is worth moving the panel for.
|
|
197
|
+
if (anchor.width >= lead * 2 || width < lead * 2)
|
|
198
|
+
return wanted;
|
|
199
|
+
// The far end carries a pixel more than the lead: `width` is offsetWidth, a whole number, while the panel itself
|
|
200
|
+
// is fractionally wider, which would otherwise leave the foot that much nearer the curve than the lead promises.
|
|
201
|
+
return Math.min(Math.max(wanted, at - width + lead + 1), at - lead);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Aims the panel's arrow, or leaves it out. The tip is kept clear of the panel's rounded corners, and the arrow is
|
|
205
|
+
* left out when it would point at nothing: the target is off the window, the panel is too narrow to carry an arrow
|
|
206
|
+
* clear of its corners, the panel was pushed along its edge until the tip is more than half a base from the target,
|
|
207
|
+
* or it was pushed away from the target across the edge.
|
|
208
|
+
*/
|
|
209
|
+
function aimArrow(panel, side, box, aim, drawn = getComputedStyle(panel)) {
|
|
210
|
+
if (aim) {
|
|
211
|
+
const lead = arrowLead(drawn);
|
|
212
|
+
const x = Math.min(Math.max(aim.at - box.left, lead), box.width - lead);
|
|
213
|
+
const edge = side === "below" ? box.top : box.top + box.height;
|
|
214
|
+
const tip = box.left + x;
|
|
215
|
+
const points = box.width >= lead * 2 && Math.abs(tip - aim.at) <= ARROW_HALF && Math.abs(edge - aim.facing) <= aim.room + 1;
|
|
216
|
+
if (points && onScreen(aim.facing, aim.at)) {
|
|
217
|
+
panel.dataset.nxArrow = side;
|
|
218
|
+
// An absolutely placed arrow starts at the panel's padding box, so the border it is drawn over comes off here.
|
|
219
|
+
const inside = x - (parseFloat(drawn.borderLeftWidth) || 0);
|
|
220
|
+
panel.style.setProperty("--nx-popover-arrow-x", `${Math.round(inside * 10) / 10}px`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
delete panel.dataset.nxArrow;
|
|
225
|
+
panel.style.removeProperty("--nx-popover-arrow-x");
|
|
226
|
+
}
|
|
227
|
+
/** Whether what the arrow points at is still in the window: a button scrolled out of sight gets no arrow. */
|
|
228
|
+
function onScreen(across, along) {
|
|
229
|
+
return across >= 0 && across <= window.innerHeight && along >= 0 && along <= window.innerWidth;
|
|
230
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { InputParams, InputValidationContext, RowsInputProps, RowValue } from "../Interfaces/FormInterfaces.ts";
|
|
2
|
-
export type RowsStoreOptions<TRow extends RowValue = RowValue> = Pick<RowsInputProps<TRow>, "inputs" | "required" | "min" | "max" | "unique" | "validate" | "itemLabel" | "itemLabelPlural" | "rowKey" | "newRow" | "onRowChange"> & {
|
|
2
|
+
export type RowsStoreOptions<TRow extends RowValue = RowValue> = Pick<RowsInputProps<TRow>, "inputs" | "required" | "min" | "max" | "unique" | "validate" | "itemLabel" | "itemLabelPlural" | "rowKey" | "newRow" | "onRowChange" | "deriveRows"> & {
|
|
3
3
|
/** The row's name when there is no `itemLabel`: "Row" in a table, "Item" in sections. */
|
|
4
4
|
fallbackItem?: string;
|
|
5
5
|
/** The user's date settings, so messages write dates the way the fields show them. */
|
|
@@ -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. */
|
|
@@ -151,6 +156,12 @@ export declare class RowsStore<TRow extends RowValue = RowValue> {
|
|
|
151
156
|
private checkEverything;
|
|
152
157
|
/** After a change: repeated values, the rows' own message, the rows to render again, and value listeners. */
|
|
153
158
|
private settle;
|
|
159
|
+
/**
|
|
160
|
+
* Values the rows' positions decide rather than the user. Every change funnels through `settle`, so this is the one
|
|
161
|
+
* place a move, an add, a remove and a keystroke all pass. The patches are written as code writes, which draws only
|
|
162
|
+
* the cells whose value really changed: a move renumbers its column and leaves focus on the row's own menu.
|
|
163
|
+
*/
|
|
164
|
+
private derive;
|
|
154
165
|
private bump;
|
|
155
166
|
private touchAllRows;
|
|
156
167
|
private emit;
|
|
@@ -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);
|
|
@@ -526,6 +538,7 @@ export class RowsStore {
|
|
|
526
538
|
}
|
|
527
539
|
/** After a change: repeated values, the rows' own message, the rows to render again, and value listeners. */
|
|
528
540
|
settle(changed) {
|
|
541
|
+
this.derive(changed);
|
|
529
542
|
for (const name of this.dirtyUnique)
|
|
530
543
|
this.checkUnique(name, changed);
|
|
531
544
|
this.dirtyUnique.clear();
|
|
@@ -535,6 +548,27 @@ export class RowsStore {
|
|
|
535
548
|
this.refresh();
|
|
536
549
|
this.emit();
|
|
537
550
|
}
|
|
551
|
+
/**
|
|
552
|
+
* Values the rows' positions decide rather than the user. Every change funnels through `settle`, so this is the one
|
|
553
|
+
* place a move, an add, a remove and a keystroke all pass. The patches are written as code writes, which draws only
|
|
554
|
+
* the cells whose value really changed: a move renumbers its column and leaves focus on the row's own menu.
|
|
555
|
+
*/
|
|
556
|
+
derive(changed) {
|
|
557
|
+
const derive = this.options.deriveRows;
|
|
558
|
+
if (!derive)
|
|
559
|
+
return;
|
|
560
|
+
const patches = derive(this.getValue());
|
|
561
|
+
if (!patches)
|
|
562
|
+
return;
|
|
563
|
+
this.entries.forEach((entry, index) => {
|
|
564
|
+
const patch = patches[index];
|
|
565
|
+
if (!patch)
|
|
566
|
+
return;
|
|
567
|
+
// Depth 1, so a derived value never sets `onRowChange` off: these are the store's own writes, not the user's.
|
|
568
|
+
for (const [name, value] of Object.entries(patch))
|
|
569
|
+
this.write(entry.key, name, value, "code", changed, 1);
|
|
570
|
+
});
|
|
571
|
+
}
|
|
538
572
|
bump(key) {
|
|
539
573
|
this.versions.set(key, (this.versions.get(key) ?? 0) + 1);
|
|
540
574
|
const listeners = this.rowListeners.get(key);
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The cells of the header a column stands in, from left to right. The header is one row, or — when any column names a
|
|
3
|
+
* group — a band of group headings over a row holding the headings of the grouped columns only, where a column with no
|
|
4
|
+
* group keeps its heading in the band row across both (`rowSpan`). A band says how many headings it covers with its
|
|
5
|
+
* `colSpan`, so the two rows read back as one list: the table measures the columns from it, and a drag drops among them.
|
|
6
|
+
*/
|
|
7
|
+
export declare function headLeafCells(head: HTMLTableSectionElement | null | undefined): HTMLElement[];
|
|
1
8
|
export interface ColumnDragOptions {
|
|
2
9
|
/** The heading pressed. */
|
|
3
10
|
heading: HTMLElement;
|
|
@@ -9,18 +9,46 @@ const DRAG_THRESHOLD = 4;
|
|
|
9
9
|
/** Pixels from the edge of the scroller where a drag scrolls the columns. */
|
|
10
10
|
const EDGE = 52;
|
|
11
11
|
const KEY = "data-col";
|
|
12
|
+
const GROUP = "data-group";
|
|
12
13
|
const DROP_BEFORE = "data-drop-before";
|
|
13
14
|
const DROP_AFTER = "data-drop-after";
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The cells of the header a column stands in, from left to right. The header is one row, or — when any column names a
|
|
17
|
+
* group — a band of group headings over a row holding the headings of the grouped columns only, where a column with no
|
|
18
|
+
* group keeps its heading in the band row across both (`rowSpan`). A band says how many headings it covers with its
|
|
19
|
+
* `colSpan`, so the two rows read back as one list: the table measures the columns from it, and a drag drops among them.
|
|
20
|
+
*/
|
|
21
|
+
export function headLeafCells(head) {
|
|
22
|
+
const band = head?.rows[0];
|
|
23
|
+
if (!band)
|
|
18
24
|
return [];
|
|
25
|
+
const top = [...band.cells];
|
|
26
|
+
const leaves = head?.rows[1];
|
|
27
|
+
if (!leaves)
|
|
28
|
+
return top;
|
|
29
|
+
const under = [...leaves.cells];
|
|
30
|
+
const cells = [];
|
|
31
|
+
let at = 0;
|
|
32
|
+
for (const cell of top) {
|
|
33
|
+
if (!cell.hasAttribute(GROUP))
|
|
34
|
+
cells.push(cell);
|
|
35
|
+
else
|
|
36
|
+
for (let covered = 0; covered < cell.colSpan; covered++)
|
|
37
|
+
if (under[at])
|
|
38
|
+
cells.push(under[at++]);
|
|
39
|
+
}
|
|
40
|
+
return cells;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The headings a dragged one can be dropped between: the ones pinned to the same side, in the order they are drawn.
|
|
44
|
+
* They are taken from the whole header, so a column under a band and one beside it are the same list.
|
|
45
|
+
*/
|
|
46
|
+
function dropTargets(head, heading) {
|
|
19
47
|
const pin = heading.getAttribute("data-pin") ?? "";
|
|
20
|
-
return
|
|
48
|
+
return headLeafCells(head).filter(cell => cell.hasAttribute(KEY) && (cell.getAttribute("data-pin") ?? "") === pin);
|
|
21
49
|
}
|
|
22
|
-
function clearDropMarks(
|
|
23
|
-
for (const cell of
|
|
50
|
+
function clearDropMarks(head) {
|
|
51
|
+
for (const cell of head?.querySelectorAll(`[${DROP_BEFORE}], [${DROP_AFTER}]`) ?? []) {
|
|
24
52
|
cell.removeAttribute(DROP_BEFORE);
|
|
25
53
|
cell.removeAttribute(DROP_AFTER);
|
|
26
54
|
}
|
|
@@ -31,8 +59,8 @@ function clearDropMarks(row) {
|
|
|
31
59
|
*/
|
|
32
60
|
export function startColumnDrag(event, { heading, scroller, root, onDrop }) {
|
|
33
61
|
const key = heading.getAttribute(KEY) ?? "";
|
|
34
|
-
const
|
|
35
|
-
if (!key || !
|
|
62
|
+
const head = heading.closest("thead");
|
|
63
|
+
if (!key || !head)
|
|
36
64
|
return () => undefined;
|
|
37
65
|
const pointerId = event.pointerId;
|
|
38
66
|
const startX = event.clientX;
|
|
@@ -42,8 +70,8 @@ export function startColumnDrag(event, { heading, scroller, root, onDrop }) {
|
|
|
42
70
|
let scrollBy = 0;
|
|
43
71
|
let frame = 0;
|
|
44
72
|
function place(x) {
|
|
45
|
-
const targets = dropTargets(heading);
|
|
46
|
-
clearDropMarks(
|
|
73
|
+
const targets = dropTargets(head, heading);
|
|
74
|
+
clearDropMarks(head);
|
|
47
75
|
target = null;
|
|
48
76
|
for (let index = 0; index < targets.length; index++) {
|
|
49
77
|
const cell = targets[index];
|
|
@@ -63,7 +91,7 @@ export function startColumnDrag(event, { heading, scroller, root, onDrop }) {
|
|
|
63
91
|
}
|
|
64
92
|
// A drop where the column already sits changes nothing, so no line shows there.
|
|
65
93
|
if (target.before === key || cellKey === key) {
|
|
66
|
-
clearDropMarks(
|
|
94
|
+
clearDropMarks(head);
|
|
67
95
|
target = null;
|
|
68
96
|
}
|
|
69
97
|
return;
|
|
@@ -144,7 +172,7 @@ export function startColumnDrag(event, { heading, scroller, root, onDrop }) {
|
|
|
144
172
|
cancelAnimationFrame(frame);
|
|
145
173
|
if (!dragging)
|
|
146
174
|
return;
|
|
147
|
-
clearDropMarks(
|
|
175
|
+
clearDropMarks(head);
|
|
148
176
|
heading.removeAttribute("data-dragging");
|
|
149
177
|
root?.removeAttribute("data-col-drag");
|
|
150
178
|
if (heading.hasPointerCapture?.(pointerId))
|
|
@@ -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 }) {
|