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
Expand Up @@ -10,6 +10,7 @@ import android.graphics.Rect
import android.graphics.text.LineBreaker
import android.os.Build
import android.text.Editable
import android.text.InputFilter
import android.text.InputType
import android.text.Spannable
import android.text.SpannableString
Expand Down Expand Up @@ -46,6 +47,7 @@ import com.swmansion.enriched.textinput.events.MentionHandler
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.OnMaxLengthExceededEvent
import com.swmansion.enriched.textinput.events.OnRequestHtmlResultEvent
import com.swmansion.enriched.textinput.events.OnSubmitEditingEvent
import com.swmansion.enriched.textinput.spans.EnrichedInputH1Span
Expand Down Expand Up @@ -122,6 +124,7 @@ class EnrichedTextInputView :
private var fontWeight: Int = ReactConstants.UNSET
private var defaultValue: CharSequence? = null
private var defaultValueDirty: Boolean = false
private var maxLength: Int? = null

private var inputMethodManager: InputMethodManager? = null
private val spannableFactory = EnrichedTextInputSpannableFactory()
Expand Down Expand Up @@ -338,6 +341,38 @@ class EnrichedTextInputView :
}
}

fun setMaxLength(value: Int?) {
maxLength = value
filters =
if (value == null) {
emptyArray()
} else {
arrayOf(
InputFilter { source, start, end, dest, dstart, dend ->
val remaining = value - ((dest?.length ?: 0) - (dend - dstart))
val incomingLength = end - start

if (incomingLength <= 0 || incomingLength <= remaining) {
return@InputFilter null
}

emitOnMaxLengthExceededEvent(value)
""
},
)
}
}

private fun emitOnMaxLengthExceededEvent(limit: Int) {
val context = context as ReactContext
val surfaceId = UIManagerHelper.getSurfaceId(context)
val dispatcher = UIManagerHelper.getEventDispatcherForReactTag(context, id)

dispatcher?.dispatchEvent(
OnMaxLengthExceededEvent(surfaceId, id, limit, experimentalSynchronousEvents),
)
}

