@budibase/frontend-core 2.5.6-alpha.5 → 2.5.6-alpha.7

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 (63) hide show
  1. package/package.json +6 -4
  2. package/src/components/grid/cells/AttachmentCell.svelte +153 -0
  3. package/src/components/grid/cells/BooleanCell.svelte +44 -0
  4. package/src/components/grid/cells/DataCell.svelte +86 -0
  5. package/src/components/grid/cells/DateCell.svelte +77 -0
  6. package/src/components/grid/cells/FormulaCell.svelte +5 -0
  7. package/src/components/grid/cells/GridCell.svelte +159 -0
  8. package/src/components/grid/cells/HeaderCell.svelte +237 -0
  9. package/src/components/grid/cells/JSONCell.svelte +36 -0
  10. package/src/components/grid/cells/LongFormCell.svelte +117 -0
  11. package/src/components/grid/cells/MultiSelectCell.svelte +7 -0
  12. package/src/components/grid/cells/NumberCell.svelte +7 -0
  13. package/src/components/grid/cells/OptionsCell.svelte +239 -0
  14. package/src/components/grid/cells/RelationshipCell.svelte +503 -0
  15. package/src/components/grid/cells/TextCell.svelte +110 -0
  16. package/src/components/grid/controls/AddColumnButton.svelte +16 -0
  17. package/src/components/grid/controls/AddRowButton.svelte +18 -0
  18. package/src/components/grid/controls/BetaButton.svelte +46 -0
  19. package/src/components/grid/controls/DeleteButton.svelte +72 -0
  20. package/src/components/grid/controls/HideColumnsButton.svelte +91 -0
  21. package/src/components/grid/controls/RowHeightButton.svelte +70 -0
  22. package/src/components/grid/controls/SortButton.svelte +107 -0
  23. package/src/components/grid/index.js +1 -0
  24. package/src/components/grid/layout/Avatar.svelte +24 -0
  25. package/src/components/grid/layout/Grid.svelte +250 -0
  26. package/src/components/grid/layout/GridBody.svelte +38 -0
  27. package/src/components/grid/layout/GridRow.svelte +57 -0
  28. package/src/components/grid/layout/GridScrollWrapper.svelte +84 -0
  29. package/src/components/grid/layout/HeaderRow.svelte +30 -0
  30. package/src/components/grid/layout/NewRowTop.svelte +247 -0
  31. package/src/components/grid/layout/RowCount.svelte +9 -0
  32. package/src/components/grid/layout/StickyColumn.svelte +245 -0
  33. package/src/components/grid/layout/UserAvatars.svelte +20 -0
  34. package/src/components/grid/lib/constants.js +11 -0
  35. package/src/components/grid/lib/events.js +29 -0
  36. package/src/components/grid/lib/renderers.js +29 -0
  37. package/src/components/grid/lib/utils.js +26 -0
  38. package/src/components/grid/lib/websocket.js +63 -0
  39. package/src/components/grid/overlays/KeyboardManager.svelte +180 -0
  40. package/src/components/grid/overlays/MenuOverlay.svelte +95 -0
  41. package/src/components/grid/overlays/ReorderOverlay.svelte +63 -0
  42. package/src/components/grid/overlays/ResizeOverlay.svelte +70 -0
  43. package/src/components/grid/overlays/ScrollOverlay.svelte +121 -0
  44. package/src/components/grid/stores/bounds.js +16 -0
  45. package/src/components/grid/stores/clipboard.js +34 -0
  46. package/src/components/grid/stores/columns.js +206 -0
  47. package/src/components/grid/stores/index.js +48 -0
  48. package/src/components/grid/stores/menu.js +49 -0
  49. package/src/components/grid/stores/pagination.js +24 -0
  50. package/src/components/grid/stores/reorder.js +155 -0
  51. package/src/components/grid/stores/resize.js +124 -0
  52. package/src/components/grid/stores/rows.js +462 -0
  53. package/src/components/grid/stores/scroll.js +201 -0
  54. package/src/components/grid/stores/ui.js +168 -0
  55. package/src/components/grid/stores/users.js +115 -0
  56. package/src/components/grid/stores/validation.js +45 -0
  57. package/src/components/grid/stores/viewport.js +141 -0
  58. package/src/components/index.js +1 -0
  59. package/src/fetch/DataFetch.js +27 -21
  60. package/src/fetch/QueryFetch.js +1 -1
  61. package/src/themes/midnight.css +3 -0
  62. package/src/themes/nord.css +3 -0
  63. package/src/utils/utils.js +20 -0
