111 lines
5.2 KiB
TypeScript
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
|
|
}
|