Add WIP virtual dom implementation to massively increase performance

This commit is contained in:
Andrew Kingston 2023-03-01 08:44:02 +00:00
parent db469711cf
commit 43eadf2ec6
7 changed files with 168 additions and 39 deletions

View File

@ -14,7 +14,7 @@
}
</script>
<div class="row new" style="--top:{($rows.length + 1) * cellHeight}px;">
<div class="row new">
<SheetCell label on:click={addRow} width="40" left="0">
<Icon hoverable name="Add" size="S" />
</SheetCell>
@ -33,9 +33,7 @@
<style>
.row {
display: flex;
top: var(--top);
width: inherit;
position: absolute;
}
:global(.sheet:not(.is-resizing):not(.is-reordering) .row:hover .cell) {
background: var(--cell-background-hover);

View File

@ -12,8 +12,8 @@
let left = 0
let columnCount = 0
$: cutoff = $scroll.left + 40 + $columns[0]?.width || 0
$: scrollLeft = $scroll.left
$: cutoff = scrollLeft + 40 + $columns[0]?.width || 0
$: rowCount = $visibleRows.length
const startResizing = (idx, e) => {

View File

@ -0,0 +1,124 @@
<script>
import { getContext } from "svelte"
import { domDebounce, debounce, throttle } from "../../utils/utils"
const { scroll, bounds, rows, cellHeight, columns } =
getContext("spreadsheet")
// Bar config
const barOffset = 4
// State for dragging bars
let initialMouse
let initialScroll
// Memoize store primitives to reduce reactive statement invalidations
$: scrollTop = $scroll.top
$: scrollLeft = $scroll.left
$: height = $bounds.height
$: width = $bounds.width
// Calculate V scrollbar size and offset
$: contentHeight = ($rows.length + 1) * cellHeight
$: barHeight = Math.max(50, (height / contentHeight) * height)
$: availHeight = height - barHeight - 2 * barOffset
$: maxScrollTop = contentHeight - height
$: barTop = barOffset + availHeight * (scrollTop / maxScrollTop)
// Calculate H scrollbar size and offset
$: lastCol = $columns[$columns.length - 1]
$: contentWidth = lastCol ? lastCol?.left + lastCol?.width : 0
$: barWidth = Math.max(50, (width / contentWidth) * width)
$: availWidth = width - barWidth - 8
$: maxScrollLeft = contentWidth - width
$: barLeft = 4 + availWidth * (scrollLeft / maxScrollLeft)
// Calculate whether to show scrollbars or not
$: showVScrollbar = contentHeight > height
$: showHScrollbar = contentWidth > width
// V scrollbar drag handlers
const startVDragging = e => {
e.preventDefault()
initialMouse = e.clientY
initialScroll = scrollTop
document.addEventListener("mousemove", moveVDragging)
document.addEventListener("mouseup", stopVDragging)
}
const moveVDragging = domDebounce(e => {
const delta = e.clientY - initialMouse
const weight = delta / availHeight
const newScrollTop = initialScroll + weight * maxScrollTop
scroll.update(state => ({
...state,
top: Math.max(0, Math.min(newScrollTop, maxScrollTop)),
}))
})
const stopVDragging = () => {
document.removeEventListener("mousemove", moveVDragging)
document.removeEventListener("mouseup", stopVDragging)
}
// H scrollbar drag handlers
const startHDragging = e => {
e.preventDefault()
initialMouse = e.clientX
initialScroll = scrollLeft
document.addEventListener("mousemove", moveHDragging)
document.addEventListener("mouseup", stopHDragging)
}
const moveHDragging = domDebounce(e => {
const delta = e.clientX - initialMouse
const weight = delta / availWidth
const newScrollLeft = initialScroll + weight * maxScrollLeft
scroll.update(state => ({
...state,
left: Math.max(0, Math.min(newScrollLeft, maxScrollLeft)),
}))
})
const stopHDragging = () => {
document.removeEventListener("mousemove", moveHDragging)
document.removeEventListener("mouseup", stopHDragging)
}
</script>
{#if showVScrollbar}
<div
class="v-scrollbar"
style="--top:{barTop}px; height:{barHeight}px;"
on:mousedown={startVDragging}
/>
{/if}
{#if showHScrollbar}
<div
class="h-scrollbar"
style="--left:{barLeft}px; width:{barWidth}px;"
on:mousedown={startHDragging}
/>
{/if}
<style>
div {
position: absolute;
background: var(--spectrum-global-color-gray-600);
opacity: 0.6;
border-radius: 4px;
z-index: 20;
transition: opacity 130ms ease-out;
}
div:hover {
opacity: 0.9;
}
.v-scrollbar {
top: var(--top);
height: var(--height);
right: 4px;
width: 8px;
}
.h-scrollbar {
bottom: 4px;
height: 8px;
width: var(--width);
left: var(--left);
}
</style>

View File

@ -100,9 +100,10 @@
<div class="sheet" style="--cell-height:{cellHeight}px;" id="sheet-{rand}">
<SheetHeader />
<HeaderRow />
<SheetBody>
<HeaderRow />
{#each $visibleRows as row (row._id)}
{#each $visibleRows as row}
<SheetRow {row} />
{/each}
<NewRow />

View File

@ -1,6 +1,7 @@
<script>
import { getContext, onMount } from "svelte"
import { Utils } from "../../utils"
import ScrollOverlay from "./ScrollOverlay.svelte"
const { columns, selectedCellId, cellHeight, rows, bounds, scroll } =
getContext("spreadsheet")
@ -12,6 +13,7 @@
$: contentHeight = ($rows.length + 2) * cellHeight
$: contentWidth = computeContentWidth($columns)
$: scrollLeft = $scroll.left
$: scrollTop = $scroll.top
const computeContentWidth = columns => {
if (!columns.length) {
@ -29,6 +31,26 @@
updateScrollStore(e.target.scrollLeft, e.target.scrollTop)
}
const handleWheel = e => {
const step = cellHeight * 3
const deltaY = e.deltaY < 0 ? -1 : 1
const offset = deltaY * step
let newScrollTop = scrollTop
newScrollTop += offset
newScrollTop = Math.max(0, newScrollTop)
newScrollTop = Math.min(
newScrollTop,
$rows.length * cellHeight - $bounds.height
)
scroll.update(state => ({
...state,
top: newScrollTop,
}))
}
$: fakeOffsetY = -1 * (scrollTop % cellHeight)
$: fakeOffsetX = -1 * scrollLeft
onMount(() => {
// Observe and record the height of the body
const observer = new ResizeObserver(() => {
@ -46,20 +68,15 @@
class="sheet-body"
class:horizontally-scrolled={scrollLeft > 0}
on:click|self={() => ($selectedCellId = null)}
on:scroll={handleScroll}
on:wheel|passive={handleWheel}
>
<div
class="content"
style="height:{contentHeight + padding}px; width:{contentWidth +
padding}px;"
style="width:{contentWidth}px; --offset-y:{fakeOffsetY}px; --offset-x:{fakeOffsetX}px;"
>
<div
class="data-content"
style="height:{contentHeight}px; width:{contentWidth}px;"
>
<slot />
</div>
<slot />
</div>
<ScrollOverlay />
</div>
<style>
@ -67,24 +84,19 @@
display: block;
position: relative;
cursor: default;
overflow: auto;
overflow: hidden;
flex: 1 1 auto;
height: 0;
}
.sheet-body::-webkit-scrollbar-track {
background: transparent;
}
.content,
.data-content {
position: absolute;
}
.content {
min-width: 100%;
min-height: 100%;
background: var(--background-alt);
}
.data-content {
background: var(--cell-background);
transform: translate3d(var(--offset-x), var(--offset-y), 0);
overflow: hidden;
}
/* Add shadow to sticky cells when horizontally scrolled */

View File

@ -29,6 +29,8 @@
link: RelationshipCell,
}
console.log("mount")
$: rowSelected = !!$selectedRows[row._id]
const selectRow = id => {
@ -39,7 +41,7 @@
}
</script>
<div class="row" style="--top:{(row.__idx + 1) * cellHeight}px;">
<div class="row">
<SpreadsheetCell label {rowSelected} on:click={() => selectRow(row._id)}>
<div class="checkbox" class:visible={rowSelected}>
<Checkbox value={rowSelected} />
@ -76,8 +78,7 @@
<style>
.row {
display: flex;
position: absolute;
top: var(--top);
position: relative;
width: inherit;
}
:global(.sheet:not(.is-resizing):not(.is-reordering) .row:hover .cell) {

View File

@ -16,25 +16,18 @@ export const createViewportStores = context => {
// Debounce scroll updates so we can slow down visible row computation
scroll.subscribe(({ left, top }) => {
window.requestAnimationFrame(() => {
// Only update local state when big changes occur
if (Math.abs(top - scrollTop) > cellHeight * 4) {
scrollTop = top
scrollTopStore.set(top)
}
if (Math.abs(left - scrollLeft) > 100) {
scrollLeft = left
scrollLeftStore.set(left)
}
})
scrollTop = top
scrollTopStore.set(top)
scrollLeft = left
scrollLeftStore.set(left)
})
// Derive visible rows
const visibleRows = derived(
[rows, scrollTopStore, height],
([$rows, $scrollTop, $height]) => {
const maxRows = Math.ceil($height / cellHeight) + 16
const firstRow = Math.max(0, Math.floor($scrollTop / cellHeight) - 8)
const maxRows = Math.ceil($height / cellHeight) + 1
const firstRow = Math.max(0, Math.floor($scrollTop / cellHeight))
return $rows.slice(firstRow, firstRow + maxRows)
}
)