🐛 fix(vscode): 代码阅读按 ⌘↓ 跳高亮不再来回闪,筛选栏加上上一处、下一处
This commit is contained in:
1 parent
70a53e5a68
commit
b16cc6409e
8 files changed
+221
-36
No files matched your search
@@ -88,7 +88,7 @@ export function App(): ReactElement | null {
|
||||
expand,
|
||||
}
|
||||
: undefined, [doc, unshown, covered, view.editing, collapsed, expand])
|
||||
const highlight = useOccurrences(doc?.version, doc?.translatorId, occurrencePage)
|
||||
const { highlight, moveOccurrence } = useOccurrences(doc?.version, doc?.translatorId, occurrencePage)
|
||||
|
||||
useEffect(() => {
|
||||
const off = onMessage((msg) => {
|
||||
@@ -164,6 +164,7 @@ export function App(): ReactElement | null {
|
||||
excluded={excluded}
|
||||
view={filter}
|
||||
highlight={highlight}
|
||||
onMoveOccurrence={moveOccurrence}
|
||||
onChange={next => postMessage({ type: 'setFilter', excluded: next })}
|
||||
translators={doc.translators}
|
||||
translatorId={doc.translatorId}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { ReactElement, ReactNode } from 'react'
|
||||
import type { Highlight } from './occurrences'
|
||||
import { isValidElement } from 'react'
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { FilterBar } from './FilterBar'
|
||||
|
||||
const view = { hidden: new Set<string>(), temporary: new Set<string>(), hiddenCount: 0, total: 3 }
|
||||
|
||||
function bar(highlight: Highlight | undefined, onMoveOccurrence: (step: 1 | -1) => void): ReactElement {
|
||||
return FilterBar({
|
||||
attributes: [],
|
||||
excluded: {},
|
||||
view,
|
||||
highlight,
|
||||
onMoveOccurrence,
|
||||
onChange: () => {},
|
||||
translators: [{ id: 'plain', label: '不翻译' }],
|
||||
translatorId: 'plain',
|
||||
onSelectTranslator: () => {},
|
||||
})
|
||||
}
|
||||
|
||||
function buttons(node: ReactNode): ReactElement<{ 'data-tooltip'?: string, 'disabled'?: boolean, 'onClick'?: () => void }>[] {
|
||||
const found: ReactElement<{ 'data-tooltip'?: string, 'disabled'?: boolean, 'onClick'?: () => void }>[] = []
|
||||
const visit = (child: ReactNode): void => {
|
||||
if (Array.isArray(child)) {
|
||||
child.forEach(visit)
|
||||
return
|
||||
}
|
||||
if (!isValidElement(child))
|
||||
return
|
||||
if (child.type === 'button')
|
||||
found.push(child as ReactElement<{ 'data-tooltip'?: string, 'disabled'?: boolean, 'onClick'?: () => void }>)
|
||||
visit((child.props as { children?: ReactNode }).children)
|
||||
}
|
||||
visit(node)
|
||||
return found
|
||||
}
|
||||
|
||||
const at = (start: number): Highlight['occurrences'][number] => ({ start, end: start + 1, kind: 'read' })
|
||||
|
||||
describe('筛选栏上的上一处 / 下一处', () => {
|
||||
it('名字和「1/3」右边有两个按钮,点了按方向跳,不用原生 title', () => {
|
||||
const steps: Array<1 | -1> = []
|
||||
const tree = bar({ name: 'AIGC_CONTENT_PRODUCER', occurrences: [at(1), at(2), at(3)], current: 0 }, step => steps.push(step))
|
||||
const html = renderToStaticMarkup(tree)
|
||||
const countAt = html.indexOf('1/3')
|
||||
const prevAt = html.indexOf('data-tooltip="上一处"')
|
||||
const nextAt = html.indexOf('data-tooltip="下一处"')
|
||||
expect(countAt).toBeGreaterThan(0)
|
||||
expect(prevAt).toBeGreaterThan(countAt)
|
||||
expect(nextAt).toBeGreaterThan(prevAt)
|
||||
expect(html).not.toContain(' title=')
|
||||
const nav = buttons(tree).filter(button => button.props['data-tooltip'] === '上一处' || button.props['data-tooltip'] === '下一处')
|
||||
expect(nav.map(button => button.props['data-tooltip'])).toEqual(['上一处', '下一处'])
|
||||
expect(nav.every(button => button.props.disabled !== true)).toBe(true)
|
||||
nav[0]?.props.onClick?.()
|
||||
nav[1]?.props.onClick?.()
|
||||
expect(steps).toEqual([-1, 1])
|
||||
})
|
||||
|
||||
it('只有一处时两个按钮不可点', () => {
|
||||
const tree = bar({ name: 'A', occurrences: [at(1)], current: 0 }, () => {})
|
||||
const nav = buttons(tree).filter(button => button.props['data-tooltip'] === '上一处' || button.props['data-tooltip'] === '下一处')
|
||||
expect(nav).toHaveLength(2)
|
||||
expect(nav.every(button => button.props.disabled === true)).toBe(true)
|
||||
})
|
||||
|
||||
it('没有高亮时不显示这组按钮', () => {
|
||||
const html = renderToStaticMarkup(bar(undefined, () => {}))
|
||||
expect(html).not.toContain('data-tooltip="上一处"')
|
||||
expect(html).not.toContain('data-tooltip="下一处"')
|
||||
})
|
||||
})
|
||||
@@ -2,7 +2,7 @@ import type { FocusEvent, KeyboardEvent, ReactElement, ReactNode } from 'react'
|
||||
import type { Facet, FilterView } from './filter'
|
||||
import type { Highlight } from './occurrences'
|
||||
import type { Excluded, FileAttribute, TranslatorOption } from './types'
|
||||
import { Check, ChevronDown, ListFilter } from 'lucide-react'
|
||||
import { ArrowDown, ArrowUp, Check, ChevronDown, ListFilter } from 'lucide-react'
|
||||
import { useId, useLayoutEffect, useRef, useState } from 'react'
|
||||
import { SelectMenu } from '@/components/ui/select-menu'
|
||||
import { facetLabel, facetsOf, setChecked, toggleValue } from './filter'
|
||||
@@ -14,6 +14,8 @@ export interface FilterBarProps {
|
||||
view: FilterView
|
||||
/** 眼下高亮着的名字:汇总前面写「名字 · 2/5」(当前是第几处 / 共几处,见 occurrenceCount),折叠、筛掉的块里的出现也算在总数里 */
|
||||
highlight: Highlight | undefined
|
||||
/** 汇总里「上一处 / 下一处」,和快捷键走同一条跳转 */
|
||||
onMoveOccurrence: (step: 1 | -1) => void
|
||||
onChange: (excluded: Excluded) => void
|
||||
translators: TranslatorOption[]
|
||||
translatorId: string
|
||||
@@ -23,10 +25,10 @@ export interface FilterBarProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览顶部钉住的一条:「筛选」、这份文件里每个能筛的属性一个下拉框、右边汇总(高亮着名字时先是「名字 · 2/5」,再是共 N 块 / 已隐藏 N 块 · 清除),
|
||||
* 预览顶部钉住的一条:「筛选」、这份文件里每个能筛的属性一个下拉框、右边汇总(高亮着名字时先是「名字 · 2/5」,接着上一处 / 下一处,再是共 N 块 / 已隐藏 N 块 · 清除),
|
||||
* 多于一种翻译时最右边是翻译下拉框;标差异时下面再接一行(below)。内容和代码块同宽对齐,滚动时块从它下面过去。
|
||||
*/
|
||||
export function FilterBar({ attributes, excluded, view, highlight, onChange, translators, translatorId, onSelectTranslator, below }: FilterBarProps): ReactElement {
|
||||
export function FilterBar({ attributes, excluded, view, highlight, onMoveOccurrence, onChange, translators, translatorId, onSelectTranslator, below }: FilterBarProps): ReactElement {
|
||||
return (
|
||||
<div className="cr-filter-bar">
|
||||
<div className="mx-auto box-content flex max-w-[880px] flex-wrap items-center gap-2 px-6 py-1.5">
|
||||
@@ -48,6 +50,21 @@ export function FilterBar({ attributes, excluded, view, highlight, onChange, tra
|
||||
<span className="cr-occurrence-name max-w-[240px] min-w-0 truncate" data-occurrence="normal" data-tooltip={highlight.name}>{highlight.name}</span>
|
||||
<span aria-hidden>·</span>
|
||||
{occurrenceCount(highlight)}
|
||||
<span className="flex items-center" role="group" aria-label="各处出现之间跳">
|
||||
{([[-1, ArrowUp, '上一处'], [1, ArrowDown, '下一处']] as const).map(([step, Icon, label]) => (
|
||||
<button
|
||||
key={step}
|
||||
type="button"
|
||||
className="cr-tool flex h-[22px] w-6 items-center justify-center rounded disabled:opacity-40"
|
||||
data-tooltip={label}
|
||||
aria-label={label}
|
||||
disabled={highlight.occurrences.length < 2}
|
||||
onClick={() => onMoveOccurrence(step)}
|
||||
>
|
||||
<Icon size={14} strokeWidth={1.8} className="text-[var(--vscode-foreground)]" aria-hidden />
|
||||
</button>
|
||||
))}
|
||||
</span>
|
||||
<span className="mx-1.5 h-3 border-l border-[var(--vscode-panel-border)]" aria-hidden />
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createPythonAdapter } from '../../../src/codeReader/languages/python'
|
||||
import { WASM_DIR, withBlocks } from '../../../src/codeReader/testSupport'
|
||||
import { pythonZhTranslator } from '../../../src/codeReader/translators/pythonZh'
|
||||
import { DefinitionLinks } from './definitionLinks'
|
||||
import { clickAction, isCurrent, KEY_SCROLL_MS, KEY_TO_COMMAND_MS, KeyScroll, occurrenceCount, OccurrenceHighlight, OccurrenceNavigation, occurrenceStyle, segmentOccurrence, sourceOf, stepIndex } from './occurrences'
|
||||
import { clickAction, isCurrent, KEY_SCROLL_MS, KEY_TO_COMMAND_MS, KeyScroll, occurrenceCount, OccurrenceHighlight, occurrenceJumpKey, OccurrenceNavigation, occurrenceStyle, segmentOccurrence, sourceOf, stepIndex } from './occurrences'
|
||||
|
||||
function name(text: string, offset: number): Segment {
|
||||
return { text, role: 'variable', offset }
|
||||
@@ -359,4 +359,34 @@ describe('occurrences:用键盘在各处出现之间跳', () => {
|
||||
scroll.forget()
|
||||
expect(scroll.scrolled({ x: 0, y: 300 }, 1130)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('我们自己滚过的位置不算浏览器滚走:差 1 像素、滚进视口都不再拉一次,真被滚走仍拉回', () => {
|
||||
const scroll = new KeyScroll()
|
||||
scroll.press({ x: 0, y: 40 }, 1000)
|
||||
scroll.take(1012)
|
||||
// scrollBack 先滚到 39 再回到 40;滚进视口则落到另一个位置。这些事件可能晚于滚动调用才到
|
||||
scroll.own({ x: 0, y: 39 })
|
||||
scroll.own({ x: 0, y: 40 })
|
||||
scroll.own({ x: 0, y: 800 })
|
||||
expect(scroll.scrolled({ x: 0, y: 39 }, 1020)).toBeUndefined()
|
||||
expect(scroll.scrolled({ x: 0, y: 39 }, 1021)).toBeUndefined()
|
||||
expect(scroll.scrolled({ x: 0, y: 800 }, 1022)).toBeUndefined()
|
||||
scroll.hold({ x: 0, y: 800 })
|
||||
expect(scroll.scrolled({ x: 0, y: 39 }, 1030)).toBeUndefined()
|
||||
expect(scroll.scrolled({ x: 0, y: 300 }, 1040)).toEqual({ x: 0, y: 800 })
|
||||
})
|
||||
|
||||
it('默认快捷键:mac 是 ⌘↓ / ⌘↑,别的系统是 ctrl;带了别的修饰键不算', () => {
|
||||
const key = (name: string, mod: Partial<{ metaKey: boolean, ctrlKey: boolean, altKey: boolean, shiftKey: boolean }> = {}) =>
|
||||
({ key: name, metaKey: false, ctrlKey: false, altKey: false, shiftKey: false, ...mod })
|
||||
expect(occurrenceJumpKey(key('ArrowDown', { metaKey: true }), true)).toBe(true)
|
||||
expect(occurrenceJumpKey(key('ArrowUp', { metaKey: true }), true)).toBe(true)
|
||||
expect(occurrenceJumpKey(key('ArrowDown', { ctrlKey: true }), true)).toBe(false)
|
||||
expect(occurrenceJumpKey(key('ArrowDown', { ctrlKey: true }), false)).toBe(true)
|
||||
expect(occurrenceJumpKey(key('ArrowUp', { ctrlKey: true }), false)).toBe(true)
|
||||
expect(occurrenceJumpKey(key('ArrowDown', { metaKey: true }), false)).toBe(false)
|
||||
expect(occurrenceJumpKey(key('ArrowDown', { metaKey: true, shiftKey: true }), true)).toBe(false)
|
||||
expect(occurrenceJumpKey(key('ArrowDown', { metaKey: true, altKey: true }), true)).toBe(false)
|
||||
expect(occurrenceJumpKey(key('ArrowLeft', { metaKey: true }), true)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -129,31 +129,55 @@ export const KEY_SCROLL_MS = 500
|
||||
export interface ScrollPosition { x: number, y: number }
|
||||
|
||||
/**
|
||||
* 跳到下一处 / 上一处的命令由 VS Code 的快捷键系统触发,页面不知道绑的是哪个键,浏览器对这个键自己的动作(⌘↓ 滚到底)拦不住;
|
||||
* 它还可能早于命令开始,也可能晚于命令(实测命令十几毫秒就到,滚动动画再过一帧才开始)。所以不管哪个键:
|
||||
* 默认的「下一处 / 上一处」快捷键:mac 是 ⌘↓ / ⌘↑,别的系统是 ctrl。
|
||||
* 只用来取消浏览器滚到底 / 滚到顶,不拿它判断命令到了没有(命令是 VS Code 发来的)。
|
||||
*/
|
||||
export function occurrenceJumpKey(
|
||||
event: { key: string, metaKey: boolean, ctrlKey: boolean, altKey: boolean, shiftKey: boolean },
|
||||
mac: boolean,
|
||||
): boolean {
|
||||
if (event.altKey || event.shiftKey || (event.key !== 'ArrowDown' && event.key !== 'ArrowUp'))
|
||||
return false
|
||||
return mac ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳到下一处 / 上一处的命令由 VS Code 的快捷键系统触发。默认的 ⌘↓ / ⌘↑(Windows 是 ctrl)在能跳的时候会取消浏览器滚到底 / 滚到顶,
|
||||
* 但用户改过快捷键时别的键仍会自己滚,而且可能早于或晚于命令。所以任何键:
|
||||
* 按键那一刻记下页面滚在哪(press);命令发来时先回到那里(take)再滚到目标;跳完守住跳到的位置(hold),
|
||||
* 按键后 KEY_SCROLL_MS 内浏览器自己滚走了就拉回来(scrolled)。
|
||||
* 按键后点了鼠标、滚了滚轮、又按了键,或跳转隔了 KEY_TO_COMMAND_MS 以上才来(从命令面板运行),都不算这个键触发的。
|
||||
* 我们自己滚到的位置用 own 记下(拉回时差的那一像素、滚进视口),对应的滚动事件不算浏览器滚走;事件可能晚于滚动调用才到。
|
||||
* 按键后点了鼠标、滚了滚轮、又按了键,或跳转隔了 KEY_TO_COMMAND_MS 以上才来(从命令面板运行),都不算这个键触发的。筛选栏上的按钮另走,不经过这里。
|
||||
*/
|
||||
export class KeyScroll {
|
||||
private pressed: { position: ScrollPosition, at: number } | undefined
|
||||
private held: { position: ScrollPosition, until: number } | undefined
|
||||
/** 我们自己滚到的位置。守着的这段时间里,落到这里不算浏览器把页面滚走。 */
|
||||
private owned: ScrollPosition[] = []
|
||||
|
||||
/** 按下任何键(照常交给浏览器和 VS Code 处理)时记下页面滚在哪。 */
|
||||
press(position: ScrollPosition, at: number): void {
|
||||
this.pressed = { position, at }
|
||||
this.held = undefined
|
||||
this.owned = []
|
||||
}
|
||||
|
||||
forget(): void {
|
||||
this.pressed = undefined
|
||||
this.held = undefined
|
||||
this.owned = []
|
||||
}
|
||||
|
||||
/** 记下我们马上要滚到的位置。滚动事件晚到也认。 */
|
||||
own(position: ScrollPosition): void {
|
||||
this.owned.push(position)
|
||||
}
|
||||
|
||||
/** 跳转发来时:刚按过键就交出按键那一刻的位置(只交一次),从这时起守住它,直到跳完换成跳到的位置。 */
|
||||
take(now: number): ScrollPosition | undefined {
|
||||
const pressed = this.pressed
|
||||
this.pressed = undefined
|
||||
this.owned = []
|
||||
this.held = pressed && now - pressed.at <= KEY_TO_COMMAND_MS ? { position: pressed.position, until: pressed.at + KEY_SCROLL_MS } : undefined
|
||||
return this.held?.position
|
||||
}
|
||||
@@ -164,13 +188,16 @@ export class KeyScroll {
|
||||
this.held = { ...this.held, position }
|
||||
}
|
||||
|
||||
/** 页面滚动了:还在守着、又被浏览器滚走了,给出要拉回的位置。 */
|
||||
/** 页面滚动了:还在守着、又被浏览器滚走了,给出要拉回的位置。我们自己滚到的位置不算。 */
|
||||
scrolled(position: ScrollPosition, now: number): ScrollPosition | undefined {
|
||||
const held = this.held
|
||||
if (!held || now > held.until) {
|
||||
this.held = undefined
|
||||
this.owned = []
|
||||
return undefined
|
||||
}
|
||||
if (this.owned.some(item => item.x === position.x && item.y === position.y))
|
||||
return undefined
|
||||
return held.position.x === position.x && held.position.y === position.y ? undefined : held.position
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Click, Highlight, ScrollPosition } from './occurrences'
|
||||
import type { BlockView, Occurrence } from './types'
|
||||
import { createContext, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import { clickAction, KeyScroll, OccurrenceHighlight, OccurrenceNavigation } from './occurrences'
|
||||
import { createContext, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
|
||||
import { clickAction, KeyScroll, OccurrenceHighlight, occurrenceJumpKey, OccurrenceNavigation } from './occurrences'
|
||||
import { occurrencePlan } from './reveal'
|
||||
import { showInViewport } from './useReveal'
|
||||
import { onMessage, postMessage } from './vscode'
|
||||
@@ -52,29 +52,41 @@ function scrollPosition(): ScrollPosition {
|
||||
return { x: window.scrollX, y: window.scrollY }
|
||||
}
|
||||
|
||||
/** mac 用 ⌘,别的系统用 ctrl。页面跑在 VS Code 的网页里,平台从浏览器读。 */
|
||||
function isMac(): boolean {
|
||||
return /Mac|iPhone|iPad/.test(navigator.platform)
|
||||
}
|
||||
|
||||
/**
|
||||
* 立刻滚到这里,顺带打断浏览器正在播(或刚排上、还没开始播)的滚动动画。滚到原地浏览器当没滚、不打断动画,
|
||||
* 所以先挪一个像素再回来:同一段脚本里连着滚两下,中间不画,画面上看不出。
|
||||
* 这两处位置先交给 own:滚动事件可能晚于这次调用才到,不能被当成浏览器把页面滚走。
|
||||
*/
|
||||
function scrollBack({ x, y }: ScrollPosition): void {
|
||||
window.scrollTo({ left: x, top: y > 0 ? y - 1 : y + 1, behavior: 'instant' })
|
||||
function scrollBack({ x, y }: ScrollPosition, own: (position: ScrollPosition) => void): void {
|
||||
const nudged = { x, y: y > 0 ? y - 1 : y + 1 }
|
||||
own(nudged)
|
||||
own({ x, y })
|
||||
window.scrollTo({ left: x, top: nudged.y, behavior: 'instant' })
|
||||
window.scrollTo({ left: x, top: y, behavior: 'instant' })
|
||||
}
|
||||
|
||||
/**
|
||||
* 单击名字高亮它在文档里的各处出现,点空白处、Esc、换文档版本或翻译清掉;返回眼下高亮着的名字。
|
||||
* 高亮着时扩展端的「跳到下一处 / 上一处高亮」命令发来 moveOccurrence:当前那一处换成下一处页面上找得到的出现,
|
||||
* 展开把它折叠住的块、属性行和子行,再把它滚进视口。
|
||||
* 触发命令的那个键浏览器自己的滚动不拦(页面不知道是哪个键),跳的时候盖掉它,见 KeyScroll。
|
||||
* 单击名字高亮它在文档里的各处出现,点空白处、Esc、换文档版本或翻译清掉;返回眼下高亮着的名字,和筛选栏上「上一处 / 下一处」用的跳转。
|
||||
* 高亮着时扩展端的「跳到下一处 / 上一处高亮」命令发来 moveOccurrence,筛选栏上的按钮走同一条跳转:当前那一处换成下一处页面上找得到的出现,
|
||||
* 展开把它折叠住的块、属性行和子行,再把它滚进视口。按钮不是按键,不按 ⌘↓ 那样拉回按键时的位置。
|
||||
* 默认的 ⌘↓ / ⌘↑(Windows 是 ctrl)在能跳的时候取消浏览器滚到底 / 滚到顶,但不停事件继续传(VS Code 在后面才把按键收成命令)。
|
||||
* 别的键浏览器仍可能自己滚,跳的时候盖掉它,见 KeyScroll。
|
||||
*/
|
||||
export function useOccurrences(version: number | undefined, translatorId: string | undefined, page: OccurrencePage | undefined): Highlight | undefined {
|
||||
export function useOccurrences(version: number | undefined, translatorId: string | undefined, page: OccurrencePage | undefined): { highlight: Highlight | undefined, moveOccurrence: (step: 1 | -1) => void } {
|
||||
const [highlight, setHighlight] = useState<Highlight | undefined>()
|
||||
const [occurrences] = useState(() => new OccurrenceHighlight(postMessage, setHighlight))
|
||||
const [navigation] = useState(() => new OccurrenceNavigation(postMessage, { focused: document.hasFocus(), typing: isTyping(document.activeElement) }))
|
||||
const [keyScroll] = useState(() => new KeyScroll())
|
||||
/** 用键盘跳过几次:每跳一次,画完后把当前那一处滚进视口 */
|
||||
/** 用键盘或筛选栏按钮跳过几次:每跳一次,画完后把当前那一处滚进视口 */
|
||||
const [moves, setMoves] = useState(0)
|
||||
const latestPage = useRef(page)
|
||||
/** 筛选栏按钮调用的跳转。真正的函数在下面的效果里,挂上监听时才有。 */
|
||||
const moveFromButton = useRef<(step: 1 | -1) => void>(() => {})
|
||||
|
||||
useLayoutEffect(() => {
|
||||
latestPage.current = page
|
||||
@@ -91,16 +103,22 @@ export function useOccurrences(version: number | undefined, translatorId: string
|
||||
return
|
||||
const el = document.querySelector<HTMLElement>('[data-current]')
|
||||
if (el)
|
||||
showInViewport(el)
|
||||
showInViewport(el, position => keyScroll.own(position))
|
||||
keyScroll.hold(scrollPosition())
|
||||
}, [moves, keyScroll])
|
||||
|
||||
useEffect(() => {
|
||||
const move = (step: 1 | -1): void => {
|
||||
// 触发命令的那个键浏览器可能已经照自己的意思滚了(⌘↓ 滚到底),先回到按键那一刻
|
||||
const before = keyScroll.take(performance.now())
|
||||
if (before)
|
||||
scrollBack(before)
|
||||
const move = (step: 1 | -1, fromKey: boolean): void => {
|
||||
if (fromKey) {
|
||||
// 触发命令的那个键浏览器可能已经照自己的意思滚了(⌘↓ 滚到底),先回到按键那一刻
|
||||
const before = keyScroll.take(performance.now())
|
||||
if (before)
|
||||
scrollBack(before, position => keyScroll.own(position))
|
||||
}
|
||||
else {
|
||||
// 筛选栏按钮不是按键,别把这次滚动当成 ⌘↓ 拉回按键时的位置
|
||||
keyScroll.forget()
|
||||
}
|
||||
const current = latestPage.current
|
||||
const planOf = (occurrence: Occurrence): ReturnType<typeof occurrencePlan> => current && occurrencePlan(current.blocks, occurrence, current.skip)
|
||||
const target = current && occurrences.move(step, occurrence => planOf(occurrence) !== undefined)
|
||||
@@ -116,13 +134,18 @@ export function useOccurrences(version: number | undefined, translatorId: string
|
||||
}
|
||||
// 捕获阶段:赶在块里的按钮处理点击、折叠箭头换图标之前读出点在哪
|
||||
const onClick = (event: MouseEvent): void => occurrences.click(clickAction(clickOf(event)))
|
||||
// 任何键都照常交给浏览器和 VS Code,这里只记下按键那一刻页面滚在哪
|
||||
const onKeyDown = (event: KeyboardEvent): void => keyScroll.press(scrollPosition(), event.timeStamp)
|
||||
moveFromButton.current = step => move(step, false)
|
||||
// 任何键都照常交给 VS Code。能跳时,默认的 ⌘↓ / ⌘↑(Windows 是 ctrl)取消浏览器滚到底 / 滚到顶,但不停冒泡,命令还是收得到
|
||||
const onKeyDown = (event: KeyboardEvent): void => {
|
||||
if (navigation.available && occurrenceJumpKey(event, isMac()))
|
||||
event.preventDefault()
|
||||
keyScroll.press(scrollPosition(), event.timeStamp)
|
||||
}
|
||||
const onPointer = (): void => keyScroll.forget()
|
||||
const onScroll = (event: Event): void => {
|
||||
const back = keyScroll.scrolled(scrollPosition(), event.timeStamp)
|
||||
if (back)
|
||||
scrollBack(back)
|
||||
scrollBack(back, position => keyScroll.own(position))
|
||||
}
|
||||
const onKey = (event: KeyboardEvent): void => {
|
||||
// 输入框、下拉列表自己用掉的 Esc 不算
|
||||
@@ -137,7 +160,7 @@ export function useOccurrences(version: number | undefined, translatorId: string
|
||||
if (msg.type === 'occurrencesFound')
|
||||
occurrences.answer(msg.id, msg.occurrences)
|
||||
else if (msg.type === 'moveOccurrence')
|
||||
move(msg.step)
|
||||
move(msg.step, true)
|
||||
})
|
||||
navigation.update({})
|
||||
document.addEventListener('click', onClick, true)
|
||||
@@ -165,5 +188,6 @@ export function useOccurrences(version: number | undefined, translatorId: string
|
||||
}
|
||||
}, [occurrences, navigation, keyScroll])
|
||||
|
||||
return highlight
|
||||
const moveOccurrence = useCallback((step: 1 | -1) => moveFromButton.current(step), [])
|
||||
return { highlight, moveOccurrence }
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ScrollPosition } from './occurrences'
|
||||
import type { DocumentMessage, Reveal } from './types'
|
||||
import { useLayoutEffect } from 'react'
|
||||
import { blockPath } from './filter'
|
||||
@@ -24,12 +25,16 @@ export function flash(el: HTMLElement): void {
|
||||
}
|
||||
|
||||
/** 把 el 滚进钉住的筛选栏下面的视口,规则见 scrollTopToShow:已经整个看得见就不滚。 */
|
||||
export function showInViewport(el: HTMLElement): void {
|
||||
/** 把 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)
|
||||
window.scrollTo(0, top - pinned)
|
||||
if (top === undefined)
|
||||
return
|
||||
const position = { x: 0, y: top - pinned }
|
||||
beforeScroll?.(position)
|
||||
window.scrollTo(position.x, position.y)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user