diff --git a/src/components/InlineWikilinkInput.tsx b/src/components/InlineWikilinkInput.tsx index 02f27a93..b358d507 100644 --- a/src/components/InlineWikilinkInput.tsx +++ b/src/components/InlineWikilinkInput.tsx @@ -1,5 +1,7 @@ import { useMemo, + useRef, + useState, type ReactNode, } from 'react' import type { VaultEntry } from '../types' @@ -14,6 +16,12 @@ import { extractInlineWikilinkReferences, findActiveWikilinkQuery, } from './inlineWikilinkText' +import { serializeInlineNode } from './inlineWikilinkDom' +import { + buildPendingPasteState, + type PendingPasteState, + shouldRecoverPendingPaste, +} from './inlineWikilinkPasteRecovery' import { InlineWikilinkEditorField, InlineWikilinkPaletteLayout, @@ -48,7 +56,6 @@ function collapseSelectionRange(nextSelectionIndex: number) { end: nextSelectionIndex, } } - function submitInlineValue({ onSubmit, submitOnEmpty, @@ -66,51 +73,6 @@ function submitInlineValue({ onSubmit(normalizedValue, references) } -function renderInlineEditorField({ - value, - placeholder, - disabled, - inputRef, - dataTestId, - editorClassName, - onInput, - onKeyDown, - onPaste, - onSelectionChange, - segments, - typeEntryMap, -}: { - value: string - placeholder?: string - disabled: boolean - inputRef: React.Ref - dataTestId: string - editorClassName?: string - onInput: () => void - onKeyDown: (event: React.KeyboardEvent) => void - onPaste: (event: React.ClipboardEvent) => void - onSelectionChange: () => void - segments: ReturnType - typeEntryMap: Record -}) { - return ( - - ) -} - function renderInlineSuggestionList({ suggestions, selectedSuggestionIndex, @@ -160,12 +122,14 @@ export function InlineWikilinkInput({ paletteEmptyState, paletteFooter, }: InlineWikilinkInputProps) { + const [, forceRender] = useState(0) const segments = useMemo( () => buildInlineWikilinkSegments(value, entries), [entries, value], ) const typeEntryMap = useMemo(() => buildTypeEntryMap(entries), [entries]) const { + editorRef, selectionRange, selectionIndex, setSelectionRange, @@ -178,6 +142,7 @@ export function InlineWikilinkInput({ onChange, inputRef, }) + const pendingPasteRef = useRef(null) const activeQuery = useMemo( () => selectionRange.start === selectionRange.end ? findActiveWikilinkQuery(value, selectionIndex) @@ -218,11 +183,30 @@ export function InlineWikilinkInput({ const pastedText = normalizeInlineWikilinkValue(event.clipboardData.getData('text/plain')) if (!pastedText) return - event.preventDefault() const nextState = replaceInlineSelection(value, selectionRange, pastedText) + pendingPasteRef.current = buildPendingPasteState(value, selectionRange, pastedText) + + event.preventDefault() onChange(nextState.value) setSelectionRange(nextState.selection) } + const handleInput = () => { + const editor = editorRef.current + const pendingPaste = pendingPasteRef.current + if (editor && pendingPaste) { + const nextValue = normalizeInlineWikilinkValue(serializeInlineNode(editor)) + pendingPasteRef.current = null + + if (shouldRecoverPendingPaste(nextValue, pendingPaste)) { + onChange(pendingPaste.expectedValue) + forceRender((current) => current + 1) + setSelectionRange({ ...pendingPaste.expectedSelection }) + return + } + } + + commitValueFromEditor() + } const submitValue = () => submitInlineValue({ onSubmit, submitOnEmpty, value, references }) const handleKeyDown = (event: React.KeyboardEvent) => @@ -237,20 +221,22 @@ export function InlineWikilinkInput({ canSubmit: onSubmit !== undefined, onSubmit: submitValue, }) - const editor = renderInlineEditorField({ - value, - placeholder, - disabled, - inputRef: setCombinedRef, - dataTestId, - editorClassName, - onInput: commitValueFromEditor, - onKeyDown: handleKeyDown, - onPaste: handlePaste, - onSelectionChange: syncSelectionRange, - segments, - typeEntryMap, - }) + const editor = ( + + ) const suggestionList = renderInlineSuggestionList({ suggestions, selectedSuggestionIndex, diff --git a/src/components/inlineWikilinkPasteRecovery.test.ts b/src/components/inlineWikilinkPasteRecovery.test.ts new file mode 100644 index 00000000..ae15871a --- /dev/null +++ b/src/components/inlineWikilinkPasteRecovery.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from 'vitest' +import { + buildPendingPasteState, + shouldRecoverPendingPaste, +} from './inlineWikilinkPasteRecovery' + +describe('inlineWikilinkPasteRecovery', () => { + it('recognizes the expected post-paste value as recoverable', () => { + const pendingPaste = buildPendingPasteState(' ', { start: 1, end: 1 }, 'hello world') + + expect(pendingPaste).toEqual({ + duplicatedValue: ' hello worldhello world', + expectedValue: ' hello world', + expectedSelection: { start: 12, end: 12 }, + }) + expect(shouldRecoverPendingPaste(' hello world', pendingPaste)).toBe(true) + }) + + it('recognizes the duplicate native replay as recoverable when pasting into the middle of text', () => { + const pendingPaste = buildPendingPasteState('prefix suffix', { start: 7, end: 7 }, 'hello ') + + expect(pendingPaste.duplicatedValue).toBe('prefix hello hello suffix') + expect(shouldRecoverPendingPaste('prefix hello hello suffix', pendingPaste)).toBe(true) + expect(shouldRecoverPendingPaste('prefix hello suffix!', pendingPaste)).toBe(false) + }) +}) diff --git a/src/components/inlineWikilinkPasteRecovery.ts b/src/components/inlineWikilinkPasteRecovery.ts new file mode 100644 index 00000000..09a4a23c --- /dev/null +++ b/src/components/inlineWikilinkPasteRecovery.ts @@ -0,0 +1,32 @@ +import type { InlineSelectionRange } from './inlineWikilinkDom' +import { replaceInlineSelection } from './inlineWikilinkEdits' + +export interface PendingPasteState { + duplicatedValue: string + expectedValue: string + expectedSelection: InlineSelectionRange +} + +export function buildPendingPasteState( + value: string, + selectionRange: InlineSelectionRange, + pastedText: string, +): PendingPasteState { + const nextState = replaceInlineSelection(value, selectionRange, pastedText) + + return { + duplicatedValue: replaceInlineSelection(nextState.value, nextState.selection, pastedText).value, + expectedValue: nextState.value, + expectedSelection: nextState.selection, + } +} + +export function shouldRecoverPendingPaste( + nextValue: string, + pendingPaste: PendingPasteState, +): boolean { + return ( + nextValue === pendingPaste.expectedValue || + nextValue === pendingPaste.duplicatedValue + ) +} diff --git a/src/components/useInlineWikilinkSelection.ts b/src/components/useInlineWikilinkSelection.ts index 80ac0e45..d93ee0c5 100644 --- a/src/components/useInlineWikilinkSelection.ts +++ b/src/components/useInlineWikilinkSelection.ts @@ -69,6 +69,7 @@ export function useInlineWikilinkSelection({ }, [selectionRange, value]) return { + editorRef, selectionRange, selectionIndex: selectionRange.end, setSelectionRange,