Przeglądaj źródła

Merge pull request #15119 from denispol/dev

fix: Preserve line breaks on multi-line paste in mobile WebViews
Tim Jaeryang Baek 3 miesięcy temu
rodzic
commit
7cb105abd4
1 zmienionych plików z 50 dodań i 23 usunięć
  1. 50 23
      src/lib/components/common/RichTextInput.svelte

+ 50 - 23
src/lib/components/common/RichTextInput.svelte

@@ -18,6 +18,7 @@
 
 	import { Fragment } from 'prosemirror-model';
 	import { EditorState, Plugin, PluginKey, TextSelection } from 'prosemirror-state';
+	import { Fragment } from 'prosemirror-model';
 	import { Decoration, DecorationSet } from 'prosemirror-view';
 	import { Editor } from '@tiptap/core';
 
@@ -532,46 +533,72 @@
 					},
 					paste: (view, event) => {
 						if (event.clipboardData) {
-							// Extract plain text from clipboard and paste it without formatting
 							const plainText = event.clipboardData.getData('text/plain');
 							if (plainText) {
-								if (largeTextAsFile) {
-									if (plainText.length > PASTED_TEXT_CHARACTER_LIMIT) {
-										// Dispatch paste event to parent component
-										eventDispatch('paste', { event });
-										event.preventDefault();
-										return true;
-									}
+								if (largeTextAsFile && plainText.length > PASTED_TEXT_CHARACTER_LIMIT) {
+									// Delegate handling of large text pastes to the parent component.
+									eventDispatch('paste', { event });
+									event.preventDefault();
+									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;
 							}
 
-							// 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) =>
 								file.type.startsWith('image/')
 							);
-
-							// Check for image in dataTransfer items (for cases where files are not available)
+							// Fallback for cases where an image is in dataTransfer.items but not clipboardData.files.
 							const hasImageItem = Array.from(event.clipboardData.items).some((item) =>
 								item.type.startsWith('image/')
 							);
-							if (hasImageFile) {
-								// 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
+							if (hasImageFile || hasImageItem) {
 								eventDispatch('paste', { event });
 								event.preventDefault();
 								return true;
 							}
 						}
-
-						// For all other cases (text, formatted text, etc.), let ProseMirror handle it
-						view.dispatch(view.state.tr.scrollIntoView()); // Move viewport to the cursor after pasting
+						// For all other cases, let ProseMirror perform its default paste behavior.
+						view.dispatch(view.state.tr.scrollIntoView());
 						return false;
 					}
 				}