@warkypublic/svelix 0.1.46 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/README.md +93 -33
  2. package/dist/components/BetterMenu/BetterMenu.svelte +21 -37
  3. package/dist/components/BetterMenu/BetterMenuAsyncButton.svelte +21 -31
  4. package/dist/components/BetterMenu/BetterMenuPreview.svelte +27 -29
  5. package/dist/components/BetterMenu/MenuRenderer.svelte +40 -51
  6. package/dist/components/Boxer/Boxer.svelte +492 -602
  7. package/dist/components/Boxer/BoxerTarget.svelte +6 -46
  8. package/dist/components/Boxer/BoxerTarget.svelte.d.ts +1 -0
  9. package/dist/components/Boxer/types.d.ts +0 -1
  10. package/dist/components/Button.svelte +19 -43
  11. package/dist/components/ButtonPreview.svelte +2 -11
  12. package/dist/components/CardGrid/CardGrid.svelte +489 -614
  13. package/dist/components/CardGrid/CardGrid.svelte.d.ts +4 -0
  14. package/dist/components/CardGrid/CardGridFilterPanel.svelte +111 -90
  15. package/dist/components/CardGrid/DefaultCard.svelte +86 -97
  16. package/dist/components/CardGrid/DefaultCard.svelte.d.ts +1 -0
  17. package/dist/components/CardGrid/ImageCardStory.svelte +87 -36
  18. package/dist/components/CardGrid/cardGrid.d.ts +24 -0
  19. package/dist/components/CardGrid/cardGrid.js +20 -0
  20. package/dist/components/ContentEditor/CollaboraInsertTextDemo.svelte +5 -10
  21. package/dist/components/ContentEditor/ContentEditor.svelte +24 -50
  22. package/dist/components/ContentEditor/subcomponents/AudioPlayer.svelte +17 -17
  23. package/dist/components/ContentEditor/subcomponents/CollaboraEditor.svelte +244 -309
  24. package/dist/components/ContentEditor/subcomponents/EmailViewer.svelte +2 -3
  25. package/dist/components/ContentEditor/subcomponents/ImageViewer.svelte +14 -15
  26. package/dist/components/ContentEditor/subcomponents/MarkdownViewer.svelte +74 -90
  27. package/dist/components/ContentEditor/subcomponents/MonacoEditor.svelte +75 -103
  28. package/dist/components/ContentEditor/subcomponents/Office365Editor.svelte +234 -297
  29. package/dist/components/ContentEditor/subcomponents/PdfViewer.svelte +14 -20
  30. package/dist/components/ContentEditor/subcomponents/TextEditor.svelte +122 -150
  31. package/dist/components/ContentEditor/subcomponents/UnknownFile.svelte +105 -126
  32. package/dist/components/ContentEditor/subcomponents/VideoPlayer.svelte +17 -17
  33. package/dist/components/ContentEditor/subcomponents/ZipViewer.svelte +2 -3
  34. package/dist/components/ErrorBoundary/ErrorBoundary.svelte +41 -58
  35. package/dist/components/ErrorBoundary/ErrorBoundaryPreview.svelte +8 -16
  36. package/dist/components/Former/Former.svelte +306 -424
  37. package/dist/components/Former/FormerButtonArea.svelte +17 -68
  38. package/dist/components/Former/FormerDrawer.svelte +16 -80
  39. package/dist/components/Former/FormerDrawerPreview.svelte +99 -89
  40. package/dist/components/Former/FormerModal.svelte +16 -74
  41. package/dist/components/Former/FormerModalPreview.svelte +99 -89
  42. package/dist/components/Former/FormerPreview.svelte +76 -78
  43. package/dist/components/Former/FormerRestApiPreview.svelte +55 -48
  44. package/dist/components/Former/formerOverlayHelpers.svelte.d.ts +11 -0
  45. package/dist/components/Former/formerOverlayHelpers.svelte.js +38 -0
  46. package/dist/components/Former/index.d.ts +1 -0
  47. package/dist/components/Former/index.js +1 -0
  48. package/dist/components/FormerControllers/ButtonCtrl.svelte +13 -49
  49. package/dist/components/FormerControllers/CtrlFieldShell.svelte +15 -0
  50. package/dist/components/FormerControllers/CtrlFieldShell.svelte.d.ts +12 -0
  51. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrl.svelte +285 -457
  52. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlCalendar.svelte +95 -136
  53. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlPickerPanel.svelte +3 -62
  54. package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrlTimeFields.svelte +143 -217
  55. package/dist/components/FormerControllers/IconButtonCtrl.svelte +13 -47
  56. package/dist/components/FormerControllers/InlineWrapper.svelte +12 -41
  57. package/dist/components/FormerControllers/InlineWrapperPreview.svelte +4 -26
  58. package/dist/components/FormerControllers/NativeSelectCtrl.svelte +7 -39
  59. package/dist/components/FormerControllers/NumberInputCtrl.svelte +21 -43
  60. package/dist/components/FormerControllers/PasswordInputCtrl.svelte +8 -33
  61. package/dist/components/FormerControllers/SwitchCtrl.svelte +4 -20
  62. package/dist/components/FormerControllers/TextAreaCtrl.svelte +7 -34
  63. package/dist/components/FormerControllers/TextInputCtrl.svelte +7 -34
  64. package/dist/components/FormerControllers/ctrlLoading.svelte.d.ts +8 -0
  65. package/dist/components/FormerControllers/ctrlLoading.svelte.js +43 -0
  66. package/dist/components/FormerControllers/index.d.ts +2 -0
  67. package/dist/components/FormerControllers/index.js +2 -0
  68. package/dist/components/FormerControllers/utils.d.ts +1 -0
  69. package/dist/components/FormerControllers/utils.js +3 -0
  70. package/dist/components/GlobalStateStore/GlobalStateStoreProvider.svelte +73 -120
  71. package/dist/components/Gridler/components/Gridler.svelte +344 -442
  72. package/dist/components/Gridler/components/GridlerCanvas.svelte +720 -1115
  73. package/dist/components/Gridler/components/GridlerEditor.svelte +2 -15
  74. package/dist/components/Gridler/components/GridlerFull.svelte +521 -841
  75. package/dist/components/Gridler/components/GridlerFull.svelte.d.ts +1 -1
  76. package/dist/components/Gridler/components/GridlerFullWithFormerPreview.svelte +194 -147
  77. package/dist/components/Gridler/components/GridlerSearch.svelte +7 -16
  78. package/dist/components/Gridler/components/GridlerSearchToggle.svelte +7 -19
  79. package/dist/components/Gridler/types.d.ts +1 -1
  80. package/dist/components/Gridler/utils/serverPaging.d.ts +1 -0
  81. package/dist/components/Gridler/utils/serverPaging.js +7 -0
  82. package/dist/components/Icons/IconAdd.svelte +1 -3
  83. package/dist/components/Icons/IconAlertCircle.svelte +1 -3
  84. package/dist/components/Icons/IconAlertTriangle.svelte +1 -3
  85. package/dist/components/Icons/IconCamera.svelte +1 -3
  86. package/dist/components/Icons/IconClose.svelte +1 -3
  87. package/dist/components/Icons/IconEdit.svelte +1 -3
  88. package/dist/components/Icons/IconFilter.svelte +1 -3
  89. package/dist/components/Icons/IconSearch.svelte +1 -3
  90. package/dist/components/Icons/IconSort.svelte +1 -3
  91. package/dist/components/Icons/IconTrash.svelte +1 -3
  92. package/dist/components/Portal/Portal.svelte +14 -38
  93. package/dist/components/Screenshot/Screenshot.svelte +7 -13
  94. package/dist/components/Svark/Svark.svelte +680 -852
  95. package/dist/components/Svark/SvarkContextMenu.svelte +44 -63
  96. package/dist/components/Svark/SvarkInfiniteLoader.svelte +2 -6
  97. package/dist/components/Svark/SvarkPager.svelte +2 -28
  98. package/dist/components/Svark/SvarkSelectionDemo.svelte +4 -8
  99. package/dist/components/Svark/SvarkTopBar.svelte +9 -74
  100. package/dist/components/SvarkGrid/SvarkGrid.svelte +877 -1123
  101. package/dist/components/SvarkGrid/components/SvarkColumnFilterForm.svelte +45 -58
  102. package/dist/components/SvarkGrid/components/SvarkContextMenu.svelte +44 -63
  103. package/dist/components/SvarkGrid/components/SvarkHeaderFilterCell.svelte +41 -75
  104. package/dist/components/SvarkGrid/components/SvarkInfiniteLoader.svelte +2 -6
  105. package/dist/components/SvarkGrid/components/SvarkPager.svelte +2 -28
  106. package/dist/components/SvarkGrid/components/SvarkSearchPopover.svelte +8 -16
  107. package/dist/components/SvarkGrid/components/SvarkTopBar.svelte +6 -62
  108. package/dist/components/SvarkGrid/internal/SvarkGridView.svelte +345 -507
  109. package/dist/components/VTree/VTree.svelte +454 -431
  110. package/dist/components/VTree/VTree.utils.d.ts +23 -0
  111. package/dist/components/VTree/VTree.utils.js +73 -0
  112. package/dist/components/VTree/VTreeContextMenu.svelte +21 -33
  113. package/dist/components/VTree/VTreeControlledSelectionDemo.svelte +20 -0
  114. package/dist/components/VTree/VTreeControlledSelectionDemo.svelte.d.ts +18 -0
  115. package/dist/components/VTree/VTreeCustomizationDemo.svelte +23 -0
  116. package/dist/components/VTree/VTreeCustomizationDemo.svelte.d.ts +18 -0
  117. package/dist/components/VTree/VTreeEventsDemo.svelte +48 -58
  118. package/dist/components/VTree/VTreeInteractionDemo.svelte +29 -0
  119. package/dist/components/VTree/VTreeInteractionDemo.svelte.d.ts +18 -0
  120. package/dist/components/VTree/VTreeMultiSelectDemo.svelte +27 -0
  121. package/dist/components/VTree/VTreeMultiSelectDemo.svelte.d.ts +18 -0
  122. package/dist/components/VTree/VTreeRow.svelte +77 -115
  123. package/dist/components/VTree/VTreeRow.svelte.d.ts +34 -18
  124. package/dist/components/VTree/VTreeSearch.svelte +6 -13
  125. package/dist/components/VTree/VTreeVirtualViewport.svelte +54 -143
  126. package/dist/components/VTree/VTreeVirtualViewport.svelte.d.ts +35 -18
  127. package/dist/components/VTree/types.d.ts +39 -1
  128. package/llm/COMPONENT_GUIDE.md +25 -0
  129. package/package.json +51 -50
