Improve scroll logic and handle horizontal wheel events
This commit is contained in:
parent
1620b81e96
commit
f19ba2ea20
|
@ -1,13 +1,20 @@
|
||||||
<script>
|
<script>
|
||||||
import { getContext } from "svelte"
|
import { getContext } from "svelte"
|
||||||
import { domDebounce, debounce, throttle } from "../../utils/utils"
|
import { domDebounce } from "../../utils/utils"
|
||||||
|
|
||||||
const { scroll, bounds, rows, cellHeight, columns, stickyColumn } =
|
const {
|
||||||
getContext("spreadsheet")
|
scroll,
|
||||||
|
bounds,
|
||||||
|
cellHeight,
|
||||||
|
stickyColumn,
|
||||||
|
contentHeight,
|
||||||
|
maxScrollTop,
|
||||||
|
contentWidth,
|
||||||
|
maxScrollLeft,
|
||||||
|
} = getContext("spreadsheet")
|
||||||
|
|
||||||
// Bar config
|
// Bar config
|
||||||
const barOffset = 4
|
const barOffset = 4
|
||||||
const padding = 180
|
|
||||||
|
|
||||||
// State for dragging bars
|
// State for dragging bars
|
||||||
let initialMouse
|
let initialMouse
|
||||||
|
@ -21,61 +28,23 @@
|
||||||
|
|
||||||
// Calculate V scrollbar size and offset
|
// Calculate V scrollbar size and offset
|
||||||
// Terminology is the same for both axes:
|
// 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
|
// 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
|
// 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
|
$: renderHeight = height - 2 * barOffset
|
||||||
$: barHeight = Math.max(50, (height / contentHeight) * renderHeight)
|
$: barHeight = Math.max(50, (height / $contentHeight) * renderHeight)
|
||||||
$: availHeight = renderHeight - barHeight
|
$: 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
|
// 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
|
$: renderWidth = totalWidth - 2 * barOffset
|
||||||
$: barWidth = Math.max(50, (totalWidth / contentWidth) * renderWidth)
|
$: barWidth = Math.max(50, (totalWidth / $contentWidth) * renderWidth)
|
||||||
$: availWidth = renderWidth - barWidth
|
$: 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
|
// Calculate whether to show scrollbars or not
|
||||||
$: showVScrollbar = contentHeight > height
|
$: showVScrollbar = $contentHeight > height
|
||||||
$: showHScrollbar = contentWidth > totalWidth
|
$: 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// V scrollbar drag handlers
|
// V scrollbar drag handlers
|
||||||
const startVDragging = e => {
|
const startVDragging = e => {
|
||||||
|
@ -88,10 +57,10 @@
|
||||||
const moveVDragging = domDebounce(e => {
|
const moveVDragging = domDebounce(e => {
|
||||||
const delta = e.clientY - initialMouse
|
const delta = e.clientY - initialMouse
|
||||||
const weight = delta / availHeight
|
const weight = delta / availHeight
|
||||||
const newScrollTop = initialScroll + weight * maxScrollTop
|
const newScrollTop = initialScroll + weight * $maxScrollTop
|
||||||
scroll.update(state => ({
|
scroll.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
top: Math.max(0, Math.min(newScrollTop, maxScrollTop)),
|
top: Math.max(0, Math.min(newScrollTop, $maxScrollTop)),
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
const stopVDragging = () => {
|
const stopVDragging = () => {
|
||||||
|
@ -110,10 +79,10 @@
|
||||||
const moveHDragging = domDebounce(e => {
|
const moveHDragging = domDebounce(e => {
|
||||||
const delta = e.clientX - initialMouse
|
const delta = e.clientX - initialMouse
|
||||||
const weight = delta / availWidth
|
const weight = delta / availWidth
|
||||||
const newScrollLeft = initialScroll + weight * maxScrollLeft
|
const newScrollLeft = initialScroll + weight * $maxScrollLeft
|
||||||
scroll.update(state => ({
|
scroll.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
left: Math.max(0, Math.min(newScrollLeft, maxScrollLeft)),
|
left: Math.max(0, Math.min(newScrollLeft, $maxScrollLeft)),
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
const stopHDragging = () => {
|
const stopHDragging = () => {
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
<svelte:options immutable={true} />
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { setContext } from "svelte"
|
import { setContext } from "svelte"
|
||||||
import { writable } from "svelte/store"
|
import { writable } from "svelte/store"
|
||||||
|
@ -7,6 +5,7 @@
|
||||||
import { createViewportStores } from "./stores/viewport"
|
import { createViewportStores } from "./stores/viewport"
|
||||||
import { createRowsStore } from "./stores/rows"
|
import { createRowsStore } from "./stores/rows"
|
||||||
import { createColumnsStores } from "./stores/columns"
|
import { createColumnsStores } from "./stores/columns"
|
||||||
|
import { createScrollStores } from "./stores/scroll"
|
||||||
import SheetControls from "./SheetControls.svelte"
|
import SheetControls from "./SheetControls.svelte"
|
||||||
import SheetBody from "./SheetBody.svelte"
|
import SheetBody from "./SheetBody.svelte"
|
||||||
import SheetRow from "./SheetRow.svelte"
|
import SheetRow from "./SheetRow.svelte"
|
||||||
|
@ -59,6 +58,7 @@
|
||||||
context = { ...context, rows, schema }
|
context = { ...context, rows, schema }
|
||||||
const { columns, stickyColumn } = createColumnsStores(context)
|
const { columns, stickyColumn } = createColumnsStores(context)
|
||||||
context = { ...context, columns, stickyColumn }
|
context = { ...context, columns, stickyColumn }
|
||||||
|
context = { ...context, ...createScrollStores(context) }
|
||||||
const { visibleRows, visibleColumns } = createViewportStores(context)
|
const { visibleRows, visibleColumns } = createViewportStores(context)
|
||||||
context = { ...context, visibleRows, visibleColumns }
|
context = { ...context, visibleRows, visibleColumns }
|
||||||
const { reorder } = createReorderStores(context)
|
const { reorder } = createReorderStores(context)
|
||||||
|
|
|
@ -5,84 +5,25 @@
|
||||||
cellHeight,
|
cellHeight,
|
||||||
scroll,
|
scroll,
|
||||||
bounds,
|
bounds,
|
||||||
rows,
|
|
||||||
columns,
|
columns,
|
||||||
visibleRows,
|
visibleRows,
|
||||||
visibleColumns,
|
visibleColumns,
|
||||||
hoveredRowId,
|
hoveredRowId,
|
||||||
|
maxScrollTop,
|
||||||
|
maxScrollLeft,
|
||||||
} = getContext("spreadsheet")
|
} = getContext("spreadsheet")
|
||||||
|
|
||||||
export let scrollVertically = true
|
export let scrollVertically = true
|
||||||
export let scrollHorizontally = true
|
export let scrollHorizontally = true
|
||||||
export let wheelInteractive = true
|
export let wheelInteractive = true
|
||||||
|
|
||||||
$: scrollTop = $scroll.top
|
|
||||||
$: scrollLeft = $scroll.left
|
|
||||||
$: offsetY = -1 * (scrollTop % cellHeight)
|
|
||||||
$: hiddenWidths = calculateHiddenWidths($visibleColumns)
|
$: hiddenWidths = calculateHiddenWidths($visibleColumns)
|
||||||
$: offsetX = -1 * scrollLeft + hiddenWidths
|
$: scrollLeft = $scroll.left
|
||||||
$: rowCount = $visibleRows.length
|
$: scrollTop = $scroll.top
|
||||||
$: contentWidth = calculateContentWidth($visibleColumns, scrollHorizontally)
|
$: offsetX = scrollHorizontally ? -1 * scrollLeft + hiddenWidths : 0
|
||||||
$: contentHeight = calculateContentHeight(rowCount, scrollVertically)
|
$: offsetY = scrollVertically ? -1 * (scrollTop % cellHeight) : 0
|
||||||
$: 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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Calculates with total width of all columns currently not rendered
|
||||||
const calculateHiddenWidths = visibleColumns => {
|
const calculateHiddenWidths = visibleColumns => {
|
||||||
const idx = visibleColumns[0]?.idx
|
const idx = visibleColumns[0]?.idx
|
||||||
let width = 0
|
let width = 0
|
||||||
|
@ -94,37 +35,23 @@
|
||||||
return width
|
return width
|
||||||
}
|
}
|
||||||
|
|
||||||
const calculateContentWidth = (columns, scroll) => {
|
// Handles a wheel even and updates the scroll offsets
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleWheel = e => {
|
const handleWheel = e => {
|
||||||
const step = cellHeight * 3
|
e.preventDefault()
|
||||||
const deltaY = e.deltaY < 0 ? -1 : 1
|
|
||||||
const offset = deltaY * step
|
// Calculate new scroll top
|
||||||
let newScrollTop = scrollTop
|
let newScrollTop = scrollTop + e.deltaY
|
||||||
newScrollTop += offset
|
newScrollTop = Math.max(0, Math.min(newScrollTop, $maxScrollTop))
|
||||||
newScrollTop = Math.min(
|
|
||||||
newScrollTop,
|
// Calculate new scroll left
|
||||||
($rows.length + 1) * cellHeight - $bounds.height + 180
|
let newScrollLeft = scrollLeft + e.deltaX
|
||||||
)
|
newScrollLeft = Math.max(0, Math.min(newScrollLeft, $maxScrollLeft))
|
||||||
newScrollTop = Math.max(0, newScrollTop)
|
|
||||||
scroll.update(state => ({
|
// Update state
|
||||||
...state,
|
scroll.set({
|
||||||
|
left: newScrollLeft,
|
||||||
top: newScrollTop,
|
top: newScrollTop,
|
||||||
}))
|
})
|
||||||
|
|
||||||
// Hover row under cursor
|
// Hover row under cursor
|
||||||
const y = e.clientY - $bounds.top + (newScrollTop % cellHeight)
|
const y = e.clientY - $bounds.top + (newScrollTop % cellHeight)
|
||||||
|
@ -133,15 +60,13 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="outer" on:wheel|passive={wheelInteractive ? handleWheel : null}>
|
<div class="outer" on:wheel={wheelInteractive ? handleWheel : null}>
|
||||||
<div class="inner" style={innerStyle}>
|
<div class="inner" style="--offset-x:{offsetX}px;--offset-y:{offsetY}px;">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
div {
|
|
||||||
}
|
|
||||||
.outer {
|
.outer {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
min-height: 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