@warkypublic/svelix 0.1.44 → 0.1.47
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 +488 -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 +1141 -0
- package/dist/components/CardGrid/CardGrid.svelte.d.ts +61 -0
- package/dist/components/CardGrid/CardGridFilterPanel.svelte +320 -0
- package/dist/components/CardGrid/CardGridFilterPanel.svelte.d.ts +11 -0
- package/dist/components/CardGrid/DefaultCard.svelte +287 -0
- package/dist/components/CardGrid/DefaultCard.svelte.d.ts +10 -0
- package/dist/components/CardGrid/ImageCardStory.svelte +308 -0
- package/dist/components/CardGrid/ImageCardStory.svelte.d.ts +18 -0
- package/dist/components/CardGrid/cardGrid.d.ts +33 -0
- package/dist/components/CardGrid/cardGrid.js +21 -0
- package/dist/components/CardGrid/index.d.ts +4 -0
- package/dist/components/CardGrid/index.js +4 -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 +26 -78
- package/dist/components/Former/FormerDrawerPreview.svelte +99 -89
- package/dist/components/Former/FormerModal.svelte +24 -70
- 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/FormerControllers/ButtonCtrl.svelte +13 -44
- package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrl.svelte +280 -445
- 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 -42
- package/dist/components/FormerControllers/InlineWrapper.svelte +12 -41
- package/dist/components/FormerControllers/InlineWrapperPreview.svelte +4 -26
- package/dist/components/FormerControllers/NativeSelectCtrl.svelte +3 -29
- package/dist/components/FormerControllers/NumberInputCtrl.svelte +9 -35
- package/dist/components/FormerControllers/PasswordInputCtrl.svelte +4 -23
- package/dist/components/FormerControllers/SwitchCtrl.svelte +3 -20
- package/dist/components/FormerControllers/TextAreaCtrl.svelte +3 -24
- package/dist/components/FormerControllers/TextInputCtrl.svelte +3 -24
- package/dist/components/GlobalStateStore/GlobalStateStoreProvider.svelte +73 -120
- package/dist/components/Gridler/components/Gridler.svelte +338 -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 +520 -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/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 +349 -431
- package/dist/components/VTree/VTreeContextMenu.svelte +21 -33
- package/dist/components/VTree/VTreeEventsDemo.svelte +48 -58
- package/dist/components/VTree/VTreeRow.svelte +13 -65
- package/dist/components/VTree/VTreeSearch.svelte +6 -13
- package/dist/components/VTree/VTreeVirtualViewport.svelte +37 -106
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/llm/COMPONENT_GUIDE.md +240 -0
- package/llm/plans/card_grid.plan.md +305 -0
- package/package.json +48 -46
|
@@ -1,844 +1,523 @@
|
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// ── Filter state (controlled or internal) ─────────────────────────────────
|
|
524
|
-
|
|
525
|
-
let internalFilters = $state<GridColumnFilters>({});
|
|
526
|
-
const resolvedFilters = $derived(filters ?? internalFilters);
|
|
527
|
-
|
|
528
|
-
function handleFilterChange(next: GridColumnFilters) {
|
|
529
|
-
if (filters === undefined) internalFilters = next;
|
|
530
|
-
onFilterChange?.(next);
|
|
531
|
-
onGridEvent?.("filter_changed", undefined, undefined, undefined, { filters: next });
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
// ── Server-side state ──────────────────────────────────────────────────────
|
|
536
|
-
|
|
537
|
-
let serverData = $state<Record<string, unknown>[]>([]);
|
|
538
|
-
let serverTotal = $state(0);
|
|
539
|
-
let serverCursor = $state<string | undefined>(undefined);
|
|
540
|
-
let serverFetchVersion = 0;
|
|
541
|
-
let serverLoading = $state(false);
|
|
542
|
-
let serverAllLoaded = $state(false);
|
|
543
|
-
|
|
544
|
-
$effect(() => { total = serverTotal; });
|
|
545
|
-
$effect(() => { loading = serverLoading; });
|
|
546
|
-
let serverError = $state<string | null>(null);
|
|
547
|
-
|
|
548
|
-
type Adapter = GridlerResolveSpecAdapter | GridlerRestHeaderSpecAdapter;
|
|
549
|
-
|
|
550
|
-
const serverAdapter = $derived.by((): Adapter | null => {
|
|
551
|
-
if (!resolvedUrl || !resolvedSchema || !resolvedEntity) return null;
|
|
552
|
-
const config: GridlerAdapterConfig = {
|
|
553
|
-
url: resolvedUrl,
|
|
554
|
-
token: resolvedToken,
|
|
555
|
-
schema: resolvedSchema,
|
|
556
|
-
entity: resolvedEntity,
|
|
557
|
-
uniqueID: resolvedUniqueID,
|
|
558
|
-
extraOptions: dataSourceOptions?.extraOptions,
|
|
559
|
-
};
|
|
560
|
-
return dataSource === "headerspec"
|
|
561
|
-
? new GridlerRestHeaderSpecAdapter(config)
|
|
562
|
-
: new GridlerResolveSpecAdapter(config);
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
function buildAllFilters(search: string | null): FilterOption[] {
|
|
566
|
-
const base = Object.keys(resolvedFilters).length
|
|
567
|
-
? gridColumnFiltersToFilterOptions(resolvedFilters)
|
|
568
|
-
: [];
|
|
569
|
-
if (!search?.trim()) return base;
|
|
570
|
-
return [...base, ...buildSearchFilters(search, columnsState, searchColumns)];
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
// Compute column fields (dataKey ?? id) merged with hotfields, deduped.
|
|
574
|
-
const resolvedFields = $derived.by(() => {
|
|
575
|
-
const columnFields = columnsState.map((c) => c.dataKey ?? c.id);
|
|
576
|
-
const hotfields = dataSourceOptions?.hotfields ?? [];
|
|
577
|
-
const seen = new Set(columnFields);
|
|
578
|
-
const extra = hotfields.filter((f) => !seen.has(f));
|
|
579
|
-
return [...columnFields, ...extra];
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
// ── Sort state (controlled or internal) ───────────────────────────────────
|
|
583
|
-
|
|
584
|
-
let internalSortOrder = $state<GridColumnSortOrder>({});
|
|
585
|
-
const resolvedSortOrder = $derived(sortOrder ?? internalSortOrder);
|
|
586
|
-
|
|
587
|
-
function handleSortOrderChange(order: GridColumnSortOrder) {
|
|
588
|
-
if (sortOrder === undefined) internalSortOrder = order;
|
|
589
|
-
onSortOrderChange?.(order);
|
|
590
|
-
onGridEvent?.("sort_changed", undefined, undefined, undefined, { sortOrder: order });
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
const resolvedSort = $derived(
|
|
594
|
-
Object.values(resolvedSortOrder).some((d) => d !== "none")
|
|
595
|
-
? sortOrderToSortOptions(resolvedSortOrder)
|
|
596
|
-
: undefined,
|
|
597
|
-
);
|
|
598
|
-
|
|
599
|
-
// ── Local sorted/filtered data ────────────────────────────────────────────
|
|
600
|
-
|
|
601
|
-
const sortedDataState = $derived(applyLocalSort(dataState, resolvedSort ?? [], columnsState));
|
|
602
|
-
const filteredDataState = $derived(applyLocalFilter(sortedDataState, resolvedFilters, columnsState));
|
|
603
|
-
|
|
604
|
-
// Full reset + first page fetch.
|
|
605
|
-
$effect(() => {
|
|
606
|
-
const _adapter = serverAdapter;
|
|
607
|
-
const _sort = resolvedSort;
|
|
608
|
-
const _search = serverSideSearch ? resolvedSearchValue : null;
|
|
609
|
-
const _pageSize = pageSize;
|
|
610
|
-
|
|
611
|
-
// Track resolvedFilters so internal filter changes also trigger a re-fetch.
|
|
612
|
-
const _filters = resolvedFilters;
|
|
613
|
-
// Track manual refresh trigger.
|
|
614
|
-
const _refresh = refreshTrigger;
|
|
615
|
-
|
|
616
|
-
// _refresh is tracked to trigger re-fetch on manual refresh.
|
|
617
|
-
void _refresh;
|
|
618
|
-
if (!_adapter) return;
|
|
619
|
-
|
|
620
|
-
let cancelled = false;
|
|
621
|
-
serverFetchVersion++;
|
|
622
|
-
serverLoading = true;
|
|
623
|
-
serverError = null;
|
|
624
|
-
serverData = [];
|
|
625
|
-
serverTotal = 0;
|
|
626
|
-
serverCursor = undefined;
|
|
627
|
-
serverAllLoaded = false;
|
|
628
|
-
|
|
629
|
-
const columnFilters = Object.keys(_filters).length
|
|
630
|
-
? gridColumnFiltersToFilterOptions(_filters)
|
|
631
|
-
: [];
|
|
632
|
-
const searchFilters = _search?.trim()
|
|
633
|
-
? buildSearchFilters(_search, columnsState, searchColumns)
|
|
634
|
-
: [];
|
|
635
|
-
const allFilters = [...columnFilters, ...searchFilters];
|
|
636
|
-
|
|
637
|
-
_adapter
|
|
638
|
-
.readPage(_pageSize, undefined, _sort, allFilters, untrack(() => resolvedFields))
|
|
639
|
-
.then((result) => {
|
|
640
|
-
if (cancelled) return;
|
|
641
|
-
serverData = result.data;
|
|
642
|
-
serverTotal = result.total;
|
|
643
|
-
serverCursor = result.nextCursor;
|
|
644
|
-
serverAllLoaded = result.data.length < _pageSize || !result.nextCursor;
|
|
645
|
-
onGridEvent?.("load");
|
|
646
|
-
})
|
|
647
|
-
.catch((e) => {
|
|
648
|
-
if (cancelled) return;
|
|
649
|
-
const msg = e instanceof Error ? e.message : "Failed to load data";
|
|
650
|
-
serverError = msg;
|
|
651
|
-
onLoadError?.(msg);
|
|
652
|
-
})
|
|
653
|
-
.finally(() => {
|
|
654
|
-
if (!cancelled) serverLoading = false;
|
|
655
|
-
});
|
|
656
|
-
|
|
657
|
-
return () => {
|
|
658
|
-
cancelled = true;
|
|
659
|
-
};
|
|
660
|
-
});
|
|
661
|
-
|
|
662
|
-
// Append next cursor page.
|
|
663
|
-
async function loadNextPage() {
|
|
664
|
-
const _adapter = serverAdapter;
|
|
665
|
-
if (!_adapter || serverLoading || serverAllLoaded || !serverCursor) return;
|
|
666
|
-
|
|
667
|
-
const cursorSnapshot = serverCursor;
|
|
668
|
-
const versionSnapshot = serverFetchVersion;
|
|
669
|
-
serverLoading = true;
|
|
670
|
-
|
|
671
|
-
const allFilters = buildAllFilters(
|
|
672
|
-
serverSideSearch ? resolvedSearchValue : null,
|
|
673
|
-
);
|
|
674
|
-
|
|
675
|
-
try {
|
|
676
|
-
const result = await _adapter.readPage(
|
|
677
|
-
pageSize,
|
|
678
|
-
cursorSnapshot,
|
|
679
|
-
resolvedSort,
|
|
680
|
-
allFilters,
|
|
681
|
-
resolvedFields,
|
|
682
|
-
);
|
|
683
|
-
if (serverFetchVersion !== versionSnapshot) return;
|
|
684
|
-
serverData = [...serverData, ...result.data];
|
|
685
|
-
serverCursor = result.nextCursor;
|
|
686
|
-
serverAllLoaded =
|
|
687
|
-
result.data.length < pageSize ||
|
|
688
|
-
!result.nextCursor ||
|
|
689
|
-
result.nextCursor === cursorSnapshot;
|
|
690
|
-
onGridEvent?.("page_loaded", undefined, undefined, undefined, { data: result.data, total: serverData.length });
|
|
691
|
-
} catch (e) {
|
|
692
|
-
const msg = e instanceof Error ? e.message : "Failed to load page";
|
|
693
|
-
onLoadError?.(msg);
|
|
694
|
-
} finally {
|
|
695
|
-
serverLoading = false;
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
// Trigger next page when the visible range nears the end of loaded data.
|
|
700
|
-
function handleVisibleRangeChange(range: VisibleRange) {
|
|
701
|
-
untrack(() => {
|
|
702
|
-
if (!isServerMode || serverLoading || serverAllLoaded || !serverCursor)
|
|
703
|
-
return;
|
|
704
|
-
const PREFETCH_THRESHOLD = Math.ceil(pageSize / 2);
|
|
705
|
-
if (range.lastRow >= serverData.length - PREFETCH_THRESHOLD) {
|
|
706
|
-
loadNextPage();
|
|
707
|
-
}
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
// ── Double-click ───────────────────────────────────────────────────────────
|
|
712
|
-
|
|
713
|
-
function resolveRowData(row: number): Record<string, unknown> | undefined {
|
|
714
|
-
return isServerMode ? serverData[row] : filteredDataState[row];
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
function handleCellDblClick(item: Item, cell: GridlerCell) {
|
|
718
|
-
const rowData = resolveRowData(item[1]);
|
|
719
|
-
onCellDblClick?.(item, cell, rowData);
|
|
720
|
-
onRowDblClick?.(item[1], rowData);
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
function handleRowClick(row: number) {
|
|
724
|
-
onRowClick?.(row, resolveRowData(row));
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
function handleRowContextMenu(row: number, x: number, y: number) {
|
|
728
|
-
contextMenuRowData = resolveRowData(row);
|
|
729
|
-
menu?.show("gridler-menu", { x, y, items: buildMenuItems() });
|
|
730
|
-
onRowContextMenu?.(row, contextMenuRowData, x, y);
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
// ── Selection → selectedItems bridge ──────────────────────────────────────
|
|
734
|
-
|
|
735
|
-
let internalSelectedItems = $state<Record<string, unknown>[]>([]);
|
|
736
|
-
|
|
737
|
-
$effect(() => {
|
|
738
|
-
if (_selectedItems === undefined) return;
|
|
739
|
-
internalSelectedItems = Array.isArray(_selectedItems) ? _selectedItems : [];
|
|
740
|
-
});
|
|
741
|
-
|
|
742
|
-
function handleSelectionChange(sel: Selection) {
|
|
743
|
-
let items: Record<string, unknown>[] = [];
|
|
744
|
-
if (sel.type === "row") {
|
|
745
|
-
items = sel.rows
|
|
746
|
-
.map((i) => resolveRowData(i))
|
|
747
|
-
.filter((r): r is Record<string, unknown> => r !== undefined);
|
|
748
|
-
} else if (sel.type === "cell") {
|
|
749
|
-
const row = resolveRowData(sel.item[1]);
|
|
750
|
-
if (row) items = [row];
|
|
751
|
-
} else if (sel.type === "range") {
|
|
752
|
-
const minRow = Math.min(sel.start[1], sel.end[1]);
|
|
753
|
-
const maxRow = Math.max(sel.start[1], sel.end[1]);
|
|
754
|
-
for (let i = minRow; i <= maxRow; i++) {
|
|
755
|
-
const row = resolveRowData(i);
|
|
756
|
-
if (row) items.push(row);
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
internalSelectedItems = items;
|
|
760
|
-
if (items.length > 0) onSelectedItemsChange?.(items);
|
|
761
|
-
onSelectionChange?.(sel, items.length > 0 ? items : undefined);
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
// ── Settings change event ─────────────────────────────────────────────────
|
|
765
|
-
|
|
766
|
-
let lastSettings = $state<typeof settings>(undefined);
|
|
767
|
-
$effect(() => {
|
|
768
|
-
if (settings === untrack(() => lastSettings)) return;
|
|
769
|
-
lastSettings = settings;
|
|
770
|
-
onGridEvent?.("settings_changed", undefined, undefined, undefined, { settings });
|
|
771
|
-
});
|
|
772
|
-
|
|
773
|
-
// ── Cell content ───────────────────────────────────────────────────────────
|
|
774
|
-
|
|
775
|
-
const serverGetCellContent = $derived(makeGetCellContent(serverData, columnsState));
|
|
776
|
-
const localGetCellContent = $derived(makeGetCellContent(filteredDataState, columnsState));
|
|
777
|
-
|
|
778
|
-
const resolvedGetCellContent = $derived.by(() => {
|
|
779
|
-
if (getCellContent) return getCellContent;
|
|
780
|
-
if (isServerMode) return serverGetCellContent;
|
|
781
|
-
return localGetCellContent;
|
|
782
|
-
});
|
|
783
|
-
|
|
784
|
-
// ── Row count ──────────────────────────────────────────────────────────────
|
|
785
|
-
|
|
786
|
-
const resolvedRows = $derived.by(() => {
|
|
787
|
-
if (isServerMode) return serverTotal;
|
|
788
|
-
if (rows != null) return rows;
|
|
789
|
-
return filteredDataState.length;
|
|
790
|
-
});
|
|
791
|
-
|
|
792
|
-
// ── Cell editing ───────────────────────────────────────────────────────────
|
|
793
|
-
|
|
794
|
-
async function applyEdit(item: Item, value: GridlerCell) {
|
|
795
|
-
if (getCellContent || isServerMode) return;
|
|
796
|
-
const [col, row] = item;
|
|
797
|
-
const colDef = columnsState[col];
|
|
798
|
-
const key = colDef?.dataKey ?? colDef?.id;
|
|
799
|
-
if (!key) return;
|
|
800
|
-
const existing = filteredDataState[row];
|
|
801
|
-
if (!existing) return;
|
|
802
|
-
const originalIndex = dataState.indexOf(existing);
|
|
803
|
-
if (originalIndex === -1) return;
|
|
804
|
-
const next = dataState.slice();
|
|
805
|
-
next[originalIndex] = {
|
|
806
|
-
...existing,
|
|
807
|
-
[key]: value.data ?? value.displayData,
|
|
808
|
-
};
|
|
809
|
-
dataState = next;
|
|
810
|
-
await onDataChange?.(next);
|
|
811
|
-
onGridEvent?.("data_changed", next[originalIndex], columnsState[col], undefined, { data: next });
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
async function handleGridMenuClick(d?: {
|
|
815
|
-
x: number;
|
|
816
|
-
y: number;
|
|
817
|
-
item?: Record<string, unknown>;
|
|
818
|
-
column?: GridlerColumn;
|
|
819
|
-
}) {
|
|
820
|
-
contextMenuRowData = undefined;
|
|
821
|
-
if (onMenuClick) {
|
|
822
|
-
await onMenuClick(
|
|
823
|
-
d?.item,
|
|
824
|
-
d?.column,
|
|
825
|
-
d ? { x: d.x, y: d.y } : undefined,
|
|
826
|
-
);
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
|
-
if (!d) return;
|
|
830
|
-
menu?.show("gridler-menu", { x: d.x, y: d.y, items: buildMenuItems() });
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
// ── Public API ────────────────────────────────────────────────────────────
|
|
834
|
-
|
|
835
|
-
export function getSelectedRows(): Record<string, unknown>[] {
|
|
836
|
-
return internalSelectedItems;
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
export function getLoadedData(): Record<string, unknown>[] {
|
|
840
|
-
return isServerMode ? serverData : filteredDataState;
|
|
841
|
-
}
|
|
1
|
+
<script lang="ts">import { untrack } from "svelte";
|
|
2
|
+
import Gridler from "./Gridler.svelte";
|
|
3
|
+
import GridlerSearchToggle from "./GridlerSearchToggle.svelte";
|
|
4
|
+
import BetterMenu from "../../BetterMenu/BetterMenu.svelte";
|
|
5
|
+
import { GridlerResolveSpecAdapter } from "../adapters/GridlerResolveSpecAdapter.js";
|
|
6
|
+
import { GridlerRestHeaderSpecAdapter } from "../adapters/GridlerRestHeaderSpecAdapter.js";
|
|
7
|
+
import { reorderColumns } from "../utils/columns.js";
|
|
8
|
+
import { applyLocalFilter, buildSearchFilters, gridColumnFiltersToFilterOptions } from "../utils/filters.js";
|
|
9
|
+
import { applyLocalSort, sortOrderToSortOptions } from "../utils/sort.js";
|
|
10
|
+
import { makeGetCellContent } from "../utils/cellContent.js";
|
|
11
|
+
let { columns, data, getCellContent, rows, manageColumns = true, onColumnsChange, searchValue, defaultSearchValue = "", onSearchValueChange, onDataChange, dataSource, dataSourceOptions, pageSize = 200, uniqueID, serverSideSearch = false, searchColumns, onLoadError, total = $bindable(0), loading = $bindable(false), filters, onFilterChange, sortOrder, onSortOrderChange, selectedItems: _selectedItems, onSelectedItemsChange, settings, fixedColumns = 0, rowMarkers, showSearch, height = 500, onColumnMoved, onCellEdited, onCellDblClick, onRowDblClick, onRowClick, onRowContextMenu, onMenuClick, onSelectionChange, onCellEvent, onGridEvent, menuItems, onMenuItemSelect, children, ...rest } = $props();
|
|
12
|
+
// ── Column management ─────────────────────────────────────────────────────
|
|
13
|
+
// eslint-disable-next-line svelte/no-unused-svelte-ignore
|
|
14
|
+
// svelte-ignore state_referenced_locally -- intentional initial capture; $effect below syncs changes
|
|
15
|
+
let columnsState = $state(columns.slice());
|
|
16
|
+
const columnsSignature = $derived.by(() => columns.map((c) => `${c.id}:${c.title}:${c.width}`).join("|"));
|
|
17
|
+
// eslint-disable-next-line svelte/no-unused-svelte-ignore
|
|
18
|
+
// svelte-ignore state_referenced_locally -- intentional initial capture; $effect below syncs changes
|
|
19
|
+
let lastColumnsSignature = $state(columnsSignature);
|
|
20
|
+
$effect(() => {
|
|
21
|
+
if (columnsSignature === untrack(() => lastColumnsSignature)) return;
|
|
22
|
+
columnsState = columns.slice();
|
|
23
|
+
lastColumnsSignature = columnsSignature;
|
|
24
|
+
});
|
|
25
|
+
// ── Local data management (non-server mode) ────────────────────────────────
|
|
26
|
+
// eslint-disable-next-line svelte/no-unused-svelte-ignore
|
|
27
|
+
// svelte-ignore state_referenced_locally -- intentional initial capture; $effect below syncs changes
|
|
28
|
+
let dataState = $state(Array.isArray(data) ? data.slice() : []);
|
|
29
|
+
// eslint-disable-next-line svelte/no-unused-svelte-ignore
|
|
30
|
+
// svelte-ignore state_referenced_locally -- intentional initial capture; $effect below syncs changes
|
|
31
|
+
let lastDataRef = $state(data);
|
|
32
|
+
$effect(() => {
|
|
33
|
+
if (!data) return;
|
|
34
|
+
if (data === untrack(() => lastDataRef)) return;
|
|
35
|
+
lastDataRef = data;
|
|
36
|
+
if (typeof data === "function") {
|
|
37
|
+
data().then((result) => {
|
|
38
|
+
dataState = result;
|
|
39
|
+
onGridEvent?.("load");
|
|
40
|
+
}).catch((e) => {
|
|
41
|
+
const msg = e instanceof Error ? e.message : "Failed to load data";
|
|
42
|
+
onLoadError?.(msg);
|
|
43
|
+
});
|
|
44
|
+
} else {
|
|
45
|
+
dataState = data.slice();
|
|
46
|
+
onGridEvent?.("load");
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
// ── Server config (resolved from dataSource / dataSourceOptions) ───────────
|
|
50
|
+
const resolvedUrl = $derived(dataSourceOptions?.url);
|
|
51
|
+
const resolvedToken = $derived(dataSourceOptions?.authToken);
|
|
52
|
+
const resolvedSchema = $derived(dataSourceOptions?.schema);
|
|
53
|
+
const resolvedEntity = $derived(dataSourceOptions?.entity);
|
|
54
|
+
const resolvedUniqueID = $derived(dataSourceOptions?.uniqueID ?? uniqueID ?? "id");
|
|
55
|
+
const isServerMode = $derived(!!(dataSource && dataSource !== "local" && dataSource !== "custom" && resolvedUrl && resolvedSchema && resolvedEntity));
|
|
56
|
+
// ── Search visibility ──────────────────────────────────────────────────────
|
|
57
|
+
const resolvedShowSearch = $derived(showSearch ?? settings?.showSearch ?? false);
|
|
58
|
+
// eslint-disable-next-line svelte/no-unused-svelte-ignore
|
|
59
|
+
// svelte-ignore state_referenced_locally -- intentional initial capture
|
|
60
|
+
let showSearchBar = $state(resolvedShowSearch);
|
|
61
|
+
function toggleSearch() {
|
|
62
|
+
showSearchBar = !showSearchBar;
|
|
63
|
+
if (!showSearchBar) emitSearchValue("");
|
|
64
|
+
}
|
|
65
|
+
function closeSearch() {
|
|
66
|
+
showSearchBar = false;
|
|
67
|
+
emitSearchValue("");
|
|
68
|
+
}
|
|
69
|
+
// ── Search ─────────────────────────────────────────────────────────────────
|
|
70
|
+
// eslint-disable-next-line svelte/no-unused-svelte-ignore
|
|
71
|
+
// svelte-ignore state_referenced_locally -- intentional initial capture; $effect below syncs controlled value
|
|
72
|
+
let internalSearchValue = $state(defaultSearchValue);
|
|
73
|
+
$effect(() => {
|
|
74
|
+
if (searchValue === undefined) return;
|
|
75
|
+
internalSearchValue = searchValue;
|
|
76
|
+
});
|
|
77
|
+
const resolvedSearchValue = $derived(searchValue ?? internalSearchValue);
|
|
78
|
+
function emitSearchValue(value) {
|
|
79
|
+
if (searchValue === undefined) {
|
|
80
|
+
internalSearchValue = value;
|
|
81
|
+
}
|
|
82
|
+
onSearchValueChange?.(value);
|
|
83
|
+
onGridEvent?.("search_changed", undefined, undefined, undefined, { searchValue: value });
|
|
84
|
+
}
|
|
85
|
+
// ── Context menu ───────────────────────────────────────────────────────────
|
|
86
|
+
let refreshTrigger = $state(0);
|
|
87
|
+
let contextMenuRowData = $state(undefined);
|
|
88
|
+
let menu = $state(null);
|
|
89
|
+
function refresh() {
|
|
90
|
+
if (isServerMode) {
|
|
91
|
+
refreshTrigger++;
|
|
92
|
+
} else if (typeof data === "function") {
|
|
93
|
+
data().then((result) => {
|
|
94
|
+
dataState = result;
|
|
95
|
+
}).catch((e) => {
|
|
96
|
+
const msg = e instanceof Error ? e.message : "Failed to load data";
|
|
97
|
+
onLoadError?.(msg);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function buildMenuItems() {
|
|
102
|
+
const items = [];
|
|
103
|
+
items.push({
|
|
104
|
+
label: "Refresh",
|
|
105
|
+
onClick: () => refresh()
|
|
106
|
+
});
|
|
107
|
+
const _hasSearch = resolvedSearchValue.length > 0;
|
|
108
|
+
const _hasFilters = Object.keys(resolvedFilters).length > 0;
|
|
109
|
+
const _hasSort = Object.values(resolvedSortOrder).some((d) => d !== "none");
|
|
110
|
+
if (_hasSearch || _hasFilters || _hasSort) {
|
|
111
|
+
items.push({ isDivider: true });
|
|
112
|
+
if (_hasSearch) items.push({
|
|
113
|
+
label: "Clear Search",
|
|
114
|
+
onClick: () => emitSearchValue("")
|
|
115
|
+
});
|
|
116
|
+
if (_hasFilters) items.push({
|
|
117
|
+
label: "Clear Filters",
|
|
118
|
+
onClick: () => handleFilterChange({})
|
|
119
|
+
});
|
|
120
|
+
if (_hasSort) items.push({
|
|
121
|
+
label: "Clear Sort",
|
|
122
|
+
onClick: () => handleSortOrderChange({})
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
if (menuItems?.length) {
|
|
126
|
+
items.push({ isDivider: true });
|
|
127
|
+
for (const item of menuItems) {
|
|
128
|
+
if (item.kind === "separator") {
|
|
129
|
+
items.push({ isDivider: true });
|
|
130
|
+
} else {
|
|
131
|
+
items.push({
|
|
132
|
+
id: item.id,
|
|
133
|
+
label: item.label,
|
|
134
|
+
icon: item.icon,
|
|
135
|
+
onClick: () => {
|
|
136
|
+
item.onselect?.(contextMenuRowData);
|
|
137
|
+
onMenuItemSelect?.(item, contextMenuRowData);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return items;
|
|
144
|
+
}
|
|
145
|
+
let filterPanelColId = $state("");
|
|
146
|
+
let filterPanelColTitle = $state("");
|
|
147
|
+
let filterPanelOp = $state("contains");
|
|
148
|
+
let filterPanelValue = $state("");
|
|
149
|
+
let filterPanelValue2 = $state("");
|
|
150
|
+
// Snippet reference captured from the template — assigned via {(filterRenderer ??= filterForm, '')}
|
|
151
|
+
let filterRenderer = $state(null);
|
|
152
|
+
function openFilterPanel(colId, colTitle, x, y) {
|
|
153
|
+
const existing = resolvedFilters[colId];
|
|
154
|
+
filterPanelColId = colId;
|
|
155
|
+
filterPanelColTitle = colTitle;
|
|
156
|
+
filterPanelOp = existing?.op ?? "contains";
|
|
157
|
+
filterPanelValue = existing?.value ?? "";
|
|
158
|
+
filterPanelValue2 = existing?.value2 ?? "";
|
|
159
|
+
menu?.show("gridler-filter-panel", {
|
|
160
|
+
x,
|
|
161
|
+
y,
|
|
162
|
+
renderer: filterRenderer
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function applyFilterPanel() {
|
|
166
|
+
const needsValue = filterPanelOp !== "isNull" && filterPanelOp !== "isNotNull";
|
|
167
|
+
if (needsValue && !filterPanelValue) {
|
|
168
|
+
const next = { ...resolvedFilters };
|
|
169
|
+
delete next[filterPanelColId];
|
|
170
|
+
handleFilterChange(next);
|
|
171
|
+
} else {
|
|
172
|
+
const entry = {
|
|
173
|
+
value: filterPanelValue,
|
|
174
|
+
op: filterPanelOp
|
|
175
|
+
};
|
|
176
|
+
if ((filterPanelOp === "between" || filterPanelOp === "notBetween") && filterPanelValue2) {
|
|
177
|
+
entry.value2 = filterPanelValue2;
|
|
178
|
+
}
|
|
179
|
+
handleFilterChange({
|
|
180
|
+
...resolvedFilters,
|
|
181
|
+
[filterPanelColId]: entry
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
menu?.hide("gridler-filter-panel");
|
|
185
|
+
}
|
|
186
|
+
// ── Column header context menu ─────────────────────────────────────────────
|
|
187
|
+
function handleHeaderContextMenu(colIndex, x, y) {
|
|
188
|
+
const column = columnsState[colIndex];
|
|
189
|
+
if (!column) return;
|
|
190
|
+
const colId = column.id;
|
|
191
|
+
const currentSort = resolvedSortOrder[colId] ?? "none";
|
|
192
|
+
const currentFilter = resolvedFilters[colId];
|
|
193
|
+
const isSortable = column.disableSort !== true;
|
|
194
|
+
const isFilterable = column.disableFilter !== true;
|
|
195
|
+
const items = [];
|
|
196
|
+
// Column name (non-clickable header label)
|
|
197
|
+
items.push({ label: column.title });
|
|
198
|
+
items.push({ isDivider: true });
|
|
199
|
+
// Sort section
|
|
200
|
+
if (isSortable) {
|
|
201
|
+
items.push({
|
|
202
|
+
label: `${currentSort === "asc" ? "✓ " : ""}Sort Ascending`,
|
|
203
|
+
onClick: () => handleSortOrderChange({
|
|
204
|
+
...resolvedSortOrder,
|
|
205
|
+
[colId]: currentSort === "asc" ? "none" : "asc"
|
|
206
|
+
})
|
|
207
|
+
});
|
|
208
|
+
items.push({
|
|
209
|
+
label: `${currentSort === "desc" ? "✓ " : ""}Sort Descending`,
|
|
210
|
+
onClick: () => handleSortOrderChange({
|
|
211
|
+
...resolvedSortOrder,
|
|
212
|
+
[colId]: currentSort === "desc" ? "none" : "desc"
|
|
213
|
+
})
|
|
214
|
+
});
|
|
215
|
+
if (currentSort !== "none") {
|
|
216
|
+
items.push({
|
|
217
|
+
label: "Clear Sort",
|
|
218
|
+
onClick: () => {
|
|
219
|
+
const next = {
|
|
220
|
+
...resolvedSortOrder,
|
|
221
|
+
[colId]: "none"
|
|
222
|
+
};
|
|
223
|
+
handleSortOrderChange(next);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// Filter section
|
|
229
|
+
if (isFilterable) {
|
|
230
|
+
items.push({ isDivider: true });
|
|
231
|
+
if (currentFilter) {
|
|
232
|
+
items.push({ label: `Filter: ${currentFilter.value}` });
|
|
233
|
+
items.push({
|
|
234
|
+
label: "Edit Filter…",
|
|
235
|
+
onClick: () => openFilterPanel(colId, column.title, x, y)
|
|
236
|
+
});
|
|
237
|
+
items.push({
|
|
238
|
+
label: "Clear Filter",
|
|
239
|
+
onClick: () => {
|
|
240
|
+
const next = { ...resolvedFilters };
|
|
241
|
+
delete next[colId];
|
|
242
|
+
handleFilterChange(next);
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
} else {
|
|
246
|
+
items.push({
|
|
247
|
+
label: "Set Filter…",
|
|
248
|
+
onClick: () => openFilterPanel(colId, column.title, x, y)
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Search section
|
|
253
|
+
if (resolvedSearchValue) {
|
|
254
|
+
items.push({ isDivider: true });
|
|
255
|
+
items.push({ label: `Search: "${resolvedSearchValue}"` });
|
|
256
|
+
items.push({
|
|
257
|
+
label: "Clear Search",
|
|
258
|
+
onClick: () => emitSearchValue("")
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
menu?.show("gridler-column-menu", {
|
|
262
|
+
x,
|
|
263
|
+
y,
|
|
264
|
+
items
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
// ── Column reorder ─────────────────────────────────────────────────────────
|
|
268
|
+
function handleColumnReorder(startIndex, endIndex) {
|
|
269
|
+
const next = reorderColumns(columnsState, startIndex, endIndex);
|
|
270
|
+
if (next === columnsState) return;
|
|
271
|
+
columnsState = next;
|
|
272
|
+
onColumnsChange?.(next);
|
|
273
|
+
}
|
|
274
|
+
// ── Filter state (controlled or internal) ─────────────────────────────────
|
|
275
|
+
let internalFilters = $state({});
|
|
276
|
+
const resolvedFilters = $derived(filters ?? internalFilters);
|
|
277
|
+
function handleFilterChange(next) {
|
|
278
|
+
if (filters === undefined) internalFilters = next;
|
|
279
|
+
onFilterChange?.(next);
|
|
280
|
+
onGridEvent?.("filter_changed", undefined, undefined, undefined, { filters: next });
|
|
281
|
+
}
|
|
282
|
+
// ── Server-side state ──────────────────────────────────────────────────────
|
|
283
|
+
let serverData = $state([]);
|
|
284
|
+
let serverTotal = $state(0);
|
|
285
|
+
let serverCursor = $state(undefined);
|
|
286
|
+
let serverFetchVersion = 0;
|
|
287
|
+
let serverLoading = $state(false);
|
|
288
|
+
let serverAllLoaded = $state(false);
|
|
289
|
+
$effect(() => {
|
|
290
|
+
total = serverTotal;
|
|
291
|
+
});
|
|
292
|
+
$effect(() => {
|
|
293
|
+
loading = serverLoading;
|
|
294
|
+
});
|
|
295
|
+
let serverError = $state(null);
|
|
296
|
+
const serverAdapter = $derived.by(() => {
|
|
297
|
+
if (!resolvedUrl || !resolvedSchema || !resolvedEntity) return null;
|
|
298
|
+
const config = {
|
|
299
|
+
url: resolvedUrl,
|
|
300
|
+
token: resolvedToken,
|
|
301
|
+
schema: resolvedSchema,
|
|
302
|
+
entity: resolvedEntity,
|
|
303
|
+
uniqueID: resolvedUniqueID,
|
|
304
|
+
extraOptions: dataSourceOptions?.extraOptions
|
|
305
|
+
};
|
|
306
|
+
return dataSource === "headerspec" ? new GridlerRestHeaderSpecAdapter(config) : new GridlerResolveSpecAdapter(config);
|
|
307
|
+
});
|
|
308
|
+
function buildAllFilters(search) {
|
|
309
|
+
const base = Object.keys(resolvedFilters).length ? gridColumnFiltersToFilterOptions(resolvedFilters) : [];
|
|
310
|
+
if (!search?.trim()) return base;
|
|
311
|
+
return [...base, ...buildSearchFilters(search, columnsState, searchColumns)];
|
|
312
|
+
}
|
|
313
|
+
// Compute column fields (dataKey ?? id) merged with hotfields, deduped.
|
|
314
|
+
const resolvedFields = $derived.by(() => {
|
|
315
|
+
const columnFields = columnsState.map((c) => c.dataKey ?? c.id);
|
|
316
|
+
const hotfields = dataSourceOptions?.hotfields ?? [];
|
|
317
|
+
const seen = new Set(columnFields);
|
|
318
|
+
const extra = hotfields.filter((f) => !seen.has(f));
|
|
319
|
+
return [...columnFields, ...extra];
|
|
320
|
+
});
|
|
321
|
+
// ── Sort state (controlled or internal) ───────────────────────────────────
|
|
322
|
+
let internalSortOrder = $state({});
|
|
323
|
+
const resolvedSortOrder = $derived(sortOrder ?? internalSortOrder);
|
|
324
|
+
function handleSortOrderChange(order) {
|
|
325
|
+
if (sortOrder === undefined) internalSortOrder = order;
|
|
326
|
+
onSortOrderChange?.(order);
|
|
327
|
+
onGridEvent?.("sort_changed", undefined, undefined, undefined, { sortOrder: order });
|
|
328
|
+
}
|
|
329
|
+
const resolvedSort = $derived(Object.values(resolvedSortOrder).some((d) => d !== "none") ? sortOrderToSortOptions(resolvedSortOrder) : undefined);
|
|
330
|
+
// ── Local sorted/filtered data ────────────────────────────────────────────
|
|
331
|
+
const sortedDataState = $derived(applyLocalSort(dataState, resolvedSort ?? [], columnsState));
|
|
332
|
+
const filteredDataState = $derived(applyLocalFilter(sortedDataState, resolvedFilters, columnsState));
|
|
333
|
+
// Full reset + first page fetch.
|
|
334
|
+
$effect(() => {
|
|
335
|
+
const _adapter = serverAdapter;
|
|
336
|
+
const _sort = resolvedSort;
|
|
337
|
+
const _search = serverSideSearch ? resolvedSearchValue : null;
|
|
338
|
+
const _pageSize = pageSize;
|
|
339
|
+
// Track resolvedFilters so internal filter changes also trigger a re-fetch.
|
|
340
|
+
const _filters = resolvedFilters;
|
|
341
|
+
// Track manual refresh trigger.
|
|
342
|
+
const _refresh = refreshTrigger;
|
|
343
|
+
// _refresh is tracked to trigger re-fetch on manual refresh.
|
|
344
|
+
void _refresh;
|
|
345
|
+
if (!_adapter) return;
|
|
346
|
+
let cancelled = false;
|
|
347
|
+
serverFetchVersion++;
|
|
348
|
+
serverLoading = true;
|
|
349
|
+
serverError = null;
|
|
350
|
+
serverData = [];
|
|
351
|
+
serverTotal = 0;
|
|
352
|
+
serverCursor = undefined;
|
|
353
|
+
serverAllLoaded = false;
|
|
354
|
+
const columnFilters = Object.keys(_filters).length ? gridColumnFiltersToFilterOptions(_filters) : [];
|
|
355
|
+
const searchFilters = _search?.trim() ? buildSearchFilters(_search, columnsState, searchColumns) : [];
|
|
356
|
+
const allFilters = [...columnFilters, ...searchFilters];
|
|
357
|
+
_adapter.readPage(_pageSize, undefined, _sort, allFilters, untrack(() => resolvedFields)).then((result) => {
|
|
358
|
+
if (cancelled) return;
|
|
359
|
+
serverData = result.data;
|
|
360
|
+
serverTotal = result.total;
|
|
361
|
+
serverCursor = result.nextCursor;
|
|
362
|
+
serverAllLoaded = result.data.length < _pageSize || !result.nextCursor;
|
|
363
|
+
onGridEvent?.("load");
|
|
364
|
+
}).catch((e) => {
|
|
365
|
+
if (cancelled) return;
|
|
366
|
+
const msg = e instanceof Error ? e.message : "Failed to load data";
|
|
367
|
+
serverError = msg;
|
|
368
|
+
onLoadError?.(msg);
|
|
369
|
+
}).finally(() => {
|
|
370
|
+
if (!cancelled) serverLoading = false;
|
|
371
|
+
});
|
|
372
|
+
return () => {
|
|
373
|
+
cancelled = true;
|
|
374
|
+
};
|
|
375
|
+
});
|
|
376
|
+
// Append next cursor page.
|
|
377
|
+
async function loadNextPage() {
|
|
378
|
+
const _adapter = serverAdapter;
|
|
379
|
+
if (!_adapter || serverLoading || serverAllLoaded || !serverCursor) return;
|
|
380
|
+
const cursorSnapshot = serverCursor;
|
|
381
|
+
const versionSnapshot = serverFetchVersion;
|
|
382
|
+
serverLoading = true;
|
|
383
|
+
const allFilters = buildAllFilters(serverSideSearch ? resolvedSearchValue : null);
|
|
384
|
+
try {
|
|
385
|
+
const result = await _adapter.readPage(pageSize, cursorSnapshot, resolvedSort, allFilters, resolvedFields);
|
|
386
|
+
if (serverFetchVersion !== versionSnapshot) return;
|
|
387
|
+
serverData = [...serverData, ...result.data];
|
|
388
|
+
serverCursor = result.nextCursor;
|
|
389
|
+
serverAllLoaded = result.data.length < pageSize || !result.nextCursor || result.nextCursor === cursorSnapshot;
|
|
390
|
+
onGridEvent?.("page_loaded", undefined, undefined, undefined, {
|
|
391
|
+
data: result.data,
|
|
392
|
+
total: serverData.length
|
|
393
|
+
});
|
|
394
|
+
} catch (e) {
|
|
395
|
+
const msg = e instanceof Error ? e.message : "Failed to load page";
|
|
396
|
+
onLoadError?.(msg);
|
|
397
|
+
} finally {
|
|
398
|
+
serverLoading = false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// Trigger next page when the visible range nears the end of loaded data.
|
|
402
|
+
function handleVisibleRangeChange(range) {
|
|
403
|
+
untrack(() => {
|
|
404
|
+
if (!isServerMode || serverLoading || serverAllLoaded || !serverCursor) return;
|
|
405
|
+
const PREFETCH_THRESHOLD = Math.ceil(pageSize / 2);
|
|
406
|
+
if (range.lastRow >= serverData.length - PREFETCH_THRESHOLD) {
|
|
407
|
+
loadNextPage();
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
// ── Double-click ───────────────────────────────────────────────────────────
|
|
412
|
+
function resolveRowData(row) {
|
|
413
|
+
return isServerMode ? serverData[row] : filteredDataState[row];
|
|
414
|
+
}
|
|
415
|
+
function handleCellDblClick(item, cell) {
|
|
416
|
+
const rowData = resolveRowData(item[1]);
|
|
417
|
+
onCellDblClick?.(item, cell, rowData);
|
|
418
|
+
onRowDblClick?.(item[1], rowData);
|
|
419
|
+
}
|
|
420
|
+
function handleRowClick(row) {
|
|
421
|
+
onRowClick?.(row, resolveRowData(row));
|
|
422
|
+
}
|
|
423
|
+
function handleRowContextMenu(row, x, y) {
|
|
424
|
+
contextMenuRowData = resolveRowData(row);
|
|
425
|
+
menu?.show("gridler-menu", {
|
|
426
|
+
x,
|
|
427
|
+
y,
|
|
428
|
+
items: buildMenuItems()
|
|
429
|
+
});
|
|
430
|
+
onRowContextMenu?.(row, contextMenuRowData, x, y);
|
|
431
|
+
}
|
|
432
|
+
// ── Selection → selectedItems bridge ──────────────────────────────────────
|
|
433
|
+
let internalSelectedItems = $state([]);
|
|
434
|
+
$effect(() => {
|
|
435
|
+
if (_selectedItems === undefined) return;
|
|
436
|
+
internalSelectedItems = Array.isArray(_selectedItems) ? _selectedItems : [];
|
|
437
|
+
});
|
|
438
|
+
function handleSelectionChange(sel) {
|
|
439
|
+
let items = [];
|
|
440
|
+
if (sel.type === "row") {
|
|
441
|
+
items = sel.rows.map((i) => resolveRowData(i)).filter((r) => r !== undefined);
|
|
442
|
+
} else if (sel.type === "cell") {
|
|
443
|
+
const row = resolveRowData(sel.item[1]);
|
|
444
|
+
if (row) items = [row];
|
|
445
|
+
} else if (sel.type === "range") {
|
|
446
|
+
const minRow = Math.min(sel.start[1], sel.end[1]);
|
|
447
|
+
const maxRow = Math.max(sel.start[1], sel.end[1]);
|
|
448
|
+
for (let i = minRow; i <= maxRow; i++) {
|
|
449
|
+
const row = resolveRowData(i);
|
|
450
|
+
if (row) items.push(row);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
internalSelectedItems = items;
|
|
454
|
+
if (items.length > 0) onSelectedItemsChange?.(items);
|
|
455
|
+
onSelectionChange?.(sel, items.length > 0 ? items : undefined);
|
|
456
|
+
}
|
|
457
|
+
// ── Settings change event ─────────────────────────────────────────────────
|
|
458
|
+
let lastSettings = $state(undefined);
|
|
459
|
+
$effect(() => {
|
|
460
|
+
if (settings === untrack(() => lastSettings)) return;
|
|
461
|
+
lastSettings = settings;
|
|
462
|
+
onGridEvent?.("settings_changed", undefined, undefined, undefined, { settings });
|
|
463
|
+
});
|
|
464
|
+
// ── Cell content ───────────────────────────────────────────────────────────
|
|
465
|
+
const serverGetCellContent = $derived(makeGetCellContent(serverData, columnsState));
|
|
466
|
+
const localGetCellContent = $derived(makeGetCellContent(filteredDataState, columnsState));
|
|
467
|
+
const resolvedGetCellContent = $derived.by(() => {
|
|
468
|
+
if (getCellContent) return getCellContent;
|
|
469
|
+
if (isServerMode) return serverGetCellContent;
|
|
470
|
+
return localGetCellContent;
|
|
471
|
+
});
|
|
472
|
+
// ── Row count ──────────────────────────────────────────────────────────────
|
|
473
|
+
const resolvedRows = $derived.by(() => {
|
|
474
|
+
if (isServerMode) return serverTotal;
|
|
475
|
+
if (rows != null) return rows;
|
|
476
|
+
return filteredDataState.length;
|
|
477
|
+
});
|
|
478
|
+
// ── Cell editing ───────────────────────────────────────────────────────────
|
|
479
|
+
async function applyEdit(item, value) {
|
|
480
|
+
if (getCellContent || isServerMode) return;
|
|
481
|
+
const [col, row] = item;
|
|
482
|
+
const colDef = columnsState[col];
|
|
483
|
+
const key = colDef?.dataKey ?? colDef?.id;
|
|
484
|
+
if (!key) return;
|
|
485
|
+
const existing = filteredDataState[row];
|
|
486
|
+
if (!existing) return;
|
|
487
|
+
const originalIndex = dataState.indexOf(existing);
|
|
488
|
+
if (originalIndex === -1) return;
|
|
489
|
+
const next = dataState.slice();
|
|
490
|
+
next[originalIndex] = {
|
|
491
|
+
...existing,
|
|
492
|
+
[key]: value.data ?? value.displayData
|
|
493
|
+
};
|
|
494
|
+
dataState = next;
|
|
495
|
+
await onDataChange?.(next);
|
|
496
|
+
onGridEvent?.("data_changed", next[originalIndex], columnsState[col], undefined, { data: next });
|
|
497
|
+
}
|
|
498
|
+
async function handleGridMenuClick(d) {
|
|
499
|
+
contextMenuRowData = undefined;
|
|
500
|
+
if (onMenuClick) {
|
|
501
|
+
await onMenuClick(d?.item, d?.column, d ? {
|
|
502
|
+
x: d.x,
|
|
503
|
+
y: d.y
|
|
504
|
+
} : undefined);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (!d) return;
|
|
508
|
+
menu?.show("gridler-menu", {
|
|
509
|
+
x: d.x,
|
|
510
|
+
y: d.y,
|
|
511
|
+
items: buildMenuItems()
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
// ── Public API ────────────────────────────────────────────────────────────
|
|
515
|
+
export function getSelectedRows() {
|
|
516
|
+
return internalSelectedItems;
|
|
517
|
+
}
|
|
518
|
+
export function getLoadedData() {
|
|
519
|
+
return isServerMode ? serverData : filteredDataState;
|
|
520
|
+
}
|
|
842
521
|
</script>
|
|
843
522
|
|
|
844
523
|
<BetterMenu bind:this={menu}>
|