expo-paste-input 0.1.14 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/android/build/.transforms/04cfbef6d6438014a0f56f30321e9943/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/expo/modules/pasteinput/ExpoPasteInputView$1.dex +0 -0
  2. package/android/build/.transforms/04cfbef6d6438014a0f56f30321e9943/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/expo/modules/pasteinput/ExpoPasteInputView$ClipboardPayload.dex +0 -0
  3. package/android/build/.transforms/04cfbef6d6438014a0f56f30321e9943/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/expo/modules/pasteinput/ExpoPasteInputView$createActionModeCallback$1.dex +0 -0
  4. package/android/build/.transforms/04cfbef6d6438014a0f56f30321e9943/transformed/bundleLibRuntimeToDirDebug/bundleLibRuntimeToDirDebug_dex/expo/modules/pasteinput/ExpoPasteInputView.dex +0 -0
  5. package/android/build/.transforms/61052c0b101cc72f1be04cd5637e14e3/transformed/classes/classes_dex/classes.dex +0 -0
  6. package/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar +0 -0
  7. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/expo/modules/pasteinput/ExpoPasteInputView$1.class +0 -0
  8. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/expo/modules/pasteinput/ExpoPasteInputView$ClipboardPayload.class +0 -0
  9. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/expo/modules/pasteinput/ExpoPasteInputView$createActionModeCallback$1.class +0 -0
  10. package/android/build/intermediates/runtime_library_classes_dir/debug/bundleLibRuntimeToDirDebug/expo/modules/pasteinput/ExpoPasteInputView.class +0 -0
  11. package/android/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar +0 -0
  12. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab +0 -0
  13. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/inputs/source-to-output.tab.values.at +0 -0
  14. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab +0 -0
  15. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-attributes.tab.values.at +0 -0
  16. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab +0 -0
  17. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/class-fq-name-to-source.tab.values.at +0 -0
  18. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab +0 -0
  19. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/internal-name-to-source.tab.values.at +0 -0
  20. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab +0 -0
  21. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/proto.tab.values.at +0 -0
  22. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab +0 -0
  23. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/source-to-classes.tab.values.at +0 -0
  24. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab +0 -0
  25. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/subtypes.tab.values.at +0 -0
  26. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab +0 -0
  27. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/jvm/kotlin/supertypes.tab.values.at +0 -0
  28. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/counters.tab +1 -1
  29. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab +0 -0
  30. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/file-to-id.tab.values.at +0 -0
  31. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab +0 -0
  32. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream +0 -0
  33. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.keystream.len +0 -0
  34. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.len +0 -0
  35. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab.values.at +0 -0
  36. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/id-to-file.tab_i +0 -0
  37. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab +0 -0
  38. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream +0 -0
  39. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.keystream.len +0 -0
  40. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.len +0 -0
  41. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab.values.at +0 -0
  42. package/android/build/kotlin/compileDebugKotlin/cacheable/caches-jvm/lookups/lookups.tab_i +0 -0
  43. package/android/build/kotlin/compileDebugKotlin/cacheable/last-build.bin +0 -0
  44. package/android/build/kotlin/compileDebugKotlin/classpath-snapshot/shrunk-classpath-snapshot.bin +0 -0
  45. package/android/build/kotlin/compileDebugKotlin/local-state/build-history.bin +0 -0
  46. package/android/build/tmp/kotlin-classes/debug/expo/modules/pasteinput/ExpoPasteInputView$1.class +0 -0
  47. package/android/build/tmp/kotlin-classes/debug/expo/modules/pasteinput/ExpoPasteInputView$ClipboardPayload.class +0 -0
  48. package/android/build/tmp/kotlin-classes/debug/expo/modules/pasteinput/ExpoPasteInputView$createActionModeCallback$1.class +0 -0
  49. package/android/build/tmp/kotlin-classes/debug/expo/modules/pasteinput/ExpoPasteInputView.class +0 -0
  50. package/android/src/main/java/expo/modules/pasteinput/ExpoPasteInputView.kt +22 -69
  51. package/ios/ExpoPasteInputView.swift +49 -91
  52. package/package.json +1 -1
@@ -11,7 +11,6 @@ import android.view.View
11
11
  import android.view.ViewGroup
12
12
  import android.view.ActionMode
13
13
  import android.widget.EditText
14
- import kotlin.math.max
15
14
  import androidx.core.view.ContentInfoCompat
16
15
  import androidx.core.view.OnReceiveContentListener
17
16
  import androidx.core.view.ViewCompat
@@ -61,91 +60,45 @@ class ExpoPasteInputView(context: Context, appContext: AppContext) : ExpoView(co
61
60
  })
