@warkypublic/svelix 0.1.46 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -33
- package/dist/components/BetterMenu/BetterMenu.svelte +21 -37
- package/dist/components/BetterMenu/BetterMenuAsyncButton.svelte +21 -31
- package/dist/components/BetterMenu/BetterMenuPreview.svelte +27 -29
- package/dist/components/BetterMenu/MenuRenderer.svelte +40 -51
- package/dist/components/Boxer/Boxer.svelte +492 -602
- package/dist/components/Boxer/BoxerTarget.svelte +6 -46
- package/dist/components/Boxer/BoxerTarget.svelte.d.ts +1 -0
- package/dist/components/Boxer/types.d.ts +0 -1
- package/dist/components/Button.svelte +19 -43
- package/dist/components/ButtonPreview.svelte +2 -11
- package/dist/components/CardGrid/CardGrid.svelte +489 -614
- package/dist/components/CardGrid/CardGrid.svelte.d.ts +4 -0
- package/dist/components/CardGrid/CardGridFilterPanel.svelte +111 -90
- package/dist/components/CardGrid/DefaultCard.svelte +86 -97
- package/dist/components/CardGrid/DefaultCard.svelte.d.ts +1 -0
- package/dist/components/CardGrid/ImageCardStory.svelte +87 -36
- package/dist/components/CardGrid/cardGrid.d.ts +24 -0
- package/dist/components/CardGrid/cardGrid.js +20 -0
- package/dist/components/ContentEditor/CollaboraInsertTextDemo.svelte +5 -10
- package/dist/components/ContentEditor/ContentEditor.svelte +24 -50
- package/dist/components/ContentEditor/subcomponents/AudioPlayer.svelte +17 -17
- package/dist/components/ContentEditor/subcomponents/CollaboraEditor.svelte +244 -309
- package/dist/components/ContentEditor/subcomponents/EmailViewer.svelte +2 -3
- package/dist/components/ContentEditor/subcomponents/ImageViewer.svelte +14 -15
- package/dist/components/ContentEditor/subcomponents/MarkdownViewer.svelte +74 -90
- package/dist/components/ContentEditor/subcomponents/MonacoEditor.svelte +75 -103
- package/dist/components/ContentEditor/subcomponents/Office365Editor.svelte +234 -297
- package/dist/components/ContentEditor/subcomponents/PdfViewer.svelte +14 -20
- package/dist/components/ContentEditor/subcomponents/TextEditor.svelte +122 -150
- package/dist/components/ContentEditor/subcomponents/UnknownFile.svelte +105 -126
- package/dist/components/ContentEditor/subcomponents/VideoPlayer.svelte +17 -17
- package/dist/components/ContentEditor/subcomponents/ZipViewer.svelte +2 -3
- package/dist/components/ErrorBoundary/ErrorBoundary.svelte +41 -58
- package/dist/components/ErrorBoundary/ErrorBoundaryPreview.svelte +8 -16
- package/dist/components/Former/Former.svelte +306 -424
- package/dist/components/Former/FormerButtonArea.svelte +17 -68
- package/dist/components/Former/FormerDrawer.svelte +16 -80
- package/dist/components/Former/FormerDrawerPreview.svelte +99 -89
- package/dist/components/Former/FormerModal.svelte +16 -74
- package/dist/components/Former/FormerModalPreview.svelte +99 -89
- package/dist/components/Former/FormerPreview.svelte +76 -78
- package/dist/components/Former/FormerRestApiPreview.svelte +55 -48
- package/dist/components/Former/formerOverlayHelpers.svelte.d.ts +11 -0
- package/dist/components/Former/formerOverlayHelpers.svelte.js +38 -0
- package/dist/components/Former/index.d.ts +1 -0
- package/dist/components/Former/index.js +1 -0
- package/dist/components/FormerControllers/ButtonCtrl.svelte +13 -49
- package/dist/components/FormerControllers/CtrlFieldShell.svelte +15 -0
- package/dist/components/FormerControllers/CtrlFieldShell.svelte.d.ts +12 -0
- package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrl.svelte +285 -457
- package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlCalendar.svelte +95 -136
- package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlPickerPanel.svelte +3 -62
- package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlTimeFields.svelte +143 -217
- package/dist/components/FormerControllers/IconButtonCtrl.svelte +13 -47
- package/dist/components/FormerControllers/InlineWrapper.svelte +12 -41
- package/dist/components/FormerControllers/InlineWrapperPreview.svelte +4 -26
- package/dist/components/FormerControllers/NativeSelectCtrl.svelte +7 -39
- package/dist/components/FormerControllers/NumberInputCtrl.svelte +21 -43
- package/dist/components/FormerControllers/PasswordInputCtrl.svelte +8 -33
- package/dist/components/FormerControllers/SwitchCtrl.svelte +4 -20
- package/dist/components/FormerControllers/TextAreaCtrl.svelte +7 -34
- package/dist/components/FormerControllers/TextInputCtrl.svelte +7 -34
- package/dist/components/FormerControllers/ctrlLoading.svelte.d.ts +8 -0
- package/dist/components/FormerControllers/ctrlLoading.svelte.js +43 -0
- package/dist/components/FormerControllers/index.d.ts +2 -0
- package/dist/components/FormerControllers/index.js +2 -0
- package/dist/components/FormerControllers/utils.d.ts +1 -0
- package/dist/components/FormerControllers/utils.js +3 -0
- package/dist/components/GlobalStateStore/GlobalStateStoreProvider.svelte +73 -120
- package/dist/components/Gridler/components/Gridler.svelte +344 -442
- package/dist/components/Gridler/components/GridlerCanvas.svelte +720 -1115
- package/dist/components/Gridler/components/GridlerEditor.svelte +2 -15
- package/dist/components/Gridler/components/GridlerFull.svelte +521 -841
- package/dist/components/Gridler/components/GridlerFull.svelte.d.ts +1 -1
- package/dist/components/Gridler/components/GridlerFullWithFormerPreview.svelte +194 -147
- package/dist/components/Gridler/components/GridlerSearch.svelte +7 -16
- package/dist/components/Gridler/components/GridlerSearchToggle.svelte +7 -19
- package/dist/components/Gridler/types.d.ts +1 -1
- package/dist/components/Gridler/utils/serverPaging.d.ts +1 -0
- package/dist/components/Gridler/utils/serverPaging.js +7 -0
- package/dist/components/Icons/IconAdd.svelte +1 -3
- package/dist/components/Icons/IconAlertCircle.svelte +1 -3
- package/dist/components/Icons/IconAlertTriangle.svelte +1 -3
- package/dist/components/Icons/IconCamera.svelte +1 -3
- package/dist/components/Icons/IconClose.svelte +1 -3
- package/dist/components/Icons/IconEdit.svelte +1 -3
- package/dist/components/Icons/IconFilter.svelte +1 -3
- package/dist/components/Icons/IconSearch.svelte +1 -3
- package/dist/components/Icons/IconSort.svelte +1 -3
- package/dist/components/Icons/IconTrash.svelte +1 -3
- package/dist/components/Portal/Portal.svelte +14 -38
- package/dist/components/Screenshot/Screenshot.svelte +7 -13
- package/dist/components/Svark/Svark.svelte +680 -852
- package/dist/components/Svark/SvarkContextMenu.svelte +44 -63
- package/dist/components/Svark/SvarkInfiniteLoader.svelte +2 -6
- package/dist/components/Svark/SvarkPager.svelte +2 -28
- package/dist/components/Svark/SvarkSelectionDemo.svelte +4 -8
- package/dist/components/Svark/SvarkTopBar.svelte +9 -74
- package/dist/components/SvarkGrid/SvarkGrid.svelte +877 -1123
- package/dist/components/SvarkGrid/components/SvarkColumnFilterForm.svelte +45 -58
- package/dist/components/SvarkGrid/components/SvarkContextMenu.svelte +44 -63
- package/dist/components/SvarkGrid/components/SvarkHeaderFilterCell.svelte +41 -75
- package/dist/components/SvarkGrid/components/SvarkInfiniteLoader.svelte +2 -6
- package/dist/components/SvarkGrid/components/SvarkPager.svelte +2 -28
- package/dist/components/SvarkGrid/components/SvarkSearchPopover.svelte +8 -16
- package/dist/components/SvarkGrid/components/SvarkTopBar.svelte +6 -62
- package/dist/components/SvarkGrid/internal/SvarkGridView.svelte +345 -507
- package/dist/components/VTree/VTree.svelte +454 -431
- package/dist/components/VTree/VTree.utils.d.ts +23 -0
- package/dist/components/VTree/VTree.utils.js +73 -0
- package/dist/components/VTree/VTreeContextMenu.svelte +21 -33
- package/dist/components/VTree/VTreeControlledSelectionDemo.svelte +20 -0
- package/dist/components/VTree/VTreeControlledSelectionDemo.svelte.d.ts +18 -0
- package/dist/components/VTree/VTreeCustomizationDemo.svelte +23 -0
- package/dist/components/VTree/VTreeCustomizationDemo.svelte.d.ts +18 -0
- package/dist/components/VTree/VTreeEventsDemo.svelte +48 -58
- package/dist/components/VTree/VTreeInteractionDemo.svelte +29 -0
- package/dist/components/VTree/VTreeInteractionDemo.svelte.d.ts +18 -0
- package/dist/components/VTree/VTreeMultiSelectDemo.svelte +27 -0
- package/dist/components/VTree/VTreeMultiSelectDemo.svelte.d.ts +18 -0
- package/dist/components/VTree/VTreeRow.svelte +77 -115
- package/dist/components/VTree/VTreeRow.svelte.d.ts +34 -18
- package/dist/components/VTree/VTreeSearch.svelte +6 -13
- package/dist/components/VTree/VTreeVirtualViewport.svelte +54 -143
- package/dist/components/VTree/VTreeVirtualViewport.svelte.d.ts +35 -18
- package/dist/components/VTree/types.d.ts +39 -1
- package/llm/COMPONENT_GUIDE.md +25 -0
- package/package.json +51 -50
|
@@ -1,903 +1,731 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
baseUrl?: string;
|
|
33
|
-
token?: string;
|
|
34
|
-
schema: string;
|
|
35
|
-
entity: string;
|
|
36
|
-
adapterType?: SvarkAdapterType;
|
|
37
|
-
// Backwards compatibility alias for `adapterType`.
|
|
38
|
-
clientType?: SvarkAdapterType;
|
|
39
|
-
columns: IColumnConfig[];
|
|
40
|
-
config?: Omit<IConfig, 'data'>;
|
|
41
|
-
query?: SvarkInitialQuery;
|
|
42
|
-
pageSize?: number;
|
|
43
|
-
serverSide?: boolean;
|
|
44
|
-
height?: string | number;
|
|
45
|
-
select?: boolean;
|
|
46
|
-
multiselect?: boolean;
|
|
47
|
-
virtualScroll?: boolean;
|
|
48
|
-
infiniteScroll?: boolean;
|
|
49
|
-
topBarLeft?: Snippet;
|
|
50
|
-
topBarRight?: Snippet;
|
|
51
|
-
onchange?: (event: SvarkChangeEvent) => void;
|
|
52
|
-
onerror?: (message: string) => void;
|
|
53
|
-
onselect?: (ids: (string | number)[]) => void;
|
|
54
|
-
onrowclick?: (event: SvarkRowInteractionEvent) => void;
|
|
55
|
-
onrowdblclick?: (event: SvarkRowInteractionEvent) => void;
|
|
56
|
-
onrowcontextmenu?: (event: SvarkRowInteractionEvent) => void;
|
|
57
|
-
contextMenuProps?: SvarkContextMenuProps;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
let {
|
|
61
|
-
url,
|
|
62
|
-
baseUrl,
|
|
63
|
-
token,
|
|
64
|
-
schema,
|
|
65
|
-
entity,
|
|
66
|
-
adapterType,
|
|
67
|
-
clientType,
|
|
68
|
-
columns,
|
|
69
|
-
config = {},
|
|
70
|
-
query = {},
|
|
71
|
-
pageSize = 100,
|
|
72
|
-
serverSide = false,
|
|
73
|
-
height = '400px',
|
|
74
|
-
select = true,
|
|
75
|
-
multiselect = false,
|
|
76
|
-
virtualScroll = false,
|
|
77
|
-
infiniteScroll = false,
|
|
78
|
-
topBarLeft,
|
|
79
|
-
topBarRight,
|
|
80
|
-
onchange,
|
|
81
|
-
onerror,
|
|
82
|
-
onselect,
|
|
83
|
-
onrowclick,
|
|
84
|
-
onrowdblclick,
|
|
85
|
-
onrowcontextmenu,
|
|
86
|
-
contextMenuProps
|
|
87
|
-
}: Props = $props();
|
|
88
|
-
|
|
89
|
-
function normalizeAdapterType(t: SvarkAdapterType): 'ResolveSpec' | 'RestHeaderSpec' {
|
|
90
|
-
return t === 'header' || t === 'RestHeaderSpec' ? 'RestHeaderSpec' : 'ResolveSpec';
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const FILTER_OPERATOR_LABELS: Record<string, string> = {
|
|
94
|
-
contains: 'Contains',
|
|
95
|
-
eq: 'Equals',
|
|
96
|
-
neq: 'Not equal',
|
|
97
|
-
startswith: 'Starts with',
|
|
98
|
-
endswith: 'Ends with',
|
|
99
|
-
gt: 'Greater than',
|
|
100
|
-
gte: 'Greater or equal',
|
|
101
|
-
lt: 'Less than',
|
|
102
|
-
lte: 'Less or equal'
|
|
1
|
+
<script lang="ts">import { Grid, Willow, WillowDark } from "@svar-ui/svelte-grid";
|
|
2
|
+
import SvarkInfiniteLoader from "./SvarkInfiniteLoader.svelte";
|
|
3
|
+
import SvarkPager from "./SvarkPager.svelte";
|
|
4
|
+
import { SvarkResolveSpecAdapter } from "./SvarkResolveSpecAdapter.js";
|
|
5
|
+
import { SvarkRestHeaderSpecAdapter } from "./SvarkRestHeaderSpecAdapter.js";
|
|
6
|
+
import SvarkTopBar from "./SvarkTopBar.svelte";
|
|
7
|
+
import { reduceServerFilterState } from "./filtering.js";
|
|
8
|
+
import SvarkContextMenu from "./SvarkContextMenu.svelte";
|
|
9
|
+
let { url, baseUrl, token, schema, entity, adapterType, clientType, columns, config = {}, query = {}, pageSize = 100, serverSide = false, height = "400px", select = true, multiselect = false, virtualScroll = false, infiniteScroll = false, topBarLeft, topBarRight, onchange, onerror, onselect, onrowclick, onrowdblclick, onrowcontextmenu, contextMenuProps } = $props();
|
|
10
|
+
function normalizeAdapterType(t) {
|
|
11
|
+
return t === "header" || t === "RestHeaderSpec" ? "RestHeaderSpec" : "ResolveSpec";
|
|
12
|
+
}
|
|
13
|
+
const FILTER_OPERATOR_LABELS = {
|
|
14
|
+
contains: "Contains",
|
|
15
|
+
eq: "Equals",
|
|
16
|
+
neq: "Not equal",
|
|
17
|
+
startswith: "Starts with",
|
|
18
|
+
endswith: "Ends with",
|
|
19
|
+
gt: "Greater than",
|
|
20
|
+
gte: "Greater or equal",
|
|
21
|
+
lt: "Less than",
|
|
22
|
+
lte: "Less or equal"
|
|
23
|
+
};
|
|
24
|
+
function normalizeOperatorToken(value) {
|
|
25
|
+
return String(value ?? "contains").trim().toLowerCase().replace(/[\s_-]+/g, "");
|
|
26
|
+
}
|
|
27
|
+
function mapPopupOperatorValue(operator, value) {
|
|
28
|
+
const normalized = normalizeOperatorToken(operator);
|
|
29
|
+
if (normalized === "contains") return {
|
|
30
|
+
operator: "ilike",
|
|
31
|
+
value
|
|
103
32
|
};
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
33
|
+
if (normalized === "startswith") return {
|
|
34
|
+
operator: "ilike",
|
|
35
|
+
value: `${value}%`
|
|
36
|
+
};
|
|
37
|
+
if (normalized === "endswith") return {
|
|
38
|
+
operator: "ilike",
|
|
39
|
+
value: `%${value}`
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
operator: normalized,
|
|
43
|
+
value
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// Enable column sorting by default.
|
|
47
|
+
// Opt-out: set `sort: false` on a column.
|
|
48
|
+
const gridColumns = $derived((columns ?? []).map((col) => {
|
|
49
|
+
return {
|
|
50
|
+
...col,
|
|
51
|
+
sort: col.sort ?? true
|
|
52
|
+
};
|
|
53
|
+
}));
|
|
54
|
+
const filterableColumns = $derived(gridColumns.filter((col) => col.id != null && !col.hidden).map((col) => ({
|
|
55
|
+
id: String(col.id),
|
|
56
|
+
label: typeof col.header === "string" ? String(col.header) : String(col.header?.text ?? col.id),
|
|
57
|
+
column: col
|
|
58
|
+
})));
|
|
59
|
+
const canFilter = $derived(serverSide && filterableColumns.length > 0);
|
|
60
|
+
function getColumnDefinition(columnId) {
|
|
61
|
+
return filterableColumns.find((entry) => entry.id === columnId)?.column;
|
|
62
|
+
}
|
|
63
|
+
function getColumnOptions(columnId) {
|
|
64
|
+
const column = getColumnDefinition(columnId);
|
|
65
|
+
const source = column?.filterOptions;
|
|
66
|
+
if (!source?.length) return [];
|
|
67
|
+
return source.map((option) => typeof option === "object" && option != null && "id" in option && "label" in option ? {
|
|
68
|
+
value: String(option.id),
|
|
69
|
+
label: String(option.label)
|
|
70
|
+
} : {
|
|
71
|
+
value: String(option),
|
|
72
|
+
label: String(option)
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
function getSupportedOperators(columnId) {
|
|
76
|
+
const column = getColumnDefinition(columnId);
|
|
77
|
+
const configured = column?.filterOperators?.map((item) => normalizeOperatorToken(item)).filter(Boolean);
|
|
78
|
+
if (configured?.length) return configured;
|
|
79
|
+
if (column?.filterOptions?.length) return ["eq", "neq"];
|
|
80
|
+
return [
|
|
81
|
+
"contains",
|
|
82
|
+
"eq",
|
|
83
|
+
"neq",
|
|
84
|
+
"startswith",
|
|
85
|
+
"endswith"
|
|
86
|
+
];
|
|
87
|
+
}
|
|
88
|
+
function getDefaultOperator(columnId) {
|
|
89
|
+
return getSupportedOperators(columnId)[0] ?? "contains";
|
|
90
|
+
}
|
|
91
|
+
let popupFilterId = $state(0);
|
|
92
|
+
function nextPopupFilterId() {
|
|
93
|
+
popupFilterId += 1;
|
|
94
|
+
return popupFilterId;
|
|
95
|
+
}
|
|
96
|
+
function createPopupDraft(columnId) {
|
|
97
|
+
const selectedColumn = columnId ?? filterableColumns[0]?.id ?? "";
|
|
98
|
+
return {
|
|
99
|
+
id: nextPopupFilterId(),
|
|
100
|
+
column: selectedColumn,
|
|
101
|
+
operator: getDefaultOperator(selectedColumn),
|
|
102
|
+
value: "",
|
|
103
|
+
logic: "AND"
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function clonePopupDrafts(filters) {
|
|
107
|
+
return filters.map((filter) => ({ ...filter }));
|
|
108
|
+
}
|
|
109
|
+
function stripPopupFilters(state) {
|
|
110
|
+
return Object.fromEntries(Object.entries(state).filter(([key]) => !key.startsWith("popup:")));
|
|
111
|
+
}
|
|
112
|
+
function buildPopupFilterState(filters) {
|
|
113
|
+
const next = {};
|
|
114
|
+
for (const filter of filters) {
|
|
115
|
+
const value = filter.value.trim();
|
|
116
|
+
if (!filter.column || value === "") continue;
|
|
117
|
+
const mapped = mapPopupOperatorValue(filter.operator, value);
|
|
118
|
+
next[`popup:${filter.id}`] = {
|
|
119
|
+
column: filter.column,
|
|
120
|
+
operator: mapped.operator,
|
|
121
|
+
value: mapped.value,
|
|
122
|
+
logic_operator: filter.logic
|
|
187
123
|
};
|
|
188
124
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
125
|
+
return next;
|
|
126
|
+
}
|
|
127
|
+
function syncPopupFilters(filters) {
|
|
128
|
+
popupAppliedFilters = clonePopupDrafts(filters).filter((filter) => filter.column && filter.value.trim() !== "");
|
|
129
|
+
filterState = {
|
|
130
|
+
...stripPopupFilters(filterState),
|
|
131
|
+
...buildPopupFilterState(popupAppliedFilters)
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const resolvedUrl = $derived(url ?? baseUrl ?? "");
|
|
135
|
+
const resolvedAdapterType = $derived(normalizeAdapterType(adapterType ?? clientType ?? "body"));
|
|
136
|
+
const heightStyle = $derived(typeof height === "number" ? `${height}px` : height);
|
|
137
|
+
let data = $state([]);
|
|
138
|
+
let loading = $state(false);
|
|
139
|
+
let infiniteLoading = $state(false);
|
|
140
|
+
let error = $state(null);
|
|
141
|
+
let total = $state(0);
|
|
142
|
+
let sortState = $state([]);
|
|
143
|
+
let filterState = $state({});
|
|
144
|
+
let searchFilterState = $state({});
|
|
145
|
+
let page = $state(0);
|
|
146
|
+
let reload = $state(0);
|
|
147
|
+
let gridApi = $state(null);
|
|
148
|
+
let searchOpen = $state(false);
|
|
149
|
+
let searchValue = $state("");
|
|
150
|
+
let filterMenuOpen = $state(false);
|
|
151
|
+
let popupAppliedFilters = $state([]);
|
|
152
|
+
let popupDraftFilters = $state([]);
|
|
153
|
+
// Paging is active when serverSide with no scroll variant
|
|
154
|
+
const showPager = $derived(serverSide && !infiniteScroll && !virtualScroll);
|
|
155
|
+
const totalPages = $derived(pageSize > 0 ? Math.ceil(total / pageSize) : 1);
|
|
156
|
+
const pageStart = $derived(total === 0 ? 0 : page * pageSize + 1);
|
|
157
|
+
const pageEnd = $derived(Math.min((page + 1) * pageSize, total));
|
|
158
|
+
// Debounced mirrors of sortState/filterState used by the main fetch effect.
|
|
159
|
+
// When serverSide is true, changes are held for 300 ms before committing so
|
|
160
|
+
// rapid keystrokes / click-sort chains don't fire a request per event.
|
|
161
|
+
// Page resets to 0 atomically with the debounced update so only one fetch fires.
|
|
162
|
+
let debouncedSort = $state([]);
|
|
163
|
+
let debouncedFilter = $state({});
|
|
164
|
+
let _debounceTimer = null;
|
|
165
|
+
$effect(() => {
|
|
166
|
+
const s = sortState;
|
|
167
|
+
const f = {
|
|
168
|
+
...filterState,
|
|
169
|
+
...searchFilterState
|
|
170
|
+
};
|
|
171
|
+
if (!serverSide) {
|
|
172
|
+
debouncedSort = s;
|
|
173
|
+
debouncedFilter = f;
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (_debounceTimer !== null) clearTimeout(_debounceTimer);
|
|
177
|
+
_debounceTimer = setTimeout(() => {
|
|
178
|
+
_debounceTimer = null;
|
|
179
|
+
page = 0;
|
|
180
|
+
debouncedSort = s;
|
|
181
|
+
debouncedFilter = f;
|
|
182
|
+
}, 300);
|
|
183
|
+
return () => {
|
|
184
|
+
if (_debounceTimer !== null) {
|
|
185
|
+
clearTimeout(_debounceTimer);
|
|
186
|
+
_debounceTimer = null;
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
// Dark mode detection — priority: inline color-scheme style > .dark class > OS media query
|
|
191
|
+
let isDark = $state(false);
|
|
192
|
+
$effect(() => {
|
|
193
|
+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
|
|
194
|
+
const html = document.documentElement;
|
|
195
|
+
function update() {
|
|
196
|
+
const explicit = html.style.colorScheme;
|
|
197
|
+
if (explicit === "dark" || explicit === "light") {
|
|
198
|
+
isDark = explicit === "dark";
|
|
199
|
+
} else {
|
|
200
|
+
isDark = html.classList.contains("dark") || mq.matches;
|
|
212
201
|
}
|
|
213
|
-
return next;
|
|
214
202
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
203
|
+
update();
|
|
204
|
+
mq.addEventListener("change", update);
|
|
205
|
+
const observer = new MutationObserver(update);
|
|
206
|
+
observer.observe(html, { attributeFilter: ["class", "style"] });
|
|
207
|
+
return () => {
|
|
208
|
+
mq.removeEventListener("change", update);
|
|
209
|
+
observer.disconnect();
|
|
210
|
+
};
|
|
211
|
+
});
|
|
212
|
+
// Bind to outer container so we can find .wx-scroll for infinite scroll
|
|
213
|
+
let containerRef = $state();
|
|
214
|
+
let scrollContainerRef = $state(null);
|
|
215
|
+
function getLocalSearchColumns() {
|
|
216
|
+
const columnsMap = {};
|
|
217
|
+
for (const col of gridColumns) {
|
|
218
|
+
const id = col.id;
|
|
219
|
+
if (id == null) continue;
|
|
220
|
+
if (col.hidden) continue;
|
|
221
|
+
if (col.searchable === false) continue;
|
|
222
|
+
columnsMap[String(id)] = true;
|
|
223
|
+
}
|
|
224
|
+
return Object.keys(columnsMap).length > 0 ? columnsMap : undefined;
|
|
225
|
+
}
|
|
226
|
+
function applyServerSearchFilters(value) {
|
|
227
|
+
if (!serverSide) return;
|
|
228
|
+
if (!value) {
|
|
229
|
+
searchFilterState = {};
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const searchableIds = [];
|
|
233
|
+
for (const col of gridColumns) {
|
|
234
|
+
const id = col.id;
|
|
235
|
+
if (id == null) continue;
|
|
236
|
+
if (col.hidden) continue;
|
|
237
|
+
if (col.searchable === true) searchableIds.push(String(id));
|
|
238
|
+
}
|
|
239
|
+
if (searchableIds.length === 0) {
|
|
240
|
+
searchFilterState = {};
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const next = {};
|
|
244
|
+
for (const id of searchableIds) {
|
|
245
|
+
// Avoid clobbering an existing column filter the user set elsewhere.
|
|
246
|
+
const hasExistingFilter = Object.entries(filterState).some(([key, filter]) => {
|
|
247
|
+
const target = filter.column ?? key;
|
|
248
|
+
return target === id && filter.value != null && filter.value !== "";
|
|
249
|
+
});
|
|
250
|
+
if (hasExistingFilter) continue;
|
|
251
|
+
next[id] = {
|
|
252
|
+
value,
|
|
253
|
+
operator: "ilike",
|
|
254
|
+
logic_operator: "OR"
|
|
221
255
|
};
|
|
222
256
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
let loading = $state(false);
|
|
231
|
-
let infiniteLoading = $state(false);
|
|
232
|
-
let error = $state<string | null>(null);
|
|
233
|
-
let total = $state(0);
|
|
234
|
-
let sortState = $state<SvarkSortState[]>([]);
|
|
235
|
-
let filterState = $state<SvarkFilterState>({});
|
|
236
|
-
let searchFilterState = $state<SvarkFilterState>({});
|
|
237
|
-
let page = $state(0);
|
|
238
|
-
let reload = $state(0);
|
|
239
|
-
|
|
240
|
-
let gridApi = $state<IApi | null>(null);
|
|
241
|
-
let searchOpen = $state(false);
|
|
242
|
-
let searchValue = $state('');
|
|
243
|
-
let filterMenuOpen = $state(false);
|
|
244
|
-
let popupAppliedFilters = $state<SvarkPopupFilterDraft[]>([]);
|
|
245
|
-
let popupDraftFilters = $state<SvarkPopupFilterDraft[]>([]);
|
|
246
|
-
|
|
247
|
-
// Paging is active when serverSide with no scroll variant
|
|
248
|
-
const showPager = $derived(serverSide && !infiniteScroll && !virtualScroll);
|
|
249
|
-
const totalPages = $derived(pageSize > 0 ? Math.ceil(total / pageSize) : 1);
|
|
250
|
-
const pageStart = $derived(total === 0 ? 0 : page * pageSize + 1);
|
|
251
|
-
const pageEnd = $derived(Math.min((page + 1) * pageSize, total));
|
|
252
|
-
|
|
253
|
-
// Debounced mirrors of sortState/filterState used by the main fetch effect.
|
|
254
|
-
// When serverSide is true, changes are held for 300 ms before committing so
|
|
255
|
-
// rapid keystrokes / click-sort chains don't fire a request per event.
|
|
256
|
-
// Page resets to 0 atomically with the debounced update so only one fetch fires.
|
|
257
|
-
let debouncedSort = $state<SvarkSortState[]>([]);
|
|
258
|
-
let debouncedFilter = $state<SvarkFilterState>({});
|
|
259
|
-
let _debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
260
|
-
|
|
261
|
-
$effect(() => {
|
|
262
|
-
const s = sortState;
|
|
263
|
-
const f = { ...filterState, ...searchFilterState };
|
|
264
|
-
|
|
265
|
-
if (!serverSide) {
|
|
266
|
-
debouncedSort = s;
|
|
267
|
-
debouncedFilter = f;
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (_debounceTimer !== null) clearTimeout(_debounceTimer);
|
|
272
|
-
_debounceTimer = setTimeout(() => {
|
|
273
|
-
_debounceTimer = null;
|
|
274
|
-
page = 0; // reset page atomically — prevents a double-fetch
|
|
275
|
-
debouncedSort = s;
|
|
276
|
-
debouncedFilter = f;
|
|
277
|
-
}, 300);
|
|
278
|
-
|
|
279
|
-
return () => {
|
|
280
|
-
if (_debounceTimer !== null) {
|
|
281
|
-
clearTimeout(_debounceTimer);
|
|
282
|
-
_debounceTimer = null;
|
|
283
|
-
}
|
|
284
|
-
};
|
|
257
|
+
searchFilterState = next;
|
|
258
|
+
}
|
|
259
|
+
function setSearch(next) {
|
|
260
|
+
searchValue = next;
|
|
261
|
+
gridApi?.exec("search-rows", {
|
|
262
|
+
search: next,
|
|
263
|
+
columns: getLocalSearchColumns()
|
|
285
264
|
});
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
265
|
+
applyServerSearchFilters(next.trim());
|
|
266
|
+
}
|
|
267
|
+
function openSearch() {
|
|
268
|
+
searchOpen = true;
|
|
269
|
+
}
|
|
270
|
+
function closeSearch() {
|
|
271
|
+
searchOpen = false;
|
|
272
|
+
}
|
|
273
|
+
function openFilterMenu() {
|
|
274
|
+
popupDraftFilters = popupAppliedFilters.length > 0 ? clonePopupDrafts(popupAppliedFilters) : [createPopupDraft()];
|
|
275
|
+
filterMenuOpen = true;
|
|
276
|
+
}
|
|
277
|
+
function closeFilterMenu() {
|
|
278
|
+
filterMenuOpen = false;
|
|
279
|
+
}
|
|
280
|
+
function toggleFilterMenu() {
|
|
281
|
+
if (filterMenuOpen) {
|
|
282
|
+
closeFilterMenu();
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
openFilterMenu();
|
|
286
|
+
}
|
|
287
|
+
function addPopupDraft() {
|
|
288
|
+
popupDraftFilters = [...popupDraftFilters, createPopupDraft()];
|
|
289
|
+
}
|
|
290
|
+
function removePopupDraft(id) {
|
|
291
|
+
const next = popupDraftFilters.filter((filter) => filter.id !== id);
|
|
292
|
+
popupDraftFilters = next.length > 0 ? next : [createPopupDraft()];
|
|
293
|
+
}
|
|
294
|
+
function updatePopupDraft(id, updates, resetValue = false) {
|
|
295
|
+
popupDraftFilters = popupDraftFilters.map((filter) => {
|
|
296
|
+
if (filter.id !== id) return filter;
|
|
297
|
+
const next = {
|
|
298
|
+
...filter,
|
|
299
|
+
...updates
|
|
300
|
+
};
|
|
301
|
+
if (updates.column) {
|
|
302
|
+
const supported = getSupportedOperators(updates.column);
|
|
303
|
+
if (!supported.includes(normalizeOperatorToken(next.operator))) {
|
|
304
|
+
next.operator = getDefaultOperator(updates.column);
|
|
300
305
|
}
|
|
306
|
+
if (resetValue) next.value = "";
|
|
301
307
|
}
|
|
302
|
-
|
|
303
|
-
update();
|
|
304
|
-
mq.addEventListener('change', update);
|
|
305
|
-
|
|
306
|
-
const observer = new MutationObserver(update);
|
|
307
|
-
observer.observe(html, { attributeFilter: ['class', 'style'] });
|
|
308
|
-
|
|
309
|
-
return () => {
|
|
310
|
-
mq.removeEventListener('change', update);
|
|
311
|
-
observer.disconnect();
|
|
312
|
-
};
|
|
308
|
+
return next;
|
|
313
309
|
});
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
310
|
+
}
|
|
311
|
+
function applyPopupDrafts() {
|
|
312
|
+
syncPopupFilters(popupDraftFilters);
|
|
313
|
+
filterMenuOpen = false;
|
|
314
|
+
}
|
|
315
|
+
function clearPopupFilters() {
|
|
316
|
+
popupAppliedFilters = [];
|
|
317
|
+
popupDraftFilters = [createPopupDraft()];
|
|
318
|
+
filterState = stripPopupFilters(filterState);
|
|
319
|
+
}
|
|
320
|
+
function removeAppliedPopupFilter(id) {
|
|
321
|
+
syncPopupFilters(popupAppliedFilters.filter((filter) => filter.id !== id));
|
|
322
|
+
}
|
|
323
|
+
$effect(() => {
|
|
324
|
+
if (!containerRef) return;
|
|
325
|
+
const node = containerRef;
|
|
326
|
+
function onKeyDown(ev) {
|
|
327
|
+
const key = ev.key?.toLowerCase?.() ?? "";
|
|
328
|
+
const isFind = (ev.ctrlKey || ev.metaKey) && !ev.altKey && !ev.shiftKey && key === "f";
|
|
329
|
+
if (isFind) {
|
|
330
|
+
ev.preventDefault();
|
|
331
|
+
ev.stopPropagation();
|
|
332
|
+
openSearch();
|
|
336
333
|
return;
|
|
337
334
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (id == null) continue;
|
|
343
|
-
if ((col as any).hidden) continue;
|
|
344
|
-
if ((col as any).searchable === true) searchableIds.push(String(id));
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
if (searchableIds.length === 0) {
|
|
348
|
-
searchFilterState = {};
|
|
349
|
-
return;
|
|
335
|
+
if (searchOpen && ev.key === "Escape") {
|
|
336
|
+
ev.preventDefault();
|
|
337
|
+
ev.stopPropagation();
|
|
338
|
+
closeSearch();
|
|
350
339
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const hasExistingFilter = Object.entries(filterState).some(([key, filter]) => {
|
|
356
|
-
const target = filter.column ?? key;
|
|
357
|
-
return target === id && filter.value != null && filter.value !== '';
|
|
358
|
-
});
|
|
359
|
-
if (hasExistingFilter) continue;
|
|
360
|
-
next[id] = { value, operator: 'ilike', logic_operator: 'OR' };
|
|
340
|
+
if (contextMenuOpen && ev.key === "Escape") {
|
|
341
|
+
ev.preventDefault();
|
|
342
|
+
ev.stopPropagation();
|
|
343
|
+
closeContextMenu();
|
|
361
344
|
}
|
|
362
|
-
searchFilterState = next;
|
|
363
345
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
346
|
+
node.addEventListener("keydown", onKeyDown, true);
|
|
347
|
+
return () => node.removeEventListener("keydown", onKeyDown, true);
|
|
348
|
+
});
|
|
349
|
+
let selectedIdsInternal = $state([]);
|
|
350
|
+
const selectedRow = $derived.by(() => {
|
|
351
|
+
const id = selectedIdsInternal[0];
|
|
352
|
+
if (id == null) return null;
|
|
353
|
+
return data.find((item) => String(item?.id) === String(id)) ?? null;
|
|
354
|
+
});
|
|
355
|
+
function resolveRowFromTarget(target) {
|
|
356
|
+
if (!containerRef || !(target instanceof Element)) return undefined;
|
|
357
|
+
const rowEl = target.closest("[data-id]");
|
|
358
|
+
if (!rowEl || !containerRef.contains(rowEl)) return undefined;
|
|
359
|
+
const attrId = rowEl.getAttribute("data-id");
|
|
360
|
+
if (attrId == null) return undefined;
|
|
361
|
+
const row = data.find((item) => String(item?.id) === attrId);
|
|
362
|
+
return {
|
|
363
|
+
id: row?.id ?? attrId,
|
|
364
|
+
row
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
let contextMenuOpen = $state(false);
|
|
368
|
+
let contextMenuX = $state(0);
|
|
369
|
+
let contextMenuY = $state(0);
|
|
370
|
+
let contextMenuRowId = $state(null);
|
|
371
|
+
let contextMenuRow = $state(null);
|
|
372
|
+
let contextMenuEvent = $state(null);
|
|
373
|
+
function buildContextMenuCtx() {
|
|
374
|
+
if (!contextMenuEvent) return null;
|
|
375
|
+
return {
|
|
376
|
+
api: gridApi ?? undefined,
|
|
377
|
+
row: contextMenuRow,
|
|
378
|
+
rowId: contextMenuRowId,
|
|
379
|
+
selectedIds: selectedIdsInternal,
|
|
380
|
+
data,
|
|
381
|
+
event: contextMenuEvent
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
const contextMenuItems = $derived.by(() => {
|
|
385
|
+
if (!contextMenuOpen) return [];
|
|
386
|
+
if (!contextMenuProps) return [];
|
|
387
|
+
const ctx = buildContextMenuCtx();
|
|
388
|
+
if (!ctx) return [];
|
|
389
|
+
const resolved = typeof contextMenuProps.items === "function" ? contextMenuProps.items(ctx) : contextMenuProps.items ?? [];
|
|
390
|
+
return (resolved ?? []).map((it) => ({
|
|
391
|
+
...it,
|
|
392
|
+
kind: it.kind ?? "item"
|
|
393
|
+
}));
|
|
394
|
+
});
|
|
395
|
+
function closeContextMenu() {
|
|
396
|
+
contextMenuOpen = false;
|
|
397
|
+
contextMenuEvent = null;
|
|
398
|
+
contextMenuRowId = null;
|
|
399
|
+
contextMenuRow = null;
|
|
400
|
+
}
|
|
401
|
+
function handleContextMenuItem(item) {
|
|
402
|
+
if (item.id === "__close__") {
|
|
403
|
+
closeContextMenu();
|
|
404
|
+
return;
|
|
383
405
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
406
|
+
if (!("label" in item)) {
|
|
407
|
+
closeContextMenu();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const ctx = buildContextMenuCtx();
|
|
411
|
+
closeContextMenu();
|
|
412
|
+
if (!ctx) return;
|
|
413
|
+
item.onselect?.(ctx);
|
|
414
|
+
contextMenuProps?.onselectitem?.(item, ctx);
|
|
415
|
+
}
|
|
416
|
+
$effect(() => {
|
|
417
|
+
if (!containerRef) return;
|
|
418
|
+
const node = containerRef;
|
|
419
|
+
function handleClick(ev) {
|
|
420
|
+
const resolved = resolveRowFromTarget(ev.target);
|
|
421
|
+
if (!resolved) return;
|
|
422
|
+
onrowclick?.({
|
|
423
|
+
...resolved,
|
|
424
|
+
event: ev
|
|
425
|
+
});
|
|
387
426
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (
|
|
391
|
-
closeFilterMenu();
|
|
427
|
+
function handleContextMenu(ev) {
|
|
428
|
+
// Only handle right-clicks on the grid surface itself, not on Svark's toolbar/pager.
|
|
429
|
+
if (!containerRef || !(ev.target instanceof Element) || !ev.target.closest(".wx-grid") || !containerRef.contains(ev.target.closest(".wx-grid"))) {
|
|
392
430
|
return;
|
|
393
431
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
432
|
+
const resolved = resolveRowFromTarget(ev.target);
|
|
433
|
+
if (resolved) {
|
|
434
|
+
onrowcontextmenu?.({
|
|
435
|
+
...resolved,
|
|
436
|
+
event: ev
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
if (!contextMenuProps) return;
|
|
440
|
+
// Always prevent the browser context menu when a custom menu is configured.
|
|
441
|
+
ev.preventDefault();
|
|
442
|
+
ev.stopPropagation();
|
|
443
|
+
// Right-clicking a row should behave like "select + open menu".
|
|
444
|
+
if (resolved?.id != null && gridApi && select !== false) {
|
|
445
|
+
gridApi.exec("select-row", {
|
|
446
|
+
id: resolved.id,
|
|
447
|
+
toggle: false,
|
|
448
|
+
range: false
|
|
449
|
+
});
|
|
450
|
+
selectedIdsInternal = [resolved.id];
|
|
451
|
+
}
|
|
452
|
+
contextMenuEvent = ev;
|
|
453
|
+
contextMenuX = ev.clientX;
|
|
454
|
+
contextMenuY = ev.clientY;
|
|
455
|
+
// Prefer the right-clicked row; otherwise fall back to current selection.
|
|
456
|
+
const ctxRowId = resolved?.id ?? selectedIdsInternal[0] ?? null;
|
|
457
|
+
const ctxRow = resolved?.row ?? (ctxRowId != null ? data.find((item) => String(item?.id) === String(ctxRowId)) ?? null : selectedRow) ?? null;
|
|
458
|
+
contextMenuRowId = ctxRowId;
|
|
459
|
+
contextMenuRow = ctxRow;
|
|
460
|
+
contextMenuOpen = true;
|
|
461
|
+
}
|
|
462
|
+
function handleDoubleClick(ev) {
|
|
463
|
+
const resolved = resolveRowFromTarget(ev.target);
|
|
464
|
+
if (!resolved) return;
|
|
465
|
+
onrowdblclick?.({
|
|
466
|
+
...resolved,
|
|
467
|
+
event: ev
|
|
422
468
|
});
|
|
423
469
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
470
|
+
node.addEventListener("click", handleClick);
|
|
471
|
+
node.addEventListener("dblclick", handleDoubleClick);
|
|
472
|
+
node.addEventListener("contextmenu", handleContextMenu);
|
|
473
|
+
return () => {
|
|
474
|
+
node.removeEventListener("click", handleClick);
|
|
475
|
+
node.removeEventListener("dblclick", handleDoubleClick);
|
|
476
|
+
node.removeEventListener("contextmenu", handleContextMenu);
|
|
477
|
+
};
|
|
478
|
+
});
|
|
479
|
+
const adapter = $derived(resolvedAdapterType === "RestHeaderSpec" ? new SvarkRestHeaderSpecAdapter({
|
|
480
|
+
url: resolvedUrl,
|
|
481
|
+
token,
|
|
482
|
+
schema,
|
|
483
|
+
entity
|
|
484
|
+
}) : new SvarkResolveSpecAdapter({
|
|
485
|
+
url: resolvedUrl,
|
|
486
|
+
token,
|
|
487
|
+
schema,
|
|
488
|
+
entity
|
|
489
|
+
}));
|
|
490
|
+
// For virtual scroll: set dynamic rowCount once total is known
|
|
491
|
+
const gridDynamic = $derived(virtualScroll && total > 0 ? { rowCount: total } : undefined);
|
|
492
|
+
// ── Main data effect (initial load + sort/filter/page/reload triggers) ──────
|
|
493
|
+
$effect(() => {
|
|
494
|
+
const _adapter = adapter;
|
|
495
|
+
const _limit = virtualScroll ? 1 : pageSize;
|
|
496
|
+
const _sort = serverSide ? debouncedSort : [];
|
|
497
|
+
const _filters = serverSide ? debouncedFilter : {};
|
|
498
|
+
// Paging offset: only applied when server-side paging is active
|
|
499
|
+
const _offset = showPager ? page * pageSize : 0;
|
|
500
|
+
void reload;
|
|
501
|
+
let cancelled = false;
|
|
502
|
+
loading = true;
|
|
503
|
+
error = null;
|
|
504
|
+
_adapter.read(_sort, _filters, _limit, _offset, query).then((response) => {
|
|
505
|
+
if (cancelled) return;
|
|
506
|
+
if (response.success) {
|
|
507
|
+
const incoming = response.data ?? [];
|
|
508
|
+
total = response.metadata?.total ?? incoming.length;
|
|
509
|
+
if (virtualScroll) {
|
|
510
|
+
data = [];
|
|
511
|
+
} else {
|
|
512
|
+
data = incoming;
|
|
465
513
|
}
|
|
514
|
+
} else {
|
|
515
|
+
const msg = response.error?.message ?? "Failed to load data";
|
|
516
|
+
error = msg;
|
|
517
|
+
onerror?.(msg);
|
|
466
518
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
519
|
+
}).catch((e) => {
|
|
520
|
+
if (cancelled) return;
|
|
521
|
+
const msg = e instanceof Error ? e.message : "Unknown error";
|
|
522
|
+
error = msg;
|
|
523
|
+
onerror?.(msg);
|
|
524
|
+
}).finally(() => {
|
|
525
|
+
if (!cancelled) loading = false;
|
|
470
526
|
});
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
527
|
+
return () => {
|
|
528
|
+
cancelled = true;
|
|
529
|
+
};
|
|
530
|
+
});
|
|
531
|
+
// ── Infinite scroll: append pages when the user nears the bottom ───────────
|
|
532
|
+
function maybeLoadNextPage() {
|
|
533
|
+
if (!infiniteScroll || !containerRef || loading || infiniteLoading || data.length >= total) return;
|
|
534
|
+
const scrollEl = scrollContainerRef;
|
|
535
|
+
if (scrollEl && scrollEl.scrollHeight > scrollEl.clientHeight + 1) {
|
|
536
|
+
const { scrollTop, scrollHeight, clientHeight } = scrollEl;
|
|
537
|
+
if (scrollHeight - scrollTop - clientHeight < 150) {
|
|
538
|
+
loadNextPage();
|
|
539
|
+
}
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const { bottom } = containerRef.getBoundingClientRect();
|
|
543
|
+
if (bottom - window.innerHeight < 150) {
|
|
544
|
+
loadNextPage();
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
$effect(() => {
|
|
548
|
+
if (!infiniteScroll || loading || infiniteLoading) return;
|
|
549
|
+
void data.length;
|
|
550
|
+
void total;
|
|
551
|
+
queueMicrotask(() => {
|
|
552
|
+
maybeLoadNextPage();
|
|
477
553
|
});
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
554
|
+
});
|
|
555
|
+
$effect(() => {
|
|
556
|
+
if (!infiniteScroll || !containerRef) return;
|
|
557
|
+
let scrollEl = null;
|
|
558
|
+
function attach(el) {
|
|
559
|
+
scrollEl = el;
|
|
560
|
+
scrollContainerRef = el;
|
|
561
|
+
el.addEventListener("scroll", onScroll);
|
|
562
|
+
queueMicrotask(onViewportChange);
|
|
563
|
+
}
|
|
564
|
+
function onScroll() {
|
|
565
|
+
maybeLoadNextPage();
|
|
566
|
+
}
|
|
567
|
+
function onViewportChange() {
|
|
568
|
+
maybeLoadNextPage();
|
|
569
|
+
}
|
|
570
|
+
window.addEventListener("scroll", onViewportChange, true);
|
|
571
|
+
window.addEventListener("resize", onViewportChange);
|
|
572
|
+
// SVAR renders .wx-scroll asynchronously; watch for it with MutationObserver
|
|
573
|
+
const candidate = containerRef.querySelector(".wx-scroll");
|
|
574
|
+
if (candidate) {
|
|
575
|
+
attach(candidate);
|
|
576
|
+
} else {
|
|
577
|
+
const obs = new MutationObserver(() => {
|
|
578
|
+
const el = containerRef?.querySelector(".wx-scroll");
|
|
579
|
+
if (el) {
|
|
580
|
+
attach(el);
|
|
581
|
+
obs.disconnect();
|
|
582
|
+
}
|
|
583
|
+
});
|
|
584
|
+
obs.observe(containerRef, {
|
|
585
|
+
childList: true,
|
|
586
|
+
subtree: true
|
|
587
|
+
});
|
|
588
|
+
return () => {
|
|
589
|
+
obs.disconnect();
|
|
590
|
+
scrollEl?.removeEventListener("scroll", onScroll);
|
|
591
|
+
window.removeEventListener("scroll", onViewportChange, true);
|
|
592
|
+
window.removeEventListener("resize", onViewportChange);
|
|
593
|
+
scrollContainerRef = null;
|
|
510
594
|
};
|
|
511
595
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
function handleContextMenuItem(item: SvarkContextMenuItem | { id: '__close__' }) {
|
|
536
|
-
if (item.id === '__close__') {
|
|
537
|
-
closeContextMenu();
|
|
538
|
-
return;
|
|
539
|
-
}
|
|
540
|
-
if (!('label' in item)) {
|
|
541
|
-
closeContextMenu();
|
|
542
|
-
return;
|
|
543
|
-
}
|
|
544
|
-
const ctx = buildContextMenuCtx();
|
|
545
|
-
closeContextMenu();
|
|
546
|
-
if (!ctx) return;
|
|
547
|
-
item.onselect?.(ctx);
|
|
548
|
-
contextMenuProps?.onselectitem?.(item, ctx);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
$effect(() => {
|
|
552
|
-
if (!containerRef) return;
|
|
553
|
-
const node = containerRef;
|
|
554
|
-
|
|
555
|
-
function handleClick(ev: MouseEvent) {
|
|
556
|
-
const resolved = resolveRowFromTarget(ev.target);
|
|
557
|
-
if (!resolved) return;
|
|
558
|
-
onrowclick?.({ ...resolved, event: ev });
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
function handleContextMenu(ev: MouseEvent) {
|
|
562
|
-
// Only handle right-clicks on the grid surface itself, not on Svark's toolbar/pager.
|
|
563
|
-
if (
|
|
564
|
-
!containerRef ||
|
|
565
|
-
!(ev.target instanceof Element) ||
|
|
566
|
-
!ev.target.closest('.wx-grid') ||
|
|
567
|
-
!containerRef.contains(ev.target.closest('.wx-grid'))
|
|
568
|
-
) {
|
|
596
|
+
return () => {
|
|
597
|
+
scrollEl?.removeEventListener("scroll", onScroll);
|
|
598
|
+
window.removeEventListener("scroll", onViewportChange, true);
|
|
599
|
+
window.removeEventListener("resize", onViewportChange);
|
|
600
|
+
scrollContainerRef = null;
|
|
601
|
+
};
|
|
602
|
+
});
|
|
603
|
+
async function loadNextPage() {
|
|
604
|
+
// Block if main fetch is running (sort/filter change in progress)
|
|
605
|
+
if (loading || infiniteLoading || data.length >= total) return;
|
|
606
|
+
// Snapshot debounced sort+filter so we can discard the response if they
|
|
607
|
+
// change while the request is in-flight (race condition guard).
|
|
608
|
+
const snapshotSort = serverSide ? JSON.stringify(debouncedSort) : "";
|
|
609
|
+
const snapshotFilter = serverSide ? JSON.stringify(debouncedFilter) : "";
|
|
610
|
+
const snapshotOffset = data.length;
|
|
611
|
+
infiniteLoading = true;
|
|
612
|
+
try {
|
|
613
|
+
const response = await adapter.read(serverSide ? debouncedSort : [], serverSide ? debouncedFilter : {}, pageSize, snapshotOffset, query);
|
|
614
|
+
// Discard if sort/filter changed while we were fetching
|
|
615
|
+
if (serverSide) {
|
|
616
|
+
if (JSON.stringify(debouncedSort) !== snapshotSort || JSON.stringify(debouncedFilter) !== snapshotFilter) {
|
|
569
617
|
return;
|
|
570
618
|
}
|
|
571
|
-
|
|
572
|
-
const resolved = resolveRowFromTarget(ev.target);
|
|
573
|
-
if (resolved) {
|
|
574
|
-
onrowcontextmenu?.({ ...resolved, event: ev });
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
if (!contextMenuProps) return;
|
|
578
|
-
// Always prevent the browser context menu when a custom menu is configured.
|
|
579
|
-
ev.preventDefault();
|
|
580
|
-
ev.stopPropagation();
|
|
581
|
-
|
|
582
|
-
// Right-clicking a row should behave like "select + open menu".
|
|
583
|
-
if (resolved?.id != null && gridApi && select !== false) {
|
|
584
|
-
gridApi.exec('select-row', { id: resolved.id, toggle: false, range: false });
|
|
585
|
-
selectedIdsInternal = [resolved.id];
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
contextMenuEvent = ev;
|
|
589
|
-
contextMenuX = ev.clientX;
|
|
590
|
-
contextMenuY = ev.clientY;
|
|
591
|
-
|
|
592
|
-
// Prefer the right-clicked row; otherwise fall back to current selection.
|
|
593
|
-
const ctxRowId = (resolved?.id ?? selectedIdsInternal[0] ?? null) as string | number | null;
|
|
594
|
-
const ctxRow =
|
|
595
|
-
resolved?.row ??
|
|
596
|
-
(ctxRowId != null
|
|
597
|
-
? data.find((item) => String(item?.id) === String(ctxRowId)) ?? null
|
|
598
|
-
: selectedRow) ??
|
|
599
|
-
null;
|
|
600
|
-
|
|
601
|
-
contextMenuRowId = ctxRowId;
|
|
602
|
-
contextMenuRow = ctxRow;
|
|
603
|
-
contextMenuOpen = true;
|
|
604
619
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const resolved = resolveRowFromTarget(ev.target);
|
|
608
|
-
if (!resolved) return;
|
|
609
|
-
onrowdblclick?.({ ...resolved, event: ev });
|
|
620
|
+
if (response.success) {
|
|
621
|
+
data = [...data, ...response.data ?? []];
|
|
610
622
|
}
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
623
|
+
} catch {} finally {
|
|
624
|
+
infiniteLoading = false;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
// ── SVAR Grid init ─────────────────────────────────────────────────────────
|
|
628
|
+
function handleInit(api) {
|
|
629
|
+
gridApi = api;
|
|
630
|
+
// Row selection callback
|
|
631
|
+
selectedIdsInternal = api.getState().selectedRows ?? [];
|
|
632
|
+
api.on("select-row", () => {
|
|
633
|
+
const ids = api.getState().selectedRows ?? [];
|
|
634
|
+
selectedIdsInternal = ids;
|
|
635
|
+
onselect?.(ids);
|
|
621
636
|
});
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
);
|
|
633
|
-
|
|
634
|
-
// ── Main data effect (initial load + sort/filter/page/reload triggers) ──────
|
|
635
|
-
|
|
636
|
-
$effect(() => {
|
|
637
|
-
const _adapter = adapter;
|
|
638
|
-
const _limit = virtualScroll ? 1 : pageSize;
|
|
639
|
-
const _sort = serverSide ? debouncedSort : [];
|
|
640
|
-
const _filters = serverSide ? debouncedFilter : {};
|
|
641
|
-
// Paging offset: only applied when server-side paging is active
|
|
642
|
-
const _offset = showPager ? page * pageSize : 0;
|
|
643
|
-
void reload;
|
|
644
|
-
let cancelled = false;
|
|
645
|
-
|
|
646
|
-
loading = true;
|
|
647
|
-
error = null;
|
|
648
|
-
|
|
649
|
-
_adapter
|
|
650
|
-
.read(_sort, _filters, _limit, _offset, query)
|
|
651
|
-
.then((response) => {
|
|
652
|
-
if (cancelled) return;
|
|
637
|
+
// Virtual scroll: SVAR fires request-data when the visible row window changes.
|
|
638
|
+
// We fetch only that window from the API and replace data.
|
|
639
|
+
if (virtualScroll) {
|
|
640
|
+
api.on("request-data", async (ev) => {
|
|
641
|
+
const { start, end } = ev.row;
|
|
642
|
+
const limit = end - start;
|
|
643
|
+
if (limit <= 0) return;
|
|
644
|
+
loading = true;
|
|
645
|
+
try {
|
|
646
|
+
const response = await adapter.read(serverSide ? debouncedSort : [], serverSide ? debouncedFilter : {}, limit, start, query);
|
|
653
647
|
if (response.success) {
|
|
654
|
-
|
|
655
|
-
total = (response as any).metadata?.total ?? incoming.length;
|
|
656
|
-
|
|
657
|
-
if (virtualScroll) {
|
|
658
|
-
data = [];
|
|
659
|
-
} else {
|
|
660
|
-
data = incoming;
|
|
661
|
-
}
|
|
662
|
-
} else {
|
|
663
|
-
const msg = (response as any).error?.message ?? 'Failed to load data';
|
|
664
|
-
error = msg;
|
|
665
|
-
onerror?.(msg);
|
|
648
|
+
data = response.data ?? [];
|
|
666
649
|
}
|
|
667
|
-
})
|
|
668
|
-
|
|
669
|
-
if (cancelled) return;
|
|
670
|
-
const msg = e instanceof Error ? e.message : 'Unknown error';
|
|
650
|
+
} catch (e) {
|
|
651
|
+
const msg = e instanceof Error ? e.message : "Failed to load data";
|
|
671
652
|
error = msg;
|
|
672
653
|
onerror?.(msg);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
if (!cancelled) loading = false;
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
return () => {
|
|
679
|
-
cancelled = true;
|
|
680
|
-
};
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
// ── Infinite scroll: append pages when the user nears the bottom ───────────
|
|
684
|
-
|
|
685
|
-
function maybeLoadNextPage() {
|
|
686
|
-
if (!infiniteScroll || !containerRef || loading || infiniteLoading || data.length >= total) return;
|
|
687
|
-
|
|
688
|
-
const scrollEl = scrollContainerRef;
|
|
689
|
-
if (scrollEl && scrollEl.scrollHeight > scrollEl.clientHeight + 1) {
|
|
690
|
-
const { scrollTop, scrollHeight, clientHeight } = scrollEl;
|
|
691
|
-
if (scrollHeight - scrollTop - clientHeight < 150) {
|
|
692
|
-
loadNextPage();
|
|
654
|
+
} finally {
|
|
655
|
+
loading = false;
|
|
693
656
|
}
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
const { bottom } = containerRef.getBoundingClientRect();
|
|
698
|
-
if (bottom - window.innerHeight < 150) {
|
|
699
|
-
loadNextPage();
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
$effect(() => {
|
|
704
|
-
if (!infiniteScroll || loading || infiniteLoading) return;
|
|
705
|
-
void data.length;
|
|
706
|
-
void total;
|
|
707
|
-
|
|
708
|
-
queueMicrotask(() => {
|
|
709
|
-
maybeLoadNextPage();
|
|
710
657
|
});
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
$effect(() => {
|
|
714
|
-
if (!infiniteScroll || !containerRef) return;
|
|
715
|
-
|
|
716
|
-
let scrollEl: Element | null = null;
|
|
717
|
-
|
|
718
|
-
function attach(el: Element) {
|
|
719
|
-
scrollEl = el;
|
|
720
|
-
scrollContainerRef = el as HTMLElement;
|
|
721
|
-
el.addEventListener('scroll', onScroll);
|
|
722
|
-
queueMicrotask(onViewportChange);
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
function onScroll() {
|
|
726
|
-
maybeLoadNextPage();
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
function onViewportChange() {
|
|
730
|
-
maybeLoadNextPage();
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
window.addEventListener('scroll', onViewportChange, true);
|
|
734
|
-
window.addEventListener('resize', onViewportChange);
|
|
735
|
-
|
|
736
|
-
// SVAR renders .wx-scroll asynchronously; watch for it with MutationObserver
|
|
737
|
-
const candidate = containerRef.querySelector('.wx-scroll');
|
|
738
|
-
if (candidate) {
|
|
739
|
-
attach(candidate);
|
|
740
|
-
} else {
|
|
741
|
-
const obs = new MutationObserver(() => {
|
|
742
|
-
const el = containerRef?.querySelector('.wx-scroll');
|
|
743
|
-
if (el) {
|
|
744
|
-
attach(el);
|
|
745
|
-
obs.disconnect();
|
|
746
|
-
}
|
|
747
|
-
});
|
|
748
|
-
obs.observe(containerRef, { childList: true, subtree: true });
|
|
749
|
-
return () => {
|
|
750
|
-
obs.disconnect();
|
|
751
|
-
scrollEl?.removeEventListener('scroll', onScroll);
|
|
752
|
-
window.removeEventListener('scroll', onViewportChange, true);
|
|
753
|
-
window.removeEventListener('resize', onViewportChange);
|
|
754
|
-
scrollContainerRef = null;
|
|
755
|
-
};
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
return () => {
|
|
759
|
-
scrollEl?.removeEventListener('scroll', onScroll);
|
|
760
|
-
window.removeEventListener('scroll', onViewportChange, true);
|
|
761
|
-
window.removeEventListener('resize', onViewportChange);
|
|
762
|
-
scrollContainerRef = null;
|
|
763
|
-
};
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
async function loadNextPage() {
|
|
767
|
-
// Block if main fetch is running (sort/filter change in progress)
|
|
768
|
-
if (loading || infiniteLoading || data.length >= total) return;
|
|
769
|
-
|
|
770
|
-
// Snapshot debounced sort+filter so we can discard the response if they
|
|
771
|
-
// change while the request is in-flight (race condition guard).
|
|
772
|
-
const snapshotSort = serverSide ? JSON.stringify(debouncedSort) : '';
|
|
773
|
-
const snapshotFilter = serverSide ? JSON.stringify(debouncedFilter) : '';
|
|
774
|
-
const snapshotOffset = data.length;
|
|
775
|
-
|
|
776
|
-
infiniteLoading = true;
|
|
777
|
-
try {
|
|
778
|
-
const response = await adapter.read(
|
|
779
|
-
serverSide ? debouncedSort : [],
|
|
780
|
-
serverSide ? debouncedFilter : {},
|
|
781
|
-
pageSize,
|
|
782
|
-
snapshotOffset,
|
|
783
|
-
query
|
|
784
|
-
);
|
|
785
|
-
// Discard if sort/filter changed while we were fetching
|
|
786
|
-
if (serverSide) {
|
|
787
|
-
if (
|
|
788
|
-
JSON.stringify(debouncedSort) !== snapshotSort ||
|
|
789
|
-
JSON.stringify(debouncedFilter) !== snapshotFilter
|
|
790
|
-
) {
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
if (response.success) {
|
|
795
|
-
data = [...data, ...(response.data ?? [])];
|
|
796
|
-
}
|
|
797
|
-
} catch {
|
|
798
|
-
// Silent — don't replace existing rows with an error on pagination
|
|
799
|
-
} finally {
|
|
800
|
-
infiniteLoading = false;
|
|
801
|
-
}
|
|
802
658
|
}
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
selectedIdsInternal = ids;
|
|
814
|
-
onselect?.(ids);
|
|
815
|
-
});
|
|
816
|
-
|
|
817
|
-
// Virtual scroll: SVAR fires request-data when the visible row window changes.
|
|
818
|
-
// We fetch only that window from the API and replace data.
|
|
819
|
-
if (virtualScroll) {
|
|
820
|
-
api.on('request-data', async (ev: any) => {
|
|
821
|
-
const { start, end } = ev.row as { start: number; end: number };
|
|
822
|
-
const limit = end - start;
|
|
823
|
-
if (limit <= 0) return;
|
|
824
|
-
|
|
825
|
-
loading = true;
|
|
826
|
-
try {
|
|
827
|
-
const response = await adapter.read(
|
|
828
|
-
serverSide ? debouncedSort : [],
|
|
829
|
-
serverSide ? debouncedFilter : {},
|
|
830
|
-
limit,
|
|
831
|
-
start,
|
|
832
|
-
query
|
|
833
|
-
);
|
|
834
|
-
if (response.success) {
|
|
835
|
-
data = response.data ?? [];
|
|
836
|
-
}
|
|
837
|
-
} catch (e) {
|
|
838
|
-
const msg = e instanceof Error ? e.message : 'Failed to load data';
|
|
839
|
-
error = msg;
|
|
840
|
-
onerror?.(msg);
|
|
841
|
-
} finally {
|
|
842
|
-
loading = false;
|
|
843
|
-
}
|
|
844
|
-
});
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
if (serverSide) {
|
|
848
|
-
api.intercept('sort-rows', (ev: any) => {
|
|
849
|
-
if (ev.key) {
|
|
850
|
-
if (ev.add) {
|
|
851
|
-
const idx = sortState.findIndex((s) => s.key === ev.key);
|
|
852
|
-
if (idx >= 0) {
|
|
853
|
-
sortState[idx] = { key: ev.key, order: ev.order ?? 'asc' };
|
|
854
|
-
} else {
|
|
855
|
-
sortState = [...sortState, { key: ev.key, order: ev.order ?? 'asc' }];
|
|
856
|
-
}
|
|
659
|
+
if (serverSide) {
|
|
660
|
+
api.intercept("sort-rows", (ev) => {
|
|
661
|
+
if (ev.key) {
|
|
662
|
+
if (ev.add) {
|
|
663
|
+
const idx = sortState.findIndex((s) => s.key === ev.key);
|
|
664
|
+
if (idx >= 0) {
|
|
665
|
+
sortState[idx] = {
|
|
666
|
+
key: ev.key,
|
|
667
|
+
order: ev.order ?? "asc"
|
|
668
|
+
};
|
|
857
669
|
} else {
|
|
858
|
-
sortState = [
|
|
670
|
+
sortState = [...sortState, {
|
|
671
|
+
key: ev.key,
|
|
672
|
+
order: ev.order ?? "asc"
|
|
673
|
+
}];
|
|
859
674
|
}
|
|
860
675
|
} else {
|
|
861
|
-
sortState = [
|
|
676
|
+
sortState = [{
|
|
677
|
+
key: ev.key,
|
|
678
|
+
order: ev.order ?? "asc"
|
|
679
|
+
}];
|
|
862
680
|
}
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
api.intercept('filter-rows', (ev: any) => {
|
|
867
|
-
filterState = reduceServerFilterState(filterState, ev ?? {});
|
|
868
|
-
return false;
|
|
869
|
-
});
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
api.on('add-row', async (ev: any) => {
|
|
873
|
-
try {
|
|
874
|
-
await adapter.create(ev.row);
|
|
875
|
-
onchange?.({ action: 'add', row: ev.row });
|
|
876
|
-
reload++;
|
|
877
|
-
} catch (e) {
|
|
878
|
-
onerror?.(e instanceof Error ? e.message : 'Create failed');
|
|
879
|
-
}
|
|
880
|
-
});
|
|
881
|
-
|
|
882
|
-
api.on('delete-row', async (ev: any) => {
|
|
883
|
-
try {
|
|
884
|
-
await adapter.delete(ev.id);
|
|
885
|
-
onchange?.({ action: 'delete', id: ev.id });
|
|
886
|
-
reload++;
|
|
887
|
-
} catch (e) {
|
|
888
|
-
onerror?.(e instanceof Error ? e.message : 'Delete failed');
|
|
681
|
+
} else {
|
|
682
|
+
sortState = [];
|
|
889
683
|
}
|
|
684
|
+
return false;
|
|
890
685
|
});
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
await adapter.update(ev.id, { [String(ev.column)]: ev.value });
|
|
895
|
-
onchange?.({ action: 'update', id: ev.id, column: ev.column, value: ev.value });
|
|
896
|
-
} catch (e) {
|
|
897
|
-
onerror?.(e instanceof Error ? e.message : 'Update failed');
|
|
898
|
-
}
|
|
686
|
+
api.intercept("filter-rows", (ev) => {
|
|
687
|
+
filterState = reduceServerFilterState(filterState, ev ?? {});
|
|
688
|
+
return false;
|
|
899
689
|
});
|
|
900
690
|
}
|
|
691
|
+
api.on("add-row", async (ev) => {
|
|
692
|
+
try {
|
|
693
|
+
await adapter.create(ev.row);
|
|
694
|
+
onchange?.({
|
|
695
|
+
action: "add",
|
|
696
|
+
row: ev.row
|
|
697
|
+
});
|
|
698
|
+
reload++;
|
|
699
|
+
} catch (e) {
|
|
700
|
+
onerror?.(e instanceof Error ? e.message : "Create failed");
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
api.on("delete-row", async (ev) => {
|
|
704
|
+
try {
|
|
705
|
+
await adapter.delete(ev.id);
|
|
706
|
+
onchange?.({
|
|
707
|
+
action: "delete",
|
|
708
|
+
id: ev.id
|
|
709
|
+
});
|
|
710
|
+
reload++;
|
|
711
|
+
} catch (e) {
|
|
712
|
+
onerror?.(e instanceof Error ? e.message : "Delete failed");
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
api.on("update-cell", async (ev) => {
|
|
716
|
+
try {
|
|
717
|
+
await adapter.update(ev.id, { [String(ev.column)]: ev.value });
|
|
718
|
+
onchange?.({
|
|
719
|
+
action: "update",
|
|
720
|
+
id: ev.id,
|
|
721
|
+
column: ev.column,
|
|
722
|
+
value: ev.value
|
|
723
|
+
});
|
|
724
|
+
} catch (e) {
|
|
725
|
+
onerror?.(e instanceof Error ? e.message : "Update failed");
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
}
|
|
901
729
|
</script>
|
|
902
730
|
|
|
903
731
|
<div class="svark-container" class:svark-dark={isDark} style:height={heightStyle} bind:this={containerRef}>
|