✨ feat(vscode): 代码阅读滚进卡片的子块时把外层卡片钉在顶上,同 VS Code 的 Sticky Scroll
This commit is contained in:
1 parent
5ce2e904f4
commit
eb3d1b90cd
17 files changed
+548
-31
No files matched your search
@@ -0,0 +1,101 @@
|
||||
import type { ReactElement, RefObject } from 'react'
|
||||
import type { BlockView, StickyScrollSettings } from './types'
|
||||
import { useLayoutEffect, useMemo } from 'react'
|
||||
import { KindIcon } from './kindIcons'
|
||||
import { SegmentText } from './SegmentText'
|
||||
import { HighlightContext } from './useOccurrences'
|
||||
import { flash } from './useReveal'
|
||||
import { pinnedHeight, usePinnedHeight } from './useScrollPosition'
|
||||
import { STICKY_LINE_HEIGHT, stickyInset, useStickyLines } from './useStickyScroll'
|
||||
|
||||
/** 块 id → 块,连同各层子块 */
|
||||
function blocksById(blocks: readonly BlockView[]): Map<string, BlockView> {
|
||||
const byId = new Map<string, BlockView>()
|
||||
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 (
|
||||
<div className="flex h-full min-w-0 items-center gap-2.5 pr-2.5 pl-[9px]">
|
||||
<span className="w-3.5 shrink-0" />
|
||||
<KindIcon concepts={block.concepts} />
|
||||
<p className="cr-code cr-sticky-title min-w-0 flex-1">
|
||||
{title && ('markdown' in title
|
||||
? <span className="cr-role-comment">{title.markdown.split('\n', 1)[0].trim()}</span>
|
||||
: <SegmentText segments={title} />)}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 钉在顶上的外层卡片(同 VS Code 编辑器的 Sticky Scroll,设置也用它的):滚进一张卡片的子块、卡片自己的开头已经滚到筛选栏后面时,
|
||||
* 筛选栏下面钉一行这张卡片的头(PinnedHead),外层的在上、里层的往下摞,每行和那张卡片左右对齐(越里层越往右);
|
||||
* 这张卡片的子块快滚完时,最里那行被下面的内容顶上去、从上一行下面滑走。哪几张见 pinnedLines,只按页面上画出来的算:
|
||||
* 折叠着的、被筛掉的、正在编辑的卡片下面没有子块,不钉。点一行把那张卡片滚到顶上(外层还钉着的几行下面)、闪几下。
|
||||
* 钉着的名字不画页面上的高亮(页面上找当前那一处只认卡片里的);跳到定义、悬停看卡片照旧。
|
||||
* content 是块树所在的页面内容,blocks 是这份文档的块。
|
||||
*/
|
||||
export function StickyLines({ content, blocks, settings }: { content: RefObject<HTMLElement | null>, blocks: readonly BlockView[], settings: StickyScrollSettings }): ReactElement | null {
|
||||
const top = usePinnedHeight()
|
||||
const lines = useStickyLines(content, settings.maxLineCount)
|
||||
const byId = useMemo(() => blocksById(blocks), [blocks])
|
||||
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<HTMLElement>(`.cr-block[data-id="${id}"]`)
|
||||
if (!card)
|
||||
return
|
||||
window.scrollTo(0, card.getBoundingClientRect().top + window.scrollY - pinnedHeight() - stickyInset(card, settings))
|
||||
flash(card)
|
||||
}
|
||||
|
||||
return (
|
||||
<HighlightContext value={undefined}>
|
||||
<div className="cr-sticky" style={{ top, height }}>
|
||||
{lines.scopes.map((scope, index) => {
|
||||
const block = byId.get(scope.id)
|
||||
const last = index === count - 1
|
||||
return block && (
|
||||
<div
|
||||
key={scope.id}
|
||||
className="cr-sticky-line"
|
||||
data-last={last ? '' : undefined}
|
||||
style={{ top: index * STICKY_LINE_HEIGHT + (last ? lines.offset : 0), height: STICKY_LINE_HEIGHT }}
|
||||
onClick={() => jump(scope.id)}
|
||||
>
|
||||
<div className="h-full" style={{ marginLeft: scope.left, width: scope.right - scope.left }}>
|
||||
<PinnedHead block={block} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</HighlightContext>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user