fun handleTextPaste(item: ClipData.Item) {
val currentText = text as Spannable
val start = selectionStart.coerceAtLeast(0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.swmansion.enriched.textinput.events.OnInputBlurEvent
import com.swmansion.enriched.textinput.events.OnInputFocusEvent
import com.swmansion.enriched.textinput.events.OnInputKeyPressEvent
import com.swmansion.enriched.textinput.events.OnLinkDetectedEvent
import com.swmansion.enriched.textinput.events.OnMaxLengthExceededEvent
import com.swmansion.enriched.textinput.events.OnMentionDetectedEvent
import com.swmansion.enriched.textinput.events.OnMentionEvent
import com.swmansion.enriched.textinput.events.OnPasteImagesEvent
Expand Down Expand Up @@ -74,6 +75,7 @@ class EnrichedTextInputViewManager :
map.put(OnRequestHtmlResultEvent.EVENT_NAME, mapOf("registrationName" to OnRequestHtmlResultEvent.EVENT_NAME))
map.put(OnInputKeyPressEvent.EVENT_NAME, mapOf("registrationName" to OnInputKeyPressEvent.EVENT_NAME))
map.put(OnPasteImagesEvent.EVENT_NAME, mapOf("registrationName" to OnPasteImagesEvent.EVENT_NAME))
map.put(OnMaxLengthExceededEvent.EVENT_NAME, mapOf("registrationName" to OnMaxLengthExceededEvent.EVENT_NAME))
map.put(OnContextMenuItemPressEvent.EVENT_NAME, mapOf("registrationName" to OnContextMenuItemPressEvent.EVENT_NAME))
map.put(OnSubmitEditingEvent.EVENT_NAME, mapOf("registrationName" to OnSubmitEditingEvent.EVENT_NAME))

Expand All @@ -96,6 +98,14 @@ class EnrichedTextInputViewManager :
view?.setPlaceholder(value)
}

@ReactProp(name = "maxLength", defaultInt = 0)
override fun setMaxLength(
view: EnrichedTextInputView?,
value: Int,
) {
view?.setMaxLength(value.takeIf { it > 0 })
}

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

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

class OnMaxLengthExceededEvent(
surfaceId: Int,
viewId: Int,
private val maxLength: Int,
private val experimentalSynchronousEvents: Boolean,
) : Event<OnMaxLengthExceededEvent>(surfaceId, viewId) {
override fun getEventName(): String = EVENT_NAME

override fun getEventData(): WritableMap {
val eventData: WritableMap = Arguments.createMap()
eventData.putInt("maxLength", maxLength)
return eventData
}

override fun experimental_isSynchronous(): Boolean = experimentalSynchronousEvents

companion object {
const val EVENT_NAME: String = "onMaxLengthExceeded"
}
}
3 changes: 3 additions & 0 deletions ios/EnrichedTextInputView.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,14 @@ NS_ASSUME_NONNULL_BEGIN
BOOL useHtmlNormalizer;
@public
NSValue *dotReplacementRange;
@public
NSInteger maxLength;
}
- (CGSize)measureSize:(CGFloat)maxWidth;
- (void)emitOnLinkDetectedEvent:(LinkData *)linkData range:(NSRange)range;
- (void)emitOnMentionEvent:(NSString *)indicator text:(nullable NSString *)text;
- (void)emitOnPasteImagesEvent:(NSArray<NSDictionary *> *)images;
- (void)emitOnMaxLengthExceededEvent:(NSInteger)maxLength;
- (void)anyTextMayHaveBeenModified;
- (void)scheduleRelayoutIfNeeded;
- (BOOL)handleStyleBlocksAndConflicts:(StyleType)type range:(NSRange)range;
Expand Down
21 changes: 21 additions & 0 deletions ios/EnrichedTextInputView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -782,6 +782,10 @@ - (void)updateProps:(Props::Shared const &)props
textView.editable = newViewProps.editable;
}

if (newViewProps.maxLength != oldViewProps.maxLength) {
maxLength = newViewProps.maxLength;
}

// useHtmlNormalizer
if (newViewProps.useHtmlNormalizer != oldViewProps.useHtmlNormalizer) {
useHtmlNormalizer = newViewProps.useHtmlNormalizer;
Expand Down Expand Up @@ -1526,6 +1530,13 @@ - (void)emitOnKeyPressEvent:(NSString *)key {
}
}

- (void)emitOnMaxLengthExceededEvent:(NSInteger)limit {
auto emitter = [self getEventEmitter];
if (emitter != nullptr) {
emitter->onMaxLengthExceeded({.maxLength = static_cast<int>(limit)});
}
}

// MARK: - Styles manipulation

