@svgrid/grid 1.2.22 → 1.2.24

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 (263) hide show
  1. package/dist/GridFooter.svelte +34 -20
  2. package/dist/GridFooter.svelte.d.ts +8 -0
  3. package/dist/SvAutoComplete.svelte +89 -0
  4. package/dist/SvAutoComplete.svelte.d.ts +16 -0
  5. package/dist/SvButton.svelte +97 -0
  6. package/dist/SvButton.svelte.d.ts +23 -0
  7. package/dist/SvCalendar.svelte +409 -0
  8. package/dist/SvCalendar.svelte.d.ts +52 -0
  9. package/dist/SvCheckBox.svelte +82 -0
  10. package/dist/SvCheckBox.svelte.d.ts +19 -0
  11. package/dist/SvColorInput.svelte +160 -0
  12. package/dist/SvColorInput.svelte.d.ts +15 -0
  13. package/dist/SvComboBox.svelte +108 -0
  14. package/dist/SvComboBox.svelte.d.ts +15 -0
  15. package/dist/SvCountryInput.svelte +125 -0
  16. package/dist/SvCountryInput.svelte.d.ts +14 -0
  17. package/dist/SvDateTimePicker.svelte +265 -0
  18. package/dist/SvDateTimePicker.svelte.d.ts +38 -0
  19. package/dist/SvDropDownList.svelte +120 -0
  20. package/dist/SvDropDownList.svelte.d.ts +15 -0
  21. package/dist/SvForm.svelte +147 -0
  22. package/dist/SvForm.svelte.d.ts +29 -0
  23. package/dist/SvGauge.svelte +97 -0
  24. package/dist/SvGauge.svelte.d.ts +26 -0
  25. package/dist/SvGrid.controller.svelte.d.ts +16 -2
  26. package/dist/SvGrid.controller.svelte.js +63 -8
  27. package/dist/SvGrid.css +84 -31
  28. package/dist/SvGrid.helpers.d.ts +4 -0
  29. package/dist/SvGrid.helpers.js +40 -12
  30. package/dist/SvGrid.svelte +84 -5
  31. package/dist/SvGrid.types.d.ts +36 -0
  32. package/dist/SvGridDropdown.svelte +16 -30
  33. package/dist/SvGridDropdown.svelte.d.ts +4 -0
  34. package/dist/SvListBox.svelte +89 -0
  35. package/dist/SvListBox.svelte.d.ts +22 -0
  36. package/dist/SvMaskedInput.svelte +66 -0
  37. package/dist/SvMaskedInput.svelte.d.ts +15 -0
  38. package/dist/SvNumberInput.svelte +116 -0
  39. package/dist/SvNumberInput.svelte.d.ts +18 -0
  40. package/dist/SvPasswordInput.svelte +100 -0
  41. package/dist/SvPasswordInput.svelte.d.ts +18 -0
  42. package/dist/SvPhoneInput.svelte +87 -0
  43. package/dist/SvPhoneInput.svelte.d.ts +19 -0
  44. package/dist/SvRadioGroup.svelte +85 -0
  45. package/dist/SvRadioGroup.svelte.d.ts +20 -0
  46. package/dist/SvRating.svelte +91 -0
  47. package/dist/SvRating.svelte.d.ts +18 -0
  48. package/dist/SvRepeatButton.svelte +89 -0
  49. package/dist/SvRepeatButton.svelte.d.ts +21 -0
  50. package/dist/SvSlider.svelte +155 -0
  51. package/dist/SvSlider.svelte.d.ts +20 -0
  52. package/dist/SvSwitchButton.svelte +87 -0
  53. package/dist/SvSwitchButton.svelte.d.ts +14 -0
  54. package/dist/SvTabs.svelte +109 -0
  55. package/dist/SvTabs.svelte.d.ts +26 -0
  56. package/dist/SvTagsInput.svelte +70 -0
  57. package/dist/SvTagsInput.svelte.d.ts +14 -0
  58. package/dist/SvTimePicker.svelte +201 -0
  59. package/dist/SvTimePicker.svelte.d.ts +30 -0
  60. package/dist/SvToggleButton.svelte +57 -0
  61. package/dist/SvToggleButton.svelte.d.ts +16 -0
  62. package/dist/SvTree.svelte +131 -0
  63. package/dist/SvTree.svelte.d.ts +27 -0
  64. package/dist/build-api.js +78 -1
  65. package/dist/cdn/svgrid.js +11674 -5907
  66. package/dist/cdn/svgrid.svelte-external.js +10436 -4671
  67. package/dist/cell-render.d.ts +4 -0
  68. package/dist/cell-render.js +35 -1
  69. package/dist/clipboard.js +19 -1
  70. package/dist/collaboration.d.ts +2 -0
  71. package/dist/collaboration.js +4 -0
  72. package/dist/core.d.ts +22 -1
  73. package/dist/countries.d.ts +15 -0
  74. package/dist/countries.js +61 -0
  75. package/dist/createAutocomplete.svelte.d.ts +77 -0
  76. package/dist/createAutocomplete.svelte.js +100 -0
  77. package/dist/createCalendar.svelte.d.ts +192 -0
  78. package/dist/createCalendar.svelte.js +402 -0
  79. package/dist/createCheckbox.svelte.d.ts +45 -0
  80. package/dist/createCheckbox.svelte.js +26 -0
  81. package/dist/createColorInput.svelte.d.ts +67 -0
  82. package/dist/createColorInput.svelte.js +100 -0
  83. package/dist/createCombobox.svelte.d.ts +102 -0
  84. package/dist/createCombobox.svelte.js +171 -0
  85. package/dist/createCountryInput.svelte.d.ts +86 -0
  86. package/dist/createCountryInput.svelte.js +126 -0
  87. package/dist/createDateTimePicker.svelte.d.ts +83 -0
  88. package/dist/createDateTimePicker.svelte.js +202 -0
  89. package/dist/createDropdownList.svelte.d.ts +78 -0
  90. package/dist/createDropdownList.svelte.js +117 -0
  91. package/dist/createGauge.svelte.d.ts +56 -0
  92. package/dist/createGauge.svelte.js +49 -0
  93. package/dist/createListbox.svelte.d.ts +79 -0
  94. package/dist/createListbox.svelte.js +117 -0
  95. package/dist/createMaskedInput.svelte.d.ts +30 -0
  96. package/dist/createMaskedInput.svelte.js +62 -0
  97. package/dist/createNumberInput.svelte.d.ts +72 -0
  98. package/dist/createNumberInput.svelte.js +167 -0
  99. package/dist/createPasswordInput.svelte.d.ts +60 -0
  100. package/dist/createPasswordInput.svelte.js +72 -0
  101. package/dist/createPhoneInput.svelte.d.ts +46 -0
  102. package/dist/createPhoneInput.svelte.js +105 -0
  103. package/dist/createRadioGroup.svelte.d.ts +64 -0
  104. package/dist/createRadioGroup.svelte.js +73 -0
  105. package/dist/createRating.svelte.d.ts +70 -0
  106. package/dist/createRating.svelte.js +85 -0
  107. package/dist/createSlider.svelte.d.ts +71 -0
  108. package/dist/createSlider.svelte.js +123 -0
  109. package/dist/createSwitch.svelte.d.ts +43 -0
  110. package/dist/createSwitch.svelte.js +41 -0
  111. package/dist/createTabs.svelte.d.ts +78 -0
  112. package/dist/createTabs.svelte.js +92 -0
  113. package/dist/createTagsInput.svelte.d.ts +65 -0
  114. package/dist/createTagsInput.svelte.js +93 -0
  115. package/dist/createTimePicker.svelte.d.ts +106 -0
  116. package/dist/createTimePicker.svelte.js +248 -0
  117. package/dist/createToggle.svelte.d.ts +43 -0
  118. package/dist/createToggle.svelte.js +39 -0
  119. package/dist/createTree.svelte.d.ts +96 -0
  120. package/dist/createTree.svelte.js +194 -0
  121. package/dist/datetime/date-core.d.ts +88 -0
  122. package/dist/datetime/date-core.js +165 -0
  123. package/dist/datetime/date-format.d.ts +39 -0
  124. package/dist/datetime/date-format.js +371 -0
  125. package/dist/datetime/date-restrict.d.ts +23 -0
  126. package/dist/datetime/date-restrict.js +50 -0
  127. package/dist/datetime/date-selection.d.ts +25 -0
  128. package/dist/datetime/date-selection.js +94 -0
  129. package/dist/datetime/mask.d.ts +21 -0
  130. package/dist/datetime/mask.js +90 -0
  131. package/dist/editing.d.ts +1 -1
  132. package/dist/editing.js +7 -0
  133. package/dist/editor-contract.d.ts +48 -0
  134. package/dist/editor-contract.js +26 -0
  135. package/dist/editors/cell-editors.d.ts +2 -2
  136. package/dist/editors/cell-editors.js +3 -1
  137. package/dist/export-format.d.ts +178 -0
  138. package/dist/export-format.js +419 -0
  139. package/dist/hyperformula-adapter.d.ts +4 -0
  140. package/dist/hyperformula-adapter.js +15 -6
  141. package/dist/index.d.ts +60 -0
  142. package/dist/index.js +72 -0
  143. package/dist/list-option.d.ts +10 -0
  144. package/dist/list-option.js +7 -0
  145. package/dist/menus.js +18 -2
  146. package/dist/popover.d.ts +48 -0
  147. package/dist/popover.js +65 -0
  148. package/dist/row-resize.d.ts +4 -4
  149. package/dist/row-resize.js +8 -5
  150. package/dist/selection.d.ts +6 -0
  151. package/dist/selection.js +31 -5
  152. package/dist/server-data-source.d.ts +43 -0
  153. package/dist/server-data-source.js +100 -0
  154. package/dist/sparkline.js +8 -4
  155. package/dist/svgrid-wrapper.types.d.ts +26 -1
  156. package/package.json +6 -1
  157. package/src/GridFooter.svelte +34 -20
  158. package/src/SvAutoComplete.svelte +89 -0
  159. package/src/SvButton.svelte +97 -0
  160. package/src/SvCalendar.svelte +409 -0
  161. package/src/SvCalendar.test.ts +211 -0
  162. package/src/SvCheckBox.svelte +82 -0
  163. package/src/SvColorInput.svelte +160 -0
  164. package/src/SvComboBox.svelte +108 -0
  165. package/src/SvCountryInput.svelte +125 -0
  166. package/src/SvDateTimePicker.svelte +265 -0
  167. package/src/SvDateTimePicker.test.ts +136 -0
  168. package/src/SvDropDownList.svelte +120 -0
  169. package/src/SvForm.svelte +147 -0
  170. package/src/SvGauge.svelte +97 -0
  171. package/src/SvGrid.controller.svelte.ts +74 -8
  172. package/src/SvGrid.css +84 -31
  173. package/src/SvGrid.helpers.ts +41 -12
  174. package/src/SvGrid.svelte +84 -5
  175. package/src/SvGrid.types.ts +33 -0
  176. package/src/SvGridDropdown.svelte +16 -30
  177. package/src/SvListBox.svelte +89 -0
  178. package/src/SvMaskedInput.svelte +66 -0
  179. package/src/SvNumberInput.svelte +116 -0
  180. package/src/SvPasswordInput.svelte +100 -0
  181. package/src/SvPhoneInput.svelte +87 -0
  182. package/src/SvRadioGroup.svelte +85 -0
  183. package/src/SvRating.svelte +91 -0
  184. package/src/SvRepeatButton.svelte +89 -0
  185. package/src/SvSlider.svelte +155 -0
  186. package/src/SvSwitchButton.svelte +87 -0
  187. package/src/SvTabs.svelte +109 -0
  188. package/src/SvTagsInput.svelte +70 -0
  189. package/src/SvTimePicker.svelte +201 -0
  190. package/src/SvTimePicker.test.ts +98 -0
  191. package/src/SvToggleButton.svelte +57 -0
  192. package/src/SvTree.svelte +131 -0
  193. package/src/build-api.ts +115 -0
  194. package/src/cell-render.test.ts +38 -0
  195. package/src/cell-render.ts +43 -0
  196. package/src/clipboard.test.ts +18 -0
  197. package/src/clipboard.ts +24 -1
  198. package/src/collaboration.test.ts +30 -0
  199. package/src/collaboration.ts +6 -0
  200. package/src/core.ts +27 -3
  201. package/src/countries.ts +71 -0
  202. package/src/createAutocomplete.svelte.ts +104 -0
  203. package/src/createCalendar.svelte.ts +488 -0
  204. package/src/createCheckbox.svelte.ts +74 -0
  205. package/src/createColorInput.svelte.ts +115 -0
  206. package/src/createCombobox.svelte.ts +158 -0
  207. package/src/createCountryInput.svelte.ts +126 -0
  208. package/src/createDateTimePicker.svelte.ts +216 -0
  209. package/src/createDropdownList.svelte.ts +125 -0
  210. package/src/createGauge.svelte.ts +86 -0
  211. package/src/createListbox.svelte.ts +186 -0
  212. package/src/createMaskedInput.svelte.ts +81 -0
  213. package/src/createNumberInput.svelte.ts +193 -0
  214. package/src/createPasswordInput.svelte.ts +87 -0
  215. package/src/createPhoneInput.svelte.ts +119 -0
  216. package/src/createRadioGroup.svelte.ts +132 -0
  217. package/src/createRating.svelte.ts +145 -0
  218. package/src/createSlider.svelte.ts +160 -0
  219. package/src/createSwitch.svelte.ts +78 -0
  220. package/src/createTabs.svelte.ts +129 -0
  221. package/src/createTagsInput.svelte.ts +98 -0
  222. package/src/createTimePicker.svelte.ts +289 -0
  223. package/src/createToggle.svelte.ts +69 -0
  224. package/src/createTree.svelte.ts +216 -0
  225. package/src/datetime/date-core.test.ts +217 -0
  226. package/src/datetime/date-core.ts +204 -0
  227. package/src/datetime/date-format.test.ts +121 -0
  228. package/src/datetime/date-format.ts +317 -0
  229. package/src/datetime/date-restrict.ts +60 -0
  230. package/src/datetime/date-selection.test.ts +129 -0
  231. package/src/datetime/date-selection.ts +137 -0
  232. package/src/datetime/mask.test.ts +36 -0
  233. package/src/datetime/mask.ts +84 -0
  234. package/src/editing.test.ts +22 -0
  235. package/src/editing.ts +7 -1
  236. package/src/editor-contract.test.ts +32 -0
  237. package/src/editor-contract.ts +60 -0
  238. package/src/editors/cell-editors.ts +7 -1
  239. package/src/export-data-api.test.ts +126 -0
  240. package/src/export-format.test.ts +107 -0
  241. package/src/export-format.ts +598 -0
  242. package/src/hyperformula-adapter.test.ts +35 -1
  243. package/src/hyperformula-adapter.ts +18 -6
  244. package/src/index.ts +110 -0
  245. package/src/list-option.ts +15 -0
  246. package/src/menus.ts +21 -2
  247. package/src/popover.ts +79 -0
  248. package/src/row-resize.test.ts +25 -0
  249. package/src/row-resize.ts +8 -5
  250. package/src/selection.test.ts +36 -4
  251. package/src/selection.ts +30 -9
  252. package/src/server-data-source.test.ts +201 -0
  253. package/src/server-data-source.ts +148 -0
  254. package/src/sparkline.test.ts +9 -0
  255. package/src/sparkline.ts +9 -4
  256. package/src/svgrid-wrapper.types.ts +28 -1
  257. package/src/svgrid.behavior.test.ts +27 -0
  258. package/src/svgrid.new-features.wrapper.test.ts +27 -1
  259. package/src/ui-buttons.test.ts +105 -0
  260. package/src/ui-composite.test.ts +89 -0
  261. package/src/ui-inputs.test.ts +118 -0
  262. package/src/ui-range.test.ts +61 -0
  263. package/src/ui-selection.test.ts +119 -0
