commit
d937071517
|
@ -15,6 +15,7 @@
|
|||
00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */; };
|
||||
00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */; };
|
||||
00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */; };
|
||||
00E356F31AD99517003FC87E /* SampleAppTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* SampleAppTests.m */; };
|
||||
133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 78C398B91ACF4ADC00677621 /* libRCTLinking.a */; };
|
||||
13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; };
|
||||
13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; };
|
||||
|
@ -74,6 +75,13 @@
|
|||
remoteGlobalIDString = 832C81801AAF6DEF007FA2F7;
|
||||
remoteInfo = RCTVibration;
|
||||
};
|
||||
00E356F41AD99517003FC87E /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 13B07F861A680F5B00A75B9A;
|
||||
remoteInfo = SampleApp;
|
||||
};
|
||||
146834031AC3E56700842450 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */;
|
||||
|
@ -106,6 +114,9 @@
|
|||
00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = ../../Libraries/Image/RCTImage.xcodeproj; sourceTree = "<group>"; };
|
||||
00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTNetwork.xcodeproj; path = ../../Libraries/Network/RCTNetwork.xcodeproj; sourceTree = "<group>"; };
|
||||
00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = ../../Libraries/Vibration/RCTVibration.xcodeproj; sourceTree = "<group>"; };
|
||||
00E356EE1AD99517003FC87E /* SampleAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SampleAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
00E356F21AD99517003FC87E /* SampleAppTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SampleAppTests.m; sourceTree = "<group>"; };
|
||||
13B07F961A680F5B00A75B9A /* SampleApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SampleApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = iOS/AppDelegate.h; sourceTree = "<group>"; };
|
||||
13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = iOS/AppDelegate.m; sourceTree = "<group>"; };
|
||||
|
@ -119,6 +130,13 @@
|
|||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
00E356EB1AD99517003FC87E /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
@ -195,6 +213,23 @@
|
|||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
00E356EF1AD99517003FC87E /* SampleAppTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
00E356F21AD99517003FC87E /* SampleAppTests.m */,
|
||||
00E356F01AD99517003FC87E /* Supporting Files */,
|
||||
);
|
||||
path = SampleAppTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
00E356F01AD99517003FC87E /* Supporting Files */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
00E356F11AD99517003FC87E /* Info.plist */,
|
||||
);
|
||||
name = "Supporting Files";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
13B07FAE1A68108700A75B9A /* SampleApp */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -255,6 +290,7 @@
|
|||
children = (
|
||||
13B07FAE1A68108700A75B9A /* SampleApp */,
|
||||
832341AE1AAA6A7D00B99B32 /* Libraries */,
|
||||
00E356EF1AD99517003FC87E /* SampleAppTests */,
|
||||
83CBBA001A601CBA00E9B192 /* Products */,
|
||||
);
|
||||
indentWidth = 2;
|
||||
|
@ -265,6 +301,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
13B07F961A680F5B00A75B9A /* SampleApp.app */,
|
||||
00E356EE1AD99517003FC87E /* SampleAppTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
|
@ -272,6 +309,24 @@
|
|||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
00E356ED1AD99517003FC87E /* SampleAppTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "SampleAppTests" */;
|
||||
buildPhases = (
|
||||
00E356EA1AD99517003FC87E /* Sources */,
|
||||
00E356EB1AD99517003FC87E /* Frameworks */,
|
||||
00E356EC1AD99517003FC87E /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
00E356F51AD99517003FC87E /* PBXTargetDependency */,
|
||||
);
|
||||
name = SampleAppTests;
|
||||
productName = SampleAppTests;
|
||||
productReference = 00E356EE1AD99517003FC87E /* SampleAppTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
13B07F861A680F5B00A75B9A /* SampleApp */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "SampleApp" */;
|
||||
|
@ -297,6 +352,12 @@
|
|||
attributes = {
|
||||
LastUpgradeCheck = 0610;
|
||||
ORGANIZATIONNAME = Facebook;
|
||||
TargetAttributes = {
|
||||
00E356ED1AD99517003FC87E = {
|
||||
CreatedOnToolsVersion = 6.2;
|
||||
TestTargetID = 13B07F861A680F5B00A75B9A;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "SampleApp" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
|
@ -354,6 +415,7 @@
|
|||
projectRoot = "";
|
||||
targets = (
|
||||
13B07F861A680F5B00A75B9A /* SampleApp */,
|
||||
00E356ED1AD99517003FC87E /* SampleAppTests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
@ -432,6 +494,13 @@
|
|||
/* End PBXReferenceProxy section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
00E356EC1AD99517003FC87E /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
@ -445,6 +514,14 @@
|
|||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
00E356EA1AD99517003FC87E /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
00E356F31AD99517003FC87E /* SampleAppTests.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
13B07F871A680F5B00A75B9A /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
|
@ -456,6 +533,14 @@
|
|||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
00E356F51AD99517003FC87E /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 13B07F861A680F5B00A75B9A /* SampleApp */;
|
||||
targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = {
|
||||
isa = PBXVariantGroup;
|
||||
|
@ -469,6 +554,43 @@
|
|||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
00E356F61AD99517003FC87E /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/Developer/Library/Frameworks",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
INFOPLIST_FILE = SampleAppTests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.2;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SampleApp.app/SampleApp";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
00E356F71AD99517003FC87E /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/Developer/Library/Frameworks",
|
||||
"$(inherited)",
|
||||
);
|
||||
INFOPLIST_FILE = SampleAppTests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 8.2;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SampleApp.app/SampleApp";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
13B07F941A680F5B00A75B9A /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
|
@ -590,6 +712,15 @@
|
|||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "SampleAppTests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
00E356F61AD99517003FC87E /* Debug */,
|
||||
00E356F71AD99517003FC87E /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "SampleApp" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
|
|
@ -20,6 +20,20 @@
|
|||
ReferencedContainer = "container:SampleApp.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
|
||||
BuildableName = "SampleAppTests.xctest"
|
||||
BlueprintName = "SampleAppTests"
|
||||
ReferencedContainer = "container:SampleApp.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
|
@ -28,6 +42,16 @@
|
|||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
buildConfiguration = "Debug">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "00E356ED1AD99517003FC87E"
|
||||
BuildableName = "SampleAppTests.xctest"
|
||||
BlueprintName = "SampleAppTests"
|
||||
ReferencedContainer = "container:SampleApp.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>BNDL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* 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 <UIKit/UIKit.h>
|
||||
#import <XCTest/XCTest.h>
|
||||
|
||||
#import "RCTAssert.h"
|
||||
#import "RCTRedBox.h"
|
||||
#import "RCTRootView.h"
|
||||
|
||||
#define TIMEOUT_SECONDS 240
|
||||
#define TEXT_TO_LOOK_FOR @"Welcome to React Native!"
|
||||
|
||||
@interface SampleAppTests : XCTestCase
|
||||
|
||||
@end
|
||||
|
||||
@implementation SampleAppTests
|
||||
|
||||
|
||||
- (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test
|
||||
{
|
||||
if (test(view)) {
|
||||
return YES;
|
||||
}
|
||||
for (UIView *subview in [view subviews]) {
|
||||
if ([self findSubviewInView:subview matching:test]) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)testRendersWelcomeScreen {
|
||||
UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController];
|
||||
NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS];
|
||||
BOOL foundElement = NO;
|
||||
NSString *redboxError = nil;
|
||||
|
||||
while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) {
|
||||
[[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:date];
|
||||
[[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:date];
|
||||
|
||||
redboxError = [[RCTRedBox sharedInstance] currentErrorMessage];
|
||||
|
||||
foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) {
|
||||
if ([view respondsToSelector:@selector(attributedText)]) {
|
||||
NSString *text = [(id)view attributedText].string;
|
||||
if ([text isEqualToString:TEXT_TO_LOOK_FOR]) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}];
|
||||
}
|
||||
|
||||
XCTAssertNil(redboxError, @"RedBox error: %@", redboxError);
|
||||
XCTAssertTrue(foundElement, @"Cound't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS);
|
||||
}
|
||||
|
||||
|
||||
@end
|
|
@ -226,10 +226,8 @@ exports.examples = [
|
|||
Contain
|
||||
</Text>
|
||||
<Image
|
||||
style={[
|
||||
styles.resizeMode,
|
||||
{resizeMode: Image.resizeMode.contain}
|
||||
]}
|
||||
style={styles.resizeMode}
|
||||
resizeMode={Image.resizeMode.contain}
|
||||
source={fullImage}
|
||||
/>
|
||||
</View>
|
||||
|
@ -238,10 +236,8 @@ exports.examples = [
|
|||
Cover
|
||||
</Text>
|
||||
<Image
|
||||
style={[
|
||||
styles.resizeMode,
|
||||
{resizeMode: Image.resizeMode.cover}
|
||||
]}
|
||||
style={styles.resizeMode}
|
||||
resizeMode={Image.resizeMode.cover}
|
||||
source={fullImage}
|
||||
/>
|
||||
</View>
|
||||
|
@ -250,10 +246,8 @@ exports.examples = [
|
|||
Stretch
|
||||
</Text>
|
||||
<Image
|
||||
style={[
|
||||
styles.resizeMode,
|
||||
{resizeMode: Image.resizeMode.stretch}
|
||||
]}
|
||||
style={styles.resizeMode}
|
||||
resizeMode={Image.resizeMode.stretch}
|
||||
source={fullImage}
|
||||
/>
|
||||
</View>
|
||||
|
|
|
@ -74,7 +74,8 @@ var BatchedBridgeFactory = {
|
|||
setLoggingEnabled: messageQueue.setLoggingEnabled.bind(messageQueue),
|
||||
getLoggedOutgoingItems: messageQueue.getLoggedOutgoingItems.bind(messageQueue),
|
||||
getLoggedIncomingItems: messageQueue.getLoggedIncomingItems.bind(messageQueue),
|
||||
replayPreviousLog: messageQueue.replayPreviousLog.bind(messageQueue)
|
||||
replayPreviousLog: messageQueue.replayPreviousLog.bind(messageQueue),
|
||||
processBatch: messageQueue.processBatch.bind(messageQueue),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
@ -17,7 +17,6 @@ var PointPropType = require('PointPropType');
|
|||
var RCTScrollView = require('NativeModules').UIManager.RCTScrollView;
|
||||
var RCTScrollViewConsts = RCTScrollView.Constants;
|
||||
var React = require('React');
|
||||
var ReactIOSTagHandles = require('ReactIOSTagHandles');
|
||||
var ReactIOSViewAttributes = require('ReactIOSViewAttributes');
|
||||
var RCTUIManager = require('NativeModules').UIManager;
|
||||
var ScrollResponder = require('ScrollResponder');
|
||||
|
@ -32,6 +31,7 @@ var flattenStyle = require('flattenStyle');
|
|||
var insetsDiffer = require('insetsDiffer');
|
||||
var invariant = require('invariant');
|
||||
var pointsDiffer = require('pointsDiffer');
|
||||
var requireNativeComponent = require('requireNativeComponent');
|
||||
|
||||
var PropTypes = React.PropTypes;
|
||||
|
||||
|
@ -73,6 +73,12 @@ var ScrollView = React.createClass({
|
|||
* the `alwaysBounce*` props are true. The default value is true.
|
||||
*/
|
||||
bounces: PropTypes.bool,
|
||||
/**
|
||||
* When true, gestures can drive zoom past min/max and the zoom will animate
|
||||
* to the min/max value at gesture end, otherwise the zoom will not exceed
|
||||
* the limits.
|
||||
*/
|
||||
bouncesZoom: PropTypes.bool,
|
||||
/**
|
||||
* When true, the scroll view bounces horizontally when it reaches the end
|
||||
* even if the content is smaller than the scroll view itself. The default
|
||||
|
@ -120,6 +126,16 @@ var ScrollView = React.createClass({
|
|||
* instead of vertically in a column. The default value is false.
|
||||
*/
|
||||
horizontal: PropTypes.bool,
|
||||
/**
|
||||
* When true, the ScrollView will try to lock to only vertical or horizontal
|
||||
* scrolling while dragging. The default value is false.
|
||||
*/
|
||||
directionalLockEnabled: PropTypes.bool,
|
||||
/**
|
||||
* When false, once tracking starts, won't try to drag if the touch moves.
|
||||
* The default value is true.
|
||||
*/
|
||||
canCancelContentTouches: PropTypes.bool,
|
||||
/**
|
||||
* Determines whether the keyboard gets dismissed in response to a drag.
|
||||
* - 'none' (the default), drags do not dismiss the keyboard.
|
||||
|
@ -359,6 +375,7 @@ if (Platform.OS === 'android') {
|
|||
validAttributes: validAttributes,
|
||||
uiViewClassName: 'RCTScrollView',
|
||||
});
|
||||
var RCTScrollView = requireNativeComponent('RCTScrollView', ScrollView);
|
||||
}
|
||||
|
||||
module.exports = ScrollView;
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
'use strict';
|
||||
|
||||
var NativeMethodsMixin = require('NativeMethodsMixin');
|
||||
var Platform = require('Platform');
|
||||
var PropTypes = require('ReactPropTypes');
|
||||
var React = require('React');
|
||||
var ReactIOSViewAttributes = require('ReactIOSViewAttributes');
|
||||
|
@ -21,6 +22,7 @@ var View = require('View');
|
|||
var createReactIOSNativeComponentClass =
|
||||
require('createReactIOSNativeComponentClass');
|
||||
var merge = require('merge');
|
||||
var requireNativeComponent = require('requireNativeComponent');
|
||||
|
||||
type Event = Object;
|
||||
|
||||
|
@ -96,16 +98,20 @@ var styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
var validAttributes = {
|
||||
...ReactIOSViewAttributes.UIView,
|
||||
value: true,
|
||||
minimumValue: true,
|
||||
maximumValue: true,
|
||||
};
|
||||
if (Platform.OS === 'ios') {
|
||||
var RCTSlider = requireNativeComponent('RCTSlider', SliderIOS);
|
||||
} else {
|
||||
var validAttributes = {
|
||||
...ReactIOSViewAttributes.UIView,
|
||||
value: true,
|
||||
minimumValue: true,
|
||||
maximumValue: true,
|
||||
};
|
||||
|
||||
var RCTSlider = createReactIOSNativeComponentClass({
|
||||
validAttributes: validAttributes,
|
||||
uiViewClassName: 'RCTSlider',
|
||||
});
|
||||
var RCTSlider = createReactIOSNativeComponentClass({
|
||||
validAttributes: validAttributes,
|
||||
uiViewClassName: 'RCTSlider',
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = SliderIOS;
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
var Dimensions = require('Dimensions');
|
||||
var React = require('React');
|
||||
var View = require('View');
|
||||
var StyleSheet = require('StyleSheet');
|
||||
|
@ -33,8 +32,10 @@ var styles = StyleSheet.create({
|
|||
tab: {
|
||||
// TODO(5405356): Implement overflow: visible so position: absolute isn't useless
|
||||
// position: 'absolute',
|
||||
width: Dimensions.get('window').width,
|
||||
height: Dimensions.get('window').height,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
borderColor: 'red',
|
||||
borderWidth: 1,
|
||||
}
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
var Image = require('Image');
|
||||
var React = require('React');
|
||||
var ReactIOSViewAttributes = require('ReactIOSViewAttributes');
|
||||
var Dimensions = require('Dimensions');
|
||||
var StaticContainer = require('StaticContainer.react');
|
||||
var StyleSheet = require('StyleSheet');
|
||||
var View = require('View');
|
||||
|
@ -134,8 +133,10 @@ var TabBarItemIOS = React.createClass({
|
|||
var styles = StyleSheet.create({
|
||||
tab: {
|
||||
position: 'absolute',
|
||||
width: Dimensions.get('window').width,
|
||||
height: Dimensions.get('window').height,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -72,7 +72,7 @@ var notMultiline = {
|
|||
onSubmitEditing: true,
|
||||
};
|
||||
|
||||
var TextInputAndroidAttributes = {
|
||||
var AndroidTextInputAttributes = {
|
||||
autoCapitalize: true,
|
||||
autoCorrect: true,
|
||||
autoFocus: true,
|
||||
|
@ -80,14 +80,19 @@ var TextInputAndroidAttributes = {
|
|||
multiline: true,
|
||||
password: true,
|
||||
placeholder: true,
|
||||
value: true,
|
||||
text: true,
|
||||
testID: true,
|
||||
};
|
||||
|
||||
var AndroidTextInput = createReactIOSNativeComponentClass({
|
||||
validAttributes: TextInputAndroidAttributes,
|
||||
var viewConfigIOS = {
|
||||
uiViewClassName: 'RCTTextField',
|
||||
validAttributes: RCTTextFieldAttributes,
|
||||
};
|
||||
|
||||
var viewConfigAndroid = {
|
||||
uiViewClassName: 'AndroidTextInput',
|
||||
});
|
||||
validAttributes: AndroidTextInputAttributes,
|
||||
};
|
||||
|
||||
var crossPlatformKeyboardTypeMap = {
|
||||
'numeric': 'decimal-pad',
|
||||
|
@ -293,10 +298,8 @@ var TextInput = React.createClass({
|
|||
*/
|
||||
mixins: [NativeMethodsMixin, TimerMixin],
|
||||
|
||||
viewConfig: {
|
||||
uiViewClassName: 'RCTTextField',
|
||||
validAttributes: RCTTextFieldAttributes,
|
||||
},
|
||||
viewConfig: ((Platform.OS === 'ios' ? viewConfigIOS :
|
||||
(Platform.OS === 'android' ? viewConfigAndroid : {})) : Object),
|
||||
|
||||
isFocused: function(): boolean {
|
||||
return TextInputState.currentlyFocusedField() ===
|
||||
|
@ -521,7 +524,7 @@ var TextInput = React.createClass({
|
|||
onSubmitEditing={this.props.onSubmitEditing}
|
||||
password={this.props.password || this.props.secureTextEntry}
|
||||
placeholder={this.props.placeholder}
|
||||
value={this.state.bufferedValue}
|
||||
text={this.state.bufferedValue}
|
||||
/>;
|
||||
|
||||
return (
|
||||
|
@ -591,4 +594,9 @@ var RCTTextField = createReactIOSNativeComponentClass({
|
|||
uiViewClassName: 'RCTTextField',
|
||||
});
|
||||
|
||||
var AndroidTextInput = createReactIOSNativeComponentClass({
|
||||
validAttributes: AndroidTextInputAttributes,
|
||||
uiViewClassName: 'AndroidTextInput',
|
||||
});
|
||||
|
||||
module.exports = TextInput;
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* Common implementation for a simple stubbed view. Simply applies the view's styles to the inner
|
||||
* View component and renders its children.
|
||||
*
|
||||
* @providesModule UnimplementedView
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var React = require('React');
|
||||
var StyleSheet = require('StyleSheet');
|
||||
var View = require('View');
|
||||
|
||||
var UnimplementedView = React.createClass({
|
||||
setNativeProps: function() {
|
||||
// Do nothing.
|
||||
// This method is required in order to use this view as a Touchable* child.
|
||||
// See ensureComponentIsNative.js for more info
|
||||
},
|
||||
render: function() {
|
||||
return (
|
||||
<View style={[styles.unimplementedView, this.props.style]}>
|
||||
{this.props.children}
|
||||
</View>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
unimplementedView: {
|
||||
borderWidth: 1,
|
||||
borderColor: 'red',
|
||||
alignSelf: 'flex-start',
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = UnimplementedView;
|
|
@ -35,6 +35,7 @@ var NavigatorNavigationBar = require('NavigatorNavigationBar');
|
|||
var NavigatorSceneConfigs = require('NavigatorSceneConfigs');
|
||||
var NavigatorStaticContextContainer = require('NavigatorStaticContextContainer');
|
||||
var PanResponder = require('PanResponder');
|
||||
var Platform = require('Platform');
|
||||
var React = require('React');
|
||||
var StaticContainer = require('StaticContainer.react');
|
||||
var StyleSheet = require('StyleSheet');
|
||||
|
@ -287,9 +288,10 @@ var Navigator = React.createClass({
|
|||
),
|
||||
idStack: routeStack.map(() => getuid()),
|
||||
routeStack,
|
||||
// These are tracked to avoid rendering everything all the time.
|
||||
updatingRangeStart: initialRouteIndex,
|
||||
updatingRangeLength: initialRouteIndex + 1,
|
||||
// `updatingRange*` allows us to only render the visible or staged scenes
|
||||
// On first render, we will render every scene in the initialRouteStack
|
||||
updatingRangeStart: 0,
|
||||
updatingRangeLength: routeStack.length,
|
||||
// Either animating or gesturing.
|
||||
isAnimating: false,
|
||||
jumpToIndex: routeStack.length - 1,
|
||||
|
@ -397,22 +399,22 @@ var Navigator = React.createClass({
|
|||
this._emitDidFocus(this.state.routeStack[this.state.presentedIndex]);
|
||||
if (this.parentNavigator) {
|
||||
this.parentNavigator.setHandler(this._handleRequest);
|
||||
} else {
|
||||
} else if (Platform.OS === 'android') {
|
||||
// There is no navigator in our props or context, so this is the
|
||||
// top-level navigator. We will handle back button presses here
|
||||
BackAndroid.addEventListener('hardwareBackPress', this._handleBackPress);
|
||||
BackAndroid.addEventListener('hardwareBackPress', this._handleAndroidBackPress);
|
||||
}
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
if (this.parentNavigator) {
|
||||
this.parentNavigator.setHandler(null);
|
||||
} else {
|
||||
BackAndroid.removeEventListener('hardwareBackPress', this._handleBackPress);
|
||||
} else if (Platform.OS === 'android') {
|
||||
BackAndroid.removeEventListener('hardwareBackPress', this._handleAndroidBackPress);
|
||||
}
|
||||
},
|
||||
|
||||
_handleBackPress: function() {
|
||||
_handleAndroidBackPress: function() {
|
||||
var didPop = this.pop();
|
||||
if (!didPop) {
|
||||
BackAndroid.exitApp();
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
var EdgeInsetsPropType = require('EdgeInsetsPropType');
|
||||
var NativeMethodsMixin = require('NativeMethodsMixin');
|
||||
var NativeModules = require('NativeModules');
|
||||
var Platform = require('Platform');
|
||||
var PropTypes = require('ReactPropTypes');
|
||||
var ImageResizeMode = require('ImageResizeMode');
|
||||
var ImageStylePropTypes = require('ImageStylePropTypes');
|
||||
|
@ -27,7 +28,9 @@ var flattenStyle = require('flattenStyle');
|
|||
var insetsDiffer = require('insetsDiffer');
|
||||
var invariant = require('invariant');
|
||||
var merge = require('merge');
|
||||
var requireNativeComponent = require('requireNativeComponent');
|
||||
var warning = require('warning');
|
||||
var verifyPropTypes = require('verifyPropTypes');
|
||||
|
||||
/**
|
||||
* A react component for displaying different types of images,
|
||||
|
@ -64,6 +67,13 @@ var Image = React.createClass({
|
|||
source: PropTypes.shape({
|
||||
uri: PropTypes.string,
|
||||
}),
|
||||
/**
|
||||
* A static image to display while downloading the final image off the
|
||||
* network.
|
||||
*/
|
||||
defaultSource: PropTypes.shape({
|
||||
uri: PropTypes.string,
|
||||
}),
|
||||
/**
|
||||
* Whether this element should be revealed as an accessible element.
|
||||
*/
|
||||
|
@ -80,6 +90,11 @@ var Image = React.createClass({
|
|||
* [Apple documentation](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIImage_Class/index.html#//apple_ref/occ/instm/UIImage/resizableImageWithCapInsets)
|
||||
*/
|
||||
capInsets: EdgeInsetsPropType,
|
||||
/**
|
||||
* Determines how to resize the image when the frame doesn't match the raw
|
||||
* image dimensions.
|
||||
*/
|
||||
resizeMode: PropTypes.oneOf(['cover', 'contain', 'stretch']),
|
||||
style: StyleSheetPropType(ImageStylePropTypes),
|
||||
/**
|
||||
* A unique identifier for this element to be used in UI Automation
|
||||
|
@ -104,6 +119,12 @@ var Image = React.createClass({
|
|||
},
|
||||
|
||||
render: function() {
|
||||
for (var prop in nativeOnlyProps) {
|
||||
if (this.props[prop] !== undefined) {
|
||||
console.warn('Prop `' + prop + ' = ' + this.props[prop] + '` should ' +
|
||||
'not be set directly on Image.');
|
||||
}
|
||||
}
|
||||
var style = flattenStyle([styles.base, this.props.style]);
|
||||
invariant(style, "style must be initialized");
|
||||
var source = this.props.source;
|
||||
|
@ -119,28 +140,36 @@ var Image = React.createClass({
|
|||
if (this.props.style && this.props.style.tintColor) {
|
||||
warning(RawImage === RCTStaticImage, 'tintColor style only supported on static images.');
|
||||
}
|
||||
|
||||
var resizeMode = this.props.resizeMode || style.resizeMode;
|
||||
var contentModes = NativeModules.UIManager.UIView.ContentMode;
|
||||
var resizeMode;
|
||||
if (style.resizeMode === ImageResizeMode.stretch) {
|
||||
resizeMode = contentModes.ScaleToFill;
|
||||
} else if (style.resizeMode === ImageResizeMode.contain) {
|
||||
resizeMode = contentModes.ScaleAspectFit;
|
||||
var contentMode;
|
||||
if (resizeMode === ImageResizeMode.stretch) {
|
||||
contentMode = contentModes.ScaleToFill;
|
||||
} else if (resizeMode === ImageResizeMode.contain) {
|
||||
contentMode = contentModes.ScaleAspectFit;
|
||||
} else { // ImageResizeMode.cover or undefined
|
||||
resizeMode = contentModes.ScaleAspectFill;
|
||||
contentMode = contentModes.ScaleAspectFill;
|
||||
}
|
||||
|
||||
var nativeProps = merge(this.props, {
|
||||
style,
|
||||
resizeMode,
|
||||
contentMode,
|
||||
tintColor: style.tintColor,
|
||||
});
|
||||
if (Platform.OS === 'android') {
|
||||
// TODO: update android native code to not need this
|
||||
nativeProps.resizeMode = contentMode;
|
||||
delete nativeProps.contentMode;
|
||||
}
|
||||
|
||||
if (isStored) {
|
||||
nativeProps.imageTag = source.uri;
|
||||
} else {
|
||||
nativeProps.src = source.uri;
|
||||
}
|
||||
if (this.props.defaultSource) {
|
||||
nativeProps.defaultImageSrc = this.props.defaultSource.uri;
|
||||
}
|
||||
return <RawImage {...nativeProps} />;
|
||||
}
|
||||
});
|
||||
|
@ -151,24 +180,39 @@ var styles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
var CommonImageViewAttributes = merge(ReactIOSViewAttributes.UIView, {
|
||||
accessible: true,
|
||||
accessibilityLabel: true,
|
||||
capInsets: {diff: insetsDiffer}, // UIEdgeInsets=UIEdgeInsetsZero
|
||||
imageTag: true,
|
||||
resizeMode: true,
|
||||
if (Platform.OS === 'android') {
|
||||
var CommonImageViewAttributes = merge(ReactIOSViewAttributes.UIView, {
|
||||
accessible: true,
|
||||
accessibilityLabel: true,
|
||||
capInsets: {diff: insetsDiffer}, // UIEdgeInsets=UIEdgeInsetsZero
|
||||
imageTag: true,
|
||||
resizeMode: true,
|
||||
src: true,
|
||||
testID: PropTypes.string,
|
||||
});
|
||||
|
||||
var RCTStaticImage = createReactIOSNativeComponentClass({
|
||||
validAttributes: merge(CommonImageViewAttributes, { tintColor: true }),
|
||||
uiViewClassName: 'RCTStaticImage',
|
||||
});
|
||||
|
||||
var RCTNetworkImage = createReactIOSNativeComponentClass({
|
||||
validAttributes: merge(CommonImageViewAttributes, { defaultImageSrc: true }),
|
||||
uiViewClassName: 'RCTNetworkImageView',
|
||||
});
|
||||
} else {
|
||||
var RCTStaticImage = requireNativeComponent('RCTStaticImage', null);
|
||||
var RCTNetworkImage = requireNativeComponent('RCTNetworkImageView', null);
|
||||
}
|
||||
var nativeOnlyProps = {
|
||||
src: true,
|
||||
testID: PropTypes.string,
|
||||
});
|
||||
|
||||
var RCTStaticImage = createReactIOSNativeComponentClass({
|
||||
validAttributes: merge(CommonImageViewAttributes, { tintColor: true }),
|
||||
uiViewClassName: 'RCTStaticImage',
|
||||
});
|
||||
|
||||
var RCTNetworkImage = createReactIOSNativeComponentClass({
|
||||
validAttributes: merge(CommonImageViewAttributes, { defaultImageSrc: true }),
|
||||
uiViewClassName: 'RCTNetworkImageView',
|
||||
});
|
||||
defaultImageSrc: true,
|
||||
imageTag: true,
|
||||
contentMode: true,
|
||||
};
|
||||
if (__DEV__) {
|
||||
verifyPropTypes(Image, RCTStaticImage.viewConfig, nativeOnlyProps);
|
||||
verifyPropTypes(Image, RCTNetworkImage.viewConfig, nativeOnlyProps);
|
||||
}
|
||||
|
||||
module.exports = Image;
|
||||
|
|
|
@ -29,6 +29,6 @@ RCT_EXPORT_MODULE()
|
|||
|
||||
RCT_REMAP_VIEW_PROPERTY(defaultImageSrc, defaultImage, UIImage)
|
||||
RCT_REMAP_VIEW_PROPERTY(src, imageURL, NSURL)
|
||||
RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode, UIViewContentMode)
|
||||
RCT_EXPORT_VIEW_PROPERTY(contentMode, UIViewContentMode)
|
||||
|
||||
@end
|
||||
|
|
|
@ -26,7 +26,7 @@ RCT_EXPORT_MODULE()
|
|||
}
|
||||
|
||||
RCT_EXPORT_VIEW_PROPERTY(capInsets, UIEdgeInsets)
|
||||
RCT_REMAP_VIEW_PROPERTY(resizeMode, contentMode, UIViewContentMode)
|
||||
RCT_EXPORT_VIEW_PROPERTY(contentMode, UIViewContentMode)
|
||||
RCT_CUSTOM_VIEW_PROPERTY(src, NSURL, RCTStaticImage)
|
||||
{
|
||||
if (json) {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*
|
||||
* @providesModule loadSourceMap
|
||||
*
|
||||
* -- disabled flow due to mysterious validation errors --
|
||||
*/
|
||||
|
||||
|
|
|
@ -10,22 +10,23 @@
|
|||
* @flow
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
'use strict';
|
||||
|
||||
var ImageStylePropTypes = require('ImageStylePropTypes');
|
||||
var TextStylePropTypes = require('TextStylePropTypes');
|
||||
var ViewStylePropTypes = require('ViewStylePropTypes');
|
||||
|
||||
var deepDiffer = require('deepDiffer');
|
||||
var keyMirror = require('keyMirror');
|
||||
var matricesDiffer = require('matricesDiffer');
|
||||
var merge = require('merge');
|
||||
var sizesDiffer = require('sizesDiffer');
|
||||
|
||||
var ReactIOSStyleAttributes = merge(
|
||||
keyMirror(ViewStylePropTypes),
|
||||
keyMirror(TextStylePropTypes)
|
||||
);
|
||||
var ReactIOSStyleAttributes = {
|
||||
...keyMirror(ViewStylePropTypes),
|
||||
...keyMirror(TextStylePropTypes),
|
||||
...keyMirror(ImageStylePropTypes),
|
||||
};
|
||||
|
||||
ReactIOSStyleAttributes.transformMatrix = { diff: matricesDiffer };
|
||||
ReactIOSStyleAttributes.shadowOffset = { diff: deepDiffer };
|
||||
ReactIOSStyleAttributes.shadowOffset = { diff: sizesDiffer };
|
||||
|
||||
module.exports = ReactIOSStyleAttributes;
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* 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.
|
||||
*
|
||||
* @providesModule requireNativeComponent
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var RCTUIManager = require('NativeModules').UIManager;
|
||||
var UnimplementedView = require('UnimplementedView');
|
||||
|
||||
var createReactIOSNativeComponentClass = require('createReactIOSNativeComponentClass');
|
||||
var deepDiffer = require('deepDiffer');
|
||||
var insetsDiffer = require('insetsDiffer');
|
||||
var pointsDiffer = require('pointsDiffer');
|
||||
var matricesDiffer = require('matricesDiffer');
|
||||
var sizesDiffer = require('sizesDiffer');
|
||||
var verifyPropTypes = require('verifyPropTypes');
|
||||
|
||||
/**
|
||||
* Used to create React components that directly wrap native component
|
||||
* implementations. Config information is extracted from data exported from the
|
||||
* RCTUIManager module. You should also wrap the native component in a
|
||||
* hand-written component with full propTypes definitions and other
|
||||
* documentation - pass the hand-written component in as `wrapperComponent` to
|
||||
* verify all the native props are documented via `propTypes`.
|
||||
*
|
||||
* If some native props shouldn't be exposed in the wrapper interface, you can
|
||||
* pass null for `wrapperComponent` and call `verifyPropTypes` directly
|
||||
* with `nativePropsToIgnore`;
|
||||
*
|
||||
* Common types are lined up with the appropriate prop differs with
|
||||
* `TypeToDifferMap`. Non-scalar types not in the map default to `deepDiffer`.
|
||||
*/
|
||||
function requireNativeComponent(
|
||||
viewName: string,
|
||||
wrapperComponent: ?Function
|
||||
): Function {
|
||||
var viewConfig = RCTUIManager.viewConfigs && RCTUIManager.viewConfigs[viewName];
|
||||
if (!viewConfig) {
|
||||
return UnimplementedView;
|
||||
}
|
||||
var nativeProps = {
|
||||
...RCTUIManager.viewConfigs.RCTView.nativeProps,
|
||||
...viewConfig.nativeProps,
|
||||
};
|
||||
viewConfig.validAttributes = {};
|
||||
for (var key in nativeProps) {
|
||||
// TODO: deep diff by default in diffRawProperties instead of setting it here
|
||||
var differ = TypeToDifferMap[nativeProps[key].type] || deepDiffer;
|
||||
viewConfig.validAttributes[key] = {diff: differ};
|
||||
}
|
||||
if (__DEV__) {
|
||||
wrapperComponent && verifyPropTypes(wrapperComponent, viewConfig);
|
||||
}
|
||||
return createReactIOSNativeComponentClass(viewConfig);
|
||||
}
|
||||
|
||||
var TypeToDifferMap = {
|
||||
// iOS Types
|
||||
CATransform3D: matricesDiffer,
|
||||
CGPoint: pointsDiffer,
|
||||
CGSize: sizesDiffer,
|
||||
UIEdgeInsets: insetsDiffer,
|
||||
// Android Types
|
||||
// (not yet implemented)
|
||||
};
|
||||
|
||||
module.exports = requireNativeComponent;
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* 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.
|
||||
*
|
||||
* @providesModule verifyPropTypes
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var ReactIOSStyleAttributes = require('ReactIOSStyleAttributes');
|
||||
var View = require('View');
|
||||
|
||||
function verifyPropTypes(
|
||||
component: Function,
|
||||
viewConfig: Object,
|
||||
nativePropsToIgnore?: Object
|
||||
) {
|
||||
if (!viewConfig) {
|
||||
return; // This happens for UnimplementedView.
|
||||
}
|
||||
var nativeProps = viewConfig.nativeProps;
|
||||
for (var prop in viewConfig.nativeProps) {
|
||||
if (!component.propTypes[prop] &&
|
||||
!View.propTypes[prop] &&
|
||||
!ReactIOSStyleAttributes[prop] &&
|
||||
(!nativePropsToIgnore || !nativePropsToIgnore[prop])) {
|
||||
throw new Error(
|
||||
'`' + component.displayName + '` has no propType for native prop `' +
|
||||
viewConfig.uiViewClassName + '.' + prop + '` of native type `' +
|
||||
nativeProps[prop].type + '`'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = verifyPropTypes;
|
|
@ -17,6 +17,24 @@ var invariant = require('invariant');
|
|||
|
||||
var dimensions = NativeModules.UIManager.Dimensions;
|
||||
|
||||
// We calculate the window dimensions in JS so that we don't encounter loss of
|
||||
// precision in transferring the dimensions (which could be non-integers) over
|
||||
// the bridge.
|
||||
if (dimensions.windowPhysicalPixels) {
|
||||
// parse/stringify => Clone hack
|
||||
dimensions = JSON.parse(JSON.stringify(dimensions));
|
||||
|
||||
var windowPhysicalPixels = dimensions.windowPhysicalPixels;
|
||||
dimensions.window = {
|
||||
width: windowPhysicalPixels.width / windowPhysicalPixels.scale,
|
||||
height: windowPhysicalPixels.height / windowPhysicalPixels.scale,
|
||||
scale: windowPhysicalPixels.scale,
|
||||
};
|
||||
|
||||
// delete so no callers rely on this existing
|
||||
delete dimensions.windowPhysicalPixels;
|
||||
}
|
||||
|
||||
class Dimensions {
|
||||
/**
|
||||
* This should only be called from native code.
|
||||
|
|
|
@ -307,6 +307,25 @@ var MessageQueueMixin = {
|
|||
);
|
||||
},
|
||||
|
||||
processBatch: function (batch) {
|
||||
var self = this;
|
||||
batch.forEach(function (call) {
|
||||
invariant(
|
||||
call.module === 'BatchedBridge',
|
||||
'All the calls should pass through the BatchedBridge module'
|
||||
);
|
||||
if (call.method === 'callFunctionReturnFlushedQueue') {
|
||||
self.callFunction.apply(self, call.args);
|
||||
} else if (call.method === 'invokeCallbackAndReturnFlushedQueue') {
|
||||
self.invokeCallback.apply(self, call.args);
|
||||
} else {
|
||||
throw new Error(
|
||||
'Unrecognized method called on BatchedBridge: ' + call.method);
|
||||
}
|
||||
});
|
||||
return this.flushedQueue();
|
||||
},
|
||||
|
||||
setLoggingEnabled: function(enabled) {
|
||||
this._enableLogging = enabled;
|
||||
this._loggedIncomingItems = [];
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* Copyright 2004-present Facebook. All Rights Reserved.
|
||||
*
|
||||
* @providesModule sizesDiffer
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var dummySize = {w: undefined, h: undefined};
|
||||
|
||||
var sizesDiffer = function(one, two) {
|
||||
one = one || dummySize;
|
||||
two = two || dummySize;
|
||||
return one !== two && (
|
||||
one.w !== two.w ||
|
||||
one.h !== two.h
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = sizesDiffer;
|
|
@ -59,6 +59,7 @@ var ReactNative = Object.assign(Object.create(require('React')), {
|
|||
// Plugins
|
||||
DeviceEventEmitter: require('RCTDeviceEventEmitter'),
|
||||
NativeModules: require('NativeModules'),
|
||||
requireNativeComponent: require('requireNativeComponent'),
|
||||
|
||||
addons: {
|
||||
LinkedStateMixin: require('LinkedStateMixin'),
|
||||
|
|
|
@ -42,6 +42,45 @@ typedef NS_ENUM(NSUInteger, RCTBridgeFields) {
|
|||
RCTBridgeFieldFlushDateMillis
|
||||
};
|
||||
|
||||
/**
|
||||
* Temporarily allow to turn on and off the call batching in case someone wants
|
||||
* to profile both
|
||||
*/
|
||||
#define BATCHED_BRIDGE 1
|
||||
|
||||
#ifdef DEBUG
|
||||
|
||||
#define RCT_PROFILE_START() \
|
||||
_Pragma("clang diagnostic push") \
|
||||
_Pragma("clang diagnostic ignored \"-Wshadow\"") \
|
||||
NSTimeInterval __rct_profile_start = CACurrentMediaTime() \
|
||||
_Pragma("clang diagnostic pop")
|
||||
|
||||
#define RCT_PROFILE_END(cat, args, profileName...) \
|
||||
do { \
|
||||
if (_profile) { \
|
||||
[_profileLock lock]; \
|
||||
[_profile addObject:@{ \
|
||||
@"name": [@[profileName] componentsJoinedByString: @"_"], \
|
||||
@"cat": @ #cat, \
|
||||
@"ts": @((NSUInteger)((__rct_profile_start - _startingTime) * 1e6)), \
|
||||
@"dur": @((NSUInteger)((CACurrentMediaTime() - __rct_profile_start) * 1e6)), \
|
||||
@"ph": @"X", \
|
||||
@"pid": @([[NSProcessInfo processInfo] processIdentifier]), \
|
||||
@"tid": [[NSThread currentThread] description], \
|
||||
@"args": args ?: [NSNull null], \
|
||||
}]; \
|
||||
[_profileLock unlock]; \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
#else
|
||||
|
||||
#define RCT_PROFILE_START(...)
|
||||
#define RCT_PROFILE_END(...)
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef __LP64__
|
||||
typedef uint64_t RCTHeaderValue;
|
||||
typedef struct section_64 RCTHeaderSection;
|
||||
|
@ -191,10 +230,16 @@ static NSArray *RCTBridgeModuleClassesByModuleID(void)
|
|||
|
||||
@interface RCTBridge ()
|
||||
|
||||
@property (nonatomic, copy, readonly) NSArray *profile;
|
||||
|
||||
- (void)_invokeAndProcessModule:(NSString *)module
|
||||
method:(NSString *)method
|
||||
arguments:(NSArray *)args;
|
||||
|
||||
- (void)_actuallyInvokeAndProcessModule:(NSString *)module
|
||||
method:(NSString *)method
|
||||
arguments:(NSArray *)args;
|
||||
|
||||
@end
|
||||
|
||||
/**
|
||||
|
@ -754,7 +799,13 @@ static NSDictionary *RCTLocalModulesConfig()
|
|||
RCTBridgeModuleProviderBlock _moduleProvider;
|
||||
RCTDisplayLink *_displayLink;
|
||||
NSMutableSet *_frameUpdateObservers;
|
||||
NSMutableArray *_scheduledCalls;
|
||||
NSMutableArray *_scheduledCallbacks;
|
||||
BOOL _loading;
|
||||
|
||||
NSUInteger _startingTime;
|
||||
NSMutableArray *_profile;
|
||||
NSLock *_profileLock;
|
||||
}
|
||||
|
||||
static id<RCTJavaScriptExecutor> _latestJSExecutor;
|
||||
|
@ -782,6 +833,8 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
|
|||
_shadowQueue = dispatch_queue_create("com.facebook.React.ShadowQueue", DISPATCH_QUEUE_SERIAL);
|
||||
_displayLink = [[RCTDisplayLink alloc] initWithBridge:self];
|
||||
_frameUpdateObservers = [[NSMutableSet alloc] init];
|
||||
_scheduledCalls = [[NSMutableArray alloc] init];
|
||||
_scheduledCallbacks = [[NSMutableArray alloc] init];
|
||||
|
||||
// Register passed-in module instances
|
||||
NSMutableDictionary *preregisteredModules = [[NSMutableDictionary alloc] init];
|
||||
|
@ -1005,20 +1058,54 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Private hack to support `setTimeout(fn, 0)`
|
||||
*/
|
||||
- (void)_immediatelyCallTimer:(NSNumber *)timer
|
||||
{
|
||||
NSString *moduleDotMethod = @"RCTJSTimers.callTimers";
|
||||
NSNumber *moduleID = RCTLocalModuleIDs[moduleDotMethod];
|
||||
RCTAssert(moduleID != nil, @"Module '%@' not registered.",
|
||||
[[moduleDotMethod componentsSeparatedByString:@"."] firstObject]);
|
||||
|
||||
NSNumber *methodID = RCTLocalMethodIDs[moduleDotMethod];
|
||||
RCTAssert(methodID != nil, @"Method '%@' not registered.", moduleDotMethod);
|
||||
|
||||
if (!_loading) {
|
||||
#if BATCHED_BRIDGE
|
||||
[self _actuallyInvokeAndProcessModule:@"BatchedBridge"
|
||||
method:@"callFunctionReturnFlushedQueue"
|
||||
arguments:@[moduleID, methodID, @[@[timer]]]];
|
||||
|
||||
#else
|
||||
|
||||
[self _invokeAndProcessModule:@"BatchedBridge"
|
||||
method:@"callFunctionReturnFlushedQueue"
|
||||
arguments:@[moduleID, methodID, @[@[timer]]]];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
- (void)enqueueApplicationScript:(NSString *)script url:(NSURL *)url onComplete:(RCTJavaScriptCompleteBlock)onComplete
|
||||
{
|
||||
RCTAssert(onComplete != nil, @"onComplete block passed in should be non-nil");
|
||||
RCT_PROFILE_START();
|
||||
[_javaScriptExecutor executeApplicationScript:script sourceURL:url onComplete:^(NSError *scriptLoadError) {
|
||||
RCT_PROFILE_END(js_call, scriptLoadError, @"initial_script");
|
||||
if (scriptLoadError) {
|
||||
onComplete(scriptLoadError);
|
||||
return;
|
||||
}
|
||||
|
||||
RCT_PROFILE_START();
|
||||
[_javaScriptExecutor executeJSCall:@"BatchedBridge"
|
||||
method:@"flushedQueue"
|
||||
arguments:@[]
|
||||
callback:^(id json, NSError *error) {
|
||||
RCT_PROFILE_END(js_call, error, @"initial_call", @"BatchedBridge.flushedQueue");
|
||||
RCT_PROFILE_START();
|
||||
[self _handleBuffer:json];
|
||||
RCT_PROFILE_END(objc_call, json, @"batched_js_calls");
|
||||
onComplete(error);
|
||||
}];
|
||||
}];
|
||||
|
@ -1028,11 +1115,46 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
|
|||
|
||||
- (void)_invokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args
|
||||
{
|
||||
#if BATCHED_BRIDGE
|
||||
RCT_PROFILE_START();
|
||||
|
||||
if ([module isEqualToString:@"RCTEventEmitter"]) {
|
||||
for (NSDictionary *call in _scheduledCalls) {
|
||||
if ([call[@"module"] isEqualToString:module] && [call[@"method"] isEqualToString:method] && [call[@"args"][0] isEqualToString:args[0]]) {
|
||||
[_scheduledCalls removeObject:call];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
id call = @{
|
||||
@"module": module,
|
||||
@"method": method,
|
||||
@"args": args,
|
||||
};
|
||||
|
||||
if ([method isEqualToString:@"invokeCallbackAndReturnFlushedQueue"]) {
|
||||
[_scheduledCallbacks addObject:call];
|
||||
} else {
|
||||
[_scheduledCalls addObject:call];
|
||||
}
|
||||
|
||||
RCT_PROFILE_END(js_call, args, @"schedule", module, method);
|
||||
}
|
||||
|
||||
- (void)_actuallyInvokeAndProcessModule:(NSString *)module method:(NSString *)method arguments:(NSArray *)args
|
||||
{
|
||||
#endif
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:RCTEnqueueNotification object:nil userInfo:nil];
|
||||
|
||||
NSString *moduleDotMethod = [NSString stringWithFormat:@"%@.%@", module, method];
|
||||
RCT_PROFILE_START();
|
||||
RCTJavaScriptCallback processResponse = ^(id json, NSError *error) {
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:RCTDequeueNotification object:nil userInfo:nil];
|
||||
RCT_PROFILE_END(js_call, args, moduleDotMethod);
|
||||
|
||||
RCT_PROFILE_START();
|
||||
[self _handleBuffer:json];
|
||||
RCT_PROFILE_END(objc_call, json, @"batched_js_calls");
|
||||
};
|
||||
|
||||
[_javaScriptExecutor executeJSCall:module
|
||||
|
@ -1151,12 +1273,34 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
|
|||
|
||||
- (void)_update:(CADisplayLink *)displayLink
|
||||
{
|
||||
RCT_PROFILE_START();
|
||||
|
||||
RCTFrameUpdate *frameUpdate = [[RCTFrameUpdate alloc] initWithDisplayLink:displayLink];
|
||||
for (id<RCTFrameUpdateObserver> observer in _frameUpdateObservers) {
|
||||
if (![observer respondsToSelector:@selector(isPaused)] || ![observer isPaused]) {
|
||||
[observer didUpdateFrame:frameUpdate];
|
||||
}
|
||||
}
|
||||
|
||||
[self _runScheduledCalls];
|
||||
|
||||
RCT_PROFILE_END(display_link, nil, @"main_thread");
|
||||
}
|
||||
|
||||
- (void)_runScheduledCalls
|
||||
{
|
||||
#if BATCHED_BRIDGE
|
||||
|
||||
NSArray *calls = [_scheduledCallbacks arrayByAddingObjectsFromArray:_scheduledCalls];
|
||||
if (calls.count > 0) {
|
||||
_scheduledCalls = [[NSMutableArray alloc] init];
|
||||
_scheduledCallbacks = [[NSMutableArray alloc] init];
|
||||
[self _actuallyInvokeAndProcessModule:@"BatchedBridge"
|
||||
method:@"processBatch"
|
||||
arguments:@[calls]];
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)addFrameUpdateObserver:(id<RCTFrameUpdateObserver>)observer
|
||||
|
@ -1194,4 +1338,42 @@ static id<RCTJavaScriptExecutor> _latestJSExecutor;
|
|||
callback:^(id json, NSError *error) {}];
|
||||
}
|
||||
|
||||
- (void)startProfiling
|
||||
{
|
||||
if (![_bundleURL.scheme isEqualToString:@"http"]) {
|
||||
RCTLogError(@"To run the profiler you must be running from the dev server");
|
||||
return;
|
||||
}
|
||||
_profileLock = [[NSLock alloc] init];
|
||||
_startingTime = CACurrentMediaTime();
|
||||
|
||||
[_profileLock lock];
|
||||
_profile = [[NSMutableArray alloc] init];
|
||||
[_profileLock unlock];
|
||||
}
|
||||
|
||||
- (void)stopProfiling
|
||||
{
|
||||
[_profileLock lock];
|
||||
NSArray *profile = _profile;
|
||||
_profile = nil;
|
||||
[_profileLock unlock];
|
||||
_profileLock = nil;
|
||||
|
||||
NSString *log = RCTJSONStringify(profile, NULL);
|
||||
NSString *URLString = [NSString stringWithFormat:@"%@://%@:%@/profile", _bundleURL.scheme, _bundleURL.host, _bundleURL.port];
|
||||
NSURL *URL = [NSURL URLWithString:URLString];
|
||||
NSMutableURLRequest *URLRequest = [NSMutableURLRequest requestWithURL:URL];
|
||||
URLRequest.HTTPMethod = @"POST";
|
||||
[URLRequest setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
|
||||
NSURLSessionTask *task = [[NSURLSession sharedSession] uploadTaskWithRequest:URLRequest
|
||||
fromData:[log dataUsingEncoding:NSUTF8StringEncoding]
|
||||
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
|
||||
if (error) {
|
||||
RCTLogError(@"%@", error.localizedDescription);
|
||||
}
|
||||
}];
|
||||
[task resume];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -14,6 +14,15 @@
|
|||
#import "RCTSourceCode.h"
|
||||
#import "RCTWebViewExecutor.h"
|
||||
|
||||
@interface RCTBridge (RCTDevMenu)
|
||||
|
||||
@property (nonatomic, copy, readonly) NSArray *profile;
|
||||
|
||||
- (void)startProfiling;
|
||||
- (void)stopProfiling;
|
||||
|
||||
@end
|
||||
|
||||
@interface RCTDevMenu () <UIActionSheetDelegate>
|
||||
|
||||
@end
|
||||
|
@ -37,11 +46,12 @@
|
|||
NSString *debugTitleChrome = _bridge.executorClass != Nil && _bridge.executorClass == NSClassFromString(@"RCTWebSocketExecutor") ? @"Disable Chrome Debugging" : @"Enable Chrome Debugging";
|
||||
NSString *debugTitleSafari = _bridge.executorClass == [RCTWebViewExecutor class] ? @"Disable Safari Debugging" : @"Enable Safari Debugging";
|
||||
NSString *liveReloadTitle = _liveReload ? @"Disable Live Reload" : @"Enable Live Reload";
|
||||
NSString *profilingTitle = _bridge.profile ? @"Stop Profiling" : @"Start Profiling";
|
||||
UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:@"React Native: Development"
|
||||
delegate:self
|
||||
cancelButtonTitle:@"Cancel"
|
||||
destructiveButtonTitle:nil
|
||||
otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, liveReloadTitle, nil];
|
||||
otherButtonTitles:@"Reload", debugTitleChrome, debugTitleSafari, liveReloadTitle, profilingTitle, nil];
|
||||
actionSheet.actionSheetStyle = UIBarStyleBlack;
|
||||
[actionSheet showInView:[[[[UIApplication sharedApplication] keyWindow] rootViewController] view]];
|
||||
}
|
||||
|
@ -61,6 +71,12 @@
|
|||
} else if (buttonIndex == 3) {
|
||||
_liveReload = !_liveReload;
|
||||
[self _pollAndReload];
|
||||
} else if (buttonIndex == 4) {
|
||||
if (_bridge.profile) {
|
||||
[_bridge stopProfiling];
|
||||
} else {
|
||||
[_bridge startProfiling];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -68,7 +68,6 @@
|
|||
|
||||
_bridge = bridge;
|
||||
_moduleName = moduleName;
|
||||
_touchHandler = [[RCTTouchHandler alloc] initWithBridge:_bridge];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(bundleFinishedLoading)
|
||||
|
@ -105,7 +104,6 @@
|
|||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
[_touchHandler invalidate];
|
||||
if (_contentView) {
|
||||
[_bridge enqueueJSCall:@"ReactIOS.unmountComponentAtNodeAndRemoveContainer"
|
||||
args:@[_contentView.reactTag]];
|
||||
|
@ -148,7 +146,6 @@ RCT_IMPORT_METHOD(ReactIOS, unmountComponentAtNodeAndRemoveContainer)
|
|||
* NOTE: Since the bridge persists, the RootViews might be reused, so now
|
||||
* the react tag is assigned every time we load new content.
|
||||
*/
|
||||
[_touchHandler invalidate];
|
||||
[_contentView removeFromSuperview];
|
||||
_contentView = [[UIView alloc] initWithFrame:self.bounds];
|
||||
_contentView.reactTag = [_bridge.uiManager allocateRootTag];
|
||||
|
|
|
@ -9,14 +9,12 @@
|
|||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "RCTInvalidating.h"
|
||||
#import "RCTFrameUpdate.h"
|
||||
|
||||
@class RCTBridge;
|
||||
|
||||
@interface RCTTouchHandler : UIGestureRecognizer<RCTInvalidating>
|
||||
@interface RCTTouchHandler : UIGestureRecognizer
|
||||
|
||||
- (instancetype)initWithBridge:(RCTBridge *)bridge NS_DESIGNATED_INITIALIZER;
|
||||
- (void)startOrResetInteractionTiming;
|
||||
- (NSDictionary *)endAndResetInteractionTiming;
|
||||
|
||||
@end
|
||||
|
|
|
@ -20,39 +20,6 @@
|
|||
|
||||
// TODO: this class behaves a lot like a module, and could be implemented as a
|
||||
// module if we were to assume that modules and RootViews had a 1:1 relationship
|
||||
|
||||
@interface RCTTouchEvent : NSObject
|
||||
|
||||
@property (nonatomic, assign, readonly) NSUInteger id;
|
||||
@property (nonatomic, copy, readonly) NSString *eventName;
|
||||
@property (nonatomic, copy, readonly) NSArray *touches;
|
||||
@property (nonatomic, copy, readonly) NSArray *changedIndexes;
|
||||
@property (nonatomic, assign, readonly) CFTimeInterval originatingTime;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation RCTTouchEvent
|
||||
|
||||
+ (instancetype)touchWithEventName:(NSString *)eventName touches:(NSArray *)touches changedIndexes:(NSArray *)changedIndexes originatingTime:(CFTimeInterval)originatingTime
|
||||
{
|
||||
RCTTouchEvent *touchEvent = [[self alloc] init];
|
||||
touchEvent->_id = [self newTaskID];
|
||||
touchEvent->_eventName = [eventName copy];
|
||||
touchEvent->_touches = [touches copy];
|
||||
touchEvent->_changedIndexes = [changedIndexes copy];
|
||||
touchEvent->_originatingTime = originatingTime;
|
||||
return touchEvent;
|
||||
}
|
||||
|
||||
+ (NSUInteger)newTaskID
|
||||
{
|
||||
static NSUInteger taskID = 0;
|
||||
return ++taskID;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation RCTTouchHandler
|
||||
{
|
||||
__weak RCTBridge *_bridge;
|
||||
|
@ -69,7 +36,6 @@
|
|||
|
||||
BOOL _recordingInteractionTiming;
|
||||
CFTimeInterval _mostRecentEnqueueJS;
|
||||
CADisplayLink *_displayLink;
|
||||
NSMutableArray *_pendingTouches;
|
||||
NSMutableArray *_bridgeInteractionTiming;
|
||||
}
|
||||
|
@ -86,12 +52,9 @@
|
|||
_reactTouches = [[NSMutableArray alloc] init];
|
||||
_touchViews = [[NSMutableArray alloc] init];
|
||||
|
||||
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_update:)];
|
||||
_pendingTouches = [[NSMutableArray alloc] init];
|
||||
_bridgeInteractionTiming = [[NSMutableArray alloc] init];
|
||||
|
||||
[_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
|
||||
|
||||
// `cancelsTouchesInView` is needed in order to be used as a top level event delegated recognizer. Otherwise, lower
|
||||
// level components not build using RCT, will fail to recognize gestures.
|
||||
self.cancelsTouchesInView = NO;
|
||||
|
@ -99,17 +62,6 @@
|
|||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)isValid
|
||||
{
|
||||
return _displayLink != nil;
|
||||
}
|
||||
|
||||
- (void)invalidate
|
||||
{
|
||||
[_displayLink invalidate];
|
||||
_displayLink = nil;
|
||||
}
|
||||
|
||||
typedef NS_ENUM(NSInteger, RCTTouchEventType) {
|
||||
RCTTouchEventTypeStart,
|
||||
RCTTouchEventTypeMove,
|
||||
|
@ -216,7 +168,6 @@ RCT_IMPORT_METHOD(RCTEventEmitter, receiveTouches);
|
|||
- (void)_updateAndDispatchTouches:(NSSet *)touches eventName:(NSString *)eventName originatingTime:(CFTimeInterval)originatingTime
|
||||
{
|
||||
// Update touches
|
||||
CFTimeInterval enqueueTime = CACurrentMediaTime();
|
||||
NSMutableArray *changedIndexes = [[NSMutableArray alloc] init];
|
||||
for (UITouch *touch in touches) {
|
||||
NSInteger index = [_nativeTouches indexOfObject:touch];
|
||||
|
@ -239,71 +190,8 @@ RCT_IMPORT_METHOD(RCTEventEmitter, receiveTouches);
|
|||
[reactTouches addObject:[touch copy]];
|
||||
}
|
||||
|
||||
RCTTouchEvent *touch = [RCTTouchEvent touchWithEventName:eventName
|
||||
touches:reactTouches
|
||||
changedIndexes:changedIndexes
|
||||
originatingTime:originatingTime];
|
||||
[_pendingTouches addObject:touch];
|
||||
|
||||
if (_recordingInteractionTiming) {
|
||||
[_bridgeInteractionTiming addObject:@{
|
||||
@"timeSeconds": @(touch.originatingTime),
|
||||
@"operation": @"taskOriginated",
|
||||
@"taskID": @(touch.id),
|
||||
}];
|
||||
[_bridgeInteractionTiming addObject:@{
|
||||
@"timeSeconds": @(enqueueTime),
|
||||
@"operation": @"taskEnqueuedPending",
|
||||
@"taskID": @(touch.id),
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_update:(CADisplayLink *)sender
|
||||
{
|
||||
// Dispatch touch event
|
||||
NSUInteger pendingCount = _pendingTouches.count;
|
||||
for (RCTTouchEvent *touch in _pendingTouches) {
|
||||
_mostRecentEnqueueJS = CACurrentMediaTime();
|
||||
[_bridge enqueueJSCall:@"RCTEventEmitter.receiveTouches"
|
||||
args:@[touch.eventName, touch.touches, touch.changedIndexes]];
|
||||
}
|
||||
|
||||
if (_recordingInteractionTiming) {
|
||||
for (RCTTouchEvent *touch in _pendingTouches) {
|
||||
[_bridgeInteractionTiming addObject:@{
|
||||
@"timeSeconds": @(sender.timestamp),
|
||||
@"operation": @"frameAlignedDispatch",
|
||||
@"taskID": @(touch.id),
|
||||
}];
|
||||
}
|
||||
|
||||
if (pendingCount > 0 || sender.timestamp - _mostRecentEnqueueJS < 0.1) {
|
||||
[_bridgeInteractionTiming addObject:@{
|
||||
@"timeSeconds": @(sender.timestamp),
|
||||
@"operation": @"mainThreadDisplayLink",
|
||||
@"taskID": @([RCTTouchEvent newTaskID]),
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
[_pendingTouches removeAllObjects];
|
||||
}
|
||||
|
||||
- (void)startOrResetInteractionTiming
|
||||
{
|
||||
RCTAssertMainThread();
|
||||
[_bridgeInteractionTiming removeAllObjects];
|
||||
_recordingInteractionTiming = YES;
|
||||
}
|
||||
|
||||
- (NSDictionary *)endAndResetInteractionTiming
|
||||
{
|
||||
RCTAssertMainThread();
|
||||
_recordingInteractionTiming = NO;
|
||||
NSArray *_prevInteractionTimingData = _bridgeInteractionTiming;
|
||||
_bridgeInteractionTiming = [[NSMutableArray alloc] init];
|
||||
return @{ @"interactionTiming": _prevInteractionTimingData };
|
||||
[_bridge enqueueJSCall:@"RCTEventEmitter.receiveTouches"
|
||||
args:@[eventName, reactTouches, changedIndexes]];
|
||||
}
|
||||
|
||||
#pragma mark - Gesture Recognizer Delegate Callbacks
|
||||
|
|
|
@ -218,7 +218,7 @@ static NSError *RCTNSErrorFromJSError(JSContextRef context, JSValueRef jsError)
|
|||
|
||||
- (void)dealloc
|
||||
{
|
||||
RCTAssert(!self.valid, @"must call -invalidate before -dealloc");
|
||||
[self invalidate];
|
||||
}
|
||||
|
||||
- (void)executeJSCall:(NSString *)name
|
||||
|
|
|
@ -15,6 +15,15 @@
|
|||
#import "RCTSparseArray.h"
|
||||
#import "RCTUtils.h"
|
||||
|
||||
@interface RCTBridge (Private)
|
||||
|
||||
/**
|
||||
* Allow super fast, one time, timers to skip the queue and be directly executed
|
||||
*/
|
||||
- (void)_immediatelyCallTimer:(NSNumber *)timer;
|
||||
|
||||
@end
|
||||
|
||||
@interface RCTTimer : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSDate *target;
|
||||
|
@ -160,7 +169,7 @@ RCT_EXPORT_METHOD(createTimer:(NSNumber *)callbackID
|
|||
{
|
||||
if (jsDuration == 0 && repeats == NO) {
|
||||
// For super fast, one-off timers, just enqueue them immediately rather than waiting a frame.
|
||||
[_bridge enqueueJSCall:@"RCTJSTimers.callTimers" args:@[@[callbackID]]];
|
||||
[_bridge _immediatelyCallTimer:callbackID];
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -193,6 +193,7 @@ static UIViewAnimationCurve UIViewAnimationCurveFromRCTAnimationType(RCTAnimatio
|
|||
NSMutableDictionary *_defaultShadowViews; // RCT thread only
|
||||
NSMutableDictionary *_defaultViews; // Main thread only
|
||||
NSDictionary *_viewManagers;
|
||||
NSDictionary *_viewConfigs;
|
||||
NSUInteger _rootTag;
|
||||
}
|
||||
|
||||
|
@ -219,6 +220,28 @@ static NSString *RCTViewNameForModuleName(NSString *moduleName)
|
|||
return name;
|
||||
}
|
||||
|
||||
static NSDictionary *RCTViewConfigForModule(Class managerClass, NSString *viewName)
|
||||
{
|
||||
NSMutableDictionary *nativeProps = [[NSMutableDictionary alloc] init];
|
||||
static const char *prefix = "getPropConfig";
|
||||
static const NSUInteger prefixLength = sizeof("getPropConfig") - 1;
|
||||
unsigned int methodCount = 0;
|
||||
Method *methods = class_copyMethodList(objc_getMetaClass(class_getName(managerClass)), &methodCount);
|
||||
for (unsigned int i = 0; i < methodCount; i++) {
|
||||
Method method = methods[i];
|
||||
SEL getInfo = method_getName(method);
|
||||
const char *selName = sel_getName(getInfo);
|
||||
if (strlen(selName) > prefixLength && strncmp(selName, prefix, prefixLength) == 0) {
|
||||
NSDictionary *info = ((NSDictionary *(*)(id, SEL))method_getImplementation(method))(managerClass, getInfo);
|
||||
nativeProps[info[@"name"]] = info;
|
||||
}
|
||||
}
|
||||
return @{
|
||||
@"uiViewClassName": viewName,
|
||||
@"nativeProps": nativeProps
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This private constructor should only be called when creating
|
||||
* isolated UIImanager instances for testing. Normal initialization
|
||||
|
@ -292,13 +315,17 @@ static NSString *RCTViewNameForModuleName(NSString *moduleName)
|
|||
|
||||
// Get view managers from bridge
|
||||
NSMutableDictionary *viewManagers = [[NSMutableDictionary alloc] init];
|
||||
NSMutableDictionary *viewConfigs = [[NSMutableDictionary alloc] init];
|
||||
[_bridge.modules enumerateKeysAndObjectsUsingBlock:^(NSString *moduleName, RCTViewManager *manager, BOOL *stop) {
|
||||
if ([manager isKindOfClass:[RCTViewManager class]]) {
|
||||
viewManagers[RCTViewNameForModuleName(moduleName)] = manager;
|
||||
NSString *viewName = RCTViewNameForModuleName(moduleName);
|
||||
viewManagers[viewName] = manager;
|
||||
viewConfigs[viewName] = RCTViewConfigForModule([manager class], viewName);
|
||||
}
|
||||
}];
|
||||
|
||||
_viewManagers = [viewManagers copy];
|
||||
_viewConfigs = [viewConfigs copy];
|
||||
}
|
||||
|
||||
- (void)registerRootView:(UIView *)rootView;
|
||||
|
@ -650,14 +677,12 @@ RCT_EXPORT_METHOD(manageChildren:(NSNumber *)containerReactTag
|
|||
|
||||
[self _purgeChildren:permanentlyRemovedChildren fromRegistry:registry];
|
||||
|
||||
// TODO (#5906496): optimize all these loops - constantly calling array.count is not efficient
|
||||
|
||||
// Figure out what to insert - merge temporary inserts and adds
|
||||
NSMutableDictionary *destinationsToChildrenToAdd = [NSMutableDictionary dictionary];
|
||||
for (NSInteger index = 0; index < temporarilyRemovedChildren.count; index++) {
|
||||
for (NSInteger index = 0, length = temporarilyRemovedChildren.count; index < length; index++) {
|
||||
destinationsToChildrenToAdd[moveToIndices[index]] = temporarilyRemovedChildren[index];
|
||||
}
|
||||
for (NSInteger index = 0; index < addAtIndices.count; index++) {
|
||||
for (NSInteger index = 0, length = addAtIndices.count; index < length; index++) {
|
||||
id view = registry[addChildReactTags[index]];
|
||||
if (view) {
|
||||
destinationsToChildrenToAdd[addAtIndices[index]] = view;
|
||||
|
@ -1374,7 +1399,7 @@ RCT_EXPORT_METHOD(clearJSResponder)
|
|||
allJSConstants[name] = [constantsNamespace copy];
|
||||
}
|
||||
}];
|
||||
|
||||
allJSConstants[@"viewConfigs"] = _viewConfigs;
|
||||
return allJSConstants;
|
||||
}
|
||||
|
||||
|
@ -1393,41 +1418,6 @@ RCT_EXPORT_METHOD(configureNextLayoutAnimation:(NSDictionary *)config
|
|||
callback:callback];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(startOrResetInteractionTiming)
|
||||
{
|
||||
NSSet *rootViewTags = [_rootViewTags copy];
|
||||
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
|
||||
for (NSNumber *reactTag in rootViewTags) {
|
||||
UIView *rootView = viewRegistry[reactTag];
|
||||
for (RCTTouchHandler *handler in rootView.gestureRecognizers) {
|
||||
if ([handler isKindOfClass:[RCTTouchHandler class]]) {
|
||||
[handler startOrResetInteractionTiming];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
RCT_EXPORT_METHOD(endAndResetInteractionTiming:(RCTResponseSenderBlock)onSuccess
|
||||
onError:(RCTResponseSenderBlock)onError)
|
||||
{
|
||||
NSSet *rootViewTags = [_rootViewTags copy];
|
||||
[self addUIBlock:^(RCTUIManager *uiManager, RCTSparseArray *viewRegistry) {
|
||||
NSMutableDictionary *timingData = [[NSMutableDictionary alloc] init];
|
||||
for (NSNumber *reactTag in rootViewTags) {
|
||||
UIView *rootView = viewRegistry[reactTag];
|
||||
for (RCTTouchHandler *handler in rootView.gestureRecognizers) {
|
||||
if ([handler isKindOfClass:[RCTTouchHandler class]]) {
|
||||
[handler endAndResetInteractionTiming];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
onSuccess(@[timingData]);
|
||||
}];
|
||||
}
|
||||
|
||||
static UIView *_jsResponder;
|
||||
|
||||
+ (UIView *)JSResponder
|
||||
|
|
|
@ -47,27 +47,27 @@ const CGFloat RCTMapZoomBoundBuffer = 0.01;
|
|||
[_regionChangeObserveTimer invalidate];
|
||||
}
|
||||
|
||||
- (void)reactSetFrame:(CGRect)frame
|
||||
{
|
||||
self.frame = frame;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
|
||||
// Force resize subviews - only the layer is resized by default
|
||||
CGRect mapFrame = self.frame;
|
||||
self.frame = CGRectZero;
|
||||
self.frame = mapFrame;
|
||||
|
||||
if (_legalLabel) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
CGRect frame = _legalLabel.frame;
|
||||
if (_legalLabelInsets.left) {
|
||||
frame.origin.x = _legalLabelInsets.left;
|
||||
} else if (_legalLabelInsets.right) {
|
||||
frame.origin.x = mapFrame.size.width - _legalLabelInsets.right - frame.size.width;
|
||||
frame.origin.x = self.frame.size.width - _legalLabelInsets.right - frame.size.width;
|
||||
}
|
||||
if (_legalLabelInsets.top) {
|
||||
frame.origin.y = _legalLabelInsets.top;
|
||||
} else if (_legalLabelInsets.bottom) {
|
||||
frame.origin.y = mapFrame.size.height - _legalLabelInsets.bottom - frame.size.height;
|
||||
frame.origin.y = self.frame.size.height - _legalLabelInsets.bottom - frame.size.height;
|
||||
}
|
||||
_legalLabel.frame = frame;
|
||||
});
|
||||
|
@ -93,7 +93,7 @@ const CGFloat RCTMapZoomBoundBuffer = 0.01;
|
|||
}
|
||||
}
|
||||
|
||||
- (void)setRegion:(MKCoordinateRegion)region
|
||||
- (void)setRegion:(MKCoordinateRegion)region animated:(BOOL)animated
|
||||
{
|
||||
// If location is invalid, abort
|
||||
if (!CLLocationCoordinate2DIsValid(region.center)) {
|
||||
|
@ -109,7 +109,7 @@ const CGFloat RCTMapZoomBoundBuffer = 0.01;
|
|||
}
|
||||
|
||||
// Animate to new position
|
||||
[super setRegion:region animated:YES];
|
||||
[super setRegion:region animated:animated];
|
||||
}
|
||||
|
||||
- (void)setAnnotations:(MKShapeArray *)annotations
|
||||
|
|
|
@ -41,9 +41,11 @@ RCT_EXPORT_VIEW_PROPERTY(scrollEnabled, BOOL)
|
|||
RCT_EXPORT_VIEW_PROPERTY(maxDelta, CGFloat)
|
||||
RCT_EXPORT_VIEW_PROPERTY(minDelta, CGFloat)
|
||||
RCT_EXPORT_VIEW_PROPERTY(legalLabelInsets, UIEdgeInsets)
|
||||
RCT_EXPORT_VIEW_PROPERTY(region, MKCoordinateRegion)
|
||||
RCT_EXPORT_VIEW_PROPERTY(annotations, MKShapeArray)
|
||||
|
||||
RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, RCTMap)
|
||||
{
|
||||
[view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:YES];
|
||||
}
|
||||
|
||||
#pragma mark MKMapViewDelegate
|
||||
|
||||
|
|
|
@ -109,6 +109,7 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v
|
|||
* within the view or shadowView.
|
||||
*/
|
||||
#define RCT_REMAP_VIEW_PROPERTY(name, keyPath, type) \
|
||||
RCT_EXPORT_VIEW_PROP_CONFIG(name, type) \
|
||||
- (void)set_##name:(id)json forView:(id)view withDefaultView:(id)defaultView { \
|
||||
if ((json && !RCTSetProperty(view, @#keyPath, @selector(type:), json)) || \
|
||||
(!json && !RCTCopyProperty(view, defaultView, @#keyPath))) { \
|
||||
|
@ -117,6 +118,7 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v
|
|||
}
|
||||
|
||||
#define RCT_REMAP_SHADOW_PROPERTY(name, keyPath, type) \
|
||||
RCT_EXPORT_SHADOW_PROP_CONFIG(name, type) \
|
||||
- (void)set_##name:(id)json forShadowView:(id)view withDefaultView:(id)defaultView { \
|
||||
if ((json && !RCTSetProperty(view, @#keyPath, @selector(type:), json)) || \
|
||||
(!json && !RCTCopyProperty(view, defaultView, @#keyPath))) { \
|
||||
|
@ -130,9 +132,11 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v
|
|||
* refer to "json", "view" and "defaultView" to implement the required logic.
|
||||
*/
|
||||
#define RCT_CUSTOM_VIEW_PROPERTY(name, type, viewClass) \
|
||||
RCT_EXPORT_VIEW_PROP_CONFIG(name, type) \
|
||||
- (void)set_##name:(id)json forView:(viewClass *)view withDefaultView:(viewClass *)defaultView
|
||||
|
||||
#define RCT_CUSTOM_SHADOW_PROPERTY(name, type, viewClass) \
|
||||
RCT_EXPORT_SHADOW_PROP_CONFIG(name, type) \
|
||||
- (void)set_##name:(id)json forShadowView:(viewClass *)view withDefaultView:(viewClass *)defaultView
|
||||
|
||||
/**
|
||||
|
@ -160,4 +164,17 @@ typedef void (^RCTViewManagerUIBlock)(RCTUIManager *uiManager, RCTSparseArray *v
|
|||
[self set_##newName:json forView:view withDefaultView:defaultView]; \
|
||||
}
|
||||
|
||||
/**
|
||||
* PROP_CONFIG macros should only be paired with property setters.
|
||||
*/
|
||||
#define RCT_EXPORT_VIEW_PROP_CONFIG(name, type) \
|
||||
+ (NSDictionary *)getPropConfigView_##name { \
|
||||
return @{@"name": @#name, @"type": @#type}; \
|
||||
}
|
||||
|
||||
#define RCT_EXPORT_SHADOW_PROP_CONFIG(name, type) \
|
||||
+ (NSDictionary *)getPropConfigShadow_##name { \
|
||||
return @{@"name": @#name, @"type": @#type}; \
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -61,7 +61,6 @@ RCT_EXPORT_MODULE()
|
|||
#pragma mark - View properties
|
||||
|
||||
RCT_EXPORT_VIEW_PROPERTY(accessibilityLabel, NSString)
|
||||
RCT_EXPORT_VIEW_PROPERTY(hidden, BOOL)
|
||||
RCT_EXPORT_VIEW_PROPERTY(backgroundColor, UIColor)
|
||||
RCT_REMAP_VIEW_PROPERTY(accessible, isAccessibilityElement, BOOL)
|
||||
RCT_REMAP_VIEW_PROPERTY(testID, accessibilityIdentifier, NSString)
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
'use strict';
|
||||
|
||||
jest
|
||||
.autoMockOff()
|
||||
.mock('../../lib/declareOpts')
|
||||
.mock('fs');
|
||||
|
||||
var fs = require('fs');
|
||||
var AssetServer = require('../');
|
||||
var Promise = require('bluebird');
|
||||
|
||||
describe('AssetServer', function() {
|
||||
pit('should work for the simple case', function() {
|
||||
var server = new AssetServer({
|
||||
projectRoots: ['/root'],
|
||||
assetExts: ['png'],
|
||||
});
|
||||
|
||||
fs.__setMockFilesystem({
|
||||
'root': {
|
||||
imgs: {
|
||||
'b.png': 'b image',
|
||||
'b@2x.png': 'b2 image',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
server.get('imgs/b.png'),
|
||||
server.get('imgs/b@1x.png'),
|
||||
]).then(function(resp) {
|
||||
resp.forEach(function(data) {
|
||||
expect(data).toBe('b image');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
pit.only('should pick the bigger one', function() {
|
||||
var server = new AssetServer({
|
||||
projectRoots: ['/root'],
|
||||
assetExts: ['png'],
|
||||
});
|
||||
|
||||
fs.__setMockFilesystem({
|
||||
'root': {
|
||||
imgs: {
|
||||
'b@1x.png': 'b1 image',
|
||||
'b@2x.png': 'b2 image',
|
||||
'b@4x.png': 'b4 image',
|
||||
'b@4.5x.png': 'b4.5 image',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return server.get('imgs/b@3x.png').then(function(data) {
|
||||
expect(data).toBe('b4 image');
|
||||
});
|
||||
});
|
||||
|
||||
pit('should support multiple project roots', function() {
|
||||
var server = new AssetServer({
|
||||
projectRoots: ['/root'],
|
||||
assetExts: ['png'],
|
||||
});
|
||||
|
||||
fs.__setMockFilesystem({
|
||||
'root': {
|
||||
imgs: {
|
||||
'b.png': 'b image',
|
||||
},
|
||||
'root2': {
|
||||
'newImages': {
|
||||
'imgs': {
|
||||
'b@1x.png': 'b1 image',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
return server.get('newImages/imgs/b.png').then(function(data) {
|
||||
expect(data).toBe('b1 image');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,132 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var declareOpts = require('../lib/declareOpts');
|
||||
var extractAssetResolution = require('../lib/extractAssetResolution');
|
||||
var path = require('path');
|
||||
var Promise = require('bluebird');
|
||||
var fs = require('fs');
|
||||
|
||||
var lstat = Promise.promisify(fs.lstat);
|
||||
var readDir = Promise.promisify(fs.readdir);
|
||||
var readFile = Promise.promisify(fs.readFile);
|
||||
|
||||
module.exports = AssetServer;
|
||||
|
||||
var validateOpts = declareOpts({
|
||||
projectRoots: {
|
||||
type: 'array',
|
||||
required: true,
|
||||
},
|
||||
assetExts: {
|
||||
type: 'array',
|
||||
default: ['png'],
|
||||
},
|
||||
});
|
||||
|
||||
function AssetServer(options) {
|
||||
var opts = validateOpts(options);
|
||||
this._roots = opts.projectRoots;
|
||||
this._assetExts = opts.assetExts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a request for an image by path. That could contain a resolution
|
||||
* postfix, we need to find that image (or the closest one to it's resolution)
|
||||
* in one of the project roots:
|
||||
*
|
||||
* 1. We first parse the directory of the asset
|
||||
* 2. We check to find a matching directory in one of the project roots
|
||||
* 3. We then build a map of all assets and their resolutions in this directory
|
||||
* 4. Then pick the closest resolution (rounding up) to the requested one
|
||||
*/
|
||||
|
||||
AssetServer.prototype.get = function(assetPath) {
|
||||
var filename = path.basename(assetPath);
|
||||
|
||||
return findRoot(
|
||||
this._roots,
|
||||
path.dirname(assetPath)
|
||||
).then(function(dir) {
|
||||
return [
|
||||
dir,
|
||||
readDir(dir),
|
||||
];
|
||||
}).spread(function(dir, files) {
|
||||
// Easy case. File exactly what the client requested.
|
||||
var index = files.indexOf(filename);
|
||||
if (index > -1) {
|
||||
return readFile(path.join(dir, filename));
|
||||
}
|
||||
|
||||
var assetData = extractAssetResolution(filename);
|
||||
var map = buildAssetMap(dir, files);
|
||||
var record = map[assetData.assetName];
|
||||
|
||||
if (!record) {
|
||||
throw new Error('Asset not found');
|
||||
}
|
||||
|
||||
for (var i = 0; i < record.resolutions.length; i++) {
|
||||
if (record.resolutions[i] >= assetData.resolution) {
|
||||
return readFile(record.files[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return readFile(record.files[record.files.length - 1]);
|
||||
});
|
||||
};
|
||||
|
||||
function findRoot(roots, dir) {
|
||||
return Promise.some(
|
||||
roots.map(function(root) {
|
||||
var absPath = path.join(root, dir);
|
||||
return lstat(absPath).then(function(stat) {
|
||||
if (!stat.isDirectory()) {
|
||||
throw new Error('Looking for dirs');
|
||||
}
|
||||
stat._path = absPath;
|
||||
return stat;
|
||||
});
|
||||
}),
|
||||
1
|
||||
).spread(
|
||||
function(stat) {
|
||||
return stat._path;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function buildAssetMap(dir, files) {
|
||||
var assets = files.map(extractAssetResolution);
|
||||
var map = Object.create(null);
|
||||
assets.forEach(function(asset, i) {
|
||||
var file = files[i];
|
||||
var record = map[asset.assetName];
|
||||
if (!record) {
|
||||
record = map[asset.assetName] = {
|
||||
resolutions: [],
|
||||
files: [],
|
||||
};
|
||||
}
|
||||
|
||||
var insertIndex;
|
||||
var length = record.resolutions.length;
|
||||
for (insertIndex = 0; insertIndex < length; insertIndex++) {
|
||||
if (asset.resolution < record.resolutions[insertIndex]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
record.resolutions.splice(insertIndex, 0, asset.resolution);
|
||||
record.files.splice(insertIndex, 0, path.join(dir, file));
|
||||
});
|
||||
|
||||
return map;
|
||||
}
|
|
@ -14,6 +14,7 @@ jest
|
|||
.dontMock('absolute-path')
|
||||
.dontMock('../docblock')
|
||||
.dontMock('../../replacePatterns')
|
||||
.dontMock('../../../../lib/extractAssetResolution')
|
||||
.setMock('../../../ModuleDescriptor', function(data) {return data;});
|
||||
|
||||
describe('DependencyGraph', function() {
|
||||
|
|
|
@ -18,6 +18,7 @@ var isAbsolutePath = require('absolute-path');
|
|||
var debug = require('debug')('DependecyGraph');
|
||||
var util = require('util');
|
||||
var declareOpts = require('../../../lib/declareOpts');
|
||||
var extractAssetResolution = require('../../../lib/extractAssetResolution');
|
||||
|
||||
var readFile = Promise.promisify(fs.readFile);
|
||||
var readDir = Promise.promisify(fs.readdir);
|
||||
|
@ -421,7 +422,7 @@ DependecyGraph.prototype._processModule = function(modulePath) {
|
|||
var module;
|
||||
|
||||
if (this._assetExts.indexOf(extname(modulePath)) > -1) {
|
||||
var assetData = extractResolutionPostfix(this._lookupName(modulePath));
|
||||
var assetData = extractAssetResolution(this._lookupName(modulePath));
|
||||
moduleData.id = assetData.assetName;
|
||||
moduleData.resolution = assetData.resolution;
|
||||
moduleData.isAsset = true;
|
||||
|
@ -772,28 +773,6 @@ function extname(name) {
|
|||
return path.extname(name).replace(/^\./, '');
|
||||
}
|
||||
|
||||
function extractResolutionPostfix(filename) {
|
||||
var ext = extname(filename);
|
||||
var re = new RegExp('@([\\d\\.]+)x\\.' + ext + '$');
|
||||
|
||||
var match = filename.match(re);
|
||||
var resolution;
|
||||
|
||||
if (!(match && match[1])) {
|
||||
resolution = 1;
|
||||
} else {
|
||||
resolution = parseFloat(match[1], 10);
|
||||
if (isNaN(resolution)) {
|
||||
resolution = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
resolution: resolution,
|
||||
assetName: match ? filename.replace(re, '.' + ext) : filename,
|
||||
};
|
||||
}
|
||||
|
||||
function NotFoundError() {
|
||||
Error.call(this);
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
get: function() {
|
||||
console.error(
|
||||
'Your code is broken! Do not iterate over arrays with ' +
|
||||
'for...in. See https://fburl.com/31944000 for more information.'
|
||||
'for...in.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -49,9 +49,6 @@
|
|||
* - Use a regular for loop with index.
|
||||
* - Use one of the array methods: a.forEach, a.map, etc.
|
||||
* - Guard your body of your loop with a `arr.hasOwnProperty(key)` check.
|
||||
*
|
||||
* More info:
|
||||
* https://our.intern.facebook.com/intern/dex/qa/669736606441771/
|
||||
*/
|
||||
if (this == null) {
|
||||
throw new TypeError(
|
||||
|
@ -92,9 +89,6 @@
|
|||
* - Use a regular for loop with index.
|
||||
* - Use one of the array methods: a.forEach, a.map, etc.
|
||||
* - Guard your body of your loop with a `arr.hasOwnProperty(key)` check.
|
||||
*
|
||||
* More info:
|
||||
* https://our.intern.facebook.com/intern/dex/qa/669736606441771/
|
||||
*/
|
||||
if (this == null) {
|
||||
throw new TypeError('Array.prototype.find called on null or undefined');
|
||||
|
|
|
@ -111,7 +111,7 @@ describe('Packager', function() {
|
|||
var imgModule = {
|
||||
isStatic: true,
|
||||
path: '/root/img/new_image.png',
|
||||
uri: 'img/new_image.png',
|
||||
uri: 'assets/img/new_image.png',
|
||||
width: 25,
|
||||
height: 50,
|
||||
};
|
||||
|
|
|
@ -194,7 +194,7 @@ function generateAssetModule(module, relPath) {
|
|||
var img = {
|
||||
isStatic: true,
|
||||
path: module.path, //TODO(amasad): this should be path inside tar file.
|
||||
uri: relPath,
|
||||
uri: path.join('assets', relPath),
|
||||
width: dimensions.width / module.resolution,
|
||||
height: dimensions.height / module.resolution,
|
||||
};
|
||||
|
|
|
@ -229,4 +229,32 @@ describe('processRequest', function() {
|
|||
expect(res.end).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe.only('/assets endpoint', function() {
|
||||
var AssetServer;
|
||||
beforeEach(function() {
|
||||
AssetServer = require('../../AssetServer');
|
||||
});
|
||||
|
||||
it('should serve simple case', function() {
|
||||
var req = {
|
||||
url: '/assets/imgs/a.png',
|
||||
};
|
||||
var res = {
|
||||
end: jest.genMockFn(),
|
||||
};
|
||||
|
||||
AssetServer.prototype.get.mockImpl(function() {
|
||||
return Promise.resolve('i am image');
|
||||
});
|
||||
|
||||
server.processRequest(req, res);
|
||||
jest.runAllTimers();
|
||||
expect(res.end).toBeCalledWith('i am image');
|
||||
});
|
||||
|
||||
it('should return 404', function() {
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,8 +14,11 @@ var declareOpts = require('../lib/declareOpts');
|
|||
var FileWatcher = require('../FileWatcher');
|
||||
var Packager = require('../Packager');
|
||||
var Activity = require('../Activity');
|
||||
var AssetServer = require('../AssetServer');
|
||||
var Promise = require('bluebird');
|
||||
var _ = require('underscore');
|
||||
var exec = require('child_process').exec;
|
||||
var fs = require('fs');
|
||||
|
||||
module.exports = Server;
|
||||
|
||||
|
@ -99,6 +102,11 @@ function Server(options) {
|
|||
packagerOpts.fileWatcher = this._fileWatcher;
|
||||
this._packager = new Packager(packagerOpts);
|
||||
|
||||
this._assetServer = new AssetServer({
|
||||
projectRoots: opts.projectRoots,
|
||||
assetExts: opts.assetExts,
|
||||
});
|
||||
|
||||
var onFileChange = this._onFileChange.bind(this);
|
||||
this._fileWatcher.on('all', onFileChange);
|
||||
|
||||
|
@ -230,6 +238,58 @@ Server.prototype._processOnChangeRequest = function(req, res) {
|
|||
});
|
||||
};
|
||||
|
||||
Server.prototype._processAssetsRequest = function(req, res) {
|
||||
var urlObj = url.parse(req.url, true);
|
||||
var assetPath = urlObj.pathname.match(/^\/assets\/(.+)$/);
|
||||
this._assetServer.get(assetPath[1])
|
||||
.then(
|
||||
function(data) {
|
||||
res.end(data);
|
||||
},
|
||||
function(error) {
|
||||
console.error(error.stack);
|
||||
res.writeHead('404');
|
||||
res.end('Asset not found');
|
||||
}
|
||||
).done();
|
||||
};
|
||||
|
||||
Server.prototype._processProfile = function(req, res) {
|
||||
console.log('Dumping profile information...');
|
||||
var dumpName = '/tmp/dump_' + Date.now() + '.json';
|
||||
var prefix = process.env.TRACE_VIEWER_PATH || '';
|
||||
var cmd = path.join(prefix, 'trace2html') + ' ' + dumpName;
|
||||
fs.writeFileSync(dumpName, req.rawBody);
|
||||
exec(cmd, function (error) {
|
||||
if (error) {
|
||||
if (error.code === 127) {
|
||||
console.error(
|
||||
'\n** Failed executing `' + cmd + '` **\n\n' +
|
||||
'Google trace-viewer is required to visualize the data, do you have it installled?\n\n' +
|
||||
'You can get it at:\n\n' +
|
||||
' https://github.com/google/trace-viewer\n\n' +
|
||||
'If it\'s not in your path, you can set a custom path with:\n\n' +
|
||||
' TRACE_VIEWER_PATH=/path/to/trace-viewer\n\n' +
|
||||
'NOTE: Your profile data was kept at:\n\n' +
|
||||
' ' + dumpName
|
||||
);
|
||||
} else {
|
||||
console.error('Unknown error', error);
|
||||
}
|
||||
res.end();
|
||||
return;
|
||||
} else {
|
||||
exec('rm ' + dumpName);
|
||||
exec('open ' + dumpName.replace(/json$/, 'html'), function (error) {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
}
|
||||
res.end();
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Server.prototype.processRequest = function(req, res, next) {
|
||||
var urlObj = url.parse(req.url, true);
|
||||
var pathname = urlObj.pathname;
|
||||
|
@ -245,6 +305,12 @@ Server.prototype.processRequest = function(req, res, next) {
|
|||
} else if (pathname.match(/^\/onchange\/?$/)) {
|
||||
this._processOnChangeRequest(req, res);
|
||||
return;
|
||||
} else if (pathname.match(/^\/assets\//)) {
|
||||
this._processAssetsRequest(req, res);
|
||||
return;
|
||||
} else if (pathname.match(/^\/profile\/?$/)) {
|
||||
this._processProfile(req, res);
|
||||
return;
|
||||
} else {
|
||||
next();
|
||||
return;
|
||||
|
|
|
@ -42,6 +42,11 @@ fs.readdir.mockImpl(function(filepath, callback) {
|
|||
});
|
||||
|
||||
fs.readFile.mockImpl(function(filepath, encoding, callback) {
|
||||
if (arguments.length === 2) {
|
||||
callback = encoding;
|
||||
encoding = null;
|
||||
}
|
||||
|
||||
try {
|
||||
var node = getToNode(filepath);
|
||||
// dir check
|
|
@ -0,0 +1,42 @@
|
|||
'use strict';
|
||||
|
||||
jest.autoMockOff();
|
||||
var extractAssetResolution = require('../extractAssetResolution');
|
||||
|
||||
describe('extractAssetResolution', function() {
|
||||
it('should extract resolution simple case', function() {
|
||||
var data = extractAssetResolution('test@2x.png');
|
||||
expect(data).toEqual({
|
||||
assetName: 'test.png',
|
||||
resolution: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it('should default resolution to 1', function() {
|
||||
var data = extractAssetResolution('test.png');
|
||||
expect(data).toEqual({
|
||||
assetName: 'test.png',
|
||||
resolution: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('should support float', function() {
|
||||
var data = extractAssetResolution('test@1.1x.png');
|
||||
expect(data).toEqual({
|
||||
assetName: 'test.png',
|
||||
resolution: 1.1,
|
||||
});
|
||||
|
||||
data = extractAssetResolution('test@.1x.png');
|
||||
expect(data).toEqual({
|
||||
assetName: 'test.png',
|
||||
resolution: 0.1,
|
||||
});
|
||||
|
||||
data = extractAssetResolution('test@0.2x.png');
|
||||
expect(data).toEqual({
|
||||
assetName: 'test.png',
|
||||
resolution: 0.2,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
'use strict';
|
||||
|
||||
var path = require('path');
|
||||
|
||||
function extractAssetResolution(filename) {
|
||||
var ext = path.extname(filename);
|
||||
|
||||
var re = new RegExp('@([\\d\\.]+)x\\' + ext + '$');
|
||||
|
||||
var match = filename.match(re);
|
||||
var resolution;
|
||||
|
||||
if (!(match && match[1])) {
|
||||
resolution = 1;
|
||||
} else {
|
||||
resolution = parseFloat(match[1], 10);
|
||||
if (isNaN(resolution)) {
|
||||
resolution = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
resolution: resolution,
|
||||
assetName: match ? filename.replace(re, ext) : filename,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = extractAssetResolution;
|
Loading…
Reference in New Issue