62
61
  }
63
62
 
64
- /**
65
- * Ensure this wrapper behaves like a normal container in React Native layouts.
66
- *
67
- * Without an explicit measure/layout pass-through, some parent layouts can end up
68
- * treating this view as having near-zero height, collapsing the wrapped EditText.
69
- */
70
63
  override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
71
- var maxChildWidth = 0
72
- var maxChildHeight = 0
73
- var childState = 0
64
+ // Let the wrapper resolve its own size first, then size children to the
65
+ // resolved content box so wrapped RN inputs can fill like a normal container.
66
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
67
+
68
+ val availableWidth = (measuredWidth - paddingLeft - paddingRight).coerceAtLeast(0)
69
+ val availableHeight = (measuredHeight - paddingTop - paddingBottom).coerceAtLeast(0)
70
+
71
+ val childWidthSpec = MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.EXACTLY)
72
+ val childHeightSpec = MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY)
74
73
 
75
74
  for (i in 0 until childCount) {
76
75
  val child = getChildAt(i)
77
- if (child.visibility == View.GONE) continue
78
-
79
- val lp = child.layoutParams
80
- if (lp is MarginLayoutParams) {
81
- measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0)
82
- maxChildWidth = max(maxChildWidth, child.measuredWidth + lp.leftMargin + lp.rightMargin)
83
- maxChildHeight = max(maxChildHeight, child.measuredHeight + lp.topMargin + lp.bottomMargin)
84
- } else {
85
- measureChild(child, widthMeasureSpec, heightMeasureSpec)
86
- maxChildWidth = max(maxChildWidth, child.measuredWidth)
87
- maxChildHeight = max(maxChildHeight, child.measuredHeight)
76
+ if (child.visibility != View.GONE) {
77
+ child.measure(childWidthSpec, childHeightSpec)
88
78
  }
89
-
90
- childState = View.combineMeasuredStates(childState, child.measuredState)
91
79
  }
92
-
93
- maxChildWidth += paddingLeft + paddingRight
94
- maxChildHeight += paddingTop + paddingBottom
95
-
96
- val measuredWidth = View.resolveSizeAndState(maxChildWidth, widthMeasureSpec, childState)
97
- val measuredHeight =
98
- View.resolveSizeAndState(maxChildHeight, heightMeasureSpec, childState shl View.MEASURED_HEIGHT_STATE_SHIFT)
99
-
100
- setMeasuredDimension(measuredWidth, measuredHeight)
101
80
  }
102
81
 
103
82
  override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
104
- val parentLeft = paddingLeft
105
- val parentTop = paddingTop
106
- val parentRight = r - l - paddingRight
83
+ val left = paddingLeft
84
+ val top = paddingTop
85
+ val right = (r - l - paddingRight).coerceAtLeast(left)
86
+ val bottom = (b - t - paddingBottom).coerceAtLeast(top)
107
87
 
108
88
  for (i in 0 until childCount) {
109
89
  val child = getChildAt(i)
110
- if (child.visibility == View.GONE) continue
111
-
112
- val lp = child.layoutParams
113
- val left: Int
114
- val top: Int
115
- val right: Int
116
- val bottom: Int
117
-
118
- if (lp is MarginLayoutParams) {
119
- left = parentLeft + lp.leftMargin
120
- top = parentTop + lp.topMargin
121
- right = (parentRight - lp.rightMargin).coerceAtLeast(left)
122
- bottom = (top + child.measuredHeight).coerceAtMost(b - t - paddingBottom - lp.bottomMargin)
123
- } else {
124
- left = parentLeft
125
- top = parentTop
126
- right = parentRight.coerceAtLeast(left)
127
- bottom = (top + child.measuredHeight).coerceAtMost(b - t - paddingBottom)
90
+ if (child.visibility != View.GONE) {
91
+ child.layout(left, top, right, bottom)
128
92
  }
129
-
130
- child.layout(left, top, right, bottom)
131
93
  }
132
94
  }
133
-
134
- // Pass through all touch events to children - never intercept
135
- override fun onInterceptTouchEvent(ev: android.view.MotionEvent?): Boolean {
136
- return false
137
- }
138
-
139
- override fun onTouchEvent(event: android.view.MotionEvent?): Boolean {
140
- return false
141
- }
142
-
143
- override fun dispatchTouchEvent(ev: android.view.MotionEvent?): Boolean {
144
- return super.dispatchTouchEvent(ev)
145
- }
146
-
95
+
147
96
  override fun onViewAdded(child: View?) {
148
97
  super.onViewAdded(child)
98
+ // Let default container sizing recalculate after RN inserts a child.
99
+ requestLayout()
100
+ invalidate()
101
+
149
102
  // Re-scan for text input when a new child is added
150
103
  if (!isMonitoring) {
151
104
  startMonitoring()
@@ -511,14 +511,17 @@ class ExpoPasteInputView: ExpoView {
511
511
  var attachmentRanges: [NSRange] = []
512
512
  var mediaPayloads: [MediaPayload] = []
513
513
 
514
+ // Only track ranges for attachments we successfully extract a real payload
515
+ // from. Attachments without a payload (e.g. iOS dictation placeholders)
516
+ // are left alone — sanitizing them would delete characters the system
517
+ // manages itself, and emitting "unsupported" would raise a spurious error.
514
518
  attributedText.enumerateAttribute(.attachment, in: NSRange(location: 0, length: attributedText.length), options: []) { value, range, _ in
515
519
  guard let attachment = value as? NSTextAttachment else {
516
520
  return
517
521
  }
518
522
 
519
- attachmentRanges.append(range)
520
-
521
523
  if let payload = self.extractMediaPayload(from: attachment, textView: textView, range: range) {
524
+ attachmentRanges.append(range)
522
525
  mediaPayloads.append(payload)
523
526
  }
524
527
  }
@@ -529,9 +532,8 @@ class ExpoPasteInputView: ExpoView {
529
532
  return
530
533
  }
531
534
 
532
- attachmentRanges.append(range)
533
-
534
535
  if let payload = self.extractMediaPayload(from: adaptiveGlyph) {
536
+ attachmentRanges.append(range)
535
537
  mediaPayloads.append(payload)
536
538
  }
537
539
  }
@@ -539,17 +541,12 @@ class ExpoPasteInputView: ExpoView {
539
541
 
540
542
  attachmentRanges = uniqueRanges(attachmentRanges)
541
543
 
542
- guard !attachmentRanges.isEmpty else {
544
+ guard !mediaPayloads.isEmpty else {
543
545
  return
544
546
  }
545
547
 
546
548
  sanitizeAttachments(in: textView, ranges: attachmentRanges)
547
549
 
548
- guard !mediaPayloads.isEmpty else {
549
- handleUnsupportedPaste()
550
- return
551
- }
552
-
553
550
  emitImagesAsync(for: mediaPayloads)
554
551
  }
555
552
 
@@ -651,6 +648,11 @@ class ExpoPasteInputView: ExpoView {
651
648
  }
652
649
 
653
650
  private func extractMediaPayload(from attachment: NSTextAttachment, textView: UITextView, range: NSRange) -> MediaPayload? {
651
+ // Only accept attachments that carry real image payloads. We intentionally
652
+ // do not fall back to `image(forBounds:)` or rendering the text view's
653
+ // hierarchy, because system-inserted attachments (e.g. the iOS dictation
654
+ // placeholder) draw themselves via those paths and would cause us to
655
+ // emit a screenshot of the composer as a "pasted image".
654
656
  if let fileWrapperData = attachment.fileWrapper?.regularFileContents,
655
657
  let payload = extractMediaPayload(fromData: fileWrapperData) {
656
658
  return payload
@@ -667,20 +669,6 @@ class ExpoPasteInputView: ExpoView {
667
669
  return .image(image)
668
670
  }
669
671
 
670
- let attachmentBounds = attachment.bounds.size.width > 0 && attachment.bounds.size.height > 0
671
- ? attachment.bounds
672
- : CGRect(origin: .zero, size: CGSize(width: 128, height: 128))
673
-
674
- if let image = attachment.image(forBounds: attachmentBounds, textContainer: textView.textContainer, characterIndex: range.location),
675
- image.size.width > 0,
676
- image.size.height > 0 {
677
- return .image(image)
678
- }
679
-
680
- if let renderedImage = renderTextAttachment(in: textView, range: range) {
681
- return .image(renderedImage)
682
- }
683
-
684
672
  return nil
685
673
  }
686
674
 
@@ -701,47 +689,6 @@ class ExpoPasteInputView: ExpoView {
701
689
  return .imageData(data)
702
690
  }
703
691
 
704
- private func renderTextAttachment(in textView: UITextView, range: NSRange) -> UIImage? {
705
- let glyphRange = textView.layoutManager.glyphRange(forCharacterRange: range, actualCharacterRange: nil)
706
- var rect = textView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textView.textContainer)
707
-
708
- rect.origin.x += textView.textContainerInset.left - textView.contentOffset.x
709
- rect.origin.y += textView.textContainerInset.top - textView.contentOffset.y
710
- rect = rect.integral
711
-
712
- guard rect.width > 1, rect.height > 1 else {
713
- return nil
714
- }
715
-
716
- let format = UIGraphicsImageRendererFormat.default()
717
- format.scale = textView.window?.screen.scale ?? UIScreen.main.scale
718
- format.opaque = false
719
-
720
- let image = UIGraphicsImageRenderer(size: rect.size, format: format).image { _ in
721
- let drawRect = CGRect(
722
- origin: CGPoint(x: -rect.origin.x, y: -rect.origin.y),
723
- size: textView.bounds.size
724
- )
725
-
726
- if textView.window != nil {
727
- textView.drawHierarchy(in: drawRect, afterScreenUpdates: false)
728
- } else {
729
- guard let context = UIGraphicsGetCurrentContext() else {
730
- return
731
- }
732
-
733
- context.translateBy(x: -rect.origin.x, y: -rect.origin.y)
734
- textView.layer.render(in: context)
735
- }
736
- }
737
-
738
- guard image.size.width > 0, image.size.height > 0 else {
739
- return nil
740
- }
741
-
742
- return image
743
- }
744
-
745
692
  @available(iOS 18.0, *)
746
693
  private func handleAdaptiveImageGlyphInsertion(_ adaptiveGlyph: NSAdaptiveImageGlyph) -> Bool {
747
694
  guard let payload = extractMediaPayload(from: adaptiveGlyph) else {
@@ -910,32 +857,32 @@ class ExpoPasteInputView: ExpoView {
910
857
  return headerBytes == gif87aSignature || headerBytes == gif89aSignature
911
858
  }
912
859
 
913
- /// Safely creates a UIImage from data, validating it first to prevent ImageIO errors
914
- private func safeCreateImage(from data: Data) -> UIImage? {
915
- guard data.count > 0 else { return nil }
916
-
917
- // Use ImageIO to validate the data before creating UIImage
918
- // This prevents ImageIO errors from corrupted or invalid image data
919
- guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil) else {
920
- return nil
921
- }
922
-
923
- // Check if the image source has at least one image
924
- guard CGImageSourceGetCount(imageSource) > 0 else {
925
- return nil
926
- }
927
-
928
- // Create UIImage from the original data so imageOrientation from metadata is preserved.
929
- guard let image = UIImage(data: data) else {
930
- return nil
860
+ private func inferredImageFileExtension(from imageSource: CGImageSource, fallbackData data: Data) -> String {
861
+ if let type = CGImageSourceGetType(imageSource) as String? {
862
+ switch type {
863
+ case "public.png":
864
+ return "png"
865
+ case "public.jpeg":
866
+ return "jpg"
867
+ case "public.gif":
868
+ return "gif"
869
+ case "public.webp", "org.webmproject.webp":
870
+ return "webp"
871
+ case "public.heic":
872
+ return "heic"
873
+ case "public.heif":
874
+ return "heif"
875
+ case "public.tiff":
876
+ return "tiff"
877
+ default:
878
+ break
879
+ }
931
880
  }
932
-
933
- // Validate the image has valid dimensions
934
- guard image.size.width > 0 && image.size.height > 0 else {
935
- return nil
881
+
882
+ if isGIFData(data) {
883
+ return "gif"
936
884
  }
937
-
938
- return image
885
+ return "png"
939
886
  }
940
887
 
941
888
  private func processTextPaste() {
@@ -1016,11 +963,22 @@ class ExpoPasteInputView: ExpoView {
1016
963
  }
1017
964
 
1018
965
  private func writeTemporaryImageData(_ data: Data) -> String? {
1019
- guard let image = safeCreateImage(from: data) else {
966
+ guard let imageSource = CGImageSourceCreateWithData(data as CFData, nil),
967
+ CGImageSourceGetCount(imageSource) > 0 else {
1020
968
  return nil
1021
969
  }
1022
970
 
1023
- return writeTemporaryImage(image)
971
+ let fileExtension = inferredImageFileExtension(from: imageSource, fallbackData: data)
972
+ let fileURL = FileManager.default.temporaryDirectory
973
+ .appendingPathComponent(UUID().uuidString)
974
+ .appendingPathExtension(fileExtension)
975
+
976
+ do {
977
+ try data.write(to: fileURL)
978
+ return fileURL.absoluteString
979
+ } catch {
980
+ return nil
981
+ }
1024
982
  }
1025
983
 
1026
984
  private func writeTemporaryImage(_ image: UIImage) -> String? {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-paste-input",
3
- "version": "0.1.14",
3
+ "version": "0.2.0",
4
4
  "description": "A wrapper around React Native TextInput to paste images and GIFs from the clipboard (iOS, Android, Web)",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",