|
|
|
@@ -580,9 +580,37 @@ export function EditorCore({
|
|
|
|
[onDismiss, closeCard],
|
|
|
|
[onDismiss, closeCard],
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// Click a red-underlined word to open its spelling popover, anchored under the
|
|
|
|
// openMisspellAt resolves the word at a document position and, if the checker
|
|
|
|
// word. posAtCoords→wordAt resolves the exact PM span (robust to the same
|
|
|
|
// flags it as misspelled, opens the spelling popover anchored under the word.
|
|
|
|
// misspelling appearing elsewhere); nspell supplies the corrections.
|
|
|
|
// 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
|
|
|
|
// 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).
|
|
|
|
// no hover, so the tap is the only way in (mouse users still get hover).
|
|
|
|
@@ -603,26 +631,16 @@ export function EditorCore({
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!editor || !spellChecker) return
|
|
|
|
if (!editor) return
|
|
|
|
const target = (e.target as HTMLElement).closest('.petal-misspelling') as HTMLElement | null
|
|
|
|
// Only act on clicks in the editor text itself — the floating cards/popovers
|
|
|
|
if (!target) return
|
|
|
|
// are children of this same wrapper, and a click on one shouldn't resolve a
|
|
|
|
const wrapper = wrapperRef.current
|
|
|
|
// (hidden) word behind it.
|
|
|
|
if (!wrapper) return
|
|
|
|
if (!(e.target as HTMLElement).closest('.petal-prose')) return
|
|
|
|
const coords = editor.view.posAtCoords({ left: e.clientX, top: e.clientY })
|
|
|
|
const coords = editor.view.posAtCoords({ left: e.clientX, top: e.clientY })
|
|
|
|
if (!coords) return
|
|
|
|
if (!coords) return
|
|
|
|
const range = wordAt(editor.state.doc, coords.pos)
|
|
|
|
openMisspellAt(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 })
|
|
|
|
|
|
|
|
},
|
|
|
|
},
|
|
|
|
[editor, spellChecker, closeCard, openCardFor, railEnabled],
|
|
|
|
[editor, openMisspellAt, openCardFor, railEnabled],
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
const replaceMisspelling = useCallback(
|
|
|
|
const replaceMisspelling = useCallback(
|
|
|
|
@@ -757,8 +775,10 @@ export function EditorCore({
|
|
|
|
.catch((err) => console.error('vocab save failed', err))
|
|
|
|
.catch((err) => console.error('vocab save failed', err))
|
|
|
|
}, [wordInfo, exampleAt, docId])
|
|
|
|
}, [wordInfo, exampleAt, docId])
|
|
|
|
|
|
|
|
|
|
|
|
// Right-click a word to look it up. Right-clicking off any word falls through
|
|
|
|
// Right-click a misspelled word for spelling corrections (the familiar "did you
|
|
|
|
// to the native menu (so copy/paste-by-menu still works — see the Selection fix).
|
|
|
|
// 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(
|
|
|
|
const handleContextMenu = useCallback(
|
|
|
|
(e: React.MouseEvent) => {
|
|
|
|
(e: React.MouseEvent) => {
|
|
|
|
if (!editor) return
|
|
|
|
if (!editor) return
|
|
|
|
@@ -766,9 +786,11 @@ export function EditorCore({
|
|
|
|
if (!coords) return
|
|
|
|
if (!coords) return
|
|
|
|
if (!wordAt(editor.state.doc, coords.pos)) return
|
|
|
|
if (!wordAt(editor.state.doc, coords.pos)) return
|
|
|
|
e.preventDefault()
|
|
|
|
e.preventDefault()
|
|
|
|
|
|
|
|
// A misspelled word offers corrections first; otherwise look it up.
|
|
|
|
|
|
|
|
if (openMisspellAt(coords.pos)) return
|
|
|
|
openWordLookup(coords.pos)
|
|
|
|
openWordLookup(coords.pos)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
[editor, openWordLookup],
|
|
|
|
[editor, openMisspellAt, openWordLookup],
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// Touch has no hover or right-click, so a long-press (~500ms without moving)
|
|
|
|
// Touch has no hover or right-click, so a long-press (~500ms without moving)
|
|
|
|
|