@warkypublic/svelix 0.1.29 → 0.1.32
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/dist/components/Boxer/Boxer.svelte +16 -1
- package/dist/components/GlobalStateStore/GlobalStateStore.d.ts +2 -1
- package/dist/components/GlobalStateStore/GlobalStateStore.js +150 -29
- package/dist/components/GlobalStateStore/GlobalStateStore.types.d.ts +1 -0
- package/dist/components/GlobalStateStore/GlobalStateStore.utils.js +2 -1
- package/dist/components/Gridler/components/Gridler.svelte +51 -7
- package/dist/components/Gridler/components/Gridler.svelte.d.ts +4 -0
- package/dist/components/Gridler/components/GridlerCanvas.svelte +104 -6
- package/dist/components/Gridler/components/GridlerCanvas.svelte.d.ts +9 -0
- package/dist/components/Gridler/components/GridlerFull.svelte +60 -3
- package/dist/components/Gridler/components/GridlerFull.svelte.d.ts +5 -1
- package/dist/components/Gridler/types.d.ts +4 -0
- package/llm/README.md +1 -0
- package/llm/gridler-events.md +139 -0
- package/package.json +29 -29
|
@@ -97,6 +97,7 @@
|
|
|
97
97
|
let dropdownZ = $state(1100);
|
|
98
98
|
let pointerInteractingWithDropdown = $state(false);
|
|
99
99
|
let insideDialog = $state(false);
|
|
100
|
+
let suppressOpenOnFocus = $state(false);
|
|
100
101
|
const effectiveDisablePortal = $derived(disablePortal || insideDialog);
|
|
101
102
|
// Plain variable — NOT $state to avoid deep proxy on the complex virtualizer object.
|
|
102
103
|
let rawVirtualizer: SvelteVirtualizer<
|
|
@@ -303,6 +304,12 @@
|
|
|
303
304
|
activeOptionIndex = null;
|
|
304
305
|
}
|
|
305
306
|
break;
|
|
307
|
+
case "Tab":
|
|
308
|
+
if (multiSelect && $store.opened && $store.boxerData.length > 0) {
|
|
309
|
+
e.preventDefault();
|
|
310
|
+
onOptionSubmit(0);
|
|
311
|
+
}
|
|
312
|
+
break;
|
|
306
313
|
}
|
|
307
314
|
}
|
|
308
315
|
|
|
@@ -331,6 +338,7 @@
|
|
|
331
338
|
e.preventDefault();
|
|
332
339
|
store.setOpened(false);
|
|
333
340
|
activeOptionIndex = null;
|
|
341
|
+
suppressOpenOnFocus = true;
|
|
334
342
|
targetRef?.focus();
|
|
335
343
|
break;
|
|
336
344
|
}
|
|
@@ -387,6 +395,7 @@
|
|
|
387
395
|
activeOptionIndex = null;
|
|
388
396
|
}
|
|
389
397
|
export function focus() {
|
|
398
|
+
suppressOpenOnFocus = false;
|
|
390
399
|
targetRef?.focus();
|
|
391
400
|
}
|
|
392
401
|
export function getValue() {
|
|
@@ -504,7 +513,13 @@
|
|
|
504
513
|
isFetching={$store.isFetching}
|
|
505
514
|
search={$store.input}
|
|
506
515
|
onclear={onClear}
|
|
507
|
-
onfocus={() =>
|
|
516
|
+
onfocus={() => {
|
|
517
|
+
if (suppressOpenOnFocus) {
|
|
518
|
+
suppressOpenOnFocus = false;
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
store.setOpened(true);
|
|
522
|
+
}}
|
|
508
523
|
onkeydown={onInputKeydown}
|
|
509
524
|
onsearch={(v) => {
|
|
510
525
|
store.setSearch(v);
|
|
@@ -6,6 +6,7 @@ type GlobalStateStoreApi = {
|
|
|
6
6
|
setState: (partial: PartialUpdater, replace?: boolean) => void;
|
|
7
7
|
subscribe: (run: (value: GlobalStateStoreType) => void) => () => void;
|
|
8
8
|
};
|
|
9
|
+
declare const createGlobalStateStore: () => GlobalStateStoreApi;
|
|
9
10
|
declare const GlobalStateStore: GlobalStateStoreApi;
|
|
10
11
|
declare const languageStore: Readable<SupportedLanguage>;
|
|
11
12
|
declare function useGlobalStateStore(): Readable<GlobalStateStoreType>;
|
|
@@ -18,4 +19,4 @@ declare const setAuthToken: (token: string) => void;
|
|
|
18
19
|
declare const GetGlobalState: () => GlobalStateStoreType;
|
|
19
20
|
declare const setLanguage: (lang: SupportedLanguage) => void;
|
|
20
21
|
export type { GlobalStateStoreApi };
|
|
21
|
-
export { getApiURL, getAuthToken, GetGlobalState, GlobalStateStore, isLoggedIn, languageStore, setApiURL, setAuthToken, setLanguage, useGlobalStateStore, };
|
|
22
|
+
export { createGlobalStateStore, getApiURL, getAuthToken, GetGlobalState, GlobalStateStore, isLoggedIn, languageStore, setApiURL, setAuthToken, setLanguage, useGlobalStateStore, };
|
|
@@ -30,6 +30,7 @@ const createInitialState = () => ({
|
|
|
30
30
|
connected: true,
|
|
31
31
|
loading: false,
|
|
32
32
|
loggedIn: false,
|
|
33
|
+
validated: true,
|
|
33
34
|
},
|
|
34
35
|
user: {
|
|
35
36
|
guid: "",
|
|
@@ -45,6 +46,62 @@ const toPersistedState = (state) => ({
|
|
|
45
46
|
session: state.session,
|
|
46
47
|
user: state.user,
|
|
47
48
|
});
|
|
49
|
+
const isSessionExpired = (session) => {
|
|
50
|
+
if (!session?.expiryDate) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
return new Date(session.expiryDate) < new Date();
|
|
54
|
+
};
|
|
55
|
+
const hasSessionCredentials = (session) => {
|
|
56
|
+
return Boolean(session?.authToken || session?.loggedIn);
|
|
57
|
+
};
|
|
58
|
+
const createClearedAuthenticatedState = (state, options) => {
|
|
59
|
+
const initialState = createInitialState();
|
|
60
|
+
return {
|
|
61
|
+
owner: initialState.owner,
|
|
62
|
+
program: initialState.program,
|
|
63
|
+
session: {
|
|
64
|
+
...initialState.session,
|
|
65
|
+
apiURL: state.session.apiURL,
|
|
66
|
+
connected: options?.connected ?? true,
|
|
67
|
+
error: options?.error,
|
|
68
|
+
loading: options?.loading ?? false,
|
|
69
|
+
validated: true,
|
|
70
|
+
},
|
|
71
|
+
user: {
|
|
72
|
+
...initialState.user,
|
|
73
|
+
language: state.user.language ?? initialState.user.language,
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
const getSessionValidationMismatch = ({ fetchedSession, fetchedUser, previousState, }) => {
|
|
78
|
+
const previousAuthToken = previousState.session.authToken ?? "";
|
|
79
|
+
const previousGuid = previousState.user.guid ?? "";
|
|
80
|
+
const previousUsername = previousState.user.username ?? "";
|
|
81
|
+
if (fetchedSession?.loggedIn === false) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
if (previousAuthToken &&
|
|
85
|
+
fetchedSession?.authToken &&
|
|
86
|
+
fetchedSession.authToken !== previousAuthToken) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
if (previousGuid && fetchedUser?.guid && fetchedUser.guid !== previousGuid) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (previousUsername &&
|
|
93
|
+
fetchedUser?.username &&
|
|
94
|
+
fetchedUser.username !== previousUsername) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
};
|
|
99
|
+
const hasSessionValidationSignal = (session, user) => {
|
|
100
|
+
return Boolean(session?.loggedIn === true ||
|
|
101
|
+
session?.authToken ||
|
|
102
|
+
user?.guid ||
|
|
103
|
+
user?.username);
|
|
104
|
+
};
|
|
48
105
|
const createGlobalStateStore = () => {
|
|
49
106
|
const initialState = createInitialState();
|
|
50
107
|
let isStorageInitialized = false;
|
|
@@ -96,6 +153,59 @@ const createGlobalStateStore = () => {
|
|
|
96
153
|
try {
|
|
97
154
|
const currentState = getState();
|
|
98
155
|
const result = await currentState.onFetchSession?.(currentState);
|
|
156
|
+
const hasExistingSession = hasSessionCredentials(currentState.session);
|
|
157
|
+
const nextUser = { ...currentState.user, ...result?.user };
|
|
158
|
+
const nextSession = {
|
|
159
|
+
...currentState.session,
|
|
160
|
+
...result?.session,
|
|
161
|
+
apiURL: url || currentState.session.apiURL,
|
|
162
|
+
connected: true,
|
|
163
|
+
loading: false,
|
|
164
|
+
};
|
|
165
|
+
const serverConfirmsSession = result?.session?.loggedIn === true || Boolean(result?.session?.authToken);
|
|
166
|
+
const serverConfirmsUser = Boolean(result?.user?.guid || result?.user?.username);
|
|
167
|
+
const alreadyValidated = currentState.session.validated === true;
|
|
168
|
+
const requiresValidation = (hasExistingSession && !alreadyValidated) || serverConfirmsSession;
|
|
169
|
+
const hasValidationSignal = serverConfirmsSession || serverConfirmsUser || alreadyValidated;
|
|
170
|
+
const hasValidationMismatch = getSessionValidationMismatch({
|
|
171
|
+
fetchedSession: result?.session,
|
|
172
|
+
fetchedUser: result?.user,
|
|
173
|
+
previousState: currentState,
|
|
174
|
+
});
|
|
175
|
+
const resolvedLoggedIn = result?.session?.loggedIn ??
|
|
176
|
+
(hasValidationSignal && Boolean(nextSession.authToken));
|
|
177
|
+
const resolvedSession = {
|
|
178
|
+
...nextSession,
|
|
179
|
+
loggedIn: resolvedLoggedIn,
|
|
180
|
+
validated: !requiresValidation || hasValidationSignal,
|
|
181
|
+
};
|
|
182
|
+
const isInvalidSession = hasValidationMismatch ||
|
|
183
|
+
isSessionExpired(resolvedSession) ||
|
|
184
|
+
(requiresValidation &&
|
|
185
|
+
(!hasValidationSignal ||
|
|
186
|
+
!resolvedSession.loggedIn ||
|
|
187
|
+
!resolvedSession.authToken));
|
|
188
|
+
if (isInvalidSession) {
|
|
189
|
+
const clearedState = createClearedAuthenticatedState(currentState);
|
|
190
|
+
setGlobalState((state) => ({
|
|
191
|
+
...state,
|
|
192
|
+
...result,
|
|
193
|
+
layout: { ...state.layout, ...result?.layout },
|
|
194
|
+
navigation: { ...state.navigation, ...result?.navigation },
|
|
195
|
+
owner: { ...clearedState.owner, ...result?.owner },
|
|
196
|
+
program: {
|
|
197
|
+
...clearedState.program,
|
|
198
|
+
...result?.program,
|
|
199
|
+
updatedAt: new Date().toISOString(),
|
|
200
|
+
},
|
|
201
|
+
session: {
|
|
202
|
+
...clearedState.session,
|
|
203
|
+
apiURL: url || state.session.apiURL,
|
|
204
|
+
},
|
|
205
|
+
user: clearedState.user,
|
|
206
|
+
}));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
99
209
|
setGlobalState((state) => ({
|
|
100
210
|
...state,
|
|
101
211
|
...result,
|
|
@@ -107,21 +217,17 @@ const createGlobalStateStore = () => {
|
|
|
107
217
|
...result?.program,
|
|
108
218
|
updatedAt: new Date().toISOString(),
|
|
109
219
|
},
|
|
110
|
-
session:
|
|
111
|
-
|
|
112
|
-
...result?.session,
|
|
113
|
-
connected: true,
|
|
114
|
-
loading: false,
|
|
115
|
-
},
|
|
116
|
-
user: { ...state.user, ...result?.user },
|
|
220
|
+
session: resolvedSession,
|
|
221
|
+
user: nextUser,
|
|
117
222
|
}));
|
|
118
223
|
}
|
|
119
224
|
catch (e) {
|
|
225
|
+
const error = `Load Exception: ${String(e)}`;
|
|
120
226
|
setGlobalState((state) => ({
|
|
121
227
|
session: {
|
|
122
228
|
...state.session,
|
|
123
229
|
connected: false,
|
|
124
|
-
error
|
|
230
|
+
error,
|
|
125
231
|
loading: false,
|
|
126
232
|
},
|
|
127
233
|
}));
|
|
@@ -134,10 +240,10 @@ const createGlobalStateStore = () => {
|
|
|
134
240
|
},
|
|
135
241
|
isLoggedIn: () => {
|
|
136
242
|
const session = getState().session;
|
|
137
|
-
if (!session.loggedIn || !session.authToken) {
|
|
243
|
+
if (!session.validated || !session.loggedIn || !session.authToken) {
|
|
138
244
|
return false;
|
|
139
245
|
}
|
|
140
|
-
if (
|
|
246
|
+
if (isSessionExpired(session)) {
|
|
141
247
|
return false;
|
|
142
248
|
}
|
|
143
249
|
return true;
|
|
@@ -152,7 +258,8 @@ const createGlobalStateStore = () => {
|
|
|
152
258
|
authToken: authToken ?? "",
|
|
153
259
|
expiryDate: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
|
|
154
260
|
loading: true,
|
|
155
|
-
loggedIn:
|
|
261
|
+
loggedIn: false,
|
|
262
|
+
validated: false,
|
|
156
263
|
},
|
|
157
264
|
user: {
|
|
158
265
|
...state.user,
|
|
@@ -162,6 +269,7 @@ const createGlobalStateStore = () => {
|
|
|
162
269
|
const currentState = getState();
|
|
163
270
|
const result = await currentState.onLogin?.(currentState);
|
|
164
271
|
if (result) {
|
|
272
|
+
const loginValidated = hasSessionValidationSignal(result.session, result.user);
|
|
165
273
|
setGlobalState((state) => ({
|
|
166
274
|
owner: result.owner
|
|
167
275
|
? { ...state.owner, ...result.owner }
|
|
@@ -169,9 +277,13 @@ const createGlobalStateStore = () => {
|
|
|
169
277
|
program: result.program
|
|
170
278
|
? { ...state.program, ...result.program }
|
|
171
279
|
: state.program,
|
|
172
|
-
session:
|
|
173
|
-
|
|
174
|
-
|
|
280
|
+
session: {
|
|
281
|
+
...state.session,
|
|
282
|
+
...result.session,
|
|
283
|
+
loggedIn: result.session?.loggedIn ??
|
|
284
|
+
(loginValidated && Boolean(result.session?.authToken ?? state.session.authToken)),
|
|
285
|
+
validated: loginValidated ? true : state.session.validated,
|
|
286
|
+
},
|
|
175
287
|
user: result.user
|
|
176
288
|
? { ...state.user, ...result.user }
|
|
177
289
|
: state.user,
|
|
@@ -180,14 +292,14 @@ const createGlobalStateStore = () => {
|
|
|
180
292
|
await fetchDataInternal();
|
|
181
293
|
}
|
|
182
294
|
catch (e) {
|
|
295
|
+
const currentState = getState();
|
|
296
|
+
const clearedState = createClearedAuthenticatedState(currentState, {
|
|
297
|
+
connected: false,
|
|
298
|
+
error: `Login Exception: ${String(e)}`,
|
|
299
|
+
});
|
|
183
300
|
setGlobalState((state) => ({
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
connected: false,
|
|
187
|
-
error: `Login Exception: ${String(e)}`,
|
|
188
|
-
loading: false,
|
|
189
|
-
loggedIn: false,
|
|
190
|
-
},
|
|
301
|
+
...state,
|
|
302
|
+
...clearedState,
|
|
191
303
|
}));
|
|
192
304
|
}
|
|
193
305
|
finally {
|
|
@@ -203,6 +315,7 @@ const createGlobalStateStore = () => {
|
|
|
203
315
|
logout: async () => {
|
|
204
316
|
await waitForInitialization();
|
|
205
317
|
return withOperationLock(async () => {
|
|
318
|
+
const previousState = getState();
|
|
206
319
|
try {
|
|
207
320
|
setGlobalState((state) => ({
|
|
208
321
|
...createInitialState(),
|
|
@@ -215,7 +328,7 @@ const createGlobalStateStore = () => {
|
|
|
215
328
|
},
|
|
216
329
|
}));
|
|
217
330
|
const currentState = getState();
|
|
218
|
-
const result = await currentState.onLogout?.(
|
|
331
|
+
const result = await currentState.onLogout?.(previousState);
|
|
219
332
|
if (result) {
|
|
220
333
|
setGlobalState((state) => ({
|
|
221
334
|
owner: result.owner
|
|
@@ -235,13 +348,14 @@ const createGlobalStateStore = () => {
|
|
|
235
348
|
await fetchDataInternal();
|
|
236
349
|
}
|
|
237
350
|
catch (e) {
|
|
351
|
+
const currentState = getState();
|
|
352
|
+
const clearedState = createClearedAuthenticatedState(currentState, {
|
|
353
|
+
connected: false,
|
|
354
|
+
error: `Logout Exception: ${String(e)}`,
|
|
355
|
+
});
|
|
238
356
|
setGlobalState((state) => ({
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
connected: false,
|
|
242
|
-
error: `Logout Exception: ${String(e)}`,
|
|
243
|
-
loading: false,
|
|
244
|
-
},
|
|
357
|
+
...state,
|
|
358
|
+
...clearedState,
|
|
245
359
|
}));
|
|
246
360
|
}
|
|
247
361
|
finally {
|
|
@@ -319,6 +433,7 @@ const createGlobalStateStore = () => {
|
|
|
319
433
|
store.set(initialStoreState);
|
|
320
434
|
initializationPromise = loadStorage()
|
|
321
435
|
.then((loadedState) => {
|
|
436
|
+
const hasPersistedSession = hasSessionCredentials(loadedState.session);
|
|
322
437
|
setGlobalState((current) => ({
|
|
323
438
|
...current,
|
|
324
439
|
...loadedState,
|
|
@@ -328,6 +443,12 @@ const createGlobalStateStore = () => {
|
|
|
328
443
|
...loadedState.session,
|
|
329
444
|
connected: true,
|
|
330
445
|
loading: false,
|
|
446
|
+
loggedIn: hasPersistedSession
|
|
447
|
+
? false
|
|
448
|
+
: loadedState.session?.loggedIn ?? current.session.loggedIn,
|
|
449
|
+
validated: hasPersistedSession
|
|
450
|
+
? false
|
|
451
|
+
: loadedState.session?.validated ?? current.session.validated,
|
|
331
452
|
},
|
|
332
453
|
}));
|
|
333
454
|
})
|
|
@@ -386,4 +507,4 @@ const GetGlobalState = () => {
|
|
|
386
507
|
const setLanguage = (lang) => {
|
|
387
508
|
GlobalStateStore.getState().setLanguage(lang);
|
|
388
509
|
};
|
|
389
|
-
export { getApiURL, getAuthToken, GetGlobalState, GlobalStateStore, isLoggedIn, languageStore, setApiURL, setAuthToken, setLanguage, useGlobalStateStore, };
|
|
510
|
+
export { createGlobalStateStore, getApiURL, getAuthToken, GetGlobalState, GlobalStateStore, isLoggedIn, languageStore, setApiURL, setAuthToken, setLanguage, useGlobalStateStore, };
|
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
item?: Record<string, unknown>;
|
|
29
29
|
column?: GridlerColumn;
|
|
30
30
|
}) => void;
|
|
31
|
+
onRowClick?: (row: number) => void;
|
|
32
|
+
onRowContextMenu?: (row: number) => void;
|
|
33
|
+
/** Resolve raw row data by row index for populating onCellEvent. */
|
|
34
|
+
getRowData?: (row: number) => Record<string, unknown> | undefined;
|
|
31
35
|
children?: Snippet;
|
|
32
36
|
}
|
|
33
37
|
|
|
@@ -57,6 +61,8 @@
|
|
|
57
61
|
onHeaderContextMenu,
|
|
58
62
|
onMenuClick,
|
|
59
63
|
onGridMenuOpen,
|
|
64
|
+
onRowClick,
|
|
65
|
+
onRowContextMenu,
|
|
60
66
|
theme = {},
|
|
61
67
|
selection = { type: "none" } as Selection,
|
|
62
68
|
onSelectionChange,
|
|
@@ -68,8 +74,9 @@
|
|
|
68
74
|
sortOrder,
|
|
69
75
|
onFilterChange: _onFilterChange,
|
|
70
76
|
filters: _filters,
|
|
71
|
-
selectedItems
|
|
77
|
+
selectedItems,
|
|
72
78
|
onSelectedItemsChange: _onSelectedItemsChange,
|
|
79
|
+
getRowData,
|
|
73
80
|
settings,
|
|
74
81
|
children,
|
|
75
82
|
}: Props = $props();
|
|
@@ -245,6 +252,7 @@
|
|
|
245
252
|
|
|
246
253
|
function handleKeyDown(e: KeyboardEvent) {
|
|
247
254
|
if (!canvasComponent?.getHasFocus()) return;
|
|
255
|
+
onGridEvent?.("keydown", undefined, undefined, { x: 0, y: 0, code: e.key });
|
|
248
256
|
|
|
249
257
|
// Search toggle
|
|
250
258
|
if ((e.ctrlKey || e.metaKey) && e.key === "f") {
|
|
@@ -308,7 +316,7 @@
|
|
|
308
316
|
if (!resolvedReadonly) {
|
|
309
317
|
beginEdit(focusedCell);
|
|
310
318
|
}
|
|
311
|
-
onCellEvent?.("enter_key", {}, columns[col] ?? { id: "", title: "" });
|
|
319
|
+
onCellEvent?.("enter_key", getRowData?.(row) ?? {}, columns[col] ?? { id: "", title: "" });
|
|
312
320
|
e.preventDefault();
|
|
313
321
|
return;
|
|
314
322
|
case "Escape": {
|
|
@@ -334,7 +342,7 @@
|
|
|
334
342
|
onRowAppended?.();
|
|
335
343
|
}
|
|
336
344
|
}
|
|
337
|
-
onCellEvent?.("tab_key", {}, columns[col] ?? { id: "", title: "" });
|
|
345
|
+
onCellEvent?.("tab_key", getRowData?.(row) ?? {}, columns[col] ?? { id: "", title: "" });
|
|
338
346
|
e.preventDefault();
|
|
339
347
|
break;
|
|
340
348
|
case "Delete":
|
|
@@ -346,7 +354,7 @@
|
|
|
346
354
|
onDelete?.(delSel);
|
|
347
355
|
}
|
|
348
356
|
}
|
|
349
|
-
onCellEvent?.("
|
|
357
|
+
onCellEvent?.("delete_key", getRowData?.(row) ?? {}, columns[col] ?? { id: "", title: "" });
|
|
350
358
|
return;
|
|
351
359
|
}
|
|
352
360
|
default:
|
|
@@ -376,7 +384,7 @@
|
|
|
376
384
|
|
|
377
385
|
function handleVisibleRangeChange(range: VisibleRange) {
|
|
378
386
|
onVisibleRangeChange?.(range);
|
|
379
|
-
onGridEvent?.("scroll", { row: range.firstRow, column: range.firstCol });
|
|
387
|
+
onGridEvent?.("scroll", undefined, undefined, undefined, { row: range.firstRow, column: range.firstCol });
|
|
380
388
|
}
|
|
381
389
|
|
|
382
390
|
async function handleMenuOpen(d: {
|
|
@@ -423,15 +431,51 @@
|
|
|
423
431
|
onSelectionChange={(sel) => {
|
|
424
432
|
currentSelection = sel;
|
|
425
433
|
onSelectionChange?.(sel);
|
|
434
|
+
onGridEvent?.("selection_changed");
|
|
426
435
|
}}
|
|
427
436
|
onCellDblClick={(item, cell) => {
|
|
428
437
|
onCellDblClick?.(item, cell);
|
|
429
|
-
onCellEvent?.("dblclick", {}, columns[item[0]] ?? { id: "", title: "" });
|
|
438
|
+
onCellEvent?.("dblclick", getRowData?.(item[1]) ?? {}, columns[item[0]] ?? { id: "", title: "" });
|
|
439
|
+
onGridEvent?.("dblclick", getRowData?.(item[1]) ?? {}, columns[item[0]] ?? { id: "", title: "" });
|
|
440
|
+
}}
|
|
441
|
+
onRowClick={(row) => {
|
|
442
|
+
onRowClick?.(row);
|
|
443
|
+
}}
|
|
444
|
+
onCellClick={(item) => {
|
|
445
|
+
onCellEvent?.("click", getRowData?.(item[1]) ?? {}, columns[item[0]] ?? { id: "", title: "" });
|
|
446
|
+
onGridEvent?.("click", getRowData?.(item[1]) ?? {}, columns[item[0]] ?? { id: "", title: "" });
|
|
447
|
+
}}
|
|
448
|
+
onCellHover={(item) => {
|
|
449
|
+
onCellEvent?.("hover", getRowData?.(item[1]) ?? {}, columns[item[0]] ?? { id: "", title: "" });
|
|
450
|
+
onGridEvent?.("hover", getRowData?.(item[1]) ?? {}, columns[item[0]] ?? { id: "", title: "" });
|
|
451
|
+
}}
|
|
452
|
+
onCellLeave={(item) => {
|
|
453
|
+
onCellEvent?.("leave", getRowData?.(item[1]) ?? {}, columns[item[0]] ?? { id: "", title: "" });
|
|
454
|
+
onGridEvent?.("leave", getRowData?.(item[1]) ?? {}, columns[item[0]] ?? { id: "", title: "" });
|
|
455
|
+
}}
|
|
456
|
+
onCellContextMenu={(item, x, y) => {
|
|
457
|
+
onCellEvent?.("contextmenu", getRowData?.(item[1]) ?? {}, columns[item[0]] ?? { id: "", title: "" }, { x, y });
|
|
458
|
+
onGridEvent?.("contextmenu", getRowData?.(item[1]) ?? {}, columns[item[0]] ?? { id: "", title: "" }, { x, y });
|
|
459
|
+
}}
|
|
460
|
+
onRowContextMenu={(row) => {
|
|
461
|
+
onRowContextMenu?.(row);
|
|
462
|
+
}}
|
|
463
|
+
onColumnResized={(col, width) => {
|
|
464
|
+
onGridEvent?.("column_resized", undefined, columns[col], undefined, { width });
|
|
465
|
+
}}
|
|
466
|
+
onGridResize={(width, height) => {
|
|
467
|
+
onGridEvent?.("resize", undefined, undefined, undefined, { width, height });
|
|
468
|
+
}}
|
|
469
|
+
onGridEnter={() => {
|
|
470
|
+
onGridEvent?.("enter");
|
|
430
471
|
}}
|
|
431
472
|
{sortOrder}
|
|
432
473
|
{onSortOrderChange}
|
|
433
474
|
{onHeaderContextMenu}
|
|
434
|
-
{onColumnMoved
|
|
475
|
+
onColumnMoved={onColumnMoved || onGridEvent ? (from, to) => {
|
|
476
|
+
onColumnMoved?.(from, to);
|
|
477
|
+
onGridEvent?.("column_moved", undefined, undefined, undefined, { from, to });
|
|
478
|
+
} : undefined}
|
|
435
479
|
{onCellEdited}
|
|
436
480
|
{onDelete}
|
|
437
481
|
{onRowAppended}
|
|
@@ -13,6 +13,10 @@ export interface Props extends Partial<GridlerProps> {
|
|
|
13
13
|
item?: Record<string, unknown>;
|
|
14
14
|
column?: GridlerColumn;
|
|
15
15
|
}) => void;
|
|
16
|
+
onRowClick?: (row: number) => void;
|
|
17
|
+
onRowContextMenu?: (row: number) => void;
|
|
18
|
+
/** Resolve raw row data by row index for populating onCellEvent. */
|
|
19
|
+
getRowData?: (row: number) => Record<string, unknown> | undefined;
|
|
16
20
|
children?: Snippet;
|
|
17
21
|
}
|
|
18
22
|
declare const Gridler: import("svelte").Component<Props, {}, "">;
|
|
@@ -56,6 +56,8 @@
|
|
|
56
56
|
onVisibleRangeChange?: (range: VisibleRange) => void;
|
|
57
57
|
onSelectionChange?: (sel: Selection) => void;
|
|
58
58
|
onCellDblClick?: (item: Item, cell: GridlerCell) => void;
|
|
59
|
+
onRowClick?: (row: number) => void;
|
|
60
|
+
onRowContextMenu?: (row: number) => void;
|
|
59
61
|
onColumnMoved?: (from: number, to: number) => void;
|
|
60
62
|
onCellEdited?: (item: Item, cell: GridlerCell) => void;
|
|
61
63
|
onDelete?: (sel: Selection) => void;
|
|
@@ -72,6 +74,13 @@
|
|
|
72
74
|
item?: Record<string, unknown>;
|
|
73
75
|
column?: GridColumn<Record<string, unknown>>;
|
|
74
76
|
}) => void;
|
|
77
|
+
onCellClick?: (item: Item) => void;
|
|
78
|
+
onCellHover?: (item: Item) => void;
|
|
79
|
+
onCellLeave?: (item: Item) => void;
|
|
80
|
+
onCellContextMenu?: (item: Item, x: number, y: number) => void;
|
|
81
|
+
onColumnResized?: (col: number, width: number) => void;
|
|
82
|
+
onGridResize?: (width: number, height: number) => void;
|
|
83
|
+
onGridEnter?: () => void;
|
|
75
84
|
onkeydown?: (e: KeyboardEvent) => void;
|
|
76
85
|
children?: Snippet;
|
|
77
86
|
}
|
|
@@ -100,6 +109,8 @@
|
|
|
100
109
|
onVisibleRangeChange,
|
|
101
110
|
onSelectionChange,
|
|
102
111
|
onCellDblClick,
|
|
112
|
+
onRowClick,
|
|
113
|
+
onRowContextMenu,
|
|
103
114
|
onColumnMoved,
|
|
104
115
|
onCellEdited: _onCellEdited,
|
|
105
116
|
onDelete: _onDelete,
|
|
@@ -111,6 +122,13 @@
|
|
|
111
122
|
onSearchValueChange,
|
|
112
123
|
onSearchClose,
|
|
113
124
|
onGridMenuOpen,
|
|
125
|
+
onCellClick,
|
|
126
|
+
onCellHover,
|
|
127
|
+
onCellLeave,
|
|
128
|
+
onCellContextMenu,
|
|
129
|
+
onColumnResized,
|
|
130
|
+
onGridResize,
|
|
131
|
+
onGridEnter,
|
|
114
132
|
onkeydown,
|
|
115
133
|
children,
|
|
116
134
|
}: Props = $props();
|
|
@@ -133,6 +151,7 @@
|
|
|
133
151
|
let hasFocus = $state(false);
|
|
134
152
|
const gridId = `gridler-${Math.random().toString(36).slice(2, 10)}`;
|
|
135
153
|
|
|
154
|
+
let hoveredCell = $state<Item | null>(null);
|
|
136
155
|
let isDraggingVScrollbar = $state(false);
|
|
137
156
|
let isDraggingHScrollbar = $state(false);
|
|
138
157
|
let vScrollDragStartY = $state(0);
|
|
@@ -286,6 +305,7 @@
|
|
|
286
305
|
if (entry) {
|
|
287
306
|
containerWidth = entry.contentRect.width;
|
|
288
307
|
containerHeight = entry.contentRect.height;
|
|
308
|
+
onGridResize?.(entry.contentRect.width, entry.contentRect.height);
|
|
289
309
|
}
|
|
290
310
|
});
|
|
291
311
|
observer.observe(containerRef);
|
|
@@ -468,8 +488,7 @@
|
|
|
468
488
|
// ── Header context menu ───────────────────────────────────────────────────────
|
|
469
489
|
|
|
470
490
|
function handleContextMenu(e: MouseEvent) {
|
|
471
|
-
if (
|
|
472
|
-
if (e.offsetY >= headerHeight || e.offsetX < rowMarkerWidth) return;
|
|
491
|
+
if (e.offsetY < headerHeight && e.offsetX < rowMarkerWidth) return;
|
|
473
492
|
const fixedBoundaryX =
|
|
474
493
|
fixedColumns > 0
|
|
475
494
|
? getColumnX(effectiveColumns, Math.min(fixedColumns, columns.length))
|
|
@@ -477,10 +496,39 @@
|
|
|
477
496
|
const localX = e.offsetX - rowMarkerWidth;
|
|
478
497
|
const effectiveScrollX =
|
|
479
498
|
fixedColumns > 0 && localX >= 0 && localX < fixedBoundaryX ? 0 : scrollX;
|
|
480
|
-
|
|
481
|
-
if (
|
|
499
|
+
|
|
500
|
+
if (e.offsetY < headerHeight) {
|
|
501
|
+
if (!onHeaderContextMenu) return;
|
|
502
|
+
const col = getColumnFromX(localX + effectiveScrollX, effectiveColumns);
|
|
503
|
+
if (col >= 0 && col < columns.length) {
|
|
504
|
+
e.preventDefault();
|
|
505
|
+
onHeaderContextMenu(col, e.clientX, e.clientY);
|
|
506
|
+
}
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const row = getRowFromOffsetY(e.offsetY);
|
|
511
|
+
if (row !== null) {
|
|
482
512
|
e.preventDefault();
|
|
483
|
-
|
|
513
|
+
onRowContextMenu?.(row);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (e.offsetX < rowMarkerWidth) return;
|
|
517
|
+
|
|
518
|
+
if (onCellContextMenu) {
|
|
519
|
+
const cell = getCellFromPoint(
|
|
520
|
+
localX,
|
|
521
|
+
e.offsetY,
|
|
522
|
+
effectiveScrollX,
|
|
523
|
+
scrollY,
|
|
524
|
+
effectiveColumns,
|
|
525
|
+
rowHeight,
|
|
526
|
+
headerHeight,
|
|
527
|
+
);
|
|
528
|
+
if (cell) {
|
|
529
|
+
e.preventDefault();
|
|
530
|
+
onCellContextMenu(cell, e.clientX, e.clientY);
|
|
531
|
+
}
|
|
484
532
|
}
|
|
485
533
|
}
|
|
486
534
|
|
|
@@ -501,6 +549,12 @@
|
|
|
501
549
|
|
|
502
550
|
// ── Mouse handlers ────────────────────────────────────────────────────────────
|
|
503
551
|
|
|
552
|
+
function getRowFromOffsetY(offsetY: number): number | null {
|
|
553
|
+
if (offsetY < headerHeight) return null;
|
|
554
|
+
const row = Math.floor((offsetY - headerHeight + scrollY) / rowHeight);
|
|
555
|
+
return row >= 0 && row < rows ? row : null;
|
|
556
|
+
}
|
|
557
|
+
|
|
504
558
|
function handleMouseDown(e: MouseEvent) {
|
|
505
559
|
if (e.button !== 0) return;
|
|
506
560
|
pendingGridMenuOpen = false;
|
|
@@ -594,6 +648,11 @@
|
|
|
594
648
|
return;
|
|
595
649
|
}
|
|
596
650
|
|
|
651
|
+
const row = getRowFromOffsetY(e.offsetY);
|
|
652
|
+
if (row !== null) {
|
|
653
|
+
onRowClick?.(row);
|
|
654
|
+
}
|
|
655
|
+
|
|
597
656
|
if (readonly) return;
|
|
598
657
|
|
|
599
658
|
const cell = getCellFromPoint(
|
|
@@ -619,6 +678,7 @@
|
|
|
619
678
|
}
|
|
620
679
|
|
|
621
680
|
if (cell) {
|
|
681
|
+
onCellClick?.(cell);
|
|
622
682
|
if (e.shiftKey && focusedCell) {
|
|
623
683
|
const newSelection: Selection = {
|
|
624
684
|
type: "range",
|
|
@@ -802,6 +862,36 @@
|
|
|
802
862
|
return;
|
|
803
863
|
}
|
|
804
864
|
|
|
865
|
+
// Hover cell tracking
|
|
866
|
+
if (e.offsetY >= headerHeight && e.offsetX >= rowMarkerWidth) {
|
|
867
|
+
const hoverFixedBoundaryX =
|
|
868
|
+
fixedColumns > 0
|
|
869
|
+
? getColumnX(effectiveColumns, Math.min(fixedColumns, columns.length))
|
|
870
|
+
: 0;
|
|
871
|
+
const hoverLocalX = e.offsetX - rowMarkerWidth;
|
|
872
|
+
const hoverScrollX =
|
|
873
|
+
fixedColumns > 0 && hoverLocalX >= 0 && hoverLocalX < hoverFixedBoundaryX
|
|
874
|
+
? 0
|
|
875
|
+
: scrollX;
|
|
876
|
+
const hoverCell = getCellFromPoint(
|
|
877
|
+
hoverLocalX,
|
|
878
|
+
e.offsetY,
|
|
879
|
+
hoverScrollX,
|
|
880
|
+
scrollY,
|
|
881
|
+
effectiveColumns,
|
|
882
|
+
rowHeight,
|
|
883
|
+
headerHeight,
|
|
884
|
+
);
|
|
885
|
+
if (hoverCell?.[0] !== hoveredCell?.[0] || hoverCell?.[1] !== hoveredCell?.[1]) {
|
|
886
|
+
if (hoveredCell) onCellLeave?.(hoveredCell);
|
|
887
|
+
hoveredCell = hoverCell;
|
|
888
|
+
if (hoverCell) onCellHover?.(hoverCell);
|
|
889
|
+
}
|
|
890
|
+
} else if (hoveredCell !== null) {
|
|
891
|
+
onCellLeave?.(hoveredCell);
|
|
892
|
+
hoveredCell = null;
|
|
893
|
+
}
|
|
894
|
+
|
|
805
895
|
if (!isDragging || !dragStart) return;
|
|
806
896
|
|
|
807
897
|
const fixedBoundaryX =
|
|
@@ -865,6 +955,9 @@
|
|
|
865
955
|
scheduleDraw();
|
|
866
956
|
}
|
|
867
957
|
|
|
958
|
+
if (resizingColumn !== null) {
|
|
959
|
+
onColumnResized?.(resizingColumn, columns[resizingColumn].width);
|
|
960
|
+
}
|
|
868
961
|
isDragging = false;
|
|
869
962
|
dragStart = null;
|
|
870
963
|
resizingColumn = null;
|
|
@@ -876,6 +969,10 @@
|
|
|
876
969
|
hoverResizeCol = null;
|
|
877
970
|
hoverMenuButton = false;
|
|
878
971
|
hoverSortCol = null;
|
|
972
|
+
if (hoveredCell) {
|
|
973
|
+
onCellLeave?.(hoveredCell);
|
|
974
|
+
hoveredCell = null;
|
|
975
|
+
}
|
|
879
976
|
if (!isDraggingVScrollbar && !isDraggingHScrollbar) {
|
|
880
977
|
handleMouseUp(e);
|
|
881
978
|
} else {
|
|
@@ -981,7 +1078,7 @@
|
|
|
981
1078
|
}
|
|
982
1079
|
|
|
983
1080
|
if (rowMarkers !== "none" && offsetX < rowMarkerWidth) {
|
|
984
|
-
const row =
|
|
1081
|
+
const row = getRowFromOffsetY(offsetY);
|
|
985
1082
|
if (row >= 0 && row < rows) {
|
|
986
1083
|
onRowToggle?.(row);
|
|
987
1084
|
}
|
|
@@ -1073,6 +1170,7 @@
|
|
|
1073
1170
|
style:--gridler-header-height={`${headerHeight}px`}
|
|
1074
1171
|
onfocusin={handleFocusIn}
|
|
1075
1172
|
onfocusout={handleFocusOut}
|
|
1173
|
+
onmouseenter={onGridEnter}
|
|
1076
1174
|
{onkeydown}
|
|
1077
1175
|
>
|
|
1078
1176
|
<canvas
|
|
@@ -25,6 +25,8 @@ interface Props {
|
|
|
25
25
|
onVisibleRangeChange?: (range: VisibleRange) => void;
|
|
26
26
|
onSelectionChange?: (sel: Selection) => void;
|
|
27
27
|
onCellDblClick?: (item: Item, cell: GridlerCell) => void;
|
|
28
|
+
onRowClick?: (row: number) => void;
|
|
29
|
+
onRowContextMenu?: (row: number) => void;
|
|
28
30
|
onColumnMoved?: (from: number, to: number) => void;
|
|
29
31
|
onCellEdited?: (item: Item, cell: GridlerCell) => void;
|
|
30
32
|
onDelete?: (sel: Selection) => void;
|
|
@@ -41,6 +43,13 @@ interface Props {
|
|
|
41
43
|
item?: Record<string, unknown>;
|
|
42
44
|
column?: GridColumn<Record<string, unknown>>;
|
|
43
45
|
}) => void;
|
|
46
|
+
onCellClick?: (item: Item) => void;
|
|
47
|
+
onCellHover?: (item: Item) => void;
|
|
48
|
+
onCellLeave?: (item: Item) => void;
|
|
49
|
+
onCellContextMenu?: (item: Item, x: number, y: number) => void;
|
|
50
|
+
onColumnResized?: (col: number, width: number) => void;
|
|
51
|
+
onGridResize?: (width: number, height: number) => void;
|
|
52
|
+
onGridEnter?: () => void;
|
|
44
53
|
onkeydown?: (e: KeyboardEvent) => void;
|
|
45
54
|
children?: Snippet;
|
|
46
55
|
}
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
| "onSearchValueChange"
|
|
39
39
|
| "onVisibleRangeChange"
|
|
40
40
|
| "onCellDblClick"
|
|
41
|
+
| "onRowClick"
|
|
42
|
+
| "onRowContextMenu"
|
|
41
43
|
> {
|
|
42
44
|
columns: GridlerColumn[];
|
|
43
45
|
|
|
@@ -81,6 +83,16 @@
|
|
|
81
83
|
row: number,
|
|
82
84
|
rowData: Record<string, unknown> | undefined,
|
|
83
85
|
) => void;
|
|
86
|
+
/** Called on row click with the row index and resolved row data. */
|
|
87
|
+
onRowClick?: (
|
|
88
|
+
row: number,
|
|
89
|
+
rowData: Record<string, unknown> | undefined,
|
|
90
|
+
) => void;
|
|
91
|
+
/** Called on row context-menu with the row index and resolved row data. */
|
|
92
|
+
onRowContextMenu?: (
|
|
93
|
+
row: number,
|
|
94
|
+
rowData: Record<string, unknown> | undefined,
|
|
95
|
+
) => void;
|
|
84
96
|
|
|
85
97
|
// ── Server-side fetching ──────────────────────────────────────────────────
|
|
86
98
|
// Use dataSource + dataSourceOptions (from GridCommonProps) for server config.
|
|
@@ -157,6 +169,8 @@
|
|
|
157
169
|
onCellEdited,
|
|
158
170
|
onCellDblClick,
|
|
159
171
|
onRowDblClick,
|
|
172
|
+
onRowClick,
|
|
173
|
+
onRowContextMenu,
|
|
160
174
|
onMenuClick,
|
|
161
175
|
onSelectionChange,
|
|
162
176
|
onCellEvent,
|
|
@@ -199,9 +213,11 @@
|
|
|
199
213
|
if (typeof data === "function") {
|
|
200
214
|
data().then((result) => {
|
|
201
215
|
dataState = result;
|
|
216
|
+
onGridEvent?.("load");
|
|
202
217
|
});
|
|
203
218
|
} else {
|
|
204
219
|
dataState = data.slice();
|
|
220
|
+
onGridEvent?.("load");
|
|
205
221
|
}
|
|
206
222
|
});
|
|
207
223
|
|
|
@@ -258,6 +274,7 @@
|
|
|
258
274
|
internalSearchValue = value;
|
|
259
275
|
}
|
|
260
276
|
onSearchValueChange?.(value);
|
|
277
|
+
onGridEvent?.("search_changed", undefined, undefined, undefined, { searchValue: value });
|
|
261
278
|
}
|
|
262
279
|
|
|
263
280
|
// ── Context menu ───────────────────────────────────────────────────────────
|
|
@@ -477,6 +494,7 @@
|
|
|
477
494
|
function handleFilterChange(next: GridColumnFilters) {
|
|
478
495
|
if (filters === undefined) internalFilters = next;
|
|
479
496
|
onFilterChange?.(next);
|
|
497
|
+
onGridEvent?.("filter_changed", undefined, undefined, undefined, { filters: next });
|
|
480
498
|
}
|
|
481
499
|
|
|
482
500
|
|
|
@@ -530,6 +548,7 @@
|
|
|
530
548
|
function handleSortOrderChange(order: GridColumnSortOrder) {
|
|
531
549
|
if (sortOrder === undefined) internalSortOrder = order;
|
|
532
550
|
onSortOrderChange?.(order);
|
|
551
|
+
onGridEvent?.("sort_changed", undefined, undefined, undefined, { sortOrder: order });
|
|
533
552
|
}
|
|
534
553
|
|
|
535
554
|
const resolvedSort = $derived(
|
|
@@ -619,6 +638,7 @@
|
|
|
619
638
|
serverData = [...serverData, ...result.data];
|
|
620
639
|
serverCursor = result.nextCursor;
|
|
621
640
|
serverAllLoaded = result.data.length < pageSize || !result.nextCursor;
|
|
641
|
+
onGridEvent?.("page_loaded", undefined, undefined, undefined, { data: result.data, total: serverData.length });
|
|
622
642
|
} catch {
|
|
623
643
|
// Silent — don't clobber existing rows with an error on append.
|
|
624
644
|
} finally {
|
|
@@ -650,18 +670,49 @@
|
|
|
650
670
|
onRowDblClick?.(item[1], rowData);
|
|
651
671
|
}
|
|
652
672
|
|
|
673
|
+
function handleRowClick(row: number) {
|
|
674
|
+
onRowClick?.(row, resolveRowData(row));
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function handleRowContextMenu(row: number) {
|
|
678
|
+
onRowContextMenu?.(row, resolveRowData(row));
|
|
679
|
+
}
|
|
680
|
+
|
|
653
681
|
// ── Selection → selectedItems bridge ──────────────────────────────────────
|
|
654
682
|
|
|
683
|
+
let internalSelectedItems = $state<Record<string, unknown>[]>([]);
|
|
684
|
+
|
|
655
685
|
function handleSelectionChange(sel: Selection) {
|
|
656
|
-
|
|
657
|
-
|
|
686
|
+
let items: Record<string, unknown>[] = [];
|
|
687
|
+
if (sel.type === "row") {
|
|
688
|
+
items = sel.rows
|
|
658
689
|
.map((i) => resolveRowData(i))
|
|
659
690
|
.filter((r): r is Record<string, unknown> => r !== undefined);
|
|
660
|
-
|
|
691
|
+
} else if (sel.type === "cell") {
|
|
692
|
+
const row = resolveRowData(sel.item[1]);
|
|
693
|
+
if (row) items = [row];
|
|
694
|
+
} else if (sel.type === "range") {
|
|
695
|
+
const minRow = Math.min(sel.start[1], sel.end[1]);
|
|
696
|
+
const maxRow = Math.max(sel.start[1], sel.end[1]);
|
|
697
|
+
for (let i = minRow; i <= maxRow; i++) {
|
|
698
|
+
const row = resolveRowData(i);
|
|
699
|
+
if (row) items.push(row);
|
|
700
|
+
}
|
|
661
701
|
}
|
|
702
|
+
internalSelectedItems = items;
|
|
703
|
+
if (items.length > 0) onSelectedItemsChange?.(items);
|
|
662
704
|
onSelectionChange?.(sel);
|
|
663
705
|
}
|
|
664
706
|
|
|
707
|
+
// ── Settings change event ─────────────────────────────────────────────────
|
|
708
|
+
|
|
709
|
+
let lastSettings = $state(settings);
|
|
710
|
+
$effect(() => {
|
|
711
|
+
if (settings === untrack(() => lastSettings)) return;
|
|
712
|
+
lastSettings = settings;
|
|
713
|
+
onGridEvent?.("settings_changed", undefined, undefined, undefined, { settings });
|
|
714
|
+
});
|
|
715
|
+
|
|
665
716
|
// ── Cell content ───────────────────────────────────────────────────────────
|
|
666
717
|
|
|
667
718
|
const serverGetCellContent = $derived(makeGetCellContent(serverData, columnsState));
|
|
@@ -700,6 +751,7 @@
|
|
|
700
751
|
};
|
|
701
752
|
dataState = next;
|
|
702
753
|
await onDataChange?.(next);
|
|
754
|
+
onGridEvent?.("data_changed", next[originalIndex], columnsState[col], undefined, { data: next });
|
|
703
755
|
}
|
|
704
756
|
|
|
705
757
|
async function handleGridMenuClick(d?: {
|
|
@@ -747,7 +799,12 @@
|
|
|
747
799
|
onCellEdited?.(item, value);
|
|
748
800
|
}}
|
|
749
801
|
onCellDblClick={handleCellDblClick}
|
|
802
|
+
onRowClick={handleRowClick}
|
|
803
|
+
onRowContextMenu={handleRowContextMenu}
|
|
804
|
+
selectedItems={internalSelectedItems}
|
|
805
|
+
getRowData={resolveRowData}
|
|
750
806
|
onSelectionChange={handleSelectionChange}
|
|
807
|
+
{onMenuClick}
|
|
751
808
|
onGridMenuOpen={handleGridMenuClick}
|
|
752
809
|
sortOrder={resolvedSortOrder}
|
|
753
810
|
onSortOrderChange={handleSortOrderChange}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Snippet } from "svelte";
|
|
2
2
|
import type { GridlerCell, GridlerColumn, GridlerContextMenuItem, GridlerProps, Item } from "../types";
|
|
3
|
-
export interface Props extends Omit<Partial<GridlerProps>, "columns" | "rows" | "getCellContent" | "searchValue" | "onSearchValueChange" | "onVisibleRangeChange" | "onCellDblClick"> {
|
|
3
|
+
export interface Props extends Omit<Partial<GridlerProps>, "columns" | "rows" | "getCellContent" | "searchValue" | "onSearchValueChange" | "onVisibleRangeChange" | "onCellDblClick" | "onRowClick" | "onRowContextMenu"> {
|
|
4
4
|
columns: GridlerColumn[];
|
|
5
5
|
/**
|
|
6
6
|
* Optional row data. When provided (and `getCellContent` is omitted), the default
|
|
@@ -28,6 +28,10 @@ export interface Props extends Omit<Partial<GridlerProps>, "columns" | "rows" |
|
|
|
28
28
|
onCellDblClick?: (item: Item, cell: GridlerCell, rowData: Record<string, unknown> | undefined) => void;
|
|
29
29
|
/** Called on row double-click with the row index and resolved row data. */
|
|
30
30
|
onRowDblClick?: (row: number, rowData: Record<string, unknown> | undefined) => void;
|
|
31
|
+
/** Called on row click with the row index and resolved row data. */
|
|
32
|
+
onRowClick?: (row: number, rowData: Record<string, unknown> | undefined) => void;
|
|
33
|
+
/** Called on row context-menu with the row index and resolved row data. */
|
|
34
|
+
onRowContextMenu?: (row: number, rowData: Record<string, unknown> | undefined) => void;
|
|
31
35
|
/** Rows per cursor-forward page. Defaults to 200. */
|
|
32
36
|
pageSize?: number;
|
|
33
37
|
/**
|
|
@@ -103,6 +103,10 @@ export interface GridlerProps extends GridCommonProps<Record<string, unknown>> {
|
|
|
103
103
|
onCellEdited?: (item: Item, value: GridlerCell) => void;
|
|
104
104
|
/** Called on cell double-click (low-level; GridlerFull enriches with row data). */
|
|
105
105
|
onCellDblClick?: (item: Item, cell: GridlerCell) => void;
|
|
106
|
+
/** Called when a body row is clicked. */
|
|
107
|
+
onRowClick?: (row: number) => void;
|
|
108
|
+
/** Called when a body row receives a context-menu event. */
|
|
109
|
+
onRowContextMenu?: (row: number) => void;
|
|
106
110
|
/** Called when Tab moves past the last column of the last row. */
|
|
107
111
|
onRowAppended?: () => void;
|
|
108
112
|
/** Called when Delete/Backspace is pressed with a selection. */
|
package/llm/README.md
CHANGED
|
@@ -16,6 +16,7 @@ This folder contains AI-readable documentation that ships with the package.
|
|
|
16
16
|
- [tools/SVAR.md](./tools/SVAR.md)
|
|
17
17
|
- [tools/resolvespec-js.md](./tools/resolvespec-js.md)
|
|
18
18
|
- [plans/canvasgrid.md](./plans/canvasgrid.md)
|
|
19
|
+
- [gridler-events.md](./gridler-events.md)
|
|
19
20
|
|
|
20
21
|
## Installed package paths
|
|
21
22
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# Gridler Events
|
|
2
|
+
|
|
3
|
+
`GridlerFull` (and the lower-level `Gridler`) emit two event callbacks defined in `GridCommonProps` (`src/lib/components/Types/generic_grid.ts`):
|
|
4
|
+
|
|
5
|
+
| Callback | Purpose |
|
|
6
|
+
|---|---|
|
|
7
|
+
| `onGridEvent` | Grid-level events (interaction, state, lifecycle) |
|
|
8
|
+
| `onCellEvent` | Cell-level events (mouse + keyboard on a specific cell) |
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## `onGridEvent`
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
onGridEvent?: (
|
|
16
|
+
type: GridEventType,
|
|
17
|
+
item?: Record<string, unknown>, // resolved raw row data
|
|
18
|
+
column?: GridColumn,
|
|
19
|
+
coords?: GridEventCoords, // { x, y, code? }
|
|
20
|
+
detail?: GridEventDetail, // event-specific payload
|
|
21
|
+
) => void
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### All `GridEventType` values
|
|
25
|
+
|
|
26
|
+
| Type | Fired from | `item` | `column` | `coords` | `detail` |
|
|
27
|
+
|---|---|---|---|---|---|
|
|
28
|
+
| `click` | Mouse click on a cell | row data | clicked column | — | — |
|
|
29
|
+
| `dblclick` | Double-click on a cell | row data | clicked column | — | — |
|
|
30
|
+
| `contextmenu` | Right-click on a cell | row data | clicked column | `{ x, y }` client coords | — |
|
|
31
|
+
| `keydown` | Any key pressed while grid focused | — | — | `{ x:0, y:0, code }` | — |
|
|
32
|
+
| `hover` | Mouse moves over a new cell | row data | hovered column | — | — |
|
|
33
|
+
| `leave` | Mouse leaves a cell | row data | vacated column | — | — |
|
|
34
|
+
| `enter` | Mouse enters the grid container | — | — | — | — |
|
|
35
|
+
| `load` | Local `data` prop set/changed, or first server page loaded | — | — | — | — |
|
|
36
|
+
| `scroll` | Visible range changes (scroll) | — | — | — | `{ row, column }` first visible |
|
|
37
|
+
| `resize` | Grid container resized (ResizeObserver) | — | — | — | `{ width, height }` |
|
|
38
|
+
| `page_loaded` | Next cursor page loaded from server | — | — | — | `{ data, total }` |
|
|
39
|
+
| `sort_changed` | Sort order applied or cleared | — | — | — | `{ sortOrder: GridColumnSortOrder }` |
|
|
40
|
+
| `filter_changed` | Filter applied or cleared | — | — | — | `{ filters: GridColumnFilters }` |
|
|
41
|
+
| `search_changed` | Search input value changed | — | — | — | `{ searchValue: string }` |
|
|
42
|
+
| `selection_changed` | Cell, row, range, or column selection changes | — | — | — | — |
|
|
43
|
+
| `column_moved` | Column dragged to new position | — | — | — | `{ from: number, to: number }` |
|
|
44
|
+
| `column_resized` | Column edge dragged to new width | — | moved column | — | `{ width: number }` |
|
|
45
|
+
| `data_changed` | Cell edited and committed (local mode only) | updated row | edited column | — | `{ data: Row[] }` full new dataset |
|
|
46
|
+
| `settings_changed` | `settings` prop reference changed | — | — | — | `{ settings }` |
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## `onCellEvent`
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
onCellEvent?: (
|
|
54
|
+
type: GridCellEventType,
|
|
55
|
+
item: Record<string, unknown>, // resolved raw row data
|
|
56
|
+
column: GridColumn,
|
|
57
|
+
coords?: GridEventCoords,
|
|
58
|
+
detail?: GridEventDetail,
|
|
59
|
+
) => void
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### All `GridCellEventType` values
|
|
63
|
+
|
|
64
|
+
| Type | Fired when |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `click` | Mouse click on the cell |
|
|
67
|
+
| `dblclick` | Double-click on the cell |
|
|
68
|
+
| `contextmenu` | Right-click on the cell |
|
|
69
|
+
| `hover` | Mouse moves over this cell |
|
|
70
|
+
| `leave` | Mouse leaves this cell |
|
|
71
|
+
| `enter_key` | Enter pressed while this cell is focused |
|
|
72
|
+
| `tab_key` | Tab pressed while this cell is focused |
|
|
73
|
+
| `delete_key` | Delete or Backspace pressed while this cell is focused |
|
|
74
|
+
|
|
75
|
+
For all `onCellEvent` calls, `item` carries the **resolved raw row data** (from `serverData` or local `filteredDataState`). When the row cannot be resolved by index (e.g. from a top-level menu button), `item` falls back to the value in `selectedItems[0]`.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Row data resolution
|
|
80
|
+
|
|
81
|
+
`GridlerFull` passes `getRowData` to `Gridler`, which calls `resolveRowData(rowIndex)`:
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
function resolveRowData(row: number): Record<string, unknown> | undefined {
|
|
85
|
+
return isServerMode ? serverData[row] : filteredDataState[row];
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The current selection's row data is also tracked in `internalSelectedItems` state and passed as `selectedItems` to `Gridler`. This is used as a fallback when no row index is available.
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Field selection sent to adapters (hotfields)
|
|
94
|
+
|
|
95
|
+
When `dataSource="resolvespec"` or `"headerspec"`, `GridlerFull` derives a `resolvedFields` array and passes it to every `readPage` call as `options.columns` (maps to `X-Select-Fields` in headerspec):
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const resolvedFields = $derived.by(() => {
|
|
99
|
+
const columnFields = columnsState.map((c) => c.dataKey ?? c.id);
|
|
100
|
+
const hotfields = dataSourceOptions?.hotfields ?? [];
|
|
101
|
+
const seen = new Set(columnFields);
|
|
102
|
+
const extra = hotfields.filter((f) => !seen.has(f));
|
|
103
|
+
return [...columnFields, ...extra];
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- **Column fields** come from `column.dataKey ?? column.id` for every column.
|
|
108
|
+
- **Hotfields** (`dataSourceOptions.hotfields`) are extra fields needed server-side (for filtering, sorting, FK lookups) that are not grid columns. They are appended without duplicating existing column fields.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Usage example
|
|
113
|
+
|
|
114
|
+
```svelte
|
|
115
|
+
<GridlerFull
|
|
116
|
+
{columns}
|
|
117
|
+
{data}
|
|
118
|
+
onGridEvent={(type, item, column, coords, detail) => {
|
|
119
|
+
if (type === 'sort_changed') console.log('Sort:', detail?.sortOrder);
|
|
120
|
+
if (type === 'filter_changed') console.log('Filter:', detail?.filters);
|
|
121
|
+
if (type === 'search_changed') console.log('Search:', detail?.searchValue);
|
|
122
|
+
if (type === 'data_changed') console.log('Edited row:', item, 'All data:', detail?.data);
|
|
123
|
+
if (type === 'column_moved') console.log('Moved col', detail?.from, '→', detail?.to);
|
|
124
|
+
if (type === 'page_loaded') console.log('New page:', detail?.data, 'Total so far:', detail?.total);
|
|
125
|
+
}}
|
|
126
|
+
onCellEvent={(type, item, column) => {
|
|
127
|
+
if (type === 'click') console.log('Clicked:', item, column.id);
|
|
128
|
+
if (type === 'enter_key') console.log('Enter on row:', item);
|
|
129
|
+
}}
|
|
130
|
+
dataSource="resolvespec"
|
|
131
|
+
dataSourceOptions={{
|
|
132
|
+
url: 'https://api.example.com',
|
|
133
|
+
schema: 'public',
|
|
134
|
+
entity: 'users',
|
|
135
|
+
uniqueID: 'id',
|
|
136
|
+
hotfields: ['tenant_id', 'deleted_at'], // fetched but not shown as columns
|
|
137
|
+
}}
|
|
138
|
+
/>
|
|
139
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@warkypublic/svelix",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.32",
|
|
4
4
|
"description": "Svelte 5 component library with Skeleton UI and Tailwind CSS",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"exports": {
|
|
@@ -26,43 +26,43 @@
|
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@changesets/cli": "^2.30.0",
|
|
29
|
-
"@chromatic-com/storybook": "^5.
|
|
30
|
-
"@eslint/compat": "^2.0.
|
|
29
|
+
"@chromatic-com/storybook": "^5.1.1",
|
|
30
|
+
"@eslint/compat": "^2.0.4",
|
|
31
31
|
"@eslint/js": "^10.0.1",
|
|
32
|
-
"@playwright/test": "^1.
|
|
33
|
-
"@sentry/svelte": "^10.
|
|
34
|
-
"@skeletonlabs/skeleton": "^4.
|
|
35
|
-
"@skeletonlabs/skeleton-svelte": "^4.
|
|
36
|
-
"@storybook/addon-a11y": "^10.3.
|
|
37
|
-
"@storybook/addon-docs": "^10.3.
|
|
38
|
-
"@storybook/addon-svelte-csf": "^5.
|
|
39
|
-
"@storybook/addon-vitest": "^10.3.
|
|
40
|
-
"@storybook/sveltekit": "^10.3.
|
|
32
|
+
"@playwright/test": "^1.59.1",
|
|
33
|
+
"@sentry/svelte": "^10.47.0",
|
|
34
|
+
"@skeletonlabs/skeleton": "^4.15.1",
|
|
35
|
+
"@skeletonlabs/skeleton-svelte": "^4.15.1",
|
|
36
|
+
"@storybook/addon-a11y": "^10.3.4",
|
|
37
|
+
"@storybook/addon-docs": "^10.3.4",
|
|
38
|
+
"@storybook/addon-svelte-csf": "^5.1.2",
|
|
39
|
+
"@storybook/addon-vitest": "^10.3.4",
|
|
40
|
+
"@storybook/sveltekit": "^10.3.4",
|
|
41
41
|
"@storybook/test": "^8.6.15",
|
|
42
42
|
"@sveltejs/adapter-auto": "^7.0.1",
|
|
43
|
-
"@sveltejs/kit": "^2.
|
|
43
|
+
"@sveltejs/kit": "^2.56.1",
|
|
44
44
|
"@sveltejs/package": "^2.5.7",
|
|
45
45
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
46
46
|
"@tailwindcss/vite": "^4.2.2",
|
|
47
47
|
"@tanstack/svelte-virtual": "^3.13.23",
|
|
48
|
-
"@types/node": "^25.5.
|
|
49
|
-
"@vitest/browser-playwright": "^4.1.
|
|
50
|
-
"@vitest/coverage-v8": "^4.1.
|
|
51
|
-
"eslint": "^10.0
|
|
52
|
-
"eslint-plugin-svelte": "^3.
|
|
48
|
+
"@types/node": "^25.5.2",
|
|
49
|
+
"@vitest/browser-playwright": "^4.1.3",
|
|
50
|
+
"@vitest/coverage-v8": "^4.1.3",
|
|
51
|
+
"eslint": "^10.2.0",
|
|
52
|
+
"eslint-plugin-svelte": "^3.17.0",
|
|
53
53
|
"globals": "^17.4.0",
|
|
54
|
-
"playwright": "^1.
|
|
54
|
+
"playwright": "^1.59.1",
|
|
55
55
|
"publint": "^0.3.18",
|
|
56
|
-
"storybook": "^10.3.
|
|
57
|
-
"svelte": "^5.
|
|
58
|
-
"svelte-check": "^4.4.
|
|
56
|
+
"storybook": "^10.3.4",
|
|
57
|
+
"svelte": "^5.55.1",
|
|
58
|
+
"svelte-check": "^4.4.6",
|
|
59
59
|
"tailwindcss": "^4.2.2",
|
|
60
60
|
"tslib": "^2.8.1",
|
|
61
|
-
"typescript": "^
|
|
62
|
-
"typescript-eslint": "^8.
|
|
63
|
-
"vite": "^8.0.
|
|
61
|
+
"typescript": "^6.0.2",
|
|
62
|
+
"typescript-eslint": "^8.58.0",
|
|
63
|
+
"vite": "^8.0.7",
|
|
64
64
|
"vite-plugin-monaco-editor": "^1.1.0",
|
|
65
|
-
"vitest": "^4.1.
|
|
65
|
+
"vitest": "^4.1.3"
|
|
66
66
|
},
|
|
67
67
|
"svelte": "./dist/index.js",
|
|
68
68
|
"types": "./dist/index.d.ts",
|
|
@@ -76,13 +76,13 @@
|
|
|
76
76
|
"@cartamd/plugin-math": "^4.3.1",
|
|
77
77
|
"@friendofsvelte/tipex": "^0.1.1",
|
|
78
78
|
"@js-temporal/polyfill": "^0.5.1",
|
|
79
|
-
"@svar-ui/svelte-grid": "^2.6.
|
|
79
|
+
"@svar-ui/svelte-grid": "^2.6.1",
|
|
80
80
|
"@warkypublic/resolvespec-js": "^1.0.1",
|
|
81
81
|
"@warkypublic/artemis-kit": "^1.0.10",
|
|
82
82
|
"carta-md": "^4.11.1",
|
|
83
83
|
"github-markdown-css": "^5.9.0",
|
|
84
|
-
"isomorphic-dompurify": "^3.
|
|
85
|
-
"katex": "^0.16.
|
|
84
|
+
"isomorphic-dompurify": "^3.7.1",
|
|
85
|
+
"katex": "^0.16.45",
|
|
86
86
|
"monaco-editor": "^0.55.1"
|
|
87
87
|
},
|
|
88
88
|
"scripts": {
|