opencode / packages /app /src /pages /session /timeline /observe-element-offset.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
3e05655 verified
Raw History Blame Contribute Delete
2.66 kB
import { observeElementOffset, type Virtualizer } from "@tanstack/solid-virtual"
export function observeElementOffsetReconnectAware<TScrollElement extends Element, TItemElement extends Element>(
instance: Virtualizer<TScrollElement, TItemElement>,
callback: (offset: number, isScrolling: boolean) => void,
) {
let active = true
const deliver = (offset: number, isScrolling: boolean) => {
if (!active) return
callback(offset, isScrolling)
}
const cleanupOffset = observeElementOffset(instance, deliver)
const element = instance.scrollElement
const targetWindow = instance.targetWindow
const root = element?.closest("main") ?? element?.ownerDocument.body
if (!element || !targetWindow || !root)
return () => {
active = false
cleanupOffset?.()
}
let removed = false
let frame: number | undefined
const clearCheck = () => {
if (frame === undefined) return
targetWindow.cancelAnimationFrame(frame)
frame = undefined
}
const startCheck = () => {
clearCheck()
const deadline = targetWindow.performance.now() + instance.options.isScrollingResetDelay
let framesAfterDeadline = 0
const check = (time: number) => {
frame = undefined
if (element.isConnected) {
const offset = instance.options.horizontal
? element.scrollLeft * (instance.options.isRtl ? -1 : 1)
: element.scrollTop
if (instance.scrollOffset === null || Math.abs(offset - instance.scrollOffset) > 1) deliver(offset, false)
}
if (time >= deadline) framesAfterDeadline += 1
if (framesAfterDeadline >= 2) return
frame = targetWindow.requestAnimationFrame(check)
}
frame = targetWindow.requestAnimationFrame(check)
}
const observer = new targetWindow.MutationObserver((records) => {
if (!active) return
records.forEach((record) => {
if (record.target === element || element.contains(record.target)) return
if (mutationNodesContainElement(record.removedNodes, element)) {
removed = true
clearCheck()
}
if (!removed || !element.isConnected || !mutationNodesContainElement(record.addedNodes, element)) return
removed = false
startCheck()
})
})
// Session routes are replaced below persistent main; body is the fallback for isolated hosts.
observer.observe(root, { childList: true, subtree: true })
return () => {
active = false
observer.disconnect()
clearCheck()
cleanupOffset?.()
}
}
export function mutationNodesContainElement(nodes: Iterable<Node>, element: Element) {
return [...nodes].some((node) => node === element || node.contains(element))
}