✨ feat(vscode): 代码阅读滚进卡片的子块时把外层卡片钉在顶上,同 VS Code 的 Sticky Scroll
This commit is contained in:
1 parent
5ce2e904f4
commit
eb3d1b90cd
17 files changed
+548
-31
No files matched your search
@@ -0,0 +1,105 @@
|
||||
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 里能钉住的卡片:画出了子层的那些(子层带 data-owner,挂在子行上的子块那层在卡片里面),按页面先后;竖直坐标以筛选栏下沿为 0 */
|
||||
function measureScopes(root: HTMLElement): 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-id]')) {
|
||||
const id = card.dataset.id ?? ''
|
||||
const levelBottom = bottoms.get(id)
|
||||
if (levelBottom === undefined)
|
||||
continue
|
||||
const rect = card.getBoundingClientRect()
|
||||
scopes.push({ id, depth: depthOf(card), top: rect.top - base, bottom: Math.max(rect.bottom, levelBottom) - base, 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 next = root ? pinnedLines(measureScopes(root), STICKY_LINE_HEIGHT, limitOf(maxLineCount)) : 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
|
||||
}
|
||||
Reference in new issue
Block a user