Skip to content
Open
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
@@ -0,0 +1,120 @@
package com.swmansion.enriched

import android.view.KeyEvent
import android.view.inputmethod.InputConnection
import android.view.inputmethod.InputConnectionWrapper
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.UIManagerHelper
import com.swmansion.enriched.events.OnInputKeyPressEvent

class EnrichedTextInputConnectionWrapper(
target: InputConnection,
private val reactContext: ReactContext,
private val editText: EnrichedTextInputView,
private val experimentalSynchronousEvents: Boolean
) : InputConnectionWrapper(target, false) {

private var isBatchEdit = false
private var key: String? = null

override fun beginBatchEdit(): Boolean {
isBatchEdit = true
return super.beginBatchEdit()
}

override fun endBatchEdit(): Boolean {
isBatchEdit = false
key?.let { k ->
dispatchKeyEvent(k)
key = null
}
return super.endBatchEdit()
}

override fun setComposingText(text: CharSequence, newCursorPosition: Int): Boolean {
val previousSelectionStart = editText.selectionStart
val previousSelectionEnd = editText.selectionEnd

val consumed = super.setComposingText(text, newCursorPosition)

val currentSelectionStart = editText.selectionStart
val noPreviousSelection = previousSelectionStart == previousSelectionEnd
val cursorDidNotMove = currentSelectionStart == previousSelectionStart
val cursorMovedBackwardsOrAtBeginningOfInput =
currentSelectionStart < previousSelectionStart || currentSelectionStart <= 0

val inputKey =
if (
cursorMovedBackwardsOrAtBeginningOfInput || (!noPreviousSelection && cursorDidNotMove)
) {
BACKSPACE_KEY_VALUE
} else {
editText.text?.get(currentSelectionStart - 1).toString()
}

dispatchKeyEventOrEnqueue(inputKey)
return consumed
}

override fun commitText(text: CharSequence, newCursorPosition: Int): Boolean {
var inputKey = text.toString()
// Assume not a keyPress if length > 1 (or 2 if unicode)
if (inputKey.length <= 2) {
if (inputKey.isEmpty()) {
inputKey = BACKSPACE_KEY_VALUE
}
dispatchKeyEventOrEnqueue(inputKey)
}
return super.commitText(text, newCursorPosition)
}

override fun deleteSurroundingText(beforeLength: Int, afterLength: Int): Boolean {
dispatchKeyEvent(BACKSPACE_KEY_VALUE)
return super.deleteSurroundingText(beforeLength, afterLength)
}

// Called by SwiftKey when cursor at beginning of input when there is a delete
// or when enter is pressed anywhere in the text. Whereas stock Android Keyboard calls
// [InputConnection.deleteSurroundingText] & [InputConnection.commitText]
// in each case, respectively.
override fun sendKeyEvent(event: KeyEvent): Boolean {
if (event.action == KeyEvent.ACTION_DOWN) {
val isNumberKey = event.unicodeChar in 48..57
when (event.keyCode) {
KeyEvent.KEYCODE_DEL -> dispatchKeyEvent(BACKSPACE_KEY_VALUE)
KeyEvent.KEYCODE_ENTER -> dispatchKeyEvent(ENTER_KEY_VALUE)
else ->
if (isNumberKey) {
dispatchKeyEvent(event.number.toString())
}
}
}
return super.sendKeyEvent(event)
}

private fun dispatchKeyEventOrEnqueue(inputKey: String) {
if (isBatchEdit) {
key = inputKey
} else {
dispatchKeyEvent(inputKey)
}
}

private fun dispatchKeyEvent(inputKey: String) {
val resolvedKey = if (inputKey == NEWLINE_RAW_VALUE) ENTER_KEY_VALUE else inputKey
val surfaceId = UIManagerHelper.getSurfaceId(editText)
val eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(reactContext, editText.id)
eventDispatcher?.dispatchEvent(OnInputKeyPressEvent(
surfaceId = surfaceId,
viewId = editText.id,
key = resolvedKey,
experimentalSynchronousEvents = experimentalSynchronousEvents
))
}

companion object {
const val NEWLINE_RAW_VALUE: String = "\n"
const val BACKSPACE_KEY_VALUE: String = "Backspace"
const val ENTER_KEY_VALUE: String = "Enter"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import android.util.Log
import android.util.TypedValue
import android.view.Gravity
import android.view.MotionEvent
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputConnection
import android.view.inputmethod.InputMethodManager
import androidx.appcompat.widget.AppCompatEditText
import com.facebook.react.bridge.ReactContext
Expand Down Expand Up @@ -89,6 +91,21 @@ class EnrichedTextInputView : AppCompatEditText {
prepareComponent()
}

override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? {
var inputConnection = super.onCreateInputConnection(outAttrs)
if (inputConnection != null) {
inputConnection =
EnrichedTextInputConnectionWrapper(
inputConnection,
context as ReactContext,
this,
experimentalSynchronousEvents
)
}

return inputConnection
}

init {
inputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.swmansion.enriched.events.OnChangeSelectionEvent
import com.swmansion.enriched.events.OnChangeStateEvent
import com.swmansion.enriched.events.OnChangeTextEvent
import com.swmansion.enriched.events.OnInputFocusEvent
import com.swmansion.enriched.events.OnInputKeyPressEvent
import com.swmansion.enriched.events.OnLinkDetectedEvent
import com.swmansion.enriched.events.OnMentionDetectedEvent
import com.swmansion.enriched.events.OnMentionEvent
Expand Down Expand Up @@ -72,6 +73,7 @@ class EnrichedTextInputViewManager : SimpleViewManager<EnrichedTextInputView>(),
map.put(OnMentionDetectedEvent.EVENT_NAME, mapOf("registrationName" to OnMentionDetectedEvent.EVENT_NAME))
map.put(OnMentionEvent.EVENT_NAME, mapOf("registrationName" to OnMentionEvent.EVENT_NAME))
map.put(OnChangeSelectionEvent.EVENT_NAME, mapOf("registrationName" to OnChangeSelectionEvent.EVENT_NAME))
map.put(OnInputKeyPressEvent.EVENT_NAME, mapOf("registrationName" to OnInputKeyPressEvent.EVENT_NAME))

return map
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.swmansion.enriched.events

import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event

class OnInputKeyPressEvent(surfaceId: Int, viewId: Int, private val key: String, private val experimentalSynchronousEvents: Boolean) :
Event<OnInputKeyPressEvent>(surfaceId, viewId) {
override fun getEventName(): String {
return EVENT_NAME
}

override fun getEventData(): WritableMap {
val eventData: WritableMap = Arguments.createMap()
eventData.putString("key", key)

return eventData
}

override fun experimental_isSynchronous(): Boolean {
return experimentalSynchronousEvents
}

companion object {
const val EVENT_NAME: String = "onInputKeyPress"
}
}
6 changes: 6 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
type OnChangeStateEvent,
type OnChangeSelectionEvent,
type HtmlStyle,
type OnKeyPressEvent,
} from 'react-native-enriched';
import { useRef, useState } from 'react';
import { Button } from './components/Button';
Expand Down Expand Up @@ -243,6 +244,10 @@ export default function App() {
console.log('Input blurred');
};

const handleKeyPress = (e: NativeSyntheticEvent<OnKeyPressEvent>) => {
console.log('Key pressed:', e.nativeEvent.key);
};

const handleLinkDetected = (state: CurrentLinkState) => {
console.log(state);
setCurrentLink(state);
Expand Down Expand Up @@ -283,6 +288,7 @@ export default function App() {
onEndMention={handleEndMention}
onFocus={handleFocusEvent}
onBlur={handleBlurEvent}
onKeyPress={handleKeyPress}
onChangeSelection={handleSelectionChangeEvent}
androidExperimentalSynchronousEvents={
ANDROID_EXPERIMENTAL_SYNCHRONOUS_EVENTS
Expand Down
1 change: 1 addition & 0 deletions ios/EnrichedTextInputView.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)emitOnMentionEvent:(NSString *)indicator text:(nullable NSString *)text;
- (void)anyTextMayHaveBeenModified;
- (BOOL)handleStyleBlocksAndConflicts:(StyleType)type range:(NSRange)range;
- (void)emitOnKeyPressEvent:(NSString *)key;
@end

NS_ASSUME_NONNULL_END
Expand Down
23 changes: 23 additions & 0 deletions ios/EnrichedTextInputView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -1151,8 +1151,31 @@ - (void)textViewDidEndEditing:(UITextView *)textView {
}
}

- (void)emitOnKeyPressEvent:(NSString *)key {
auto emitter = [self getEventEmitter];
if (emitter != nullptr) {
emitter->onInputKeyPress({
.key = [key toCppString]
});
}
}

- (bool)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
recentlyChangedRange = NSMakeRange(range.location, text.length);
NSString *key = nil;
if (text.length == 0 && range.length > 0) {
key = @"Backspace";
} else if ([text isEqualToString:@"\n"]) {
key = @"Enter";
} else if ([text isEqualToString:@"\t"]) {
key = @"Tab";
} else if (text.length == 1) {
key = text;
}

if(key != nil) {
[self emitOnKeyPressEvent: key];
}

UnorderedListStyle *uStyle = stylesDict[@([UnorderedListStyle getStyleType])];
OrderedListStyle *oStyle = stylesDict[@([OrderedListStyle getStyleType])];
Expand Down
15 changes: 15 additions & 0 deletions ios/inputTextView/InputTextView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ - (void)paste:(id)sender {
EnrichedTextInputView *typedInput = (EnrichedTextInputView *)_input;
if(typedInput == nullptr) { return; }

_textWasPasted = YES;

UIPasteboard *pasteboard = [UIPasteboard generalPasteboard];
NSArray<NSString *> *pasteboardTypes = pasteboard.pasteboardTypes;
NSRange currentRange = typedInput->textView.selectedRange;
Expand Down Expand Up @@ -114,4 +116,17 @@ - (void)cut:(id)sender {
[typedInput anyTextMayHaveBeenModified];
}

- (void)deleteBackward {
EnrichedTextInputView *typedInput = (EnrichedTextInputView *)_input;
if (typedInput != nullptr) {
[typedInput emitOnKeyPressEvent:@"Backspace"];
}

[super deleteBackward];

if (typedInput != nullptr) {
[typedInput anyTextMayHaveBeenModified];
}
}

@end
4 changes: 4 additions & 0 deletions src/EnrichedTextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import EnrichedTextInputNativeComponent, {
type OnMentionDetected,
type OnMentionDetectedInternal,
type MentionStyleProperties,
type OnKeyPressEvent,
} from './EnrichedTextInputNativeComponent';
import type {
ColorValue,
Expand Down Expand Up @@ -141,6 +142,7 @@ export interface EnrichedTextInputProps extends Omit<ViewProps, 'children'> {
onChangeMention?: (e: OnChangeMentionEvent) => void;
onEndMention?: (indicator: string) => void;
onChangeSelection?: (e: NativeSyntheticEvent<OnChangeSelectionEvent>) => void;
onKeyPress?: (e: NativeSyntheticEvent<OnKeyPressEvent>) => void;
/**
* If true, Android will use experimental synchronous events.
* This will prevent from input flickering when updating component size.
Expand Down Expand Up @@ -191,6 +193,7 @@ export const EnrichedTextInput = ({
onChangeMention,
onEndMention,
onChangeSelection,
onKeyPress,
androidExperimentalSynchronousEvents = false,
...rest
}: EnrichedTextInputProps) => {
Expand Down Expand Up @@ -349,6 +352,7 @@ export const EnrichedTextInput = ({
onMentionDetected={handleMentionDetected}
onMention={handleMentionEvent}
onChangeSelection={onChangeSelection}
onInputKeyPress={onKeyPress}
androidExperimentalSynchronousEvents={
androidExperimentalSynchronousEvents
}
Expand Down
5 changes: 5 additions & 0 deletions src/EnrichedTextInputNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ export interface MentionStyleProperties {
textDecorationLine?: 'underline' | 'none';
}

export interface OnKeyPressEvent {
key: string;
}

export interface HtmlStyleInternal {
h1?: {
fontSize?: Float;
Expand Down Expand Up @@ -146,6 +150,7 @@ export interface NativeProps extends ViewProps {
onMentionDetected?: DirectEventHandler<OnMentionDetectedInternal>;
onMention?: DirectEventHandler<OnMentionEvent>;
onChangeSelection?: DirectEventHandler<OnChangeSelectionEvent>;
onInputKeyPress?: DirectEventHandler<OnKeyPressEvent>;

// Style related props - used for generating proper setters in component's manager
// These should not be passed as regular props
Expand Down
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export type {
OnLinkDetected,
OnMentionDetected,
OnChangeSelectionEvent,
OnKeyPressEvent,
} from './EnrichedTextInputNativeComponent';