Clean up more sheet state and increase performance

This commit is contained in:
Andrew Kingston 2023-02-27 08:59:36 +00:00
parent fae24276f9
commit efca3eef4f
6 changed files with 134 additions and 180 deletions

View File

@ -2,7 +2,7 @@
import { get } from "svelte/store" import { get } from "svelte/store"
import { getContext } from "svelte" import { getContext } from "svelte"
const { columns, rand, visibleColumns } = getContext("spreadsheet") const { columns, rand, scroll, visibleColumns } = getContext("spreadsheet")
const MinColumnWidth = 100 const MinColumnWidth = 100
let initialMouseX = null let initialMouseX = null
@ -48,56 +48,7 @@
return state return state
}) })
// let newStyle = `--col-${columnIdx}-width:${newWidth}px;`
//
// let offset = left + newWidth
// for (let i = columnIdx + 1; i < columnCount; i++) {
// const colWidth = parseInt(styles.getPropertyValue(`--col-${i}-width`))
// newStyle += `--col-${i}-left:${offset}px;`
// offset += colWidth
// }
//
// sheet.style.cssText += newStyle
// let cells = sheet.querySelectorAll(`[data-col="${columnIdx}"]`)
// let left
// cells.forEach(cell => {
// cell.style.width = `${newWidth}px`
// cell.dataset.width = newWidth
// if (!left) {
// left = parseInt(cell.dataset.left)
// }
// })
//
// let offset = left + newWidth
// for (let i = columnIdx + 1; i < columnCount; i++) {
// cells = sheet.querySelectorAll(`[data-col="${i}"]`)
// let colWidth
// cells.forEach(cell => {
// cell.style.transform = `translateX(${offset}px)`
// cell.dataset.left = offset
// if (!colWidth) {
// colWidth = parseInt(cell.dataset.width)
// }
// })
// offset += colWidth
// }
width = newWidth width = newWidth
// Update width of column
// columns.update(state => {
// state[$resize.columnIdx].width = Math.round(newWidth)
//
// // Update left offset of other columns
// let offset = 40
// state.forEach(col => {
// col.left = offset
// offset += col.width
// })
//
// return state
// })
} }
const stopResizing = () => { const stopResizing = () => {
@ -108,12 +59,14 @@
} }
</script> </script>
{#each $columns as col} {#each $visibleColumns as col}
<div <div
class="resize-slider" class="resize-slider"
class:visible={columnIdx === col.idx} class:visible={columnIdx === col.idx}
on:mousedown={e => startResizing(col.idx, e)} on:mousedown={e => startResizing(col.idx, e)}
style="--left:{col.left + col.width}px;" style="--left:{col.left +
col.width -
(col.idx === 0 ? 0 : $scroll.left)}px;"
> >
<div class="resize-indicator" /> <div class="resize-indicator" />
</div> </div>

View File