package/dist/menus.js CHANGED
@@ -216,21 +216,37 @@ export function createMenus(ctx) {
216
216
  ctx.filterRowValues = next;
217
217
  }
218
218
  }
219
+ // External pagination is controlled: emit the requested page/size and let
220
+ // the consumer fetch + update props. Never mutate local pagination state.
221
+ function emitExternalPagination(pageIndex, pageSize) {
222
+ if (ctx.props.externalPagination !== true)
223
+ return false;
224
+ ctx.props.onPaginationChange?.({ pageIndex: Math.max(0, pageIndex), pageSize });
225
+ ctx.scrollContainer?.scrollTo?.({ top: 0 });
226
+ return true;
227
+ }
219
228
  function changePage(delta) {
229
+ if (emitExternalPagination((ctx.props.pageIndex ?? 0) + delta, ctx.props.pageSize ?? 10))
230
+ return;
220
231
  ctx.grid.setPagination((prev) => ({
221
232
  ...prev,
222
233
  pageIndex: Math.max((prev?.pageIndex ?? 0) + delta, 0),
223
234
  }));
224
- ctx.scrollContainer?.scrollTo({ top: 0 });
235
+ ctx.scrollContainer?.scrollTo?.({ top: 0 });
225
236
  }
226
237
  function goToPage(pageIndex) {
238
+ if (emitExternalPagination(pageIndex, ctx.props.pageSize ?? 10))
239
+ return;
227
240
  ctx.grid.setPagination((prev) => ({
228
241
  ...prev,
229
242
  pageIndex: Math.max(0, pageIndex),
230
243
  }));
231
- ctx.scrollContainer?.scrollTo({ top: 0 });
244
+ ctx.scrollContainer?.scrollTo?.({ top: 0 });
232
245
  }
