react-native/React/Base/RCTEventDispatcher.m
Spencer Ahrens 961c1eb429 [ReactNative] TextInput bug fixes and features
Summary:
This introduces event counts to make sure JS doesn't set out of date values on
native text inputs, which can cause dropped characters and can mess with
autocomplete, and obviates the need for the input buffering which added lag and
complexity to the component.  Made sure to test simulated super-slow JS text
event processing to make sure characters aren't dropped, as well as typing
obviously correctable words and making sure autocomplete works as expected.

TextInput is now a controlled input by default without causing any issues for
most cases, so I removed the `controlled` prop.

Fixes selection state jumping by restoring it after setting new text values, so
highlighting the middle of some text in the new ReWrite example and hitting
space will replace that selection with an underscore and keep the cursor at a
sensible position as expected, instead of jumping to the end.

Ads `maxLength` prop to support the most commonly needed syncronous behavior:
preventing the user from typing too many characters.  It can also be used to
prevent users from continuing to type after entering special characters by
changing it to the current length after a regex match.  Made sure to verify it
works well with pasted input (including in the middle of existing text),
truncating it and collapsing the selection the same way it does on the web.

Fixes bug in TextEventsExample where it wouldn't show the submit and end events,
even though there were firing correctly.
2015-07-21 12:45:07 -08:00

197 lines
4.1 KiB
Objective-C

/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
#import "RCTEventDispatcher.h"
#import "RCTAssert.h"
#import "RCTBridge.h"
const NSInteger RCTTextUpdateLagWarningThreshold = 3;
static NSNumber *RCTGetEventID(id<RCTEvent> event)
{
return @(
[event.viewTag intValue] |
(((uint64_t)event.eventName.hash & 0xFFFF) << 32) |
(((uint64_t)event.coalescingKey) << 48)
);
}
@implementation RCTBaseEvent
@synthesize viewTag = _viewTag;
@synthesize eventName = _eventName;
@synthesize body = _body;
- (instancetype)initWithViewTag:(NSNumber *)viewTag
eventName:(NSString *)eventName
body:(NSDictionary *)body
{
RCTAssertParam(eventName);
if ((self = [super init])) {
_viewTag = viewTag;
_eventName = eventName;
_body = body;
}
return self;
}
RCT_NOT_IMPLEMENTED(-init)
- (uint16_t)coalescingKey
{
return 0;
}
- (BOOL)canCoalesce
{
return YES;
}
- (id<RCTEvent>)coalesceWithEvent:(id<RCTEvent>)newEvent
{
return newEvent;
}
+ (NSString *)moduleDotMethod
{
return nil;
}
@end
@interface RCTEventDispatcher() <RCTBridgeModule, RCTFrameUpdateObserver>
@end
@implementation RCTEventDispatcher
{
NSMutableDictionary *_eventQueue;
NSLock *_eventQueueLock;
}
@synthesize bridge = _bridge;
@synthesize paused = _paused;
RCT_EXPORT_MODULE()
- (instancetype)init
{
if ((self = [super init])) {
_eventQueue = [[NSMutableDictionary alloc] init];
_eventQueueLock = [[NSLock alloc] init];
}
return self;
}
- (void)sendAppEventWithName:(NSString *)name body:(id)body
{
[_bridge enqueueJSCall:@"RCTNativeAppEventEmitter.emit"
args:body ? @[name, body] : @[name]];
}
- (void)sendDeviceEventWithName:(NSString *)name body:(id)body
{
[_bridge enqueueJSCall:@"RCTDeviceEventEmitter.emit"
args:body ? @[name, body] : @[name]];
}
- (void)sendInputEventWithName:(NSString *)name body:(NSDictionary *)body
{
RCTAssert([body[@"target"] isKindOfClass:[NSNumber class]],
@"Event body dictionary must include a 'target' property containing a React tag");
[_bridge enqueueJSCall:@"RCTEventEmitter.receiveEvent"
args:body ? @[body[@"target"], name, body] : @[body[@"target"], name]];
}
- (void)sendTextEventWithType:(RCTTextEventType)type
reactTag:(NSNumber *)reactTag
text:(NSString *)text
eventCount:(NSInteger)eventCount
{
static NSString *events[] = {
@"topFocus",
@"topBlur",
@"topChange",
@"topSubmitEditing",
@"topEndEditing",
};
[self sendInputEventWithName:events[type] body:text ? @{
@"text": text,
@"eventCount": @(eventCount),
@"target": reactTag
} : @{
@"eventCount": @(eventCount),
@"target": reactTag
}];
}
- (void)sendEvent:(id<RCTEvent>)event
{
if (!event.canCoalesce) {
[self dispatchEvent:event];
return;
}
[_eventQueueLock lock];
NSNumber *eventID = RCTGetEventID(event);
id<RCTEvent> previousEvent = _eventQueue[eventID];
if (previousEvent) {
event = [previousEvent coalesceWithEvent:event];
}
_eventQueue[eventID] = event;
_paused = NO;
[_eventQueueLock unlock];
}
- (void)dispatchEvent:(id<RCTEvent>)event
{
NSMutableArray *arguments = [[NSMutableArray alloc] init];
if (event.viewTag) {
[arguments addObject:event.viewTag];
}
[arguments addObject:event.eventName];
if (event.body) {
[arguments addObject:event.body];
}
[_bridge enqueueJSCall:[[event class] moduleDotMethod]
args:arguments];
}
- (dispatch_queue_t)methodQueue
{
return RCTJSThread;
}
- (void)didUpdateFrame:(__unused RCTFrameUpdate *)update
{
[_eventQueueLock lock];
NSDictionary *eventQueue = _eventQueue;
_eventQueue = [[NSMutableDictionary alloc] init];
_paused = YES;
[_eventQueueLock unlock];
for (id<RCTEvent> event in eventQueue.allValues) {
[self dispatchEvent:event];
}
}
@end