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 滚进钉住的筛选栏下面的视口。已经整个看得见就不滚。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, 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('[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, filtered: ReadonlySet, covered: ReadonlyMap, 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 block = plan ? document.querySelector(`.cr-block[data-id="${plan.id}"]`) : null const row = plan?.row === undefined ? null : document.querySelector(`: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]) }