1
0
Fork 0
mirror of synced 2024-09-10 22:46:09 +12:00

Improve scroll logic and handle horizontal wheel events

This commit is contained in:
Andrew Kingston 2023-03-02 11:08:48 +00:00
parent 1620b81e96
commit f19ba2ea20
4 changed files with 126 additions and 154 deletions

View file

@ -1,13 +1,20 @@
<script>
import { getContext } from "svelte"
import { domDebounce, debounce, throttle } from "../../utils/utils"
import { domDebounce } from "../../utils/utils"
const { scroll, bounds, rows, cellHeight, columns, stickyColumn } =
getContext("spreadsheet")
const {
scroll,
bounds,
cellHeight,
stickyColumn,
contentHeight,
maxScrollTop,
contentWidth,
maxScrollLeft,
} = getContext("spreadsheet")
// Bar config
const barOffset = 4
const padding = 180
// State for dragging bars
let initialMouse
@ -21,61 +28,23 @@
// Calculate V scrollbar size and offset
// Terminology is the same for both axes:
// contentX - the size of the rendered content, including padding
// renderX - the space available to render the bar in, edge to edge
// barX - the length of the bar
// availX - the space available to render the bar in, until the edge
// barX - the offset of the bar
$: contentHeight = ($rows.length + 1) * cellHeight + padding
$: renderHeight = height - 2 * barOffset
$: barHeight = Math.max(50, (height / contentHeight) * renderHeight)
$: barHeight = Math.max(50, (height / $contentHeight) * renderHeight)
$: availHeight = renderHeight - barHeight
$: maxScrollTop = Math.max(contentHeight - height, 0)
$: barTop = barOffset + cellHeight + availHeight * (scrollTop / maxScrollTop)
$: barTop = barOffset + cellHeight + availHeight * (scrollTop / $maxScrollTop)
// Calculate H scrollbar size and offset
$: contentWidth = calculateContentWidth($columns, $stickyColumn) + padding
$: totalWidth = width + 40 + $stickyColumn?.width || 0
$: totalWidth = width + 40 + ($stickyColumn?.width || 0)
$: renderWidth = totalWidth - 2 * barOffset
$: barWidth = Math.max(50, (totalWidth / contentWidth) * renderWidth)
$: barWidth = Math.max(50, (totalWidth / $contentWidth) * renderWidth)
$: availWidth = renderWidth - barWidth
$: maxScrollLeft = Math.max(contentWidth - totalWidth, 0)
$: barLeft = barOffset + availWidth * (scrollLeft / maxScrollLeft)
$: barLeft = barOffset + availWidth * (scrollLeft / $maxScrollLeft)
// Calculate whether to show scrollbars or not
$: showVScrollbar = contentHeight > height
$: showHScrollbar = contentWidth > totalWidth
// Ensure scroll state never goes invalid, which can happen when changing
// rows or tables
$: {
if (scrollTop > maxScrollTop) {
setTimeout(() => {
scroll.update(state => ({
...state,
top: maxScrollTop,
}))
})
}
}
$: {
if (scrollLeft > maxScrollLeft) {
setTimeout(() => {
scroll.update(state => ({
...state,
left: maxScrollLeft,
}))
})
}
}
const calculateContentWidth = (columns, stickyColumn) => {
let width = 40 + stickyColumn?.width
columns.forEach(col => {
width += col.width
})
return width
}
$: showVScrollbar = $contentHeight > height
$: showHScrollbar = $contentWidth > totalWidth
// V scrollbar drag handlers
const startVDragging = e => {
@ -88,10 +57,10 @@
const moveVDragging = domDebounce(e => {
const delta = e.clientY - initialMouse
const weight = delta / availHeight
const newScrollTop = initialScroll + weight * maxScrollTop
const newScrollTop = initialScroll + weight * $maxScrollTop
scroll.update(state => ({
...state,
top: Math.max(0, Math.min(newScrollTop, maxScrollTop)),
top: Math.max(0, Math.min(newScrollTop, $maxScrollTop)),
}))
})
const stopVDragging = () => {
@ -110,10 +79,10 @@
const moveHDragging = domDebounce(e => {
const delta = e.clientX - initialMouse
const weight = delta / availWidth
const newScrollLeft = initialScroll + weight * maxScrollLeft
const newScrollLeft = initialScroll + weight * $maxScrollLeft
scroll.update(state => ({
...state,
left: Math.max(0, Math.min(newScrollLeft, maxScrollLeft)),
left: Math.max(0, Math.min(newScrollLeft, $maxScrollLeft)),
}))
})
const stopHDragging = () => {

View file

@ -1,5 +1,3 @@
<svelte:options immutable={true} />
<script>
import { setContext } from "svelte"
import { writable } from "svelte/store"
@ -7,6 +5,7 @@
import { createViewportStores } from "./stores/viewport"
import { createRowsStore } from "./stores/rows"
import { createColumnsStores } from "./stores/columns"
import { createScrollStores } from "./stores/scroll"
import SheetControls from "./SheetControls.svelte"
import SheetBody from "./SheetBody.svelte"
import SheetRow from "./SheetRow.svelte"
@ -59,6 +58,7 @@
context = { ...context, rows, schema }
const { columns, stickyColumn } = createColumnsStores(context)
context = { ...context, columns, stickyColumn }
context = { ...context, ...createScrollStores(context) }
const { visibleRows, visibleColumns } = createViewportStores(context)
context = { ...context, visibleRows, visibleColumns }
const { reorder } = createReorderStores(context)

View file

@ -5,84 +5,25 @@
cellHeight,
scroll,
bounds,
rows,
columns,
visibleRows,
visibleColumns,
hoveredRowId,
maxScrollTop,
maxScrollLeft,
} = getContext("spreadsheet")
export let scrollVertically = true
export let scrollHorizontally = true
export let wheelInteractive = true
$: scrollTop = $scroll.top
$: scrollLeft = $scroll.left
$: offsetY = -1 * (scrollTop % cellHeight)
$: hiddenWidths = calculateHiddenWidths($visibleColumns)
$: offsetX = -1 * scrollLeft + hiddenWidths
$: rowCount = $visibleRows.length
$: contentWidth = calculateContentWidth($visibleColumns, scrollHorizontally)
$: contentHeight = calculateContentHeight(rowCount, scrollVertically)
$: innerStyle = getInnerStyle(
offsetX,
offsetY,
contentWidth,
contentHeight,
scrollHorizontally,
scrollVertically
)
$: outerStyle = getOuterStyle(
offsetX,
offsetY,
contentWidth,
contentHeight,
scrollHorizontally,
scrollVertically
)
const getInnerStyle = (
offsetX,
offsetY,
contentWidth,
contentHeight,
scrollH,
scrollV
) => {
if (!scrollH) {
offsetX = 0
}
if (!scrollV) {
offsetY = 0
}
let style = `--offset-x:${offsetX}px;--offset-y:${offsetY}px;`
// if (scrollH && contentWidth) {
// style += `width:${contentWidth}px;`
// }
// if (scrollV && contentHeight) {
// style += `height:${contentHeight}px;`
// }
return style
}
const getOuterStyle = (
offsetX,
offsetY,
contentWidth,
contentHeight,
scrollH,
scrollV
) => {
let style = ""
// if (scrollV) {
// style += `height:${contentHeight + offsetY}px;`
// }
// if (scrollH) {
// style += `width:${contentWidth + offsetX}px;`
// }
return style
}
$: scrollLeft = $scroll.left
$: scrollTop = $scroll.top
$: offsetX = scrollHorizontally ? -1 * scrollLeft + hiddenWidths : 0
$: offsetY = scrollVertically ? -1 * (scrollTop % cellHeight) : 0
// Calculates with total width of all columns currently not rendered
const calculateHiddenWidths = visibleColumns => {
const idx = visibleColumns[0]?.idx
let width = 0
@ -94,37 +35,23 @@
return width
}
const calculateContentWidth = (columns, scroll) => {
if (!scroll) {
return null
}
let width = 0
columns.forEach(col => (width += col.width))
return width
}
const calculateContentHeight = (rowCount, scroll) => {
if (!scroll) {
return null
}
return (rowCount + 1) * cellHeight
}
// Handles a wheel even and updates the scroll offsets
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.min(
newScrollTop,
($rows.length + 1) * cellHeight - $bounds.height + 180
)
newScrollTop = Math.max(0, newScrollTop)
scroll.update(state => ({
...state,
e.preventDefault()
// Calculate new scroll top
let newScrollTop = scrollTop + e.deltaY
newScrollTop = Math.max(0, Math.min(newScrollTop, $maxScrollTop))
// Calculate new scroll left
let newScrollLeft = scrollLeft + e.deltaX
newScrollLeft = Math.max(0, Math.min(newScrollLeft, $maxScrollLeft))
// Update state
scroll.set({
left: newScrollLeft,
top: newScrollTop,
}))
})
// Hover row under cursor
const y = e.clientY - $bounds.top + (newScrollTop % cellHeight)
@ -133,15 +60,13 @@
}
</script>
<div class="outer" on:wheel|passive={wheelInteractive ? handleWheel : null}>
<div class="inner" style={innerStyle}>
<div class="outer" on:wheel={wheelInteractive ? handleWheel : null}>
<div class="inner" style="--offset-x:{offsetX}px;--offset-y:{offsetY}px;">
<slot />
</div>
</div>
<style>
div {
}
.outer {
min-width: 100%;
min-height: 100%;

View file

@ -0,0 +1,78 @@
import { derived } from "svelte/store"
export const createScrollStores = context => {
const { scroll, rows, columns, stickyColumn, bounds, cellHeight } = context
const padding = 180
// Memoize store primitives
const scrollTop = derived(scroll, $scroll => $scroll.top)
const scrollLeft = derived(scroll, $scroll => $scroll.left)
// Derive vertical limits
const height = derived(bounds, $bounds => $bounds.height, 0)
const width = derived(bounds, $bounds => $bounds.width, 0)
const contentHeight = derived(
rows,
$rows => ($rows.length + 1) * cellHeight + padding,
0
)
const maxScrollTop = derived(
[height, contentHeight],
([$height, $contentHeight]) => Math.max($contentHeight - $height, 0),
0
)
// Derive horizontal limits
const contentWidth = derived(
[columns, stickyColumn],
([$columns, $stickyColumn]) => {
let width = 40 + padding + ($stickyColumn?.width || 0)
$columns.forEach(col => {
width += col.width
})
return width
},
0
)
const screenWidth = derived(
[width, stickyColumn],
([$width, $stickyColumn]) => $width + 40 + ($stickyColumn?.width || 0),
0
)
const maxScrollLeft = derived(
[contentWidth, screenWidth],
([$contentWidth, $screenWidth]) => {
return Math.max($contentWidth - $screenWidth, 0)
},
0
)
// Ensure scroll state never goes invalid, which can happen when changing
// rows or tables
derived([scrollTop, maxScrollTop], ([$scrollTop, $maxScrollTop]) => {
console.log($scrollTop, $maxScrollTop, "check")
if ($scrollTop > $maxScrollTop) {
scroll.update(state => ({
...state,
top: $maxScrollTop,
}))
}
})
// $: {
// if (scrollLeft > maxScrollLeft) {
// setTimeout(() => {
// scroll.update(state => ({
// ...state,
// left: maxScrollLeft,
// }))
// })
// }
// }
return {
contentHeight,
contentWidth,
maxScrollTop,
maxScrollLeft,
}
}