- (void)toggleRegularStyle:(StyleType)type {
Expand Down Expand Up @@ -1994,6 +2005,16 @@ - (void)handleKeyPressInRange:(NSString *)text range:(NSRange)range {
- (bool)textView:(UITextView *)textView
shouldChangeTextInRange:(NSRange)range
replacementText:(NSString *)text {
if (maxLength > 0) {
NSString *currentText = textView.textStorage.string ?: @"";
NSUInteger proposedLength = currentText.length - range.length + text.length;

if (proposedLength > maxLength) {
[self emitOnMaxLengthExceededEvent:maxLength];
return NO;
}
}

// Check if the user pressed "Enter"
if ([text isEqualToString:@"\n"]) {
const bool shouldSubmit = [self textInputShouldSubmitOnReturn];
Expand Down
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type {
OnChangeSelectionEvent,
OnKeyPressEvent,
OnPasteImagesEvent,
OnMaxLengthExceededEvent,
OnSubmitEditing,
HtmlStyle,
MentionStyleProperties,
Expand Down
4 changes: 4 additions & 0 deletions src/native/EnrichedTextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,15 @@ export const EnrichedTextInput = ({
onChangeSelection,
onKeyPress,
onSubmitEditing,
onMaxLengthExceeded,
returnKeyType,
returnKeyLabel,
submitBehavior,
contextMenuItems,
androidExperimentalSynchronousEvents = ENRICHED_TEXT_INPUT_DEFAULT_PROPS.androidExperimentalSynchronousEvents,
useHtmlNormalizer = ENRICHED_TEXT_INPUT_DEFAULT_PROPS.useHtmlNormalizer,
scrollEnabled = ENRICHED_TEXT_INPUT_DEFAULT_PROPS.scrollEnabled,
maxLength,
...rest
}: EnrichedTextInputProps) => {
const nativeRef = useRef<ComponentType | null>(null);
Expand Down Expand Up @@ -348,6 +350,7 @@ export const EnrichedTextInput = ({
onChangeSelection={onChangeSelection}
onRequestHtmlResult={handleRequestHtmlResult}
onInputKeyPress={onKeyPress}
onMaxLengthExceeded={onMaxLengthExceeded}
contextMenuItems={nativeContextMenuItems}
onContextMenuItemPress={handleContextMenuItemPress}
onSubmitEditing={onSubmitEditing}
Expand All @@ -359,6 +362,7 @@ export const EnrichedTextInput = ({
}
useHtmlNormalizer={useHtmlNormalizer}
scrollEnabled={scrollEnabled}
maxLength={maxLength}
{...rest}
/>
);
Expand Down
6 changes: 6 additions & 0 deletions src/spec/EnrichedTextInputNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,10 @@ export interface OnPasteImagesEvent {
}[];
}

export interface OnMaxLengthExceededEvent {
maxLength: Int32;
}

type Heading = {
fontSize?: Float;
bold?: boolean;
Expand Down Expand Up @@ -356,6 +360,7 @@ export interface NativeProps extends ViewProps {
editable?: boolean;
defaultValue?: string;
placeholder?: string;
maxLength?: Int32;
placeholderTextColor?: ColorValue;
mentionIndicators: string[];
cursorColor?: ColorValue;
Expand All @@ -382,6 +387,7 @@ export interface NativeProps extends ViewProps {
onRequestHtmlResult?: DirectEventHandler<OnRequestHtmlResultEvent>;
onInputKeyPress?: DirectEventHandler<OnKeyPressEvent>;
onPasteImages?: DirectEventHandler<OnPasteImagesEvent>;
onMaxLengthExceeded?: DirectEventHandler<OnMaxLengthExceededEvent>;
onContextMenuItemPress?: DirectEventHandler<OnContextMenuItemPressEvent>;
onSubmitEditing?: BubblingEventHandler<OnSubmitEditing>;

Expand Down
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,10 @@ export interface OnPasteImagesEvent {
}[];
}

export interface OnMaxLengthExceededEvent {
maxLength: number;
}

export interface OnSubmitEditing {
text: string;
}
Expand Down Expand Up @@ -445,6 +449,7 @@ export interface EnrichedTextInputProps extends Omit<ViewProps, 'children'> {
mentionIndicators?: string[];
defaultValue?: string;
placeholder?: string;
maxLength?: number;
placeholderTextColor?: ColorValue;
cursorColor?: ColorValue;
selectionColor?: ColorValue;
Expand All @@ -470,6 +475,9 @@ export interface EnrichedTextInputProps extends Omit<ViewProps, 'children'> {
onKeyPress?: (e: NativeSyntheticEvent<OnKeyPressEvent>) => void;
onSubmitEditing?: (e: NativeSyntheticEvent<OnSubmitEditing>) => void;
onPasteImages?: (e: NativeSyntheticEvent<OnPasteImagesEvent>) => void;
onMaxLengthExceeded?: (
e: NativeSyntheticEvent<OnMaxLengthExceededEvent>
) => void;
contextMenuItems?: ContextMenuItem[];
/**
* If true, Android will use experimental synchronous events.
Expand Down