Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import android.graphics.Color
import android.graphics.Rect
import android.graphics.text.LineBreaker
import android.os.Build
import android.text.Editable
import android.text.InputType
import android.text.Spannable
import android.util.AttributeSet
Expand All @@ -17,12 +18,14 @@ import android.util.Patterns
import android.util.TypedValue
import android.view.ActionMode
import android.view.Gravity
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.view.ViewCompat
import com.facebook.react.bridge.ReactContext
Expand All @@ -43,6 +46,7 @@ import com.swmansion.enriched.textinput.events.OnContextMenuItemPressEvent
import com.swmansion.enriched.textinput.events.OnInputBlurEvent
import com.swmansion.enriched.textinput.events.OnInputFocusEvent
import com.swmansion.enriched.textinput.events.OnRequestHtmlResultEvent
import com.swmansion.enriched.textinput.events.OnSubmitEditingEvent
import com.swmansion.enriched.textinput.spans.EnrichedInputH1Span
import com.swmansion.enriched.textinput.spans.EnrichedInputH2Span
import com.swmansion.enriched.textinput.spans.EnrichedInputH3Span
Expand Down Expand Up @@ -72,7 +76,9 @@ import java.util.regex.Pattern
import java.util.regex.PatternSyntaxException
import kotlin.math.ceil

