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

110 lines
4.7 KiB
TypeScript

import type { ScrollPosition } from './occurrences'
import type { DocumentMessage, Reveal } from './types'
import { useLayoutEffect } from 'react'
import { blockPath } from './filter'
import { pageLayout } from './pageLayout'
import { revealPlan, scrollTopToShow, uncoveredPlan } from './reveal'
import { stickyHeight } from './useStickyScroll'
/** 正在闪的元素和到点摘掉 data-flash 的计时器 */
const flashing = new WeakMap<HTMLElement, number>()
/**
* 让一块或属性表里的一行闪几下(codeReader.css 的 [data-flash]);正在闪的同一处再跳一次时从头闪。
* 闪完(按 CSS 动画的时长)摘掉 data-flash:用计时器而不是等 animationend,滚到画面外被跳过排版的块(content-visibility)里动画停着,等不到它。
*/
export function flash(el: HTMLElement): void {
window.clearTimeout(flashing.get(el))
el.setAttribute('data-flash', '')
const animations = el.getAnimations().filter(animation => animation instanceof CSSAnimation)
for (const animation of animations)
animation.currentTime = 0
const end = Math.max(0, ...animations.map(animation => Number(animation.effect?.getComputedTiming().endTime ?? 0)))
flashing.set(el, window.setTimeout(() => {
el.removeAttribute('data-flash')
flashing.delete(el)
}, end))
}
/** 把 el 滚进钉住的筛选栏和卡片下面的视口,量之前先把它那一带排版好(pageLayout.settle)。已经整个看得见就不滚。beforeScroll 在真正滚动前拿到将要落到的位置。 */
export function showInViewport(el: HTMLElement, beforeScroll?: (position: ScrollPosition) => void): void {
pageLayout.settle(el)
const rect = el.getBoundingClientRect()
const pinned = pageLayout.pinnedHeight() + stickyHeight()
const top = scrollTopToShow(
{ top: rect.top + window.scrollY, bottom: rect.bottom + window.scrollY },
window.scrollY,
window.innerHeight,
pinned,
)
if (top === undefined)
return
const position = { x: 0, y: top }
beforeScroll?.(position)
window.scrollTo(position.x, position.y)
}
/** 这块自己显示着的、下标就是定义处的那个名字。子块里的同名下标不算,免得闪到旁边那张卡片。 */
function nameAt(block: HTMLElement, offset: number): HTMLElement | null {
for (const el of block.querySelectorAll<HTMLElement>('[data-offset]')) {
if (Number(el.dataset.offset) !== offset)
continue
if (el.closest('.cr-block') === block)
return el
}
return null
}
/**
* 滚到 reveal 指的位置,处理完调 done:
* ① 从文本编辑器切过来(没有 definition):包含那一行的块顶到视口顶部(筛选栏下面),折叠着的、被筛掉的不展开;
* ② 跳到定义:目标或它的祖先块被筛掉了,先临时露出这条路径(show);再展开把目标折叠住的块、属性行和子行(expand)——
* 每一步之后重新渲染、再进来一次;最后把目标滚进筛选栏下面并闪几下。
* 属性行 / 子行是这处定义时闪那一行;这块里显示着这个名字(下标盖住定义处)就闪这个名字,不闪整张卡片;
* 名字不在页面上才闪整块。目标被正在编辑的祖先盖住时闪那个祖先(uncoveredPlan)。
*/
export function useReveal(
doc: DocumentMessage | null,
reveal: Reveal | null,
collapsed: ReadonlySet<string>,
filtered: ReadonlySet<string>,
covered: ReadonlyMap<string, string>,
show: (ids: string[]) => void,
expand: (ids: string[]) => void,
done: () => void,
): void {
useLayoutEffect(() => {
if (!doc || !reveal)
return
if (reveal.definition === undefined) {
const top = pageLayout.cardForLine(reveal.line)?.top
if (top !== undefined)
window.scrollTo(0, top - pageLayout.pinnedHeight())
done()
return
}
const found = revealPlan(doc.blocks, reveal.line, reveal.definition)
const plan = found && uncoveredPlan(found, covered)
const path = plan ? blockPath(plan.id) : []
if (path.some(id => filtered.has(id))) {
show(path)
return
}
const hidden = plan?.expand.filter(id => collapsed.has(id)) ?? []
if (hidden.length > 0) {
expand(hidden)
return
}
// 块正在编辑时页面上没有属性表,就闪整块
const block = plan ? document.querySelector<HTMLElement>(`.cr-block[data-id="${plan.id}"]`) : null
const row = plan?.row === undefined ? null : document.querySelector<HTMLElement>(`:is(.cr-prop-row, .cr-prop-item, .cr-table-row)[data-id="${plan.row}"]`)
const name = row || !block ? null : nameAt(block, reveal.definition)
const el = row ?? name ?? block
if (el) {
showInViewport(el)
flash(el)
}
done()
}, [doc, reveal, collapsed, filtered, covered, show, expand, done])
}