fix: enhance paste handling for mobile WebViews and large text

This commit is contained in:
denispol 2025-06-18 14:50:16 +02:00
parent 1547235d47
commit 6555cf9d7a

View File

@ -16,6 +16,7 @@
const eventDispatch = createEventDispatcher(); const eventDispatch = createEventDispatcher();
import { EditorState, Plugin, PluginKey, TextSelection } from 'prosemirror-state'; import { EditorState, Plugin, PluginKey, TextSelection } from 'prosemirror-state';
import { Fragment } from 'prosemirror-model';
import { Decoration, DecorationSet } from 'prosemirror-view'; import { Decoration, DecorationSet } from 'prosemirror-view';
import { Editor } from '@tiptap/core'; import { Editor } from '@tiptap/core';
@ -351,46 +352,72 @@
}, },
paste: (view, event) => { paste: (view, event) => {
if (event.clipboardData) { if (event.clipboardData) {
// Extract plain text from clipboard and paste it without formatting
const plainText = event.clipboardData.getData('text/plain'); const plainText = event.clipboardData.getData('text/plain');
if (plainText) { if (plainText) {
if (largeTextAsFile) { if (largeTextAsFile && plainText.length > PASTED_TEXT_CHARACTER_LIMIT) {
if (plainText.length > PASTED_TEXT_CHARACTER_LIMIT) { // Delegate handling of large text pastes to the parent component.
// Dispatch paste event to parent component eventDispatch('paste', { event });
eventDispatch('paste', { event }); event.preventDefault();
event.preventDefault(); return true;
return true;
}
} }
// Workaround for mobile WebViews that strip line breaks when pasting from
// clipboard suggestions (e.g., Gboard clipboard history).
const isMobile = /Android|iPhone|iPad|iPod|Windows Phone/i.test(
navigator.userAgent
);
const isWebView =
typeof window !== 'undefined' &&
(/wv/i.test(navigator.userAgent) || // Standard Android WebView flag
(navigator.userAgent.includes('Android') &&
!navigator.userAgent.includes('Chrome')) || // Other generic Android WebViews
(navigator.userAgent.includes('Safari') &&
!navigator.userAgent.includes('Version'))); // iOS WebView (in-app browsers)
if (isMobile && isWebView && plainText.includes('\n')) {
// Manually deconstruct the pasted text and insert it with hard breaks
// to preserve the multi-line formatting.
const { state, dispatch } = view;
const { from, to } = state.selection;
const lines = plainText.split('\n');
const nodes = [];
lines.forEach((line, index) => {
if (index > 0) {
nodes.push(state.schema.nodes.hardBreak.create());
}
if (line.length > 0) {
nodes.push(state.schema.text(line));
}
});
const fragment = Fragment.fromArray(nodes);
const tr = state.tr.replaceWith(from, to, fragment);
dispatch(tr.scrollIntoView());
event.preventDefault();
return true;
}
// Let ProseMirror handle normal text paste in non-problematic environments.
return false; return false;
} }
// Check if the pasted content contains image files // Delegate image paste handling to the parent component.
const hasImageFile = Array.from(event.clipboardData.files).some((file) => const hasImageFile = Array.from(event.clipboardData.files).some((file) =>
file.type.startsWith('image/') file.type.startsWith('image/')
); );
// Fallback for cases where an image is in dataTransfer.items but not clipboardData.files.
// Check for image in dataTransfer items (for cases where files are not available)
const hasImageItem = Array.from(event.clipboardData.items).some((item) => const hasImageItem = Array.from(event.clipboardData.items).some((item) =>
item.type.startsWith('image/') item.type.startsWith('image/')
); );
if (hasImageFile) { if (hasImageFile || hasImageItem) {
// If there's an image, dispatch the event to the parent
eventDispatch('paste', { event });
event.preventDefault();
return true;
}
if (hasImageItem) {
// If there's an image item, dispatch the event to the parent
eventDispatch('paste', { event }); eventDispatch('paste', { event });
event.preventDefault(); event.preventDefault();
return true; return true;
} }
} }
// For all other cases, let ProseMirror perform its default paste behavior.
// For all other cases (text, formatted text, etc.), let ProseMirror handle it view.dispatch(view.state.tr.scrollIntoView());
view.dispatch(view.state.tr.scrollIntoView()); // Move viewport to the cursor after pasting
return false; return false;
} }
} }