diff --git a/.changeset/rude-bikes-rule.md b/.changeset/rude-bikes-rule.md new file mode 100644 index 000000000..07513fac3 --- /dev/null +++ b/.changeset/rude-bikes-rule.md @@ -0,0 +1,5 @@ +--- +'slate-react': patch +--- + +Fix an issue on Android where content containing a newline wouldn't be pasted properly diff --git a/packages/slate-react/src/hooks/android-input-manager/android-input-manager.ts b/packages/slate-react/src/hooks/android-input-manager/android-input-manager.ts index 37cf8ba30..b58f26cb4 100644 --- a/packages/slate-react/src/hooks/android-input-manager/android-input-manager.ts +++ b/packages/slate-react/src/hooks/android-input-manager/android-input-manager.ts @@ -36,6 +36,10 @@ const FLUSH_DELAY = 200 // Replace with `const debug = console.log` to debug const debug = (..._: unknown[]) => {} +// Type guard to check if a value is a DataTransfer +const isDataTransfer = (value: any): value is DataTransfer => + value?.constructor.name === 'DataTransfer' + export type CreateAndroidInputManagerOptions = { editor: ReactEditor @@ -343,7 +347,8 @@ export function createAndroidInputManager({ const { inputType: type } = event let targetRange: Range | null = null - const data = (event as any).dataTransfer || event.data || undefined + const data: DataTransfer | string | undefined = + (event as any).dataTransfer || event.data || undefined if ( insertPositionHint !== false && @@ -577,18 +582,12 @@ export function createAndroidInputManager({ case 'insertFromYank': case 'insertReplacementText': case 'insertText': { - if (data?.constructor.name === 'DataTransfer') { + if (isDataTransfer(data)) { return scheduleAction(() => ReactEditor.insertData(editor, data), { at: targetRange, }) } - if (typeof data === 'string' && data.includes('\n')) { - return scheduleAction(() => Editor.insertSoftBreak(editor), { - at: Range.end(targetRange), - }) - } - let text = data ?? '' // COMPAT: If we are writing inside a placeholder, the ime inserts the text inside @@ -597,6 +596,34 @@ export function createAndroidInputManager({ text = text.replace('\uFEFF', '') } + // Pastes from the Android clipboard will generate `insertText` events. + // If the copied text contains any newlines, Android will append an + // extra newline to the end of the copied text. + if (type === 'insertText' && /.*\n.*\n$/.test(text)) { + text = text.slice(0, -1) + } + + // If the text includes a newline, split it at newlines and paste each component + // string, with soft breaks in between each. + if (text.includes('\n')) { + return scheduleAction( + () => { + const parts = text.split('\n') + parts.forEach((line, i) => { + if (line) { + Editor.insertText(editor, line) + } + if (i !== parts.length - 1) { + Editor.insertSoftBreak(editor) + } + }) + }, + { + at: targetRange, + } + ) + } + if (Path.equals(targetRange.anchor.path, targetRange.focus.path)) { const [start, end] = Range.edges(targetRange)