@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.
- package/package.json +6 -4
- package/src/components/grid/cells/AttachmentCell.svelte +153 -0
- package/src/components/grid/cells/BooleanCell.svelte +44 -0
- package/src/components/grid/cells/DataCell.svelte +86 -0
- package/src/components/grid/cells/DateCell.svelte +77 -0
- package/src/components/grid/cells/FormulaCell.svelte +5 -0
- package/src/components/grid/cells/GridCell.svelte +159 -0
- package/src/components/grid/cells/HeaderCell.svelte +237 -0
- package/src/components/grid/cells/JSONCell.svelte +36 -0
- package/src/components/grid/cells/LongFormCell.svelte +117 -0
- package/src/components/grid/cells/MultiSelectCell.svelte +7 -0
- package/src/components/grid/cells/NumberCell.svelte +7 -0
- package/src/components/grid/cells/OptionsCell.svelte +239 -0
- package/src/components/grid/cells/RelationshipCell.svelte +503 -0
- package/src/components/grid/cells/TextCell.svelte +110 -0
- package/src/components/grid/controls/AddColumnButton.svelte +16 -0
- package/src/components/grid/controls/AddRowButton.svelte +18 -0
- package/src/components/grid/controls/BetaButton.svelte +46 -0
- package/src/components/grid/controls/DeleteButton.svelte +72 -0
- package/src/components/grid/controls/HideColumnsButton.svelte +91 -0
- package/src/components/grid/controls/RowHeightButton.svelte +70 -0
- package/src/components/grid/controls/SortButton.svelte +107 -0
- package/src/components/grid/index.js +1 -0
- package/src/components/grid/layout/Avatar.svelte +24 -0
- package/src/components/grid/layout/Grid.svelte +250 -0
- package/src/components/grid/layout/GridBody.svelte +38 -0
- package/src/components/grid/layout/GridRow.svelte +57 -0
- package/src/components/grid/layout/GridScrollWrapper.svelte +84 -0
- package/src/components/grid/layout/HeaderRow.svelte +30 -0
- package/src/components/grid/layout/NewRowTop.svelte +247 -0
- package/src/components/grid/layout/RowCount.svelte +9 -0
- package/src/components/grid/layout/StickyColumn.svelte +245 -0
- package/src/components/grid/layout/UserAvatars.svelte +20 -0
- package/src/components/grid/lib/constants.js +11 -0
- package/src/components/grid/lib/events.js +29 -0
- package/src/components/grid/lib/renderers.js +29 -0
- package/src/components/grid/lib/utils.js +26 -0
- package/src/components/grid/lib/websocket.js +63 -0
- package/src/components/grid/overlays/KeyboardManager.svelte +180 -0
- package/src/components/grid/overlays/MenuOverlay.svelte +95 -0
- package/src/components/grid/overlays/ReorderOverlay.svelte +63 -0
- package/src/components/grid/overlays/ResizeOverlay.svelte +70 -0
- package/src/components/grid/overlays/ScrollOverlay.svelte +121 -0
- package/src/components/grid/stores/bounds.js +16 -0
- package/src/components/grid/stores/clipboard.js +34 -0
- package/src/components/grid/stores/columns.js +206 -0
- package/src/components/grid/stores/index.js +48 -0
- package/src/components/grid/stores/menu.js +49 -0
- package/src/components/grid/stores/pagination.js +24 -0
- package/src/components/grid/stores/reorder.js +155 -0
- package/src/components/grid/stores/resize.js +124 -0
- package/src/components/grid/stores/rows.js +462 -0
- package/src/components/grid/stores/scroll.js +201 -0
- package/src/components/grid/stores/ui.js +168 -0
- package/src/components/grid/stores/users.js +115 -0
- package/src/components/grid/stores/validation.js +45 -0
- package/src/components/grid/stores/viewport.js +141 -0
- package/src/components/index.js +1 -0
- package/src/fetch/DataFetch.js +27 -21
- package/src/fetch/QueryFetch.js +1 -1
- package/src/themes/midnight.css +3 -0
- package/src/themes/nord.css +3 -0
- 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
|
+
}
|