import type { ReactElement, RefObject } from 'react' import type { BlockView, StickyScrollSettings } from './types' import { useContext, useLayoutEffect, useMemo } from 'react' import { KindIcon } from './kindIcons' import { SegmentText, TextMarksContext } from './SegmentText' import { selectStore } from './store' import { flash } from './useReveal' import { pinnedHeight, usePinnedHeight } from './useScrollPosition' import { STICKY_LINE_HEIGHT, stickyInset, useStickyLines } from './useStickyScroll' /** 块 id → 块,连同各层子块 */ function blocksById(blocks: readonly BlockView[]): Map { const byId = new Map() const visit = (list: readonly BlockView[]): void => { for (const block of list) { byId.set(block.id, block) visit(block.children) } } visit(blocks) return byId } /** * 钉住的一行里卡片的头:折叠箭头那格留空、概念图标、标题,各自的位置同卡片上的,一行放不下省略。 * 标题同卡片上的写法(中文名照换):属性表的第一行(名称)的值,或一行译文;Markdown 只取第一行。 */ export function PinnedHead({ block }: { block: BlockView }): ReactElement { const title = 'properties' in block ? block.properties[0]?.value : block.translation return (

{title && ('markdown' in title ? {title.markdown.split('\n', 1)[0].trim()} : )}

) } /** * 钉在顶上的卡片(同 VS Code 编辑器的 Sticky Scroll,设置也用它的,一张卡片就是一层):卡片的开头已经滚到筛选栏后面、卡片(连同子块) * 还有一部分看得见时,筛选栏下面钉一行这张卡片的头(PinnedHead),外层的在上、里层的往下摞,每行和那张卡片左右对齐(越里层越往右); * 这张卡片快滚完时,最里那行被下面的内容顶上去、从上一行下面滑走。哪几张见 pinnedLines,只按页面上画出来的算: * 折叠着的卡片自己照样钉(它的子块没画出来,不算);被筛掉的不在页面上,正在编辑的不钉。点一行把那张卡片滚到顶上(外层还钉着的几行下面)、闪几下。 * 钉着的名字不画页面上的高亮(页面上找当前那一处只认卡片里的);跳到定义、悬停看卡片照旧。 * content 是块树所在的页面内容,blocks 是这份文档的块。 */ export function StickyLines({ content, blocks, settings }: { content: RefObject, blocks: readonly BlockView[], settings: StickyScrollSettings }): ReactElement | null { const top = usePinnedHeight() const lines = useStickyLines(content, settings.maxLineCount) const byId = useMemo(() => blocksById(blocks), [blocks]) // 钉着的名字照换中文名,不画页面上的高亮 const marks = useContext(TextMarksContext) const unhighlighted = useMemo(() => selectStore(marks, ({ names }) => ({ names })), [marks]) const count = lines.scopes.length const height = count * STICKY_LINE_HEIGHT + lines.offset // 浏览器自己滚的时候(输入框拿到焦点、打字跟着光标走)也让开筛选栏和钉着的几行:问 AI 的输入框、块的编辑框不被盖住 useLayoutEffect(() => { const page = document.documentElement page.style.scrollPaddingTop = `${top + height}px` return () => { page.style.scrollPaddingTop = '' } }, [top, height]) if (count === 0) return null const jump = (id: string): void => { const card = content.current?.querySelector(`.cr-block[data-id="${id}"]`) if (!card) return window.scrollTo(0, card.getBoundingClientRect().top + window.scrollY - pinnedHeight() - stickyInset(card, settings)) flash(card) } return (
{lines.scopes.map((scope, index) => { const block = byId.get(scope.id) const last = index === count - 1 return block && (
jump(scope.id)} >
) })}
) }