Files
superwork/vscode/webview-ui/src/codeReader/useStickyScroll.ts
T

111 lines
5.2 KiB
TypeScript

import type { RefObject } from 'react'
import type { PinnedLines, StickyScope } from './stickyScroll'
import type { StickyScrollSettings } from './types'
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { insetAbove, lineLimit, NO_PINNED, pinnedLines, samePinned } from './stickyScroll'
import { pinnedHeight } from './useScrollPosition'
import { onMessage } from './vscode'
/** 钉在顶上的一行多高(像素) */
export const STICKY_LINE_HEIGHT = 24
/** 卡片外面套着几张卡片:它所在的各层子层(BlockLevel 画的、带 data-owner 的)一层算一张 */
function depthOf(card: Element): number {
let depth = 0
for (let level = card.closest('.cr-level[data-owner]'); level; level = level.parentElement?.closest('.cr-level[data-owner]') ?? null)
depth++
return depth
}
/** 筛选栏下面的可视区域里最多钉几行 */
function limitOf(maxLineCount: number): number {
return lineLimit(window.innerHeight - pinnedHeight(), STICKY_LINE_HEIGHT, maxLineCount)
}
/**
* root 里能钉住的卡片,按页面先后,竖直坐标以筛选栏下沿为 0:画出来了、不在编辑的每一张(BlockCard 带 data-line;删掉的块那张没有),
* 折叠着的、没有子块的也算,卡片自己就是一层。下沿算到它画出来的子块的最后(它名下的子层带 data-owner,挂在子行上的子块那层在卡片里面)。
* 只留可能钉住的:上沿在钉住的那几行(高 band)以内或更高、下沿还在筛选栏下沿以下
*/
function measureScopes(root: HTMLElement, band: number): StickyScope[] {
const bottoms = new Map<string, number>()
for (const level of root.querySelectorAll<HTMLElement>('.cr-level[data-owner]')) {
const owner = level.dataset.owner ?? ''
bottoms.set(owner, Math.max(bottoms.get(owner) ?? Number.NEGATIVE_INFINITY, level.getBoundingClientRect().bottom))
}
const base = pinnedHeight()
const scopes: StickyScope[] = []
for (const card of root.querySelectorAll<HTMLElement>('.cr-block[data-line]:not([data-editing])')) {
const id = card.dataset.id ?? ''
const rect = card.getBoundingClientRect()
const top = rect.top - base
const bottom = Math.max(rect.bottom, bottoms.get(id) ?? rect.bottom) - base
if (top < band && bottom >= 0)
scopes.push({ id, depth: depthOf(card), top, bottom, left: rect.left, right: rect.right })
}
return scopes
}
/** VS Code 的 Sticky Scroll 设置:扩展端 ready 后先于第一份文档发来,设置改了再发;收到之前不钉。 */
export function useStickySettings(): StickyScrollSettings | undefined {
const [settings, setSettings] = useState<StickyScrollSettings | undefined>()
useEffect(() => onMessage((msg) => {
if (msg.type === 'stickyScroll')
setSettings({ enabled: msg.enabled, maxLineCount: msg.maxLineCount })
}), [])
return settings
}
/**
* 眼下 content 里钉着哪几张卡片(见 pinnedLines),最多 maxLineCount 行(窗口矮时更少,见 lineLimit)。滚动、窗口大小变了、内容的大小变了、用它的组件重画了(页面状态变了)时排一次,
* 每帧最多量一次、算一次(requestAnimationFrame);钉的没变不更新。只有用它的组件跟着重画,块树不动。
*/
export function useStickyLines(content: RefObject<HTMLElement | null>, maxLineCount: number): PinnedLines {
const [lines, setLines] = useState(NO_PINNED)
const schedule = useRef<() => void>(() => {})
useEffect(() => {
let frame = 0
const update = (): void => {
frame = 0
const root = content.current
const limit = limitOf(maxLineCount)
const next = root ? pinnedLines(measureScopes(root, limit * STICKY_LINE_HEIGHT), STICKY_LINE_HEIGHT, limit) : NO_PINNED
setLines(prev => samePinned(prev, next) ? prev : next)
}
schedule.current = () => {
if (frame === 0)
frame = requestAnimationFrame(update)
}
const onChange = (): void => schedule.current()
const observer = new ResizeObserver(onChange)
if (content.current)
observer.observe(content.current)
window.addEventListener('scroll', onChange, { passive: true })
window.addEventListener('resize', onChange)
onChange()
return () => {
cancelAnimationFrame(frame)
observer.disconnect()
window.removeEventListener('scroll', onChange)
window.removeEventListener('resize', onChange)
schedule.current = () => {}
}
}, [content, maxLineCount])
// 展开、折叠、筛选、换文档都会让用它的组件跟着页面重画:画完再量(大小没变时 ResizeObserver 不报)
useLayoutEffect(() => schedule.current())
return lines
}
/** 一张卡片滚到顶上时要让出的高度:它上面会钉着的外层卡片(Sticky Scroll 关着时没有)。 */
export function stickyInset(card: Element, settings: StickyScrollSettings | undefined): number {
return settings?.enabled ? insetAbove(depthOf(card), STICKY_LINE_HEIGHT, limitOf(settings.maxLineCount)) : 0
}
/** 眼下钉着的几行占多高(StickyLines 画的 .cr-sticky),没钉时是 0。 */
export function stickyHeight(): number {
return document.querySelector<HTMLElement>('.cr-sticky')?.offsetHeight ?? 0
}