@@ -1,844 +1,524 @@
1
- <script lang="ts">
2
- import { untrack } from "svelte";
3
- import type { Snippet } from "svelte";
4
- import type { FilterOption } from "@warkypublic/resolvespec-js";
5
- import Gridler from "./Gridler.svelte";
6
- import GridlerSearchToggle from "./GridlerSearchToggle.svelte";
7
- import BetterMenu from "../../BetterMenu/BetterMenu.svelte";
8
- import type {
9
- BetterMenuInstance,
10
- BetterMenuInstanceItem,
11
- } from "../../BetterMenu/types.js";
12
- import { GridlerResolveSpecAdapter } from "../adapters/GridlerResolveSpecAdapter.js";
13
- import { GridlerRestHeaderSpecAdapter } from "../adapters/GridlerRestHeaderSpecAdapter.js";
14
- import { reorderColumns } from "../utils/columns.js";
15
- import { applyLocalFilter, buildSearchFilters, gridColumnFiltersToFilterOptions } from "../utils/filters.js";
16
- import { applyLocalSort, sortOrderToSortOptions } from "../utils/sort.js";
17
- import { makeGetCellContent } from "../utils/cellContent.js";
18
- import type {
19
- GridlerCell,
20
- GridlerColumn,
21
- GridlerContextMenuItem,
22
- GridlerProps,
23
- GridlerAdapterConfig,
24
- GridColumnSortOrder,
25
- GridColumnFilters,
26
- Item,
27
- Selection,
28
- VisibleRange,
29
- } from "../types";
30
-
31
- export interface Props
32
- extends Omit<
33
- Partial<GridlerProps>,
34
- | "columns"
35
- | "rows"
36
- | "getCellContent"
37
- | "searchValue"
38
- | "onSearchValueChange"
39
- | "onVisibleRangeChange"
40
- | "onCellDblClick"
41
- | "onRowClick"
42
- | "onRowContextMenu"
43
- > {
44
- columns: GridlerColumn[];
45
-
46
- /**
47
- * Optional row data. When provided (and `getCellContent` is omitted), the default
48
- * cell renderer uses `columns[col].dataKey ?? columns[col].id` as the field name on each row object.
49
- * Also accepts a function returning a Promise for async data loading.
50
- */
51
- data?:
52
- | Record<string, unknown>[]
53
- | (() => Promise<Record<string, unknown>[]>);
54
-
55
- /** If provided, takes precedence over `data` rendering. */
56
- getCellContent?: (item: Item) => GridlerCell;
57
- /** Total rows. Defaults to `data.length`. */
58
- rows?: number;
59
-
60
- /** If true, reorders `columns` locally when the user drags headers. */
61
- manageColumns?: boolean;
62
- /** Optional notification when local column order changes. */
63
- onColumnsChange?: (columns: GridlerColumn[]) => void;
64
-
65
- /** Controlled search value (optional). */
66
- searchValue?: string;
67
- /** Uncontrolled initial search value (only used when `searchValue` is undefined). */
68
- defaultSearchValue?: string;
69
- /** Called whenever the search input changes. */
70
- onSearchValueChange?: (value: string) => void;
71
-
72
- /** Called when `GridlerFull` mutates its internal `data` in response to edits. */
73
- onDataChange?: (data: Record<string, unknown>[]) => Promise<void>;
74
-
75
- /** Called on cell double-click with the cell item, cell content, and resolved row data. */
76
- onCellDblClick?: (
77
- item: Item,
78
- cell: GridlerCell,
79
- rowData: Record<string, unknown> | undefined,
80
- ) => void;
81
- /** Called on row double-click with the row index and resolved row data. */
82
- onRowDblClick?: (
83
- row: number,
84
- rowData: Record<string, unknown> | undefined,
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, resolved row data, and click coordinates. */
92
- onRowContextMenu?: (
93
- row: number,
94
- rowData: Record<string, unknown> | undefined,
95
- x: number,
96
- y: number,
97
- ) => void;
98
-
99
- // ── Server-side fetching ──────────────────────────────────────────────────
100
- // Use dataSource + dataSourceOptions (from GridCommonProps) for server config.
101
- // dataSource: 'resolvespec' | 'headerspec' | 'websockspec' | 'local' | 'custom'
102
- // dataSourceOptions: { url, authToken, schema, entity, uniqueID, headers }
103
-
104
- /** Rows per cursor-forward page. Defaults to 200. */
105
- pageSize?: number;
106
- /**
107
- * Shorthand for dataSourceOptions.uniqueID.
108
- * Row field whose value is passed as `cursor_forward` on subsequent requests.
109
- * Defaults to 'id'.
110
- */
111
- uniqueID?: string;
112
- /**
113
- * When true, the search input value is translated into `ilike` filters sent to
114
- * the server instead of only being used for client-side highlighting.
115
- */
116
- serverSideSearch?: boolean;
117
- /**
118
- * Columns searched when `serverSideSearch` is true.
119
- * Defaults to all column IDs when omitted.
120
- */
121
- searchColumns?: string[];
122
- /** Called when a server fetch fails. */
123
- onLoadError?: (error: string) => void;
124
-
125
- /**
126
- * Bindable. Reflects the server-reported total row count.
127
- * Updated after every successful page fetch; 0 until the first response.
128
- * Read-only from the parent — do not write this value.
129
- */
130
- total?: number;
131
- /**
132
- * Bindable. `true` while any server fetch is in flight (initial load or next-page).
133
- * Read-only from the parent — do not write this value.
134
- */
135
- loading?: boolean;
136
-
137
- /**
138
- * Extra items appended to the built-in context menu (Refresh, Clear Search, Clear Filters, Clear Sort).
139
- * Pass a separator item `{ id: 'sep', kind: 'separator', label: '' }` to group your items.
140
- */
141
- menuItems?: GridlerContextMenuItem[];
142
- /** Called when any context menu item is selected (built-in or custom). */
143
- onMenuItemSelect?: (item: GridlerContextMenuItem, rowData?: Record<string, unknown>) => void;
144
-
145
- children?: Snippet;
146
- }
147
-
148
- let {
149
- columns,
150
- data,
151
- getCellContent,
152
- rows,
153
- manageColumns = true,
154
- onColumnsChange,
155
- searchValue,
156
- defaultSearchValue = "",
157
- onSearchValueChange,
158
- onDataChange,
159
- // server-side (via GridCommonProps dataSource/dataSourceOptions)
160
- dataSource,
161
- dataSourceOptions,
162
- pageSize = 200,
163
- uniqueID,
164
- serverSideSearch = false,
165
- searchColumns,
166
- onLoadError,
167
- total = $bindable(0),
168
- loading = $bindable(false),
169
- // GridCommonProps filters/sort (generic types)
170
- filters,
171
- onFilterChange,
172
- sortOrder,
173
- onSortOrderChange,
174
- // GridCommonProps selectedItems
175
- selectedItems: _selectedItems,
176
- onSelectedItemsChange,
177
- // GridCommonProps settings
178
- settings,
179
- // gridler pass-through
180
- fixedColumns = 0,
181
- rowMarkers,
182
- showSearch,
183
- height = 500,
184
- onColumnMoved,
185
- onCellEdited,
186
- onCellDblClick,
187
- onRowDblClick,
188
- onRowClick,
189
- onRowContextMenu,
190
- onMenuClick,
191
- onSelectionChange,
192
- onCellEvent,
193
- onGridEvent,
194
- menuItems,
195
- onMenuItemSelect,
196
- children,
197
- ...rest
198
- }: Props = $props();
199
-
200
- // ── Column management ─────────────────────────────────────────────────────
201
-
202
- let columnsState = $state<GridlerColumn[]>(columns.slice());
203
- const columnsSignature = $derived.by(() =>
204
- columns.map((c) => `${c.id}:${c.title}:${c.width}`).join("|"),
205
- );
206
- let lastColumnsSignature = $state(columnsSignature);
207
-
208
- $effect(() => {
209
- if (columnsSignature === untrack(() => lastColumnsSignature)) return;
210
- columnsState = columns.slice();
211
- lastColumnsSignature = columnsSignature;
212
- });
213
-
214
- // ── Local data management (non-server mode) ────────────────────────────────
215
-
216
- let dataState = $state<Record<string, unknown>[]>(
217
- Array.isArray(data) ? data.slice() : [],
218
- );
219
- let lastDataRef = $state<
220
- | Record<string, unknown>[]
221
- | (() => Promise<Record<string, unknown>[]>)
222
- | undefined
223
- >(data);
224
-
225
- $effect(() => {
226
- if (!data) return;
227
- if (data === untrack(() => lastDataRef)) return;
228
- lastDataRef = data;
229
- if (typeof data === "function") {
230
- data()
231
- .then((result) => {
232
- dataState = result;
233
- onGridEvent?.("load");
234
- })
235
- .catch((e) => {
236
- const msg = e instanceof Error ? e.message : "Failed to load data";
237
- onLoadError?.(msg);
238
- });
239
- } else {
240
- dataState = data.slice();
241
- onGridEvent?.("load");
242
- }
243
- });
244
-
245
- // ── Server config (resolved from dataSource / dataSourceOptions) ───────────
246
-
247
- const resolvedUrl = $derived(dataSourceOptions?.url);
248
- const resolvedToken = $derived(dataSourceOptions?.authToken);
249
- const resolvedSchema = $derived(dataSourceOptions?.schema);
250
- const resolvedEntity = $derived(dataSourceOptions?.entity);
251
- const resolvedUniqueID = $derived(
252
- dataSourceOptions?.uniqueID ?? uniqueID ?? "id",
253
- );
254
-
255
- const isServerMode = $derived(
256
- !!(
257
- dataSource &&
258
- dataSource !== "local" &&
259
- dataSource !== "custom" &&
260
- resolvedUrl &&
261
- resolvedSchema &&
262
- resolvedEntity
263
- ),
264
- );
265
-
266
- // ── Search visibility ──────────────────────────────────────────────────────
267
-
268
- const resolvedShowSearch = $derived(
269
- showSearch ?? settings?.showSearch ?? false,
270
- );
271
- let showSearchBar = $state(resolvedShowSearch);
272
-
273
- function toggleSearch() {
274
- showSearchBar = !showSearchBar;
275
- if (!showSearchBar) emitSearchValue("");
276
- }
277
-
278
- function closeSearch() {
279
- showSearchBar = false;
280
- emitSearchValue("");
281
- }
282
-
283
- // ── Search ─────────────────────────────────────────────────────────────────
284
-
285
- let internalSearchValue = $state(defaultSearchValue);
286
- $effect(() => {
287
- if (searchValue === undefined) return;
288
- internalSearchValue = searchValue;
289
- });
290
-
291
- const resolvedSearchValue = $derived(searchValue ?? internalSearchValue);
292
-
293
- function emitSearchValue(value: string) {
294
- if (searchValue === undefined) {
295
- internalSearchValue = value;
296
- }
297
- onSearchValueChange?.(value);
298
- onGridEvent?.("search_changed", undefined, undefined, undefined, { searchValue: value });
299
- }
300
-
301
- // ── Context menu ───────────────────────────────────────────────────────────
302
-
303
- let refreshTrigger = $state(0);
304
- let contextMenuRowData = $state<Record<string, unknown> | undefined>(undefined);
305
- let menu = $state<{
306
- show: (
307
- id: string,
308
- options?: Partial<Omit<BetterMenuInstance, "id">>,
309
- ) => void;
310
- hide: (id: string) => void;
311
- } | null>(null);
312
-
313
- function refresh() {
314
- if (isServerMode) {
315
- refreshTrigger++;
316
- } else if (typeof data === "function") {
317
- data()
318
- .then((result) => {
319
- dataState = result;
320
- })
321
- .catch((e) => {
322
- const msg = e instanceof Error ? e.message : "Failed to load data";
323
- onLoadError?.(msg);
324
- });
325
- }
326
- }
327
-
328
- function buildMenuItems(): BetterMenuInstanceItem[] {
329
- const items: BetterMenuInstanceItem[] = [];
330
-
331
- items.push({ label: "Refresh", onClick: () => refresh() });
332
-
333
- const _hasSearch = resolvedSearchValue.length > 0;
334
- const _hasFilters = Object.keys(resolvedFilters).length > 0;
335
- const _hasSort = Object.values(resolvedSortOrder).some((d) => d !== "none");
336
-
337
- if (_hasSearch || _hasFilters || _hasSort) {
338
- items.push({ isDivider: true });
339
- if (_hasSearch)
340
- items.push({
341
- label: "Clear Search",
342
- onClick: () => emitSearchValue(""),
343
- });
344
- if (_hasFilters)
345
- items.push({
346
- label: "Clear Filters",
347
- onClick: () => handleFilterChange({}),
348
- });
349
- if (_hasSort)
350
- items.push({
351
- label: "Clear Sort",
352
- onClick: () => handleSortOrderChange({}),
353
- });
354
- }
355
-
356
- if (menuItems?.length) {
357
- items.push({ isDivider: true });
358
- for (const item of menuItems) {
359
- if (item.kind === "separator") {
360
- items.push({ isDivider: true });
361
- } else {
362
- items.push({
363
- id: item.id,
364
- label: item.label,
365
- icon: item.icon,
366
- onClick: () => {
367
- item.onselect?.(contextMenuRowData);
368
- onMenuItemSelect?.(item, contextMenuRowData);
369
- },
370
- });
371
- }
372
- }
373
- }
374
-
375
- return items;
376
- }
377
-
378
- // ── Filter panel ───────────────────────────────────────────────────────────
379
-
380
- type GridFilterOp = NonNullable<GridColumnFilters[string]["op"]>;
381
-
382
- let filterPanelColId = $state("");
383
- let filterPanelColTitle = $state("");
384
- let filterPanelOp = $state<GridFilterOp>("contains");
385
- let filterPanelValue = $state("");
386
- let filterPanelValue2 = $state("");
387
-
388
- // Snippet reference captured from the template — assigned via {(filterRenderer ??= filterForm, '')}
389
- let filterRenderer = $state<Snippet<
390
- [{ id: string; onClose: () => void }]
391
- > | null>(null);
392
-
393
- function openFilterPanel(
394
- colId: string,
395
- colTitle: string,
396
- x: number,
397
- y: number,
398
- ) {
399
- const existing = resolvedFilters[colId];
400
- filterPanelColId = colId;
401
- filterPanelColTitle = colTitle;
402
- filterPanelOp = existing?.op ?? "contains";
403
- filterPanelValue = existing?.value ?? "";
404
- filterPanelValue2 = existing?.value2 ?? "";
405
- menu?.show("gridler-filter-panel", {
406
- x,
407
- y,
408
- renderer: filterRenderer as unknown as (props: Record<string, unknown>) => unknown,
409
- });
410
- }
411
-
412
- function applyFilterPanel() {
413
- const needsValue =
414
- filterPanelOp !== "isNull" && filterPanelOp !== "isNotNull";
415
- if (needsValue && !filterPanelValue) {
416
- const next = { ...resolvedFilters };
417
- delete next[filterPanelColId];
418
- handleFilterChange(next);
419
- } else {
420
- const entry: GridColumnFilters[string] = { value: filterPanelValue, op: filterPanelOp };
421
- if ((filterPanelOp === "between" || filterPanelOp === "notBetween") && filterPanelValue2) {
422
- entry.value2 = filterPanelValue2;
423
- }
424
- handleFilterChange({
425
- ...resolvedFilters,
426
- [filterPanelColId]: entry,
427
- });
428
- }
429
- menu?.hide("gridler-filter-panel");
430
- }
431
-
432
- // ── Column header context menu ─────────────────────────────────────────────
433
-
434
- function handleHeaderContextMenu(colIndex: number, x: number, y: number) {
435
- const column = columnsState[colIndex];
436
- if (!column) return;
437
-
438
- const colId = column.id;
439
- const currentSort = resolvedSortOrder[colId] ?? "none";
440
- const currentFilter = resolvedFilters[colId];
441
- const isSortable = column.disableSort !== true;
442
- const isFilterable = column.disableFilter !== true;
443
-
444
- const items: BetterMenuInstanceItem[] = [];
445
-
446
- // Column name (non-clickable header label)
447
- items.push({ label: column.title });
448
- items.push({ isDivider: true });
449
-
450
- // Sort section
451
- if (isSortable) {
452
- items.push({
453
- label: `${currentSort === "asc" ? "✓ " : ""}Sort Ascending`,
454
- onClick: () =>
455
- handleSortOrderChange({
456
- ...resolvedSortOrder,
457
- [colId]: currentSort === "asc" ? "none" : "asc",
458
- }),
459
- });
460
- items.push({
461
- label: `${currentSort === "desc" ? "✓ " : ""}Sort Descending`,
462
- onClick: () =>
463
- handleSortOrderChange({
464
- ...resolvedSortOrder,
465
- [colId]: currentSort === "desc" ? "none" : "desc",
466
- }),
467
- });
468
- if (currentSort !== "none") {
469
- items.push({
470
- label: "Clear Sort",
471
- onClick: () => {
472
- const next = { ...resolvedSortOrder, [colId]: "none" as const };
473
- handleSortOrderChange(next);
474
- },
475
- });
476
- }
477
- }
478
-
479
- // Filter section
480
- if (isFilterable) {
481
- items.push({ isDivider: true });
482
- if (currentFilter) {
483
- items.push({ label: `Filter: ${currentFilter.value}` });
484
- items.push({
485
- label: "Edit Filter…",
486
- onClick: () => openFilterPanel(colId, column.title, x, y),
487
- });
488
- items.push({
489
- label: "Clear Filter",
490
- onClick: () => {
491
- const next = { ...resolvedFilters };
492
- delete next[colId];
493
- handleFilterChange(next);
494
- },
495
- });
496
- } else {
497
- items.push({
498
- label: "Set Filter…",
499
- onClick: () => openFilterPanel(colId, column.title, x, y),
500
- });
501
- }
502
- }
503
-
504
- // Search section
505
- if (resolvedSearchValue) {
506
- items.push({ isDivider: true });
507
- items.push({ label: `Search: "${resolvedSearchValue}"` });
508
- items.push({ label: "Clear Search", onClick: () => emitSearchValue("") });
509
- }
510
-
511
- menu?.show("gridler-column-menu", { x, y, items });
512
- }
513
-
514
- // ── Column reorder ─────────────────────────────────────────────────────────
515
-
516
- function handleColumnReorder(startIndex: number, endIndex: number) {
517
- const next = reorderColumns(columnsState, startIndex, endIndex);
518
- if (next === columnsState) return;
519
- columnsState = next;
520
- onColumnsChange?.(next);
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
+ import { getServerRenderableRows } from "../utils/serverPaging.js";
12
+ 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();
13
+ // ── Column management ─────────────────────────────────────────────────────
14
+ // eslint-disable-next-line svelte/no-unused-svelte-ignore
15
+ // svelte-ignore state_referenced_locally -- intentional initial capture; $effect below syncs changes
16
+ let columnsState = $state(columns.slice());
17
+ const columnsSignature = $derived.by(() => columns.map((c) => `${c.id}:${c.title}:${c.width}`).join("|"));
18
+ // eslint-disable-next-line svelte/no-unused-svelte-ignore
19
+ // svelte-ignore state_referenced_locally -- intentional initial capture; $effect below syncs changes
20
+ let lastColumnsSignature = $state(columnsSignature);
21
+ $effect(() => {
22
+ if (columnsSignature === untrack(() => lastColumnsSignature)) return;
23
+ columnsState = columns.slice();
24
+ lastColumnsSignature = columnsSignature;
25
+ });
26
+ // ── Local data management (non-server mode) ────────────────────────────────
27
+ // eslint-disable-next-line svelte/no-unused-svelte-ignore
28
+ // svelte-ignore state_referenced_locally -- intentional initial capture; $effect below syncs changes
29
+ let dataState = $state(Array.isArray(data) ? data.slice() : []);
30
+ // eslint-disable-next-line svelte/no-unused-svelte-ignore
31
+ // svelte-ignore state_referenced_locally -- intentional initial capture; $effect below syncs changes
32
+ let lastDataRef = $state(data);
33
+ $effect(() => {
34
+ if (!data) return;
35
+ if (data === untrack(() => lastDataRef)) return;
36
+ lastDataRef = data;
37
+ if (typeof data === "function") {
38
+ data().then((result) => {
39
+ dataState = result;
40
+ onGridEvent?.("load");
41
+ }).catch((e) => {
42
+ const msg = e instanceof Error ? e.message : "Failed to load data";
43
+ onLoadError?.(msg);
44
+ });
45
+ } else {
46
+ dataState = data.slice();
47
+ onGridEvent?.("load");
48
+ }
49
+ });
50
+ // ── Server config (resolved from dataSource / dataSourceOptions) ───────────
51
+ const resolvedUrl = $derived(dataSourceOptions?.url);
52
+ const resolvedToken = $derived(dataSourceOptions?.authToken);
53
+ const resolvedSchema = $derived(dataSourceOptions?.schema);
54
+ const resolvedEntity = $derived(dataSourceOptions?.entity);
55
+ const resolvedUniqueID = $derived(dataSourceOptions?.uniqueID ?? uniqueID ?? "id");
56
+ const isServerMode = $derived(!!(dataSource && dataSource !== "local" && dataSource !== "custom" && resolvedUrl && resolvedSchema && resolvedEntity));
57
+ // ── Search visibility ──────────────────────────────────────────────────────
58
+ const resolvedShowSearch = $derived(showSearch ?? settings?.showSearch ?? false);
59
+ // eslint-disable-next-line svelte/no-unused-svelte-ignore
60
+ // svelte-ignore state_referenced_locally -- intentional initial capture
61
+ let showSearchBar = $state(resolvedShowSearch);
62
+ function toggleSearch() {
63
+ showSearchBar = !showSearchBar;
64
+ if (!showSearchBar) emitSearchValue("");
65
+ }
66
+ function closeSearch() {
67
+ showSearchBar = false;
68
+ emitSearchValue("");
69
+ }
70
+ // ── Search ─────────────────────────────────────────────────────────────────
71
+ // eslint-disable-next-line svelte/no-unused-svelte-ignore
72
+ // svelte-ignore state_referenced_locally -- intentional initial capture; $effect below syncs controlled value
73
+ let internalSearchValue = $state(defaultSearchValue);
74
+ $effect(() => {
75
+ if (searchValue === undefined) return;
76
+ internalSearchValue = searchValue;
77
+ });
78
+ const resolvedSearchValue = $derived(searchValue ?? internalSearchValue);
79
+ function emitSearchValue(value) {
80
+ if (searchValue === undefined) {
81
+ internalSearchValue = value;
82
+ }
83
+ onSearchValueChange?.(value);
84
+ onGridEvent?.("search_changed", undefined, undefined, undefined, { searchValue: value });
85
+ }
86
+ // ── Context menu ───────────────────────────────────────────────────────────
87
+ let refreshTrigger = $state(0);
88
+ let contextMenuRowData = $state(undefined);
89
+ let menu = $state(null);
90
+ function refresh() {
91
+ if (isServerMode) {
92
+ refreshTrigger++;
93
+ } else if (typeof data === "function") {
94
+ data().then((result) => {
95
+ dataState = result;
96
+ }).catch((e) => {
97
+ const msg = e instanceof Error ? e.message : "Failed to load data";
98
+ onLoadError?.(msg);
99
+ });
100
+ }
101
+ }
102
+ function buildMenuItems() {
103
+ const items = [];
104
+ items.push({
105
+ label: "Refresh",
106
+ onClick: () => refresh()
107
+ });
108
+ const _hasSearch = resolvedSearchValue.length > 0;
109
+ const _hasFilters = Object.keys(resolvedFilters).length > 0;
110
+ const _hasSort = Object.values(resolvedSortOrder).some((d) => d !== "none");
111
+ if (_hasSearch || _hasFilters || _hasSort) {
112
+ items.push({ isDivider: true });
113
+ if (_hasSearch) items.push({
114
+ label: "Clear Search",
115
+ onClick: () => emitSearchValue("")
116
+ });
117
+ if (_hasFilters) items.push({
118
+ label: "Clear Filters",
119
+ onClick: () => handleFilterChange({})
120
+ });
121
+ if (_hasSort) items.push({
122
+ label: "Clear Sort",
123
+ onClick: () => handleSortOrderChange({})
124
+ });
125
+ }
126
+ if (menuItems?.length) {
127
+ items.push({ isDivider: true });
128
+ for (const item of menuItems) {
129
+ if (item.kind === "separator") {
130
+ items.push({ isDivider: true });
131
+ } else {
132
+ items.push({
133
+ id: item.id,
134
+ label: item.label,
135
+ icon: item.icon,
136
+ onClick: () => {
137
+ item.onselect?.(contextMenuRowData);
138
+ onMenuItemSelect?.(item, contextMenuRowData);
139
+ }
140
+ });
141
+ }
142
+ }
143
+ }
144
+ return items;
145
+ }
146
+ let filterPanelColId = $state("");
147
+ let filterPanelColTitle = $state("");
148
+ let filterPanelOp = $state("contains");
149
+ let filterPanelValue = $state("");
150
+ let filterPanelValue2 = $state("");
151
+ // Snippet reference captured from the template — assigned via {(filterRenderer ??= filterForm, '')}
152
+ let filterRenderer = $state(null);
153
+ function openFilterPanel(colId, colTitle, x, y) {
154
+ const existing = resolvedFilters[colId];
155
+ filterPanelColId = colId;
156
+ filterPanelColTitle = colTitle;
157
+ filterPanelOp = existing?.op ?? "contains";
158
+ filterPanelValue = existing?.value ?? "";
159
+ filterPanelValue2 = existing?.value2 ?? "";
160
+ menu?.show("gridler-filter-panel", {
161
+ x,
162
+ y,
163
+ renderer: filterRenderer
164
+ });
165
+ }
166
+ function applyFilterPanel() {
167
+ const needsValue = filterPanelOp !== "isNull" && filterPanelOp !== "isNotNull";
168
+ if (needsValue && !filterPanelValue) {
169
+ const next = { ...resolvedFilters };
170
+ delete next[filterPanelColId];
171
+ handleFilterChange(next);
172
+ } else {
173
+ const entry = {
174
+ value: filterPanelValue,
175
+ op: filterPanelOp
176
+ };
177
+ if ((filterPanelOp === "between" || filterPanelOp === "notBetween") && filterPanelValue2) {
178
+ entry.value2 = filterPanelValue2;
179
+ }
180
+ handleFilterChange({
181
+ ...resolvedFilters,
182
+ [filterPanelColId]: entry
183
+ });
184
+ }
185
+ menu?.hide("gridler-filter-panel");
186
+ }
187
+ // ── Column header context menu ─────────────────────────────────────────────
188
+ function handleHeaderContextMenu(colIndex, x, y) {
189
+ const column = columnsState[colIndex];
190
+ if (!column) return;
191
+ const colId = column.id;
192
+ const currentSort = resolvedSortOrder[colId] ?? "none";
193
+ const currentFilter = resolvedFilters[colId];
194
+ const isSortable = column.disableSort !== true;
195
+ const isFilterable = column.disableFilter !== true;
196
+ const items = [];
197
+ // Column name (non-clickable header label)
198
+ items.push({ label: column.title });
199
+ items.push({ isDivider: true });
200
+ // Sort section
201
+ if (isSortable) {
202
+ items.push({
203
+ label: `${currentSort === "asc" ? "✓ " : ""}Sort Ascending`,
204
+ onClick: () => handleSortOrderChange({
205
+ ...resolvedSortOrder,
206
+ [colId]: currentSort === "asc" ? "none" : "asc"
207
+ })
208
+ });
209
+ items.push({
210
+ label: `${currentSort === "desc" ? "✓ " : ""}Sort Descending`,
211
+ onClick: () => handleSortOrderChange({
212
+ ...resolvedSortOrder,
213
+ [colId]: currentSort === "desc" ? "none" : "desc"
214
+ })
215
+ });
216
+ if (currentSort !== "none") {
217
+ items.push({
218
+ label: "Clear Sort",
219
+ onClick: () => {
220
+ const next = {
221
+ ...resolvedSortOrder,
222
+ [colId]: "none"
223
+ };
224
+ handleSortOrderChange(next);
225
+ }
226
+ });
227
+ }
228
+ }
229
+ // Filter section
230
+ if (isFilterable) {
231
+ items.push({ isDivider: true });
232
+ if (currentFilter) {
233
+ items.push({ label: `Filter: ${currentFilter.value}` });
234
+ items.push({
235
+ label: "Edit Filter…",
236
+ onClick: () => openFilterPanel(colId, column.title, x, y)
237
+ });
238
+ items.push({
239
+ label: "Clear Filter",
240
+ onClick: () => {
241
+ const next = { ...resolvedFilters };
242
+ delete next[colId];
243
+ handleFilterChange(next);
244
+ }
245
+ });
246
+ } else {
247
+ items.push({
248
+ label: "Set Filter…",
249
+ onClick: () => openFilterPanel(colId, column.title, x, y)
250
+ });
251
+ }
252
+ }
253
+ // Search section
254
+ if (resolvedSearchValue) {
255
+ items.push({ isDivider: true });
256
+ items.push({ label: `Search: "${resolvedSearchValue}"` });
257
+ items.push({
258
+ label: "Clear Search",
259
+ onClick: () => emitSearchValue("")
260
+ });
261
+ }
262
+ menu?.show("gridler-column-menu", {
263
+ x,
264
+ y,
265
+ items
266
+ });
267
+ }
268
+ // ── Column reorder ─────────────────────────────────────────────────────────
269
+ function handleColumnReorder(startIndex, endIndex) {
270
+ const next = reorderColumns(columnsState, startIndex, endIndex);
271
+ if (next === columnsState) return;
272
+ columnsState = next;
273
+ onColumnsChange?.(next);
274
+ }
275
+ // ── Filter state (controlled or internal) ─────────────────────────────────
276
+ let internalFilters = $state({});
277
+ const resolvedFilters = $derived(filters ?? internalFilters);
278
+ function handleFilterChange(next) {
279
+ if (filters === undefined) internalFilters = next;
280
+ onFilterChange?.(next);
281
+ onGridEvent?.("filter_changed", undefined, undefined, undefined, { filters: next });
282
+ }
283
+ // ── Server-side state ──────────────────────────────────────────────────────
284
+ let serverData = $state([]);
285
+ let serverTotal = $state(0);
286
+ let serverCursor = $state(undefined);
287
+ let serverFetchVersion = 0;
288
+ let serverLoading = $state(false);
289
+ let serverAllLoaded = $state(false);
290
+ $effect(() => {
291
+ total = serverTotal;
292
+ });
293
+ $effect(() => {
294
+ loading = serverLoading;
295
+ });
296
+ let serverError = $state(null);
297
+ const serverAdapter = $derived.by(() => {
298
+ if (!resolvedUrl || !resolvedSchema || !resolvedEntity) return null;
299
+ const config = {
300
+ url: resolvedUrl,
301
+ token: resolvedToken,
302
+ schema: resolvedSchema,
303
+ entity: resolvedEntity,
304
+ uniqueID: resolvedUniqueID,
305
+ extraOptions: dataSourceOptions?.extraOptions
306
+ };
307
+ return dataSource === "headerspec" ? new GridlerRestHeaderSpecAdapter(config) : new GridlerResolveSpecAdapter(config);
308
+ });
309
+ function buildAllFilters(search) {
310
+ const base = Object.keys(resolvedFilters).length ? gridColumnFiltersToFilterOptions(resolvedFilters) : [];
311
+ if (!search?.trim()) return base;
312
+ return [...base, ...buildSearchFilters(search, columnsState, searchColumns)];
313
+ }
314
+ // Compute column fields (dataKey ?? id) merged with hotfields, deduped.
315
+ const resolvedFields = $derived.by(() => {
316
+ const columnFields = columnsState.map((c) => c.dataKey ?? c.id);
317
+ const hotfields = dataSourceOptions?.hotfields ?? [];
318
+ const seen = new Set(columnFields);
319
+ const extra = hotfields.filter((f) => !seen.has(f));
320
+ return [...columnFields, ...extra];
321
+ });
322
+ // ── Sort state (controlled or internal) ───────────────────────────────────
323
+ let internalSortOrder = $state({});
324
+ const resolvedSortOrder = $derived(sortOrder ?? internalSortOrder);
325
+ function handleSortOrderChange(order) {
326
+ if (sortOrder === undefined) internalSortOrder = order;
327
+ onSortOrderChange?.(order);
328
+ onGridEvent?.("sort_changed", undefined, undefined, undefined, { sortOrder: order });
329
+ }
330
+ const resolvedSort = $derived(Object.values(resolvedSortOrder).some((d) => d !== "none") ? sortOrderToSortOptions(resolvedSortOrder) : undefined);
331
+ // ── Local sorted/filtered data ────────────────────────────────────────────
332
+ const sortedDataState = $derived(applyLocalSort(dataState, resolvedSort ?? [], columnsState));
333
+ const filteredDataState = $derived(applyLocalFilter(sortedDataState, resolvedFilters, columnsState));
334
+ // Full reset + first page fetch.
335
+ $effect(() => {
336
+ const _adapter = serverAdapter;
337
+ const _sort = resolvedSort;
338
+ const _search = serverSideSearch ? resolvedSearchValue : null;
339
+ const _pageSize = pageSize;
340
+ // Track resolvedFilters so internal filter changes also trigger a re-fetch.
341
+ const _filters = resolvedFilters;
342
+ // Track manual refresh trigger.
343
+ const _refresh = refreshTrigger;
344
+ // _refresh is tracked to trigger re-fetch on manual refresh.
345
+ void _refresh;
346
+ if (!_adapter) return;
347
+ let cancelled = false;
348
+ serverFetchVersion++;
349
+ serverLoading = true;
350
+ serverError = null;
351
+ serverData = [];
352
+ serverTotal = 0;
353
+ serverCursor = undefined;
354
+ serverAllLoaded = false;
355
+ const columnFilters = Object.keys(_filters).length ? gridColumnFiltersToFilterOptions(_filters) : [];
356
+ const searchFilters = _search?.trim() ? buildSearchFilters(_search, columnsState, searchColumns) : [];
357
+ const allFilters = [...columnFilters, ...searchFilters];
358
+ _adapter.readPage(_pageSize, undefined, _sort, allFilters, untrack(() => resolvedFields)).then((result) => {
359
+ if (cancelled) return;
360
+ serverData = result.data;
361
+ serverTotal = result.total;
362
+ serverCursor = result.nextCursor;
363
+ serverAllLoaded = result.data.length < _pageSize || !result.nextCursor;
364
+ onGridEvent?.("load");
365
+ }).catch((e) => {
366
+ if (cancelled) return;
367
+ const msg = e instanceof Error ? e.message : "Failed to load data";
368
+ serverError = msg;
369
+ onLoadError?.(msg);
370
+ }).finally(() => {
371
+ if (!cancelled) serverLoading = false;
372
+ });
373
+ return () => {
374
+ cancelled = true;
375
+ };
376
+ });
377
+ // Append next cursor page.
378
+ async function loadNextPage() {
379
+ const _adapter = serverAdapter;
380
+ if (!_adapter || serverLoading || serverAllLoaded || !serverCursor) return;
381
+ const cursorSnapshot = serverCursor;
382
+ const versionSnapshot = serverFetchVersion;
383
+ serverLoading = true;
384
+ const allFilters = buildAllFilters(serverSideSearch ? resolvedSearchValue : null);
385
+ try {
386
+ const result = await _adapter.readPage(pageSize, cursorSnapshot, resolvedSort, allFilters, resolvedFields);
387
+ if (serverFetchVersion !== versionSnapshot) return;
388
+ serverData = [...serverData, ...result.data];
389
+ serverCursor = result.nextCursor;
390
+ serverAllLoaded = result.data.length < pageSize || !result.nextCursor || result.nextCursor === cursorSnapshot;
391
+ onGridEvent?.("page_loaded", undefined, undefined, undefined, {
392
+ data: result.data,
393
+ total: serverData.length
394
+ });
395
+ } catch (e) {
396
+ const msg = e instanceof Error ? e.message : "Failed to load page";
397
+ onLoadError?.(msg);
398
+ } finally {
399
+ serverLoading = false;
400
+ }
401
+ }
402
+ // Trigger next page when the visible range nears the end of loaded data.
403
+ function handleVisibleRangeChange(range) {
404
+ untrack(() => {
405
+ if (!isServerMode || serverLoading || serverAllLoaded || !serverCursor) return;
406
+ const PREFETCH_THRESHOLD = Math.ceil(pageSize / 2);
407
+ if (range.lastRow >= serverData.length - PREFETCH_THRESHOLD) {
408
+ loadNextPage();
409
+ }
410
+ });
411
+ }
412
+ // ── Double-click ───────────────────────────────────────────────────────────
413
+ function resolveRowData(row) {
414
+ return isServerMode ? serverData[row] : filteredDataState[row];
415
+ }
416
+ function handleCellDblClick(item, cell) {
417
+ const rowData = resolveRowData(item[1]);
418
+ onCellDblClick?.(item, cell, rowData);
419
+ onRowDblClick?.(item[1], rowData);
420
+ }
421
+ function handleRowClick(row) {
422
+ onRowClick?.(row, resolveRowData(row));
423
+ }
424
+ function handleRowContextMenu(row, x, y) {
425
+ contextMenuRowData = resolveRowData(row);
426
+ menu?.show("gridler-menu", {
427
+ x,
428
+ y,
429
+ items: buildMenuItems()
430
+ });
431
+ onRowContextMenu?.(row, contextMenuRowData, x, y);
432
+ }
433
+ // ── Selection → selectedItems bridge ──────────────────────────────────────
434
+ let internalSelectedItems = $state([]);
435
+ $effect(() => {
436
+ if (_selectedItems === undefined) return;
437
+ internalSelectedItems = Array.isArray(_selectedItems) ? _selectedItems : [];
438
+ });
439
+ function handleSelectionChange(sel) {
440
+ let items = [];
441
+ if (sel.type === "row") {
442
+ items = sel.rows.map((i) => resolveRowData(i)).filter((r) => r !== undefined);
443
+ } else if (sel.type === "cell") {
444
+ const row = resolveRowData(sel.item[1]);
445
+ if (row) items = [row];
446
+ } else if (sel.type === "range") {
447
+ const minRow = Math.min(sel.start[1], sel.end[1]);
448
+ const maxRow = Math.max(sel.start[1], sel.end[1]);
449
+ for (let i = minRow; i <= maxRow; i++) {
450
+ const row = resolveRowData(i);
451
+ if (row) items.push(row);
452
+ }
453
+ }
454
+ internalSelectedItems = items;
455
+ if (items.length > 0) onSelectedItemsChange?.(items);
456
+ onSelectionChange?.(sel, items.length > 0 ? items : undefined);
457
+ }
458
+ // ── Settings change event ─────────────────────────────────────────────────
459
+ let lastSettings = $state(undefined);
460
+ $effect(() => {
461
+ if (settings === untrack(() => lastSettings)) return;
462
+ lastSettings = settings;
463
+ onGridEvent?.("settings_changed", undefined, undefined, undefined, { settings });
464
+ });
465
+ // ── Cell content ───────────────────────────────────────────────────────────
466
+ const serverGetCellContent = $derived(makeGetCellContent(serverData, columnsState));
467
+ const localGetCellContent = $derived(makeGetCellContent(filteredDataState, columnsState));
468
+ const resolvedGetCellContent = $derived.by(() => {
469
+ if (getCellContent) return getCellContent;
470
+ if (isServerMode) return serverGetCellContent;
471
+ return localGetCellContent;
472
+ });
473
+ // ── Row count ──────────────────────────────────────────────────────────────
474
+ const resolvedRows = $derived.by(() => {
475
+ if (isServerMode) return getServerRenderableRows(serverData.length, serverTotal, serverAllLoaded);
476
+ if (rows != null) return rows;
477
+ return filteredDataState.length;
478
+ });
479
+ // ── Cell editing ───────────────────────────────────────────────────────────
480
+ async function applyEdit(item, value) {
481
+ if (getCellContent || isServerMode) return;
482
+ const [col, row] = item;
483
+ const colDef = columnsState[col];
484
+ const key = colDef?.dataKey ?? colDef?.id;
485
+ if (!key) return;
486
+ const existing = filteredDataState[row];
487
+ if (!existing) return;
488
+ const originalIndex = dataState.indexOf(existing);
489
+ if (originalIndex === -1) return;
490
+ const next = dataState.slice();
491
+ next[originalIndex] = {
492
+ ...existing,
493
+ [key]: value.data ?? value.displayData
494
+ };
495
+ dataState = next;
496
+ await onDataChange?.(next);
497
+ onGridEvent?.("data_changed", next[originalIndex], columnsState[col], undefined, { data: next });
498
+ }
499
+ async function handleGridMenuClick(d) {
500
+ contextMenuRowData = undefined;
501
+ if (onMenuClick) {
502
+ await onMenuClick(d?.item, d?.column, d ? {
503
+ x: d.x,
504
+ y: d.y
505
+ } : undefined);
506
+ return;
507
+ }
508
+ if (!d) return;
509
+ menu?.show("gridler-menu", {
510
+ x: d.x,
511
+ y: d.y,
512
+ items: buildMenuItems()
513
+ });
514
+ }
515
+ // ── Public API ────────────────────────────────────────────────────────────
516
+ export function getSelectedRows() {
517
+ return internalSelectedItems;
518
+ }
519
+ export function getLoadedData() {
520
+ return isServerMode ? serverData : filteredDataState;
521
+ }
842
522
  </script>
843
523
 
844
524
  <BetterMenu bind:this={menu}>