Improve scroll logic and handle horizontal wheel events
This commit is contained in:
parent
1620b81e96
commit
f19ba2ea20
|
@ -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 = () => {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue