@svgrid/grid 1.0.2 → 1.1.0

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 (143) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +137 -39
  3. package/dist/GridFooter.svelte +164 -0
  4. package/dist/GridFooter.svelte.d.ts +29 -0
  5. package/dist/GridMenus.svelte +570 -0
  6. package/dist/GridMenus.svelte.d.ts +31 -0
  7. package/dist/SvGrid.controller.svelte.d.ts +429 -0
  8. package/dist/SvGrid.controller.svelte.js +1732 -0
  9. package/dist/SvGrid.css +1709 -0
  10. package/dist/SvGrid.helpers.d.ts +35 -0
  11. package/dist/SvGrid.helpers.js +160 -0
  12. package/dist/SvGrid.svelte +344 -7043
  13. package/dist/SvGrid.svelte.d.ts +4 -357
  14. package/dist/SvGrid.types.d.ts +436 -0
  15. package/dist/SvGrid.types.js +1 -0
  16. package/dist/SvGridChart.svelte +1060 -23
  17. package/dist/SvGridChart.svelte.d.ts +17 -0
  18. package/dist/build-api.d.ts +5 -0
  19. package/dist/build-api.js +527 -0
  20. package/dist/cell-render.d.ts +15 -0
  21. package/dist/cell-render.js +246 -0
  22. package/dist/cell-values.d.ts +28 -0
  23. package/dist/cell-values.js +89 -0
  24. package/dist/chart.d.ts +370 -3
  25. package/dist/chart.js +1135 -42
  26. package/dist/clipboard.d.ts +15 -0
  27. package/dist/clipboard.js +356 -0
  28. package/dist/columns.d.ts +30 -0
  29. package/dist/columns.js +277 -0
  30. package/dist/core.d.ts +1 -1
  31. package/dist/css.d.ts +3 -0
  32. package/dist/editing.d.ts +24 -0
  33. package/dist/editing.js +343 -0
  34. package/dist/editors/cell-editors.d.ts +1 -1
  35. package/dist/facet-buckets.d.ts +13 -0
  36. package/dist/facet-buckets.js +54 -0
  37. package/dist/features.d.ts +5 -0
  38. package/dist/features.js +30 -0
  39. package/dist/filter-operators.d.ts +16 -0
  40. package/dist/filter-operators.js +69 -0
  41. package/dist/hyperformula-adapter.d.ts +82 -0
  42. package/dist/hyperformula-adapter.js +73 -0
  43. package/dist/index.d.ts +5 -2
  44. package/dist/index.js +5 -2
  45. package/dist/keyboard-handlers.d.ts +7 -0
  46. package/dist/keyboard-handlers.js +197 -0
  47. package/dist/menus.d.ts +40 -0
  48. package/dist/menus.js +389 -0
  49. package/dist/named-views.d.ts +27 -0
  50. package/dist/named-views.js +39 -0
  51. package/dist/row-resize.d.ts +43 -0
  52. package/dist/row-resize.js +158 -0
  53. package/dist/scroll-sync.d.ts +9 -0
  54. package/dist/scroll-sync.js +86 -0
  55. package/dist/selection.d.ts +26 -0
  56. package/dist/selection.js +387 -0
  57. package/dist/spreadsheet.d.ts +80 -0
  58. package/dist/spreadsheet.js +194 -0
  59. package/dist/summaries.d.ts +12 -0
  60. package/dist/summaries.js +65 -0
  61. package/dist/svgrid-wrapper.types.d.ts +12 -1
  62. package/dist/svgrid.comments-autocomplete.test.d.ts +1 -0
  63. package/dist/svgrid.comments-autocomplete.test.js +96 -0
  64. package/dist/svgrid.context-menu.test.d.ts +1 -0
  65. package/dist/svgrid.context-menu.test.js +102 -0
  66. package/dist/svgrid.new-features.wrapper.test.js +30 -4
  67. package/dist/svgrid.wrapper.test.js +27 -1
  68. package/dist/virtualization/column-virtualizer.d.ts +2 -0
  69. package/dist/virtualization/scroll-scaling.d.ts +28 -0
  70. package/dist/virtualization/scroll-scaling.js +64 -0
  71. package/dist/virtualization/scroll-scaling.test.d.ts +1 -0
  72. package/dist/virtualization/scroll-scaling.test.js +86 -0
  73. package/dist/virtualization/svelte-virtualizer.svelte.d.ts +2 -0
  74. package/dist/virtualization/svelte-virtualizer.svelte.js +2 -0
  75. package/dist/virtualization/virtualizer.d.ts +7 -0
  76. package/dist/virtualization/virtualizer.js +30 -0
  77. package/package.json +1 -1
  78. package/src/GridFooter.svelte +164 -0
  79. package/src/GridMenus.svelte +570 -0
  80. package/src/SvGrid.controller.svelte.ts +2195 -0
  81. package/src/SvGrid.css +1747 -0
  82. package/src/SvGrid.helpers.test.ts +415 -0
  83. package/src/SvGrid.helpers.ts +185 -0
  84. package/src/SvGrid.svelte +348 -7043
  85. package/src/SvGrid.types.ts +456 -0
  86. package/src/SvGridChart.svelte +1060 -23
  87. package/src/build-api.coverage.test.ts +532 -0
  88. package/src/build-api.ts +663 -0
  89. package/src/cell-render.test.ts +451 -0
  90. package/src/cell-render.ts +426 -0
  91. package/src/cell-values.ts +114 -0
  92. package/src/chart-export.test.ts +370 -0
  93. package/src/chart.coverage.test.ts +814 -0
  94. package/src/chart.ts +1352 -47
  95. package/src/clipboard.test.ts +731 -0
  96. package/src/clipboard.ts +524 -0
  97. package/src/collaboration.coverage.test.ts +220 -0
  98. package/src/columns.test.ts +702 -0
  99. package/src/columns.ts +419 -0
  100. package/src/core.ts +8 -0
  101. package/src/css.d.ts +3 -0
  102. package/src/editing.test.ts +837 -0
  103. package/src/editing.ts +513 -0
  104. package/src/editors/cell-editors.coverage.test.ts +156 -0
  105. package/src/editors/cell-editors.ts +1 -0
  106. package/src/facet-buckets.test.ts +353 -0
  107. package/src/facet-buckets.ts +67 -0
  108. package/src/features.ts +128 -0
  109. package/src/filter-operators.test.ts +174 -0
  110. package/src/filter-operators.ts +87 -0
  111. package/src/hyperformula-adapter.test.ts +256 -0
  112. package/src/hyperformula-adapter.ts +124 -0
  113. package/src/index.ts +37 -0
  114. package/src/keyboard-handlers.coverage.test.ts +560 -0
  115. package/src/keyboard-handlers.ts +353 -0
  116. package/src/keyboard.ts +97 -97
  117. package/src/menus.test.ts +620 -0
  118. package/src/menus.ts +554 -0
  119. package/src/named-views.coverage.test.ts +210 -0
  120. package/src/named-views.ts +48 -0
  121. package/src/row-resize.test.ts +369 -0
  122. package/src/row-resize.ts +171 -0
  123. package/src/scroll-sync.test.ts +330 -0
  124. package/src/scroll-sync.ts +216 -0
  125. package/src/selection.test.ts +722 -0
  126. package/src/selection.ts +545 -0
  127. package/src/server-data-source.coverage.test.ts +180 -0
  128. package/src/spreadsheet.test.ts +445 -0
  129. package/src/spreadsheet.ts +246 -0
  130. package/src/summaries.ts +204 -0
  131. package/src/sv-grid-scrollbar.ts +13 -1
  132. package/src/svgrid-wrapper.types.ts +12 -1
  133. package/src/svgrid.behavior.test.ts +22 -0
  134. package/src/svgrid.comments-autocomplete.test.ts +112 -0
  135. package/src/svgrid.context-menu.test.ts +126 -0
  136. package/src/svgrid.interaction.test.ts +30 -0
  137. package/src/svgrid.new-features.wrapper.test.ts +65 -4
  138. package/src/svgrid.wrapper.test.ts +27 -1
  139. package/src/test-setup.ts +9 -6
  140. package/src/virtualization/scroll-scaling.test.ts +148 -0
  141. package/src/virtualization/scroll-scaling.ts +121 -0
  142. package/src/virtualization/svelte-virtualizer.svelte.ts +2 -0
  143. package/src/virtualization/virtualizer.ts +26 -0
