Add WIP virtual dom implementation to massively increase performance
This commit is contained in:
parent
db469711cf
commit
43eadf2ec6
|
@ -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);
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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>
|
|
@ -100,9 +100,10 @@
|
|||
|
||||
<div class="sheet" style="--cell-height:{cellHeight}px;" id="sheet-{rand}">
|
||||
<SheetHeader />
|
||||
<SheetBody>
|
||||
<HeaderRow />
|
||||
{#each $visibleRows as row (row._id)}
|
||||
|
||||
<SheetBody>
|
||||
{#each $visibleRows as row}
|
||||
<SheetRow {row} />
|
||||
{/each}
|
||||
<NewRow />
|
||||
|
|
|
@ -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;"
|
||||
>
|
||||
<div
|
||||
class="data-content"
|
||||
style="height:{contentHeight}px; width:{contentWidth}px;"
|
||||
style="width:{contentWidth}px; --offset-y:{fakeOffsetY}px; --offset-x:{fakeOffsetX}px;"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</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 */
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 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)
|
||||
}
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue