114 lines
5.6 KiB
TypeScript
114 lines
5.6 KiB
TypeScript
import type { DocumentMessage, Reveal } from './types'
|
|
import { useLayoutEffect } from 'react'
|
|
import { blockPath } from './filter'
|
|
import { pageLayout } from './pageLayout'
|
|
import { placementOf } from './placement'
|
|
import { linesPlan, revealPlan, scrollTopToShow, uncoveredPlan } from './reveal'
|
|
import { scrollOwner } from './scrollOwner'
|
|
import { stickyHeight } from './stickyScroll'
|
|
|
|
/** 正在闪的元素和到点摘掉 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))
|
|
}
|
|
|
|
/**
|
|
* 把 `locate` 找到的目标(一个元素,或查找栏的一处命中这样的一段字)滚进钉住的筛选栏和卡片下面的视口,量之前先把它那一带排版好([PageLayout.settle](vscode/webview-ui/src/codeReader/pageLayout.ts#PageLayout.settle));已经整个看得见就不滚。
|
|
* 之后由 [scrollOwner] 跟住它:页面再长高变矮也停在原处,直到用户自己动手。`locate` 每次现找,页面重画以后元素可能换了一个。
|
|
*/
|
|
export function showInViewport(locate: () => HTMLElement | Range | null): void {
|
|
const target = locate()
|
|
const el = target instanceof Range ? target.startContainer.parentElement : target
|
|
if (el)
|
|
pageLayout.settle(el)
|
|
const pinned = pageLayout.pinnedHeight() + stickyHeight()
|
|
scrollOwner.reveal(locate, box => scrollTopToShow(box, window.scrollY, window.innerHeight, pinned))
|
|
}
|
|
|
|
/** 这块自己显示着的、下标(块里的)就是定义处的那个名字。子块里的同名下标不算,免得闪到旁边那张卡片。 */
|
|
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,也没有 endLine):包含那一行的块顶到视口顶部(筛选栏下面),折叠着的、被筛掉的不展开;
|
|
* ② 跳到定义:目标或它的祖先块被筛掉了,先临时露出这条路径(show);再展开把目标折叠住的块、属性行和子行(expand)——
|
|
* 每一步之后重新渲染、再进来一次;最后把目标滚进筛选栏下面并闪几下。
|
|
* 属性行 / 子行是这处定义时闪那一行;这块里显示着这个名字(下标盖住定义处)就闪这个名字,不闪整张卡片;
|
|
* 名字不在页面上才闪整块。目标被正在编辑的祖先盖住时闪那个祖先(uncoveredPlan)。
|
|
* ③ 跳到几行(有 endLine,点了带 `#L起-L止` 的链接):目标是包住这几行的块(见 [linesPlan]),露出、展开、滚过去同 ②,闪整块。
|
|
* 滚过去以后由 [scrollOwner] 跟住目标(① 跟住那块),页面再长高(注释里的图画出来、中文名到了)也停在原处。
|
|
*/
|
|
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
|
|
const { line, definition, endLine } = reveal
|
|
if (definition === undefined && endLine === undefined) {
|
|
const card = pageLayout.cardForLine(line)
|
|
if (card)
|
|
scrollOwner.align({ line: card.line, offset: 0, scrollTop: card.top })
|
|
done()
|
|
return
|
|
}
|
|
const placement = placementOf(doc.blocks)
|
|
const found = definition === undefined ? linesPlan(doc.blocks, placement, line, endLine ?? line) : revealPlan(doc.blocks, placement, line, 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 base = plan && placement.of(plan.id)?.offset
|
|
// 块正在编辑时页面上没有属性表,就闪整块
|
|
const locate = (): HTMLElement | null => {
|
|
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 || base === undefined || definition === undefined ? null : nameAt(block, definition - base)
|
|
return row ?? name ?? block
|
|
}
|
|
const el = locate()
|
|
if (el) {
|
|
showInViewport(locate)
|
|
flash(el)
|
|
}
|
|
done()
|
|
}, [doc, reveal, collapsed, filtered, covered, show, expand, done])
|
|
}
|