@ -5,6 +5,7 @@
import { LuceneUtils } from "../../utils" import { LuceneUtils } from "../../utils"
import { Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
import { createReorderStores } from "./stores/reorder" import { createReorderStores } from "./stores/reorder"
import { createViewportStores } from "./stores/viewport"
import SpreadsheetHeader from "./SheetHeader.svelte" import SpreadsheetHeader from "./SheetHeader.svelte"
import SpreadsheetBody from "./SheetBody.svelte" import SpreadsheetBody from "./SheetBody.svelte"
import SpreadsheetCell from "./SheetCell.svelte" import SpreadsheetCell from "./SheetCell.svelte"
@ -31,8 +32,6 @@
const selectedRows = writable({}) const selectedRows = writable({})
const changeCache = writable({}) const changeCache = writable({})
const newRows = writable([]) const newRows = writable([])
const visibleRows = writable([0, 0])
const visibleColumns = writable([0, 0])
const scroll = writable({ const scroll = writable({
left: 0, left: 0,
top: 0, top: 0,
@ -57,12 +56,11 @@
changeCache, changeCache,
newRows, newRows,
cellHeight, cellHeight,
visibleRows,
visibleColumns,
bounds, bounds,
scroll, scroll,
} }
const { reorder, reorderPlaceholder } = createReorderStores(context) const { reorder, reorderPlaceholder } = createReorderStores(context)
const { visibleRows, visibleColumns } = createViewportStores(context)
$: query = LuceneUtils.buildLuceneQuery(filter) $: query = LuceneUtils.buildLuceneQuery(filter)
$: fetch = createFetch(tableId) $: fetch = createFetch(tableId)
@ -76,7 +74,6 @@
$: rowCount = $rows.length $: rowCount = $rows.length
$: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length $: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length
$: updateSortedRows($fetch, $newRows) $: updateSortedRows($fetch, $newRows)
$: renderedRows = $rows.slice($visibleRows[0], $visibleRows[1])
const createFetch = tableId => { const createFetch = tableId => {
return fetchData({ return fetchData({
@ -192,7 +189,7 @@
const bIndex = newRows.indexOf(b._id) const bIndex = newRows.indexOf(b._id)
return aIndex < bIndex ? -1 : 1 return aIndex < bIndex ? -1 : 1
}) })
$rows = sortedRows $rows = sortedRows.map((x, idx) => ({ ...x, __idx: idx }))
} }
// API for children to consume // API for children to consume
@ -206,6 +203,8 @@
...context, ...context,
reorder, reorder,
reorderPlaceholder, reorderPlaceholder,
visibleRows,
visibleColumns,
spreadsheetAPI, spreadsheetAPI,
}) })
</script> </script>
@ -221,31 +220,31 @@
checked={rowCount && selectedRowCount === rowCount} checked={rowCount && selectedRowCount === rowCount}
/> />
</SpreadsheetCell> </SpreadsheetCell>
{#each $columns as field, fieldIdx} {#each $visibleColumns as column}
<SpreadsheetCell <SpreadsheetCell
header header
sticky={fieldIdx === 0} sticky={column.idx === 0}
reorderSource={$reorder.columnIdx === fieldIdx} reorderSource={$reorder.columnIdx === column.idx}
reorderTarget={$reorder.swapColumnIdx === fieldIdx} reorderTarget={$reorder.swapColumnIdx === column.idx}
on:mousedown={e => reorder.actions.startReordering(fieldIdx, e)} on:mousedown={e => reorder.actions.startReordering(column.idx, e)}
width={field.width} width={column.width}
left={field.left} left={column.left}
> >
<Icon <Icon
size="S" size="S"
name={getIconForField(field)} name={getIconForField(column)}
color="var(--spectrum-global-color-gray-600)" color="var(--spectrum-global-color-gray-600)"
/> />
<span> <span>
{field.name} {column.name}
</span> </span>
</SpreadsheetCell> </SpreadsheetCell>
{/each} {/each}
</div> </div>
<!-- All real rows --> <!-- All real rows -->
{#each renderedRows as row, rowIdx (row._id)} {#each $visibleRows as row (row._id)}
<SpreadsheetRow {row} rowIdx={rowIdx + $visibleRows[0]} /> <SpreadsheetRow {row} />
{/each} {/each}
<!-- New row placeholder --> <!-- New row placeholder -->
@ -260,16 +259,16 @@
> >
<Icon hoverable name="Add" size="S" /> <Icon hoverable name="Add" size="S" />
</SpreadsheetCell> </SpreadsheetCell>
{#each $columns as field, fieldIdx} {#each $visibleColumns as column}
<SpreadsheetCell <SpreadsheetCell
sticky={fieldIdx === 0} sticky={column.idx === 0}
rowHovered={$hoveredRowId === "new"} rowHovered={$hoveredRowId === "new"}
reorderSource={$reorder.columnIdx === fieldIdx} reorderSource={$reorder.columnIdx === column.idx}
reorderTarget={$reorder.swapColumnIdx === fieldIdx} reorderTarget={$reorder.swapColumnIdx === column.idx}
on:click={() => addRow(field)} on:click={() => addRow(column)}
on:mouseenter={() => ($hoveredRowId = "new")} on:mouseenter={() => ($hoveredRowId = "new")}
width={field.width} width={column.width}
left={field.left} left={column.left}
/> />
{/each} {/each}
</div> </div>

View File

@ -2,104 +2,30 @@
import { getContext, onMount } from "svelte" import { getContext, onMount } from "svelte"
import { Utils } from "../../utils" import { Utils } from "../../utils"
const { const { columns, selectedCellId, rand, cellHeight, rows, bounds, scroll } =
columns, getContext("spreadsheet")
selectedCellId,
rand,
visibleRows,
visibleColumns,
cellHeight,
rows,
bounds,
scroll,
} = getContext("spreadsheet")
const padding = 180 const padding = 180
let ref let ref
let scrollLeft = 0
let scrollTop = 0
$: updateVisibleRows($columns, scrollTop, $bounds.height) $: contentHeight = ($rows.length + 2) * cellHeight
$: updateVisibleColumns($columns, scrollLeft, $bounds.width)
$: contentHeight = ($rows.length + 2) * cellHeight + padding
$: contentWidth = computeContentWidth($columns) $: contentWidth = computeContentWidth($columns)
$: horizontallyScrolled = scrollLeft > 0
const computeContentWidth = columns => { const computeContentWidth = columns => {
let total = 40 + padding if (!columns.length) {
columns.forEach(col => { return 0
total += col.width }
}) const last = columns[columns.length - 1]
return total return last.left + last.width
} }
// Store the current scroll position const updateScrollStore = Utils.domDebounce((left, top) => {
// let lastTop scroll.set({ left, top })
// let lastLeft })
// let ticking = false
// const handleScroll = e => {
// lastTop = e.target.scrollTop
// lastLeft = e.target.scrollLeft
// if (!ticking) {
// ticking = true
// requestAnimationFrame(() => {
// if (Math.abs(lastTop - scrollTop) > 100) {
// scrollTop = lastTop
// }
// if (lastLeft === 0 || Math.abs(lastLeft - scrollLeft) > 100) {
// scrollLeft = lastLeft
// }
// ticking = false
// })
// }
// }
const handleScroll = Utils.domDebounce( const handleScroll = e => {
({ left, top }) => { updateScrollStore(e.target.scrollLeft, e.target.scrollTop)
// Only update local state when big changes occur
if (Math.abs(top - scrollTop) > 100) {
scrollTop = top
}
if (left === 0 || Math.abs(left - scrollLeft) > 100) {
scrollLeft = left
}
// Always update store
scroll.set({ left, top })
},
e => ({ left: e.target.scrollLeft, top: e.target.scrollTop })
)
const updateVisibleRows = (columns, scrollTop, height) => {
if (!columns.length) {
return
}
// Compute row visibility
const rows = Math.ceil(height / cellHeight) + 8
const firstRow = Math.max(0, Math.floor(scrollTop / cellHeight) - 4)
visibleRows.set([firstRow, firstRow + rows])
}
const updateVisibleColumns = (columns, scrollLeft, width) => {
if (!columns.length) {
return
}
// Compute column visibility
let startColIdx = 1
let rightEdge = columns[1].width
while (rightEdge < scrollLeft) {
startColIdx++
rightEdge += columns[startColIdx].width
}
let endColIdx = startColIdx + 1
let leftEdge = columns[0].width + 40 + rightEdge
while (leftEdge < width + scrollLeft) {
leftEdge += columns[endColIdx]?.width
endColIdx++
}
visibleColumns.set([Math.max(1, startColIdx - 2), endColIdx + 2])
} }
onMount(() => { onMount(() => {
@ -117,16 +43,22 @@
<div <div
bind:this={ref} bind:this={ref}
class="sheet-body" class="sheet-body"
class:horizontally-scrolled={horizontallyScrolled} class:horizontally-scrolled={$scroll.left > 0}
on:click|self={() => ($selectedCellId = null)} on:click|self={() => ($selectedCellId = null)}
id={`sheet-${rand}-body`} id={`sheet-${rand}-body`}
on:scroll={handleScroll} on:scroll={handleScroll}
> >
<div <div
class="content" class="content"
style="height:{contentHeight}px; width:{contentWidth}px;" style="height:{contentHeight + padding}px; width:{contentWidth +
padding}px;"
> >
<slot /> <div
class="data-content"
style="height:{contentHeight}px; width:{contentWidth}px;"
>
<slot />
</div>
</div> </div>
</div> </div>
@ -140,11 +72,19 @@
height: 0; height: 0;
} }
.sheet-body::-webkit-scrollbar-track { .sheet-body::-webkit-scrollbar-track {
background: var(--cell-background); background: transparent;
}
.content,
.data-content {
position: absolute;
} }
.content { .content {
min-width: 100%; min-width: 100%;
min-height: 100%; min-height: 100%;
background: var(--background-alt);
}
.data-content {
background: var(--cell-background);
} }
/* Add shadow to sticky cells when horizontally scrolled */ /* Add shadow to sticky cells when horizontally scrolled */

View File

@ -9,13 +9,11 @@
import TextCell from "./cells/TextCell.svelte" import TextCell from "./cells/TextCell.svelte"
export let row export let row
export let rowIdx
const { const {
selectedCellId, selectedCellId,
reorder, reorder,
hoveredRowId, hoveredRowId,
columns,
selectedRows, selectedRows,
changeCache, changeCache,
spreadsheetAPI, spreadsheetAPI,
@ -26,10 +24,6 @@
$: rowSelected = !!$selectedRows[row._id] $: rowSelected = !!$selectedRows[row._id]
$: rowHovered = $hoveredRowId === row._id $: rowHovered = $hoveredRowId === row._id
$: data = { ...row, ...$changeCache[row._id] } $: data = { ...row, ...$changeCache[row._id] }
$: renderedColumns = [
$columns[0],
...$columns.slice($visibleColumns[0], $visibleColumns[1]),
]
$: containsSelectedCell = $selectedCellId?.split("-")[0] === row._id $: containsSelectedCell = $selectedCellId?.split("-")[0] === row._id
const getCellForField = field => { const getCellForField = field => {
@ -58,7 +52,7 @@
<div <div
class="row" class="row"
style="--top:{(rowIdx + 1) * cellHeight}px;" style="--top:{(row.__idx + 1) * cellHeight}px;"
class:contains-selected-cell={containsSelectedCell} class:contains-selected-cell={containsSelectedCell}
> >
<SpreadsheetCell <SpreadsheetCell
@ -72,11 +66,11 @@
<input type="checkbox" checked={rowSelected} /> <input type="checkbox" checked={rowSelected} />
{:else} {:else}
<span> <span>
{rowIdx + 1} {row.__idx + 1}
</span> </span>
{/if} {/if}
</SpreadsheetCell> </SpreadsheetCell>
{#each renderedColumns as column (column.name)} {#each $visibleColumns as column (column.name)}
{@const cellIdx = `${row._id}-${column.name}`} {@const cellIdx = `${row._id}-${column.name}`}
<SpreadsheetCell <SpreadsheetCell
{rowSelected} {rowSelected}

View File

@ -0,0 +1,69 @@
import { writable, derived } from "svelte/store"
export const createViewportStores = context => {
const { cellHeight, columns, rows, scroll, bounds } = context
// Use local variables to avoid needing to invoke 2 svelte getters each time
// scroll state changes, but also use stores to allow use of derived stores
let scrollTop = 0
let scrollLeft = 0
const scrollTopStore = writable(0)
const scrollLeftStore = writable(0)
// Derive height and width as primitives to avoid wasted computation
const width = derived(bounds, $bounds => $bounds.width)
const height = derived(bounds, $bounds => $bounds.height)
// Debounce scroll updates so we can slow down visible row computation
scroll.subscribe(({ left, top }) => {
// Only update local state when big changes occur
if (Math.abs(top - scrollTop) > cellHeight * 2) {
scrollTop = top
scrollTopStore.set(top)
}
if (Math.abs(left - scrollLeft) > 100) {
scrollLeft = left
scrollLeftStore.set(left)
}
})
// Derive visible rows
const visibleRows = derived(
[rows, scrollTopStore, height],
([$rows, $scrollTop, $height]) => {
console.log("new rows")
const maxRows = Math.ceil($height / cellHeight) + 8
const firstRow = Math.max(0, Math.floor($scrollTop / cellHeight) - 4)
return $rows.slice(firstRow, firstRow + maxRows)
}
)
// Derive visible columns
const visibleColumns = derived(
[columns, scrollLeftStore, width],
([$columns, $scrollLeft, $width]) => {
console.log("new columns")
if (!$columns.length) {
return []
}
let startColIdx = 1
let rightEdge = $columns[1].width
while (rightEdge < $scrollLeft) {
startColIdx++
rightEdge += $columns[startColIdx].width
}
let endColIdx = startColIdx + 1
let leftEdge = $columns[0].width + 40 + rightEdge
while (leftEdge < $width + $scrollLeft) {
leftEdge += $columns[endColIdx]?.width
endColIdx++
}
return [
$columns[0],
...$columns.slice(Math.max(1, startColIdx - 2), endColIdx + 2),
]
}
)
return { visibleRows, visibleColumns }
}

View File

@ -90,18 +90,17 @@ export const throttle = (callback, minDelay = 1000) => {
/** /**
* Utility to debounce DOM activities using requestAnimationFrame * Utility to debounce DOM activities using requestAnimationFrame
* @param callback the function to run * @param callback the function to run
* @param extractParams
* @returns {Function} * @returns {Function}
*/ */
export const domDebounce = (callback, extractParams = x => x) => { export const domDebounce = callback => {
let active = false let active = false
let lastParams let lastParams
return (...params) => { return (...params) => {
lastParams = extractParams(...params) lastParams = params
if (!active) { if (!active) {
active = true active = true
requestAnimationFrame(() => { requestAnimationFrame(() => {
callback(lastParams) callback(...lastParams)
active = false active = false
}) })
} }