class EnrichedTextInputView : AppCompatEditText {
class EnrichedTextInputView :
AppCompatEditText,
TextView.OnEditorActionListener {
var stateWrapper: StateWrapper? = null
val selection: EnrichedSelection? = EnrichedSelection(this)
val spanState: EnrichedSpanState? = EnrichedSpanState(this)
Expand Down Expand Up @@ -105,6 +111,7 @@ class EnrichedTextInputView : AppCompatEditText {

var fontSize: Float? = null
private var lineHeight: Float? = null
var submitBehavior: String? = null
private var autoFocus = false
private var typefaceDirty = false
private var didAttachToWindow = false
Expand Down Expand Up @@ -137,6 +144,18 @@ class EnrichedTextInputView : AppCompatEditText {

override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
var inputConnection = super.onCreateInputConnection(outAttrs)

if (shouldSubmitOnReturn()) {
// Remove the "No Enter Action" flag if it exists
outAttrs.imeOptions = outAttrs.imeOptions and EditorInfo.IME_FLAG_NO_ENTER_ACTION.inv()

// Force the key to be "Done" (or whatever label you set) instead of "Return"
// This ensures onEditorAction gets called instead of just inserting \n
if (outAttrs.imeOptions and EditorInfo.IME_MASK_ACTION == EditorInfo.IME_ACTION_UNSPECIFIED) {
outAttrs.imeOptions = outAttrs.imeOptions or EditorInfo.IME_ACTION_DONE
}
}

if (inputConnection != null) {
inputConnection =
EnrichedTextInputConnectionWrapper(
Expand Down Expand Up @@ -182,6 +201,53 @@ class EnrichedTextInputView : AppCompatEditText {

// Handle checkbox list item clicks
this.setCheckboxClickListener()

setOnEditorActionListener(this)
setReturnKeyLabel(DEFAULT_IME_ACTION_LABEL)
}

// Similar implementation to: https://github.com/facebook/react-native/blob/c1f5445f4a59d0035389725e47da58eb3d2c267c/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.kt#L940
override fun onEditorAction(
v: TextView?,
actionId: Int,
event: KeyEvent?,
): Boolean {
// Check if it's a valid keyboard action (Done, Next, etc.) or the Enter key (IME_NULL)
val isAction = (actionId and EditorInfo.IME_MASK_ACTION) != 0 || actionId == EditorInfo.IME_NULL

if (isAction) {
val shouldSubmit = shouldSubmitOnReturn()
val shouldBlur = shouldBlurOnReturn()

if (shouldSubmit) {
emitSubmitEditing()
}

if (shouldBlur) {
clearFocus()
}

if (shouldSubmit || shouldBlur) {
return true
}
}

// Return false to let the system handle default behavior (like inserting \n)
return false
}

private fun emitSubmitEditing() {
val context = context as ReactContext
val surfaceId = UIManagerHelper.getSurfaceId(context)
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, id)
dispatcher?.dispatchEvent(
OnSubmitEditingEvent(
surfaceId,
id,
text,
experimentalSynchronousEvents,
),
)
}

// https://github.com/facebook/react-native/blob/36df97f500aa0aa8031098caf7526db358b6ddc1/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt#L295C1-L296C1
Expand Down Expand Up @@ -431,6 +497,10 @@ class EnrichedTextInputView : AppCompatEditText {
}
}

fun setReturnKeyLabel(returnKeyLabel: String?) {
setImeActionLabel(returnKeyLabel, EditorInfo.IME_ACTION_UNSPECIFIED)
}

fun setColor(colorInt: Int?) {
if (colorInt == null) {
setTextColor(Color.BLACK)
Expand Down Expand Up @@ -666,6 +736,10 @@ class EnrichedTextInputView : AppCompatEditText {
defaultValueDirty = true
}

fun shouldBlurOnReturn(): Boolean = submitBehavior == "blurAndSubmit"

fun shouldSubmitOnReturn(): Boolean = submitBehavior == "submit" || submitBehavior == "blurAndSubmit"

private fun updateDefaultValue() {
if (!defaultValueDirty) return

Expand Down Expand Up @@ -1009,5 +1083,6 @@ class EnrichedTextInputView : AppCompatEditText {
const val TAG = "EnrichedTextInputView"
const val CLIPBOARD_TAG = "react-native-enriched-clipboard"
private const val CONTEXT_MENU_ITEM_ID = 10000
const val DEFAULT_IME_ACTION_LABEL = "DONE"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import com.swmansion.enriched.textinput.events.OnMentionDetectedEvent
import com.swmansion.enriched.textinput.events.OnMentionEvent
import com.swmansion.enriched.textinput.events.OnPasteImagesEvent
import com.swmansion.enriched.textinput.events.OnRequestHtmlResultEvent
import com.swmansion.enriched.textinput.events.OnSubmitEditingEvent
import com.swmansion.enriched.textinput.spans.EnrichedSpans
import com.swmansion.enriched.textinput.styles.HtmlStyle
import com.swmansion.enriched.textinput.utils.jsonStringToStringMap
Expand Down Expand Up @@ -74,6 +75,7 @@ class EnrichedTextInputViewManager :
map.put(OnInputKeyPressEvent.EVENT_NAME, mapOf("registrationName" to OnInputKeyPressEvent.EVENT_NAME))
map.put(OnPasteImagesEvent.EVENT_NAME, mapOf("registrationName" to OnPasteImagesEvent.EVENT_NAME))
map.put(OnContextMenuItemPressEvent.EVENT_NAME, mapOf("registrationName" to OnContextMenuItemPressEvent.EVENT_NAME))
map.put(OnSubmitEditingEvent.EVENT_NAME, mapOf("registrationName" to OnSubmitEditingEvent.EVENT_NAME))

return map
}
Expand Down Expand Up @@ -110,6 +112,30 @@ class EnrichedTextInputViewManager :
view?.setCursorColor(color)
}

@ReactProp(name = "returnKeyType")
override fun setReturnKeyType(
view: EnrichedTextInputView?,
returnKeyType: String?,
) {
// Not supported on multiline text input
}

@ReactProp(name = "submitBehavior")
override fun setSubmitBehavior(
view: EnrichedTextInputView?,
submitBehavior: String?,
) {
view?.submitBehavior = submitBehavior
}

@ReactProp(name = "returnKeyLabel")
override fun setReturnKeyLabel(
view: EnrichedTextInputView?,
returnKeyLabel: String?,
) {
view?.setReturnKeyLabel(returnKeyLabel)
}

@ReactProp(name = "selectionColor", customType = "Color")
override fun setSelectionColor(
view: EnrichedTextInputView?,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.swmansion.enriched.textinput.events

import android.text.Editable
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event

class OnSubmitEditingEvent(
surfaceId: Int,
viewId: Int,
private val editable: Editable?,
private val experimentalSynchronousEvents: Boolean,
) : Event<OnSubmitEditingEvent>(surfaceId, viewId) {
override fun getEventName(): String = EVENT_NAME

override fun getEventData(): WritableMap {
val eventData: WritableMap = Arguments.createMap()
val text = editable.toString()
val normalizedText = text.replace(Regex("\\u200B"), "")
eventData.putString("text", normalizedText)
return eventData
}

override fun experimental_isSynchronous(): Boolean = experimentalSynchronousEvents

companion object {
const val EVENT_NAME: String = "onSubmitEditing"
}
}
6 changes: 6 additions & 0 deletions apps/example/src/hooks/useEditorState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type OnChangeSelectionEvent,
type OnKeyPressEvent,
type OnPasteImagesEvent,
type OnSubmitEditing,
} from 'react-native-enriched';
import { useRef, useState } from 'react';
import { type MentionItem } from '../components/MentionPopup';
Expand Down Expand Up @@ -232,6 +233,10 @@ export function useEditorState() {
}
};

const handleSubmitEditingEvent = (e: OnSubmitEditing) => {
console.log('Submitted editing:', e.text);
};

return {
ref,
stylesState,
Expand Down Expand Up @@ -269,6 +274,7 @@ export function useEditorState() {
handleChangeMention,
handleUserMentionSelected,
handleChannelMentionSelected,
handleSubmitEditingEvent,
submitLink,
submitSetValue,
selectImage,
Expand Down
3 changes: 3 additions & 0 deletions apps/example/src/screens/DevScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ export function DevScreen({ onSwitch }: DevScreenProps) {
onChangeSelection={(e) =>
editor.handleSelectionChangeEvent(e.nativeEvent)
}
onSubmitEditing={(e) =>
editor.handleSubmitEditingEvent(e.nativeEvent)
}
onKeyPress={(e) => editor.handleKeyPress(e.nativeEvent)}
androidExperimentalSynchronousEvents={
ANDROID_EXPERIMENTAL_SYNCHRONOUS_EVENTS
Expand Down
3 changes: 3 additions & 0 deletions apps/example/src/screens/TestScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ export function TestScreen({ onSwitch }: TestScreenProps) {
editor.handleSelectionChangeEvent(e.nativeEvent)
}
onKeyPress={(e) => editor.handleKeyPress(e.nativeEvent)}
onSubmitEditing={(e) =>
editor.handleSubmitEditingEvent(e.nativeEvent)
}
androidExperimentalSynchronousEvents={
ANDROID_EXPERIMENTAL_SYNCHRONOUS_EVENTS
}
Expand Down
53 changes: 53 additions & 0 deletions ios/EnrichedTextInputView.mm
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#import "EnrichedTextInputView.h"
#import "CoreText/CoreText.h"
#import "ImageAttachment.h"
#import "KeyboardUtils.h"
#import "LayoutManagerExtension.h"
#import "ParagraphAttributesUtils.h"
#import "RCTFabricComponentsPlugins.h"
Expand Down Expand Up @@ -51,6 +52,7 @@ @implementation EnrichedTextInputView {
BOOL _emitTextChange;
NSMutableDictionary<NSValue *, UIImageView *> *_attachmentViews;
NSArray<NSDictionary *> *_contextMenuItems;
NSString *_submitBehavior;
}

// MARK: - Component utils
Expand Down Expand Up @@ -836,6 +838,17 @@ - (void)updateProps:(Props::Shared const &)props
}
}

if (newViewProps.returnKeyType != oldViewProps.returnKeyType) {
NSString *str = [NSString fromCppString:newViewProps.returnKeyType];

textView.returnKeyType =
[KeyboardUtils getUIReturnKeyTypeFromReturnKeyType:str];
}

if (newViewProps.submitBehavior != oldViewProps.submitBehavior) {
_submitBehavior = [NSString fromCppString:newViewProps.submitBehavior];
}

// autoCapitalize
if (newViewProps.autoCapitalize != oldViewProps.autoCapitalize) {
NSString *str = [NSString fromCppString:newViewProps.autoCapitalize];
Expand Down Expand Up @@ -1192,6 +1205,15 @@ - (bool)isStyle:(StyleType)type activeInMap:(NSDictionary *)styleMap {
return false;
}

- (bool)textInputShouldReturn {
return [_submitBehavior isEqualToString:@"blurAndSubmit"];
}

- (bool)textInputShouldSubmitOnReturn {
return [_submitBehavior isEqualToString:@"blurAndSubmit"] ||
[_submitBehavior isEqualToString:@"submit"];
}

- (void)addStyleBlock:(StyleType)blocking to:(StyleType)blocked {
NSMutableArray *blocksArr = [blockingStyles[@(blocked)] mutableCopy];
if (![blocksArr containsObject:@(blocking)]) {
Expand Down Expand Up @@ -1351,6 +1373,19 @@ - (NSUInteger)getActualIndex:(NSInteger)visibleIndex text:(NSString *)text {
return actualIndex;
}

- (void)emitOnSubmitEdittingEvent {
auto emitter = [self getEventEmitter];
if (emitter != nullptr) {
NSString *stringToBeEmitted = [[textView.textStorage.string
stringByReplacingOccurrencesOfString:@"\u200B"
withString:@""] copy];

emitter->onSubmitEditing({
.text = [stringToBeEmitted toCppString],
});
}
}

- (void)emitOnLinkDetectedEvent:(NSString *)text
url:(NSString *)url
range:(NSRange)range {
Expand Down Expand Up @@ -1969,6 +2004,24 @@ - (void)handleKeyPressInRange:(NSString *)text range:(NSRange)range {
- (bool)textView:(UITextView *)textView
shouldChangeTextInRange:(NSRange)range
replacementText:(NSString *)text {
// Check if the user pressed "Enter"
if ([text isEqualToString:@"\n"]) {
const bool shouldSubmit = [self textInputShouldSubmitOnReturn];
const bool shouldReturn = [self textInputShouldReturn];

if (shouldSubmit) {
[self emitOnSubmitEdittingEvent];
}

if (shouldReturn) {
[textView endEditing:NO];
}

if (shouldSubmit || shouldReturn) {
return NO;
}
}

recentlyChangedRange = NSMakeRange(range.location, text.length);
[self handleKeyPressInRange:text range:range];

Expand Down
7 changes: 7 additions & 0 deletions ios/utils/KeyboardUtils.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#import <UIKit/UIKit.h>
#pragma once

@interface KeyboardUtils : NSObject
+ (UIReturnKeyType)getUIReturnKeyTypeFromReturnKeyType:
(NSString *)returnKeyType;
@end
Loading
Loading