233
246
  function setPageSize(pageSize) {
247
+ // External mode: changing page size resets to the first page.
248
+ if (emitExternalPagination(0, pageSize))
249
+ return;
234
250
  ctx.grid.setPagination((prev) => {
235
251
  const oldSize = prev?.pageSize ?? 10;
236
252
  const oldIndex = prev?.pageIndex ?? 0;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * popover - shared helpers for panels that must escape the grid's
3
+ * `overflow:hidden` scroll container by portalling to <body>: the theme-var
4
+ * snapshot + portal action, and the fixed-position anchor/flip math. Rune-free
5
+ * and DOM-only so it is reused verbatim by SvGridDropdown, SvDateTimePicker and
6
+ * any future popover. The per-component `$effect` wiring (scroll/resize
7
+ * reposition, outside-click close) stays in the component since it needs the
8
+ * component's own open state.
9
+ */
10
+ /**
11
+ * Theme tokens a portalled panel must carry with it. `--sg-*` custom properties
12
+ * are scoped to whatever wrapper the grid sits inside (e.g. a per-preset theme
13
+ * class); moving the panel to <body> leaves that scope, so we snapshot the
14
+ * resolved values and pin them inline. Keep this list in sync with the tokens
15
+ * any popover panel actually consumes.
16
+ */
17
+ export declare const PANEL_THEME_VARS: readonly ["--sg-accent", "--sg-bg", "--sg-fg", "--sg-border", "--sg-muted", "--sg-header-bg", "--sg-row-hover-bg", "--sg-selection-bg", "--sg-input-bg", "--sg-input-border", "--sg-selection-fg"];
18
+ /**
19
+ * Svelte action: snapshot the resolved theme tokens from the node's current
20
+ * (in-scope) position, then detach it and append to <body> so it can never be
21
+ * clipped by an ancestor's overflow. Removes itself on destroy.
22
+ */
23
+ export declare function portalToBody(node: HTMLElement, vars?: ReadonlyArray<string>): {
24
+ destroy(): void;
25
+ };
26
+ export type AnchoredRect = {
27
+ top: number;
28
+ left: number;
29
+ width: number;
30
+ openUpward: boolean;
31
+ };
32
+ export type AnchorOptions = {
33
+ /** Estimated panel height, used to decide whether to flip upward. */
34
+ estimatedHeight: number;
35
+ /** Gap between trigger and panel. Default 2px. */
36
+ gap?: number;
37
+ /** Force the panel at least this wide (else it matches the trigger). */
38
+ minWidth?: number;
39
+ /** Keep the panel within the viewport horizontally. Default true. */
40
+ clampHorizontal?: boolean;
41
+ };
42
+ /**
43
+ * Compute a `position: fixed` rect anchored to `triggerRect`, flipping above the
44
+ * trigger when there isn't room below and there's more room above. Mirrors the
45
+ * behavior SvGridDropdown shipped with, generalized with min-width + horizontal
46
+ * clamping for wider panels (date/time popovers).
47
+ */
48
+ export declare function anchoredRect(triggerRect: DOMRect, opts: AnchorOptions): AnchoredRect;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * popover - shared helpers for panels that must escape the grid's
3
+ * `overflow:hidden` scroll container by portalling to <body>: the theme-var
4
+ * snapshot + portal action, and the fixed-position anchor/flip math. Rune-free
5
+ * and DOM-only so it is reused verbatim by SvGridDropdown, SvDateTimePicker and
6
+ * any future popover. The per-component `$effect` wiring (scroll/resize
7
+ * reposition, outside-click close) stays in the component since it needs the
8
+ * component's own open state.
9
+ */
10
+ /**
11
+ * Theme tokens a portalled panel must carry with it. `--sg-*` custom properties
12
+ * are scoped to whatever wrapper the grid sits inside (e.g. a per-preset theme
13
+ * class); moving the panel to <body> leaves that scope, so we snapshot the
14
+ * resolved values and pin them inline. Keep this list in sync with the tokens
15
+ * any popover panel actually consumes.
16
+ */
17
+ export const PANEL_THEME_VARS = [
18
+ '--sg-accent', '--sg-bg', '--sg-fg', '--sg-border', '--sg-muted',
19
+ '--sg-header-bg', '--sg-row-hover-bg', '--sg-selection-bg',
20
+ '--sg-input-bg', '--sg-input-border', '--sg-selection-fg',
21
+ ];
22
+ /**
23
+ * Svelte action: snapshot the resolved theme tokens from the node's current
24
+ * (in-scope) position, then detach it and append to <body> so it can never be
25
+ * clipped by an ancestor's overflow. Removes itself on destroy.
26
+ */
27
+ export function portalToBody(node, vars = PANEL_THEME_VARS) {
28
+ const cs = getComputedStyle(node);
29
+ for (const v of vars) {
30
+ const val = cs.getPropertyValue(v).trim();
31
+ if (val)
32
+ node.style.setProperty(v, val);
33
+ }
34
+ document.body.appendChild(node);
35
+ return {
36
+ destroy() {
37
+ if (node.parentNode === document.body)
38
+ document.body.removeChild(node);
39
+ },
40
+ };
41
+ }
42
+ /**
43
+ * Compute a `position: fixed` rect anchored to `triggerRect`, flipping above the
44
+ * trigger when there isn't room below and there's more room above. Mirrors the
45
+ * behavior SvGridDropdown shipped with, generalized with min-width + horizontal
46
+ * clamping for wider panels (date/time popovers).
47
+ */
48
+ export function anchoredRect(triggerRect, opts) {
49
+ const gap = opts.gap ?? 2;
50
+ const spaceBelow = window.innerHeight - triggerRect.bottom;
51
+ const spaceAbove = triggerRect.top;
52
+ const openUpward = spaceBelow < opts.estimatedHeight && spaceAbove > spaceBelow;
53
+ const width = Math.max(triggerRect.width, opts.minWidth ?? 0);
54
+ let left = triggerRect.left;
55
+ if (opts.clampHorizontal !== false) {
56
+ const maxLeft = window.innerWidth - width - 4;
57
+ left = Math.max(4, Math.min(left, maxLeft));
58
+ }
59
+ return {
60
+ top: openUpward ? triggerRect.top - opts.estimatedHeight - gap : triggerRect.bottom + gap,
61
+ left,
62
+ width,
63
+ openUpward,
64
+ };
65
+ }
@@ -4,10 +4,10 @@
4
4
  * reads per-row heights from a reactive store.
5
5
  *
6
6
  * The action is OFF until a consumer attaches it AND a row-header
7
- * gutter cell exists in each row. A row-header cell is any TD with
8
- * the `sv-row-gutter` class (typically the one rendering the row
9
- * number 1, 2, 3, ...). Demos opt in by setting
10
- * `cellClass: 'sv-row-gutter'` on the gutter column.
7
+ * gutter cell exists in each row. A row-header cell is either the
8
+ * grid's built-in row-number gutter (enable `showRowNumbers`) or any
9
+ * TD tagged with the `sv-row-gutter` class (set
10
+ * `cellClass: 'sv-row-gutter'` on a custom gutter column).
11
11
  *
12
12
  * <script>
13
13
  * let heights = $state<Record<number, number>>({})
@@ -4,10 +4,10 @@
4
4
  * reads per-row heights from a reactive store.
5
5
  *
6
6
  * The action is OFF until a consumer attaches it AND a row-header
7
- * gutter cell exists in each row. A row-header cell is any TD with
8
- * the `sv-row-gutter` class (typically the one rendering the row
9
- * number 1, 2, 3, ...). Demos opt in by setting
10
- * `cellClass: 'sv-row-gutter'` on the gutter column.
7
+ * gutter cell exists in each row. A row-header cell is either the
8
+ * grid's built-in row-number gutter (enable `showRowNumbers`) or any
9
+ * TD tagged with the `sv-row-gutter` class (set
10
+ * `cellClass: 'sv-row-gutter'` on a custom gutter column).
11
11
  *
12
12
  * <script>
13
13
  * let heights = $state<Record<number, number>>({})
@@ -103,7 +103,10 @@ export function rowResize(node, opts) {
103
103
  continue;
104
104
  if (tr.classList.contains('sv-grid-row-spacer'))
105
105
  continue;
106
- const gutter = tr.querySelector('.sv-grid-cell.sv-row-gutter');
106
+ // A row-header gutter cell: either a user column tagged
107
+ // `sv-row-gutter`, or the grid's built-in row-number gutter
108
+ // (`showRowNumbers`), whose cell is `.sv-grid-row-number-cell`.
109
+ const gutter = tr.querySelector('.sv-grid-cell.sv-row-gutter, .sv-grid-cell.sv-grid-row-number-cell');
107
110
  if (!gutter)
108
111
  continue;
109
112
  if (gutter.querySelector(`:scope > .${STRIP_CLASS}`))
@@ -15,6 +15,12 @@ export declare function createSelection<TFeatures extends TableFeatures = TableF
15
15
  left: boolean;
16
16
  right: boolean;
17
17
  } | null;
18
+ fillMarqueeEdges: (rowIndex: number, colIndex: number) => {
19
+ top: boolean;
20
+ bottom: boolean;
21
+ left: boolean;
22
+ right: boolean;
23
+ } | null;
18
24
  getSelectionRects: () => {
19
25
  minRow: number;
20
26
  maxRow: number;
package/dist/selection.js CHANGED
@@ -264,6 +264,31 @@ export function createSelection(ctx) {
264
264
  colIndex <= d.sourceMaxCol;
265
265
  return !inSource;
266
266
  }
267
+ /**
268
+ * Which edges of the fill-drag rectangle a cell sits on, so the render can
269
+ * paint a single dashed marquee around the WHOLE target range (source +
270
+ * extension) - the Excel fill-handle look - instead of a thick per-cell
271
+ * border. Returns null when the cell isn't on the rectangle's boundary.
272
+ */
273
+ function fillMarqueeEdges(rowIndex, colIndex) {
274
+ const d = ctx.fillDrag;
275
+ if (!d)
276
+ return null;
277
+ const minR = Math.min(d.sourceMinRow, d.targetRow);
278
+ const maxR = Math.max(d.sourceMaxRow, d.targetRow);
279
+ const minC = Math.min(d.sourceMinCol, d.targetCol);
280
+ const maxC = Math.max(d.sourceMaxCol, d.targetCol);
281
+ if (rowIndex < minR || rowIndex > maxR || colIndex < minC || colIndex > maxC) {
282
+ return null;
283
+ }
284
+ const top = rowIndex === minR;
285
+ const bottom = rowIndex === maxR;
286
+ const left = colIndex === minC;
287
+ const right = colIndex === maxC;
288
+ if (!top && !bottom && !left && !right)
289
+ return null;
290
+ return { top, bottom, left, right };
291
+ }
267
292
  /** Look up a column by id without depending on `buildApi`'s private
268
293
  * closure (those helpers don't exist at this scope). */
269
294
  function findColumnById(columnId) {
@@ -276,11 +301,11 @@ export function createSelection(ctx) {
276
301
  const column = ctx.allColumns[colIndex];
277
302
  if (!row || !column || isGroupRow(row))
278
303
  return;
279
- const cellValue = ctx.getCellDisplayValue(row.id, column.id, getColumnBaseValue(row, column));
280
- const isCheckboxColumn = column.columnDef.editorType === "checkbox" ||
281
- typeof cellValue === "boolean";
282
- if (isCheckboxColumn)
283
- return; // let onCellClick toggle the checkbox
304
+ // NOTE: we no longer bail for checkbox columns. Bailing here meant a
305
+ // range drag could never START on a checkbox cell. A plain click still
306
+ // toggles the checkbox via onCellClick (a same-cell click), while a
307
+ // cross-cell drag fires its `click` on a common ancestor - not the cell -
308
+ // so it selects a range without toggling.
284
309
  const active = ctx.grid.getState().activeCell;
285
310
  ctx.activeAtPointerDown = active
286
311
  ? { rowIndex: active.rowIndex, colIndex: active.colIndex }
@@ -414,6 +439,7 @@ export function createSelection(ctx) {
414
439
  extendSelection,
415
440
  isCellInSelectedRange,
416
441
  getCellRangeEdges,
442
+ fillMarqueeEdges,
417
443
  getSelectionRects,
418
444
  isInFillPreview,
419
445
  findColumnById,
@@ -8,6 +8,12 @@
8
8
  * It's headless and framework-agnostic on purpose: wire `setSort` /
9
9
  * `setFilter` / `setPage` to the grid's controlled callbacks, and render the
10
10
  * grid from the `{ rows, total, loading }` it hands you. See the demo.
11
+ *
12
+ * The write side (`createRow` / `updateRow` / `deleteRow`) is optional: a
13
+ * source that only implements `getRows` stays a pure read model, and the
14
+ * matching controller methods throw a clear error if called. Mutations are
15
+ * non-optimistic for now - the grid reflects a change only after the
16
+ * follow-up re-fetch of the current page lands.
11
17
  */
12
18
  export type ServerSortModel = Array<{
13
19
  id: string;
@@ -45,11 +51,22 @@ export type ServerResult<TData> = {
45
51
  };
46
52
  export type ServerDataSource<TData> = {
47
53
  getRows(request: ServerRequest): Promise<ServerResult<TData>>;
54
+ /**
55
+ * Optional write side. Implement whichever your backend supports; the
56
+ * controller exposes matching `createRow` / `updateRow` / `deleteRow`
57
+ * methods that call through and then `refresh()` the current page.
58
+ * Calling a controller method whose source counterpart is missing throws.
59
+ */
60
+ createRow?(input: Partial<TData>): Promise<TData>;
61
+ updateRow?(id: string, patch: Partial<TData>): Promise<TData>;
62
+ deleteRow?(id: string): Promise<void>;
48
63
  };
49
64
  export type ServerState<TData> = {
50
65
  rows: ReadonlyArray<TData>;
51
66
  total: number;
52
67
  loading: boolean;
68
+ /** True while a create / update / delete mutation is in flight. */
69
+ saving: boolean;
53
70
  error: unknown;
54
71
  pageIndex: number;
55
72
  pageSize: number;
@@ -64,6 +81,23 @@ export type ServerController<TData> = {
64
81
  setFilter(filterModel: ServerFilterModel): void;
65
82
  setPage(pageIndex: number): void;
66
83
  setPageSize(pageSize: number): void;
84
+ /**
85
+ * Create a row through the source, then refresh the current page. Resolves
86
+ * with the created row. Rejects if the source has no `createRow` (or if the
87
+ * create itself fails - the read state is left untouched on failure).
88
+ */
89
+ createRow(input: Partial<TData>): Promise<TData>;
90
+ /**
91
+ * Update a row by id through the source. Non-optimistic: refreshes the page.
92
+ * Optimistic (see `optimistic` + `getRowId` options): patches the local row
93
+ * immediately, then reconciles with the server result, rolling back on error.
94
+ */
95
+ updateRow(id: string, patch: Partial<TData>): Promise<TData>;
96
+ /**
97
+ * Delete a row by id through the source. Non-optimistic: refreshes the page.
98
+ * Optimistic: removes the local row immediately, restoring it on error.
99
+ */
100
+ deleteRow(id: string): Promise<void>;
67
101
  getState(): ServerState<TData>;
68
102
  /** Stop accepting in-flight responses (call on unmount). */
69
103
  dispose(): void;
@@ -72,5 +106,14 @@ export type ServerControllerOptions<TData> = {
72
106
  pageSize?: number;
73
107
  /** Called whenever any of `rows` / `total` / `loading` / page changes. */
74
108
  onChange: (state: ServerState<TData>) => void;
109
+ /**
110
+ * Apply `updateRow` / `deleteRow` to the local rows immediately (before the
111
+ * server confirms) and roll back on error - so edits feel instant and no
112
+ * refetch is needed. Requires `getRowId` to locate rows; ignored without it.
113
+ * A subsequent `refresh()` reconciles ordering/filtering. Default false.
114
+ */
115
+ optimistic?: boolean;
116
+ /** Resolve a row's stable id, so optimistic update/delete can find it in `rows`. */
117
+ getRowId?: (row: TData) => string;
75
118
  };
76
119
  export declare function createServerDataSource<TData>(source: ServerDataSource<TData>, options: ServerControllerOptions<TData>): ServerController<TData>;
@@ -3,6 +3,7 @@ export function createServerDataSource(source, options) {
3
3
  rows: [],
4
4
  total: 0,
5
5
  loading: false,
6
+ saving: false,
6
7
  error: null,
7
8
  pageIndex: 0,
8
9
  pageSize: options.pageSize ?? 50,
@@ -51,8 +52,100 @@ export function createServerDataSource(source, options) {
51
52
  emit();
52
53
  }
53
54
  }
55
+ // Shared mutation lifecycle: flip `saving`, run the write, refresh the
56
+ // current page on success, and always clear `saving`. A missing source
57
+ // method (or a disposed controller) rejects before anything is emitted.
58
+ function mutate(name, thunk) {
59
+ if (disposed) {
60
+ return Promise.reject(new Error('createServerDataSource: controller is disposed'));
61
+ }
62
+ if (!thunk) {
63
+ return Promise.reject(new Error(`createServerDataSource: the datasource does not implement ${name}()`));
64
+ }
65
+ state.saving = true;
66
+ emit();
67
+ return (async () => {
68
+ try {
69
+ const result = await thunk();
70
+ await fetchPage();
71
+ return result;
72
+ }
73
+ finally {
74
+ state.saving = false;
75
+ emit();
76
+ }
77
+ })();
78
+ }
79
+ const optimistic = !!options.optimistic && !!options.getRowId;
80
+ const getRowId = options.getRowId;
81
+ // Optimistic update: patch the local row, reconcile with the server result,
82
+ // roll back on error. Falls back to the plain refresh path when the row
83
+ // isn't on the current page (nothing local to update).
84
+ async function optimisticUpdate(id, patch, fn) {
85
+ if (disposed)
86
+ throw new Error('createServerDataSource: controller is disposed');
87
+ const prevRows = state.rows;
88
+ const idx = prevRows.findIndex((r) => getRowId(r) === id);
89
+ if (idx < 0)
90
+ return mutate('updateRow', () => fn(id, patch));
91
+ state.rows = [...prevRows.slice(0, idx), { ...prevRows[idx], ...patch }, ...prevRows.slice(idx + 1)];
92
+ state.saving = true;
93
+ emit();
94
+ try {
95
+ const result = await fn(id, patch);
96
+ state.rows = state.rows.map((r) => (getRowId(r) === id ? result : r));
97
+ return result;
98
+ }
99
+ catch (err) {
100
+ state.rows = prevRows;
101
+ throw err;
102
+ }
103
+ finally {
104
+ state.saving = false;
105
+ emit();
106
+ }
107
+ }
108
+ // Optimistic delete: drop the local row + decrement total, restore on error.
109
+ async function optimisticDelete(id, fn) {
110
+ if (disposed)
111
+ throw new Error('createServerDataSource: controller is disposed');
112
+ const prevRows = state.rows;
113
+ const prevTotal = state.total;
114
+ const next = prevRows.filter((r) => getRowId(r) !== id);
115
+ if (next.length === prevRows.length)
116
+ return mutate('deleteRow', () => fn(id));
117
+ state.rows = next;
118
+ state.total = Math.max(0, prevTotal - (prevRows.length - next.length));
119
+ state.saving = true;
120
+ emit();
121
+ try {
122
+ await fn(id);
123
+ }
124
+ catch (err) {
125
+ state.rows = prevRows;
126
+ state.total = prevTotal;
127
+ throw err;
128
+ }
129
+ finally {
130
+ state.saving = false;
131
+ emit();
132
+ }
133
+ }
54
134
  return {
55
135
  refresh: fetchPage,
136
+ createRow: (input) => mutate('createRow', source.createRow ? () => source.createRow(input) : null),
137
+ updateRow: (id, patch) => {
138
+ if (!source.updateRow)
139
+ return mutate('updateRow', null);
140
+ const fn = source.updateRow;
141
+ return optimistic ? optimisticUpdate(id, patch, fn) : mutate('updateRow', () => fn(id, patch));
142
+ },
143
+ deleteRow: (id) => {
144
+ if (!source.deleteRow)
145
+ return mutate('deleteRow', null);
146
+ const fn = source.deleteRow;
147
+ return optimistic ? optimisticDelete(id, fn) : mutate('deleteRow', () => fn(id));
148
+ },
56
149
  setSort(sortModel) {
57
150
  state.sortModel = sortModel;
58
151
  state.pageIndex = 0;
@@ -78,6 +171,13 @@ export function createServerDataSource(source, options) {
78
171
  getState: () => ({ ...state }),
79
172
  dispose() {
80
173
  disposed = true;
174
+ // An in-flight fetch's resolution short-circuits on `disposed`, so it
175
+ // never clears `loading`. Clear it here (and emit) so a disposed
176
+ // controller doesn't report a permanent loading state.
177
+ if (state.loading) {
178
+ state.loading = false;
179
+ emit();
180
+ }
81
181
  },
82
182
  };
83
183
  }
package/dist/sparkline.js CHANGED
@@ -72,12 +72,16 @@ export function buildSparkline(values, cfg = {}) {
72
72
  const n = values.length;
73
73
  const stepX = n > 1 ? innerW / (n - 1) : 0;
74
74
  const pts = values.map((v, i) => ({
75
- x: pad + i * stepX,
75
+ // A single point has no span to walk, so centre it rather than pinning it
76
+ // to the left edge.
77
+ x: n === 1 ? pad + innerW / 2 : pad + i * stepX,
76
78
  y: pad + innerH - ((v - min) / span) * innerH,
77
79
  }));
78
- const linePath = pts
79
- .map((p, i) => `${i === 0 ? 'M' : 'L'}${round(p.x)},${round(p.y)}`)
80
- .join(' ');
80
+ const linePath = n === 1
81
+ ? // One point produces only a moveto, which draws nothing. Emit a tiny
82
+ // horizontal segment so a single value still renders a visible mark.
83
+ `M${round(pts[0].x - 1)},${round(pts[0].y)} L${round(pts[0].x + 1)},${round(pts[0].y)}`
84
+ : pts.map((p, i) => `${i === 0 ? 'M' : 'L'}${round(p.x)},${round(p.y)}`).join(' ');
81
85
  const first = pts[0];
82
86
  const last = pts[pts.length - 1];
83
87
  const baseY = pad + innerH;
@@ -1,4 +1,5 @@
1
- import type { ColumnDef, RowData, SvGridOptions, TableFeatures } from './core';
1
+ import type { CellFormatConfig, ColumnDef, RowData, SvGridOptions, TableFeatures } from './core';
2
+ import type { GridExportOptions, GridClipboardOptions } from './export-format';
2
3
  export type SvGridFilterOperator = 'contains' | 'equals' | 'startsWith' | 'greaterThan' | 'lessThan' | 'between' | 'isBlank';
3
4
  /**
4
5
  * A serializable snapshot of everything that makes up the current "view":
@@ -168,7 +169,31 @@ export type SvGridApi<TFeatures extends TableFeatures, TData extends RowData> =
168
169
  field?: string;
169
170
  header: string;
170
171
  visible: boolean;
172
+ /** The column's `format` config, when set. Lets an exporter reproduce
173
+ * the on-screen display value (currency, date pattern, etc.). */
174
+ format?: CellFormatConfig;
175
+ /** Effective horizontal alignment ('left' | 'center' | 'right'). */
176
+ align?: 'left' | 'center' | 'right';
171
177
  }>;
178
+ /**
179
+ * Export the grid to a **CSV** file. Free in the community grid; the
180
+ * richer Excel / PDF / styled formats live in @svgrid/enterprise. Values
181
+ * are formatted as shown on screen (pass `rawValues: true` for raw). Rows
182
+ * default to the current view (`rows: 'selected' | 'all'` to change).
183
+ * Resolves with the serialized text; pass `download: false` to skip the
184
+ * browser download and just get the string.
185
+ */
186
+ exportCsv(options?: GridExportOptions): Promise<string>;
187
+ /** Export the grid to a **TSV** file (tab-separated). See `exportCsv`. */
188
+ exportTsv(options?: GridExportOptions): Promise<string>;
189
+ /** Export the grid to a **JSON** file (array of `{ field: value }`). */
190
+ exportJson(options?: GridExportOptions): Promise<string>;
191
+ /**
192
+ * Copy the grid to the system clipboard. `format: 'tsv'` (default) pastes
193
+ * straight into Excel / Sheets; `'csv'` / `'markdown'` also supported.
194
+ * Resolves with the copied text.
195
+ */
196
+ copyToClipboard(options?: GridClipboardOptions): Promise<string>;
172
197
  /** Clear every checked row. Emits `onRowSelectionChange({}, [])`. */
173
198
  clearRowSelection(): void;
174
199
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svgrid/grid",
3
- "version": "1.2.22",
3
+ "version": "1.2.24",
4
4
  "description": "SvGrid - a headless-first, Svelte 5-native data grid engine and render component.",
5
5
  "author": "jQWidgets <boikom@jqwidgets.com>",
6
6
  "license": "MIT",
@@ -42,6 +42,11 @@
42
42
  "svelte": "./dist/static-functions.js",
43
43
  "import": "./dist/static-functions.js"
44
44
  },
45
+ "./format": {
46
+ "types": "./dist/export-format.d.ts",
47
+ "svelte": "./dist/export-format.js",
48
+ "import": "./dist/export-format.js"
49
+ },
45
50
  "./flex-render": {
46
51
  "types": "./dist/flex-render.d.ts",
47
52
  "svelte": "./dist/flex-render.js",
@@ -47,7 +47,23 @@
47
47
  } from "./SvGrid.controller.svelte";
48
48
  import type { SvGridController } from "./SvGrid.controller.svelte";
49
49
 
50
- let { ctrl }: { ctrl: SvGridController<TFeatures, TData> } = $props();
50
+ let {
51
+ ctrl,
52
+ pager = true,
53
+ showStatus = true,
54
+ top = false,
55
+ pageSizeOptions,
56
+ }: {
57
+ ctrl: SvGridController<TFeatures, TData>;
58
+ /** Render the pagination controls in this instance. */
59
+ pager?: boolean;
60
+ /** Render the status bar in this instance. */
61
+ showStatus?: boolean;
62
+ /** This is a top-positioned footer (styling only). */
63
+ top?: boolean;
64
+ /** Page-size choices for the selector. Defaults to `[10, 25, 50, 100]`. */
65
+ pageSizeOptions?: number[];
66
+ } = $props();
51
67
 
52
68
  // View facade: re-bind the controller's reactive members so the markup
53
69
  // (moved verbatim from SvGrid.svelte) stays identical.
@@ -63,7 +79,7 @@
63
79
  const setPageSize = $derived(ctrl.setPageSize);
64
80
  </script>
65
81
 
66
- {#if statusBarEnabled && statusBarStats}
82
+ {#if showStatus && statusBarEnabled && statusBarStats}
67
83
  {@const s = statusBarStats}
68
84
  <div class="sv-grid-status-bar" role="status" aria-live="polite">
69
85
  {#each statusBarAggregates as agg (agg)}
@@ -96,31 +112,29 @@
96
112
  </div>
97
113
  {/if}
98
114
 
99
- {#if paginationEnabled}
100
- {@const totalRows = allRowsBeforePagination.length}
101
- {@const pageSize = paginationState.pageSize}
115
+ {#if pager && paginationEnabled}
116
+ {@const totalRows = ctrl.paginationTotalRows}
117
+ {@const pageSize = ctrl.paginationPageSize}
102
118
  {@const pageCount = Math.max(1, Math.ceil(totalRows / pageSize))}
103
- {@const currentPage = Math.min(paginationState.pageIndex + 1, pageCount)}
119
+ {@const currentPage = Math.min(ctrl.paginationPageIndex + 1, pageCount)}
104
120
  {@const rangeStart =
105
121
  totalRows === 0 ? 0 : (currentPage - 1) * pageSize + 1}
106
122
  {@const rangeEnd = Math.min(totalRows, currentPage * pageSize)}
107
123
  {@const onFirst = currentPage <= 1}
108
124
  {@const onLast = currentPage >= pageCount}
109
- <div class="sv-grid-pagination" role="navigation" aria-label="Pagination">
110
- <label class="sv-grid-pagination-pagesize">
125
+ {@const sizes = pageSizeOptions && pageSizeOptions.length ? pageSizeOptions : [10, 25, 50, 100]}
126
+ <div class="sv-grid-pagination" class:sv-grid-pagination-top={top} role="navigation" aria-label="Pagination">
127
+ <div class="sv-grid-pagination-pagesize">
111
128
  <span>Page Size:</span>
112
- <select
113
- onchange={(event) =>
114
- setPageSize(
115
- parseInt((event.currentTarget as HTMLSelectElement).value, 10),
116
- )}
117
- >
118
- <option value="10" selected={pageSize === 10}>10</option>
119
- <option value="25" selected={pageSize === 25}>25</option>
120
- <option value="50" selected={pageSize === 50}>50</option>
121
- <option value="100" selected={pageSize === 100}>100</option>
122
- </select>
123
- </label>
129
+ <div class="sv-grid-pagination-pagesize-dd">
130
+ <SvGridDropdown
131
+ options={sizes.map((n) => ({ value: n, label: String(n) }))}
132
+ value={pageSize}
133
+ autoOpen={false}
134
+ onChange={(v) => setPageSize(Number(v))}
135
+ />
136
+ </div>
137
+ </div>
124
138
  <span class="sv-grid-pagination-range">
125
139
  <strong>{rangeStart.toLocaleString()}</strong> to
126
140
  <strong>{rangeEnd.toLocaleString()}</strong> of