@@ -0,0 +1,462 @@
1
+ import { writable, derived, get } from "svelte/store"
2
+ import { fetchData } from "../../../fetch/fetchData"
3
+ import { notifications } from "@budibase/bbui"
4
+
5
+ const initialSortState = {
6
+ column: null,
7
+ order: "ascending",
8
+ }
9
+
10
+ export const createStores = () => {
11
+ const rows = writable([])
12
+ const table = writable(null)
13
+ const filter = writable([])
14
+ const loading = writable(false)
15
+ const loaded = writable(false)
16
+ const sort = writable(initialSortState)
17
+ const rowChangeCache = writable({})
18
+ const inProgressChanges = writable({})
19
+
20
+ // Generate a lookup map to quick find a row by ID
21
+ const rowLookupMap = derived(
22
+ rows,
23
+ $rows => {
24
+ let map = {}
25
+ for (let i = 0; i < $rows.length; i++) {
26
+ map[$rows[i]._id] = i
27
+ }
28
+ return map
29
+ },
30
+ {}
31
+ )
32
+
33
+ // Mark loaded as true if we've ever stopped loading
34
+ let hasStartedLoading = false
35
+ loading.subscribe($loading => {
36
+ if ($loading) {
37
+ hasStartedLoading = true
38
+ } else if (hasStartedLoading) {
39
+ loaded.set(true)
40
+ }
41
+ })
42
+
43
+ return {
44
+ rows,
45
+ rowLookupMap,
46
+ table,
47
+ filter,
48
+ loaded,
49
+ loading,
50
+ sort,
51
+ rowChangeCache,
52
+ inProgressChanges,
53
+ }
54
+ }
55
+
56
+ export const deriveStores = context => {
57
+ const {
58
+ rows,
59
+ rowLookupMap,
60
+ table,
61
+ filter,
62
+ loading,
63
+ sort,
64
+ tableId,
65
+ API,
66
+ scroll,
67
+ validation,
68
+ focusedCellId,
69
+ columns,
70
+ rowChangeCache,
71
+ inProgressChanges,
72
+ previousFocusedRowId,
73
+ } = context
74
+ const instanceLoaded = writable(false)
75
+ const fetch = writable(null)
76
+
77
+ // Local cache of row IDs to speed up checking if a row exists
78
+ let rowCacheMap = {}
79
+
80
+ // Enrich rows with an index property and any pending changes
81
+ const enrichedRows = derived(
82
+ [rows, rowChangeCache],
83
+ ([$rows, $rowChangeCache]) => {
84
+ return $rows.map((row, idx) => ({
85
+ ...row,
86
+ ...$rowChangeCache[row._id],
87
+ __idx: idx,
88
+ }))
89
+ },
90
+ []
91
+ )
92
+
93
+ // Reset everything when table ID changes
94
+ let unsubscribe = null
95
+ let lastResetKey = null
96
+ tableId.subscribe($tableId => {
97
+ // Unsub from previous fetch if one exists
98
+ unsubscribe?.()
99
+ fetch.set(null)
100
+ instanceLoaded.set(false)
101
+ loading.set(true)
102
+
103
+ // Reset state
104
+ filter.set([])
105
+
106
+ // Create new fetch model
107
+ const newFetch = fetchData({
108
+ API,
109
+ datasource: {
110
+ type: "table",
111
+ tableId: $tableId,
112
+ },
113
+ options: {
114
+ filter: [],
115
+ sortColumn: initialSortState.column,
116
+ sortOrder: initialSortState.order,
117
+ limit: 100,
118
+ paginate: true,
119
+ },
120
+ })
121
+
122
+ // Subscribe to changes of this fetch model
123
+ unsubscribe = newFetch.subscribe($fetch => {
124
+ if ($fetch.loaded && !$fetch.loading) {
125
+ const $instanceLoaded = get(instanceLoaded)
126
+ const resetRows = $fetch.resetKey !== lastResetKey
127
+ lastResetKey = $fetch.resetKey
128
+
129
+ // Reset state properties when dataset changes
130
+ if (!$instanceLoaded || resetRows) {
131
+ table.set($fetch.definition)
132
+ sort.set({
133
+ column: $fetch.sortColumn,
134
+ order: $fetch.sortOrder,
135
+ })
136
+ }
137
+
138
+ // Reset scroll state when data changes
139
+ if (!$instanceLoaded) {
140
+ // Reset both top and left for a new table ID
141
+ instanceLoaded.set(true)
142
+ scroll.set({ top: 0, left: 0 })
143
+ } else if (resetRows) {
144
+ // Only reset top scroll position when resetting rows
145
+ scroll.update(state => ({ ...state, top: 0 }))
146
+ }
147
+
148
+ // Process new rows
149
+ handleNewRows($fetch.rows, resetRows)
150
+
151
+ // Notify that we're loaded
152
+ loading.set(false)
153
+ }
154
+ })
155
+
156
+ fetch.set(newFetch)
157
+ })
158
+
159
+ // Update fetch when filter or sort config changes
160
+ filter.subscribe($filter => {
161
+ get(fetch)?.update({
162
+ filter: $filter,
163
+ })
164
+ })
165
+ sort.subscribe($sort => {
166
+ get(fetch)?.update({
167
+ sortOrder: $sort.order,
168
+ sortColumn: $sort.column,
169
+ })
170
+ })
171
+
172
+ // Gets a row by ID
173
+ const getRow = id => {
174
+ const index = get(rowLookupMap)[id]
175
+ return index >= 0 ? get(rows)[index] : null
176
+ }
177
+
178
+ // Handles validation errors from the rows API and updates local validation
179
+ // state, storing error messages against relevant cells
180
+ const handleValidationError = (rowId, error) => {
181
+ if (error?.json?.validationErrors) {
182
+ // Normal validation error
183
+ const keys = Object.keys(error.json.validationErrors)
184
+ const $columns = get(columns)
185
+ for (let column of keys) {
186
+ validation.actions.setError(
187
+ `${rowId}-${column}`,
188
+ `${column} ${error.json.validationErrors[column]}`
189
+ )
190
+
191
+ // Ensure the column is visible
192
+ const index = $columns.findIndex(x => x.name === column)
193
+ if (index !== -1 && !$columns[index].visible) {
194
+ columns.update(state => {
195
+ state[index].visible = true
196
+ return state.slice()
197
+ })
198
+ }
199
+ }
200
+ // Focus the first cell with an error
201
+ focusedCellId.set(`${rowId}-${keys[0]}`)
202
+ } else {
203
+ // Some other error - just update the current cell
204
+ validation.actions.setError(get(focusedCellId), error?.message || "Error")
205
+ }
206
+ }
207
+
208
+ // Adds a new row
209
+ const addRow = async (row, idx, bubble = false) => {
210
+ try {
211
+ // Create row
212
+ let newRow = await API.saveRow({ ...row, tableId: get(tableId) })
213
+ newRow = await fetchRow(newRow._id)
214
+
215
+ // Update state
216
+ if (idx != null) {
217
+ rowCacheMap[newRow._id] = true
218
+ rows.update(state => {
219
+ state.splice(idx, 0, newRow)
220
+ return state.slice()
221
+ })
222
+ } else {
223
+ handleNewRows([newRow])
224
+ }
225
+
226
+ // Refresh row to ensure data is in the correct format
227
+ notifications.success("Row created successfully")
228
+ return newRow
229
+ } catch (error) {
230
+ if (bubble) {
231
+ throw error
232
+ } else {
233
+ handleValidationError("new", error)
234
+ }
235
+ }
236
+ }
237
+
238
+ // Duplicates a row, inserting the duplicate row after the existing one
239
+ const duplicateRow = async row => {
240
+ let clone = { ...row }
241
+ delete clone._id
242
+ delete clone._rev
243
+ delete clone.__idx
244
+ try {
245
+ return await addRow(clone, row.__idx + 1, true)
246
+ } catch (error) {
247
+ handleValidationError(row._id, error)
248
+ }
249
+ }
250
+
251
+ // Fetches a row by ID using the search endpoint
252
+ const fetchRow = async id => {
253
+ const res = await API.searchTable({
254
+ tableId: get(tableId),
255
+ limit: 1,
256
+ query: {
257
+ equal: {
258
+ _id: id,
259
+ },
260
+ },
261
+ paginate: false,
262
+ })
263
+ return res?.rows?.[0]
264
+ }
265
+
266
+ // Refreshes a specific row, handling updates, addition or deletion
267
+ const refreshRow = async id => {
268
+ // Fetch row from the server again
269
+ const newRow = await fetchRow(id)
270
+
271
+ // Get index of row to check if it exists
272
+ const $rows = get(rows)
273
+ const $rowLookupMap = get(rowLookupMap)
274
+ const index = $rowLookupMap[id]
275
+
276
+ // Process as either an update, addition or deletion
277
+ if (newRow) {
278
+ if (index != null) {
279
+ // An existing row was updated
280
+ rows.update(state => {
281
+ state[index] = { ...newRow }
282
+ return state
283
+ })
284
+ } else {
285
+ // A new row was created
286
+ handleNewRows([newRow])
287
+ }
288
+ } else if (index != null) {
289
+ // A row was removed
290
+ handleRemoveRows([$rows[index]])
291
+ }
292
+ }
293
+
294
+ // Refreshes all data
295
+ const refreshData = () => {
296
+ get(fetch)?.getInitialData()
297
+ }
298
+
299
+ // Patches a row with some changes
300
+ const updateRow = async (rowId, changes) => {
301
+ const $rows = get(rows)
302
+ const $rowLookupMap = get(rowLookupMap)
303
+ const index = $rowLookupMap[rowId]
304
+ const row = $rows[index]
305
+ if (index == null || !Object.keys(changes || {}).length) {
306
+ return
307
+ }
308
+
309
+ // Abandon if no changes
310
+ let same = true
311
+ for (let column of Object.keys(changes)) {
312
+ if (row[column] !== changes[column]) {
313
+ same = false
314
+ break
315
+ }
316
+ }
317
+ if (same) {
318
+ return
319
+ }
320
+
321
+ // Immediately update state so that the change is reflected
322
+ rowChangeCache.update(state => ({
323
+ ...state,
324
+ [rowId]: {
325
+ ...state[rowId],
326
+ ...changes,
327
+ },
328
+ }))
329
+
330
+ // Save change
331
+ try {
332
+ inProgressChanges.update(state => ({
333
+ ...state,
334
+ [rowId]: true,
335
+ }))
336
+ const newRow = { ...row, ...get(rowChangeCache)[rowId] }
337
+ const saved = await API.saveRow(newRow)
338
+
339
+ // Update state after a successful change
340
+ rows.update(state => {
341
+ state[index] = {
342
+ ...newRow,
343
+ _rev: saved._rev,
344
+ }
345
+ return state.slice()
346
+ })
347
+ rowChangeCache.update(state => {
348
+ delete state[rowId]
349
+ return state
350
+ })
351
+ } catch (error) {
352
+ handleValidationError(rowId, error)
353
+ }
354
+ inProgressChanges.update(state => ({
355
+ ...state,
356
+ [rowId]: false,
357
+ }))
358
+ }
359
+
360
+ // Updates a value of a row
361
+ const updateValue = async (rowId, column, value) => {
362
+ return await updateRow(rowId, { [column]: value })
363
+ }
364
+
365
+ // Deletes an array of rows
366
+ const deleteRows = async rowsToDelete => {
367
+ if (!rowsToDelete?.length) {
368
+ return
369
+ }
370
+
371
+ // Actually delete rows
372
+ rowsToDelete.forEach(row => {
373
+ delete row.__idx
374
+ })
375
+ await API.deleteRows({
376
+ tableId: get(tableId),
377
+ rows: rowsToDelete,
378
+ })
379
+
380
+ // Update state
381
+ handleRemoveRows(rowsToDelete)
382
+ }
383
+
384
+ // Local handler to process new rows inside the fetch, and append any new
385
+ // rows to state that we haven't encountered before
386
+ const handleNewRows = (newRows, resetRows) => {
387
+ if (resetRows) {
388
+ rowCacheMap = {}
389
+ }
390
+ let rowsToAppend = []
391
+ let newRow
392
+ for (let i = 0; i < newRows.length; i++) {
393
+ newRow = newRows[i]
394
+ if (!rowCacheMap[newRow._id]) {
395
+ rowCacheMap[newRow._id] = true
396
+ rowsToAppend.push(newRow)
397
+ }
398
+ }
399
+ if (resetRows) {
400
+ rows.set(rowsToAppend)
401
+ } else if (rowsToAppend.length) {
402
+ rows.update(state => [...state, ...rowsToAppend])
403
+ }
404
+ }
405
+
406
+ // Local handler to remove rows from state
407
+ const handleRemoveRows = rowsToRemove => {
408
+ const deletedIds = rowsToRemove.map(row => row._id)
409
+
410
+ // We deliberately do not remove IDs from the cache map as the data may
411
+ // still exist inside the fetch, but we don't want to add it again
412
+ rows.update(state => {
413
+ return state.filter(row => !deletedIds.includes(row._id))
414
+ })
415
+ }
416
+
417
+ // Loads the next page of data if available
418
+ const loadNextPage = () => {
419
+ get(fetch)?.nextPage()
420
+ }
421
+
422
+ // Refreshes the schema of the data fetch subscription
423
+ const refreshTableDefinition = async () => {
424
+ const definition = await API.fetchTableDefinition(get(tableId))
425
+ table.set(definition)
426
+ }
427
+
428
+ // Checks if we have a row with a certain ID
429
+ const hasRow = id => {
430
+ return get(rowLookupMap)[id] != null
431
+ }
432
+
433
+ // Wipe the row change cache when changing row
434
+ previousFocusedRowId.subscribe(id => {
435
+ if (id && !get(inProgressChanges)[id]) {
436
+ rowChangeCache.update(state => {
437
+ delete state[id]
438
+ return state
439
+ })
440
+ }
441
+ })
442
+
443
+ return {
444
+ enrichedRows,
445
+ rows: {
446
+ ...rows,
447
+ actions: {
448
+ addRow,
449
+ duplicateRow,
450
+ getRow,
451
+ updateValue,
452
+ updateRow,
453
+ deleteRows,
454
+ hasRow,
455
+ loadNextPage,
456
+ refreshRow,
457
+ refreshData,
458
+ refreshTableDefinition,
459
+ },
460
+ },
461
+ }
462
+ }
@@ -0,0 +1,201 @@
1
+ import { writable, derived, get } from "svelte/store"
2
+ import { tick } from "svelte"
3
+ import { Padding, GutterWidth, DefaultRowHeight } from "../lib/constants"
4
+
5
+ export const createStores = () => {
6
+ const scroll = writable({
7
+ left: 0,
8
+ top: 0,
9
+ })
10
+
11
+ // Derive height and width as primitives to avoid wasted computation
12
+ const scrollTop = derived(scroll, $scroll => $scroll.top, 0)
13
+ const scrollLeft = derived(scroll, $scroll => $scroll.left, 0)
14
+
15
+ return {
16
+ scroll,
17
+ scrollTop,
18
+ scrollLeft,
19
+ }
20
+ }
21
+
22
+ export const deriveStores = context => {
23
+ const { rows, visibleColumns, stickyColumn, rowHeight, width, height } =
24
+ context
25
+
26
+ // Memoize store primitives
27
+ const stickyColumnWidth = derived(stickyColumn, $col => $col?.width || 0, 0)
28
+
29
+ // Derive vertical limits
30
+ const contentHeight = derived(
31
+ [rows, rowHeight],
32
+ ([$rows, $rowHeight]) => $rows.length * $rowHeight + Padding,
33
+ 0
34
+ )
35
+ const maxScrollTop = derived(
36
+ [height, contentHeight],
37
+ ([$height, $contentHeight]) => Math.max($contentHeight - $height, 0),
38
+ 0
39
+ )
40
+
41
+ // Derive horizontal limits
42
+ const contentWidth = derived(
43
+ [visibleColumns, stickyColumnWidth],
44
+ ([$visibleColumns, $stickyColumnWidth]) => {
45
+ let width = GutterWidth + Padding + $stickyColumnWidth
46
+ $visibleColumns.forEach(col => {
47
+ width += col.width
48
+ })
49
+ return width
50
+ },
51
+ 0
52
+ )
53
+ const screenWidth = derived(
54
+ [width, stickyColumnWidth],
55
+ ([$width, $stickyColumnWidth]) => $width + GutterWidth + $stickyColumnWidth,
56
+ 0
57
+ )
58
+ const maxScrollLeft = derived(
59
+ [contentWidth, screenWidth],
60
+ ([$contentWidth, $screenWidth]) => {
61
+ return Math.max($contentWidth - $screenWidth, 0)
62
+ },
63
+ 0
64
+ )
65
+
66
+ // Derive whether to show scrollbars or not
67
+ const showVScrollbar = derived(
68
+ [contentHeight, height],
69
+ ([$contentHeight, $height]) => {
70
+ return $contentHeight > $height
71
+ }
72
+ )
73
+ const showHScrollbar = derived(
74
+ [contentWidth, screenWidth],
75
+ ([$contentWidth, $screenWidth]) => {
76
+ return $contentWidth > $screenWidth
77
+ }
78
+ )
79
+
80
+ return {
81
+ contentHeight,
82
+ contentWidth,
83
+ screenWidth,
84
+ maxScrollTop,
85
+ maxScrollLeft,
86
+ showHScrollbar,
87
+ showVScrollbar,
88
+ }
89
+ }
90
+
91
+ export const initialise = context => {
92
+ const {
93
+ focusedCellId,
94
+ focusedRow,
95
+ scroll,
96
+ bounds,
97
+ rowHeight,
98
+ visibleColumns,
99
+ scrollTop,
100
+ maxScrollTop,
101
+ scrollLeft,
102
+ maxScrollLeft,
103
+ } = context
104
+
105
+ // Ensure scroll state never goes invalid, which can happen when changing
106
+ // rows or tables
107
+ const overscrollTop = derived(
108
+ [scrollTop, maxScrollTop],
109
+ ([$scrollTop, $maxScrollTop]) => $scrollTop > $maxScrollTop,
110
+ false
111
+ )
112
+ const overscrollLeft = derived(
113
+ [scrollLeft, maxScrollLeft],
114
+ ([$scrollLeft, $maxScrollLeft]) => $scrollLeft > $maxScrollLeft,
115
+ false
116
+ )
117
+ overscrollTop.subscribe(overscroll => {
118
+ if (overscroll) {
119
+ scroll.update(state => ({
120
+ ...state,
121
+ top: get(maxScrollTop),
122
+ }))
123
+ }
124
+ })
125
+ overscrollLeft.subscribe(overscroll => {
126
+ if (overscroll) {
127
+ scroll.update(state => ({
128
+ ...state,
129
+ left: get(maxScrollLeft),
130
+ }))
131
+ }
132
+ })
133
+
134
+ // Ensure the selected cell is visible
135
+ focusedCellId.subscribe(async $focusedCellId => {
136
+ await tick()
137
+ const $focusedRow = get(focusedRow)
138
+ const $scroll = get(scroll)
139
+ const $bounds = get(bounds)
140
+ const $rowHeight = get(rowHeight)
141
+ const verticalOffset = DefaultRowHeight * 1.5
142
+
143
+ // Ensure vertical position is viewable
144
+ if ($focusedRow) {
145
+ // Ensure row is not below bottom of screen
146
+ const rowYPos = $focusedRow.__idx * $rowHeight
147
+ const bottomCutoff =
148
+ $scroll.top + $bounds.height - $rowHeight - verticalOffset
149
+ let delta = rowYPos - bottomCutoff
150
+ if (delta > 0) {
151
+ scroll.update(state => ({
152
+ ...state,
153
+ top: state.top + delta,
154
+ }))
155
+ }
156
+
157
+ // Ensure row is not above top of screen
158
+ else {
159
+ const delta = $scroll.top - rowYPos + verticalOffset
160
+ if (delta > 0) {
161
+ scroll.update(state => ({
162
+ ...state,
163
+ top: Math.max(0, state.top - delta),
164
+ }))
165
+ }
166
+ }
167
+ }
168
+
169
+ // Ensure horizontal position is viewable
170
+ // Check horizontal position of columns next
171
+ const $visibleColumns = get(visibleColumns)
172
+ const columnName = $focusedCellId?.split("-")[1]
173
+ const column = $visibleColumns.find(col => col.name === columnName)
174
+ const horizontalOffset = 50
175
+ if (!column) {
176
+ return
177
+ }
178
+
179
+ // Ensure column is not cutoff on left edge
180
+ let delta = $scroll.left - column.left + horizontalOffset
181
+ if (delta > 0) {
182
+ scroll.update(state => ({
183
+ ...state,
184
+ left: Math.max(0, state.left - delta),
185
+ }))
186
+ }
187
+
188
+ // Ensure column is not cutoff on right edge
189
+ else {
190
+ const rightEdge = column.left + column.width
191
+ const rightBound = $bounds.width + $scroll.left - horizontalOffset
192
+ delta = rightEdge - rightBound
193
+ if (delta > 0) {
194
+ scroll.update(state => ({
195
+ ...state,
196
+ left: state.left + delta,
197
+ }))
198
+ }
199
+ }
200
+ })
201
+ }