Merge feat/spelling-popover: right-click corrections + robust word resolution
This commit is contained in:
@@ -34,9 +34,12 @@ export function AskPetal({ suggestionId, explanation }: Props) {
|
||||
if (el) el.scrollTop = el.scrollHeight
|
||||
}, [messages])
|
||||
|
||||
// Focus the input when the panel opens.
|
||||
// Focus the input when the panel opens. preventScroll: the card is already on
|
||||
// screen as an absolutely-positioned overlay, and a default focus() would make
|
||||
// the browser scroll its ancestor to "reveal" the input — jumping the document
|
||||
// to the top.
|
||||
useEffect(() => {
|
||||
inputRef.current?.focus()
|
||||
inputRef.current?.focus({ preventScroll: true })
|
||||
}, [])
|
||||
|
||||
// Fetch the Chinese translation of the explanation to seed the first bubble.
|
||||
@@ -95,7 +98,7 @@ export function AskPetal({ suggestionId, explanation }: Props) {
|
||||
console.error('Ask Petal chat failed:', err)
|
||||
} finally {
|
||||
setStreaming(false)
|
||||
inputRef.current?.focus()
|
||||
inputRef.current?.focus({ preventScroll: true })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -580,9 +580,37 @@ export function EditorCore({
|
||||
[onDismiss, closeCard],
|
||||
)
|
||||
|
||||
// Click a red-underlined word to open its spelling popover, anchored under the
|
||||
// word. posAtCoords→wordAt resolves the exact PM span (robust to the same
|
||||
// misspelling appearing elsewhere); nspell supplies the corrections.
|
||||
// openMisspellAt resolves the word at a document position and, if the checker
|
||||
// flags it as misspelled, opens the spelling popover anchored under the word.
|
||||
// Returns true if it opened a card. We resolve the word from coordinates and
|
||||
// the checker rather than from the `.petal-misspelling` DOM span: clicking a
|
||||
// word moves the caret into it, which fires the decoration rebuild that
|
||||
// deliberately un-underlines the caret word (see SpellCheck's caret exemption),
|
||||
// so by the time a click/contextmenu handler runs the span is already gone.
|
||||
const openMisspellAt = useCallback(
|
||||
(pos: number): boolean => {
|
||||
if (!editor || !spellChecker) return false
|
||||
const range = wordAt(editor.state.doc, pos)
|
||||
if (!range || spellChecker.correct(range.word)) return false
|
||||
const wrapper = wrapperRef.current
|
||||
if (!wrapper) return false
|
||||
const start = editor.view.coordsAtPos(range.from)
|
||||
const end = editor.view.coordsAtPos(range.to)
|
||||
const wrapRect = wrapper.getBoundingClientRect()
|
||||
const cardWidth = 240
|
||||
const left = Math.max(0, Math.min(start.left - wrapRect.left, wrapper.clientWidth - cardWidth))
|
||||
const top = end.bottom - wrapRect.top + 6
|
||||
// Opening a spelling popover supersedes any AI-suggestion or lookup card.
|
||||
closeCard()
|
||||
setWordInfo(null)
|
||||
setMisspell({ ...range, suggestions: spellChecker.suggest(range.word), top, left })
|
||||
return true
|
||||
},
|
||||
[editor, spellChecker, closeCard],
|
||||
)
|
||||
|
||||
// Click a misspelled word to open its spelling popover, anchored under the
|
||||
// word; nspell supplies the corrections.
|
||||
//
|
||||
// Tapping an AI-suggestion highlight also opens its card here — on touch there's
|
||||
// no hover, so the tap is the only way in (mouse users still get hover).
|
||||
@@ -603,26 +631,16 @@ export function EditorCore({
|
||||
}
|
||||
return
|
||||
}
|
||||
if (!editor || !spellChecker) return
|
||||
const target = (e.target as HTMLElement).closest('.petal-misspelling') as HTMLElement | null
|
||||
if (!target) return
|
||||
const wrapper = wrapperRef.current
|
||||
if (!wrapper) return
|
||||
if (!editor) return
|
||||
// Only act on clicks in the editor text itself — the floating cards/popovers
|
||||
// are children of this same wrapper, and a click on one shouldn't resolve a
|
||||
// (hidden) word behind it.
|
||||
if (!(e.target as HTMLElement).closest('.petal-prose')) return
|
||||
const coords = editor.view.posAtCoords({ left: e.clientX, top: e.clientY })
|
||||
if (!coords) return
|
||||
const range = wordAt(editor.state.doc, coords.pos)
|
||||
if (!range) return
|
||||
const elRect = target.getBoundingClientRect()
|
||||
const wrapRect = wrapper.getBoundingClientRect()
|
||||
const cardWidth = 240
|
||||
const left = Math.max(0, Math.min(elRect.left - wrapRect.left, wrapper.clientWidth - cardWidth))
|
||||
const top = elRect.bottom - wrapRect.top + 6
|
||||
// Opening a spelling popover supersedes any AI-suggestion hover card.
|
||||
closeCard()
|
||||
setWordInfo(null)
|
||||
setMisspell({ ...range, suggestions: spellChecker.suggest(range.word), top, left })
|
||||
openMisspellAt(coords.pos)
|
||||
},
|
||||
[editor, spellChecker, closeCard, openCardFor, railEnabled],
|
||||
[editor, openMisspellAt, openCardFor, railEnabled],
|
||||
)
|
||||
|
||||
const replaceMisspelling = useCallback(
|
||||
@@ -757,8 +775,10 @@ export function EditorCore({
|
||||
.catch((err) => console.error('vocab save failed', err))
|
||||
}, [wordInfo, exampleAt, docId])
|
||||
|
||||
// Right-click a word to look it up. Right-clicking off any word falls through
|
||||
// to the native menu (so copy/paste-by-menu still works — see the Selection fix).
|
||||
// Right-click a misspelled word for spelling corrections (the familiar "did you
|
||||
// mean" gesture); right-click any other word to look it up. Right-clicking off
|
||||
// any word falls through to the native menu (so copy/paste-by-menu still works
|
||||
// — see the Selection fix).
|
||||
const handleContextMenu = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (!editor) return
|
||||
@@ -766,9 +786,11 @@ export function EditorCore({
|
||||
if (!coords) return
|
||||
if (!wordAt(editor.state.doc, coords.pos)) return
|
||||
e.preventDefault()
|
||||
// A misspelled word offers corrections first; otherwise look it up.
|
||||
if (openMisspellAt(coords.pos)) return
|
||||
openWordLookup(coords.pos)
|
||||
},
|
||||
[editor, openWordLookup],
|
||||
[editor, openMisspellAt, openWordLookup],
|
||||
)
|
||||
|
||||
// Touch has no hover or right-click, so a long-press (~500ms without moving)
|
||||
|
||||
Reference in New Issue
Block a user