Smoother scrolling in gallery when using keyboard navigation.
This commit is contained in:
@@ -232,18 +232,23 @@ defineExpose({
|
|||||||
} else {
|
} else {
|
||||||
store.selected.add(key)
|
store.selected.add(key)
|
||||||
}
|
}
|
||||||
|
markKeyboardFollow()
|
||||||
this.cursorMove(1, null)
|
this.cursorMove(1, null)
|
||||||
},
|
},
|
||||||
up(ev: KeyboardEvent) {
|
up(ev: KeyboardEvent) {
|
||||||
|
markKeyboardFollow()
|
||||||
this.cursorMove(-columns.value, ev)
|
this.cursorMove(-columns.value, ev)
|
||||||
},
|
},
|
||||||
down(ev: KeyboardEvent) {
|
down(ev: KeyboardEvent) {
|
||||||
|
markKeyboardFollow()
|
||||||
this.cursorMove(columns.value, ev)
|
this.cursorMove(columns.value, ev)
|
||||||
},
|
},
|
||||||
left(ev: KeyboardEvent) {
|
left(ev: KeyboardEvent) {
|
||||||
|
markKeyboardFollow()
|
||||||
this.cursorMove(-1, ev)
|
this.cursorMove(-1, ev)
|
||||||
},
|
},
|
||||||
right(ev: KeyboardEvent) {
|
right(ev: KeyboardEvent) {
|
||||||
|
markKeyboardFollow()
|
||||||
this.cursorMove(1, ev)
|
this.cursorMove(1, ev)
|
||||||
},
|
},
|
||||||
cursorMove(d: number, ev: KeyboardEvent | null) {
|
cursorMove(d: number, ev: KeyboardEvent | null) {
|
||||||
@@ -270,7 +275,9 @@ defineExpose({
|
|||||||
if (Math.abs(d) >= N || Math.sign(d) !== Math.sign(moveto - index)) moveto = N
|
if (Math.abs(d) >= N || Math.sign(d) !== Math.sign(moveto - index)) moveto = N
|
||||||
}
|
}
|
||||||
store.cursor = docs[moveto]?.key ?? ''
|
store.cursor = docs[moveto]?.key ?? ''
|
||||||
const tr = store.cursor ? document.getElementById(`file-${store.cursor}`) : ''
|
const tr = store.cursor
|
||||||
|
? (document.getElementById(`file-${store.cursor}`) as HTMLElement | null)
|
||||||
|
: null
|
||||||
if (select) {
|
if (select) {
|
||||||
// Go forwards, possibly wrapping over the end; the last entry is not toggled
|
// Go forwards, possibly wrapping over the end; the last entry is not toggled
|
||||||
let [begin, end] = d > 0 ? [index, moveto] : [moveto, index]
|
let [begin, end] = d > 0 ? [index, moveto] : [moveto, index]
|
||||||
@@ -281,14 +288,7 @@ defineExpose({
|
|||||||
else store.selected.add(key)
|
else store.selected.add(key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
keepCursorVisibleSmooth(tr)
|
||||||
scrolltr = tr
|
|
||||||
if (!scrolltimer) {
|
|
||||||
scrolltimer = setTimeout(() => {
|
|
||||||
if (scrolltr) scrolltr.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
|
||||||
scrolltimer = null
|
|
||||||
}, 300)
|
|
||||||
}
|
|
||||||
// When leaving the file list: up goes to breadcrumbs, down goes to header
|
// When leaving the file list: up goes to breadcrumbs, down goes to header
|
||||||
if (moveto === N) {
|
if (moveto === N) {
|
||||||
if (d < 0) focusBreadcrumb()
|
if (d < 0) focusBreadcrumb()
|
||||||
@@ -306,8 +306,109 @@ const focusBreadcrumb = () => {
|
|||||||
const el = document.querySelector('.breadcrumb') as HTMLElement | null
|
const el = document.querySelector('.breadcrumb') as HTMLElement | null
|
||||||
if (el) el.focus()
|
if (el) el.focus()
|
||||||
}
|
}
|
||||||
let scrolltimer: any = null
|
let scrollAnimationFrame: number | null = null
|
||||||
let scrolltr: any = null
|
let scrollTargetY: number | null = null
|
||||||
|
let scrollVelocity = 0
|
||||||
|
let keyboardFollowUntil = 0
|
||||||
|
|
||||||
|
const markKeyboardFollow = () => {
|
||||||
|
keyboardFollowUntil = performance.now() + 260
|
||||||
|
}
|
||||||
|
const keyboardFollowActive = () => performance.now() < keyboardFollowUntil
|
||||||
|
|
||||||
|
const getScrollContainer = () =>
|
||||||
|
(document.querySelector('main') as HTMLElement | null) ?? document.documentElement
|
||||||
|
|
||||||
|
const clampScrollY = (y: number, scroller: HTMLElement) => {
|
||||||
|
const maxY = Math.max(0, scroller.scrollHeight - scroller.clientHeight)
|
||||||
|
return Math.min(maxY, Math.max(0, y))
|
||||||
|
}
|
||||||
|
|
||||||
|
const cursorScrollTarget = (el: HTMLElement): number | null => {
|
||||||
|
const scroller = getScrollContainer()
|
||||||
|
const rect = el.getBoundingClientRect()
|
||||||
|
const scrollerRect = scroller.getBoundingClientRect()
|
||||||
|
const topPad = 84
|
||||||
|
const bottomPad = 84
|
||||||
|
const visibleTop = scrollerRect.top + topPad
|
||||||
|
const visibleBottom = scrollerRect.bottom - bottomPad
|
||||||
|
|
||||||
|
if (rect.top >= visibleTop && rect.bottom <= visibleBottom) return null
|
||||||
|
|
||||||
|
// Keep the cursor within a comfortable viewport band instead of forcing center.
|
||||||
|
if (rect.top < visibleTop) {
|
||||||
|
return clampScrollY(scroller.scrollTop + (rect.top - visibleTop), scroller)
|
||||||
|
}
|
||||||
|
|
||||||
|
return clampScrollY(scroller.scrollTop + (rect.bottom - visibleBottom), scroller)
|
||||||
|
}
|
||||||
|
|
||||||
|
const runSmoothCursorScroll = () => {
|
||||||
|
if (scrollAnimationFrame != null) return
|
||||||
|
|
||||||
|
const step = () => {
|
||||||
|
if (scrollTargetY == null) {
|
||||||
|
scrollVelocity *= 0.78
|
||||||
|
if (Math.abs(scrollVelocity) > 0.05) {
|
||||||
|
const scroller = getScrollContainer()
|
||||||
|
const next = clampScrollY(scroller.scrollTop + scrollVelocity, scroller)
|
||||||
|
scroller.scrollTop = next
|
||||||
|
scrollAnimationFrame = requestAnimationFrame(step)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
scrollVelocity = 0
|
||||||
|
scrollAnimationFrame = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const scroller = getScrollContainer()
|
||||||
|
const current = scroller.scrollTop
|
||||||
|
const delta = scrollTargetY - current
|
||||||
|
const absDelta = Math.abs(delta)
|
||||||
|
if (absDelta < 0.6 && Math.abs(scrollVelocity) < 0.08) {
|
||||||
|
scroller.scrollTop = scrollTargetY
|
||||||
|
scrollVelocity = 0
|
||||||
|
scrollTargetY = null
|
||||||
|
scrollAnimationFrame = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spring + damping momentum model for smoother retargeting during rapid selection changes.
|
||||||
|
const stiffness = Math.min(0.03, 0.012 + absDelta / 8000)
|
||||||
|
const damping = 0.84
|
||||||
|
scrollVelocity += delta * stiffness
|
||||||
|
scrollVelocity *= damping
|
||||||
|
|
||||||
|
const next = clampScrollY(current + scrollVelocity, scroller)
|
||||||
|
if (next === current) scrollVelocity = 0
|
||||||
|
scroller.scrollTop = next
|
||||||
|
scrollAnimationFrame = requestAnimationFrame(step)
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollAnimationFrame = requestAnimationFrame(step)
|
||||||
|
}
|
||||||
|
|
||||||
|
const keepCursorVisibleSmooth = (el: HTMLElement | null) => {
|
||||||
|
if (!keyboardFollowActive()) {
|
||||||
|
scrollTargetY = null
|
||||||
|
scrollVelocity = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!el) {
|
||||||
|
scrollTargetY = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = cursorScrollTarget(el)
|
||||||
|
if (target == null) {
|
||||||
|
scrollTargetY = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollTargetY = target
|
||||||
|
runSmoothCursorScroll()
|
||||||
|
}
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
if (store.cursor && store.cursor !== editing.value?.key) editing.value = null
|
if (store.cursor && store.cursor !== editing.value?.key) editing.value = null
|
||||||
if (editing.value) store.cursor = editing.value.key
|
if (editing.value) store.cursor = editing.value.key
|
||||||
@@ -316,8 +417,7 @@ watchEffect(() => {
|
|||||||
`#file-${store.cursor}`
|
`#file-${store.cursor}`
|
||||||
) as HTMLAnchorElement | null
|
) as HTMLAnchorElement | null
|
||||||
if (a) {
|
if (a) {
|
||||||
a.focus()
|
a.focus({ preventScroll: true })
|
||||||
a.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -331,8 +431,7 @@ let resizeObserver: ResizeObserver | null = null
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const active = document.querySelector('.cursor') as HTMLElement | null
|
const active = document.querySelector('.cursor') as HTMLElement | null
|
||||||
if (active) {
|
if (active) {
|
||||||
active.scrollIntoView({ block: 'center', behavior: 'instant' })
|
active.focus({ preventScroll: true })
|
||||||
active.focus()
|
|
||||||
}
|
}
|
||||||
updateColumns()
|
updateColumns()
|
||||||
seedFromDocs()
|
seedFromDocs()
|
||||||
@@ -343,6 +442,9 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
if (scrollAnimationFrame != null) cancelAnimationFrame(scrollAnimationFrame)
|
||||||
|
scrollAnimationFrame = null
|
||||||
|
scrollTargetY = null
|
||||||
resizeObserver?.disconnect()
|
resizeObserver?.disconnect()
|
||||||
gallery.value?.removeEventListener('load', onImgLoad, { capture: true })
|
gallery.value?.removeEventListener('load', onImgLoad, { capture: true })
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user