89 lines
3.8 KiB
TypeScript
89 lines
3.8 KiB
TypeScript
import type { ScrollPosition } from './occurrences'
|
|
import type { DocumentMessage, Reveal } from './types'
|
|
import { useLayoutEffect } from 'react'
|
|
import { blockPath } from './filter'
|
|
import { revealPlan, scrollTopToShow, uncoveredPlan } from './reveal'
|
|
import { blockForLine } from './scrollPosition'
|
|
import { measureBlocks, pinnedHeight } from './useScrollPosition'
|
|
|
|
/** 让一块或属性表里的一行闪几下(codeReader.css 的 [data-flash]);正在闪的同一处再跳一次时从头闪。 */
|
|
export function flash(el: HTMLElement): void {
|
|
const running = el.getAnimations().filter(animation => animation instanceof CSSAnimation)
|
|
if (running.length > 0) {
|
|
for (const animation of running)
|
|
animation.currentTime = 0
|
|
return
|
|
}
|
|
const onEnd = (event: AnimationEvent): void => {
|
|
if (event.target !== el)
|
|
return
|
|
el.removeAttribute('data-flash')
|
|
el.removeEventListener('animationend', onEnd)
|
|
}
|
|
el.addEventListener('animationend', onEnd)
|
|
el.setAttribute('data-flash', '')
|
|
}
|
|
|
|
/** 把 el 滚进钉住的筛选栏下面的视口,规则见 scrollTopToShow:已经整个看得见就不滚。 */
|
|
/** 把 el 滚进钉住的筛选栏下面的视口,规则见 scrollTopToShow:已经整个看得见就不滚。beforeScroll 在真正滚动前拿到将要落到的位置。 */
|
|
export function showInViewport(el: HTMLElement, beforeScroll?: (position: ScrollPosition) => void): void {
|
|
const rect = el.getBoundingClientRect()
|
|
const pinned = pinnedHeight()
|
|
const top = scrollTopToShow({ top: rect.top + window.scrollY, bottom: rect.bottom + window.scrollY }, window.scrollY + pinned, window.innerHeight - pinned)
|
|
if (top === undefined)
|
|
return
|
|
const position = { x: 0, y: top - pinned }
|
|
beforeScroll?.(position)
|
|
window.scrollTo(position.x, position.y)
|
|
}
|
|
|
|
/**
|
|
* 滚到 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 = blockForLine(measureBlocks(), reveal.line)?.top
|
|
if (top !== undefined)
|
|
window.scrollTo(0, top - 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 row = plan?.row === undefined ? null : document.querySelector<HTMLElement>(`:is(.cr-prop-row, .cr-prop-item, .cr-table-row)[data-id="${plan.row}"]`)
|
|
const el = row ?? (plan && document.querySelector<HTMLElement>(`.cr-block[data-id="${plan.id}"]`))
|
|
if (el) {
|
|
showInViewport(el)
|
|
flash(el)
|
|
}
|
|
done()
|
|
}, [doc, reveal, collapsed, filtered, covered, show, expand, done])
|
|
}
|