@@ -0,0 +1,220 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
+ import {
3
+ broadcastChannelTransport,
4
+ createCollaboration,
5
+ type CollabMessage,
6
+ type CollabTransport,
7
+ } from './collaboration'
8
+
9
+ const userA = { id: 'a', name: 'Ada', color: '#f00' }
10
+ const userB = { id: 'b', name: 'Bob', color: '#00f' }
11
+
12
+ /** A controllable transport whose subscriber we can drive by hand. */
13
+ function manualTransport() {
14
+ const handlers = new Set<(m: CollabMessage) => void>()
15
+ const posted: CollabMessage[] = []
16
+ const transport: CollabTransport = {
17
+ post: (msg) => {
18
+ posted.push(msg)
19
+ },
20
+ subscribe: (h) => {
21
+ handlers.add(h)
22
+ return () => handlers.delete(h)
23
+ },
24
+ }
25
+ const deliver = (msg: CollabMessage) => {
26
+ for (const h of [...handlers]) h(msg)
27
+ }
28
+ return { transport, posted, deliver, handlers }
29
+ }
30
+
31
+ describe('createCollaboration - presence / edit upserts', () => {
32
+ it('hello reply carries the responder own cell', () => {
33
+ const t = manualTransport()
34
+ const a = createCollaboration({ user: userA, transport: t.transport })
35
+ a.setCell({ rowId: 'r0', columnId: 'c0' })
36
+ t.posted.length = 0
37
+ // a newcomer says hello -> A replies presence with its own cell.
38
+ t.deliver({ kind: 'hello', user: userB })
39
+ const reply = t.posted.find((m) => m.kind === 'presence')
40
+ expect(reply).toEqual({ kind: 'presence', user: userA, cell: { rowId: 'r0', columnId: 'c0' } })
41
+ expect(a.peers().map((p) => p.id)).toEqual(['b'])
42
+ a.dispose()
43
+ })
44
+
45
+ it('presence message records the peer cursor', () => {
46
+ const t = manualTransport()
47
+ const onPeersChange = vi.fn()
48
+ const a = createCollaboration({ user: userA, transport: t.transport, onPeersChange })
49
+ t.deliver({ kind: 'presence', user: userB, cell: { rowId: 'r9', columnId: 'name' } })
50
+ expect(a.peers()[0]?.cell).toEqual({ rowId: 'r9', columnId: 'name' })
51
+ expect(onPeersChange).toHaveBeenCalled()
52
+ a.dispose()
53
+ })
54
+
55
+ it('edit from another peer sets a cursor AND fires onRemoteEdit', () => {
56
+ const t = manualTransport()
57
+ const onRemoteEdit = vi.fn()
58
+ const a = createCollaboration({ user: userA, transport: t.transport, onRemoteEdit })
59
+ t.deliver({ kind: 'edit', user: userB, rowId: 'r1', columnId: 'age', value: 30 })
60
+ expect(a.peers()[0]?.cell).toEqual({ rowId: 'r1', columnId: 'age' })
61
+ expect(onRemoteEdit).toHaveBeenCalledWith({
62
+ rowId: 'r1',
63
+ columnId: 'age',
64
+ value: 30,
65
+ user: userB,
66
+ })
67
+ a.dispose()
68
+ })
69
+
70
+ it('ignores own id on upsert (presence/edit echoed back)', () => {
71
+ const t = manualTransport()
72
+ const onRemoteEdit = vi.fn()
73
+ const a = createCollaboration({ user: userA, transport: t.transport, onRemoteEdit })
74
+ // self-echo: should not appear as a peer and should not call onRemoteEdit
75
+ t.deliver({ kind: 'presence', user: userA, cell: { rowId: 'x', columnId: 'y' } })
76
+ t.deliver({ kind: 'edit', user: userA, rowId: 'x', columnId: 'y', value: 1 })
77
+ expect(a.peers()).toHaveLength(0)
78
+ expect(onRemoteEdit).not.toHaveBeenCalled()
79
+ a.dispose()
80
+ })
81
+
82
+ it('bye for an unknown peer does not notify', () => {
83
+ const t = manualTransport()
84
+ const onPeersChange = vi.fn()
85
+ const a = createCollaboration({ user: userA, transport: t.transport, onPeersChange })
86
+ onPeersChange.mockClear()
87
+ t.deliver({ kind: 'bye', userId: 'ghost' })
88
+ expect(onPeersChange).not.toHaveBeenCalled()
89
+ a.dispose()
90
+ })
91
+
92
+ it('setCell broadcasts a presence with the new cell', () => {
93
+ const t = manualTransport()
94
+ const a = createCollaboration({ user: userA, transport: t.transport })
95
+ t.posted.length = 0
96
+ a.setCell(null)
97
+ expect(t.posted).toContainEqual({ kind: 'presence', user: userA, cell: null })
98
+ a.dispose()
99
+ })
100
+
101
+ it('sendEdit broadcasts an edit message', () => {
102
+ const t = manualTransport()
103
+ const a = createCollaboration({ user: userA, transport: t.transport })
104
+ t.posted.length = 0
105
+ a.sendEdit('r5', 'col', 'v')
106
+ expect(t.posted).toContainEqual({
107
+ kind: 'edit',
108
+ user: userA,
109
+ rowId: 'r5',
110
+ columnId: 'col',
111
+ value: 'v',
112
+ })
113
+ a.dispose()
114
+ })
115
+
116
+ it('drops messages after dispose and unsubscribes', () => {
117
+ const t = manualTransport()
118
+ const onPeersChange = vi.fn()
119
+ const a = createCollaboration({ user: userA, transport: t.transport, onPeersChange })
120
+ a.dispose()
121
+ onPeersChange.mockClear()
122
+ // After dispose the handler is unsubscribed; even if delivered it no-ops.
123
+ t.deliver({ kind: 'hello', user: userB })
124
+ expect(onPeersChange).not.toHaveBeenCalled()
125
+ expect(a.peers()).toHaveLength(0)
126
+ // second dispose is a no-op
127
+ a.dispose()
128
+ })
129
+ })
130
+
131
+ describe('createCollaboration - prune timer', () => {
132
+ beforeEach(() => vi.useFakeTimers())
133
+ afterEach(() => vi.useRealTimers())
134
+
135
+ it('prunes peers gone silent past the timeout', () => {
136
+ const t = manualTransport()
137
+ const onPeersChange = vi.fn()
138
+ const a = createCollaboration({
139
+ user: userA,
140
+ transport: t.transport,
141
+ onPeersChange,
142
+ peerTimeoutMs: 3000,
143
+ })
144
+ t.deliver({ kind: 'presence', user: userB, cell: null })
145
+ expect(a.peers()).toHaveLength(1)
146
+ onPeersChange.mockClear()
147
+ // advance past the timeout; prune interval is max(1000, timeout/3) = 1000
148
+ vi.advanceTimersByTime(4000)
149
+ expect(a.peers()).toHaveLength(0)
150
+ expect(onPeersChange).toHaveBeenCalled()
151
+ a.dispose()
152
+ })
153
+
154
+ it('prune tick with no expired peers does not notify', () => {
155
+ const t = manualTransport()
156
+ const onPeersChange = vi.fn()
157
+ const a = createCollaboration({
158
+ user: userA,
159
+ transport: t.transport,
160
+ onPeersChange,
161
+ peerTimeoutMs: 9000,
162
+ })
163
+ t.deliver({ kind: 'presence', user: userB, cell: null })
164
+ onPeersChange.mockClear()
165
+ vi.advanceTimersByTime(3000) // one tick, peer still fresh
166
+ expect(onPeersChange).not.toHaveBeenCalled()
167
+ expect(a.peers()).toHaveLength(1)
168
+ a.dispose()
169
+ })
170
+
171
+ it('dispose clears the prune timer', () => {
172
+ const clearSpy = vi.spyOn(globalThis, 'clearInterval')
173
+ const t = manualTransport()
174
+ const a = createCollaboration({ user: userA, transport: t.transport })
175
+ a.dispose()
176
+ expect(clearSpy).toHaveBeenCalled()
177
+ clearSpy.mockRestore()
178
+ })
179
+ })
180
+
181
+ describe('broadcastChannelTransport', () => {
182
+ it('round-trips a message across two channels of the same name', async () => {
183
+ const tx = broadcastChannelTransport('svgrid-test-chan')
184
+ const rx = broadcastChannelTransport('svgrid-test-chan')
185
+ const received: CollabMessage[] = []
186
+ const off = rx.subscribe((m) => received.push(m))
187
+ tx.post({ kind: 'hello', user: userA })
188
+ // BroadcastChannel delivery is async (microtask/macrotask).
189
+ await new Promise((r) => setTimeout(r, 10))
190
+ expect(received).toContainEqual({ kind: 'hello', user: userA })
191
+ off()
192
+ })
193
+
194
+ it('subscribe returns a working unsubscribe', async () => {
195
+ const tx = broadcastChannelTransport('svgrid-unsub-chan')
196
+ const rx = broadcastChannelTransport('svgrid-unsub-chan')
197
+ const received: CollabMessage[] = []
198
+ const off = rx.subscribe((m) => received.push(m))
199
+ off()
200
+ tx.post({ kind: 'bye', userId: 'a' })
201
+ await new Promise((r) => setTimeout(r, 10))
202
+ expect(received).toHaveLength(0)
203
+ })
204
+
205
+ it('no-ops gracefully when BroadcastChannel is unavailable', () => {
206
+ const original = globalThis.BroadcastChannel
207
+ // @ts-expect-error - simulate SSR / old environment
208
+ delete globalThis.BroadcastChannel
209
+ try {
210
+ const tx = broadcastChannelTransport('nope')
211
+ const handler = vi.fn()
212
+ const off = tx.subscribe(handler)
213
+ expect(() => tx.post({ kind: 'bye', userId: 'x' })).not.toThrow()
214
+ expect(() => off()).not.toThrow()
215
+ expect(handler).not.toHaveBeenCalled()
216
+ } finally {
217
+ globalThis.BroadcastChannel = original
218
+ }
219
+ })
220
+ })