Add WIP virtual dom implementation to massively increase performance
This commit is contained in:
parent
db469711cf
commit
43eadf2ec6
|
@ -14,7 +14,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</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">
|
<SheetCell label on:click={addRow} width="40" left="0">
|
||||||
<Icon hoverable name="Add" size="S" />
|
<Icon hoverable name="Add" size="S" />
|
||||||
</SheetCell>
|
</SheetCell>
|
||||||
|
@ -33,9 +33,7 @@
|
||||||
<style>
|
<style>
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
top: var(--top);
|
|
||||||
width: inherit;
|
width: inherit;
|
||||||
position: absolute;
|
|
||||||
}
|
}
|
||||||
:global(.sheet:not(.is-resizing):not(.is-reordering) .row:hover .cell) {
|
:global(.sheet:not(.is-resizing):not(.is-reordering) .row:hover .cell) {
|
||||||
background: var(--cell-background-hover);
|
background: var(--cell-background-hover);
|
||||||
|
|
|
@ -12,8 +12,8 @@
|
||||||
let left = 0
|
let left = 0
|
||||||
let columnCount = 0
|
let columnCount = 0
|
||||||
|
|
||||||
$: cutoff = $scroll.left + 40 + $columns[0]?.width || 0
|
|
||||||
$: scrollLeft = $scroll.left
|
$: scrollLeft = $scroll.left
|
||||||
|
$: cutoff = scrollLeft + 40 + $columns[0]?.width || 0
|
||||||
$: rowCount = $visibleRows.length
|
$: rowCount = $visibleRows.length
|
||||||
|
|
||||||
const startResizing = (idx, e) => {
|
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}">
|
<div class="sheet" style="--cell-height:{cellHeight}px;" id="sheet-{rand}">
|
||||||
<SheetHeader />
|
<SheetHeader />
|
||||||
|
<HeaderRow />
|
||||||
|
|
||||||
<SheetBody>
|
<SheetBody>
|
||||||
<HeaderRow />
|
{#each $visibleRows as row}
|
||||||
{#each $visibleRows as row (row._id)}
|
|
||||||
<SheetRow {row} />
|
<SheetRow {row} />
|
||||||
{/each}
|
{/each}
|
||||||
<NewRow />
|
<NewRow />
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext, onMount } from "svelte"
|
import { getContext, onMount } from "svelte"
|
||||||
import { Utils } from "../../utils"
|
import { Utils } from "../../utils"
|
||||||
|
import ScrollOverlay from "./ScrollOverlay.svelte"
|
||||||
|
|
||||||
const { columns, selectedCellId, cellHeight, rows, bounds, scroll } =
|
const { columns, selectedCellId, cellHeight, rows, bounds, scroll } =
|
||||||
getContext("spreadsheet")
|
getContext("spreadsheet")
|
||||||
|
@ -12,6 +13,7 @@
|
||||||
$: contentHeight = ($rows.length + 2) * cellHeight
|
$: contentHeight = ($rows.length + 2) * cellHeight
|
||||||
$: contentWidth = computeContentWidth($columns)
|
$: contentWidth = computeContentWidth($columns)
|
||||||
$: scrollLeft = $scroll.left
|
$: scrollLeft = $scroll.left
|
||||||
|
$: scrollTop = $scroll.top
|
||||||
|
|
||||||
const computeContentWidth = columns => {
|
const computeContentWidth = columns => {
|
||||||
if (!columns.length) {
|
if (!columns.length) {
|
||||||
|
@ -29,6 +31,26 @@
|
||||||
updateScrollStore(e.target.scrollLeft, e.target.scrollTop)
|
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(() => {
|
onMount(() => {
|
||||||
// Observe and record the height of the body
|
// Observe and record the height of the body
|
||||||
const observer = new ResizeObserver(() => {
|
const observer = new ResizeObserver(() => {
|
||||||
|
@ -46,20 +68,15 @@
|
||||||
class="sheet-body"
|
class="sheet-body"
|
||||||
class:horizontally-scrolled={scrollLeft > 0}
|
class:horizontally-scrolled={scrollLeft > 0}
|
||||||
on:click|self={() => ($selectedCellId = null)}
|
on:click|self={() => ($selectedCellId = null)}
|
||||||
on:scroll={handleScroll}
|
on:wheel|passive={handleWheel}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="content"
|
class="content"
|
||||||
style="height:{contentHeight + padding}px; width:{contentWidth +
|
style="width:{contentWidth}px; --offset-y:{fakeOffsetY}px; --offset-x:{fakeOffsetX}px;"
|
||||||
padding}px;"
|
|
||||||
>
|
>
|
||||||
<div
|
<slot />
|
||||||
class="data-content"
|
|
||||||
style="height:{contentHeight}px; width:{contentWidth}px;"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<ScrollOverlay />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
@ -67,24 +84,19 @@
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
.sheet-body::-webkit-scrollbar-track {
|
.sheet-body::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
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);
|
background: var(--background-alt);
|
||||||
}
|
transform: translate3d(var(--offset-x), var(--offset-y), 0);
|
||||||
.data-content {
|
overflow: hidden;
|
||||||
background: var(--cell-background);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Add shadow to sticky cells when horizontally scrolled */
|
/* Add shadow to sticky cells when horizontally scrolled */
|
||||||
|
|
|
@ -29,6 +29,8 @@
|
||||||
link: RelationshipCell,
|
link: RelationshipCell,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("mount")
|
||||||
|
|
||||||
$: rowSelected = !!$selectedRows[row._id]
|
$: rowSelected = !!$selectedRows[row._id]
|
||||||
|
|
||||||
const selectRow = id => {
|
const selectRow = id => {
|
||||||
|
@ -39,7 +41,7 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="row" style="--top:{(row.__idx + 1) * cellHeight}px;">
|
<div class="row">
|
||||||
<SpreadsheetCell label {rowSelected} on:click={() => selectRow(row._id)}>
|
<SpreadsheetCell label {rowSelected} on:click={() => selectRow(row._id)}>
|
||||||
<div class="checkbox" class:visible={rowSelected}>
|
<div class="checkbox" class:visible={rowSelected}>
|
||||||
<Checkbox value={rowSelected} />
|
<Checkbox value={rowSelected} />
|
||||||
|
@ -76,8 +78,7 @@
|
||||||
<style>
|
<style>
|
||||||
.row {
|
.row {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: absolute;
|
position: relative;
|
||||||
top: var(--top);
|
|
||||||
width: inherit;
|
width: inherit;
|
||||||
}
|
}
|
||||||
:global(.sheet:not(.is-resizing):not(.is-reordering) .row:hover .cell) {
|
: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
|
// Debounce scroll updates so we can slow down visible row computation
|
||||||
scroll.subscribe(({ left, top }) => {
|
scroll.subscribe(({ left, top }) => {
|
||||||
window.requestAnimationFrame(() => {
|
scrollTop = top
|
||||||
// Only update local state when big changes occur
|
scrollTopStore.set(top)
|
||||||
if (Math.abs(top - scrollTop) > cellHeight * 4) {
|
scrollLeft = left
|
||||||
scrollTop = top
|
scrollLeftStore.set(left)
|
||||||
scrollTopStore.set(top)
|
|
||||||
}
|
|
||||||
if (Math.abs(left - scrollLeft) > 100) {
|
|
||||||
scrollLeft = left
|
|
||||||
scrollLeftStore.set(left)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Derive visible rows
|
// Derive visible rows
|
||||||
const visibleRows = derived(
|
const visibleRows = derived(
|
||||||
[rows, scrollTopStore, height],
|
[rows, scrollTopStore, height],
|
||||||
([$rows, $scrollTop, $height]) => {
|
([$rows, $scrollTop, $height]) => {
|
||||||
const maxRows = Math.ceil($height / cellHeight) + 16
|
const maxRows = Math.ceil($height / cellHeight) + 1
|
||||||
const firstRow = Math.max(0, Math.floor($scrollTop / cellHeight) - 8)
|
const firstRow = Math.max(0, Math.floor($scrollTop / cellHeight))
|
||||||
return $rows.slice(firstRow, firstRow + maxRows)
|
return $rows.slice(firstRow, firstRow + maxRows)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue