react-native-enriched-markdown-scaffold 0.7.4-scaffold.10 → 0.7.4-scaffold.11
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.
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerDelegate.java +6 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextInputManagerInterface.java +2 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +10 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +7 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +5 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +1 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputManager.kt +18 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt +119 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/events/OnPasteEvent.kt +24 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputEventEmitter.kt +8 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/input/PasteTextUtils.kt +20 -0
- package/docs/API_REFERENCE.md +34 -0
- package/docs/INPUT.md +39 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.cpp +10 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/EventEmitters.h +7 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.cpp +5 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/Props.h +1 -0
- package/ios/generated/ReactCodegen/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +29 -0
- package/ios/input/ENRMInputTextView.mm +11 -1
- package/ios/input/EnrichedMarkdownTextInput.h +5 -0
- package/ios/input/EnrichedMarkdownTextInput.mm +33 -0
- package/lib/module/EnrichedMarkdownTextInput.js +15 -0
- package/lib/module/EnrichedMarkdownTextInput.js.map +1 -1
- package/lib/module/EnrichedMarkdownTextInputNativeComponent.ts +22 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts +24 -1
- package/lib/typescript/src/EnrichedMarkdownTextInput.d.ts.map +1 -1
- package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts +16 -0
- package/lib/typescript/src/EnrichedMarkdownTextInputNativeComponent.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/types/events.d.ts +15 -0
- package/lib/typescript/src/types/events.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/EnrichedMarkdownTextInput.tsx +37 -0
- package/src/EnrichedMarkdownTextInputNativeComponent.ts +22 -0
- package/src/index.tsx +1 -0
- package/src/types/events.ts +16 -0
|
@@ -50,6 +50,9 @@ public class EnrichedMarkdownTextInputManagerDelegate<T extends View, U extends
|
|
|
50
50
|
case "autoCapitalize":
|
|
51
51
|
mViewManager.setAutoCapitalize(view, value == null ? null : (String) value);
|
|
52
52
|
break;
|
|
53
|
+
case "interceptPaste":
|
|
54
|
+
mViewManager.setInterceptPaste(view, value == null ? false : (boolean) value);
|
|
55
|
+
break;
|
|
53
56
|
case "multiline":
|
|
54
57
|
mViewManager.setMultiline(view, value == null ? false : (boolean) value);
|
|
55
58
|
break;
|
|
@@ -160,6 +163,9 @@ public class EnrichedMarkdownTextInputManagerDelegate<T extends View, U extends
|
|
|
160
163
|
case "copyToClipboard":
|
|
161
164
|
mViewManager.copyToClipboard(view);
|
|
162
165
|
break;
|
|
166
|
+
case "resolvePaste":
|
|
167
|
+
mViewManager.resolvePaste(view, args.getString(0), args.getString(1));
|
|
168
|
+
break;
|
|
163
169
|
case "requestMarkdown":
|
|
164
170
|
mViewManager.requestMarkdown(view, args.getInt(0));
|
|
165
171
|
break;
|
|
@@ -24,6 +24,7 @@ public interface EnrichedMarkdownTextInputManagerInterface<T extends View> exten
|
|
|
24
24
|
void setScrollEnabled(T view, boolean value);
|
|
25
25
|
void setBlockSyntax(T view, boolean value);
|
|
26
26
|
void setAutoCapitalize(T view, @Nullable String value);
|
|
27
|
+
void setInterceptPaste(T view, boolean value);
|
|
27
28
|
void setMultiline(T view, boolean value);
|
|
28
29
|
void setCursorColor(T view, @Nullable Integer value);
|
|
29
30
|
void setSelectionColor(T view, @Nullable Integer value);
|
|
@@ -58,6 +59,7 @@ public interface EnrichedMarkdownTextInputManagerInterface<T extends View> exten
|
|
|
58
59
|
void updateMath(T view, int start, String latex, boolean displayMode);
|
|
59
60
|
void removeMath(T view, int start);
|
|
60
61
|
void copyToClipboard(T view);
|
|
62
|
+
void resolvePaste(T view, String text, String markdown);
|
|
61
63
|
void requestMarkdown(T view, int requestId);
|
|
62
64
|
void requestCaretRect(T view, int requestId);
|
|
63
65
|
}
|
package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp
CHANGED
|
@@ -247,6 +247,16 @@ payload.setProperty(runtime, "height", event.height);
|
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
|
|
250
|
+
void EnrichedMarkdownTextInputEventEmitter::onPaste(OnPaste event) const {
|
|
251
|
+
dispatchEvent("paste", [event=std::move(event)](jsi::Runtime &runtime) {
|
|
252
|
+
auto payload = jsi::Object(runtime);
|
|
253
|
+
payload.setProperty(runtime, "text", event.text);
|
|
254
|
+
payload.setProperty(runtime, "markdown", event.markdown);
|
|
255
|
+
return payload;
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
250
260
|
void EnrichedMarkdownTextInputEventEmitter::onStartMention(OnStartMention event) const {
|
|
251
261
|
dispatchEvent("startMention", [event=std::move(event)](jsi::Runtime &runtime) {
|
|
252
262
|
auto payload = jsi::Object(runtime);
|
package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h
CHANGED
|
@@ -182,6 +182,11 @@ class EnrichedMarkdownTextInputEventEmitter : public ViewEventEmitter {
|
|
|
182
182
|
double height;
|
|
183
183
|
};
|
|
184
184
|
|
|
185
|
+
struct OnPaste {
|
|
186
|
+
std::string text;
|
|
187
|
+
std::string markdown;
|
|
188
|
+
};
|
|
189
|
+
|
|
185
190
|
struct OnStartMention {
|
|
186
191
|
std::string indicator;
|
|
187
192
|
};
|
|
@@ -218,6 +223,8 @@ class EnrichedMarkdownTextInputEventEmitter : public ViewEventEmitter {
|
|
|
218
223
|
|
|
219
224
|
void onMathPress(OnMathPress value) const;
|
|
220
225
|
|
|
226
|
+
void onPaste(OnPaste value) const;
|
|
227
|
+
|
|
221
228
|
void onStartMention(OnStartMention value) const;
|
|
222
229
|
|
|
223
230
|
void onChangeMention(OnChangeMention value) const;
|
|
@@ -146,6 +146,7 @@ EnrichedMarkdownTextInputProps::EnrichedMarkdownTextInputProps(
|
|
|
146
146
|
scrollEnabled(convertRawProp(context, rawProps, "scrollEnabled", sourceProps.scrollEnabled, {false})),
|
|
147
147
|
blockSyntax(convertRawProp(context, rawProps, "blockSyntax", sourceProps.blockSyntax, {false})),
|
|
148
148
|
autoCapitalize(convertRawProp(context, rawProps, "autoCapitalize", sourceProps.autoCapitalize, {})),
|
|
149
|
+
interceptPaste(convertRawProp(context, rawProps, "interceptPaste", sourceProps.interceptPaste, {false})),
|
|
149
150
|
multiline(convertRawProp(context, rawProps, "multiline", sourceProps.multiline, {false})),
|
|
150
151
|
cursorColor(convertRawProp(context, rawProps, "cursorColor", sourceProps.cursorColor, {})),
|
|
151
152
|
selectionColor(convertRawProp(context, rawProps, "selectionColor", sourceProps.selectionColor, {})),
|
|
@@ -211,6 +212,10 @@ folly::dynamic EnrichedMarkdownTextInputProps::getDiffProps(
|
|
|
211
212
|
result["autoCapitalize"] = autoCapitalize;
|
|
212
213
|
}
|
|
213
214
|
|
|
215
|
+
if (interceptPaste != oldProps->interceptPaste) {
|
|
216
|
+
result["interceptPaste"] = interceptPaste;
|
|
217
|
+
}
|
|
218
|
+
|
|
214
219
|
if (multiline != oldProps->multiline) {
|
|
215
220
|
result["multiline"] = multiline;
|
|
216
221
|
}
|
|
@@ -3361,6 +3361,7 @@ class EnrichedMarkdownTextInputProps final : public ViewProps {
|
|
|
3361
3361
|
bool scrollEnabled{false};
|
|
3362
3362
|
bool blockSyntax{false};
|
|
3363
3363
|
std::string autoCapitalize{};
|
|
3364
|
+
bool interceptPaste{false};
|
|
3364
3365
|
bool multiline{false};
|
|
3365
3366
|
SharedColor cursorColor{};
|
|
3366
3367
|
SharedColor selectionColor{};
|
|
@@ -27,6 +27,7 @@ import com.swmansion.enriched.markdown.input.events.OnInputBlurEvent
|
|
|
27
27
|
import com.swmansion.enriched.markdown.input.events.OnInputFocusEvent
|
|
28
28
|
import com.swmansion.enriched.markdown.input.events.OnLinkDetectedEvent
|
|
29
29
|
import com.swmansion.enriched.markdown.input.events.OnMathPressEvent
|
|
30
|
+
import com.swmansion.enriched.markdown.input.events.OnPasteEvent
|
|
30
31
|
import com.swmansion.enriched.markdown.input.events.OnRequestCaretRectResultEvent
|
|
31
32
|
import com.swmansion.enriched.markdown.input.events.OnRequestMarkdownResultEvent
|
|
32
33
|
import com.swmansion.enriched.markdown.input.events.OnStartMentionEvent
|
|
@@ -112,6 +113,7 @@ class EnrichedMarkdownTextInputManager :
|
|
|
112
113
|
OnContextMenuItemPressEvent.EVENT_NAME,
|
|
113
114
|
OnLinkDetectedEvent.EVENT_NAME,
|
|
114
115
|
OnMathPressEvent.EVENT_NAME,
|
|
116
|
+
OnPasteEvent.EVENT_NAME,
|
|
115
117
|
OnStartMentionEvent.EVENT_NAME,
|
|
116
118
|
OnChangeMentionEvent.EVENT_NAME,
|
|
117
119
|
OnEndMentionEvent.EVENT_NAME,
|
|
@@ -186,6 +188,14 @@ class EnrichedMarkdownTextInputManager :
|
|
|
186
188
|
view?.setAutoCapitalize(value)
|
|
187
189
|
}
|
|
188
190
|
|
|
191
|
+
@ReactProp(name = "interceptPaste", defaultBoolean = false)
|
|
192
|
+
override fun setInterceptPaste(
|
|
193
|
+
view: EnrichedMarkdownTextInputView?,
|
|
194
|
+
value: Boolean,
|
|
195
|
+
) {
|
|
196
|
+
view?.interceptPaste = value
|
|
197
|
+
}
|
|
198
|
+
|
|
189
199
|
@ReactProp(name = "multiline", defaultBoolean = true)
|
|
190
200
|
override fun setMultiline(
|
|
191
201
|
view: EnrichedMarkdownTextInputView?,
|
|
@@ -517,6 +527,14 @@ class EnrichedMarkdownTextInputManager :
|
|
|
517
527
|
view?.copyToClipboard()
|
|
518
528
|
}
|
|
519
529
|
|
|
530
|
+
override fun resolvePaste(
|
|
531
|
+
view: EnrichedMarkdownTextInputView?,
|
|
532
|
+
text: String?,
|
|
533
|
+
markdown: String?,
|
|
534
|
+
) {
|
|
535
|
+
view?.resolvePaste(text ?: "", markdown ?: "")
|
|
536
|
+
}
|
|
537
|
+
|
|
520
538
|
override fun requestMarkdown(
|
|
521
539
|
view: EnrichedMarkdownTextInputView?,
|
|
522
540
|
requestId: Int,
|
package/android/src/main/java/com/swmansion/enriched/markdown/input/EnrichedMarkdownTextInputView.kt
CHANGED
|
@@ -20,6 +20,9 @@ import android.view.inputmethod.EditorInfo
|
|
|
20
20
|
import android.view.inputmethod.InputConnection
|
|
21
21
|
import android.view.inputmethod.InputMethodManager
|
|
22
22
|
import androidx.appcompat.widget.AppCompatEditText
|
|
23
|
+
import androidx.core.view.ContentInfoCompat
|
|
24
|
+
import androidx.core.view.OnReceiveContentListener
|
|
25
|
+
import androidx.core.view.ViewCompat
|
|
23
26
|
import com.facebook.react.common.ReactConstants
|
|
24
27
|
import com.facebook.react.uimanager.BackgroundStyleApplicator
|
|
25
28
|
import com.facebook.react.uimanager.PixelUtil
|
|
@@ -44,6 +47,7 @@ import com.swmansion.enriched.markdown.input.model.MathChipStyle
|
|
|
44
47
|
import com.swmansion.enriched.markdown.input.model.StyleType
|
|
45
48
|
import com.swmansion.enriched.markdown.input.toolbar.InputContextMenu
|
|
46
49
|
import com.swmansion.enriched.markdown.utils.input.AutoCapitalizeUtils
|
|
50
|
+
import com.swmansion.enriched.markdown.utils.input.PasteTextUtils
|
|
47
51
|
import kotlin.math.ceil
|
|
48
52
|
|
|
49
53
|
private fun Char.isLineBreak(): Boolean = this == '\n' || this == '\r' || this == '\u0085' || this == '\u2028' || this == '\u2029'
|
|
@@ -97,6 +101,25 @@ class EnrichedMarkdownTextInputView(
|
|
|
97
101
|
private var detectScrollMovement = false
|
|
98
102
|
var scrollEnabled: Boolean = true
|
|
99
103
|
|
|
104
|
+
// When on, a paste is reported to JS instead of inserted. Native never touches
|
|
105
|
+
// the buffer here (see resolvePaste): if JS dies mid-handler the paste is a
|
|
106
|
+
// visible no-op rather than a selection that silently vanished.
|
|
107
|
+
//
|
|
108
|
+
// The listener is installed only while the prop is on. Registering it
|
|
109
|
+
// unconditionally would route every consumer's paste through AppCompat's
|
|
110
|
+
// receive-content path even with the feature off, which is a behavior change
|
|
111
|
+
// nobody asked for.
|
|
112
|
+
var interceptPaste: Boolean = false
|
|
113
|
+
set(value) {
|
|
114
|
+
if (field == value) return
|
|
115
|
+
field = value
|
|
116
|
+
ViewCompat.setOnReceiveContentListener(
|
|
117
|
+
this,
|
|
118
|
+
if (value) PASTE_MIME_TYPES else null,
|
|
119
|
+
if (value) receiveContentListener else null,
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
100
123
|
// The markdown last handed over by JS, kept so `blockSyntax` can re-parse it. @ReactProp
|
|
101
124
|
// setters fire in undefined order, so `defaultValue` regularly lands before the flag does.
|
|
102
125
|
private var lastMarkdownFromJS: String? = null
|
|
@@ -488,6 +511,98 @@ class EnrichedMarkdownTextInputView(
|
|
|
488
511
|
clipboard.setPrimaryClip(ClipData.newPlainText(null, content))
|
|
489
512
|
}
|
|
490
513
|
|
|
514
|
+
// Paste arrives by two different routes and only the first one is a menu.
|
|
515
|
+
// `onTextContextMenuItem` covers the context menu and the hardware Ctrl+V /
|
|
516
|
+
// KEYCODE_PASTE shortcuts; drag-and-drop, autofill and IME `commitContent`
|
|
517
|
+
// reach the view through `onReceiveContent` instead. Hooking only one of them
|
|
518
|
+
// leaves the other pasting straight into the buffer with nobody intercepting.
|
|
519
|
+
override fun onTextContextMenuItem(id: Int): Boolean {
|
|
520
|
+
val isPaste = id == android.R.id.paste || id == android.R.id.pasteAsPlainText
|
|
521
|
+
if (isPaste && interceptPaste) {
|
|
522
|
+
val text = readClipboardText()
|
|
523
|
+
if (text.isNotEmpty()) {
|
|
524
|
+
eventEmitter.emitPaste(text, "")
|
|
525
|
+
// Stops super, which is what keeps this single-emit: AppCompat routes
|
|
526
|
+
// its own paste handling through the receive-content listener below.
|
|
527
|
+
return true
|
|
528
|
+
}
|
|
529
|
+
// Nothing textual in the clip: hand it to the platform, matching what the
|
|
530
|
+
// listener does with the items it does not claim. Both routes then decide
|
|
531
|
+
// from the same rule, so a clip never behaves differently depending on
|
|
532
|
+
// whether it came from the menu or from Ctrl+V.
|
|
533
|
+
}
|
|
534
|
+
return super.onTextContextMenuItem(id)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
private val receiveContentListener =
|
|
538
|
+
OnReceiveContentListener { _, payload ->
|
|
539
|
+
// The listener also sees autofill, drag-and-drop, IME commitContent and
|
|
540
|
+
// PROCESS_TEXT. Without this filter an autofill would turn into a paste
|
|
541
|
+
// event — and a consumer would turn that into an attachment chip.
|
|
542
|
+
if (!interceptPaste || payload.source != ContentInfoCompat.SOURCE_CLIPBOARD) {
|
|
543
|
+
return@OnReceiveContentListener payload
|
|
544
|
+
}
|
|
545
|
+
val parts = payload.partition { it.text != null }
|
|
546
|
+
parts.first?.let { accepted ->
|
|
547
|
+
val clip = accepted.clip
|
|
548
|
+
val text = PasteTextUtils.joinClipTexts((0 until clip.itemCount).map { clip.getItemAt(it).text })
|
|
549
|
+
if (text.isNotEmpty()) {
|
|
550
|
+
eventEmitter.emitPaste(text, "")
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
// Hand back only what we did not claim, so the buffer stays untouched.
|
|
554
|
+
parts.second
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
private fun readClipboardText(): String {
|
|
558
|
+
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager ?: return ""
|
|
559
|
+
val clip = clipboard.primaryClip ?: return ""
|
|
560
|
+
// `text`, not `coerceToText`: the listener can only claim items that already
|
|
561
|
+
// carry text, and the two routes have to agree on what counts as a paste.
|
|
562
|
+
return PasteTextUtils.joinClipTexts((0 until clip.itemCount).map { clip.getItemAt(it).text })
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Completes a paste reported through `onPaste`. `markdown` is accepted for
|
|
567
|
+
* parity with iOS but ignored here: Android's clipboard is plain text only
|
|
568
|
+
* (see `copyToClipboard`), so there is no flavor to restore.
|
|
569
|
+
*
|
|
570
|
+
* Resolving with an empty string and an empty selection is a zero-length edit
|
|
571
|
+
* — it still emits, which keeps the JS contract identical on both platforms.
|
|
572
|
+
*/
|
|
573
|
+
fun resolvePaste(
|
|
574
|
+
pastedText: String,
|
|
575
|
+
@Suppress("UNUSED_PARAMETER") markdown: String,
|
|
576
|
+
) {
|
|
577
|
+
val editable = text ?: return
|
|
578
|
+
val selStart = selectionStart.coerceAtLeast(0)
|
|
579
|
+
val selEnd = selectionEnd.coerceAtLeast(selStart)
|
|
580
|
+
val safeStart = selStart.coerceAtMost(editable.length)
|
|
581
|
+
val safeEnd = selEnd.coerceAtMost(editable.length)
|
|
582
|
+
|
|
583
|
+
isProcessingTextChange = true
|
|
584
|
+
try {
|
|
585
|
+
editable.replace(safeStart, safeEnd, pastedText)
|
|
586
|
+
formattingStore.adjustForEdit(safeStart, safeEnd - safeStart, pastedText.length)
|
|
587
|
+
lastProcessedText = editable.toString()
|
|
588
|
+
setSelection(safeStart + pastedText.length)
|
|
589
|
+
} finally {
|
|
590
|
+
isProcessingTextChange = false
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
applyFormatting()
|
|
594
|
+
// Parity with the un-intercepted paste, which reached the detectors through
|
|
595
|
+
// the text watcher: a pasted URL still autolinks.
|
|
596
|
+
detectorPipeline.processTextChange(editable, editable.toString(), safeStart, pastedText.length)
|
|
597
|
+
invalidate()
|
|
598
|
+
requestLayout()
|
|
599
|
+
eventEmitter.emitChangeText()
|
|
600
|
+
if (emitMarkdown) eventEmitter.emitChangeMarkdown()
|
|
601
|
+
eventEmitter.emitSelection(selectionStart, selectionEnd)
|
|
602
|
+
eventEmitter.emitState()
|
|
603
|
+
eventEmitter.emitCaretRectChangeIfNeeded()
|
|
604
|
+
}
|
|
605
|
+
|
|
491
606
|
fun setLinkForSelection(url: String) {
|
|
492
607
|
val selStart = selectionStart
|
|
493
608
|
val selEnd = selectionEnd
|
|
@@ -1155,6 +1270,10 @@ class EnrichedMarkdownTextInputView(
|
|
|
1155
1270
|
.replace("(", "%28")
|
|
1156
1271
|
.replace(")", "%29")
|
|
1157
1272
|
|
|
1273
|
+
private companion object {
|
|
1274
|
+
private val PASTE_MIME_TYPES = arrayOf("text/*")
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1158
1277
|
private data class MentionCandidate(
|
|
1159
1278
|
val indicator: String,
|
|
1160
1279
|
val start: Int,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.input.events
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.bridge.Arguments
|
|
4
|
+
import com.facebook.react.bridge.WritableMap
|
|
5
|
+
import com.facebook.react.uimanager.events.Event
|
|
6
|
+
|
|
7
|
+
class OnPasteEvent(
|
|
8
|
+
surfaceId: Int,
|
|
9
|
+
viewId: Int,
|
|
10
|
+
private val text: String,
|
|
11
|
+
private val markdown: String,
|
|
12
|
+
) : Event<OnPasteEvent>(surfaceId, viewId) {
|
|
13
|
+
override fun getEventName(): String = EVENT_NAME
|
|
14
|
+
|
|
15
|
+
override fun getEventData(): WritableMap =
|
|
16
|
+
Arguments.createMap().apply {
|
|
17
|
+
putString("text", text)
|
|
18
|
+
putString("markdown", markdown)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
companion object {
|
|
22
|
+
const val EVENT_NAME = "onPaste"
|
|
23
|
+
}
|
|
24
|
+
}
|
package/android/src/main/java/com/swmansion/enriched/markdown/input/layout/InputEventEmitter.kt
CHANGED
|
@@ -16,6 +16,7 @@ import com.swmansion.enriched.markdown.input.events.OnInputBlurEvent
|
|
|
16
16
|
import com.swmansion.enriched.markdown.input.events.OnInputFocusEvent
|
|
17
17
|
import com.swmansion.enriched.markdown.input.events.OnLinkDetectedEvent
|
|
18
18
|
import com.swmansion.enriched.markdown.input.events.OnMathPressEvent
|
|
19
|
+
import com.swmansion.enriched.markdown.input.events.OnPasteEvent
|
|
19
20
|
import com.swmansion.enriched.markdown.input.events.OnRequestCaretRectResultEvent
|
|
20
21
|
import com.swmansion.enriched.markdown.input.events.OnRequestMarkdownResultEvent
|
|
21
22
|
import com.swmansion.enriched.markdown.input.events.OnStartMentionEvent
|
|
@@ -99,6 +100,13 @@ class InputEventEmitter(
|
|
|
99
100
|
dispatch(OnMathPressEvent(surfaceId(), view.id, latex, start, end, displayMode, x, y, width, height))
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
fun emitPaste(
|
|
104
|
+
text: String,
|
|
105
|
+
markdown: String,
|
|
106
|
+
) {
|
|
107
|
+
dispatch(OnPasteEvent(surfaceId(), view.id, text, markdown))
|
|
108
|
+
}
|
|
109
|
+
|
|
102
110
|
fun emitStartMention(indicator: String) {
|
|
103
111
|
dispatch(OnStartMentionEvent(surfaceId(), view.id, indicator))
|
|
104
112
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
package com.swmansion.enriched.markdown.utils.input
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The pure half of the paste interception path. A `ClipData` cannot be built on
|
|
5
|
+
* the JVM, so the joining rule lives here where a unit test can reach it; the
|
|
6
|
+
* two call sites (context menu and `onReceiveContent`) only extract the items.
|
|
7
|
+
*/
|
|
8
|
+
object PasteTextUtils {
|
|
9
|
+
/**
|
|
10
|
+
* Flattens a clip's items into one string. A clip regularly carries more than
|
|
11
|
+
* one item — reading only `getItemAt(0)` silently drops items 2..n. Items
|
|
12
|
+
* without text (a URI or intent) are skipped, and the rest join on newlines,
|
|
13
|
+
* which is what the platform's own paste does.
|
|
14
|
+
*/
|
|
15
|
+
fun joinClipTexts(texts: List<CharSequence?>): String =
|
|
16
|
+
texts
|
|
17
|
+
.mapNotNull { it?.toString() }
|
|
18
|
+
.filter { it.isNotEmpty() }
|
|
19
|
+
.joinToString("\n")
|
|
20
|
+
}
|
package/docs/API_REFERENCE.md
CHANGED
|
@@ -532,6 +532,18 @@ Auto-capitalization behavior.
|
|
|
532
532
|
| -------- | -------------- | -------- |
|
|
533
533
|
| `string` | `'sentences'` | Both |
|
|
534
534
|
|
|
535
|
+
### `interceptPaste`
|
|
536
|
+
|
|
537
|
+
Reports pastes through [`onPaste`](#onpaste) instead of inserting them. Only takes effect when `onPaste` is also set — otherwise native would swallow every paste with nobody left to resolve it, so the library keeps the prop off.
|
|
538
|
+
|
|
539
|
+
See [Intercepting paste](./INPUT.md#intercepting-paste).
|
|
540
|
+
|
|
541
|
+
| Type | Default Value | Platform |
|
|
542
|
+
| --------- | ------------- | ------------- |
|
|
543
|
+
| `boolean` | `false` | iOS, Android |
|
|
544
|
+
|
|
545
|
+
macOS ignores this prop and always pastes inline.
|
|
546
|
+
|
|
535
547
|
### `multiline`
|
|
536
548
|
|
|
537
549
|
Whether the input supports multiple lines.
|
|
@@ -710,6 +722,18 @@ Fires when the active mention flow ends.
|
|
|
710
722
|
| ---- | ------------- | -------- |
|
|
711
723
|
| `(event: { indicator: string }) => void` | - | Both |
|
|
712
724
|
|
|
725
|
+
### `onPaste`
|
|
726
|
+
|
|
727
|
+
Fires instead of the insert when [`interceptPaste`](#interceptpaste) is on. Native reads the clipboard and leaves the buffer untouched, so the handler owns the paste from that point: it must call [`resolvePaste`](#resolvepastetext-string-markdown-string) exactly once, on every path including failures, or the paste is silently dropped.
|
|
728
|
+
|
|
729
|
+
| Type | Default Value | Platform |
|
|
730
|
+
| ---- | ------------- | -------- |
|
|
731
|
+
| `(event: { text: string; markdown: string }) => void` | - | iOS, Android |
|
|
732
|
+
|
|
733
|
+
`markdown` is non-empty only when the copy source was an `EnrichedMarkdownTextInput`, which the system clipboard can only carry on iOS; Android always reports `''`.
|
|
734
|
+
|
|
735
|
+
See [Intercepting paste](./INPUT.md#intercepting-paste).
|
|
736
|
+
|
|
713
737
|
### `writingDirection`
|
|
714
738
|
|
|
715
739
|
Paragraph writing direction in the input. iOS only — Android's `EditText` already resolves direction per paragraph via `TEXT_DIRECTION_FIRST_STRONG` and is unaffected by this prop.
|
|
@@ -942,6 +966,16 @@ Copies the input's full content to the system clipboard, matching the result of
|
|
|
942
966
|
|
|
943
967
|
On iOS and macOS the clipboard receives both plain text and a private Markdown pasteboard type, so pasting back into an `EnrichedMarkdownTextInput` restores the formatting; external apps receive plain text only. On Android the clipboard receives plain text only — inline styles are not preserved for any paste target.
|
|
944
968
|
|
|
969
|
+
### `resolvePaste(text: string, markdown?: string)`
|
|
970
|
+
|
|
971
|
+
Completes a paste reported through [`onPaste`](#onpaste). Call it exactly once per event:
|
|
972
|
+
|
|
973
|
+
- `resolvePaste(text, markdown)` — inserts, restoring the copied formatting when `markdown` is non-empty.
|
|
974
|
+
- `resolvePaste(text)` — inserts `text` verbatim.
|
|
975
|
+
- `resolvePaste('', '')` — drops the clipboard content, replacing the current selection. With an empty selection this is a no-op that still emits `onChangeText` with unchanged content.
|
|
976
|
+
|
|
977
|
+
Anything that replaces a selection replaces it here too, so a paste over selected text behaves the same on every branch.
|
|
978
|
+
|
|
945
979
|
### `startMention(indicator: string)`
|
|
946
980
|
|
|
947
981
|
Programmatically triggers a mention flow by inserting the indicator character at the current cursor position. The indicator must be listed in the `mentionIndicators` prop. Useful for toolbar buttons.
|
package/docs/INPUT.md
CHANGED
|
@@ -205,6 +205,45 @@ The input's content can be copied to the system clipboard from a ref, without re
|
|
|
205
205
|
|
|
206
206
|
On iOS and macOS the clipboard receives both plain text and a private Markdown pasteboard type, so pasting back into an `EnrichedMarkdownTextInput` restores the formatting; external apps receive plain text only. On Android the clipboard receives plain text only — inline styles are not preserved for any paste target.
|
|
207
207
|
|
|
208
|
+
## Intercepting paste
|
|
209
|
+
|
|
210
|
+
By default a paste goes straight into the buffer. Set [`interceptPaste`](API_REFERENCE.md#interceptpaste) together with [`onPaste`](API_REFERENCE.md#onpaste) to decide what a paste means before any text lands — turning a long paste into an attachment chip, stripping it, or inserting it unchanged.
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
const inputRef = useRef<EnrichedMarkdownTextInputInstance>(null);
|
|
214
|
+
|
|
215
|
+
<EnrichedMarkdownTextInput
|
|
216
|
+
ref={inputRef}
|
|
217
|
+
interceptPaste
|
|
218
|
+
onPaste={({ text, markdown }) => {
|
|
219
|
+
if (text.length > 2000) {
|
|
220
|
+
attachAsFile(text);
|
|
221
|
+
inputRef.current?.resolvePaste('', ''); // drops it, and replaces the selection
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
inputRef.current?.resolvePaste(text, markdown);
|
|
225
|
+
}}
|
|
226
|
+
/>
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Native reads the clipboard and **does not touch the buffer**. That makes the handler the only thing that can complete the paste, so the contract is strict:
|
|
230
|
+
|
|
231
|
+
- Call [`resolvePaste`](API_REFERENCE.md#resolvepastetext-string-markdown-string) **exactly once per event, on every path including failures**. A handler that throws before resolving leaves the paste dropped — the user sees nothing happen.
|
|
232
|
+
- Pass `markdown` back through when inserting. It is the copied formatting; dropping it turns a copy-then-paste inside your app into flat text.
|
|
233
|
+
- `interceptPaste` is ignored unless `onPaste` is set, so switching the prop on alone cannot make the input swallow pastes.
|
|
234
|
+
|
|
235
|
+
Because nothing is written until you resolve, an async decision is safe: the selection is still there, and the user sees an unchanged input rather than text that vanished.
|
|
236
|
+
|
|
237
|
+
### Platform behavior
|
|
238
|
+
|
|
239
|
+
| | Behavior |
|
|
240
|
+
| --- | --- |
|
|
241
|
+
| **iOS** | Context menu and hardware ⌘V. `markdown` is set when the copy source was an `EnrichedMarkdownTextInput`. |
|
|
242
|
+
| **Android** | Context menu, hardware Ctrl+V and `KEYCODE_PASTE`. `markdown` is always `''` — the Android clipboard carries plain text only. |
|
|
243
|
+
| **macOS** | `interceptPaste` is ignored, `onPaste` never fires, and pastes insert inline as usual. |
|
|
244
|
+
|
|
245
|
+
Only clipboard content is intercepted. Drag-and-drop, autofill and IME content insertion bypass `onPaste` on both platforms and insert normally — on Android they are filtered out explicitly, so an autofill never arrives looking like a paste.
|
|
246
|
+
|
|
208
247
|
## Style Detection
|
|
209
248
|
|
|
210
249
|
All of the above styles can be detected with the use of [onChangeState](API_REFERENCE.md#onchangestate) callback payload.
|
|
@@ -247,6 +247,16 @@ payload.setProperty(runtime, "height", event.height);
|
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
|
|
250
|
+
void EnrichedMarkdownTextInputEventEmitter::onPaste(OnPaste event) const {
|
|
251
|
+
dispatchEvent("paste", [event=std::move(event)](jsi::Runtime &runtime) {
|
|
252
|
+
auto payload = jsi::Object(runtime);
|
|
253
|
+
payload.setProperty(runtime, "text", event.text);
|
|
254
|
+
payload.setProperty(runtime, "markdown", event.markdown);
|
|
255
|
+
return payload;
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
|
|
250
260
|
void EnrichedMarkdownTextInputEventEmitter::onStartMention(OnStartMention event) const {
|
|
251
261
|
dispatchEvent("startMention", [event=std::move(event)](jsi::Runtime &runtime) {
|
|
252
262
|
auto payload = jsi::Object(runtime);
|
|
@@ -182,6 +182,11 @@ class EnrichedMarkdownTextInputEventEmitter : public ViewEventEmitter {
|
|
|
182
182
|
double height;
|
|
183
183
|
};
|
|
184
184
|
|
|
185
|
+
struct OnPaste {
|
|
186
|
+
std::string text;
|
|
187
|
+
std::string markdown;
|
|
188
|
+
};
|
|
189
|
+
|
|
185
190
|
struct OnStartMention {
|
|
186
191
|
std::string indicator;
|
|
187
192
|
};
|
|
@@ -218,6 +223,8 @@ class EnrichedMarkdownTextInputEventEmitter : public ViewEventEmitter {
|
|
|
218
223
|
|
|
219
224
|
void onMathPress(OnMathPress value) const;
|
|
220
225
|
|
|
226
|
+
void onPaste(OnPaste value) const;
|
|
227
|
+
|
|
221
228
|
void onStartMention(OnStartMention value) const;
|
|
222
229
|
|
|
223
230
|
void onChangeMention(OnChangeMention value) const;
|
|
@@ -146,6 +146,7 @@ EnrichedMarkdownTextInputProps::EnrichedMarkdownTextInputProps(
|
|
|
146
146
|
scrollEnabled(convertRawProp(context, rawProps, "scrollEnabled", sourceProps.scrollEnabled, {false})),
|
|
147
147
|
blockSyntax(convertRawProp(context, rawProps, "blockSyntax", sourceProps.blockSyntax, {false})),
|
|
148
148
|
autoCapitalize(convertRawProp(context, rawProps, "autoCapitalize", sourceProps.autoCapitalize, {})),
|
|
149
|
+
interceptPaste(convertRawProp(context, rawProps, "interceptPaste", sourceProps.interceptPaste, {false})),
|
|
149
150
|
multiline(convertRawProp(context, rawProps, "multiline", sourceProps.multiline, {false})),
|
|
150
151
|
cursorColor(convertRawProp(context, rawProps, "cursorColor", sourceProps.cursorColor, {})),
|
|
151
152
|
selectionColor(convertRawProp(context, rawProps, "selectionColor", sourceProps.selectionColor, {})),
|
|
@@ -211,6 +212,10 @@ folly::dynamic EnrichedMarkdownTextInputProps::getDiffProps(
|
|
|
211
212
|
result["autoCapitalize"] = autoCapitalize;
|
|
212
213
|
}
|
|
213
214
|
|
|
215
|
+
if (interceptPaste != oldProps->interceptPaste) {
|
|
216
|
+
result["interceptPaste"] = interceptPaste;
|
|
217
|
+
}
|
|
218
|
+
|
|
214
219
|
if (multiline != oldProps->multiline) {
|
|
215
220
|
result["multiline"] = multiline;
|
|
216
221
|
}
|
|
@@ -3361,6 +3361,7 @@ class EnrichedMarkdownTextInputProps final : public ViewProps {
|
|
|
3361
3361
|
bool scrollEnabled{false};
|
|
3362
3362
|
bool blockSyntax{false};
|
|
3363
3363
|
std::string autoCapitalize{};
|
|
3364
|
+
bool interceptPaste{false};
|
|
3364
3365
|
bool multiline{false};
|
|
3365
3366
|
SharedColor cursorColor{};
|
|
3366
3367
|
SharedColor selectionColor{};
|
|
@@ -36,6 +36,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
36
36
|
- (void)updateMath:(NSInteger)start latex:(NSString *)latex displayMode:(BOOL)displayMode;
|
|
37
37
|
- (void)removeMath:(NSInteger)start;
|
|
38
38
|
- (void)copyToClipboard;
|
|
39
|
+
- (void)resolvePaste:(NSString *)text markdown:(NSString *)markdown;
|
|
39
40
|
- (void)requestMarkdown:(NSInteger)requestId;
|
|
40
41
|
- (void)requestCaretRect:(NSInteger)requestId;
|
|
41
42
|
@end
|
|
@@ -399,6 +400,34 @@ if ([commandName isEqualToString:@"copyToClipboard"]) {
|
|
|
399
400
|
return;
|
|
400
401
|
}
|
|
401
402
|
|
|
403
|
+
if ([commandName isEqualToString:@"resolvePaste"]) {
|
|
404
|
+
#if RCT_DEBUG
|
|
405
|
+
if ([args count] != 2) {
|
|
406
|
+
RCTLogError(@"%@ command %@ received %d arguments, expected %d.", @"EnrichedMarkdownTextInput", commandName, (int)[args count], 2);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
#endif
|
|
410
|
+
|
|
411
|
+
NSObject *arg0 = args[0];
|
|
412
|
+
#if RCT_DEBUG
|
|
413
|
+
if (!RCTValidateTypeOfViewCommandArgument(arg0, [NSString class], @"string", @"EnrichedMarkdownTextInput", commandName, @"1st")) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
#endif
|
|
417
|
+
NSString * text = (NSString *)arg0;
|
|
418
|
+
|
|
419
|
+
NSObject *arg1 = args[1];
|
|
420
|
+
#if RCT_DEBUG
|
|
421
|
+
if (!RCTValidateTypeOfViewCommandArgument(arg1, [NSString class], @"string", @"EnrichedMarkdownTextInput", commandName, @"2nd")) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
#endif
|
|
425
|
+
NSString * markdown = (NSString *)arg1;
|
|
426
|
+
|
|
427
|
+
[componentView resolvePaste:text markdown:markdown];
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
402
431
|
if ([commandName isEqualToString:@"requestMarkdown"]) {
|
|
403
432
|
#if RCT_DEBUG
|
|
404
433
|
if ([args count] != 1) {
|
|
@@ -46,12 +46,22 @@ NSString *const kENRMMarkdownPasteboardType = @"com.swmansion.enriched-markdown.
|
|
|
46
46
|
markdown = [[NSString alloc] initWithData:markdownValue encoding:NSUTF8StringEncoding];
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
NSString *plainText = pasteboard.string;
|
|
50
|
+
|
|
51
|
+
// Both flavors are read before anything is decided, then nothing is written:
|
|
52
|
+
// JS owns the paste from here and must call resolvePaste. Mutating the buffer
|
|
53
|
+
// first would mean a handler that dies mid-flight leaves the selection gone
|
|
54
|
+
// with no text to show for it — a no-op paste is the visible failure.
|
|
55
|
+
if (self.markdownTextInput.interceptPaste && (plainText.length > 0 || markdown.length > 0)) {
|
|
56
|
+
[self.markdownTextInput emitOnPasteWithText:(plainText ?: @"") markdown:(markdown ?: @"")];
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
49
60
|
if (markdown.length > 0 && self.markdownTextInput != nil) {
|
|
50
61
|
[self.markdownTextInput pasteMarkdown:markdown];
|
|
51
62
|
return;
|
|
52
63
|
}
|
|
53
64
|
|
|
54
|
-
NSString *plainText = pasteboard.string;
|
|
55
65
|
if (plainText.length > 0) {
|
|
56
66
|
[self replaceRange:self.selectedTextRange withText:plainText];
|
|
57
67
|
}
|
|
@@ -8,12 +8,17 @@ NS_ASSUME_NONNULL_BEGIN
|
|
|
8
8
|
|
|
9
9
|
@interface EnrichedMarkdownTextInput : RCTViewComponentView
|
|
10
10
|
@property (nonatomic, assign) BOOL blockEmitting;
|
|
11
|
+
@property (nonatomic, assign) BOOL interceptPaste;
|
|
11
12
|
- (CGSize)measureSize:(CGFloat)maxWidth;
|
|
12
13
|
- (nullable NSString *)markdownForSelectedRange;
|
|
13
14
|
- (void)copyToClipboard;
|
|
14
15
|
- (void)pasteMarkdown:(NSString *)markdown;
|
|
15
16
|
- (void)replaceSelectedTextWith:(NSString *)text formattingRanges:(NSArray *)ranges;
|
|
16
17
|
- (void)scheduleRelayoutIfNeeded;
|
|
18
|
+
// Paste interception. Declared here rather than in +Internal.h because
|
|
19
|
+
// ENRMInputTextView.mm only imports that header on macOS.
|
|
20
|
+
- (void)emitOnPasteWithText:(NSString *)text markdown:(NSString *)markdown;
|
|
21
|
+
- (void)resolvePaste:(NSString *)text markdown:(NSString *)markdown;
|
|
17
22
|
@end
|
|
18
23
|
|
|
19
24
|
NS_ASSUME_NONNULL_END
|