From efca3eef4fff6b439f7caa06f6629192a8d9aa7e Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Mon, 27 Feb 2023 08:59:36 +0000 Subject: [PATCH] Clean up more sheet state and increase performance --- .../src/components/sheet/ResizeOverlay.svelte | 57 +------- .../src/components/sheet/Sheet.svelte | 47 ++++--- .../src/components/sheet/SheetBody.svelte | 122 +++++------------- .../src/components/sheet/SheetRow.svelte | 12 +- .../src/components/sheet/stores/viewport.js | 69 ++++++++++ packages/frontend-core/src/utils/utils.js | 7 +- 6 files changed, 134 insertions(+), 180 deletions(-) create mode 100644 packages/frontend-core/src/components/sheet/stores/viewport.js diff --git a/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte b/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte index aca8f59ac1..2c40f34f7d 100644 --- a/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte +++ b/packages/frontend-core/src/components/sheet/ResizeOverlay.svelte @@ -2,7 +2,7 @@ import { get } from "svelte/store" import { getContext } from "svelte" - const { columns, rand, visibleColumns } = getContext("spreadsheet") + const { columns, rand, scroll, visibleColumns } = getContext("spreadsheet") const MinColumnWidth = 100 let initialMouseX = null @@ -48,56 +48,7 @@ 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 - - // 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 = () => { @@ -108,12 +59,14 @@ } -{#each $columns as col} +{#each $visibleColumns as col}
startResizing(col.idx, e)} - style="--left:{col.left + col.width}px;" + style="--left:{col.left + + col.width - + (col.idx === 0 ? 0 : $scroll.left)}px;" >
diff --git a/packages/frontend-core/src/components/sheet/Sheet.svelte b/packages/frontend-core/src/components/sheet/Sheet.svelte index 4c3c58e5e1..637b824cb6 100644 --- a/packages/frontend-core/src/components/sheet/Sheet.svelte +++ b/packages/frontend-core/src/components/sheet/Sheet.svelte @@ -5,6 +5,7 @@ import { LuceneUtils } from "../../utils" import { Icon } from "@budibase/bbui" import { createReorderStores } from "./stores/reorder" + import { createViewportStores } from "./stores/viewport" import SpreadsheetHeader from "./SheetHeader.svelte" import SpreadsheetBody from "./SheetBody.svelte" import SpreadsheetCell from "./SheetCell.svelte" @@ -31,8 +32,6 @@ const selectedRows = writable({}) const changeCache = writable({}) const newRows = writable([]) - const visibleRows = writable([0, 0]) - const visibleColumns = writable([0, 0]) const scroll = writable({ left: 0, top: 0, @@ -57,12 +56,11 @@ changeCache, newRows, cellHeight, - visibleRows, - visibleColumns, bounds, scroll, } const { reorder, reorderPlaceholder } = createReorderStores(context) + const { visibleRows, visibleColumns } = createViewportStores(context) $: query = LuceneUtils.buildLuceneQuery(filter) $: fetch = createFetch(tableId) @@ -76,7 +74,6 @@ $: rowCount = $rows.length $: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length $: updateSortedRows($fetch, $newRows) - $: renderedRows = $rows.slice($visibleRows[0], $visibleRows[1]) const createFetch = tableId => { return fetchData({ @@ -192,7 +189,7 @@ const bIndex = newRows.indexOf(b._id) return aIndex < bIndex ? -1 : 1 }) - $rows = sortedRows + $rows = sortedRows.map((x, idx) => ({ ...x, __idx: idx })) } // API for children to consume @@ -206,6 +203,8 @@ ...context, reorder, reorderPlaceholder, + visibleRows, + visibleColumns, spreadsheetAPI, }) @@ -221,31 +220,31 @@ checked={rowCount && selectedRowCount === rowCount} /> - {#each $columns as field, fieldIdx} + {#each $visibleColumns as column} reorder.actions.startReordering(fieldIdx, e)} - width={field.width} - left={field.left} + sticky={column.idx === 0} + reorderSource={$reorder.columnIdx === column.idx} + reorderTarget={$reorder.swapColumnIdx === column.idx} + on:mousedown={e => reorder.actions.startReordering(column.idx, e)} + width={column.width} + left={column.left} > - {field.name} + {column.name} {/each}
- {#each renderedRows as row, rowIdx (row._id)} - + {#each $visibleRows as row (row._id)} + {/each} @@ -260,16 +259,16 @@ > - {#each $columns as field, fieldIdx} + {#each $visibleColumns as column} addRow(field)} + reorderSource={$reorder.columnIdx === column.idx} + reorderTarget={$reorder.swapColumnIdx === column.idx} + on:click={() => addRow(column)} on:mouseenter={() => ($hoveredRowId = "new")} - width={field.width} - left={field.left} + width={column.width} + left={column.left} /> {/each} diff --git a/packages/frontend-core/src/components/sheet/SheetBody.svelte b/packages/frontend-core/src/components/sheet/SheetBody.svelte index 12bd2dff70..fc5ea694ac 100644 --- a/packages/frontend-core/src/components/sheet/SheetBody.svelte +++ b/packages/frontend-core/src/components/sheet/SheetBody.svelte @@ -2,104 +2,30 @@ import { getContext, onMount } from "svelte" import { Utils } from "../../utils" - const { - columns, - selectedCellId, - rand, - visibleRows, - visibleColumns, - cellHeight, - rows, - bounds, - scroll, - } = getContext("spreadsheet") + const { columns, selectedCellId, rand, cellHeight, rows, bounds, scroll } = + getContext("spreadsheet") const padding = 180 let ref - let scrollLeft = 0 - let scrollTop = 0 - $: updateVisibleRows($columns, scrollTop, $bounds.height) - $: updateVisibleColumns($columns, scrollLeft, $bounds.width) - $: contentHeight = ($rows.length + 2) * cellHeight + padding + $: contentHeight = ($rows.length + 2) * cellHeight $: contentWidth = computeContentWidth($columns) - $: horizontallyScrolled = scrollLeft > 0 const computeContentWidth = columns => { - let total = 40 + padding - columns.forEach(col => { - total += col.width - }) - return total + if (!columns.length) { + return 0 + } + const last = columns[columns.length - 1] + return last.left + last.width } - // Store the current scroll position - // let lastTop - // 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 updateScrollStore = Utils.domDebounce((left, top) => { + scroll.set({ left, top }) + }) - const handleScroll = Utils.domDebounce( - ({ left, top }) => { - // 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]) + const handleScroll = e => { + updateScrollStore(e.target.scrollLeft, e.target.scrollTop) } onMount(() => { @@ -117,16 +43,22 @@
0} on:click|self={() => ($selectedCellId = null)} id={`sheet-${rand}-body`} on:scroll={handleScroll} >
- +
+ +
@@ -140,11 +72,19 @@ height: 0; } .sheet-body::-webkit-scrollbar-track { - background: var(--cell-background); + background: transparent; + } + .content, + .data-content { + position: absolute; } .content { min-width: 100%; min-height: 100%; + background: var(--background-alt); + } + .data-content { + background: var(--cell-background); } /* Add shadow to sticky cells when horizontally scrolled */ diff --git a/packages/frontend-core/src/components/sheet/SheetRow.svelte b/packages/frontend-core/src/components/sheet/SheetRow.svelte index 0a174e1548..03c6336f95 100644 --- a/packages/frontend-core/src/components/sheet/SheetRow.svelte +++ b/packages/frontend-core/src/components/sheet/SheetRow.svelte @@ -9,13 +9,11 @@ import TextCell from "./cells/TextCell.svelte" export let row - export let rowIdx const { selectedCellId, reorder, hoveredRowId, - columns, selectedRows, changeCache, spreadsheetAPI, @@ -26,10 +24,6 @@ $: rowSelected = !!$selectedRows[row._id] $: rowHovered = $hoveredRowId === row._id $: data = { ...row, ...$changeCache[row._id] } - $: renderedColumns = [ - $columns[0], - ...$columns.slice($visibleColumns[0], $visibleColumns[1]), - ] $: containsSelectedCell = $selectedCellId?.split("-")[0] === row._id const getCellForField = field => { @@ -58,7 +52,7 @@
{:else} - {rowIdx + 1} + {row.__idx + 1} {/if} - {#each renderedColumns as column (column.name)} + {#each $visibleColumns as column (column.name)} {@const cellIdx = `${row._id}-${column.name}`} { + 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 } +} diff --git a/packages/frontend-core/src/utils/utils.js b/packages/frontend-core/src/utils/utils.js index 5b23fa4d93..46d8395f77 100644 --- a/packages/frontend-core/src/utils/utils.js +++ b/packages/frontend-core/src/utils/utils.js @@ -90,18 +90,17 @@ export const throttle = (callback, minDelay = 1000) => { /** * Utility to debounce DOM activities using requestAnimationFrame * @param callback the function to run - * @param extractParams * @returns {Function} */ -export const domDebounce = (callback, extractParams = x => x) => { +export const domDebounce = callback => { let active = false let lastParams return (...params) => { - lastParams = extractParams(...params) + lastParams = params if (!active) { active = true requestAnimationFrame(() => { - callback(lastParams) + callback(...lastParams) active = false }) }