fix: stop duplicate native quick prompt paste
This commit is contained in:
@@ -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<HTMLDivElement>
|
||||
dataTestId: string
|
||||
editorClassName?: string
|
||||
onInput: () => void
|
||||
onKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void
|
||||
onPaste: (event: React.ClipboardEvent<HTMLDivElement>) => void
|
||||
onSelectionChange: () => void
|
||||
segments: ReturnType<typeof buildInlineWikilinkSegments>
|
||||
typeEntryMap: Record<string, VaultEntry>
|
||||
}) {
|
||||
return (
|
||||
<InlineWikilinkEditorField
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
inputRef={inputRef}
|
||||
dataTestId={dataTestId}
|
||||
editorClassName={editorClassName}
|
||||
onInput={onInput}
|
||||
onKeyDown={onKeyDown}
|
||||
onPaste={onPaste}
|
||||
onSelectionChange={onSelectionChange}
|
||||
segments={segments}
|
||||
typeEntryMap={typeEntryMap}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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<PendingPasteState | null>(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<HTMLDivElement>) =>
|
||||
@@ -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 = (
|
||||
<InlineWikilinkEditorField
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
inputRef={setCombinedRef}
|
||||
dataTestId={dataTestId}
|
||||
editorClassName={editorClassName}
|
||||
onInput={handleInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPaste={handlePaste}
|
||||
onSelectionChange={syncSelectionRange}
|
||||
segments={segments}
|
||||
typeEntryMap={typeEntryMap}
|
||||
/>
|
||||
)
|
||||
const suggestionList = renderInlineSuggestionList({
|
||||
suggestions,
|
||||
selectedSuggestionIndex,
|
||||
|
||||
26
src/components/inlineWikilinkPasteRecovery.test.ts
Normal file
26
src/components/inlineWikilinkPasteRecovery.test.ts
Normal file
@@ -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)
|
||||
})
|
||||
})
|
||||
32
src/components/inlineWikilinkPasteRecovery.ts
Normal file
32
src/components/inlineWikilinkPasteRecovery.ts
Normal file
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -69,6 +69,7 @@ export function useInlineWikilinkSelection({
|
||||
}, [selectionRange, value])
|
||||
|
||||
return {
|
||||
editorRef,
|
||||
selectionRange,
|
||||
selectionIndex: selectionRange.end,
|
||||
setSelectionRange,
|
||||
|
||||
Reference in New Issue
Block a user