From 4d3f669ae7c5f4429ee79d7a021874e9db98d1f7 Mon Sep 17 00:00:00 2001 From: Andrew Kingston Date: Fri, 24 Feb 2023 13:53:23 +0000 Subject: [PATCH] Optimise virtual rendering for both columns and rows to handle infinitely large datasets --- .../app/spreadsheet/Spreadsheet.svelte | 71 ++++++----- .../app/spreadsheet/SpreadsheetBody.svelte | 117 +++++++++++++----- .../app/spreadsheet/SpreadsheetCell.svelte | 13 +- .../app/spreadsheet/SpreadsheetRow.svelte | 106 ++++++++-------- .../app/spreadsheet/stores/reorder.js | 33 ++--- .../app/spreadsheet/stores/resize.js | 62 ++++++++-- 6 files changed, 251 insertions(+), 151 deletions(-) diff --git a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte index 49dbbf00d8..d57da63b19 100644 --- a/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte +++ b/packages/client/src/components/app/spreadsheet/Spreadsheet.svelte @@ -10,8 +10,6 @@ import SpreadsheetHeader from "./SpreadsheetHeader.svelte" import SpreadsheetBody from "./SpreadsheetBody.svelte" import SpreadsheetCell from "./SpreadsheetCell.svelte" - import SpacerCell from "./SpacerCell.svelte" - import VerticalSpacer from "./VerticalSpacer.svelte" import SpreadsheetRow from "./SpreadsheetRow.svelte" export let table @@ -37,7 +35,7 @@ const tableId = writable(table?.tableId) const changeCache = writable({}) const newRows = writable([]) - const visibleRows = writable([0, 0]) + const visibleCells = writable({ y: [0, 0], x: [0, 0] }) // Build up spreadsheet context and additional stores const context = { @@ -51,7 +49,7 @@ changeCache, newRows, cellHeight, - visibleRows, + visibleCells, } const { reorder, reorderPlaceholder } = createReorderStores(context) const resize = createResizeStore(context) @@ -68,7 +66,8 @@ $: generateColumns($fetch) $: rowCount = $rows.length $: selectedRowCount = Object.values($selectedRows).filter(x => !!x).length - $: updateSortedRows($fetch.rows, $newRows) + $: updateSortedRows($fetch, $newRows) + $: visibleRows = $rows.slice($visibleCells.y[0], $visibleCells.y[1]) const createFetch = datasource => { return fetchData({ @@ -92,13 +91,16 @@ if (primaryDisplay) { fields = [primaryDisplay, ...fields.filter(x => x !== primaryDisplay)] } - $columns = fields.map((field, idx) => ({ - name: field, - width: defaultWidth, - left: 40 + idx * defaultWidth, - schema: schema[field], - primaryDisplay: field === primaryDisplay, - })) + $columns = fields.map((field, idx) => { + return { + idx, + name: field, + width: defaultWidth, + left: 40 + idx * defaultWidth, + schema: schema[field], + primaryDisplay: field === primaryDisplay, + } + }) } } @@ -157,13 +159,17 @@ } const updateSortedRows = (unsortedRows, newRows) => { - let sortedRows = unsortedRows.slice() - sortedRows.sort((a, b) => { - const aIndex = newRows.indexOf(a._id) - const bIndex = newRows.indexOf(b._id) - return aIndex < bIndex ? -1 : 1 - }) - $rows = sortedRows + let foo = unsortedRows.rows + for (let i = 0; i < 10; i++) { + foo = foo.concat(foo.map(x => ({ ...x, _id: x._id + "x" }))) + } + // let sortedRows = foo.slice() + // sortedRows.sort((a, b) => { + // const aIndex = newRows.indexOf(a._id) + // const bIndex = newRows.indexOf(b._id) + // return aIndex < bIndex ? -1 : 1 + // }) + $rows = foo.slice() } // API for children to consume @@ -180,25 +186,18 @@ resize, spreadsheetAPI, }) - - let sheetStyles = "" - let left = 40 - for (let i = 0; i < 20; i++) { - sheetStyles += `--col-${i}-width:${160}px; --col-${i}-left:${left}px;` - left += 160 - }
-
+
reorder.actions.startReordering(fieldIdx, e)} - id={`sheet-${rand}-header-${fieldIdx}`} width={field.width} left={field.left} - column={fieldIdx} > - {#each $rows as row, rowIdx (row._id)} - + {#each visibleRows as row, rowIdx (row._id)} + {/each} -
+
diff --git a/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte b/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte index 5853ddceb4..185dc518f5 100644 --- a/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte +++ b/packages/client/src/components/app/spreadsheet/SpreadsheetBody.svelte @@ -1,57 +1,85 @@
($selectedCellId = null)} id={`sheet-${rand}-body`} + style={sheetStyles} >
diff --git a/packages/client/src/components/app/spreadsheet/SpreadsheetCell.svelte b/packages/client/src/components/app/spreadsheet/SpreadsheetCell.svelte index 2501d47726..b8138a501d 100644 --- a/packages/client/src/components/app/spreadsheet/SpreadsheetCell.svelte +++ b/packages/client/src/components/app/spreadsheet/SpreadsheetCell.svelte @@ -8,8 +8,8 @@ export let selected = false export let reorderSource = false export let reorderTarget = false - export let id = null - export let column + export let left + export let width
@@ -52,6 +51,8 @@ background: var(--cell-background); position: absolute; transition: border-color 130ms ease-out; + width: var(--width); + transform: translateX(var(--left)); } .cell.row-hovered { background: var(--cell-background-hover); @@ -88,9 +89,10 @@ /* Sticky styles */ .cell.sticky { position: sticky; - left: 40px; z-index: 2; border-left-color: transparent; + transform: none; + left: 40px; } .cell.sticky.selected { z-index: 3; @@ -112,6 +114,7 @@ /* Label cells */ .cell.label { + width: 40px; padding: 0 12px; border-left-width: 0; position: sticky; diff --git a/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte b/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte index c64deaa63f..fcba9caf88 100644 --- a/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte +++ b/packages/client/src/components/app/spreadsheet/SpreadsheetRow.svelte @@ -20,14 +20,18 @@ selectedRows, changeCache, spreadsheetAPI, - visibleRows, + visibleCells, cellHeight, } = getContext("spreadsheet") $: rowSelected = !!$selectedRows[row._id] $: rowHovered = $hoveredRowId === row._id $: data = { ...row, ...$changeCache[row._id] } - $: visible = rowIdx >= $visibleRows[0] && rowIdx <= $visibleRows[1] + $: visibleColumns = [ + $columns[0], + ...$columns.slice($visibleCells.x[0], $visibleCells.x[1]), + ] + $: containsSelectedCell = $selectedCellId?.split("-")[0] === row._id const getCellForField = field => { const type = field.schema.type @@ -53,64 +57,66 @@ } -{#if visible} -
+
+ ($hoveredRowId = row._id)} + on:click={() => selectRow(row._id)} + > + {#if rowSelected || rowHovered} + + {:else} + + {rowIdx + 1} + + {/if} + + {#each visibleColumns as column (column.name)} + {@const cellIdx = `${row._id}-${column.name}`} ($hoveredRowId = row._id)} - on:click={() => selectRow(row._id)} - width="40" - left="0" + on:click={() => ($selectedCellId = cellIdx)} + width={column.width} + left={column.left} + column={column.idx} > - {#if rowSelected || rowHovered} - - {:else} - - {rowIdx + 1} - - {/if} - - {#each $columns as field, fieldIdx} - {@const cellIdx = `${row._id}-${field.name}`} - ($hoveredRowId = row._id)} - on:click={() => ($selectedCellId = cellIdx)} - width={field.width} - left={field.left} - column={fieldIdx} - > - spreadsheetAPI.updateValue(row._id, field, val)} - readonly={field.schema.autocolumn} - /> - - {/each} -
-{/if} + onChange={val => spreadsheetAPI.updateValue(row._id, column, val)} + readonly={column.schema.autocolumn} + /> + + {/each} +
diff --git a/packages/client/src/components/app/spreadsheet/stores/reorder.js b/packages/client/src/components/app/spreadsheet/stores/reorder.js index 972db3a6ce..db6d1831ec 100644 --- a/packages/client/src/components/app/spreadsheet/stores/reorder.js +++ b/packages/client/src/components/app/spreadsheet/stores/reorder.js @@ -27,17 +27,16 @@ export const createReorderStores = context => { let breakpoints = [] const cols = get(columns) cols.forEach((col, idx) => { - const header = document.getElementById(`sheet-${rand}-header-${idx}`) - const bounds = header.getBoundingClientRect() - breakpoints.push(bounds.x) + breakpoints.push(col.left) if (idx === cols.length - 1) { - breakpoints.push(bounds.x + bounds.width) + breakpoints.push(col.left + col.width) } }) + console.log(breakpoints, e.clientX) // Get bounds of the selected header and sheet body - const self = document.getElementById(`sheet-${rand}-header-${columnIdx}`) - const selfBounds = self.getBoundingClientRect() + + const self = cols[columnIdx] const body = document.getElementById(`sheet-${rand}-body`) const bodyBounds = body.getBoundingClientRect() @@ -49,9 +48,9 @@ export const createReorderStores = context => { initialMouseX: e.clientX, }) placeholder.set({ - initialX: selfBounds.x - bodyBounds.x, - x: selfBounds.x - bodyBounds.x, - width: selfBounds.width, + initialX: self.left - bodyBounds.x, + x: self.left - bodyBounds.x, + width: self.width, height: (get(rows).length + 2) * 32, }) @@ -103,13 +102,15 @@ export const createReorderStores = context => { const stopReordering = () => { // Swap position of columns let { columnIdx, swapColumnIdx } = get(reorder) - const newColumns = get(columns).slice() - const removed = newColumns.splice(columnIdx, 1) - if (--swapColumnIdx < columnIdx) { - swapColumnIdx++ - } - newColumns.splice(swapColumnIdx, 0, removed[0]) - columns.set(newColumns) + columns.update(state => { + const removed = state.splice(columnIdx, 1) + if (--swapColumnIdx < columnIdx) { + swapColumnIdx++ + } + state.splice(swapColumnIdx, 0, removed[0]) + state = state.map((col, idx) => ({ ...col, idx })) + return state + }) // Reset state reorder.set(reorderInitialState) diff --git a/packages/client/src/components/app/spreadsheet/stores/resize.js b/packages/client/src/components/app/spreadsheet/stores/resize.js index aab716df99..8327f0e79d 100644 --- a/packages/client/src/components/app/spreadsheet/stores/resize.js +++ b/packages/client/src/components/app/spreadsheet/stores/resize.js @@ -23,17 +23,16 @@ export const createResizeStore = context => { const resize = writable(initialState) const startResizing = (idx, e) => { + const $columns = get(columns) // Prevent propagation to stop reordering triggering e.stopPropagation() - sheet = document.getElementById(`sheet-${rand}`) - styles = getComputedStyle(sheet) - width = parseInt(styles.getPropertyValue(`--col-${idx}-width`)) - left = parseInt(styles.getPropertyValue(`--col-${idx}-left`)) + width = $columns[idx].width + left = $columns[idx].left initialWidth = width initialMouseX = e.clientX columnIdx = idx - columnCount = get(columns).length + columnCount = $columns.length // Add mouse event listeners to handle resizing document.addEventListener("mousemove", onResizeMouseMove) @@ -48,17 +47,54 @@ export const createResizeStore = context => { return } + columns.update(state => { + state[columnIdx].width = newWidth + let offset = state[columnIdx].left + newWidth + for (let i = columnIdx + 1; i < state.length; i++) { + state[i].left = offset + offset += state[i].width + } + 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 + // } - let newStyle = `--col-${columnIdx}-width:${newWidth}px;` - let offset = left + newWidth - for (let i = columnIdx + 1; i < columnCount; i++) { - const colWidth = 160//parseInt(styles.getPropertyValue(`--col-${i}-width`)) - newStyle += `--col-${i}-left:${offset}px;` - offset += colWidth - } - sheet.style.cssText += newStyle width = newWidth // Update width of column