mirror of
https://github.com/zhigang1992/react-native-web.git
synced 2026-03-31 18:21:38 +08:00
Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71cfd23624 | ||
|
|
77b8e4a1fc | ||
|
|
9543a79c3f | ||
|
|
e3eea6e132 | ||
|
|
4d3418a968 | ||
|
|
ea9bc734f1 | ||
|
|
e03af435ac | ||
|
|
97c0a31ce6 | ||
|
|
25d11ded46 | ||
|
|
6a73d77030 | ||
|
|
0b63ba4e89 | ||
|
|
51109d0768 | ||
|
|
ac04ecd69e | ||
|
|
1a670ba6a7 | ||
|
|
7a16d5711c | ||
|
|
9dde70fff5 | ||
|
|
203980ab66 | ||
|
|
924dc36d4a | ||
|
|
9b2421cdfa | ||
|
|
36ea662402 | ||
|
|
69962ae815 | ||
|
|
62d1a0f83d | ||
|
|
910286303a | ||
|
|
706fa887e6 | ||
|
|
c589d79035 | ||
|
|
83e4c68461 | ||
|
|
54597edbaf | ||
|
|
fc31287566 | ||
|
|
21cc8f47ba | ||
|
|
bf7beb4102 | ||
|
|
127d103c0a | ||
|
|
ae6132af56 | ||
|
|
3c4d7655db | ||
|
|
190966f411 | ||
|
|
8d5ecb84d5 | ||
|
|
b4a9177ce3 | ||
|
|
ad4a6c5be7 | ||
|
|
5f795dfc6c | ||
|
|
949cb75894 | ||
|
|
2e1914080f | ||
|
|
49e9e0ab5b | ||
|
|
ee4c544957 | ||
|
|
56549cf794 | ||
|
|
e6811b2134 | ||
|
|
d8b7dcc60f | ||
|
|
62a08f09ab | ||
|
|
3e7cd1a001 | ||
|
|
8441755d61 | ||
|
|
ba9fa2a7a0 | ||
|
|
e26edfb9ea | ||
|
|
9a8a9ad209 | ||
|
|
efccbe41bb | ||
|
|
f6f8d30aba | ||
|
|
6d7d98c149 | ||
|
|
77d201988d | ||
|
|
b3d7332ddd | ||
|
|
651d519500 | ||
|
|
06d8614519 | ||
|
|
3eced7e842 | ||
|
|
e627e0cd77 | ||
|
|
7ab33727c4 | ||
|
|
7295a8fee8 | ||
|
|
9a09456532 | ||
|
|
4cd38552cd | ||
|
|
b0f35f6c66 | ||
|
|
2dff45b561 | ||
|
|
fd9232201d | ||
|
|
29f6bd363c | ||
|
|
4845de5cb5 | ||
|
|
267a9b55bf | ||
|
|
7add5c524a | ||
|
|
8e0d94e092 | ||
|
|
25f96ba8ae | ||
|
|
2b90bd736f | ||
|
|
791ede06dd | ||
|
|
0567232942 |
3
.babelrc
3
.babelrc
@@ -3,5 +3,8 @@
|
||||
"es2015",
|
||||
"stage-1",
|
||||
"react"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-decorators-legacy"
|
||||
]
|
||||
}
|
||||
|
||||
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
||||
# EditorConfig: http://editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
56
README.md
56
README.md
@@ -2,10 +2,29 @@
|
||||
|
||||
[![Build Status][travis-image]][travis-url]
|
||||
[![npm version][npm-image]][npm-url]
|
||||

|
||||

|
||||
|
||||
[React Native][react-native-url] components and APIs for the Web.
|
||||
|
||||
Browser support: Chrome, Firefox, Safari >= 7, IE 10, Edge.
|
||||
|
||||
## Overview
|
||||
|
||||
"React Native for Web" is a project to bring React Native's building blocks and
|
||||
touch handling to the Web.
|
||||
|
||||
React Native provides a foundational layer to support interoperable,
|
||||
zero-configuration React component development. This is missing from React's
|
||||
web ecosystem where OSS components rely on inline styles (usually without
|
||||
vendor prefixes), or require build tool configuration. This project allows
|
||||
components built upon React Native to be run on the Web, and it manages all
|
||||
component styling out-of-the-box.
|
||||
|
||||
For example, the [`View`](docs/apis/View.md) component makes it easy to build
|
||||
cross-browser layouts with flexbox, such as stacked and nested boxes with
|
||||
margin and padding. And the [`StyleSheet`](docs/guides/style.md) API converts
|
||||
styles defined in JavaScript into "Atomic CSS".
|
||||
|
||||
## Quick start
|
||||
|
||||
To install in your app:
|
||||
@@ -14,24 +33,17 @@ To install in your app:
|
||||
npm install --save react@0.14 react-dom@0.14 react-native-web
|
||||
```
|
||||
|
||||
Or [try it on CodePen](http://codepen.io/necolas/pen/PZzwBR).
|
||||
Read the [Client and Server rendering](docs/guides/rendering.md) guide.
|
||||
|
||||
Browser support: Chrome, Firefox, Safari >= 7, IE 10, Edge.
|
||||
## Examples
|
||||
|
||||
## Overview
|
||||
Demos:
|
||||
|
||||
This is a web implementation of React Native components and APIs. The React
|
||||
Native components are good web application building blocks, and provide a common
|
||||
foundation for component libraries.
|
||||
* [React Native for Web: Playground](http://codepen.io/necolas/pen/PZzwBR).
|
||||
* [TicTacToe](http://codepen.io/necolas/full/eJaLZd/)
|
||||
* [2048](http://codepen.io/necolas/full/wMVvxj/)
|
||||
|
||||
For example, the [`View`](docs/apis/View.md) component makes it easy to build
|
||||
common layouts with flexbox, such as stacked and nested boxes with margin and
|
||||
padding. And the [`StyleSheet`](docs/guides/style.md) API converts styles
|
||||
defined in JavaScript to "atomic" CSS.
|
||||
|
||||
## Example
|
||||
|
||||
More examples can be found in the [`examples` directory](examples).
|
||||
Sample:
|
||||
|
||||
```js
|
||||
import React, { AppRegistry, Image, StyleSheet, Text, View } from 'react-native'
|
||||
@@ -47,10 +59,6 @@ const App = () => (
|
||||
</Card>
|
||||
)
|
||||
|
||||
// App registration and rendering
|
||||
AppRegistry.registerComponent('MyApp', () => App)
|
||||
AppRegistry.runApplication('MyApp', { rootTag: document.getElementById('react-root') })
|
||||
|
||||
// Styles
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
@@ -67,6 +75,10 @@ const styles = StyleSheet.create({
|
||||
width: 40
|
||||
}
|
||||
})
|
||||
|
||||
// App registration and rendering
|
||||
AppRegistry.registerComponent('MyApp', () => App)
|
||||
AppRegistry.runApplication('MyApp', { rootTag: document.getElementById('react-root') })
|
||||
```
|
||||
|
||||
## Documentation
|
||||
@@ -90,17 +102,19 @@ Exported modules:
|
||||
* [`ScrollView`](docs/components/ScrollView.md)
|
||||
* [`Text`](docs/components/Text.md)
|
||||
* [`TextInput`](docs/components/TextInput.md)
|
||||
* [`TouchableHighlight`](docs/components/TouchableHighlight.md)
|
||||
* [`TouchableOpacity`](docs/components/TouchableOpacity.md)
|
||||
* [`TouchableHighlight`](http://facebook.github.io/react-native/releases/0.22/docs/touchablehighlight.html) (mirrors React Native)
|
||||
* [`TouchableOpacity`](http://facebook.github.io/react-native/releases/0.22/docs/touchableopacity.html) (mirrors React Native)
|
||||
* [`TouchableWithoutFeedback`](docs/components/TouchableWithoutFeedback.md)
|
||||
* [`View`](docs/components/View.md)
|
||||
* APIs
|
||||
* [`Animated`](http://facebook.github.io/react-native/releases/0.20/docs/animated.html) (mirrors React Native)
|
||||
* [`AppRegistry`](docs/apis/AppRegistry.md)
|
||||
* [`AppState`](docs/apis/AppState.md)
|
||||
* [`AsyncStorage`](docs/apis/AsyncStorage.md)
|
||||
* [`Dimensions`](docs/apis/Dimensions.md)
|
||||
* [`NativeMethods`](docs/apis/NativeMethods.md)
|
||||
* [`NetInfo`](docs/apis/NetInfo.md)
|
||||
* [`PanResponder`](http://facebook.github.io/react-native/releases/0.20/docs/panresponder.html#content) (mirrors React Native)
|
||||
* [`PixelRatio`](docs/apis/PixelRatio.md)
|
||||
* [`Platform`](docs/apis/Platform.md)
|
||||
* [`StyleSheet`](docs/apis/StyleSheet.md)
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
var path = require('path')
|
||||
|
||||
var ROOT = path.join(__dirname, '..')
|
||||
|
||||
module.exports = {
|
||||
DIST_DIRECTORY: path.join(ROOT, 'dist'),
|
||||
EXAMPLES_DIRECTORY: path.join(ROOT, 'examples'),
|
||||
SRC_DIRECTORY: path.join(ROOT, 'src'),
|
||||
ROOT_DIRECTORY: ROOT,
|
||||
TEST_ENTRY: path.join(ROOT, 'tests.webpack.js')
|
||||
}
|
||||
@@ -16,8 +16,11 @@ into `runApplication`. These should always be used as a pair.
|
||||
(web) static **prerenderApplication**(appKey:string, appParameters: object)
|
||||
|
||||
Renders the given application to an HTML string. Use this for server-side
|
||||
rendering. Return object is of type `{ html: string; style: string; }`, where
|
||||
`html` the prerendered HTML, and `style` is the prerendered style sheet.
|
||||
rendering. Return object is of type `{ html: string; style: string;
|
||||
styleElement: ReactComponent }`. `html` is the prerendered HTML, `style` is the
|
||||
prerendered style sheet, and `styleElement` is a React Component. It's
|
||||
recommended that you use `styleElement` to render the style sheet in an app
|
||||
shell.
|
||||
|
||||
static **registerConfig**(config: Array<AppConfig>)
|
||||
|
||||
@@ -51,7 +54,7 @@ into `runApplication`
|
||||
|
||||
## Example
|
||||
|
||||
```
|
||||
```js
|
||||
AppRegistry.registerComponent('MyApp', () => AppComponent)
|
||||
AppRegistry.runApplication('MyApp', {
|
||||
initialProps: {},
|
||||
|
||||
@@ -44,7 +44,7 @@ internet connectivity.
|
||||
|
||||
Fetching the connection type:
|
||||
|
||||
```
|
||||
```js
|
||||
NetInfo.fetch().then((connectionType) => {
|
||||
console.log('Connection type:', connectionType);
|
||||
});
|
||||
|
||||
@@ -11,6 +11,18 @@ outside of the render loop and are applied as inline styles. Read more about to
|
||||
|
||||
Each key of the object passed to `create` must define a style object.
|
||||
|
||||
**flatten**: function
|
||||
|
||||
Flattens an array of styles into a single style object.
|
||||
|
||||
**renderToString**: function
|
||||
|
||||
Returns a string of CSS used to style the application.
|
||||
|
||||
## Properties
|
||||
|
||||
**hairlineWidth**: number
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
|
||||
@@ -58,20 +58,22 @@ could be an http address or a base64 encoded image.
|
||||
**style**: style
|
||||
|
||||
+ ...[View#style](View.md)
|
||||
|
||||
Defaults:
|
||||
|
||||
```js
|
||||
{
|
||||
alignSelf: 'flex-start',
|
||||
backgroundColor: 'transparent'
|
||||
}
|
||||
```
|
||||
+ `resizeMode`
|
||||
|
||||
**testID**: string
|
||||
|
||||
Used to locate a view in end-to-end tests.
|
||||
|
||||
## Properties
|
||||
|
||||
static **resizeMode**: Object
|
||||
|
||||
Example usage:
|
||||
|
||||
```
|
||||
<Image resizeMode={Image.resizeMode.contain} />
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
```js
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# ScrollView
|
||||
|
||||
Scrollable `View` for use with bounded height, either by setting the height of
|
||||
the view directly (discouraged) or by bounding the height of ancestor views.
|
||||
A scrollable `View` that provides itegration with the touch-locking "responder"
|
||||
system. `ScrollView`'s must have a bounded height: either set the height of the
|
||||
view directly (discouraged) or make sure all parent views have bounded height
|
||||
(e.g., transfer `{ flex: 1}` down the view stack).
|
||||
|
||||
## Props
|
||||
|
||||
**children**: any
|
||||
|
||||
Child content.
|
||||
[...View props](./View.md)
|
||||
|
||||
**contentContainerStyle**: style
|
||||
|
||||
@@ -19,11 +19,34 @@ all of the child views.
|
||||
When true, the scroll view's children are arranged horizontally in a row
|
||||
instead of vertically in a column.
|
||||
|
||||
**keyboardDismissMode**: oneOf('none', 'on-drag') = 'none'
|
||||
|
||||
Determines whether the keyboard gets dismissed in response to a scroll drag.
|
||||
|
||||
* `none` (the default), drags do not dismiss the keyboard.
|
||||
* `on-drag`, the keyboard is dismissed when a drag begins.
|
||||
* `interactive` (not supported on web; same as `none`)
|
||||
|
||||
**onContentSizeChange**: function
|
||||
|
||||
TODO
|
||||
|
||||
Called when scrollable content view of the `ScrollView` changes. It's
|
||||
implemented using the `onLayout` handler attached to the content container
|
||||
which this `ScrollView` renders.
|
||||
|
||||
**onScroll**: function
|
||||
|
||||
Fires at most once per frame during scrolling. The frequency of the events can
|
||||
be contolled using the `scrollEventThrottle` prop.
|
||||
|
||||
**refreshControl**: element
|
||||
|
||||
TODO
|
||||
|
||||
A [RefreshControl](../RefreshControl) component, used to provide
|
||||
pull-to-refresh functionality for the `ScrollView`.
|
||||
|
||||
**scrollEnabled**: bool = true
|
||||
|
||||
When false, the content does not scroll.
|
||||
@@ -36,9 +59,26 @@ tracking the scroll position, but can lead to scroll performance problems. The
|
||||
default value is `0`, which means the scroll event will be sent only once each
|
||||
time the view is scrolled.
|
||||
|
||||
**style**: style
|
||||
## Instance methods
|
||||
|
||||
+ ...[View#style](View.md)
|
||||
**getInnerViewNode()**: any
|
||||
|
||||
Returns a reference to the underlying content container DOM node within the `ScrollView`.
|
||||
|
||||
**getScrollableNode()**: any
|
||||
|
||||
Returns a reference to the underlying scrollable DOM node.
|
||||
|
||||
**getScrollResponder()**: Component
|
||||
|
||||
Returns a reference to the underlying scroll responder, which supports
|
||||
operations like `scrollTo`. All `ScrollView`-like components should implement
|
||||
this method so that they can be composed while providing access to the
|
||||
underlying scroll responder's methods.
|
||||
|
||||
**scrollTo(options: { x: number = 0; y: number = 0; animated: boolean = true })**
|
||||
|
||||
Scrolls to a given `x`, `y` offset (animation is not currently supported).
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -50,7 +90,7 @@ export default class ScrollViewExample extends Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = {
|
||||
items: Array.from({ length: 20 }).map((_, i) => ({ id: i }))
|
||||
items: Array.from(new Array(20)).map((_, i) => ({ id: i }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,8 @@ This function is called on press.
|
||||
+ `letterSpacing`
|
||||
+ `lineHeight`
|
||||
+ `textAlign`
|
||||
+ `textDecoration`
|
||||
+ `textAlignVertical`
|
||||
+ `textDecorationLine`
|
||||
+ `textShadow`
|
||||
+ `textTransform`
|
||||
+ `whiteSpace`
|
||||
|
||||
@@ -147,6 +147,20 @@ Read about how [React form
|
||||
components](https://facebook.github.io/react/docs/forms.html) work. To prevent
|
||||
user edits to the value set `editable={false}`.
|
||||
|
||||
## Instance methods
|
||||
|
||||
**blur()**
|
||||
|
||||
Blur the underlying DOM input.
|
||||
|
||||
**clear()**
|
||||
|
||||
Clear the text from the underlying DOM input.
|
||||
|
||||
**focus()**
|
||||
|
||||
Focus the underlying DOM input.
|
||||
|
||||
## Examples
|
||||
|
||||
```js
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
# Touchable
|
||||
|
||||
A wrapper for making views respond to mouse, keyboard, and touch presses. On
|
||||
press in, the touchable area can display a highlight color, and the opacity of
|
||||
the wrapped view can be decreased.
|
||||
|
||||
This component combines the various `Touchable*` components from React Native.
|
||||
|
||||
Unsupported React Native props:
|
||||
`accessibilityComponentType` (android) – use `accessibilityRole`,
|
||||
`accessibilityTraits` (ios) – use `accessibilityRole`,
|
||||
`onHideUnderlay` – use `onPressOut`,
|
||||
`onShowUnderlay` – use `onPressIn`,
|
||||
`underlayColor` – use `activeUnderlayColor`
|
||||
|
||||
## Props
|
||||
|
||||
**accessibilityLabel**: string
|
||||
|
||||
Overrides the text that's read by the screen reader when the user interacts
|
||||
with the element.
|
||||
|
||||
(web) **accessibilityRole**: oneOf(roles) = 'button'
|
||||
|
||||
Allows assistive technologies to present and support interaction with the view
|
||||
in a manner that is consistent with user expectations for similar views of that
|
||||
type. For example, marking a touchable view with an `accessibilityRole` of
|
||||
`button`. (This is implemented using [ARIA roles](http://www.w3.org/TR/wai-aria/roles#role_definitions)).
|
||||
|
||||
Note: Avoid changing `accessibilityRole` values over time or after user
|
||||
actions. Generally, accessibility APIs do not provide a means of notifying
|
||||
assistive technologies of a `role` value change.
|
||||
|
||||
**accessible**: bool = true
|
||||
|
||||
When `false`, the view is hidden from screenreaders.
|
||||
|
||||
**activeOpacity**: number = 0.8
|
||||
|
||||
Sets the opacity of the child view when `onPressIn` is called. The opacity is
|
||||
reset when `onPressOut` is called.
|
||||
|
||||
(web) **activeUnderlayColor**: string = 'black'
|
||||
|
||||
Sets the color of the background highlight when `onPressIn` is called. The
|
||||
highlight is removed when `onPressOut` is called.
|
||||
|
||||
**children**: element
|
||||
|
||||
A single child element.
|
||||
|
||||
**delayLongPress**: number = 500
|
||||
|
||||
Delay in ms, from `onPressIn`, before `onLongPress` is called.
|
||||
|
||||
**delayPressIn**: number = 0
|
||||
|
||||
(TODO)
|
||||
|
||||
Delay in ms, from the start of the touch, before `onPressIn` is called.
|
||||
|
||||
**delayPressOut**: number = 100
|
||||
|
||||
(TODO)
|
||||
|
||||
Delay in ms, from the release of the touch, before `onPressOut` is called.
|
||||
|
||||
**onLayout**: function
|
||||
|
||||
(TODO)
|
||||
|
||||
**onLongPress**: function
|
||||
|
||||
**onPress**: function
|
||||
|
||||
**onPressIn**: function
|
||||
|
||||
**onPressOut**: function
|
||||
|
||||
**style**: style
|
||||
|
||||
+ ...[View#style](View.md)
|
||||
|
||||
## Examples
|
||||
|
||||
```js
|
||||
import React, { Component, PropTypes, Touchable } from 'react-native'
|
||||
|
||||
export default class Example extends Component {
|
||||
static propTypes = {}
|
||||
|
||||
static defaultProps = {}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Touchable />
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
70
docs/components/TouchableWithoutFeedback.md
Normal file
70
docs/components/TouchableWithoutFeedback.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# TouchableWithoutFeedback
|
||||
|
||||
Do not use unless you have a very good reason. All the elements that respond to
|
||||
press should have a visual feedback when touched. This is one of the primary
|
||||
reason a "web" app doesn't feel "native".
|
||||
|
||||
**NOTE: `TouchableWithoutFeedback` supports only one child**. If you wish to have
|
||||
several child components, wrap them in a View.
|
||||
|
||||
## Props
|
||||
|
||||
**accessibilityLabel**: string
|
||||
|
||||
Overrides the text that's read by the screen reader when the user interacts
|
||||
with the element.
|
||||
|
||||
(web) **accessibilityRole**: oneOf(roles) = 'button'
|
||||
|
||||
Allows assistive technologies to present and support interaction with the view
|
||||
|
||||
**accessible**: bool = true
|
||||
|
||||
When `false`, the view is hidden from screenreaders.
|
||||
|
||||
**delayLongPress**: number
|
||||
|
||||
Delay in ms, from `onPressIn`, before `onLongPress` is called.
|
||||
|
||||
**delayPressIn**: number
|
||||
|
||||
Delay in ms, from the start of the touch, before `onPressIn` is called.
|
||||
|
||||
**delayPressOut**: number
|
||||
|
||||
Delay in ms, from the release of the touch, before `onPressOut` is called.
|
||||
|
||||
**disabled**: bool
|
||||
|
||||
If true, disable all interactions for this component.
|
||||
|
||||
**hitSlop**: `{top: number, left: number, bottom: number, right: number}`
|
||||
|
||||
This defines how far your touch can start away from the button. This is added
|
||||
to `pressRetentionOffset` when moving off of the button. **NOTE**: The touch
|
||||
area never extends past the parent view bounds and the z-index of sibling views
|
||||
always takes precedence if a touch hits two overlapping views.
|
||||
|
||||
**onLayout**: function
|
||||
|
||||
Invoked on mount and layout changes with.
|
||||
|
||||
`{nativeEvent: {layout: {x, y, width, height}}}`
|
||||
|
||||
**onLongPress**: function
|
||||
|
||||
**onPress**: function
|
||||
|
||||
Called when the touch is released, but not if cancelled (e.g. by a scroll that steals the responder lock).
|
||||
|
||||
**onPressIn**: function
|
||||
|
||||
**onPressOut**: function
|
||||
|
||||
**pressRetentionOffset**: `{top: number, left: number, bottom: number, right: number}`
|
||||
|
||||
When the scroll view is disabled, this defines how far your touch may move off
|
||||
of the button, before deactivating the button. Once deactivated, try moving it
|
||||
back and you'll see that the button is once again reactivated! Move it back and
|
||||
forth several times while the scroll view is disabled. Ensure you pass in a
|
||||
constant to reduce memory allocations.
|
||||
@@ -4,21 +4,13 @@
|
||||
style, layout with flexbox, and accessibility controls. It can be nested
|
||||
inside another `View` and has 0-to-many children of any type.
|
||||
|
||||
Also, refer to React Native's documentation about the [Gesture Responder
|
||||
System](http://facebook.github.io/react-native/releases/0.22/docs/gesture-responder-system.html).
|
||||
|
||||
Unsupported React Native props:
|
||||
`accessibilityComponentType` (android) – use `accessibilityRole`,
|
||||
`accessibilityTraits` (ios) – use `accessibilityRole`,
|
||||
`collapsable` (android),
|
||||
`importantForAccessibility` (android),
|
||||
`needsOffscreenAlphaCompositing` (android),
|
||||
`onAccessibilityTap`,
|
||||
`onMagicTap`,
|
||||
`onMoveShouldSetResponder`,
|
||||
`onResponder*`,
|
||||
`onStartShouldSetResponder`,
|
||||
`onStartShouldSetResponderCapture`
|
||||
`removeClippedSubviews` (ios),
|
||||
`renderToHardwareTextureAndroid` (android),
|
||||
`shouldRasterizeIOS` (ios)
|
||||
`hitSlop`,
|
||||
`onMagicTap`
|
||||
|
||||
## Props
|
||||
|
||||
@@ -58,6 +50,29 @@ implemented using `aria-hidden`.)
|
||||
|
||||
(TODO)
|
||||
|
||||
**onMoveShouldSetResponder**: function
|
||||
|
||||
**onMoveShouldSetResponderCapture**: function
|
||||
|
||||
**onResponderGrant**: function
|
||||
|
||||
For most touch interactions, you'll simply want to wrap your component in
|
||||
`TouchableHighlight` or `TouchableOpacity`.
|
||||
|
||||
**onResponderMove**: function
|
||||
|
||||
**onResponderReject**: function
|
||||
|
||||
**onResponderRelease**: function
|
||||
|
||||
**onResponderTerminate**: function
|
||||
|
||||
**onResponderTerminationRequest**: function
|
||||
|
||||
**onStartShouldSetResponder**: function
|
||||
|
||||
**onStartShouldSetResponderCapture**: function
|
||||
|
||||
**pointerEvents**: oneOf('auto', 'box-only', 'box-none', 'none') = 'auto'
|
||||
|
||||
Configure the `pointerEvents` of the view. The enhanced `pointerEvents` modes
|
||||
@@ -136,6 +151,7 @@ from `style`.
|
||||
+ `right`
|
||||
+ `top`
|
||||
+ `transform`
|
||||
+ `transformMatrix`
|
||||
+ `userSelect`
|
||||
+ `visibility`
|
||||
+ `width`
|
||||
|
||||
@@ -7,22 +7,17 @@ views present on iOS/Android are released on Web. We are very much interested in
|
||||
the community's feedback on the next set of modules and views.
|
||||
|
||||
Not all the modules or views for iOS/Android can be implemented on Web. In some
|
||||
cases it will be necessary to use a Web counterpart.
|
||||
cases it will be necessary to use a Web counterpart or to guard the use of a
|
||||
module with `Platform.OS` (e.g. `NativeModules`)
|
||||
|
||||
## Missing component props
|
||||
|
||||
There are properties that do not work across all platforms. All web-specific
|
||||
props are annotated with `(web)` in the documentaiton.
|
||||
props are annotated with `(web)` in the documentation.
|
||||
|
||||
## Platform parity
|
||||
|
||||
There are some known issues in React Native where APIs could be made more
|
||||
consistent between platforms. For example, React Native for Web includes
|
||||
`ActivityIndicator` and a horizontal `ProgressBar`.
|
||||
|
||||
Other parts of React Native, such as the `Animated` and `PanResponder` APIs,
|
||||
are highly complex and have not yet been ported to React Native for Web. Given
|
||||
the difficulties keeping these APIs in sync with React Native, we'd prefer the
|
||||
APIs to be published as separate npm packages. If not, we will consider a web
|
||||
implementation, possibly using the [Web Animations
|
||||
API/polyfill](https://github.com/web-animations/web-animations-js)
|
||||
`ActivityIndicator` and a horizontal `ProgressBar` for Web use, in anticipation
|
||||
of the convergence between the iOS and Android components in React Native.
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
# React Native
|
||||
|
||||
This is an experimental feature to support: using community-developed React
|
||||
Native components on the Web; and rendering React Native apps to Web.
|
||||
|
||||
Use a module loader that supports package aliases (e.g., webpack), and alias
|
||||
`react-native` to `react-native-web`.
|
||||
|
||||
@@ -10,6 +7,7 @@ Use a module loader that supports package aliases (e.g., webpack), and alias
|
||||
// webpack.config.js
|
||||
|
||||
module.exports = {
|
||||
// ...
|
||||
resolve: {
|
||||
alias: {
|
||||
'react-native': 'react-native-web'
|
||||
@@ -18,8 +16,26 @@ module.exports = {
|
||||
}
|
||||
```
|
||||
|
||||
Web-specific implementations can use the `*.web.js` naming pattern, which
|
||||
webpack will resolve.
|
||||
## Image assets
|
||||
|
||||
In order to require image assets (e.g. `require('assets/myimage.png')`), add
|
||||
the `url-loader` to the webpack config:
|
||||
|
||||
```js
|
||||
// webpack.config.js
|
||||
|
||||
module.exports = {
|
||||
// ...
|
||||
module: {
|
||||
loaders: {
|
||||
test: /\.(gif|jpe?g|png|svg)$/,
|
||||
loader: 'url-loader',
|
||||
query: { name: '[name].[hash:16].[ext]' }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Web-specific code
|
||||
|
||||
Minor platform differences can use the `Platform` module.
|
||||
|
||||
@@ -38,3 +54,24 @@ if (Platform.OS === 'web') {
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
More substantial Web-specific implementation code should be written in files
|
||||
with the extension `.web.js`, which webpack will automatically resolve.
|
||||
|
||||
## Optimizations
|
||||
|
||||
Production builds can benefit from dead-code elimination by defining the
|
||||
following variables:
|
||||
|
||||
```js
|
||||
// webpack.config.js
|
||||
|
||||
module.exports = {
|
||||
// ...
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.NODE_ENV': JSON.stringify('production')
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -1,57 +1,97 @@
|
||||
# Client and Server rendering
|
||||
|
||||
It's recommended that you use a module loader that supports package aliases
|
||||
(e.g., webpack), and alias `react-native` to `react-native-web`.
|
||||
|
||||
```js
|
||||
// webpack.config.js
|
||||
|
||||
module.exports = {
|
||||
// ...other configuration
|
||||
resolve: {
|
||||
alias: {
|
||||
'react-native': 'react-native-web'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Client-side rendering
|
||||
|
||||
Rendering without using the `AppRegistry`:
|
||||
|
||||
```js
|
||||
import React from 'react-native'
|
||||
|
||||
// DOM render
|
||||
React.render(<div />, document.getElementById('react-app'))
|
||||
|
||||
// Server render
|
||||
React.renderToString(<div />)
|
||||
React.renderToStaticMarkup(<div />)
|
||||
```
|
||||
|
||||
Rendering using the `AppRegistry`:
|
||||
|
||||
```js
|
||||
// App.js
|
||||
|
||||
import React, { AppRegistry } from 'react-native'
|
||||
|
||||
// component that renders the app
|
||||
const AppContainer = (props) => { /* ... */ }
|
||||
export default AppContainer
|
||||
```
|
||||
|
||||
```js
|
||||
// client.js
|
||||
|
||||
import React, { AppRegistry } from 'react-native'
|
||||
import MyApp from './MyApp'
|
||||
import App from './App'
|
||||
|
||||
// register the app
|
||||
AppRegistry.registerApp('MyApp', () => MyApp)
|
||||
// registers the app
|
||||
AppRegistry.registerComponent('App', () => App)
|
||||
|
||||
// mount the app within the `rootTag` and run it
|
||||
AppRegistry.runApplication('MyApp', { initialProps, rootTag: document.getElementById('react-root') })
|
||||
|
||||
// DOM render
|
||||
React.render(<div />, document.getElementById('sidebar-app'))
|
||||
|
||||
// Server render
|
||||
React.renderToString(<div />)
|
||||
// mounts and runs the app within the `rootTag` DOM node
|
||||
AppRegistry.runApplication('App', { initialProps, rootTag: document.getElementById('react-app') })
|
||||
```
|
||||
|
||||
## Server-side rendering
|
||||
|
||||
Pre-rendering React apps on the server is a key feature for Web applications.
|
||||
React Native for Web extends `AppRegistry` to provide support for server-side
|
||||
rendering.
|
||||
|
||||
```js
|
||||
// AppShell.js
|
||||
|
||||
import React from 'react-native'
|
||||
|
||||
const AppShell = (html, styleElement) => (
|
||||
<html>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta content="initial-scale=1,width=device-width" name="viewport" />
|
||||
{styleElement}
|
||||
</head>
|
||||
<body>
|
||||
<div id="react-app" dangerouslySetInnerHTML={{ __html: html }} />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
export default AppShell
|
||||
```
|
||||
|
||||
```js
|
||||
// server.js
|
||||
|
||||
import React, { AppRegistry } from 'react-native'
|
||||
import MyApp from './MyApp'
|
||||
import App from './App'
|
||||
import AppShell from './AppShell'
|
||||
|
||||
// register the app
|
||||
AppRegistry.registerApp('MyApp', () => MyApp)
|
||||
// registers the app
|
||||
AppRegistry.registerComponent('App', () => App)
|
||||
|
||||
// prerender the app
|
||||
const { html, style } = AppRegistry.prerenderApplication('MyApp', { initialProps })
|
||||
// prerenders the app
|
||||
const { html, style, styleElement } = AppRegistry.prerenderApplication('App', { initialProps })
|
||||
|
||||
// construct full page markup
|
||||
const HtmlShell = (html, style) => (
|
||||
<html>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
<meta content="initial-scale=1,width=device-width" name="viewport" />
|
||||
{style}
|
||||
</head>
|
||||
<body>
|
||||
<div id="react-root" dangerouslySetInnerHTML={{ __html: html }} />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
||||
React.renderToStaticMarkup(<HtmlShell html={html} style={style} />)
|
||||
// renders the full-page markup
|
||||
const renderedApplicationHTML = React.renderToStaticMarkup(<AppShell html={html} styleElement={styleElement} />)
|
||||
```
|
||||
|
||||
@@ -174,16 +174,16 @@ const styles = StyleSheet.create({
|
||||
CSS output:
|
||||
|
||||
```css
|
||||
._s1 { color: gray; }
|
||||
._s2 { font-size: 2rem; }
|
||||
._s3 { font-size: 1.25rem; }
|
||||
.__style1 { color: gray; }
|
||||
.__style2 { font-size: 2rem; }
|
||||
.__style3 { font-size: 1.25rem; }
|
||||
```
|
||||
|
||||
Rendered HTML:
|
||||
|
||||
```html
|
||||
<span className="_s1 _s2">Heading</span>
|
||||
<span className="_s1 _s3">Text</span>
|
||||
<span className="__style1 __style2">Heading</span>
|
||||
<span className="__style1 __style3">Text</span>
|
||||
```
|
||||
|
||||
### Reset
|
||||
@@ -200,6 +200,7 @@ html {
|
||||
font-family: sans-serif;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color:rgba(0,0,0,0)
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -214,12 +215,6 @@ input::-moz-focus-inner {
|
||||
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
li {
|
||||
list-style:none
|
||||
display: none;
|
||||
}
|
||||
```
|
||||
|
||||
324
examples/2048/Game2048.js
Normal file
324
examples/2048/Game2048.js
Normal file
@@ -0,0 +1,324 @@
|
||||
/**
|
||||
* The examples provided by Facebook are for non-commercial testing and
|
||||
* evaluation purposes only.
|
||||
*
|
||||
* Facebook reserves all rights not expressly granted.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
|
||||
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* @providesModule Game2048
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var React = require('react-native');
|
||||
var {
|
||||
Animated,
|
||||
AppRegistry,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableBounce,
|
||||
View,
|
||||
} = React;
|
||||
|
||||
var GameBoard = require('./GameBoard');
|
||||
|
||||
var BOARD_PADDING = 3;
|
||||
var CELL_MARGIN = 4;
|
||||
var CELL_SIZE = 60;
|
||||
|
||||
class Cell extends React.Component {
|
||||
render() {
|
||||
return <View style={styles.cell} />;
|
||||
}
|
||||
}
|
||||
|
||||
class Board extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<View style={styles.board}>
|
||||
<View style={styles.row}><Cell/><Cell/><Cell/><Cell/></View>
|
||||
<View style={styles.row}><Cell/><Cell/><Cell/><Cell/></View>
|
||||
<View style={styles.row}><Cell/><Cell/><Cell/><Cell/></View>
|
||||
<View style={styles.row}><Cell/><Cell/><Cell/><Cell/></View>
|
||||
{this.props.children}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Tile extends React.Component {
|
||||
state: any;
|
||||
|
||||
static _getPosition(index): number {
|
||||
return BOARD_PADDING + (index * (CELL_SIZE + CELL_MARGIN * 2) + CELL_MARGIN);
|
||||
}
|
||||
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
|
||||
var tile = this.props.tile;
|
||||
|
||||
this.state = {
|
||||
opacity: new Animated.Value(0),
|
||||
top: new Animated.Value(Tile._getPosition(tile.toRow())),
|
||||
left: new Animated.Value(Tile._getPosition(tile.toColumn())),
|
||||
};
|
||||
}
|
||||
|
||||
calculateOffset(): {top: number; left: number; opacity: number} {
|
||||
var tile = this.props.tile;
|
||||
|
||||
var offset = {
|
||||
top: this.state.top,
|
||||
left: this.state.left,
|
||||
opacity: this.state.opacity,
|
||||
};
|
||||
|
||||
if (tile.isNew()) {
|
||||
Animated.timing(this.state.opacity, {
|
||||
duration: 100,
|
||||
toValue: 1,
|
||||
}).start();
|
||||
} else {
|
||||
Animated.parallel([
|
||||
Animated.timing(offset.top, {
|
||||
duration: 100,
|
||||
toValue: Tile._getPosition(tile.toRow()),
|
||||
}),
|
||||
Animated.timing(offset.left, {
|
||||
duration: 100,
|
||||
toValue: Tile._getPosition(tile.toColumn()),
|
||||
}),
|
||||
]).start();
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
render() {
|
||||
var tile = this.props.tile;
|
||||
|
||||
var tileStyles = [
|
||||
styles.tile,
|
||||
styles['tile' + tile.value],
|
||||
this.calculateOffset(),
|
||||
];
|
||||
|
||||
var textStyles = [
|
||||
styles.value,
|
||||
tile.value > 4 && styles.whiteText,
|
||||
tile.value > 100 && styles.threeDigits,
|
||||
tile.value > 1000 && styles.fourDigits,
|
||||
];
|
||||
|
||||
return (
|
||||
<Animated.View style={tileStyles}>
|
||||
<Text style={textStyles}>{tile.value}</Text>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GameEndOverlay extends React.Component {
|
||||
render() {
|
||||
var board = this.props.board;
|
||||
|
||||
if (!board.hasWon() && !board.hasLost()) {
|
||||
return <View/>;
|
||||
}
|
||||
|
||||
var message = board.hasWon() ?
|
||||
'Good Job!' : 'Game Over';
|
||||
|
||||
return (
|
||||
<View style={styles.overlay}>
|
||||
<Text style={styles.overlayMessage}>{message}</Text>
|
||||
<TouchableBounce onPress={this.props.onRestart} style={styles.tryAgain}>
|
||||
<Text style={styles.tryAgainText}>Try Again?</Text>
|
||||
</TouchableBounce>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Game2048 extends React.Component {
|
||||
startX: number;
|
||||
startY: number;
|
||||
state: any;
|
||||
|
||||
constructor(props: {}) {
|
||||
super(props);
|
||||
this.state = {
|
||||
board: new GameBoard(),
|
||||
};
|
||||
this.startX = 0;
|
||||
this.startY = 0;
|
||||
}
|
||||
|
||||
restartGame() {
|
||||
this.setState({board: new GameBoard()});
|
||||
}
|
||||
|
||||
handleTouchStart(event: Object) {
|
||||
if (this.state.board.hasWon()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.startX = event.nativeEvent.pageX;
|
||||
this.startY = event.nativeEvent.pageY;
|
||||
}
|
||||
|
||||
handleTouchEnd(event: Object) {
|
||||
if (this.state.board.hasWon()) {
|
||||
return;
|
||||
}
|
||||
|
||||
var deltaX = event.nativeEvent.pageX - this.startX;
|
||||
var deltaY = event.nativeEvent.pageY - this.startY;
|
||||
|
||||
var direction = -1;
|
||||
if (Math.abs(deltaX) > 3 * Math.abs(deltaY) && Math.abs(deltaX) > 30) {
|
||||
direction = deltaX > 0 ? 2 : 0;
|
||||
} else if (Math.abs(deltaY) > 3 * Math.abs(deltaX) && Math.abs(deltaY) > 30) {
|
||||
direction = deltaY > 0 ? 3 : 1;
|
||||
}
|
||||
|
||||
if (direction !== -1) {
|
||||
this.setState({board: this.state.board.move(direction)});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
var tiles = this.state.board.tiles
|
||||
.filter((tile) => tile.value)
|
||||
.map((tile) => <Tile ref={tile.id} key={tile.id} tile={tile} />);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={styles.container}
|
||||
onTouchStart={(event) => this.handleTouchStart(event)}
|
||||
onTouchEnd={(event) => this.handleTouchEnd(event)}>
|
||||
<Board>
|
||||
{tiles}
|
||||
</Board>
|
||||
<GameEndOverlay board={this.state.board} onRestart={() => this.restartGame()} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
board: {
|
||||
padding: BOARD_PADDING,
|
||||
backgroundColor: '#bbaaaa',
|
||||
borderRadius: 5,
|
||||
},
|
||||
overlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: 'rgba(221, 221, 221, 0.5)',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
overlayMessage: {
|
||||
fontSize: 40,
|
||||
marginBottom: 20,
|
||||
},
|
||||
tryAgain: {
|
||||
backgroundColor: '#887761',
|
||||
padding: 20,
|
||||
borderRadius: 5,
|
||||
},
|
||||
tryAgainText: {
|
||||
color: '#ffffff',
|
||||
fontSize: 20,
|
||||
fontWeight: '500',
|
||||
},
|
||||
cell: {
|
||||
width: CELL_SIZE,
|
||||
height: CELL_SIZE,
|
||||
borderRadius: 5,
|
||||
backgroundColor: '#ddccbb',
|
||||
margin: CELL_MARGIN,
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
tile: {
|
||||
position: 'absolute',
|
||||
width: CELL_SIZE,
|
||||
height: CELL_SIZE,
|
||||
backgroundColor: '#ddccbb',
|
||||
borderRadius: 5,
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
value: {
|
||||
fontSize: 24,
|
||||
color: '#776666',
|
||||
fontFamily: 'Verdana',
|
||||
fontWeight: '500',
|
||||
},
|
||||
tile2: {
|
||||
backgroundColor: '#eeeeee',
|
||||
},
|
||||
tile4: {
|
||||
backgroundColor: '#eeeecc',
|
||||
},
|
||||
tile8: {
|
||||
backgroundColor: '#ffbb87',
|
||||
},
|
||||
tile16: {
|
||||
backgroundColor: '#ff9966',
|
||||
},
|
||||
tile32: {
|
||||
backgroundColor: '#ff7755',
|
||||
},
|
||||
tile64: {
|
||||
backgroundColor: '#ff5533',
|
||||
},
|
||||
tile128: {
|
||||
backgroundColor: '#eecc77',
|
||||
},
|
||||
tile256: {
|
||||
backgroundColor: '#eecc66',
|
||||
},
|
||||
tile512: {
|
||||
backgroundColor: '#eecc55',
|
||||
},
|
||||
tile1024: {
|
||||
backgroundColor: '#eecc33',
|
||||
},
|
||||
tile2048: {
|
||||
backgroundColor: '#eecc22',
|
||||
},
|
||||
whiteText: {
|
||||
color: '#ffffff',
|
||||
},
|
||||
threeDigits: {
|
||||
fontSize: 20,
|
||||
},
|
||||
fourDigits: {
|
||||
fontSize: 18,
|
||||
},
|
||||
});
|
||||
|
||||
AppRegistry.registerComponent('Game2048', () => Game2048);
|
||||
|
||||
module.exports = Game2048;
|
||||
201
examples/2048/GameBoard.js
Normal file
201
examples/2048/GameBoard.js
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* The examples provided by Facebook are for non-commercial testing and
|
||||
* evaluation purposes only.
|
||||
*
|
||||
* Facebook reserves all rights not expressly granted.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
|
||||
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* @providesModule GameBoard
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
// NB: Taken straight from: https://github.com/IvanVergiliev/2048-react/blob/master/src/board.js
|
||||
// with no modification except to format it for CommonJS and fix lint/flow errors
|
||||
|
||||
var rotateLeft = function (matrix) {
|
||||
var rows = matrix.length;
|
||||
var columns = matrix[0].length;
|
||||
var res = [];
|
||||
for (var row = 0; row < rows; ++row) {
|
||||
res.push([]);
|
||||
for (var column = 0; column < columns; ++column) {
|
||||
res[row][column] = matrix[column][columns - row - 1];
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
var Tile = function (value?: number, row?: number, column?: number) {
|
||||
this.value = value || 0;
|
||||
this.row = row || -1;
|
||||
|
||||
this.column = column || -1;
|
||||
this.oldRow = -1;
|
||||
this.oldColumn = -1;
|
||||
this.markForDeletion = false;
|
||||
this.mergedInto = null;
|
||||
this.id = Tile.id++;
|
||||
};
|
||||
|
||||
Tile.id = 0;
|
||||
|
||||
Tile.prototype.moveTo = function (row, column) {
|
||||
this.oldRow = this.row;
|
||||
this.oldColumn = this.column;
|
||||
this.row = row;
|
||||
this.column = column;
|
||||
};
|
||||
|
||||
Tile.prototype.isNew = function () {
|
||||
return this.oldRow === -1 && !this.mergedInto;
|
||||
};
|
||||
|
||||
Tile.prototype.hasMoved = function () {
|
||||
return (this.fromRow() !== -1 && (this.fromRow() !== this.toRow() || this.fromColumn() !== this.toColumn())) ||
|
||||
this.mergedInto;
|
||||
};
|
||||
|
||||
Tile.prototype.fromRow = function () {
|
||||
return this.mergedInto ? this.row : this.oldRow;
|
||||
};
|
||||
|
||||
Tile.prototype.fromColumn = function () {
|
||||
return this.mergedInto ? this.column : this.oldColumn;
|
||||
};
|
||||
|
||||
Tile.prototype.toRow = function () {
|
||||
return this.mergedInto ? this.mergedInto.row : this.row;
|
||||
};
|
||||
|
||||
Tile.prototype.toColumn = function () {
|
||||
return this.mergedInto ? this.mergedInto.column : this.column;
|
||||
};
|
||||
|
||||
var Board = function () {
|
||||
this.tiles = [];
|
||||
this.cells = [];
|
||||
for (var i = 0; i < Board.size; ++i) {
|
||||
this.cells[i] = [this.addTile(), this.addTile(), this.addTile(), this.addTile()];
|
||||
}
|
||||
this.addRandomTile();
|
||||
this.setPositions();
|
||||
this.won = false;
|
||||
};
|
||||
|
||||
Board.prototype.addTile = function () {
|
||||
var res = new Tile();
|
||||
Tile.apply(res, arguments);
|
||||
this.tiles.push(res);
|
||||
return res;
|
||||
};
|
||||
|
||||
Board.size = 4;
|
||||
|
||||
Board.prototype.moveLeft = function () {
|
||||
var hasChanged = false;
|
||||
for (var row = 0; row < Board.size; ++row) {
|
||||
var currentRow = this.cells[row].filter(function (tile) { return tile.value !== 0; });
|
||||
var resultRow = [];
|
||||
for (var target = 0; target < Board.size; ++target) {
|
||||
var targetTile = currentRow.length ? currentRow.shift() : this.addTile();
|
||||
if (currentRow.length > 0 && currentRow[0].value === targetTile.value) {
|
||||
var tile1 = targetTile;
|
||||
targetTile = this.addTile(targetTile.value);
|
||||
tile1.mergedInto = targetTile;
|
||||
var tile2 = currentRow.shift();
|
||||
tile2.mergedInto = targetTile;
|
||||
targetTile.value += tile2.value;
|
||||
}
|
||||
resultRow[target] = targetTile;
|
||||
this.won = this.won || (targetTile.value === 2048);
|
||||
hasChanged = hasChanged || (targetTile.value !== this.cells[row][target].value);
|
||||
}
|
||||
this.cells[row] = resultRow;
|
||||
}
|
||||
return hasChanged;
|
||||
};
|
||||
|
||||
Board.prototype.setPositions = function () {
|
||||
this.cells.forEach(function (row, rowIndex) {
|
||||
row.forEach(function (tile, columnIndex) {
|
||||
tile.oldRow = tile.row;
|
||||
tile.oldColumn = tile.column;
|
||||
tile.row = rowIndex;
|
||||
tile.column = columnIndex;
|
||||
tile.markForDeletion = false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
Board.fourProbability = 0.1;
|
||||
|
||||
Board.prototype.addRandomTile = function () {
|
||||
var emptyCells = [];
|
||||
for (var r = 0; r < Board.size; ++r) {
|
||||
for (var c = 0; c < Board.size; ++c) {
|
||||
if (this.cells[r][c].value === 0) {
|
||||
emptyCells.push({r: r, c: c});
|
||||
}
|
||||
}
|
||||
}
|
||||
var index = Math.floor(Math.random() * emptyCells.length);
|
||||
var cell = emptyCells[index];
|
||||
var newValue = Math.random() < Board.fourProbability ? 4 : 2;
|
||||
this.cells[cell.r][cell.c] = this.addTile(newValue);
|
||||
};
|
||||
|
||||
Board.prototype.move = function (direction) {
|
||||
// 0 -> left, 1 -> up, 2 -> right, 3 -> down
|
||||
this.clearOldTiles();
|
||||
for (var i = 0; i < direction; ++i) {
|
||||
this.cells = rotateLeft(this.cells);
|
||||
}
|
||||
var hasChanged = this.moveLeft();
|
||||
for (var i = direction; i < 4; ++i) {
|
||||
this.cells = rotateLeft(this.cells);
|
||||
}
|
||||
if (hasChanged) {
|
||||
this.addRandomTile();
|
||||
}
|
||||
this.setPositions();
|
||||
return this;
|
||||
};
|
||||
|
||||
Board.prototype.clearOldTiles = function () {
|
||||
this.tiles = this.tiles.filter(function (tile) { return tile.markForDeletion === false; });
|
||||
this.tiles.forEach(function (tile) { tile.markForDeletion = true; });
|
||||
};
|
||||
|
||||
Board.prototype.hasWon = function () {
|
||||
return this.won;
|
||||
};
|
||||
|
||||
Board.deltaX = [-1, 0, 1, 0];
|
||||
Board.deltaY = [0, -1, 0, 1];
|
||||
|
||||
Board.prototype.hasLost = function () {
|
||||
var canMove = false;
|
||||
for (var row = 0; row < Board.size; ++row) {
|
||||
for (var column = 0; column < Board.size; ++column) {
|
||||
canMove = canMove || (this.cells[row][column].value === 0);
|
||||
for (var dir = 0; dir < 4; ++dir) {
|
||||
var newRow = row + Board.deltaX[dir];
|
||||
var newColumn = column + Board.deltaY[dir];
|
||||
if (newRow < 0 || newRow >= Board.size || newColumn < 0 || newColumn >= Board.size) {
|
||||
continue;
|
||||
}
|
||||
canMove = canMove || (this.cells[row][column].value === this.cells[newRow][newColumn].value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return !canMove;
|
||||
};
|
||||
|
||||
module.exports = Board;
|
||||
321
examples/TicTacToe/TicTacToe.js
Normal file
321
examples/TicTacToe/TicTacToe.js
Normal file
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* The examples provided by Facebook are for non-commercial testing and
|
||||
* evaluation purposes only.
|
||||
*
|
||||
* Facebook reserves all rights not expressly granted.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL
|
||||
* FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* @providesModule TicTacToeApp
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var React = require('react-native');
|
||||
var {
|
||||
AppRegistry,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableHighlight,
|
||||
View,
|
||||
} = React;
|
||||
|
||||
class Board {
|
||||
grid: Array<Array<number>>;
|
||||
turn: number;
|
||||
|
||||
constructor() {
|
||||
var size = 3;
|
||||
var grid = Array(size);
|
||||
for (var i = 0; i < size; i++) {
|
||||
var row = Array(size);
|
||||
for (var j = 0; j < size; j++) {
|
||||
row[j] = 0;
|
||||
}
|
||||
grid[i] = row;
|
||||
}
|
||||
this.grid = grid;
|
||||
|
||||
this.turn = 1;
|
||||
}
|
||||
|
||||
mark(row: number, col: number, player: number): Board {
|
||||
this.grid[row][col] = player;
|
||||
return this;
|
||||
}
|
||||
|
||||
hasMark(row: number, col: number): boolean {
|
||||
return this.grid[row][col] !== 0;
|
||||
}
|
||||
|
||||
winner(): ?number {
|
||||
for (var i = 0; i < 3; i++) {
|
||||
if (this.grid[i][0] !== 0 && this.grid[i][0] === this.grid[i][1] &&
|
||||
this.grid[i][0] === this.grid[i][2]) {
|
||||
return this.grid[i][0];
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < 3; i++) {
|
||||
if (this.grid[0][i] !== 0 && this.grid[0][i] === this.grid[1][i] &&
|
||||
this.grid[0][i] === this.grid[2][i]) {
|
||||
return this.grid[0][i];
|
||||
}
|
||||
}
|
||||
|
||||
if (this.grid[0][0] !== 0 && this.grid[0][0] === this.grid[1][1] &&
|
||||
this.grid[0][0] === this.grid[2][2]) {
|
||||
return this.grid[0][0];
|
||||
}
|
||||
|
||||
if (this.grid[0][2] !== 0 && this.grid[0][2] === this.grid[1][1] &&
|
||||
this.grid[0][2] === this.grid[2][0]) {
|
||||
return this.grid[0][2];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
tie(): boolean {
|
||||
for (var i = 0; i < 3; i++) {
|
||||
for (var j = 0; j < 3; j++) {
|
||||
if (this.grid[i][j] === 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.winner() === null;
|
||||
}
|
||||
}
|
||||
|
||||
var Cell = React.createClass({
|
||||
cellStyle() {
|
||||
switch (this.props.player) {
|
||||
case 1:
|
||||
return styles.cellX;
|
||||
case 2:
|
||||
return styles.cellO;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
textStyle() {
|
||||
switch (this.props.player) {
|
||||
case 1:
|
||||
return styles.cellTextX;
|
||||
case 2:
|
||||
return styles.cellTextO;
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
},
|
||||
|
||||
textContents() {
|
||||
switch (this.props.player) {
|
||||
case 1:
|
||||
return 'X';
|
||||
case 2:
|
||||
return 'O';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<TouchableHighlight
|
||||
onPress={this.props.onPress}
|
||||
underlayColor="transparent"
|
||||
activeOpacity={0.5}>
|
||||
<View style={[styles.cell, this.cellStyle()]}>
|
||||
<Text style={[styles.cellText, this.textStyle()]}>
|
||||
{this.textContents()}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var GameEndOverlay = React.createClass({
|
||||
render() {
|
||||
var board = this.props.board;
|
||||
|
||||
var tie = board.tie();
|
||||
var winner = board.winner();
|
||||
if (!winner && !tie) {
|
||||
return <View />;
|
||||
}
|
||||
|
||||
var message;
|
||||
if (tie) {
|
||||
message = 'It\'s a tie!';
|
||||
} else {
|
||||
message = (winner === 1 ? 'X' : 'O') + ' wins!';
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.overlay}>
|
||||
<Text style={styles.overlayMessage}>{message}</Text>
|
||||
<TouchableHighlight
|
||||
onPress={this.props.onRestart}
|
||||
underlayColor="transparent"
|
||||
activeOpacity={0.5}>
|
||||
<View style={styles.newGame}>
|
||||
<Text style={styles.newGameText}>New Game</Text>
|
||||
</View>
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var TicTacToeApp = React.createClass({
|
||||
getInitialState() {
|
||||
return { board: new Board(), player: 1 };
|
||||
},
|
||||
|
||||
restartGame() {
|
||||
this.setState(this.getInitialState());
|
||||
},
|
||||
|
||||
nextPlayer(): number {
|
||||
return this.state.player === 1 ? 2 : 1;
|
||||
},
|
||||
|
||||
handleCellPress(row: number, col: number) {
|
||||
if (this.state.board.hasMark(row, col)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
board: this.state.board.mark(row, col, this.state.player),
|
||||
player: this.nextPlayer(),
|
||||
});
|
||||
},
|
||||
|
||||
render() {
|
||||
var rows = this.state.board.grid.map((cells, row) =>
|
||||
<View key={'row' + row} style={styles.row}>
|
||||
{cells.map((player, col) =>
|
||||
<Cell
|
||||
key={'cell' + col}
|
||||
player={player}
|
||||
onPress={this.handleCellPress.bind(this, row, col)}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>EXTREME T3</Text>
|
||||
<View style={styles.board}>
|
||||
{rows}
|
||||
</View>
|
||||
<GameEndOverlay
|
||||
board={this.state.board}
|
||||
onRestart={this.restartGame}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'white'
|
||||
},
|
||||
title: {
|
||||
fontFamily: 'Chalkduster',
|
||||
fontSize: 39,
|
||||
marginBottom: 20,
|
||||
},
|
||||
board: {
|
||||
padding: 5,
|
||||
backgroundColor: '#47525d',
|
||||
borderRadius: 10,
|
||||
},
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
|
||||
// CELL
|
||||
|
||||
cell: {
|
||||
width: 80,
|
||||
height: 80,
|
||||
borderRadius: 5,
|
||||
backgroundColor: '#7b8994',
|
||||
margin: 5,
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
cellX: {
|
||||
backgroundColor: '#72d0eb',
|
||||
},
|
||||
cellO: {
|
||||
backgroundColor: '#7ebd26',
|
||||
},
|
||||
|
||||
// CELL TEXT
|
||||
|
||||
cellText: {
|
||||
borderRadius: 5,
|
||||
fontSize: 50,
|
||||
fontFamily: 'AvenirNext-Bold',
|
||||
},
|
||||
cellTextX: {
|
||||
color: '#19a9e5',
|
||||
},
|
||||
cellTextO: {
|
||||
color: '#b9dc2f',
|
||||
},
|
||||
|
||||
// GAME OVER
|
||||
|
||||
overlay: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
backgroundColor: 'rgba(221, 221, 221, 0.5)',
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
overlayMessage: {
|
||||
fontSize: 40,
|
||||
marginBottom: 20,
|
||||
marginLeft: 20,
|
||||
marginRight: 20,
|
||||
fontFamily: 'AvenirNext-DemiBold',
|
||||
textAlign: 'center',
|
||||
},
|
||||
newGame: {
|
||||
backgroundColor: '#887765',
|
||||
padding: 20,
|
||||
borderRadius: 5,
|
||||
},
|
||||
newGameText: {
|
||||
color: 'white',
|
||||
fontSize: 20,
|
||||
fontFamily: 'AvenirNext-DemiBold',
|
||||
},
|
||||
});
|
||||
|
||||
AppRegistry.registerComponent('TicTacToeApp', () => TicTacToeApp);
|
||||
|
||||
module.exports = TicTacToeApp;
|
||||
@@ -1,18 +1,4 @@
|
||||
import React, { Component, PropTypes, StyleSheet, View } from '../../src'
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
overflow: 'hidden'
|
||||
},
|
||||
contentContainer: {
|
||||
flexDirection: 'row',
|
||||
flexGrow: 1
|
||||
},
|
||||
// distribute all space (rather than extra space)
|
||||
column: {
|
||||
flexBasis: '0%'
|
||||
}
|
||||
})
|
||||
import React, { Component, PropTypes, StyleSheet, View } from 'react-native'
|
||||
|
||||
export default class GridView extends Component {
|
||||
static propTypes = {
|
||||
@@ -63,3 +49,17 @@ export default class GridView extends Component {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
overflow: 'hidden'
|
||||
},
|
||||
contentContainer: {
|
||||
flexDirection: 'row',
|
||||
flexGrow: 1
|
||||
},
|
||||
// distribute all space (rather than extra space)
|
||||
column: {
|
||||
flexBasis: '0%'
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import React, { StyleSheet, Text } from '../../src'
|
||||
import React, { StyleSheet, Text } from 'react-native'
|
||||
|
||||
const Heading = ({ children, size = 'normal' }) => (
|
||||
<Text
|
||||
accessibilityRole='heading'
|
||||
children={children}
|
||||
style={{ ...styles.root, ...sizeStyles[size] }}
|
||||
/>
|
||||
)
|
||||
|
||||
const sizeStyles = StyleSheet.create({
|
||||
xlarge: {
|
||||
@@ -23,12 +31,4 @@ const styles = StyleSheet.create({
|
||||
}
|
||||
})
|
||||
|
||||
const Heading = ({ children, size = 'normal' }) => (
|
||||
<Text
|
||||
accessibilityRole='heading'
|
||||
children={children}
|
||||
style={{ ...styles.root, ...sizeStyles[size] }}
|
||||
/>
|
||||
)
|
||||
|
||||
export default Heading
|
||||
|
||||
@@ -1,21 +1,4 @@
|
||||
import React, { StyleSheet, Text, View } from '../../src'
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
marginVertical: 10,
|
||||
padding: 10
|
||||
},
|
||||
heading: {
|
||||
fontWeight: 'bold',
|
||||
padding: 5,
|
||||
textAlign: 'center'
|
||||
},
|
||||
text: {
|
||||
textAlign: 'center'
|
||||
}
|
||||
})
|
||||
import React, { StyleSheet, Text, View } from 'react-native'
|
||||
|
||||
const MediaQueryWidget = ({ mediaQuery = {} }) => {
|
||||
const active = Object.keys(mediaQuery).reduce((active, alias) => {
|
||||
@@ -36,4 +19,21 @@ const MediaQueryWidget = ({ mediaQuery = {} }) => {
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
marginVertical: 10,
|
||||
padding: 10
|
||||
},
|
||||
heading: {
|
||||
fontWeight: 'bold',
|
||||
padding: 5,
|
||||
textAlign: 'center'
|
||||
},
|
||||
text: {
|
||||
textAlign: 'center'
|
||||
}
|
||||
})
|
||||
|
||||
export default MediaQueryWidget
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
<title>React Native for Web</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<div id="react-root"></div>
|
||||
<script src="/examples.js"></script>
|
||||
<script src="/bundle.js"></script>
|
||||
|
||||
@@ -1,23 +1,7 @@
|
||||
import { MediaProvider, matchMedia } from 'react-media-queries'
|
||||
import App from './components/App'
|
||||
import createGetter from 'react-media-queries/lib/createMediaQueryGetter'
|
||||
import createListener from 'react-media-queries/lib/createMediaQueryListener'
|
||||
import React, { AppRegistry } from '../src'
|
||||
import React, { AppRegistry } from 'react-native'
|
||||
import Game2048 from './2048/Game2048'
|
||||
import TicTacToeApp from './TicTacToe/TicTacToe'
|
||||
|
||||
const mediaQueries = {
|
||||
small: '(min-width: 300px)',
|
||||
medium: '(min-width: 400px)',
|
||||
large: '(min-width: 500px)'
|
||||
}
|
||||
const ResponsiveApp = matchMedia()(App)
|
||||
const WrappedApp = () => (
|
||||
<MediaProvider getMedia={createGetter(mediaQueries)} listener={createListener(mediaQueries)}>
|
||||
<ResponsiveApp />
|
||||
</MediaProvider>
|
||||
)
|
||||
|
||||
AppRegistry.registerComponent('Example', () => WrappedApp)
|
||||
|
||||
AppRegistry.runApplication('Example', {
|
||||
AppRegistry.runApplication('Game2048', {
|
||||
rootTag: document.getElementById('react-root')
|
||||
})
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
var constants = require('./constants')
|
||||
var webpack = require('webpack')
|
||||
const path = require('path')
|
||||
const webpack = require('webpack')
|
||||
|
||||
const EXAMPLES_DIRECTORY = __dirname
|
||||
|
||||
module.exports = {
|
||||
devServer: {
|
||||
contentBase: constants.EXAMPLES_DIRECTORY
|
||||
contentBase: EXAMPLES_DIRECTORY
|
||||
},
|
||||
entry: {
|
||||
example: constants.EXAMPLES_DIRECTORY
|
||||
example: EXAMPLES_DIRECTORY
|
||||
},
|
||||
module: {
|
||||
loaders: [
|
||||
@@ -19,7 +21,7 @@ module.exports = {
|
||||
]
|
||||
},
|
||||
output: {
|
||||
filename: 'examples.js'
|
||||
filename: 'bundle.js'
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
@@ -30,7 +32,7 @@ module.exports = {
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'react-native': '../../src'
|
||||
'react-native': path.join(__dirname, '../src')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
var constants = require('./constants')
|
||||
var webpack = require('webpack')
|
||||
|
||||
var testEntry = 'tests.webpack.js'
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: constants.ROOT_DIRECTORY,
|
||||
browsers: process.env.TRAVIS ? [ 'Firefox' ] : [ 'Chrome' ],
|
||||
browserNoActivityTimeout: 60000,
|
||||
client: {
|
||||
@@ -12,7 +12,7 @@ module.exports = function (config) {
|
||||
useIframe: true
|
||||
},
|
||||
files: [
|
||||
constants.TEST_ENTRY
|
||||
testEntry
|
||||
],
|
||||
frameworks: [ 'mocha' ],
|
||||
plugins: [
|
||||
@@ -24,7 +24,7 @@ module.exports = function (config) {
|
||||
'karma-webpack'
|
||||
],
|
||||
preprocessors: {
|
||||
[constants.TEST_ENTRY]: [ 'webpack', 'sourcemap' ]
|
||||
[testEntry]: [ 'webpack', 'sourcemap' ]
|
||||
},
|
||||
reporters: process.env.TRAVIS ? [ 'dots' ] : [ 'spec' ],
|
||||
singleRun: true,
|
||||
24
package.json
24
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-native-web",
|
||||
"version": "0.0.14",
|
||||
"version": "0.0.25",
|
||||
"description": "React Native for Web",
|
||||
"main": "dist/index.js",
|
||||
"files": [
|
||||
@@ -8,26 +8,26 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rm -rf ./dist && mkdir dist && babel src -d dist --ignore **/__tests__,src/modules/specHelpers",
|
||||
"build:umd": "webpack --config config/webpack.config.js --sort-assets-by --progress",
|
||||
"examples": "webpack-dev-server --config config/webpack.config.example.js --inline --hot --colors --quiet",
|
||||
"lint": "eslint config examples src",
|
||||
"build:umd": "webpack --config webpack.config.js --sort-assets-by --progress",
|
||||
"examples": "webpack-dev-server --config examples/webpack.config.js --inline --hot --colors --quiet",
|
||||
"lint": "eslint src",
|
||||
"prepublish": "npm run build && npm run build:umd",
|
||||
"test": "karma start config/karma.config.js",
|
||||
"test:watch": "npm run test:unit -- --no-single-run"
|
||||
"test": "karma start karma.config.js",
|
||||
"test:watch": "npm run test -- --no-single-run"
|
||||
},
|
||||
"dependencies": {
|
||||
"exenv": "^1.2.0",
|
||||
"inline-style-prefixer": "^0.5.3",
|
||||
"invariant": "^2.2.0",
|
||||
"lodash.debounce": "^3.1.1",
|
||||
"react-tappable": "^0.7.1",
|
||||
"react-textarea-autosize": "^3.1.0"
|
||||
"fbjs": "0.6.x || 0.7.x",
|
||||
"inline-style-prefix-all": "1.0.5",
|
||||
"lodash.debounce": "^4.0.3",
|
||||
"react-textarea-autosize": "^3.1.0",
|
||||
"react-timer-mixin": "^0.13.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.3.17",
|
||||
"babel-core": "^6.3.13",
|
||||
"babel-eslint": "^4.1.6",
|
||||
"babel-loader": "^6.2.0",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.4",
|
||||
"babel-preset-es2015": "^6.3.13",
|
||||
"babel-preset-react": "^6.3.13",
|
||||
"babel-preset-stage-1": "^6.3.13",
|
||||
|
||||
1654
src/apis/Animated/AnimatedImplementation.js
Normal file
1654
src/apis/Animated/AnimatedImplementation.js
Normal file
File diff suppressed because it is too large
Load Diff
288
src/apis/Animated/Interpolation.js
Normal file
288
src/apis/Animated/Interpolation.js
Normal file
@@ -0,0 +1,288 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* 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 Interpolation
|
||||
* @flow
|
||||
*/
|
||||
/* eslint no-bitwise: 0 */
|
||||
'use strict';
|
||||
|
||||
/* @edit start */
|
||||
var normalizeColor = require('../StyleSheet/normalizeColor');
|
||||
var invariant = require('fbjs/lib/invariant');
|
||||
/* @edit end */
|
||||
|
||||
type ExtrapolateType = 'extend' | 'identity' | 'clamp';
|
||||
|
||||
export type InterpolationConfigType = {
|
||||
inputRange: Array<number>;
|
||||
outputRange: (Array<number> | Array<string>);
|
||||
easing?: ((input: number) => number);
|
||||
extrapolate?: ExtrapolateType;
|
||||
extrapolateLeft?: ExtrapolateType;
|
||||
extrapolateRight?: ExtrapolateType;
|
||||
};
|
||||
|
||||
var linear = (t) => t;
|
||||
|
||||
/**
|
||||
* Very handy helper to map input ranges to output ranges with an easing
|
||||
* function and custom behavior outside of the ranges.
|
||||
*/
|
||||
class Interpolation {
|
||||
static create(config: InterpolationConfigType): (input: number) => number | string {
|
||||
|
||||
if (config.outputRange && typeof config.outputRange[0] === 'string') {
|
||||
return createInterpolationFromStringOutputRange(config);
|
||||
}
|
||||
|
||||
var outputRange: Array<number> = (config.outputRange: any);
|
||||
checkInfiniteRange('outputRange', outputRange);
|
||||
|
||||
var inputRange = config.inputRange;
|
||||
checkInfiniteRange('inputRange', inputRange);
|
||||
checkValidInputRange(inputRange);
|
||||
|
||||
invariant(
|
||||
inputRange.length === outputRange.length,
|
||||
'inputRange (' + inputRange.length + ') and outputRange (' +
|
||||
outputRange.length + ') must have the same length'
|
||||
);
|
||||
|
||||
var easing = config.easing || linear;
|
||||
|
||||
var extrapolateLeft: ExtrapolateType = 'extend';
|
||||
if (config.extrapolateLeft !== undefined) {
|
||||
extrapolateLeft = config.extrapolateLeft;
|
||||
} else if (config.extrapolate !== undefined) {
|
||||
extrapolateLeft = config.extrapolate;
|
||||
}
|
||||
|
||||
var extrapolateRight: ExtrapolateType = 'extend';
|
||||
if (config.extrapolateRight !== undefined) {
|
||||
extrapolateRight = config.extrapolateRight;
|
||||
} else if (config.extrapolate !== undefined) {
|
||||
extrapolateRight = config.extrapolate;
|
||||
}
|
||||
|
||||
return (input) => {
|
||||
invariant(
|
||||
typeof input === 'number',
|
||||
'Cannot interpolation an input which is not a number'
|
||||
);
|
||||
|
||||
var range = findRange(input, inputRange);
|
||||
return interpolate(
|
||||
input,
|
||||
inputRange[range],
|
||||
inputRange[range + 1],
|
||||
outputRange[range],
|
||||
outputRange[range + 1],
|
||||
easing,
|
||||
extrapolateLeft,
|
||||
extrapolateRight,
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function interpolate(
|
||||
input: number,
|
||||
inputMin: number,
|
||||
inputMax: number,
|
||||
outputMin: number,
|
||||
outputMax: number,
|
||||
easing: ((input: number) => number),
|
||||
extrapolateLeft: ExtrapolateType,
|
||||
extrapolateRight: ExtrapolateType,
|
||||
) {
|
||||
var result = input;
|
||||
|
||||
// Extrapolate
|
||||
if (result < inputMin) {
|
||||
if (extrapolateLeft === 'identity') {
|
||||
return result;
|
||||
} else if (extrapolateLeft === 'clamp') {
|
||||
result = inputMin;
|
||||
} else if (extrapolateLeft === 'extend') {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
if (result > inputMax) {
|
||||
if (extrapolateRight === 'identity') {
|
||||
return result;
|
||||
} else if (extrapolateRight === 'clamp') {
|
||||
result = inputMax;
|
||||
} else if (extrapolateRight === 'extend') {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
if (outputMin === outputMax) {
|
||||
return outputMin;
|
||||
}
|
||||
|
||||
if (inputMin === inputMax) {
|
||||
if (input <= inputMin) {
|
||||
return outputMin;
|
||||
}
|
||||
return outputMax;
|
||||
}
|
||||
|
||||
// Input Range
|
||||
if (inputMin === -Infinity) {
|
||||
result = -result;
|
||||
} else if (inputMax === Infinity) {
|
||||
result = result - inputMin;
|
||||
} else {
|
||||
result = (result - inputMin) / (inputMax - inputMin);
|
||||
}
|
||||
|
||||
// Easing
|
||||
result = easing(result);
|
||||
|
||||
// Output Range
|
||||
if (outputMin === -Infinity) {
|
||||
result = -result;
|
||||
} else if (outputMax === Infinity) {
|
||||
result = result + outputMin;
|
||||
} else {
|
||||
result = result * (outputMax - outputMin) + outputMin;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function colorToRgba(input: string): string {
|
||||
var int32Color = normalizeColor(input);
|
||||
if (int32Color === null) {
|
||||
return input;
|
||||
}
|
||||
|
||||
int32Color = int32Color || 0; // $FlowIssue
|
||||
|
||||
var r = (int32Color & 0xff000000) >>> 24;
|
||||
var g = (int32Color & 0x00ff0000) >>> 16;
|
||||
var b = (int32Color & 0x0000ff00) >>> 8;
|
||||
var a = (int32Color & 0x000000ff) / 255;
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
||||
}
|
||||
|
||||
var stringShapeRegex = /[0-9\.-]+/g;
|
||||
|
||||
/**
|
||||
* Supports string shapes by extracting numbers so new values can be computed,
|
||||
* and recombines those values into new strings of the same shape. Supports
|
||||
* things like:
|
||||
*
|
||||
* rgba(123, 42, 99, 0.36) // colors
|
||||
* -45deg // values with units
|
||||
*/
|
||||
function createInterpolationFromStringOutputRange(
|
||||
config: InterpolationConfigType,
|
||||
): (input: number) => string {
|
||||
var outputRange: Array<string> = (config.outputRange: any);
|
||||
invariant(outputRange.length >= 2, 'Bad output range');
|
||||
outputRange = outputRange.map(colorToRgba);
|
||||
checkPattern(outputRange);
|
||||
|
||||
// ['rgba(0, 100, 200, 0)', 'rgba(50, 150, 250, 0.5)']
|
||||
// ->
|
||||
// [
|
||||
// [0, 50],
|
||||
// [100, 150],
|
||||
// [200, 250],
|
||||
// [0, 0.5],
|
||||
// ]
|
||||
/* $FlowFixMe(>=0.18.0): `outputRange[0].match()` can return `null`. Need to
|
||||
* guard against this possibility.
|
||||
*/
|
||||
var outputRanges = outputRange[0].match(stringShapeRegex).map(() => []);
|
||||
outputRange.forEach(value => {
|
||||
/* $FlowFixMe(>=0.18.0): `value.match()` can return `null`. Need to guard
|
||||
* against this possibility.
|
||||
*/
|
||||
value.match(stringShapeRegex).forEach((number, i) => {
|
||||
outputRanges[i].push(+number);
|
||||
});
|
||||
});
|
||||
|
||||
/* $FlowFixMe(>=0.18.0): `outputRange[0].match()` can return `null`. Need to
|
||||
* guard against this possibility.
|
||||
*/
|
||||
var interpolations = outputRange[0].match(stringShapeRegex).map((value, i) => {
|
||||
return Interpolation.create({
|
||||
...config,
|
||||
outputRange: outputRanges[i],
|
||||
});
|
||||
});
|
||||
|
||||
return (input) => {
|
||||
var i = 0;
|
||||
// 'rgba(0, 100, 200, 0)'
|
||||
// ->
|
||||
// 'rgba(${interpolations[0](input)}, ${interpolations[1](input)}, ...'
|
||||
return outputRange[0].replace(stringShapeRegex, () => {
|
||||
return String(interpolations[i++](input));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function checkPattern(arr: Array<string>) {
|
||||
var pattern = arr[0].replace(stringShapeRegex, '');
|
||||
for (var i = 1; i < arr.length; ++i) {
|
||||
invariant(
|
||||
pattern === arr[i].replace(stringShapeRegex, ''),
|
||||
'invalid pattern ' + arr[0] + ' and ' + arr[i],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function findRange(input: number, inputRange: Array<number>) {
|
||||
for (var i = 1; i < inputRange.length - 1; ++i) {
|
||||
if (inputRange[i] >= input) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return i - 1;
|
||||
}
|
||||
|
||||
function checkValidInputRange(arr: Array<number>) {
|
||||
invariant(arr.length >= 2, 'inputRange must have at least 2 elements');
|
||||
for (var i = 1; i < arr.length; ++i) {
|
||||
invariant(
|
||||
arr[i] >= arr[i - 1],
|
||||
/* $FlowFixMe(>=0.13.0) - In the addition expression below this comment,
|
||||
* one or both of the operands may be something that doesn't cleanly
|
||||
* convert to a string, like undefined, null, and object, etc. If you really
|
||||
* mean this implicit string conversion, you can do something like
|
||||
* String(myThing)
|
||||
*/
|
||||
'inputRange must be monotonically increasing ' + arr
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function checkInfiniteRange(name: string, arr: Array<number>) {
|
||||
invariant(arr.length >= 2, name + ' must have at least 2 elements');
|
||||
invariant(
|
||||
arr.length !== 2 || arr[0] !== -Infinity || arr[1] !== Infinity,
|
||||
/* $FlowFixMe(>=0.13.0) - In the addition expression below this comment,
|
||||
* one or both of the operands may be something that doesn't cleanly convert
|
||||
* to a string, like undefined, null, and object, etc. If you really mean
|
||||
* this implicit string conversion, you can do something like
|
||||
* String(myThing)
|
||||
*/
|
||||
name + 'cannot be ]-infinity;+infinity[ ' + arr
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = Interpolation;
|
||||
103
src/apis/Animated/SpringConfig.js
Normal file
103
src/apis/Animated/SpringConfig.js
Normal file
@@ -0,0 +1,103 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* 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 SpringConfig
|
||||
* @flow
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
type SpringConfigType = {
|
||||
tension: number,
|
||||
friction: number,
|
||||
};
|
||||
|
||||
function tensionFromOrigamiValue(oValue) {
|
||||
return (oValue - 30) * 3.62 + 194;
|
||||
}
|
||||
|
||||
function frictionFromOrigamiValue(oValue) {
|
||||
return (oValue - 8) * 3 + 25;
|
||||
}
|
||||
|
||||
function fromOrigamiTensionAndFriction(
|
||||
tension: number,
|
||||
friction: number,
|
||||
): SpringConfigType {
|
||||
return {
|
||||
tension: tensionFromOrigamiValue(tension),
|
||||
friction: frictionFromOrigamiValue(friction)
|
||||
};
|
||||
}
|
||||
|
||||
function fromBouncinessAndSpeed(
|
||||
bounciness: number,
|
||||
speed: number,
|
||||
): SpringConfigType {
|
||||
function normalize(value, startValue, endValue) {
|
||||
return (value - startValue) / (endValue - startValue);
|
||||
}
|
||||
|
||||
function projectNormal(n, start, end) {
|
||||
return start + (n * (end - start));
|
||||
}
|
||||
|
||||
function linearInterpolation(t, start, end) {
|
||||
return t * end + (1 - t) * start;
|
||||
}
|
||||
|
||||
function quadraticOutInterpolation(t, start, end) {
|
||||
return linearInterpolation(2 * t - t * t, start, end);
|
||||
}
|
||||
|
||||
function b3Friction1(x) {
|
||||
return (0.0007 * Math.pow(x, 3)) -
|
||||
(0.031 * Math.pow(x, 2)) + 0.64 * x + 1.28;
|
||||
}
|
||||
|
||||
function b3Friction2(x) {
|
||||
return (0.000044 * Math.pow(x, 3)) -
|
||||
(0.006 * Math.pow(x, 2)) + 0.36 * x + 2;
|
||||
}
|
||||
|
||||
function b3Friction3(x) {
|
||||
return (0.00000045 * Math.pow(x, 3)) -
|
||||
(0.000332 * Math.pow(x, 2)) + 0.1078 * x + 5.84;
|
||||
}
|
||||
|
||||
function b3Nobounce(tension) {
|
||||
if (tension <= 18) {
|
||||
return b3Friction1(tension);
|
||||
} else if (tension > 18 && tension <= 44) {
|
||||
return b3Friction2(tension);
|
||||
} else {
|
||||
return b3Friction3(tension);
|
||||
}
|
||||
}
|
||||
|
||||
var b = normalize(bounciness / 1.7, 0, 20);
|
||||
b = projectNormal(b, 0, 0.8);
|
||||
var s = normalize(speed / 1.7, 0, 20);
|
||||
var bouncyTension = projectNormal(s, 0.5, 200);
|
||||
var bouncyFriction = quadraticOutInterpolation(
|
||||
b,
|
||||
b3Nobounce(bouncyTension),
|
||||
0.01
|
||||
);
|
||||
|
||||
return {
|
||||
tension: tensionFromOrigamiValue(bouncyTension),
|
||||
friction: frictionFromOrigamiValue(bouncyFriction)
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
fromOrigamiTensionAndFriction,
|
||||
fromBouncinessAndSpeed,
|
||||
};
|
||||
19
src/apis/Animated/index.js
Normal file
19
src/apis/Animated/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) 2016-present, Nicolas Gallagher.
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import AnimatedImplementation from './AnimatedImplementation'
|
||||
import Image from '../../components/Image'
|
||||
import Text from '../../components/Text'
|
||||
import View from '../../components/View'
|
||||
|
||||
module.exports = {
|
||||
...AnimatedImplementation,
|
||||
View: AnimatedImplementation.createAnimatedComponent(View),
|
||||
Text: AnimatedImplementation.createAnimatedComponent(Text),
|
||||
Image: AnimatedImplementation.createAnimatedComponent(Image)
|
||||
}
|
||||
15
src/apis/Animated/polyfills/Set.js
Normal file
15
src/apis/Animated/polyfills/Set.js
Normal file
@@ -0,0 +1,15 @@
|
||||
function SetPolyfill() {
|
||||
this._cache = []
|
||||
}
|
||||
|
||||
SetPolyfill.prototype.add = function (e) {
|
||||
if (this._cache.indexOf(e) === -1) {
|
||||
this._cache.push(e)
|
||||
}
|
||||
}
|
||||
|
||||
SetPolyfill.prototype.forEach = function (cb) {
|
||||
this._cache.forEach(cb)
|
||||
}
|
||||
|
||||
export default SetPolyfill
|
||||
@@ -1,10 +1,10 @@
|
||||
import Portal from '../../components/Portal'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
import StyleSheet from '../StyleSheet'
|
||||
import View from '../../components/View'
|
||||
|
||||
export default class ReactNativeApp extends Component {
|
||||
class ReactNativeApp extends Component {
|
||||
static propTypes = {
|
||||
initialProps: PropTypes.object,
|
||||
rootComponent: PropTypes.any.isRequired,
|
||||
@@ -45,3 +45,5 @@ const styles = StyleSheet.create({
|
||||
bottom: 0
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = ReactNativeApp
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import assert from 'assert'
|
||||
import React from 'react'
|
||||
import { elementId } from '../../StyleSheet'
|
||||
import { prerenderApplication } from '../renderApplication'
|
||||
|
||||
const component = () => <div />
|
||||
|
||||
suite('apis/AppRegistry/renderApplication', () => {
|
||||
test('prerenderApplication', () => {
|
||||
const { html, style, styleElement } = prerenderApplication(component, {})
|
||||
|
||||
assert.ok(html.indexOf('<div ') > -1)
|
||||
assert.ok(typeof style === 'string')
|
||||
assert.equal(styleElement.type, 'style')
|
||||
assert.equal(styleElement.props.id, elementId)
|
||||
assert.equal(styleElement.props.dangerouslySetInnerHTML.__html, style)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import { Component } from 'react'
|
||||
import invariant from 'invariant'
|
||||
import invariant from 'fbjs/lib/invariant'
|
||||
import ReactDOM from 'react-dom'
|
||||
import renderApplication, { prerenderApplication } from './renderApplication'
|
||||
|
||||
@@ -24,7 +24,7 @@ type AppConfig = {
|
||||
/**
|
||||
* `AppRegistry` is the JS entry point to running all React Native apps.
|
||||
*/
|
||||
export default class AppRegistry {
|
||||
class AppRegistry {
|
||||
static getAppKeys(): Array<string> {
|
||||
return Object.keys(runnables)
|
||||
}
|
||||
@@ -65,7 +65,7 @@ export default class AppRegistry {
|
||||
}
|
||||
|
||||
static runApplication(appKey: string, appParameters?: Object): void {
|
||||
const isDevelopment = process.env.NODE_ENV === 'development'
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production'
|
||||
const params = { ...appParameters }
|
||||
params.rootTag = `#${params.rootTag.id}`
|
||||
|
||||
@@ -88,3 +88,5 @@ export default class AppRegistry {
|
||||
ReactDOM.unmountComponentAtNode(rootTag)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AppRegistry
|
||||
|
||||
@@ -6,21 +6,23 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import invariant from 'invariant'
|
||||
import invariant from 'fbjs/lib/invariant'
|
||||
import React, { Component } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ReactDOMServer from 'react-dom/server'
|
||||
import ReactNativeApp from './ReactNativeApp'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
|
||||
const renderStyleSheetToString = () => `<style id="${StyleSheet.elementId}">${StyleSheet._renderToString()}</style>`
|
||||
const renderStyleSheetToString = () => StyleSheet.renderToString()
|
||||
const styleAsElement = (style) => <style dangerouslySetInnerHTML={{ __html: style }} id={StyleSheet.elementId} />
|
||||
const styleAsTagString = (style) => `<style id="${StyleSheet.elementId}">${style}</style>`
|
||||
|
||||
export default function renderApplication(RootComponent: Component, initialProps: Object, rootTag: any) {
|
||||
invariant(rootTag, 'Expect to have a valid rootTag, instead got ', rootTag)
|
||||
|
||||
// insert style sheet if needed
|
||||
const styleElement = document.getElementById(StyleSheet.elementId)
|
||||
if (!styleElement) { rootTag.insertAdjacentHTML('beforebegin', renderStyleSheetToString()) }
|
||||
if (!styleElement) { rootTag.insertAdjacentHTML('beforebegin', styleAsTagString(renderStyleSheetToString())) }
|
||||
|
||||
const component = (
|
||||
<ReactNativeApp
|
||||
@@ -41,5 +43,6 @@ export function prerenderApplication(RootComponent: Component, initialProps: Obj
|
||||
)
|
||||
const html = ReactDOMServer.renderToString(component)
|
||||
const style = renderStyleSheetToString()
|
||||
return { html, style }
|
||||
const styleElement = styleAsElement(style)
|
||||
return { html, style, styleElement }
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import invariant from 'invariant'
|
||||
import invariant from 'fbjs/lib/invariant'
|
||||
|
||||
const listeners = {}
|
||||
const eventTypes = [ 'change' ]
|
||||
|
||||
export default class AppState {
|
||||
class AppState {
|
||||
static get currentState() {
|
||||
switch (document.visibilityState) {
|
||||
case 'hidden':
|
||||
@@ -27,3 +27,5 @@ export default class AppState {
|
||||
delete listeners[handler]
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AppState
|
||||
|
||||
@@ -12,7 +12,7 @@ const mergeLocalStorageItem = (key, value) => {
|
||||
window.localStorage.setItem(key, nextValue)
|
||||
}
|
||||
|
||||
export default class AsyncStorage {
|
||||
class AsyncStorage {
|
||||
/**
|
||||
* Erases *all* AsyncStorage for the domain.
|
||||
*/
|
||||
@@ -157,3 +157,5 @@ export default class AsyncStorage {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AsyncStorage
|
||||
|
||||
@@ -6,26 +6,38 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import invariant from 'invariant'
|
||||
import debounce from 'lodash.debounce'
|
||||
import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'
|
||||
import invariant from 'fbjs/lib/invariant'
|
||||
|
||||
const dimensions = {
|
||||
screen: {
|
||||
fontScale: 1,
|
||||
get height() { return window.screen.height },
|
||||
scale: window.devicePixelRatio || 1,
|
||||
get width() { return window.screen.width }
|
||||
},
|
||||
window: {
|
||||
fontScale: 1,
|
||||
get height() { return document.documentElement.clientHeight },
|
||||
scale: window.devicePixelRatio || 1,
|
||||
get width() { return document.documentElement.clientWidth }
|
||||
}
|
||||
}
|
||||
const win = ExecutionEnvironment.canUseDOM ? window : { screen: {} }
|
||||
|
||||
export default class Dimensions {
|
||||
const dimensions = {}
|
||||
|
||||
class Dimensions {
|
||||
static get(dimension: string): Object {
|
||||
invariant(dimensions[dimension], 'No dimension set for key ' + dimension)
|
||||
return dimensions[dimension]
|
||||
}
|
||||
|
||||
static set(): void {
|
||||
dimensions.window = {
|
||||
fontScale: 1,
|
||||
height: win.innerHeight,
|
||||
scale: win.devicePixelRatio || 1,
|
||||
width: win.innerWidth
|
||||
}
|
||||
|
||||
dimensions.screen = {
|
||||
fontScale: 1,
|
||||
height: win.screen.height,
|
||||
scale: win.devicePixelRatio || 1,
|
||||
width: win.screen.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Dimensions.set()
|
||||
ExecutionEnvironment.canUseDOM && window.addEventListener('resize', debounce(Dimensions.set, 50))
|
||||
|
||||
module.exports = Dimensions
|
||||
|
||||
83
src/apis/Easing/bezier.js
Normal file
83
src/apis/Easing/bezier.js
Normal file
@@ -0,0 +1,83 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* https://github.com/arian/cubic-bezier
|
||||
*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2013 Arian Stolwijk
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be
|
||||
* included in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*
|
||||
* @providesModule bezier
|
||||
* @nolint
|
||||
*/
|
||||
|
||||
module.exports = function(x1, y1, x2, y2, epsilon){
|
||||
|
||||
var curveX = function(t){
|
||||
var v = 1 - t;
|
||||
return 3 * v * v * t * x1 + 3 * v * t * t * x2 + t * t * t;
|
||||
};
|
||||
|
||||
var curveY = function(t){
|
||||
var v = 1 - t;
|
||||
return 3 * v * v * t * y1 + 3 * v * t * t * y2 + t * t * t;
|
||||
};
|
||||
|
||||
var derivativeCurveX = function(t){
|
||||
var v = 1 - t;
|
||||
return 3 * (2 * (t - 1) * t + v * v) * x1 + 3 * (-t * t * t + 2 * v * t) * x2;
|
||||
};
|
||||
|
||||
return function(t){
|
||||
|
||||
var x = t, t0, t1, t2, x2, d2, i;
|
||||
|
||||
// First try a few iterations of Newton's method -- normally very fast.
|
||||
for (t2 = x, i = 0; i < 8; i++){
|
||||
x2 = curveX(t2) - x;
|
||||
if (Math.abs(x2) < epsilon) { return curveY(t2); }
|
||||
d2 = derivativeCurveX(t2);
|
||||
if (Math.abs(d2) < 1e-6) { break; }
|
||||
t2 = t2 - x2 / d2;
|
||||
}
|
||||
|
||||
t0 = 0;
|
||||
t1 = 1;
|
||||
t2 = x;
|
||||
|
||||
if (t2 < t0) { return curveY(t0); }
|
||||
if (t2 > t1) { return curveY(t1); }
|
||||
|
||||
// Fallback to the bisection method for reliability.
|
||||
while (t0 < t1){
|
||||
x2 = curveX(t2);
|
||||
if (Math.abs(x2 - x) < epsilon) { return curveY(t2); }
|
||||
if (x > x2) { t0 = t2; }
|
||||
else { t1 = t2; }
|
||||
t2 = (t1 - t0) * 0.5 + t0;
|
||||
}
|
||||
|
||||
// Failure
|
||||
return curveY(t2);
|
||||
|
||||
};
|
||||
|
||||
};
|
||||
155
src/apis/Easing/index.js
Normal file
155
src/apis/Easing/index.js
Normal file
@@ -0,0 +1,155 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* 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 Easing
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var _bezier = require('./bezier');
|
||||
|
||||
/**
|
||||
* This class implements common easing functions. The math is pretty obscure,
|
||||
* but this cool website has nice visual illustrations of what they represent:
|
||||
* http://xaedes.de/dev/transitions/
|
||||
*/
|
||||
class Easing {
|
||||
static step0(n) {
|
||||
return n > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
static step1(n) {
|
||||
return n >= 1 ? 1 : 0;
|
||||
}
|
||||
|
||||
static linear(t) {
|
||||
return t;
|
||||
}
|
||||
|
||||
static ease(t: number): number {
|
||||
return ease(t);
|
||||
}
|
||||
|
||||
static quad(t) {
|
||||
return t * t;
|
||||
}
|
||||
|
||||
static cubic(t) {
|
||||
return t * t * t;
|
||||
}
|
||||
|
||||
static poly(n) {
|
||||
return (t) => Math.pow(t, n);
|
||||
}
|
||||
|
||||
static sin(t) {
|
||||
return 1 - Math.cos(t * Math.PI / 2);
|
||||
}
|
||||
|
||||
static circle(t) {
|
||||
return 1 - Math.sqrt(1 - t * t);
|
||||
}
|
||||
|
||||
static exp(t) {
|
||||
return Math.pow(2, 10 * (t - 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple elastic interaction, similar to a spring. Default bounciness
|
||||
* is 1, which overshoots a little bit once. 0 bounciness doesn't overshoot
|
||||
* at all, and bounciness of N > 1 will overshoot about N times.
|
||||
*
|
||||
* Wolfram Plots:
|
||||
*
|
||||
* http://tiny.cc/elastic_b_1 (default bounciness = 1)
|
||||
* http://tiny.cc/elastic_b_3 (bounciness = 3)
|
||||
*/
|
||||
static elastic(bounciness: number = 1): (t: number) => number {
|
||||
var p = bounciness * Math.PI;
|
||||
return (t) => 1 - Math.pow(Math.cos(t * Math.PI / 2), 3) * Math.cos(t * p);
|
||||
}
|
||||
|
||||
static back(s: number): (t: number) => number {
|
||||
if (s === undefined) {
|
||||
s = 1.70158;
|
||||
}
|
||||
return (t) => t * t * ((s + 1) * t - s);
|
||||
}
|
||||
|
||||
static bounce(t: number): number {
|
||||
if (t < 1 / 2.75) {
|
||||
return 7.5625 * t * t;
|
||||
}
|
||||
|
||||
if (t < 2 / 2.75) {
|
||||
t -= 1.5 / 2.75;
|
||||
return 7.5625 * t * t + 0.75;
|
||||
}
|
||||
|
||||
if (t < 2.5 / 2.75) {
|
||||
t -= 2.25 / 2.75;
|
||||
return 7.5625 * t * t + 0.9375;
|
||||
}
|
||||
|
||||
t -= 2.625 / 2.75;
|
||||
return 7.5625 * t * t + 0.984375;
|
||||
}
|
||||
|
||||
static bezier(
|
||||
x1: number,
|
||||
y1: number,
|
||||
x2: number,
|
||||
y2: number,
|
||||
epsilon?: ?number,
|
||||
): (t: number) => number {
|
||||
if (epsilon === undefined) {
|
||||
// epsilon determines the precision of the solved values
|
||||
// a good approximation is:
|
||||
var duration = 500; // duration of animation in milliseconds.
|
||||
epsilon = (1000 / 60 / duration) / 4;
|
||||
}
|
||||
|
||||
return _bezier(x1, y1, x2, y2, epsilon);
|
||||
}
|
||||
|
||||
static in(
|
||||
easing: (t: number) => number,
|
||||
): (t: number) => number {
|
||||
return easing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs an easing function backwards.
|
||||
*/
|
||||
static out(
|
||||
easing: (t: number) => number,
|
||||
): (t: number) => number {
|
||||
return (t) => 1 - easing(1 - t);
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes any easing function symmetrical.
|
||||
*/
|
||||
static inOut(
|
||||
easing: (t: number) => number,
|
||||
): (t: number) => number {
|
||||
return (t) => {
|
||||
if (t < 0.5) {
|
||||
return easing(t * 2) / 2;
|
||||
}
|
||||
return 1 - easing((1 - t) * 2) / 2;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
var ease = Easing.bezier(0.42, 0, 1, 1);
|
||||
|
||||
|
||||
|
||||
module.exports = Easing;
|
||||
48
src/apis/InteractionManager/index.js
Normal file
48
src/apis/InteractionManager/index.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import keyMirror from 'fbjs/lib/keyMirror'
|
||||
import invariant from 'fbjs/lib/invariant'
|
||||
|
||||
const InteractionManager = {
|
||||
Events: keyMirror({
|
||||
interactionStart: true,
|
||||
interactionComplete: true
|
||||
}),
|
||||
|
||||
/**
|
||||
* Schedule a function to run after all interactions have completed.
|
||||
*/
|
||||
runAfterInteractions(callback: Function) {
|
||||
invariant(
|
||||
typeof callback === 'function',
|
||||
'Must specify a function to schedule.'
|
||||
)
|
||||
callback()
|
||||
},
|
||||
|
||||
/**
|
||||
* Notify manager that an interaction has started.
|
||||
*/
|
||||
createInteractionHandle() {
|
||||
return 1
|
||||
},
|
||||
|
||||
/**
|
||||
* Notify manager that an interaction has completed.
|
||||
*/
|
||||
clearInteractionHandle(handle) {
|
||||
invariant(
|
||||
!!handle,
|
||||
'Must provide a handle to clear.'
|
||||
)
|
||||
},
|
||||
|
||||
addListener: () => {}
|
||||
}
|
||||
|
||||
module.exports = InteractionManager
|
||||
@@ -6,9 +6,15 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import invariant from 'invariant'
|
||||
import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'
|
||||
import invariant from 'fbjs/lib/invariant'
|
||||
|
||||
const connection = ExecutionEnvironment.canUseDOM && (
|
||||
window.navigator.connection ||
|
||||
window.navigator.mozConnection ||
|
||||
window.navigator.webkitConnection
|
||||
)
|
||||
|
||||
const connection = window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection
|
||||
const eventTypes = [ 'change' ]
|
||||
|
||||
/**
|
||||
@@ -50,8 +56,8 @@ const NetInfo = {
|
||||
isConnected: {
|
||||
addEventListener(type: string, handler: Function): { remove: () => void } {
|
||||
invariant(eventTypes.indexOf(type) !== -1, 'Trying to subscribe to unknown event: "%s"', type)
|
||||
window.addEventListener('online', handler.bind(true), false)
|
||||
window.addEventListener('offline', handler.bind(false), false)
|
||||
window.addEventListener('online', handler.bind(null, true), false)
|
||||
window.addEventListener('offline', handler.bind(null, false), false)
|
||||
|
||||
return {
|
||||
remove: () => NetInfo.isConnected.removeEventListener(type, handler)
|
||||
@@ -60,8 +66,8 @@ const NetInfo = {
|
||||
|
||||
removeEventListener(type: string, handler: Function): void {
|
||||
invariant(eventTypes.indexOf(type) !== -1, 'Trying to subscribe to unknown event: "%s"', type)
|
||||
window.removeEventListener('online', handler.bind(true), false)
|
||||
window.removeEventListener('offline', handler.bind(false), false)
|
||||
window.removeEventListener('online', handler.bind(null, true), false)
|
||||
window.removeEventListener('offline', handler.bind(null, false), false)
|
||||
},
|
||||
|
||||
fetch(): Promise {
|
||||
@@ -75,3 +81,5 @@ const NetInfo = {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = NetInfo
|
||||
|
||||
124
src/apis/PanResponder/TouchHistoryMath.js
Normal file
124
src/apis/PanResponder/TouchHistoryMath.js
Normal file
@@ -0,0 +1,124 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var TouchHistoryMath = {
|
||||
/**
|
||||
* This code is optimized and not intended to look beautiful. This allows
|
||||
* computing of touch centroids that have moved after `touchesChangedAfter`
|
||||
* timeStamp. You can compute the current centroid involving all touches
|
||||
* moves after `touchesChangedAfter`, or you can compute the previous
|
||||
* centroid of all touches that were moved after `touchesChangedAfter`.
|
||||
*
|
||||
* @param {TouchHistoryMath} touchHistory Standard Responder touch track
|
||||
* data.
|
||||
* @param {number} touchesChangedAfter timeStamp after which moved touches
|
||||
* are considered "actively moving" - not just "active".
|
||||
* @param {boolean} isXAxis Consider `x` dimension vs. `y` dimension.
|
||||
* @param {boolean} ofCurrent Compute current centroid for actively moving
|
||||
* touches vs. previous centroid of now actively moving touches.
|
||||
* @return {number} value of centroid in specified dimension.
|
||||
*/
|
||||
centroidDimension: function(touchHistory, touchesChangedAfter, isXAxis, ofCurrent) {
|
||||
var touchBank = touchHistory.touchBank;
|
||||
var total = 0;
|
||||
var count = 0;
|
||||
|
||||
var oneTouchData = touchHistory.numberActiveTouches === 1 ?
|
||||
touchHistory.touchBank[touchHistory.indexOfSingleActiveTouch] : null;
|
||||
|
||||
if (oneTouchData !== null) {
|
||||
if (oneTouchData.touchActive && oneTouchData.currentTimeStamp > touchesChangedAfter) {
|
||||
total += ofCurrent && isXAxis ? oneTouchData.currentPageX :
|
||||
ofCurrent && !isXAxis ? oneTouchData.currentPageY :
|
||||
!ofCurrent && isXAxis ? oneTouchData.previousPageX :
|
||||
oneTouchData.previousPageY;
|
||||
count = 1;
|
||||
}
|
||||
} else {
|
||||
for (var i = 0; i < touchBank.length; i++) {
|
||||
var touchTrack = touchBank[i];
|
||||
if (touchTrack !== null &&
|
||||
touchTrack !== undefined &&
|
||||
touchTrack.touchActive &&
|
||||
touchTrack.currentTimeStamp >= touchesChangedAfter) {
|
||||
var toAdd; // Yuck, program temporarily in invalid state.
|
||||
if (ofCurrent && isXAxis) {
|
||||
toAdd = touchTrack.currentPageX;
|
||||
} else if (ofCurrent && !isXAxis) {
|
||||
toAdd = touchTrack.currentPageY;
|
||||
} else if (!ofCurrent && isXAxis) {
|
||||
toAdd = touchTrack.previousPageX;
|
||||
} else {
|
||||
toAdd = touchTrack.previousPageY;
|
||||
}
|
||||
total += toAdd;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
return count > 0 ? total / count : TouchHistoryMath.noCentroid;
|
||||
},
|
||||
|
||||
currentCentroidXOfTouchesChangedAfter: function(touchHistory, touchesChangedAfter) {
|
||||
return TouchHistoryMath.centroidDimension(
|
||||
touchHistory,
|
||||
touchesChangedAfter,
|
||||
true, // isXAxis
|
||||
true // ofCurrent
|
||||
);
|
||||
},
|
||||
|
||||
currentCentroidYOfTouchesChangedAfter: function(touchHistory, touchesChangedAfter) {
|
||||
return TouchHistoryMath.centroidDimension(
|
||||
touchHistory,
|
||||
touchesChangedAfter,
|
||||
false, // isXAxis
|
||||
true // ofCurrent
|
||||
);
|
||||
},
|
||||
|
||||
previousCentroidXOfTouchesChangedAfter: function(touchHistory, touchesChangedAfter) {
|
||||
return TouchHistoryMath.centroidDimension(
|
||||
touchHistory,
|
||||
touchesChangedAfter,
|
||||
true, // isXAxis
|
||||
false // ofCurrent
|
||||
);
|
||||
},
|
||||
|
||||
previousCentroidYOfTouchesChangedAfter: function(touchHistory, touchesChangedAfter) {
|
||||
return TouchHistoryMath.centroidDimension(
|
||||
touchHistory,
|
||||
touchesChangedAfter,
|
||||
false, // isXAxis
|
||||
false // ofCurrent
|
||||
);
|
||||
},
|
||||
|
||||
currentCentroidX: function(touchHistory) {
|
||||
return TouchHistoryMath.centroidDimension(
|
||||
touchHistory,
|
||||
0, // touchesChangedAfter
|
||||
true, // isXAxis
|
||||
true // ofCurrent
|
||||
);
|
||||
},
|
||||
|
||||
currentCentroidY: function(touchHistory) {
|
||||
return TouchHistoryMath.centroidDimension(
|
||||
touchHistory,
|
||||
0, // touchesChangedAfter
|
||||
false, // isXAxis
|
||||
true // ofCurrent
|
||||
);
|
||||
},
|
||||
|
||||
noCentroid: -1,
|
||||
};
|
||||
|
||||
module.exports = TouchHistoryMath;
|
||||
392
src/apis/PanResponder/index.js
Normal file
392
src/apis/PanResponder/index.js
Normal file
@@ -0,0 +1,392 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
import normalizeNativeEvent from './normalizeNativeEvent';
|
||||
var TouchHistoryMath = require('./TouchHistoryMath');
|
||||
|
||||
var currentCentroidXOfTouchesChangedAfter =
|
||||
TouchHistoryMath.currentCentroidXOfTouchesChangedAfter;
|
||||
var currentCentroidYOfTouchesChangedAfter =
|
||||
TouchHistoryMath.currentCentroidYOfTouchesChangedAfter;
|
||||
var previousCentroidXOfTouchesChangedAfter =
|
||||
TouchHistoryMath.previousCentroidXOfTouchesChangedAfter;
|
||||
var previousCentroidYOfTouchesChangedAfter =
|
||||
TouchHistoryMath.previousCentroidYOfTouchesChangedAfter;
|
||||
var currentCentroidX = TouchHistoryMath.currentCentroidX;
|
||||
var currentCentroidY = TouchHistoryMath.currentCentroidY;
|
||||
|
||||
/**
|
||||
* `PanResponder` reconciles several touches into a single gesture. It makes
|
||||
* single-touch gestures resilient to extra touches, and can be used to
|
||||
* recognize simple multi-touch gestures.
|
||||
*
|
||||
* It provides a predictable wrapper of the responder handlers provided by the
|
||||
* [gesture responder system](docs/gesture-responder-system.html).
|
||||
* For each handler, it provides a new `gestureState` object alongside the
|
||||
* native event object:
|
||||
*
|
||||
* ```
|
||||
* onPanResponderMove: (event, gestureState) => {}
|
||||
* ```
|
||||
*
|
||||
* A native event is a synthetic touch event with the following form:
|
||||
*
|
||||
* - `nativeEvent`
|
||||
* + `changedTouches` - Array of all touch events that have changed since the last event
|
||||
* + `identifier` - The ID of the touch
|
||||
* + `locationX` - The X position of the touch, relative to the element
|
||||
* + `locationY` - The Y position of the touch, relative to the element
|
||||
* + `pageX` - The X position of the touch, relative to the root element
|
||||
* + `pageY` - The Y position of the touch, relative to the root element
|
||||
* + `target` - The node id of the element receiving the touch event
|
||||
* + `timestamp` - A time identifier for the touch, useful for velocity calculation
|
||||
* + `touches` - Array of all current touches on the screen
|
||||
*
|
||||
* A `gestureState` object has the following:
|
||||
*
|
||||
* - `stateID` - ID of the gestureState- persisted as long as there at least
|
||||
* one touch on screen
|
||||
* - `moveX` - the latest screen coordinates of the recently-moved touch
|
||||
* - `moveY` - the latest screen coordinates of the recently-moved touch
|
||||
* - `x0` - the screen coordinates of the responder grant
|
||||
* - `y0` - the screen coordinates of the responder grant
|
||||
* - `dx` - accumulated distance of the gesture since the touch started
|
||||
* - `dy` - accumulated distance of the gesture since the touch started
|
||||
* - `vx` - current velocity of the gesture
|
||||
* - `vy` - current velocity of the gesture
|
||||
* - `numberActiveTouches` - Number of touches currently on screen
|
||||
*
|
||||
* ### Basic Usage
|
||||
*
|
||||
* ```
|
||||
* componentWillMount: function() {
|
||||
* this._panResponder = PanResponder.create({
|
||||
* // Ask to be the responder:
|
||||
* onStartShouldSetPanResponder: (evt, gestureState) => true,
|
||||
* onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
|
||||
* onMoveShouldSetPanResponder: (evt, gestureState) => true,
|
||||
* onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
|
||||
*
|
||||
* onPanResponderGrant: (evt, gestureState) => {
|
||||
* // The guesture has started. Show visual feedback so the user knows
|
||||
* // what is happening!
|
||||
*
|
||||
* // gestureState.{x,y}0 will be set to zero now
|
||||
* },
|
||||
* onPanResponderMove: (evt, gestureState) => {
|
||||
* // The most recent move distance is gestureState.move{X,Y}
|
||||
*
|
||||
* // The accumulated gesture distance since becoming responder is
|
||||
* // gestureState.d{x,y}
|
||||
* },
|
||||
* onPanResponderTerminationRequest: (evt, gestureState) => true,
|
||||
* onPanResponderRelease: (evt, gestureState) => {
|
||||
* // The user has released all touches while this view is the
|
||||
* // responder. This typically means a gesture has succeeded
|
||||
* },
|
||||
* onPanResponderTerminate: (evt, gestureState) => {
|
||||
* // Another component has become the responder, so this gesture
|
||||
* // should be cancelled
|
||||
* },
|
||||
* onShouldBlockNativeResponder: (evt, gestureState) => {
|
||||
* // Returns whether this component should block native components from becoming the JS
|
||||
* // responder. Returns true by default. Is currently only supported on android.
|
||||
* return true;
|
||||
* },
|
||||
* });
|
||||
* },
|
||||
*
|
||||
* render: function() {
|
||||
* return (
|
||||
* <View {...this._panResponder.panHandlers} />
|
||||
* );
|
||||
* },
|
||||
*
|
||||
* ```
|
||||
*
|
||||
* ### Working Example
|
||||
*
|
||||
* To see it in action, try the
|
||||
* [PanResponder example in UIExplorer](https://github.com/facebook/react-native/blob/master/Examples/UIExplorer/PanResponderExample.js)
|
||||
*/
|
||||
|
||||
var PanResponder = {
|
||||
|
||||
/**
|
||||
*
|
||||
* A graphical explanation of the touch data flow:
|
||||
*
|
||||
* +----------------------------+ +--------------------------------+
|
||||
* | ResponderTouchHistoryStore | |TouchHistoryMath |
|
||||
* +----------------------------+ +----------+---------------------+
|
||||
* |Global store of touchHistory| |Allocation-less math util |
|
||||
* |including activeness, start | |on touch history (centroids |
|
||||
* |position, prev/cur position.| |and multitouch movement etc) |
|
||||
* | | | |
|
||||
* +----^-----------------------+ +----^---------------------------+
|
||||
* | |
|
||||
* | (records relevant history |
|
||||
* | of touches relevant for |
|
||||
* | implementing higher level |
|
||||
* | gestures) |
|
||||
* | |
|
||||
* +----+-----------------------+ +----|---------------------------+
|
||||
* | ResponderEventPlugin | | | Your App/Component |
|
||||
* +----------------------------+ +----|---------------------------+
|
||||
* |Negotiates which view gets | Low level | | High level |
|
||||
* |onResponderMove events. | events w/ | +-+-------+ events w/ |
|
||||
* |Also records history into | touchHistory| | Pan | multitouch + |
|
||||
* |ResponderTouchHistoryStore. +---------------->Responder+-----> accumulative|
|
||||
* +----------------------------+ attached to | | | distance and |
|
||||
* each event | +---------+ velocity. |
|
||||
* | |
|
||||
* | |
|
||||
* +--------------------------------+
|
||||
*
|
||||
*
|
||||
*
|
||||
* Gesture that calculates cumulative movement over time in a way that just
|
||||
* "does the right thing" for multiple touches. The "right thing" is very
|
||||
* nuanced. When moving two touches in opposite directions, the cumulative
|
||||
* distance is zero in each dimension. When two touches move in parallel five
|
||||
* pixels in the same direction, the cumulative distance is five, not ten. If
|
||||
* two touches start, one moves five in a direction, then stops and the other
|
||||
* touch moves fives in the same direction, the cumulative distance is ten.
|
||||
*
|
||||
* This logic requires a kind of processing of time "clusters" of touch events
|
||||
* so that two touch moves that essentially occur in parallel but move every
|
||||
* other frame respectively, are considered part of the same movement.
|
||||
*
|
||||
* Explanation of some of the non-obvious fields:
|
||||
*
|
||||
* - moveX/moveY: If no move event has been observed, then `(moveX, moveY)` is
|
||||
* invalid. If a move event has been observed, `(moveX, moveY)` is the
|
||||
* centroid of the most recently moved "cluster" of active touches.
|
||||
* (Currently all move have the same timeStamp, but later we should add some
|
||||
* threshold for what is considered to be "moving"). If a palm is
|
||||
* accidentally counted as a touch, but a finger is moving greatly, the palm
|
||||
* will move slightly, but we only want to count the single moving touch.
|
||||
* - x0/y0: Centroid location (non-cumulative) at the time of becoming
|
||||
* responder.
|
||||
* - dx/dy: Cumulative touch distance - not the same thing as sum of each touch
|
||||
* distance. Accounts for touch moves that are clustered together in time,
|
||||
* moving the same direction. Only valid when currently responder (otherwise,
|
||||
* it only represents the drag distance below the threshold).
|
||||
* - vx/vy: Velocity.
|
||||
*/
|
||||
|
||||
_initializeGestureState: function(gestureState) {
|
||||
gestureState.moveX = 0;
|
||||
gestureState.moveY = 0;
|
||||
gestureState.x0 = 0;
|
||||
gestureState.y0 = 0;
|
||||
gestureState.dx = 0;
|
||||
gestureState.dy = 0;
|
||||
gestureState.vx = 0;
|
||||
gestureState.vy = 0;
|
||||
gestureState.numberActiveTouches = 0;
|
||||
// All `gestureState` accounts for timeStamps up until:
|
||||
gestureState._accountsForMovesUpTo = 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* This is nuanced and is necessary. It is incorrect to continuously take all
|
||||
* active *and* recently moved touches, find the centroid, and track how that
|
||||
* result changes over time. Instead, we must take all recently moved
|
||||
* touches, and calculate how the centroid has changed just for those
|
||||
* recently moved touches, and append that change to an accumulator. This is
|
||||
* to (at least) handle the case where the user is moving three fingers, and
|
||||
* then one of the fingers stops but the other two continue.
|
||||
*
|
||||
* This is very different than taking all of the recently moved touches and
|
||||
* storing their centroid as `dx/dy`. For correctness, we must *accumulate
|
||||
* changes* in the centroid of recently moved touches.
|
||||
*
|
||||
* There is also some nuance with how we handle multiple moved touches in a
|
||||
* single event. With the way `ReactNativeEventEmitter` dispatches touches as
|
||||
* individual events, multiple touches generate two 'move' events, each of
|
||||
* them triggering `onResponderMove`. But with the way `PanResponder` works,
|
||||
* all of the gesture inference is performed on the first dispatch, since it
|
||||
* looks at all of the touches (even the ones for which there hasn't been a
|
||||
* native dispatch yet). Therefore, `PanResponder` does not call
|
||||
* `onResponderMove` passed the first dispatch. This diverges from the
|
||||
* typical responder callback pattern (without using `PanResponder`), but
|
||||
* avoids more dispatches than necessary.
|
||||
*/
|
||||
_updateGestureStateOnMove: function(gestureState, touchHistory) {
|
||||
gestureState.numberActiveTouches = touchHistory.numberActiveTouches;
|
||||
gestureState.moveX = currentCentroidXOfTouchesChangedAfter(
|
||||
touchHistory,
|
||||
gestureState._accountsForMovesUpTo
|
||||
);
|
||||
gestureState.moveY = currentCentroidYOfTouchesChangedAfter(
|
||||
touchHistory,
|
||||
gestureState._accountsForMovesUpTo
|
||||
);
|
||||
var movedAfter = gestureState._accountsForMovesUpTo;
|
||||
var prevX = previousCentroidXOfTouchesChangedAfter(touchHistory, movedAfter);
|
||||
var x = currentCentroidXOfTouchesChangedAfter(touchHistory, movedAfter);
|
||||
var prevY = previousCentroidYOfTouchesChangedAfter(touchHistory, movedAfter);
|
||||
var y = currentCentroidYOfTouchesChangedAfter(touchHistory, movedAfter);
|
||||
var nextDX = gestureState.dx + (x - prevX);
|
||||
var nextDY = gestureState.dy + (y - prevY);
|
||||
|
||||
// TODO: This must be filtered intelligently.
|
||||
var dt =
|
||||
(touchHistory.mostRecentTimeStamp - gestureState._accountsForMovesUpTo);
|
||||
gestureState.vx = (nextDX - gestureState.dx) / dt;
|
||||
gestureState.vy = (nextDY - gestureState.dy) / dt;
|
||||
|
||||
gestureState.dx = nextDX;
|
||||
gestureState.dy = nextDY;
|
||||
gestureState._accountsForMovesUpTo = touchHistory.mostRecentTimeStamp;
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {object} config Enhanced versions of all of the responder callbacks
|
||||
* that provide not only the typical `ResponderSyntheticEvent`, but also the
|
||||
* `PanResponder` gesture state. Simply replace the word `Responder` with
|
||||
* `PanResponder` in each of the typical `onResponder*` callbacks. For
|
||||
* example, the `config` object would look like:
|
||||
*
|
||||
* - `onMoveShouldSetPanResponder: (e, gestureState) => {...}`
|
||||
* - `onMoveShouldSetPanResponderCapture: (e, gestureState) => {...}`
|
||||
* - `onStartShouldSetPanResponder: (e, gestureState) => {...}`
|
||||
* - `onStartShouldSetPanResponderCapture: (e, gestureState) => {...}`
|
||||
* - `onPanResponderReject: (e, gestureState) => {...}`
|
||||
* - `onPanResponderGrant: (e, gestureState) => {...}`
|
||||
* - `onPanResponderStart: (e, gestureState) => {...}`
|
||||
* - `onPanResponderEnd: (e, gestureState) => {...}`
|
||||
* - `onPanResponderRelease: (e, gestureState) => {...}`
|
||||
* - `onPanResponderMove: (e, gestureState) => {...}`
|
||||
* - `onPanResponderTerminate: (e, gestureState) => {...}`
|
||||
* - `onPanResponderTerminationRequest: (e, gestureState) => {...}`
|
||||
* - `onShouldBlockNativeResponder: (e, gestureState) => {...}`
|
||||
*
|
||||
* In general, for events that have capture equivalents, we update the
|
||||
* gestureState once in the capture phase and can use it in the bubble phase
|
||||
* as well.
|
||||
*
|
||||
* Be careful with onStartShould* callbacks. They only reflect updated
|
||||
* `gestureState` for start/end events that bubble/capture to the Node.
|
||||
* Once the node is the responder, you can rely on every start/end event
|
||||
* being processed by the gesture and `gestureState` being updated
|
||||
* accordingly. (numberActiveTouches) may not be totally accurate unless you
|
||||
* are the responder.
|
||||
*/
|
||||
create: function(config) {
|
||||
var gestureState = {
|
||||
// Useful for debugging
|
||||
stateID: Math.random(),
|
||||
};
|
||||
PanResponder._initializeGestureState(gestureState);
|
||||
var panHandlers = {
|
||||
onStartShouldSetResponder: function(e) {
|
||||
return config.onStartShouldSetPanResponder === undefined ? false :
|
||||
config.onStartShouldSetPanResponder(normalizeEvent(e), gestureState);
|
||||
},
|
||||
onMoveShouldSetResponder: function(e) {
|
||||
return config.onMoveShouldSetPanResponder === undefined ? false :
|
||||
config.onMoveShouldSetPanResponder(normalizeEvent(e), gestureState);
|
||||
},
|
||||
onStartShouldSetResponderCapture: function(e) {
|
||||
// TODO: Actually, we should reinitialize the state any time
|
||||
// touches.length increases from 0 active to > 0 active.
|
||||
if (e.nativeEvent.touches) {
|
||||
if (e.nativeEvent.touches.length === 1) {
|
||||
PanResponder._initializeGestureState(gestureState);
|
||||
}
|
||||
}
|
||||
else if (e.nativeEvent.type === 'mousedown') {
|
||||
PanResponder._initializeGestureState(gestureState);
|
||||
}
|
||||
gestureState.numberActiveTouches = e.touchHistory.numberActiveTouches;
|
||||
return config.onStartShouldSetPanResponderCapture !== undefined ?
|
||||
config.onStartShouldSetPanResponderCapture(normalizeEvent(e), gestureState) : false;
|
||||
},
|
||||
|
||||
onMoveShouldSetResponderCapture: function(e) {
|
||||
var touchHistory = e.touchHistory;
|
||||
// Responder system incorrectly dispatches should* to current responder
|
||||
// Filter out any touch moves past the first one - we would have
|
||||
// already processed multi-touch geometry during the first event.
|
||||
if (gestureState._accountsForMovesUpTo === touchHistory.mostRecentTimeStamp) {
|
||||
return false;
|
||||
}
|
||||
PanResponder._updateGestureStateOnMove(gestureState, touchHistory);
|
||||
return config.onMoveShouldSetPanResponderCapture ?
|
||||
config.onMoveShouldSetPanResponderCapture(normalizeEvent(e), gestureState) : false;
|
||||
},
|
||||
|
||||
onResponderGrant: function(e) {
|
||||
gestureState.x0 = currentCentroidX(e.touchHistory);
|
||||
gestureState.y0 = currentCentroidY(e.touchHistory);
|
||||
gestureState.dx = 0;
|
||||
gestureState.dy = 0;
|
||||
config.onPanResponderGrant && config.onPanResponderGrant(normalizeEvent(e), gestureState);
|
||||
// TODO: t7467124 investigate if this can be removed
|
||||
return config.onShouldBlockNativeResponder === undefined ? true :
|
||||
config.onShouldBlockNativeResponder();
|
||||
},
|
||||
|
||||
onResponderReject: function(e) {
|
||||
config.onPanResponderReject && config.onPanResponderReject(normalizeEvent(e), gestureState);
|
||||
},
|
||||
|
||||
onResponderRelease: function(e) {
|
||||
config.onPanResponderRelease && config.onPanResponderRelease(normalizeEvent(e), gestureState);
|
||||
PanResponder._initializeGestureState(gestureState);
|
||||
},
|
||||
|
||||
onResponderStart: function(e) {
|
||||
var touchHistory = e.touchHistory;
|
||||
gestureState.numberActiveTouches = touchHistory.numberActiveTouches;
|
||||
config.onPanResponderStart && config.onPanResponderStart(normalizeEvent(e), gestureState);
|
||||
},
|
||||
|
||||
onResponderMove: function(e) {
|
||||
var touchHistory = e.touchHistory;
|
||||
// Guard against the dispatch of two touch moves when there are two
|
||||
// simultaneously changed touches.
|
||||
if (gestureState._accountsForMovesUpTo === touchHistory.mostRecentTimeStamp) {
|
||||
return;
|
||||
}
|
||||
// Filter out any touch moves past the first one - we would have
|
||||
// already processed multi-touch geometry during the first event.
|
||||
PanResponder._updateGestureStateOnMove(gestureState, touchHistory);
|
||||
config.onPanResponderMove && config.onPanResponderMove(normalizeEvent(e), gestureState);
|
||||
},
|
||||
|
||||
onResponderEnd: function(e) {
|
||||
var touchHistory = e.touchHistory;
|
||||
gestureState.numberActiveTouches = touchHistory.numberActiveTouches;
|
||||
config.onPanResponderEnd && config.onPanResponderEnd(normalizeEvent(e), gestureState);
|
||||
},
|
||||
|
||||
onResponderTerminate: function(e) {
|
||||
config.onPanResponderTerminate &&
|
||||
config.onPanResponderTerminate(e, gestureState);
|
||||
PanResponder._initializeGestureState(gestureState);
|
||||
},
|
||||
|
||||
onResponderTerminationRequest: function(e) {
|
||||
return config.onPanResponderTerminationRequest === undefined ? true :
|
||||
config.onPanResponderTerminationRequest(normalizeEvent(e), gestureState);
|
||||
},
|
||||
};
|
||||
return {panHandlers: panHandlers};
|
||||
},
|
||||
};
|
||||
|
||||
function normalizeEvent(e) {
|
||||
const normalizedEvent = Object.create(e);
|
||||
normalizedEvent.nativeEvent = normalizeNativeEvent(e.nativeEvent, e.type);
|
||||
return normalizedEvent;
|
||||
}
|
||||
|
||||
module.exports = PanResponder;
|
||||
59
src/apis/PanResponder/injectResponderEventPlugin.js
Normal file
59
src/apis/PanResponder/injectResponderEventPlugin.js
Normal file
@@ -0,0 +1,59 @@
|
||||
// based on https://github.com/facebook/react/pull/4303/files
|
||||
|
||||
import EventConstants from 'react/lib/EventConstants'
|
||||
import EventPluginRegistry from 'react/lib/EventPluginRegistry'
|
||||
import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment'
|
||||
import ResponderEventPlugin from 'react/lib/ResponderEventPlugin'
|
||||
import ResponderTouchHistoryStore from 'react/lib/ResponderTouchHistoryStore'
|
||||
import normalizeNativeEvent from './normalizeNativeEvent'
|
||||
|
||||
const {
|
||||
topMouseDown,
|
||||
topMouseMove,
|
||||
topMouseUp,
|
||||
topScroll,
|
||||
topSelectionChange,
|
||||
topTouchCancel,
|
||||
topTouchEnd,
|
||||
topTouchMove,
|
||||
topTouchStart
|
||||
} = EventConstants.topLevelTypes
|
||||
|
||||
const supportsTouch = ExecutionEnvironment.canUseDOM && (
|
||||
'ontouchstart' in window ||
|
||||
window.DocumentTouch && document instanceof window.DocumentTouch
|
||||
)
|
||||
|
||||
const endDependencies = supportsTouch ? [ topTouchCancel, topTouchEnd ] : [ topMouseUp ]
|
||||
const moveDependencies = supportsTouch ? [ topTouchMove ] : [ topMouseMove ]
|
||||
const startDependencies = supportsTouch ? [ topTouchStart ] : [ topMouseDown ]
|
||||
|
||||
/**
|
||||
* Setup ResponderEventPlugin dependencies
|
||||
*/
|
||||
ResponderEventPlugin.eventTypes.responderMove.dependencies = moveDependencies
|
||||
ResponderEventPlugin.eventTypes.responderEnd.dependencies = endDependencies
|
||||
ResponderEventPlugin.eventTypes.responderStart.dependencies = startDependencies
|
||||
ResponderEventPlugin.eventTypes.responderRelease.dependencies = endDependencies
|
||||
ResponderEventPlugin.eventTypes.responderTerminationRequest.dependencies = []
|
||||
ResponderEventPlugin.eventTypes.responderGrant.dependencies = []
|
||||
ResponderEventPlugin.eventTypes.responderReject.dependencies = []
|
||||
ResponderEventPlugin.eventTypes.responderTerminate.dependencies = []
|
||||
ResponderEventPlugin.eventTypes.moveShouldSetResponder.dependencies = moveDependencies
|
||||
ResponderEventPlugin.eventTypes.selectionChangeShouldSetResponder.dependencies = [ topSelectionChange ]
|
||||
ResponderEventPlugin.eventTypes.scrollShouldSetResponder.dependencies = [ topScroll ]
|
||||
ResponderEventPlugin.eventTypes.startShouldSetResponder.dependencies = startDependencies
|
||||
|
||||
const originalRecordTouchTrack = ResponderTouchHistoryStore.recordTouchTrack
|
||||
|
||||
ResponderTouchHistoryStore.recordTouchTrack = (topLevelType, nativeEvent) => {
|
||||
// Filter out mouse-move events when the mouse button is not down
|
||||
if ((topLevelType === topMouseMove) && !ResponderTouchHistoryStore.touchHistory.touchBank.length) {
|
||||
return
|
||||
}
|
||||
originalRecordTouchTrack.call(ResponderTouchHistoryStore, topLevelType, normalizeNativeEvent(nativeEvent))
|
||||
}
|
||||
|
||||
EventPluginRegistry.injectEventPluginsByName({
|
||||
ResponderEventPlugin
|
||||
})
|
||||
91
src/apis/PanResponder/normalizeNativeEvent.js
Normal file
91
src/apis/PanResponder/normalizeNativeEvent.js
Normal file
@@ -0,0 +1,91 @@
|
||||
// Mobile Safari re-uses touch objects, so we copy the properties we want and normalize the identifier
|
||||
const normalizeTouches = (touches = []) => Array.prototype.slice.call(touches).map((touch) => {
|
||||
const identifier = touch.identifier > 20 ? (touch.identifier % 20) : touch.identifier
|
||||
|
||||
const rect = touch.target && touch.target.getBoundingClientRect()
|
||||
const locationX = touch.pageX - rect.left
|
||||
const locationY = touch.pageY - rect.top
|
||||
|
||||
return {
|
||||
clientX: touch.clientX,
|
||||
clientY: touch.clientY,
|
||||
force: touch.force,
|
||||
locationX: locationX,
|
||||
locationY: locationY,
|
||||
identifier: identifier,
|
||||
pageX: touch.pageX,
|
||||
pageY: touch.pageY,
|
||||
radiusX: touch.radiusX,
|
||||
radiusY: touch.radiusY,
|
||||
rotationAngle: touch.rotationAngle,
|
||||
screenX: touch.screenX,
|
||||
screenY: touch.screenY,
|
||||
target: touch.target,
|
||||
// normalize the timestamp
|
||||
// https://stackoverflow.com/questions/26177087/ios-8-mobile-safari-wrong-timestamp-on-touch-events
|
||||
timestamp: Date.now()
|
||||
}
|
||||
})
|
||||
|
||||
function normalizeTouchEvent(nativeEvent) {
|
||||
const changedTouches = normalizeTouches(nativeEvent.changedTouches)
|
||||
const touches = normalizeTouches(nativeEvent.touches)
|
||||
|
||||
const event = {
|
||||
changedTouches,
|
||||
domEvent: nativeEvent,
|
||||
pageX: nativeEvent.pageX,
|
||||
pageY: nativeEvent.pageY,
|
||||
target: nativeEvent.target,
|
||||
// normalize the timestamp
|
||||
// https://stackoverflow.com/questions/26177087/ios-8-mobile-safari-wrong-timestamp-on-touch-events
|
||||
timestamp: Date.now(),
|
||||
touches
|
||||
}
|
||||
|
||||
if (changedTouches[0]) {
|
||||
event.identifier = changedTouches[0].identifier
|
||||
event.pageX = changedTouches[0].pageX
|
||||
event.pageY = changedTouches[0].pageY
|
||||
event.locationX = changedTouches[0].locationX
|
||||
event.locationY = changedTouches[0].locationY
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
function normalizeMouseEvent(nativeEvent) {
|
||||
const touches = [{
|
||||
clientX: nativeEvent.clientX,
|
||||
clientY: nativeEvent.clientY,
|
||||
force: nativeEvent.force,
|
||||
locationX: nativeEvent.clientX,
|
||||
locationY: nativeEvent.clientY,
|
||||
identifier: 0,
|
||||
pageX: nativeEvent.pageX,
|
||||
pageY: nativeEvent.pageY,
|
||||
screenX: nativeEvent.screenX,
|
||||
screenY: nativeEvent.screenY,
|
||||
target: nativeEvent.target,
|
||||
timestamp: nativeEvent.timestamp || Date.now()
|
||||
}]
|
||||
return {
|
||||
changedTouches: touches,
|
||||
domEvent: nativeEvent,
|
||||
identifier: touches[0].identifier,
|
||||
locationX: nativeEvent.offsetX,
|
||||
locationY: nativeEvent.offsetY,
|
||||
pageX: nativeEvent.pageX,
|
||||
pageY: nativeEvent.pageY,
|
||||
target: nativeEvent.target,
|
||||
timestamp: touches[0].timestamp,
|
||||
touches: (nativeEvent.type === 'mouseup') ? [] : touches
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeNativeEvent(nativeEvent) {
|
||||
const mouse = nativeEvent.type.indexOf('mouse') >= 0
|
||||
return mouse ? normalizeMouseEvent(nativeEvent) : normalizeTouchEvent(nativeEvent)
|
||||
}
|
||||
|
||||
module.exports = normalizeNativeEvent
|
||||
@@ -11,7 +11,7 @@ import Dimensions from '../Dimensions'
|
||||
/**
|
||||
* PixelRatio gives access to the device pixel density.
|
||||
*/
|
||||
export default class PixelRatio {
|
||||
class PixelRatio {
|
||||
/**
|
||||
* Returns the device pixel density.
|
||||
*/
|
||||
@@ -45,3 +45,5 @@ export default class PixelRatio {
|
||||
return Math.round(layoutSize * ratio) / ratio
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PixelRatio
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { canUseDOM } from 'exenv'
|
||||
import { canUseDOM } from 'fbjs/lib/ExecutionEnvironment'
|
||||
|
||||
const Platform = {
|
||||
OS: 'web',
|
||||
userAgent: canUseDOM ? window.navigator.userAgent : ''
|
||||
}
|
||||
|
||||
export default Platform
|
||||
module.exports = Platform
|
||||
|
||||
@@ -22,4 +22,4 @@ const BorderPropTypes = {
|
||||
borderLeftStyle: BorderStylePropType
|
||||
}
|
||||
|
||||
export default BorderPropTypes
|
||||
module.exports = BorderPropTypes
|
||||
|
||||
@@ -1,5 +1,62 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* 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 ColorPropType
|
||||
*/
|
||||
|
||||
import { PropTypes } from 'react'
|
||||
import ReactPropTypeLocationNames from 'react/lib/ReactPropTypeLocationNames'
|
||||
|
||||
const ColorPropType = PropTypes.string
|
||||
var normalizeColor = require('./normalizeColor');
|
||||
|
||||
export default ColorPropType
|
||||
var colorPropType = function(isRequired, props, propName, componentName, location, propFullName) {
|
||||
var color = props[propName];
|
||||
if (color === undefined || color === null) {
|
||||
if (isRequired) {
|
||||
var locationName = ReactPropTypeLocationNames[location];
|
||||
return new Error(
|
||||
'Required ' + locationName + ' `' + (propFullName || propName) +
|
||||
'` was not specified in `' + componentName + '`.'
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof color === 'number') {
|
||||
// Developers should not use a number, but we are using the prop type
|
||||
// both for user provided colors and for transformed ones. This isn't ideal
|
||||
// and should be fixed but will do for now...
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalizeColor(color) === null) {
|
||||
var locationName = ReactPropTypeLocationNames[location];
|
||||
return new Error(
|
||||
'Invalid ' + locationName + ' `' + (propFullName || propName) +
|
||||
'` supplied to `' + componentName + '`: ' + color + '\n' +
|
||||
`Valid color formats are
|
||||
- '#f0f' (#rgb)
|
||||
- '#f0fc' (#rgba)
|
||||
- '#ff00ff' (#rrggbb)
|
||||
- '#ff00ff00' (#rrggbbaa)
|
||||
- 'rgb(255, 255, 255)'
|
||||
- 'rgba(255, 255, 255, 1.0)'
|
||||
- 'hsl(360, 100%, 100%)'
|
||||
- 'hsla(360, 100%, 100%, 1.0)'
|
||||
- 'transparent'
|
||||
- 'red'
|
||||
- 0xff00ff00 (0xrrggbbaa)
|
||||
`);
|
||||
}
|
||||
};
|
||||
|
||||
var ColorPropType = colorPropType.bind(null, false /* isRequired */);
|
||||
ColorPropType.isRequired = colorPropType.bind(null, true /* isRequired */);
|
||||
|
||||
module.exports = ColorPropType
|
||||
|
||||
26
src/apis/StyleSheet/EdgeInsetsPropType.js
Normal file
26
src/apis/StyleSheet/EdgeInsetsPropType.js
Normal file
@@ -0,0 +1,26 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* 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 EdgeInsetsPropType
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var PropTypes = require('react').PropTypes;
|
||||
|
||||
var createStrictShapeTypeChecker = require('./createStrictShapeTypeChecker');
|
||||
|
||||
var EdgeInsetsPropType = createStrictShapeTypeChecker({
|
||||
top: PropTypes.number,
|
||||
left: PropTypes.number,
|
||||
bottom: PropTypes.number,
|
||||
right: PropTypes.number,
|
||||
});
|
||||
|
||||
module.exports = EdgeInsetsPropType;
|
||||
@@ -51,4 +51,4 @@ const LayoutPropTypes = {
|
||||
top: numberOrString
|
||||
}
|
||||
|
||||
export default LayoutPropTypes
|
||||
module.exports = LayoutPropTypes
|
||||
|
||||
24
src/apis/StyleSheet/PointPropType.js
Normal file
24
src/apis/StyleSheet/PointPropType.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* 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 PointPropType
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var PropTypes = require('react').PropTypes;
|
||||
|
||||
var createStrictShapeTypeChecker = require('./createStrictShapeTypeChecker');
|
||||
|
||||
var PointPropType = createStrictShapeTypeChecker({
|
||||
x: PropTypes.number,
|
||||
y: PropTypes.number,
|
||||
});
|
||||
|
||||
module.exports = PointPropType;
|
||||
@@ -1,96 +0,0 @@
|
||||
import hyphenate from './hyphenate'
|
||||
import prefixer from './prefixer'
|
||||
|
||||
export default class Store {
|
||||
constructor(
|
||||
initialState:Object = {},
|
||||
options:Object = { obfuscateClassNames: false }
|
||||
) {
|
||||
this._counter = 0
|
||||
this._classNames = { ...initialState.classNames }
|
||||
this._declarations = { ...initialState.declarations }
|
||||
this._options = options
|
||||
}
|
||||
|
||||
get(property, value) {
|
||||
const key = this._getDeclarationKey(property, value)
|
||||
return this._classNames[key]
|
||||
}
|
||||
|
||||
set(property, value) {
|
||||
if (value != null) {
|
||||
const values = this._getPropertyValues(property) || []
|
||||
if (values.indexOf(value) === -1) {
|
||||
values.push(value)
|
||||
this._setClassName(property, value)
|
||||
this._setPropertyValues(property, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
const obfuscate = this._options.obfuscateClassNames
|
||||
|
||||
// sort the properties to ensure shorthands are first in the cascade
|
||||
const properties = Object.keys(this._declarations).sort()
|
||||
|
||||
// transform the class name to a valid CSS selector
|
||||
const getCssSelector = (property, value) => {
|
||||
let className = this.get(property, value)
|
||||
if (!obfuscate && className) {
|
||||
className = className.replace(/[(),":?.%\\$#]/g, '\\$&')
|
||||
}
|
||||
return className
|
||||
}
|
||||
|
||||
// transform the declarations into CSS rules with vendor-prefixes
|
||||
const buildCSSRules = (property, values) => {
|
||||
return values.reduce((cssRules, value) => {
|
||||
const declarations = prefixer.prefix({ [property]: value })
|
||||
const cssDeclarations = Object.keys(declarations).reduce((str, prop) => {
|
||||
str += `${hyphenate(prop)}:${value};`
|
||||
return str
|
||||
}, '')
|
||||
const selector = getCssSelector(property, value)
|
||||
|
||||
cssRules += `\n.${selector}{${cssDeclarations}}`
|
||||
|
||||
return cssRules
|
||||
}, '')
|
||||
}
|
||||
|
||||
const css = properties.reduce((css, property) => {
|
||||
const values = this._declarations[property]
|
||||
css += buildCSSRules(property, values)
|
||||
return css
|
||||
}, '')
|
||||
|
||||
return (`/* ${this._counter} unique declarations */${css}`)
|
||||
}
|
||||
|
||||
_getDeclarationKey(property, value) {
|
||||
return `${property}:${value}`
|
||||
}
|
||||
|
||||
_getPropertyValues(property) {
|
||||
return this._declarations[property]
|
||||
}
|
||||
|
||||
_setPropertyValues(property, values) {
|
||||
this._declarations[property] = values.map(value => value)
|
||||
}
|
||||
|
||||
_setClassName(property, value) {
|
||||
const key = this._getDeclarationKey(property, value)
|
||||
const exists = !!this._classNames[key]
|
||||
if (!exists) {
|
||||
this._counter += 1
|
||||
if (this._options.obfuscateClassNames) {
|
||||
this._classNames[key] = `_s_${this._counter}`
|
||||
} else {
|
||||
const val = `${value}`.replace(/\s/g, '-')
|
||||
this._classNames[key] = `${property}:${val}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@
|
||||
import createStrictShapeTypeChecker from './createStrictShapeTypeChecker'
|
||||
import flattenStyle from './flattenStyle'
|
||||
|
||||
export default function StyleSheetPropType(shape) {
|
||||
module.exports = function StyleSheetPropType(shape) {
|
||||
const shapePropType = createStrictShapeTypeChecker(shape)
|
||||
return function (props, propName, componentName, location?) {
|
||||
let newProps = props
|
||||
|
||||
@@ -6,41 +6,96 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import prefixAll from 'inline-style-prefix-all'
|
||||
import hyphenate from './hyphenate'
|
||||
import expandStyle from './expandStyle'
|
||||
import flattenStyle from './flattenStyle'
|
||||
import prefixer from './prefixer'
|
||||
import processTransform from './processTransform'
|
||||
import { predefinedClassNames } from './predefs'
|
||||
|
||||
export default class StyleSheetRegistry {
|
||||
static registerStyle(style: Object, store): number {
|
||||
let stylesCache = {}
|
||||
let uniqueID = 0
|
||||
|
||||
const getCacheKey = (prop, value) => `${prop}:${value}`
|
||||
|
||||
const normalizeStyle = (style) => {
|
||||
return processTransform(expandStyle(flattenStyle(style)))
|
||||
}
|
||||
|
||||
const createCssDeclarations = (style) => {
|
||||
return Object.keys(style).map((prop) => {
|
||||
const property = hyphenate(prop)
|
||||
const value = style[prop]
|
||||
return `${property}:${value};`
|
||||
}).sort().join('')
|
||||
}
|
||||
|
||||
class StyleSheetRegistry {
|
||||
/* for testing */
|
||||
static _reset() {
|
||||
stylesCache = {}
|
||||
uniqueID = 0
|
||||
}
|
||||
|
||||
static renderToString() {
|
||||
let str = `/* ${uniqueID} unique declarations */`
|
||||
|
||||
return Object.keys(stylesCache).reduce((str, key) => {
|
||||
const id = stylesCache[key].id
|
||||
const style = stylesCache[key].style
|
||||
const declarations = createCssDeclarations(style)
|
||||
const rule = `\n.${id}{${declarations}}`
|
||||
str += rule
|
||||
return str
|
||||
}, str)
|
||||
}
|
||||
|
||||
static registerStyle(style: Object): number {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
Object.freeze(style)
|
||||
}
|
||||
|
||||
const normalizedStyle = flattenStyle(style)
|
||||
const normalizedStyle = normalizeStyle(style)
|
||||
|
||||
Object.keys(normalizedStyle).forEach((prop) => {
|
||||
// add each declaration to the store
|
||||
store.set(prop, normalizedStyle[prop])
|
||||
const value = normalizedStyle[prop]
|
||||
const cacheKey = getCacheKey(prop, value)
|
||||
const exists = stylesCache[cacheKey] && stylesCache[cacheKey].id
|
||||
if (!exists) {
|
||||
const id = ++uniqueID
|
||||
// add new declaration to the store
|
||||
stylesCache[cacheKey] = {
|
||||
id: `__style${id}`,
|
||||
style: prefixAll({ [prop]: value })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return style
|
||||
}
|
||||
|
||||
static getStyleAsNativeProps(style, store) {
|
||||
let _className
|
||||
let _style = {}
|
||||
static getStyleAsNativeProps(styleSheetObject, canUseCSS = false) {
|
||||
const classList = []
|
||||
const normalizedStyle = flattenStyle(style)
|
||||
const normalizedStyle = normalizeStyle(styleSheetObject)
|
||||
let style = {}
|
||||
|
||||
for (const prop in normalizedStyle) {
|
||||
let styleClass = store.get(prop, normalizedStyle[prop])
|
||||
const value = normalizedStyle[prop]
|
||||
const cacheKey = getCacheKey(prop, value)
|
||||
let selector = stylesCache[cacheKey] && stylesCache[cacheKey].id || predefinedClassNames[cacheKey]
|
||||
|
||||
if (styleClass) {
|
||||
classList.push(styleClass)
|
||||
if (selector && canUseCSS) {
|
||||
classList.push(selector)
|
||||
} else {
|
||||
_style[prop] = normalizedStyle[prop]
|
||||
style[prop] = normalizedStyle[prop]
|
||||
}
|
||||
}
|
||||
|
||||
_className = classList.join(' ')
|
||||
_style = prefixer.prefix(_style)
|
||||
|
||||
return { className: _className, style: _style }
|
||||
return {
|
||||
className: classList.join(' '),
|
||||
style: prefixAll(style)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StyleSheetRegistry
|
||||
|
||||
@@ -10,9 +10,9 @@ import { PropTypes } from 'react'
|
||||
import ImageStylePropTypes from '../../components/Image/ImageStylePropTypes'
|
||||
import TextStylePropTypes from '../../components/Text/TextStylePropTypes'
|
||||
import ViewStylePropTypes from '../../components/View/ViewStylePropTypes'
|
||||
import invariant from 'invariant'
|
||||
import invariant from 'fbjs/lib/invariant'
|
||||
|
||||
export default class StyleSheetValidation {
|
||||
class StyleSheetValidation {
|
||||
static validateStyleProp(prop, style, caller) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (allStylePropTypes[prop] === undefined) {
|
||||
@@ -63,6 +63,7 @@ StyleSheetValidation.addValidStylePropTypes({
|
||||
direction: PropTypes.string, /* @private */
|
||||
float: PropTypes.oneOf([ 'left', 'none', 'right' ]),
|
||||
font: PropTypes.string, /* @private */
|
||||
listStyle: PropTypes.string,
|
||||
verticalAlign: PropTypes.string
|
||||
listStyle: PropTypes.string
|
||||
})
|
||||
|
||||
module.exports = StyleSheetValidation
|
||||
|
||||
@@ -38,10 +38,12 @@ const TransformPropTypes = {
|
||||
PropTypes.shape({ skewX: numberOrString }),
|
||||
PropTypes.shape({ skewY: numberOrString }),
|
||||
PropTypes.shape({ translateX: numberOrString }),
|
||||
PropTypes.shape({ translateY: numberOrString })
|
||||
PropTypes.shape({ translateY: numberOrString }),
|
||||
PropTypes.shape({ translateZ: numberOrString }),
|
||||
PropTypes.shape({ translate3d: PropTypes.string })
|
||||
])
|
||||
),
|
||||
transformMatrix: TransformMatrixPropType
|
||||
}
|
||||
|
||||
export default TransformPropTypes
|
||||
module.exports = TransformPropTypes
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import assert from 'assert'
|
||||
import Store from '../Store'
|
||||
|
||||
suite('apis/StyleSheet/Store', () => {
|
||||
suite('the constructor', () => {
|
||||
test('initialState', () => {
|
||||
const initialState = { classNames: { 'alignItems:center': '__classname__' } }
|
||||
const store = new Store(initialState)
|
||||
assert.deepEqual(store._classNames['alignItems:center'], '__classname__')
|
||||
})
|
||||
})
|
||||
|
||||
suite('#get', () => {
|
||||
test('returns a declaration-specific className', () => {
|
||||
const initialState = {
|
||||
classNames: {
|
||||
'alignItems:center': '__expected__',
|
||||
'alignItems:flex-start': '__error__'
|
||||
}
|
||||
}
|
||||
const store = new Store(initialState)
|
||||
assert.deepEqual(store.get('alignItems', 'center'), '__expected__')
|
||||
})
|
||||
})
|
||||
|
||||
suite('#set', () => {
|
||||
test('stores declarations', () => {
|
||||
const store = new Store()
|
||||
store.set('alignItems', 'center')
|
||||
store.set('flexGrow', 0)
|
||||
store.set('flexGrow', 1)
|
||||
store.set('flexGrow', 2)
|
||||
assert.deepEqual(store._declarations, {
|
||||
alignItems: [ 'center' ],
|
||||
flexGrow: [ 0, 1, 2 ]
|
||||
})
|
||||
})
|
||||
|
||||
test('human-readable classNames', () => {
|
||||
const store = new Store()
|
||||
store.set('alignItems', 'center')
|
||||
store.set('flexGrow', 0)
|
||||
store.set('flexGrow', 1)
|
||||
store.set('flexGrow', 2)
|
||||
assert.deepEqual(store._classNames, {
|
||||
'alignItems:center': 'alignItems:center',
|
||||
'flexGrow:0': 'flexGrow:0',
|
||||
'flexGrow:1': 'flexGrow:1',
|
||||
'flexGrow:2': 'flexGrow:2'
|
||||
})
|
||||
})
|
||||
|
||||
test('obfuscated classNames', () => {
|
||||
const store = new Store({}, { obfuscateClassNames: true })
|
||||
store.set('alignItems', 'center')
|
||||
store.set('flexGrow', 0)
|
||||
store.set('flexGrow', 1)
|
||||
store.set('flexGrow', 2)
|
||||
assert.deepEqual(store._classNames, {
|
||||
'alignItems:center': '_s_1',
|
||||
'flexGrow:0': '_s_2',
|
||||
'flexGrow:1': '_s_3',
|
||||
'flexGrow:2': '_s_4'
|
||||
})
|
||||
})
|
||||
|
||||
test('replaces space characters', () => {
|
||||
const store = new Store()
|
||||
|
||||
store.set('margin', '0 auto')
|
||||
assert.equal(store.get('margin', '0 auto'), 'margin\:0-auto')
|
||||
})
|
||||
})
|
||||
|
||||
suite('#toString', () => {
|
||||
test('human-readable style sheet', () => {
|
||||
const store = new Store()
|
||||
store.set('alignItems', 'center')
|
||||
store.set('backgroundColor', 'rgba(0,0,0,0)')
|
||||
store.set('color', '#fff')
|
||||
store.set('fontFamily', '"Helvetica Neue", Arial, sans-serif')
|
||||
store.set('marginBottom', '0px')
|
||||
store.set('width', '100%')
|
||||
|
||||
const expected = '/* 6 unique declarations */\n' +
|
||||
'.alignItems\\:center{align-items:center;}\n' +
|
||||
'.backgroundColor\\:rgba\\(0\\,0\\,0\\,0\\){background-color:rgba(0,0,0,0);}\n' +
|
||||
'.color\\:\\#fff{color:#fff;}\n' +
|
||||
'.fontFamily\\:\\"Helvetica-Neue\\"\\,-Arial\\,-sans-serif{font-family:"Helvetica Neue", Arial, sans-serif;}\n' +
|
||||
'.marginBottom\\:0px{margin-bottom:0px;}\n' +
|
||||
'.width\\:100\\%{width:100%;}'
|
||||
|
||||
assert.equal(store.toString(), expected)
|
||||
})
|
||||
|
||||
test('obfuscated style sheet', () => {
|
||||
const store = new Store({}, { obfuscateClassNames: true })
|
||||
store.set('alignItems', 'center')
|
||||
store.set('marginBottom', '0px')
|
||||
store.set('margin', '1px')
|
||||
store.set('margin', '2px')
|
||||
store.set('margin', '3px')
|
||||
|
||||
const expected = '/* 5 unique declarations */\n' +
|
||||
'._s_1{align-items:center;}\n' +
|
||||
'._s_3{margin:1px;}\n' +
|
||||
'._s_4{margin:2px;}\n' +
|
||||
'._s_5{margin:3px;}\n' +
|
||||
'._s_2{margin-bottom:0px;}'
|
||||
|
||||
assert.equal(store.toString(), expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
55
src/apis/StyleSheet/__tests__/StyleSheetRegistry-test.js
Normal file
55
src/apis/StyleSheet/__tests__/StyleSheetRegistry-test.js
Normal file
@@ -0,0 +1,55 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
/**
|
||||
* 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 assert from 'assert'
|
||||
import StyleSheetRegistry from '../StyleSheetRegistry'
|
||||
|
||||
suite('apis/StyleSheet/StyleSheetRegistry', () => {
|
||||
setup(() => {
|
||||
StyleSheetRegistry._reset()
|
||||
})
|
||||
|
||||
test('static renderToString', () => {
|
||||
const style1 = { alignItems: 'center', opacity: 1 }
|
||||
const style2 = { alignItems: 'center', opacity: 1 }
|
||||
StyleSheetRegistry.registerStyle(style1)
|
||||
StyleSheetRegistry.registerStyle(style2)
|
||||
|
||||
const actual = StyleSheetRegistry.renderToString()
|
||||
const expected = `/* 2 unique declarations */
|
||||
.__style1{-ms-flex-align:center;-webkit-align-items:center;-webkit-box-align:center;align-items:center;}
|
||||
.__style2{opacity:1;}`
|
||||
|
||||
assert.equal(actual, expected)
|
||||
})
|
||||
|
||||
test('static getStyleAsNativeProps', () => {
|
||||
const style = { borderColorTop: 'white', opacity: 1 }
|
||||
const style1 = { opacity: 1 }
|
||||
StyleSheetRegistry.registerStyle(style1)
|
||||
|
||||
// canUseCSS = false
|
||||
const actual1 = StyleSheetRegistry.getStyleAsNativeProps(style)
|
||||
const expected1 = {
|
||||
className: '',
|
||||
style: { borderColorTop: 'white', opacity: 1 }
|
||||
}
|
||||
assert.deepEqual(actual1, expected1)
|
||||
|
||||
// canUseCSS = true
|
||||
const actual2 = StyleSheetRegistry.getStyleAsNativeProps(style, true)
|
||||
const expected2 = {
|
||||
className: '__style1',
|
||||
style: { borderColorTop: 'white' }
|
||||
}
|
||||
assert.deepEqual(actual2, expected2)
|
||||
})
|
||||
})
|
||||
@@ -12,7 +12,7 @@
|
||||
import assert from 'assert'
|
||||
import flattenStyle from '../flattenStyle'
|
||||
|
||||
suite('flattenStyle', () => {
|
||||
suite('apis/StyleSheet/flattenStyle', () => {
|
||||
test('should merge style objects', () => {
|
||||
const style1 = {opacity: 1}
|
||||
const style2 = {order: 2}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { resetCSS, predefinedCSS } from '../predefs'
|
||||
import assert from 'assert'
|
||||
import StyleSheet from '..'
|
||||
|
||||
const styles = { root: { borderWidth: 1 } }
|
||||
const styles = { root: { opacity: 1 } }
|
||||
|
||||
suite('apis/StyleSheet', () => {
|
||||
setup(() => {
|
||||
@@ -12,55 +12,40 @@ suite('apis/StyleSheet', () => {
|
||||
})
|
||||
|
||||
suite('create', () => {
|
||||
const div = document.createElement('div')
|
||||
|
||||
setup(() => {
|
||||
document.body.appendChild(div)
|
||||
StyleSheet.create(styles)
|
||||
div.innerHTML = `<style id='${StyleSheet.elementId}'>${StyleSheet._renderToString()}</style>`
|
||||
})
|
||||
|
||||
teardown(() => {
|
||||
document.body.removeChild(div)
|
||||
})
|
||||
|
||||
test('returns styles object', () => {
|
||||
assert.equal(StyleSheet.create(styles), styles)
|
||||
})
|
||||
|
||||
test('updates already-rendered style sheet', () => {
|
||||
StyleSheet.create({ root: { color: 'red' } })
|
||||
// setup
|
||||
const div = document.createElement('div')
|
||||
document.body.appendChild(div)
|
||||
StyleSheet.create(styles)
|
||||
div.innerHTML = `<style id='${StyleSheet.elementId}'>${StyleSheet.renderToString()}</style>`
|
||||
|
||||
// test
|
||||
StyleSheet.create({ root: { color: 'red' } })
|
||||
assert.equal(
|
||||
document.getElementById(StyleSheet.elementId).textContent,
|
||||
`${resetCSS}\n${predefinedCSS}\n` +
|
||||
`/* 5 unique declarations */\n` +
|
||||
`.borderBottomWidth\\:1px{border-bottom-width:1px;}\n` +
|
||||
`.borderLeftWidth\\:1px{border-left-width:1px;}\n` +
|
||||
`.borderRightWidth\\:1px{border-right-width:1px;}\n` +
|
||||
`.borderTopWidth\\:1px{border-top-width:1px;}\n` +
|
||||
`.color\\:red{color:red;}`
|
||||
`/* 2 unique declarations */\n` +
|
||||
`.__style1{opacity:1;}\n` +
|
||||
`.__style2{color:red;}`
|
||||
)
|
||||
|
||||
// teardown
|
||||
document.body.removeChild(div)
|
||||
})
|
||||
})
|
||||
|
||||
test('resolve', () => {
|
||||
const props = { style: styles.root }
|
||||
const expected = { className: 'borderTopWidth:1px borderRightWidth:1px borderBottomWidth:1px borderLeftWidth:1px', style: {} }
|
||||
test('renderToString', () => {
|
||||
StyleSheet.create(styles)
|
||||
assert.deepEqual(StyleSheet.resolve(props), expected)
|
||||
})
|
||||
|
||||
test('_renderToString', () => {
|
||||
StyleSheet.create(styles)
|
||||
assert.equal(
|
||||
StyleSheet._renderToString(),
|
||||
StyleSheet.renderToString(),
|
||||
`${resetCSS}\n${predefinedCSS}\n` +
|
||||
`/* 4 unique declarations */\n` +
|
||||
`.borderBottomWidth\\:1px{border-bottom-width:1px;}\n` +
|
||||
`.borderLeftWidth\\:1px{border-left-width:1px;}\n` +
|
||||
`.borderRightWidth\\:1px{border-right-width:1px;}\n` +
|
||||
`.borderTopWidth\\:1px{border-top-width:1px;}`
|
||||
`/* 1 unique declarations */\n` +
|
||||
`.__style1{opacity:1;}`
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
31
src/apis/StyleSheet/__tests__/processTransform-test.js
Normal file
31
src/apis/StyleSheet/__tests__/processTransform-test.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import assert from 'assert'
|
||||
import processTransform from '../processTransform'
|
||||
|
||||
suite('apis/StyleSheet/processTransform', () => {
|
||||
test('transform', () => {
|
||||
const style = {
|
||||
transform: [
|
||||
{ scaleX: 20 },
|
||||
{ rotate: '20deg' }
|
||||
]
|
||||
}
|
||||
|
||||
assert.deepEqual(
|
||||
processTransform(style),
|
||||
{ transform: 'scaleX(20) rotate(20deg)' }
|
||||
)
|
||||
})
|
||||
|
||||
test('transformMatrix', () => {
|
||||
const style = {
|
||||
transformMatrix: [ 1, 2, 3, 4, 5, 6 ]
|
||||
}
|
||||
|
||||
assert.deepEqual(
|
||||
processTransform(style),
|
||||
{ transform: 'matrix3d(1,2,3,4,5,6)' }
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -9,10 +9,10 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import invariant from 'fbjs/lib/invariant'
|
||||
import ReactPropTypeLocationNames from 'react/lib/ReactPropTypeLocationNames'
|
||||
import invariant from 'invariant'
|
||||
|
||||
export default function createStrictShapeTypeChecker(shapeTypes) {
|
||||
module.exports = function createStrictShapeTypeChecker(shapeTypes) {
|
||||
function checkType(isRequired, props, propName, componentName, location?) {
|
||||
if (!props[propName]) {
|
||||
if (isRequired) {
|
||||
|
||||
@@ -8,16 +8,19 @@ const styleShortHands = {
|
||||
margin: [ 'marginTop', 'marginRight', 'marginBottom', 'marginLeft' ],
|
||||
marginHorizontal: [ 'marginRight', 'marginLeft' ],
|
||||
marginVertical: [ 'marginTop', 'marginBottom' ],
|
||||
overflow: [ 'overflowX', 'overflowY' ],
|
||||
padding: [ 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft' ],
|
||||
paddingHorizontal: [ 'paddingRight', 'paddingLeft' ],
|
||||
paddingVertical: [ 'paddingTop', 'paddingBottom' ],
|
||||
textDecorationLine: [ 'textDecoration' ],
|
||||
writingDirection: [ 'direction' ]
|
||||
}
|
||||
|
||||
/**
|
||||
* Alpha-sort properties, apart from shorthands which appear before the
|
||||
* properties they expand into. This ensures that more specific styles override
|
||||
* the shorthands, whatever the order in which they were originally declared.
|
||||
* Alpha-sort properties, apart from shorthands – they must appear before the
|
||||
* longhand properties that they expand into. This lets more specific styles
|
||||
* override less specific styles, whatever the order in which they were
|
||||
* originally declared.
|
||||
*/
|
||||
const sortProps = (propsArray) => propsArray.sort((a, b) => {
|
||||
const expandedA = styleShortHands[a]
|
||||
@@ -41,14 +44,17 @@ const expandStyle = (style) => {
|
||||
const expandedProps = styleShortHands[key]
|
||||
const value = normalizeValue(key, style[key])
|
||||
|
||||
if (expandedProps) {
|
||||
expandedProps.forEach((prop, i) => {
|
||||
resolvedStyle[expandedProps[i]] = value
|
||||
})
|
||||
} else if (key === 'flex') {
|
||||
// React Native treats `flex:1` like `flex:1 1 auto`
|
||||
if (key === 'flex') {
|
||||
resolvedStyle.flexGrow = value
|
||||
resolvedStyle.flexShrink = 1
|
||||
resolvedStyle.flexBasis = 'auto'
|
||||
} else if (key === 'textAlignVertical') {
|
||||
resolvedStyle.verticalAlign = (value === 'center' ? 'middle' : value)
|
||||
} else if (expandedProps) {
|
||||
expandedProps.forEach((prop, i) => {
|
||||
resolvedStyle[expandedProps[i]] = value
|
||||
})
|
||||
} else {
|
||||
resolvedStyle[key] = value
|
||||
}
|
||||
@@ -56,4 +62,4 @@ const expandStyle = (style) => {
|
||||
}, {})
|
||||
}
|
||||
|
||||
export default expandStyle
|
||||
module.exports = expandStyle
|
||||
|
||||
@@ -5,10 +5,9 @@
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
import invariant from 'invariant'
|
||||
import expandStyle from './expandStyle'
|
||||
import invariant from 'fbjs/lib/invariant'
|
||||
|
||||
export default function flattenStyle(style): ?Object {
|
||||
module.exports = function flattenStyle(style): ?Object {
|
||||
if (!style) {
|
||||
return undefined
|
||||
}
|
||||
@@ -16,9 +15,7 @@ export default function flattenStyle(style): ?Object {
|
||||
invariant(style !== true, 'style may be false but not true')
|
||||
|
||||
if (!Array.isArray(style)) {
|
||||
// we must expand styles during the flattening because expanded styles
|
||||
// override shorthands
|
||||
return expandStyle(style)
|
||||
return style
|
||||
}
|
||||
|
||||
const result = {}
|
||||
|
||||
@@ -1 +1 @@
|
||||
export default (string) => (string.replace(/([A-Z])/g, '-$1').toLowerCase()).replace(/^ms-/, '-ms-')
|
||||
module.exports = (string) => (string.replace(/([A-Z])/g, '-$1').toLowerCase()).replace(/^ms-/, '-ms-')
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { resetCSS, predefinedCSS, predefinedClassNames } from './predefs'
|
||||
import { resetCSS, predefinedCSS } from './predefs'
|
||||
import flattenStyle from './flattenStyle'
|
||||
import Store from './Store'
|
||||
import StyleSheetRegistry from './StyleSheetRegistry'
|
||||
import StyleSheetValidation from './StyleSheetValidation'
|
||||
|
||||
@@ -12,64 +11,61 @@ let lastStyleSheet = ''
|
||||
* Initialize the store with pointer-event styles mapping to our custom pointer
|
||||
* event classes
|
||||
*/
|
||||
const initialState = { classNames: predefinedClassNames }
|
||||
const options = { obfuscateClassNames: !(process.env.NODE_ENV !== 'production') }
|
||||
const createStore = () => new Store(initialState, options)
|
||||
let store = createStore()
|
||||
|
||||
/**
|
||||
* Destroy existing styles
|
||||
*/
|
||||
const _destroy = () => {
|
||||
store = createStore()
|
||||
isRendered = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the styles as a CSS style sheet
|
||||
*/
|
||||
const _renderToString = () => {
|
||||
const css = store.toString()
|
||||
isRendered = true
|
||||
return `${resetCSS}\n${predefinedCSS}\n${css}`
|
||||
StyleSheetRegistry._reset()
|
||||
}
|
||||
|
||||
const create = (styles: Object): Object => {
|
||||
for (const key in styles) {
|
||||
StyleSheetValidation.validateStyle(key, styles)
|
||||
StyleSheetRegistry.registerStyle(styles[key], store)
|
||||
StyleSheetRegistry.registerStyle(styles[key])
|
||||
}
|
||||
|
||||
// update the style sheet in place
|
||||
if (isRendered) {
|
||||
const stylesheet = document.getElementById(ELEMENT_ID)
|
||||
if (stylesheet) {
|
||||
const newStyleSheet = _renderToString()
|
||||
const newStyleSheet = renderToString()
|
||||
if (lastStyleSheet !== newStyleSheet) {
|
||||
stylesheet.textContent = newStyleSheet
|
||||
lastStyleSheet = newStyleSheet
|
||||
}
|
||||
} else if (process.env.NODE_ENV !== 'production') {
|
||||
console.error('ReactNative: cannot find "react-stylesheet" element')
|
||||
console.error(`ReactNative: cannot find "${ELEMENT_ID}" element`)
|
||||
}
|
||||
}
|
||||
|
||||
return styles
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the styles as a CSS style sheet
|
||||
*/
|
||||
const renderToString = () => {
|
||||
const css = StyleSheetRegistry.renderToString()
|
||||
isRendered = true
|
||||
return `${resetCSS}\n${predefinedCSS}\n${css}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts React props and converts inline styles to single purpose classes
|
||||
* where possible.
|
||||
*/
|
||||
const resolve = ({ style = {} }) => {
|
||||
return StyleSheetRegistry.getStyleAsNativeProps(style, store)
|
||||
return StyleSheetRegistry.getStyleAsNativeProps(style, isRendered)
|
||||
}
|
||||
|
||||
export default {
|
||||
module.exports = {
|
||||
_destroy,
|
||||
_renderToString,
|
||||
create,
|
||||
elementId: ELEMENT_ID,
|
||||
hairlineWidth: 1,
|
||||
flatten: flattenStyle,
|
||||
renderToString,
|
||||
resolve
|
||||
}
|
||||
|
||||
352
src/apis/StyleSheet/normalizeColor.js
Normal file
352
src/apis/StyleSheet/normalizeColor.js
Normal file
@@ -0,0 +1,352 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* 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 normalizeColor
|
||||
* @flow
|
||||
*/
|
||||
/* eslint no-bitwise: 0 */
|
||||
'use strict';
|
||||
|
||||
function normalizeColor(color: string | number): ?number {
|
||||
var match;
|
||||
|
||||
if (typeof color === 'number') {
|
||||
if (color >>> 0 === color && color >= 0 && color <= 0xffffffff) {
|
||||
return color;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ordered based on occurrences on Facebook codebase
|
||||
if ((match = matchers.hex6.exec(color))) {
|
||||
return parseInt(match[1] + 'ff', 16) >>> 0;
|
||||
}
|
||||
|
||||
if (names.hasOwnProperty(color)) {
|
||||
return names[color];
|
||||
}
|
||||
|
||||
if ((match = matchers.rgb.exec(color))) {
|
||||
return (
|
||||
parse255(match[1]) << 24 | // r
|
||||
parse255(match[2]) << 16 | // g
|
||||
parse255(match[3]) << 8 | // b
|
||||
0x000000ff // a
|
||||
) >>> 0;
|
||||
}
|
||||
|
||||
if ((match = matchers.rgba.exec(color))) {
|
||||
return (
|
||||
parse255(match[1]) << 24 | // r
|
||||
parse255(match[2]) << 16 | // g
|
||||
parse255(match[3]) << 8 | // b
|
||||
parse1(match[4]) // a
|
||||
) >>> 0;
|
||||
}
|
||||
|
||||
if ((match = matchers.hex3.exec(color))) {
|
||||
return parseInt(
|
||||
match[1] + match[1] + // r
|
||||
match[2] + match[2] + // g
|
||||
match[3] + match[3] + // b
|
||||
'ff', // a
|
||||
16
|
||||
) >>> 0;
|
||||
}
|
||||
|
||||
// https://drafts.csswg.org/css-color-4/#hex-notation
|
||||
if ((match = matchers.hex8.exec(color))) {
|
||||
return parseInt(match[1], 16) >>> 0;
|
||||
}
|
||||
|
||||
if ((match = matchers.hex4.exec(color))) {
|
||||
return parseInt(
|
||||
match[1] + match[1] + // r
|
||||
match[2] + match[2] + // g
|
||||
match[3] + match[3] + // b
|
||||
match[4] + match[4], // a
|
||||
16
|
||||
) >>> 0;
|
||||
}
|
||||
|
||||
if ((match = matchers.hsl.exec(color))) {
|
||||
return (
|
||||
hslToRgb(
|
||||
parse360(match[1]), // h
|
||||
parsePercentage(match[2]), // s
|
||||
parsePercentage(match[3]) // l
|
||||
) |
|
||||
0x000000ff // a
|
||||
) >>> 0;
|
||||
}
|
||||
|
||||
if ((match = matchers.hsla.exec(color))) {
|
||||
return (
|
||||
hslToRgb(
|
||||
parse360(match[1]), // h
|
||||
parsePercentage(match[2]), // s
|
||||
parsePercentage(match[3]) // l
|
||||
) |
|
||||
parse1(match[4]) // a
|
||||
) >>> 0;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function hue2rgb(p: number, q: number, t: number): number {
|
||||
if (t < 0) {
|
||||
t += 1;
|
||||
}
|
||||
if (t > 1) {
|
||||
t -= 1;
|
||||
}
|
||||
if (t < 1 / 6) {
|
||||
return p + (q - p) * 6 * t;
|
||||
}
|
||||
if (t < 1 / 2) {
|
||||
return q;
|
||||
}
|
||||
if (t < 2 / 3) {
|
||||
return p + (q - p) * (2 / 3 - t) * 6;
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
function hslToRgb(h: number, s: number, l: number): number {
|
||||
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||
var p = 2 * l - q;
|
||||
var r = hue2rgb(p, q, h + 1 / 3);
|
||||
var g = hue2rgb(p, q, h);
|
||||
var b = hue2rgb(p, q, h - 1 / 3);
|
||||
|
||||
return (
|
||||
Math.round(r * 255) << 24 |
|
||||
Math.round(g * 255) << 16 |
|
||||
Math.round(b * 255) << 8
|
||||
);
|
||||
}
|
||||
|
||||
// var INTEGER = '[-+]?\\d+';
|
||||
var NUMBER = '[-+]?\\d*\\.?\\d+';
|
||||
var PERCENTAGE = NUMBER + '%';
|
||||
|
||||
function call(...args) {
|
||||
return '\\(\\s*(' + args.join(')\\s*,\\s*(') + ')\\s*\\)';
|
||||
}
|
||||
|
||||
var matchers = {
|
||||
rgb: new RegExp('rgb' + call(NUMBER, NUMBER, NUMBER)),
|
||||
rgba: new RegExp('rgba' + call(NUMBER, NUMBER, NUMBER, NUMBER)),
|
||||
hsl: new RegExp('hsl' + call(NUMBER, PERCENTAGE, PERCENTAGE)),
|
||||
hsla: new RegExp('hsla' + call(NUMBER, PERCENTAGE, PERCENTAGE, NUMBER)),
|
||||
hex3: /^#([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
|
||||
hex4: /^#([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
|
||||
hex6: /^#([0-9a-fA-F]{6})$/,
|
||||
hex8: /^#([0-9a-fA-F]{8})$/,
|
||||
};
|
||||
|
||||
function parse255(str: string): number {
|
||||
var int = parseInt(str, 10);
|
||||
if (int < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (int > 255) {
|
||||
return 255;
|
||||
}
|
||||
return int;
|
||||
}
|
||||
|
||||
function parse360(str: string): number {
|
||||
var int = parseFloat(str);
|
||||
return (((int % 360) + 360) % 360) / 360;
|
||||
}
|
||||
|
||||
function parse1(str: string): number {
|
||||
var num = parseFloat(str);
|
||||
if (num < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (num > 1) {
|
||||
return 255;
|
||||
}
|
||||
return Math.round(num * 255);
|
||||
}
|
||||
|
||||
function parsePercentage(str: string): number {
|
||||
// parseFloat conveniently ignores the final %
|
||||
var int = parseFloat(str, 10);
|
||||
if (int < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (int > 100) {
|
||||
return 1;
|
||||
}
|
||||
return int / 100;
|
||||
}
|
||||
|
||||
var names = {
|
||||
/* @edit start */
|
||||
inherit: 'inherit',
|
||||
/* @edit end */
|
||||
transparent: 0x00000000,
|
||||
|
||||
// http://www.w3.org/TR/css3-color/#svg-color
|
||||
aliceblue: 0xf0f8ffff,
|
||||
antiquewhite: 0xfaebd7ff,
|
||||
aqua: 0x00ffffff,
|
||||
aquamarine: 0x7fffd4ff,
|
||||
azure: 0xf0ffffff,
|
||||
beige: 0xf5f5dcff,
|
||||
bisque: 0xffe4c4ff,
|
||||
black: 0x000000ff,
|
||||
blanchedalmond: 0xffebcdff,
|
||||
blue: 0x0000ffff,
|
||||
blueviolet: 0x8a2be2ff,
|
||||
brown: 0xa52a2aff,
|
||||
burlywood: 0xdeb887ff,
|
||||
burntsienna: 0xea7e5dff,
|
||||
cadetblue: 0x5f9ea0ff,
|
||||
chartreuse: 0x7fff00ff,
|
||||
chocolate: 0xd2691eff,
|
||||
coral: 0xff7f50ff,
|
||||
cornflowerblue: 0x6495edff,
|
||||
cornsilk: 0xfff8dcff,
|
||||
crimson: 0xdc143cff,
|
||||
cyan: 0x00ffffff,
|
||||
darkblue: 0x00008bff,
|
||||
darkcyan: 0x008b8bff,
|
||||
darkgoldenrod: 0xb8860bff,
|
||||
darkgray: 0xa9a9a9ff,
|
||||
darkgreen: 0x006400ff,
|
||||
darkgrey: 0xa9a9a9ff,
|
||||
darkkhaki: 0xbdb76bff,
|
||||
darkmagenta: 0x8b008bff,
|
||||
darkolivegreen: 0x556b2fff,
|
||||
darkorange: 0xff8c00ff,
|
||||
darkorchid: 0x9932ccff,
|
||||
darkred: 0x8b0000ff,
|
||||
darksalmon: 0xe9967aff,
|
||||
darkseagreen: 0x8fbc8fff,
|
||||
darkslateblue: 0x483d8bff,
|
||||
darkslategray: 0x2f4f4fff,
|
||||
darkslategrey: 0x2f4f4fff,
|
||||
darkturquoise: 0x00ced1ff,
|
||||
darkviolet: 0x9400d3ff,
|
||||
deeppink: 0xff1493ff,
|
||||
deepskyblue: 0x00bfffff,
|
||||
dimgray: 0x696969ff,
|
||||
dimgrey: 0x696969ff,
|
||||
dodgerblue: 0x1e90ffff,
|
||||
firebrick: 0xb22222ff,
|
||||
floralwhite: 0xfffaf0ff,
|
||||
forestgreen: 0x228b22ff,
|
||||
fuchsia: 0xff00ffff,
|
||||
gainsboro: 0xdcdcdcff,
|
||||
ghostwhite: 0xf8f8ffff,
|
||||
gold: 0xffd700ff,
|
||||
goldenrod: 0xdaa520ff,
|
||||
gray: 0x808080ff,
|
||||
green: 0x008000ff,
|
||||
greenyellow: 0xadff2fff,
|
||||
grey: 0x808080ff,
|
||||
honeydew: 0xf0fff0ff,
|
||||
hotpink: 0xff69b4ff,
|
||||
indianred: 0xcd5c5cff,
|
||||
indigo: 0x4b0082ff,
|
||||
ivory: 0xfffff0ff,
|
||||
khaki: 0xf0e68cff,
|
||||
lavender: 0xe6e6faff,
|
||||
lavenderblush: 0xfff0f5ff,
|
||||
lawngreen: 0x7cfc00ff,
|
||||
lemonchiffon: 0xfffacdff,
|
||||
lightblue: 0xadd8e6ff,
|
||||
lightcoral: 0xf08080ff,
|
||||
lightcyan: 0xe0ffffff,
|
||||
lightgoldenrodyellow: 0xfafad2ff,
|
||||
lightgray: 0xd3d3d3ff,
|
||||
lightgreen: 0x90ee90ff,
|
||||
lightgrey: 0xd3d3d3ff,
|
||||
lightpink: 0xffb6c1ff,
|
||||
lightsalmon: 0xffa07aff,
|
||||
lightseagreen: 0x20b2aaff,
|
||||
lightskyblue: 0x87cefaff,
|
||||
lightslategray: 0x778899ff,
|
||||
lightslategrey: 0x778899ff,
|
||||
lightsteelblue: 0xb0c4deff,
|
||||
lightyellow: 0xffffe0ff,
|
||||
lime: 0x00ff00ff,
|
||||
limegreen: 0x32cd32ff,
|
||||
linen: 0xfaf0e6ff,
|
||||
magenta: 0xff00ffff,
|
||||
maroon: 0x800000ff,
|
||||
mediumaquamarine: 0x66cdaaff,
|
||||
mediumblue: 0x0000cdff,
|
||||
mediumorchid: 0xba55d3ff,
|
||||
mediumpurple: 0x9370dbff,
|
||||
mediumseagreen: 0x3cb371ff,
|
||||
mediumslateblue: 0x7b68eeff,
|
||||
mediumspringgreen: 0x00fa9aff,
|
||||
mediumturquoise: 0x48d1ccff,
|
||||
mediumvioletred: 0xc71585ff,
|
||||
midnightblue: 0x191970ff,
|
||||
mintcream: 0xf5fffaff,
|
||||
mistyrose: 0xffe4e1ff,
|
||||
moccasin: 0xffe4b5ff,
|
||||
navajowhite: 0xffdeadff,
|
||||
navy: 0x000080ff,
|
||||
oldlace: 0xfdf5e6ff,
|
||||
olive: 0x808000ff,
|
||||
olivedrab: 0x6b8e23ff,
|
||||
orange: 0xffa500ff,
|
||||
orangered: 0xff4500ff,
|
||||
orchid: 0xda70d6ff,
|
||||
palegoldenrod: 0xeee8aaff,
|
||||
palegreen: 0x98fb98ff,
|
||||
paleturquoise: 0xafeeeeff,
|
||||
palevioletred: 0xdb7093ff,
|
||||
papayawhip: 0xffefd5ff,
|
||||
peachpuff: 0xffdab9ff,
|
||||
peru: 0xcd853fff,
|
||||
pink: 0xffc0cbff,
|
||||
plum: 0xdda0ddff,
|
||||
powderblue: 0xb0e0e6ff,
|
||||
purple: 0x800080ff,
|
||||
rebeccapurple: 0x663399ff,
|
||||
red: 0xff0000ff,
|
||||
rosybrown: 0xbc8f8fff,
|
||||
royalblue: 0x4169e1ff,
|
||||
saddlebrown: 0x8b4513ff,
|
||||
salmon: 0xfa8072ff,
|
||||
sandybrown: 0xf4a460ff,
|
||||
seagreen: 0x2e8b57ff,
|
||||
seashell: 0xfff5eeff,
|
||||
sienna: 0xa0522dff,
|
||||
silver: 0xc0c0c0ff,
|
||||
skyblue: 0x87ceebff,
|
||||
slateblue: 0x6a5acdff,
|
||||
slategray: 0x708090ff,
|
||||
slategrey: 0x708090ff,
|
||||
snow: 0xfffafaff,
|
||||
springgreen: 0x00ff7fff,
|
||||
steelblue: 0x4682b4ff,
|
||||
tan: 0xd2b48cff,
|
||||
teal: 0x008080ff,
|
||||
thistle: 0xd8bfd8ff,
|
||||
tomato: 0xff6347ff,
|
||||
turquoise: 0x40e0d0ff,
|
||||
violet: 0xee82eeff,
|
||||
wheat: 0xf5deb3ff,
|
||||
white: 0xffffffff,
|
||||
whitesmoke: 0xf5f5f5ff,
|
||||
yellow: 0xffff00ff,
|
||||
yellowgreen: 0x9acd32ff,
|
||||
};
|
||||
|
||||
module.exports = normalizeColor;
|
||||
@@ -30,4 +30,4 @@ const normalizeValue = (property, value) => {
|
||||
return value
|
||||
}
|
||||
|
||||
export default normalizeValue
|
||||
module.exports = normalizeValue
|
||||
|
||||
@@ -2,24 +2,23 @@
|
||||
* Reset unwanted styles beyond the control of React inline styles
|
||||
*/
|
||||
export const resetCSS =
|
||||
`/* React Native Web */
|
||||
html {font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}
|
||||
`/* React Native for Web */
|
||||
html {font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}
|
||||
body {margin:0}
|
||||
button::-moz-focus-inner, input::-moz-focus-inner {border:0;padding:0}
|
||||
input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration {-webkit-appearance:none}
|
||||
ol,ul,li {list-style:none}`
|
||||
input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration {display:none}`
|
||||
|
||||
/**
|
||||
* Custom pointer event styles
|
||||
*/
|
||||
export const predefinedCSS =
|
||||
`/* pointer-events */
|
||||
._s_pe-a, ._s_pe-bo, ._s_pe-bn * {pointer-events:auto}
|
||||
._s_pe-n, ._s_pe-bo *, ._s_pe-bn {pointer-events:none}`
|
||||
.__style_pea, .__style_pebo, .__style_pebn * {pointer-events:auto}
|
||||
.__style_pen, .__style_pebo *, .__style_pebn {pointer-events:none}`
|
||||
|
||||
export const predefinedClassNames = {
|
||||
'pointerEvents:auto': '_s_pe-a',
|
||||
'pointerEvents:box-none': '_s_pe-bn',
|
||||
'pointerEvents:box-only': '_s_pe-bo',
|
||||
'pointerEvents:none': '_s_pe-n'
|
||||
'pointerEvents:auto': '__style_pea',
|
||||
'pointerEvents:box-none': '__style_pebn',
|
||||
'pointerEvents:box-only': '__style_pebo',
|
||||
'pointerEvents:none': '__style_pen'
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import Prefixer from 'inline-style-prefixer'
|
||||
const prefixer = new Prefixer()
|
||||
export default prefixer
|
||||
25
src/apis/StyleSheet/processTransform.js
Normal file
25
src/apis/StyleSheet/processTransform.js
Normal file
@@ -0,0 +1,25 @@
|
||||
// { scale: 2 } => 'scale(2)'
|
||||
const mapTransform = (transform) => {
|
||||
var key = Object.keys(transform)[0]
|
||||
return `${key}(${transform[key]})`
|
||||
}
|
||||
|
||||
// [1,2,3,4,5,6] => 'matrix3d(1,2,3,4,5,6)'
|
||||
const convertTransformMatrix = (transformMatrix) => {
|
||||
var matrix = transformMatrix.join(',')
|
||||
return `matrix3d(${matrix})`
|
||||
}
|
||||
|
||||
const processTransform = (style) => {
|
||||
if (style) {
|
||||
if (style.transform) {
|
||||
style.transform = style.transform.map(mapTransform).join(' ')
|
||||
} else if (style.transformMatrix) {
|
||||
style.transform = convertTransformMatrix(style.transformMatrix)
|
||||
delete style.transformMatrix
|
||||
}
|
||||
}
|
||||
return style
|
||||
}
|
||||
|
||||
module.exports = processTransform
|
||||
@@ -73,8 +73,28 @@ suite('apis/UIManager', () => {
|
||||
})
|
||||
})
|
||||
|
||||
suite('measureInWindow', () => {
|
||||
test('provides correct layout to callback', () => {
|
||||
const node = createNode({ height: '10px', width: '10px' })
|
||||
const middle = createNode({ padding: '20px' })
|
||||
const context = createNode({ padding: '20px' })
|
||||
middle.appendChild(node)
|
||||
context.appendChild(middle)
|
||||
document.body.appendChild(context)
|
||||
|
||||
UIManager.measureInWindow(node, (x, y, width, height) => {
|
||||
assert.equal(x, 40)
|
||||
assert.equal(y, 40)
|
||||
assert.equal(width, 10)
|
||||
assert.equal(height, 10)
|
||||
})
|
||||
|
||||
document.body.removeChild(context)
|
||||
})
|
||||
})
|
||||
|
||||
suite('updateView', () => {
|
||||
test('adds new className to existing className', () => {
|
||||
test('add new className to existing className', () => {
|
||||
const node = createNode()
|
||||
node.className = 'existing'
|
||||
const props = { className: 'extra' }
|
||||
@@ -89,7 +109,20 @@ suite('apis/UIManager', () => {
|
||||
assert.equal(node.getAttribute('style'), 'color: red; opacity: 0;')
|
||||
})
|
||||
|
||||
test('sets attribute values', () => {
|
||||
test('replaces input and textarea text', () => {
|
||||
const node = createNode()
|
||||
node.value = 'initial'
|
||||
const textProp = { text: 'expected-text' }
|
||||
const valueProp = { value: 'expected-value' }
|
||||
|
||||
UIManager.updateView(node, textProp)
|
||||
assert.equal(node.value, 'expected-text')
|
||||
|
||||
UIManager.updateView(node, valueProp)
|
||||
assert.equal(node.value, 'expected-value')
|
||||
})
|
||||
|
||||
test('sets other attribute values', () => {
|
||||
const node = createNode()
|
||||
const props = { 'aria-level': '4', 'data-of-type': 'string' }
|
||||
UIManager.updateView(node, props)
|
||||
|
||||
@@ -1,35 +1,66 @@
|
||||
import CSSPropertyOperations from 'react/lib/CSSPropertyOperations'
|
||||
import flattenStyle from '../StyleSheet/flattenStyle'
|
||||
import processTransform from '../StyleSheet/processTransform'
|
||||
|
||||
const measureAll = (node, callback, relativeToNativeNode) => {
|
||||
const { height, left, top, width } = node.getBoundingClientRect()
|
||||
const _measureLayout = (node, relativeToNativeNode, callback) => {
|
||||
const relativeNode = relativeToNativeNode || node.parentNode
|
||||
const relativeRect = relativeNode.getBoundingClientRect()
|
||||
const { height, left, top, width } = node.getBoundingClientRect()
|
||||
const x = left - relativeRect.left
|
||||
const y = top - relativeRect.top
|
||||
callback(x, y, width, height, left, top)
|
||||
}
|
||||
|
||||
const UIManager = {
|
||||
blur(node) {
|
||||
try { node.blur() } catch (err) {}
|
||||
},
|
||||
|
||||
focus(node) {
|
||||
try { node.focus() } catch (err) {}
|
||||
},
|
||||
|
||||
measure(node, callback) {
|
||||
measureAll(node, callback)
|
||||
_measureLayout(node, null, callback)
|
||||
},
|
||||
|
||||
measureInWindow(node, callback) {
|
||||
const { height, left, top, width } = node.getBoundingClientRect()
|
||||
callback(left, top, width, height)
|
||||
},
|
||||
|
||||
measureLayout(node, relativeToNativeNode, onFail, onSuccess) {
|
||||
measureAll(node, (x, y, width, height) => onSuccess(x, y, width, height), relativeToNativeNode)
|
||||
const relativeTo = relativeToNativeNode || node.parentNode
|
||||
_measureLayout(node, relativeTo, onSuccess)
|
||||
},
|
||||
|
||||
updateView(node, props) {
|
||||
for (const prop in props) {
|
||||
let nativeProp
|
||||
const value = props[prop]
|
||||
if (prop === 'style') {
|
||||
CSSPropertyOperations.setValueForStyles(node, value)
|
||||
} else if (prop === 'className') {
|
||||
node.classList.add(value)
|
||||
} else {
|
||||
node.setAttribute(prop, value)
|
||||
|
||||
switch (prop) {
|
||||
case 'style':
|
||||
// convert styles to DOM-styles
|
||||
CSSPropertyOperations.setValueForStyles(node, processTransform(flattenStyle(value)))
|
||||
break
|
||||
case 'class':
|
||||
case 'className':
|
||||
nativeProp = 'class'
|
||||
// prevent class names managed by React Native from being replaced
|
||||
const className = node.getAttribute(nativeProp) + ' ' + value
|
||||
node.setAttribute(nativeProp, className)
|
||||
break
|
||||
case 'text':
|
||||
case 'value':
|
||||
// native platforms use `text` prop to replace text input value
|
||||
node.value = value
|
||||
break
|
||||
default:
|
||||
node.setAttribute(prop, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default UIManager
|
||||
module.exports = UIManager
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
@@ -18,7 +19,8 @@ const keyframeEffects = [
|
||||
{ transform: 'scale(0.95)', opacity: 0.5 }
|
||||
]
|
||||
|
||||
export default class ActivityIndicator extends Component {
|
||||
@NativeMethodsDecorator
|
||||
class ActivityIndicator extends Component {
|
||||
static propTypes = {
|
||||
animating: PropTypes.bool,
|
||||
color: PropTypes.string,
|
||||
@@ -39,19 +41,11 @@ export default class ActivityIndicator extends Component {
|
||||
if (document.documentElement.animate) {
|
||||
this._player = ReactDOM.findDOMNode(this._indicatorRef).animate(keyframeEffects, animationEffectTimingProperties)
|
||||
}
|
||||
if (this.props.animating) {
|
||||
this._player.play()
|
||||
} else {
|
||||
this._player.cancel()
|
||||
}
|
||||
this._manageAnimation()
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.animating) {
|
||||
this._player.play()
|
||||
} else {
|
||||
this._player.cancel()
|
||||
}
|
||||
this._manageAnimation()
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -77,6 +71,16 @@ export default class ActivityIndicator extends Component {
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
_manageAnimation() {
|
||||
if (this._player) {
|
||||
if (this.props.animating) {
|
||||
this._player.play()
|
||||
} else {
|
||||
this._player.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
@@ -103,3 +107,5 @@ const indicatorStyles = StyleSheet.create({
|
||||
height: 36
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = ActivityIndicator
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
|
||||
@@ -17,7 +18,8 @@ const roleComponents = {
|
||||
region: 'section'
|
||||
}
|
||||
|
||||
export default class CoreComponent extends Component {
|
||||
@NativeMethodsDecorator
|
||||
class CoreComponent extends Component {
|
||||
static propTypes = {
|
||||
accessibilityLabel: PropTypes.string,
|
||||
accessibilityLiveRegion: PropTypes.oneOf([ 'assertive', 'off', 'polite' ]),
|
||||
@@ -62,3 +64,5 @@ export default class CoreComponent extends Component {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CoreComponent
|
||||
|
||||
10
src/components/Image/ImageResizeMode.js
Normal file
10
src/components/Image/ImageResizeMode.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import keyMirror from 'fbjs/lib/keyMirror'
|
||||
|
||||
const ImageResizeMode = keyMirror({
|
||||
contain: null,
|
||||
cover: null,
|
||||
none: null,
|
||||
stretch: null
|
||||
})
|
||||
|
||||
module.exports = ImageResizeMode
|
||||
@@ -2,14 +2,16 @@ import { PropTypes } from 'react'
|
||||
import ColorPropType from '../../apis/StyleSheet/ColorPropType'
|
||||
import LayoutPropTypes from '../../apis/StyleSheet/LayoutPropTypes'
|
||||
import TransformPropTypes from '../../apis/StyleSheet/TransformPropTypes'
|
||||
import ImageResizeMode from './ImageResizeMode'
|
||||
|
||||
const hiddenOrVisible = PropTypes.oneOf([ 'hidden', 'visible' ])
|
||||
|
||||
export default {
|
||||
module.exports = {
|
||||
...LayoutPropTypes,
|
||||
...TransformPropTypes,
|
||||
backfaceVisibility: hiddenOrVisible,
|
||||
backgroundColor: ColorPropType,
|
||||
resizeMode: PropTypes.oneOf(Object.keys(ImageResizeMode)),
|
||||
/**
|
||||
* @platform web
|
||||
*/
|
||||
|
||||
@@ -3,9 +3,12 @@
|
||||
import * as utils from '../../../modules/specHelpers'
|
||||
import assert from 'assert'
|
||||
import React from 'react'
|
||||
import flattenStyle from '../../../apis/StyleSheet/flattenStyle'
|
||||
|
||||
import Image from '../'
|
||||
|
||||
const getStyleBackgroundSize = (element) => flattenStyle(element.props.style).backgroundSize
|
||||
|
||||
suite('components/Image', () => {
|
||||
test('default accessibility', () => {
|
||||
const dom = utils.renderToDOM(<Image />)
|
||||
@@ -33,6 +36,14 @@ suite('components/Image', () => {
|
||||
assert(backgroundImage.indexOf(defaultSource.uri) > -1)
|
||||
})
|
||||
|
||||
test('prop "defaultSource" with string value"', () => {
|
||||
// emulate require-ed asset
|
||||
const defaultSource = 'https://google.com/favicon.ico'
|
||||
const dom = utils.renderToDOM(<Image defaultSource={defaultSource} />)
|
||||
const backgroundImage = dom.style.backgroundImage
|
||||
assert(backgroundImage.indexOf(defaultSource) > -1)
|
||||
})
|
||||
|
||||
test('prop "onError"', function (done) {
|
||||
this.timeout(5000)
|
||||
utils.render(<Image
|
||||
@@ -47,10 +58,7 @@ suite('components/Image', () => {
|
||||
|
||||
test('prop "onLoad"', function (done) {
|
||||
this.timeout(5000)
|
||||
utils.render(<Image
|
||||
onLoad={onLoad}
|
||||
source={{ uri: 'https://google.com/favicon.ico' }}
|
||||
/>)
|
||||
utils.render(<Image onLoad={onLoad} source={{ uri: 'https://google.com/favicon.ico' }} />)
|
||||
function onLoad(e) {
|
||||
assert.equal(e.nativeEvent.type, 'load')
|
||||
done()
|
||||
@@ -61,9 +69,67 @@ suite('components/Image', () => {
|
||||
|
||||
test('prop "onLoadStart"')
|
||||
|
||||
test('prop "resizeMode"')
|
||||
suite('prop "resizeMode"', () => {
|
||||
test('value "contain"', () => {
|
||||
const result = utils.shallowRender(<Image resizeMode={Image.resizeMode.contain} />)
|
||||
assert.equal(getStyleBackgroundSize(result), 'contain')
|
||||
})
|
||||
|
||||
test('prop "source"')
|
||||
test('value "cover"', () => {
|
||||
const result = utils.shallowRender(<Image resizeMode={Image.resizeMode.cover} />)
|
||||
assert.equal(getStyleBackgroundSize(result), 'cover')
|
||||
})
|
||||
|
||||
test('value "none"', () => {
|
||||
const result = utils.shallowRender(<Image resizeMode={Image.resizeMode.none} />)
|
||||
assert.equal(getStyleBackgroundSize(result), 'auto')
|
||||
})
|
||||
|
||||
test('value "stretch"', () => {
|
||||
const result = utils.shallowRender(<Image resizeMode={Image.resizeMode.stretch} />)
|
||||
assert.equal(getStyleBackgroundSize(result), '100% 100%')
|
||||
})
|
||||
|
||||
test('no value', () => {
|
||||
const result = utils.shallowRender(<Image />)
|
||||
assert.equal(getStyleBackgroundSize(result), 'cover')
|
||||
})
|
||||
})
|
||||
|
||||
test('prop "source"', function (done) {
|
||||
this.timeout(5000)
|
||||
const source = { uri: 'https://google.com/favicon.ico' }
|
||||
utils.render(<Image onLoad={onLoad} source={source} />)
|
||||
function onLoad(e) {
|
||||
const src = e.nativeEvent.target.src
|
||||
assert.equal(src, source.uri)
|
||||
done()
|
||||
}
|
||||
})
|
||||
|
||||
test('prop "source" with string value', function (done) {
|
||||
this.timeout(5000)
|
||||
// emulate require-ed asset
|
||||
const source = 'https://google.com/favicon.ico'
|
||||
utils.render(<Image onLoad={onLoad} source={source} />)
|
||||
function onLoad(e) {
|
||||
const src = e.nativeEvent.target.src
|
||||
assert.equal(src, source)
|
||||
done()
|
||||
}
|
||||
})
|
||||
|
||||
suite('prop "style"', () => {
|
||||
test('converts "resizeMode" property', () => {
|
||||
const result = utils.shallowRender(<Image style={{ resizeMode: Image.resizeMode.contain }} />)
|
||||
assert.equal(getStyleBackgroundSize(result), 'contain')
|
||||
})
|
||||
|
||||
test('removes "resizeMode" property', () => {
|
||||
const result = utils.shallowRender(<Image style={{ resizeMode: Image.resizeMode.contain }} />)
|
||||
assert.equal(flattenStyle(result.props.style).resizeMode, undefined)
|
||||
})
|
||||
})
|
||||
|
||||
test('prop "testID"', () => {
|
||||
const testID = 'testID'
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
/* global window */
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
||||
import resolveAssetSource from './resolveAssetSource'
|
||||
import CoreComponent from '../CoreComponent'
|
||||
import ImageResizeMode from './ImageResizeMode'
|
||||
import ImageStylePropTypes from './ImageStylePropTypes'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType'
|
||||
import View from '../View'
|
||||
|
||||
@@ -12,10 +15,40 @@ const STATUS_LOADING = 'LOADING'
|
||||
const STATUS_PENDING = 'PENDING'
|
||||
const STATUS_IDLE = 'IDLE'
|
||||
|
||||
export default class Image extends Component {
|
||||
const ImageSourcePropType = PropTypes.oneOfType([
|
||||
PropTypes.shape({
|
||||
uri: PropTypes.string.isRequired
|
||||
}),
|
||||
PropTypes.string
|
||||
])
|
||||
|
||||
@NativeMethodsDecorator
|
||||
class Image extends Component {
|
||||
static propTypes = {
|
||||
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
|
||||
accessible: CoreComponent.propTypes.accessible,
|
||||
children: PropTypes.any,
|
||||
defaultSource: ImageSourcePropType,
|
||||
onError: PropTypes.func,
|
||||
onLoad: PropTypes.func,
|
||||
onLoadEnd: PropTypes.func,
|
||||
onLoadStart: PropTypes.func,
|
||||
resizeMode: PropTypes.oneOf(['contain', 'cover', 'none', 'stretch']),
|
||||
source: ImageSourcePropType,
|
||||
style: StyleSheetPropType(ImageStylePropTypes),
|
||||
testID: CoreComponent.propTypes.testID
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
accessible: true,
|
||||
style: {}
|
||||
};
|
||||
|
||||
static resizeMode = ImageResizeMode;
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
const { uri } = props.source
|
||||
const uri = resolveAssetSource(props.source)
|
||||
// state
|
||||
this.state = { status: uri ? STATUS_PENDING : STATUS_IDLE }
|
||||
// autobinding
|
||||
@@ -23,36 +56,14 @@ export default class Image extends Component {
|
||||
this._onLoad = this._onLoad.bind(this)
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
|
||||
accessible: CoreComponent.propTypes.accessible,
|
||||
children: PropTypes.any,
|
||||
defaultSource: PropTypes.object,
|
||||
onError: PropTypes.func,
|
||||
onLoad: PropTypes.func,
|
||||
onLoadEnd: PropTypes.func,
|
||||
onLoadStart: PropTypes.func,
|
||||
resizeMode: PropTypes.oneOf(['contain', 'cover', 'none', 'stretch']),
|
||||
source: PropTypes.object,
|
||||
style: StyleSheetPropType(ImageStylePropTypes),
|
||||
testID: CoreComponent.propTypes.testID
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
accessible: true,
|
||||
defaultSource: {},
|
||||
resizeMode: 'cover',
|
||||
source: {}
|
||||
};
|
||||
|
||||
_createImageLoader() {
|
||||
const { source } = this.props
|
||||
const uri = resolveAssetSource(this.props.source)
|
||||
|
||||
this._destroyImageLoader()
|
||||
this.image = new window.Image()
|
||||
this.image.onerror = this._onError
|
||||
this.image.onload = this._onLoad
|
||||
this.image.src = source.uri
|
||||
this.image.src = uri
|
||||
this._onLoadStart()
|
||||
}
|
||||
|
||||
@@ -108,9 +119,10 @@ export default class Image extends Component {
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.source.uri !== nextProps.source.uri) {
|
||||
const nextUri = resolveAssetSource(nextProps.source)
|
||||
if (resolveAssetSource(this.props.source) !== nextUri) {
|
||||
this.setState({
|
||||
status: nextProps.source.uri ? STATUS_PENDING : STATUS_IDLE
|
||||
status: nextUri ? STATUS_PENDING : STATUS_IDLE
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -125,16 +137,18 @@ export default class Image extends Component {
|
||||
accessible,
|
||||
children,
|
||||
defaultSource,
|
||||
resizeMode,
|
||||
source,
|
||||
style,
|
||||
testID
|
||||
} = this.props
|
||||
|
||||
const isLoaded = this.state.status === STATUS_LOADED
|
||||
const defaultImage = defaultSource.uri || null
|
||||
const displayImage = !isLoaded ? defaultImage : source.uri
|
||||
const displayImage = resolveAssetSource(!isLoaded ? defaultSource : source)
|
||||
const backgroundImage = displayImage ? `url("${displayImage}")` : null
|
||||
const style = StyleSheet.flatten(this.props.style)
|
||||
|
||||
const resizeMode = this.props.resizeMode || style.resizeMode || ImageResizeMode.cover
|
||||
// remove resizeMode style, as it is not supported by View
|
||||
delete style.resizeMode
|
||||
|
||||
/**
|
||||
* Image is a non-stretching View. The image is displayed as a background
|
||||
@@ -203,3 +217,5 @@ const resizeModeStyles = StyleSheet.create({
|
||||
backgroundSize: '100% 100%'
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = Image
|
||||
|
||||
5
src/components/Image/resolveAssetSource.js
Normal file
5
src/components/Image/resolveAssetSource.js
Normal file
@@ -0,0 +1,5 @@
|
||||
function resolveAssetSource(source) {
|
||||
return ((typeof source === 'object') ? source.uri : source) || null
|
||||
}
|
||||
|
||||
module.exports = resolveAssetSource
|
||||
@@ -1,7 +1,9 @@
|
||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import ScrollView from '../ScrollView'
|
||||
|
||||
export default class ListView extends Component {
|
||||
@NativeMethodsDecorator
|
||||
class ListView extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.any,
|
||||
style: ScrollView.propTypes.style
|
||||
@@ -17,3 +19,5 @@ export default class ListView extends Component {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ListView
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import invariant from 'invariant'
|
||||
import Platform from '../../apis/Platform'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
@@ -19,7 +18,7 @@ let lastUsedTag = 0
|
||||
/**
|
||||
* A container that renders all the modals on top of everything else in the application.
|
||||
*/
|
||||
export default class Portal extends Component {
|
||||
class Portal extends Component {
|
||||
static propTypes = {
|
||||
onModalVisibilityChanged: PropTypes.func.isRequired
|
||||
};
|
||||
@@ -81,10 +80,6 @@ export default class Portal extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
invariant(
|
||||
_portalRef === this || _portalRef === undefined,
|
||||
'More than one Portal instance detected. Never use <Portal> in your code.'
|
||||
)
|
||||
_portalRef = this
|
||||
if (!this.state.modals) { return null }
|
||||
const modals = []
|
||||
@@ -154,3 +149,5 @@ const styles = StyleSheet.create({
|
||||
bottom: 0
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = Portal
|
||||
|
||||
95
src/components/ScrollView/ScrollViewBase.js
Normal file
95
src/components/ScrollView/ScrollViewBase.js
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Copyright (c) 2016-present, Nicolas Gallagher.
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import debounce from 'lodash.debounce'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import View from '../View'
|
||||
|
||||
/**
|
||||
* Encapsulates the Web-specific scroll throttling and disabling logic
|
||||
*/
|
||||
export default class ScrollViewBase extends Component {
|
||||
static propTypes = {
|
||||
...View.propTypes,
|
||||
onScroll: PropTypes.func,
|
||||
onTouchMove: PropTypes.func,
|
||||
onWheel: PropTypes.func,
|
||||
scrollEnabled: PropTypes.bool,
|
||||
scrollEventThrottle: PropTypes.number
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
scrollEnabled: true
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this._debouncedOnScrollEnd = debounce(this._handleScrollEnd, 100)
|
||||
this._handlePreventableScrollEvent = this._handlePreventableScrollEvent.bind(this)
|
||||
this._handleScroll = this._handleScroll.bind(this)
|
||||
this._state = { isScrolling: false }
|
||||
}
|
||||
|
||||
_handlePreventableScrollEvent(handler) {
|
||||
return (e) => {
|
||||
if (!this.props.scrollEnabled) {
|
||||
e.preventDefault()
|
||||
} else {
|
||||
if (handler) handler(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_handleScroll(e) {
|
||||
const { scrollEventThrottle } = this.props
|
||||
// A scroll happened, so the scroll bumps the debounce.
|
||||
this._debouncedOnScrollEnd(e)
|
||||
if (this._state.isScrolling) {
|
||||
// Scroll last tick may have changed, check if we need to notify
|
||||
if (this._shouldEmitScrollEvent(this._state.scrollLastTick, scrollEventThrottle)) {
|
||||
this._handleScrollTick(e)
|
||||
}
|
||||
} else {
|
||||
// Weren't scrolling, so we must have just started
|
||||
this._handleScrollStart(e)
|
||||
}
|
||||
}
|
||||
|
||||
_handleScrollStart(e) {
|
||||
this._state.isScrolling = true
|
||||
this._state.scrollLastTick = Date.now()
|
||||
}
|
||||
|
||||
_handleScrollTick(e) {
|
||||
const { onScroll } = this.props
|
||||
this._state.scrollLastTick = Date.now()
|
||||
if (onScroll) onScroll(e)
|
||||
}
|
||||
|
||||
_handleScrollEnd(e) {
|
||||
const { onScroll } = this.props
|
||||
this._state.isScrolling = false
|
||||
if (onScroll) onScroll(e)
|
||||
}
|
||||
|
||||
_shouldEmitScrollEvent(lastTick, eventThrottle) {
|
||||
const timeSinceLastTick = Date.now() - lastTick
|
||||
return (eventThrottle > 0 && timeSinceLastTick >= (1000 / eventThrottle))
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View
|
||||
{...this.props}
|
||||
onScroll={this._handleScroll}
|
||||
onTouchMove={this._handlePreventableScrollEvent(this.props.onTouchMove)}
|
||||
onWheel={this._handlePreventableScrollEvent(this.props.onWheel)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,129 +1,227 @@
|
||||
import debounce from 'lodash.debounce'
|
||||
/**
|
||||
* Copyright (c) 2016-present, Nicolas Gallagher.
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import dismissKeyboard from '../../modules/dismissKeyboard'
|
||||
import invariant from 'fbjs/lib/invariant'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import ScrollResponder from '../../modules/ScrollResponder'
|
||||
import ScrollViewBase from './ScrollViewBase'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType'
|
||||
import View from '../View'
|
||||
import ViewStylePropTypes from '../View/ViewStylePropTypes'
|
||||
|
||||
const INNERVIEW = 'InnerScrollView'
|
||||
const SCROLLVIEW = 'ScrollView'
|
||||
|
||||
const ScrollView = React.createClass({
|
||||
propTypes: {
|
||||
...View.propTypes,
|
||||
children: View.propTypes.children,
|
||||
contentContainerStyle: StyleSheetPropType(ViewStylePropTypes),
|
||||
horizontal: PropTypes.bool,
|
||||
keyboardDismissMode: PropTypes.oneOf([ 'none', 'interactive', 'on-drag' ]),
|
||||
onContentSizeChange: PropTypes.func,
|
||||
onScroll: PropTypes.func,
|
||||
refreshControl: PropTypes.element,
|
||||
scrollEnabled: PropTypes.bool,
|
||||
scrollEventThrottle: PropTypes.number,
|
||||
style: StyleSheetPropType(ViewStylePropTypes)
|
||||
},
|
||||
|
||||
mixins: [ScrollResponder.Mixin],
|
||||
|
||||
getInitialState() {
|
||||
return this.scrollResponderMixinGetInitialState()
|
||||
},
|
||||
|
||||
setNativeProps(props: Object) {
|
||||
this.refs[SCROLLVIEW].setNativeProps(props)
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a reference to the underlying scroll responder, which supports
|
||||
* operations like `scrollTo`. All ScrollView-like components should
|
||||
* implement this method so that they can be composed while providing access
|
||||
* to the underlying scroll responder's methods.
|
||||
*/
|
||||
getScrollResponder(): Component {
|
||||
return this
|
||||
},
|
||||
|
||||
getScrollableNode(): any {
|
||||
return ReactDOM.findDOMNode(this.refs[SCROLLVIEW])
|
||||
},
|
||||
|
||||
getInnerViewNode(): any {
|
||||
return ReactDOM.findDOMNode(this.refs[INNERVIEW])
|
||||
},
|
||||
|
||||
/**
|
||||
* Scrolls to a given x, y offset, either immediately or with a smooth animation.
|
||||
* Syntax:
|
||||
*
|
||||
* scrollTo(options: {x: number = 0; y: number = 0; animated: boolean = true})
|
||||
*
|
||||
* Note: The weird argument signature is due to the fact that, for historical reasons,
|
||||
* the function also accepts separate arguments as as alternative to the options object.
|
||||
* This is deprecated due to ambiguity (y before x), and SHOULD NOT BE USED.
|
||||
*/
|
||||
scrollTo(
|
||||
y?: number | { x?: number, y?: number, animated?: boolean },
|
||||
x?: number,
|
||||
animated?: boolean
|
||||
) {
|
||||
if (typeof y === 'number') {
|
||||
console.warn('`scrollTo(y, x, animated)` is deprecated. Use `scrollTo({x: 5, y: 5, animated: true})` instead.')
|
||||
} else {
|
||||
({x, y, animated} = y || {})
|
||||
}
|
||||
|
||||
this.getScrollResponder().scrollResponderScrollTo({x: x || 0, y: y || 0, animated: animated !== false})
|
||||
},
|
||||
|
||||
/**
|
||||
* Deprecated, do not use.
|
||||
*/
|
||||
scrollWithoutAnimationTo(y: number = 0, x: number = 0) {
|
||||
console.warn('`scrollWithoutAnimationTo` is deprecated. Use `scrollTo` instead')
|
||||
this.scrollTo({x, y, animated: false})
|
||||
},
|
||||
|
||||
handleScroll(e: Object) {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (this.props.onScroll && !this.props.scrollEventThrottle) {
|
||||
console.log(
|
||||
'You specified `onScroll` on a <ScrollView> but not ' +
|
||||
'`scrollEventThrottle`. You will only receive one event. ' +
|
||||
'Using `16` you get all the events but be aware that it may ' +
|
||||
'cause frame drops, use a bigger number if you don\'t need as ' +
|
||||
'much precision.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.keyboardDismissMode === 'on-drag') {
|
||||
dismissKeyboard()
|
||||
}
|
||||
|
||||
this.scrollResponderHandleScroll(e)
|
||||
},
|
||||
|
||||
_handleContentOnLayout(e: Object) {
|
||||
const { width, height } = e.nativeEvent.layout
|
||||
this.props.onContentSizeChange && this.props.onContentSizeChange(width, height)
|
||||
},
|
||||
|
||||
render() {
|
||||
const scrollViewStyle = [
|
||||
styles.base,
|
||||
this.props.horizontal && styles.baseHorizontal
|
||||
]
|
||||
|
||||
const contentContainerStyle = [
|
||||
styles.contentContainer,
|
||||
this.props.horizontal && styles.contentContainerHorizontal,
|
||||
this.props.contentContainerStyle
|
||||
]
|
||||
|
||||
if (process.env.NODE_ENV !== 'production' && this.props.style) {
|
||||
const style = StyleSheet.flatten(this.props.style)
|
||||
const childLayoutProps = ['alignItems', 'justifyContent'].filter((prop) => style && style[prop] !== undefined)
|
||||
invariant(
|
||||
childLayoutProps.length === 0,
|
||||
'ScrollView child layout (' + JSON.stringify(childLayoutProps) +
|
||||
') must be applied through the contentContainerStyle prop.'
|
||||
)
|
||||
}
|
||||
|
||||
let contentSizeChangeProps = {}
|
||||
if (this.props.onContentSizeChange) {
|
||||
contentSizeChangeProps = {
|
||||
onLayout: this._handleContentOnLayout
|
||||
}
|
||||
}
|
||||
|
||||
const contentContainer = (
|
||||
<View
|
||||
{...contentSizeChangeProps}
|
||||
children={this.props.children}
|
||||
collapsable={false}
|
||||
ref={INNERVIEW}
|
||||
style={contentContainerStyle}
|
||||
/>
|
||||
)
|
||||
|
||||
const props = {
|
||||
...this.props,
|
||||
style: [scrollViewStyle, this.props.style],
|
||||
onTouchStart: this.scrollResponderHandleTouchStart,
|
||||
onTouchMove: this.scrollResponderHandleTouchMove,
|
||||
onTouchEnd: this.scrollResponderHandleTouchEnd,
|
||||
onScrollBeginDrag: this.scrollResponderHandleScrollBeginDrag,
|
||||
onScrollEndDrag: this.scrollResponderHandleScrollEndDrag,
|
||||
onMomentumScrollBegin: this.scrollResponderHandleMomentumScrollBegin,
|
||||
onMomentumScrollEnd: this.scrollResponderHandleMomentumScrollEnd,
|
||||
onStartShouldSetResponder: this.scrollResponderHandleStartShouldSetResponder,
|
||||
onStartShouldSetResponderCapture: this.scrollResponderHandleStartShouldSetResponderCapture,
|
||||
onScrollShouldSetResponder: this.scrollResponderHandleScrollShouldSetResponder,
|
||||
onScroll: this.handleScroll,
|
||||
onResponderGrant: this.scrollResponderHandleResponderGrant,
|
||||
onResponderTerminationRequest: this.scrollResponderHandleTerminationRequest,
|
||||
onResponderTerminate: this.scrollResponderHandleTerminate,
|
||||
onResponderRelease: this.scrollResponderHandleResponderRelease,
|
||||
onResponderReject: this.scrollResponderHandleResponderReject
|
||||
}
|
||||
|
||||
const ScrollViewClass = ScrollViewBase
|
||||
|
||||
invariant(
|
||||
ScrollViewClass !== undefined,
|
||||
'ScrollViewClass must not be undefined'
|
||||
)
|
||||
|
||||
var refreshControl = this.props.refreshControl
|
||||
if (refreshControl) {
|
||||
return React.cloneElement(
|
||||
refreshControl,
|
||||
{ style: props.style },
|
||||
<ScrollViewClass {...props} ref={SCROLLVIEW} style={styles.base}>
|
||||
{contentContainer}
|
||||
</ScrollViewClass>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollViewClass {...props} ref={SCROLLVIEW} style={props.style}>
|
||||
{contentContainer}
|
||||
</ScrollViewClass>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
initial: {
|
||||
base: {
|
||||
flex: 1,
|
||||
overflow: 'auto'
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto'
|
||||
},
|
||||
initialContentContainer: {
|
||||
baseHorizontal: {
|
||||
overflowX: 'auto',
|
||||
overflowY: 'hidden'
|
||||
},
|
||||
contentContainer: {
|
||||
flex: 1
|
||||
},
|
||||
row: {
|
||||
contentContainerHorizontal: {
|
||||
flexDirection: 'row'
|
||||
}
|
||||
})
|
||||
|
||||
export default class ScrollView extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.any,
|
||||
contentContainerStyle: View.propTypes.style,
|
||||
horizontal: PropTypes.bool,
|
||||
onScroll: PropTypes.func,
|
||||
scrollEnabled: PropTypes.bool,
|
||||
scrollEventThrottle: PropTypes.number,
|
||||
style: View.propTypes.style
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
contentContainerStyle: {},
|
||||
horizontal: false,
|
||||
scrollEnabled: true,
|
||||
scrollEventThrottle: 0,
|
||||
style: {}
|
||||
};
|
||||
|
||||
constructor(...args) {
|
||||
super(...args)
|
||||
this._debouncedOnScrollEnd = debounce(this._onScrollEnd, 100)
|
||||
this.state = {
|
||||
isScrolling: false
|
||||
}
|
||||
}
|
||||
|
||||
_onScroll(e) {
|
||||
const { scrollEventThrottle } = this.props
|
||||
const { isScrolling, scrollLastTick } = this.state
|
||||
|
||||
// A scroll happened, so the scroll bumps the debounce.
|
||||
this._debouncedOnScrollEnd(e)
|
||||
|
||||
if (isScrolling) {
|
||||
// Scroll last tick may have changed, check if we need to notify
|
||||
if (this._shouldEmitScrollEvent(scrollLastTick, scrollEventThrottle)) {
|
||||
this._onScrollTick(e)
|
||||
}
|
||||
} else {
|
||||
// Weren't scrolling, so we must have just started
|
||||
this._onScrollStart(e)
|
||||
}
|
||||
}
|
||||
|
||||
_onScrollStart() {
|
||||
this.setState({
|
||||
isScrolling: true,
|
||||
scrollLastTick: Date.now()
|
||||
})
|
||||
}
|
||||
|
||||
_onScrollTick(e) {
|
||||
const { onScroll } = this.props
|
||||
this.setState({
|
||||
scrollLastTick: Date.now()
|
||||
})
|
||||
if (onScroll) onScroll(e)
|
||||
}
|
||||
|
||||
_onScrollEnd(e) {
|
||||
const { onScroll } = this.props
|
||||
this.setState({
|
||||
isScrolling: false
|
||||
})
|
||||
if (onScroll) onScroll(e)
|
||||
}
|
||||
|
||||
_shouldEmitScrollEvent(lastTick, eventThrottle) {
|
||||
const timeSinceLastTick = Date.now() - lastTick
|
||||
return (eventThrottle > 0 && timeSinceLastTick >= (1000 / eventThrottle))
|
||||
}
|
||||
|
||||
_maybePreventScroll(e) {
|
||||
const { scrollEnabled } = this.props
|
||||
if (!scrollEnabled) e.preventDefault()
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
contentContainerStyle,
|
||||
horizontal,
|
||||
style
|
||||
} = this.props
|
||||
|
||||
return (
|
||||
<View
|
||||
_className='ScrollView'
|
||||
onScroll={(e) => this._onScroll(e)}
|
||||
onTouchMove={(e) => this._maybePreventScroll(e)}
|
||||
onWheel={(e) => this._maybePreventScroll(e)}
|
||||
style={[
|
||||
styles.initial,
|
||||
style
|
||||
]}
|
||||
>
|
||||
{children ? (
|
||||
<View
|
||||
children={children}
|
||||
style={[
|
||||
styles.initialContentContainer,
|
||||
contentContainerStyle,
|
||||
horizontal && styles.row
|
||||
]}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
}
|
||||
module.exports = ScrollView
|
||||
|
||||
@@ -23,7 +23,7 @@ import React, { Component, PropTypes } from 'react'
|
||||
* Typically, you will not need to use this component and should opt for normal
|
||||
* React reconciliation.
|
||||
*/
|
||||
export default class StaticContainer extends Component {
|
||||
class StaticContainer extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.any.isRequired,
|
||||
shouldUpdate: PropTypes.bool.isRequired
|
||||
@@ -38,3 +38,5 @@ export default class StaticContainer extends Component {
|
||||
return (child === null || child === false) ? null : React.Children.only(child)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StaticContainer
|
||||
|
||||
@@ -22,7 +22,7 @@ import { Component, PropTypes } from 'react'
|
||||
* React reconciliation.
|
||||
*/
|
||||
|
||||
export default class StaticRenderer extends Component {
|
||||
class StaticRenderer extends Component {
|
||||
static propTypes = {
|
||||
render: PropTypes.func.isRequired,
|
||||
shouldUpdate: PropTypes.bool.isRequired
|
||||
@@ -36,3 +36,5 @@ export default class StaticRenderer extends Component {
|
||||
return this.props.render()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StaticRenderer
|
||||
|
||||
@@ -5,7 +5,7 @@ import ViewStylePropTypes from '../View/ViewStylePropTypes'
|
||||
const { number, oneOf, oneOfType, string } = PropTypes
|
||||
const numberOrString = oneOfType([ number, string ])
|
||||
|
||||
export default {
|
||||
module.exports = {
|
||||
...ViewStylePropTypes,
|
||||
color: ColorPropType,
|
||||
fontFamily: string,
|
||||
@@ -15,14 +15,17 @@ export default {
|
||||
letterSpacing: numberOrString,
|
||||
lineHeight: numberOrString,
|
||||
textAlign: oneOf([ 'center', 'inherit', 'justify', 'justify-all', 'left', 'right' ]),
|
||||
/**
|
||||
* @platform web
|
||||
*/
|
||||
textDecoration: string,
|
||||
textAlignVertical: oneOf([ 'auto', 'bottom', 'center', 'top' ]),
|
||||
textDecorationLine: string,
|
||||
/* @platform web */
|
||||
textOverflow: string,
|
||||
/* @platform web */
|
||||
textShadow: string,
|
||||
/* @platform web */
|
||||
textTransform: oneOf([ 'capitalize', 'lowercase', 'none', 'uppercase' ]),
|
||||
/* @platform web */
|
||||
whiteSpace: string,
|
||||
/* @platform web */
|
||||
wordWrap: string,
|
||||
writingDirection: string
|
||||
writingDirection: oneOf([ 'auto', 'ltr', 'rtl' ])
|
||||
}
|
||||
|
||||
@@ -1,28 +1,12 @@
|
||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
||||
import CoreComponent from '../CoreComponent'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType'
|
||||
import TextStylePropTypes from './TextStylePropTypes'
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
initial: {
|
||||
color: 'inherit',
|
||||
display: 'inline',
|
||||
font: 'inherit',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
textDecoration: 'none',
|
||||
wordWrap: 'break-word'
|
||||
},
|
||||
singleLineStyle: {
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}
|
||||
})
|
||||
|
||||
export default class Text extends Component {
|
||||
@NativeMethodsDecorator
|
||||
class Text extends Component {
|
||||
static propTypes = {
|
||||
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
|
||||
accessibilityRole: CoreComponent.propTypes.accessibilityRole,
|
||||
@@ -64,3 +48,23 @@ export default class Text extends Component {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
initial: {
|
||||
color: 'inherit',
|
||||
display: 'inline',
|
||||
font: 'inherit',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
textDecorationLine: 'none',
|
||||
wordWrap: 'break-word'
|
||||
},
|
||||
singleLineStyle: {
|
||||
maxWidth: '100%',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap'
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = Text
|
||||
|
||||
55
src/components/TextInput/TextInputState.js
Normal file
55
src/components/TextInput/TextInputState.js
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Copyright (c) 2016-present, Nicolas Gallagher.
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import UIManager from '../../apis/UIManager'
|
||||
|
||||
/**
|
||||
* This class is responsible for coordinating the "focused"
|
||||
* state for TextInputs. All calls relating to the keyboard
|
||||
* should be funneled through here
|
||||
*/
|
||||
const TextInputState = {
|
||||
/**
|
||||
* Internal state
|
||||
*/
|
||||
_currentlyFocusedNode: (null: ?Object),
|
||||
|
||||
/**
|
||||
* Returns the ID of the currently focused text field, if one exists
|
||||
* If no text field is focused it returns null
|
||||
*/
|
||||
currentlyFocusedField(): ?Object {
|
||||
return this._currentlyFocusedNode
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} TextInputID id of the text field to focus
|
||||
* Focuses the specified text field
|
||||
* noop if the text field was already focused
|
||||
*/
|
||||
focusTextInput(textFieldNode: ?Object) {
|
||||
if (this._currentlyFocusedNode !== textFieldNode && textFieldNode !== null) {
|
||||
this._currentlyFocusedNode = textFieldNode
|
||||
UIManager.focus(textFieldNode)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} textFieldNode id of the text field to focus
|
||||
* Unfocuses the specified text field
|
||||
* noop if it wasn't focused
|
||||
*/
|
||||
blurTextInput(textFieldNode: ?Object) {
|
||||
if (this._currentlyFocusedNode === textFieldNode && textFieldNode !== null) {
|
||||
this._currentlyFocusedNode = null
|
||||
UIManager.blur(textFieldNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TextInputState
|
||||
@@ -1,17 +1,15 @@
|
||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
||||
import CoreComponent from '../CoreComponent'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
import Text from '../Text'
|
||||
import TextareaAutosize from 'react-textarea-autosize'
|
||||
import TextInputState from './TextInputState'
|
||||
import View from '../View'
|
||||
|
||||
export default class TextInput extends Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = { showPlaceholder: !props.value && !props.defaultValue }
|
||||
}
|
||||
|
||||
@NativeMethodsDecorator
|
||||
class TextInput extends Component {
|
||||
static propTypes = {
|
||||
...View.propTypes,
|
||||
autoComplete: PropTypes.bool,
|
||||
@@ -47,29 +45,57 @@ export default class TextInput extends Component {
|
||||
style: {}
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = { showPlaceholder: !props.value && !props.defaultValue }
|
||||
}
|
||||
|
||||
blur() {
|
||||
TextInputState.blurTextInput(ReactDOM.findDOMNode(this.refs.input))
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.setNativeProps({ text: '' })
|
||||
}
|
||||
|
||||
focus() {
|
||||
TextInputState.focusTextInput(ReactDOM.findDOMNode(this.refs.input))
|
||||
}
|
||||
|
||||
setNativeProps(props) {
|
||||
this.refs.input.setNativeProps(props)
|
||||
}
|
||||
|
||||
_onBlur(e) {
|
||||
const { onBlur } = this.props
|
||||
const value = e.target.value
|
||||
this.setState({ showPlaceholder: value === '' })
|
||||
const text = e.target.value
|
||||
this.setState({ showPlaceholder: text === '' })
|
||||
this.blur()
|
||||
if (onBlur) onBlur(e)
|
||||
}
|
||||
|
||||
_onChange(e) {
|
||||
const { onChange, onChangeText } = this.props
|
||||
const value = e.target.value
|
||||
this.setState({ showPlaceholder: value === '' })
|
||||
if (onChangeText) onChangeText(value)
|
||||
const text = e.target.value
|
||||
this.setState({ showPlaceholder: text === '' })
|
||||
if (onChange) onChange(e)
|
||||
if (onChangeText) onChangeText(text)
|
||||
if (!this.refs.input) {
|
||||
// calling `this.props.onChange` or `this.props.onChangeText`
|
||||
// may clean up the input itself. Exits here.
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_onFocus(e) {
|
||||
const { clearTextOnFocus, onFocus, selectTextOnFocus } = this.props
|
||||
const node = ReactDOM.findDOMNode(this.refs.input)
|
||||
const value = e.target.value
|
||||
if (clearTextOnFocus) node.value = ''
|
||||
if (selectTextOnFocus) node.select()
|
||||
this.setState({ showPlaceholder: value === '' })
|
||||
const text = e.target.value
|
||||
this.focus()
|
||||
if (onFocus) onFocus(e)
|
||||
if (clearTextOnFocus) this.clear()
|
||||
if (selectTextOnFocus) node.select()
|
||||
this.setState({ showPlaceholder: text === '' })
|
||||
}
|
||||
|
||||
_onSelectionChange(e) {
|
||||
@@ -215,3 +241,5 @@ const styles = StyleSheet.create({
|
||||
whiteSpace: 'pre'
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = TextInput
|
||||
|
||||
43
src/components/Touchable/BoundingDimensions.js
Normal file
43
src/components/Touchable/BoundingDimensions.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var PooledClass = require('react/lib/PooledClass');
|
||||
|
||||
var twoArgumentPooler = PooledClass.twoArgumentPooler;
|
||||
|
||||
/**
|
||||
* PooledClass representing the bounding rectangle of a region.
|
||||
*
|
||||
* @param {number} width Width of bounding rectangle.
|
||||
* @param {number} height Height of bounding rectangle.
|
||||
* @constructor BoundingDimensions
|
||||
*/
|
||||
function BoundingDimensions(width, height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
BoundingDimensions.prototype.destructor = function() {
|
||||
this.width = null;
|
||||
this.height = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} element Element to return `BoundingDimensions` for.
|
||||
* @return {BoundingDimensions} Bounding dimensions of `element`.
|
||||
*/
|
||||
BoundingDimensions.getPooledFromElement = function(element) {
|
||||
return BoundingDimensions.getPooled(
|
||||
element.offsetWidth,
|
||||
element.offsetHeight
|
||||
);
|
||||
};
|
||||
|
||||
PooledClass.addPoolingTo(BoundingDimensions, twoArgumentPooler);
|
||||
|
||||
module.exports = BoundingDimensions;
|
||||
33
src/components/Touchable/Position.js
Normal file
33
src/components/Touchable/Position.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var PooledClass = require('react/lib/PooledClass');
|
||||
|
||||
var twoArgumentPooler = PooledClass.twoArgumentPooler;
|
||||
|
||||
/**
|
||||
* Position does not expose methods for construction via an `HTMLDOMElement`,
|
||||
* because it isn't meaningful to construct such a thing without first defining
|
||||
* a frame of reference.
|
||||
*
|
||||
* @param {number} windowStartKey Key that window starts at.
|
||||
* @param {number} windowEndKey Key that window ends at.
|
||||
*/
|
||||
function Position(left, top) {
|
||||
this.left = left;
|
||||
this.top = top;
|
||||
}
|
||||
|
||||
Position.prototype.destructor = function() {
|
||||
this.left = null;
|
||||
this.top = null;
|
||||
};
|
||||
|
||||
PooledClass.addPoolingTo(Position, twoArgumentPooler);
|
||||
|
||||
module.exports = Position;
|
||||
723
src/components/Touchable/Touchable.js
Normal file
723
src/components/Touchable/Touchable.js
Normal file
@@ -0,0 +1,723 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/* @edit start */
|
||||
var BoundingDimensions = require('./BoundingDimensions');
|
||||
var Position = require('./Position');
|
||||
var TouchEventUtils = require('fbjs/lib/TouchEventUtils');
|
||||
var keyMirror = require('fbjs/lib/keyMirror');
|
||||
var UIManager = require('../../apis/UIManager');
|
||||
/* @edit end */
|
||||
|
||||
/**
|
||||
* `Touchable`: Taps done right.
|
||||
*
|
||||
* You hook your `ResponderEventPlugin` events into `Touchable`. `Touchable`
|
||||
* will measure time/geometry and tells you when to give feedback to the user.
|
||||
*
|
||||
* ====================== Touchable Tutorial ===============================
|
||||
* The `Touchable` mixin helps you handle the "press" interaction. It analyzes
|
||||
* the geometry of elements, and observes when another responder (scroll view
|
||||
* etc) has stolen the touch lock. It notifies your component when it should
|
||||
* give feedback to the user. (bouncing/highlighting/unhighlighting).
|
||||
*
|
||||
* - When a touch was activated (typically you highlight)
|
||||
* - When a touch was deactivated (typically you unhighlight)
|
||||
* - When a touch was "pressed" - a touch ended while still within the geometry
|
||||
* of the element, and no other element (like scroller) has "stolen" touch
|
||||
* lock ("responder") (Typically you bounce the element).
|
||||
*
|
||||
* A good tap interaction isn't as simple as you might think. There should be a
|
||||
* slight delay before showing a highlight when starting a touch. If a
|
||||
* subsequent touch move exceeds the boundary of the element, it should
|
||||
* unhighlight, but if that same touch is brought back within the boundary, it
|
||||
* should rehighlight again. A touch can move in and out of that boundary
|
||||
* several times, each time toggling highlighting, but a "press" is only
|
||||
* triggered if that touch ends while within the element's boundary and no
|
||||
* scroller (or anything else) has stolen the lock on touches.
|
||||
*
|
||||
* To create a new type of component that handles interaction using the
|
||||
* `Touchable` mixin, do the following:
|
||||
*
|
||||
* - Initialize the `Touchable` state.
|
||||
*
|
||||
* getInitialState: function() {
|
||||
* return merge(this.touchableGetInitialState(), yourComponentState);
|
||||
* }
|
||||
*
|
||||
* - Choose the rendered component who's touches should start the interactive
|
||||
* sequence. On that rendered node, forward all `Touchable` responder
|
||||
* handlers. You can choose any rendered node you like. Choose a node whose
|
||||
* hit target you'd like to instigate the interaction sequence:
|
||||
*
|
||||
* // In render function:
|
||||
* return (
|
||||
* <View
|
||||
* onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
|
||||
* onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}
|
||||
* onResponderGrant={this.touchableHandleResponderGrant}
|
||||
* onResponderMove={this.touchableHandleResponderMove}
|
||||
* onResponderRelease={this.touchableHandleResponderRelease}
|
||||
* onResponderTerminate={this.touchableHandleResponderTerminate}>
|
||||
* <View>
|
||||
* Even though the hit detection/interactions are triggered by the
|
||||
* wrapping (typically larger) node, we usually end up implementing
|
||||
* custom logic that highlights this inner one.
|
||||
* </View>
|
||||
* </View>
|
||||
* );
|
||||
*
|
||||
* - You may set up your own handlers for each of these events, so long as you
|
||||
* also invoke the `touchable*` handlers inside of your custom handler.
|
||||
*
|
||||
* - Implement the handlers on your component class in order to provide
|
||||
* feedback to the user. See documentation for each of these class methods
|
||||
* that you should implement.
|
||||
*
|
||||
* touchableHandlePress: function() {
|
||||
* this.performBounceAnimation(); // or whatever you want to do.
|
||||
* },
|
||||
* touchableHandleActivePressIn: function() {
|
||||
* this.beginHighlighting(...); // Whatever you like to convey activation
|
||||
* },
|
||||
* touchableHandleActivePressOut: function() {
|
||||
* this.endHighlighting(...); // Whatever you like to convey deactivation
|
||||
* },
|
||||
*
|
||||
* - There are more advanced methods you can implement (see documentation below):
|
||||
* touchableGetHighlightDelayMS: function() {
|
||||
* return 20;
|
||||
* }
|
||||
* // In practice, *always* use a predeclared constant (conserve memory).
|
||||
* touchableGetPressRectOffset: function() {
|
||||
* return {top: 20, left: 20, right: 20, bottom: 100};
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* Touchable states.
|
||||
*/
|
||||
var States = keyMirror({
|
||||
NOT_RESPONDER: null, // Not the responder
|
||||
RESPONDER_INACTIVE_PRESS_IN: null, // Responder, inactive, in the `PressRect`
|
||||
RESPONDER_INACTIVE_PRESS_OUT: null, // Responder, inactive, out of `PressRect`
|
||||
RESPONDER_ACTIVE_PRESS_IN: null, // Responder, active, in the `PressRect`
|
||||
RESPONDER_ACTIVE_PRESS_OUT: null, // Responder, active, out of `PressRect`
|
||||
RESPONDER_ACTIVE_LONG_PRESS_IN: null, // Responder, active, in the `PressRect`, after long press threshold
|
||||
RESPONDER_ACTIVE_LONG_PRESS_OUT: null, // Responder, active, out of `PressRect`, after long press threshold
|
||||
ERROR: null
|
||||
});
|
||||
|
||||
/**
|
||||
* Quick lookup map for states that are considered to be "active"
|
||||
*/
|
||||
var IsActive = {
|
||||
RESPONDER_ACTIVE_PRESS_OUT: true,
|
||||
RESPONDER_ACTIVE_PRESS_IN: true
|
||||
};
|
||||
|
||||
/**
|
||||
* Quick lookup for states that are considered to be "pressing" and are
|
||||
* therefore eligible to result in a "selection" if the press stops.
|
||||
*/
|
||||
var IsPressingIn = {
|
||||
RESPONDER_INACTIVE_PRESS_IN: true,
|
||||
RESPONDER_ACTIVE_PRESS_IN: true,
|
||||
RESPONDER_ACTIVE_LONG_PRESS_IN: true,
|
||||
};
|
||||
|
||||
var IsLongPressingIn = {
|
||||
RESPONDER_ACTIVE_LONG_PRESS_IN: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Inputs to the state machine.
|
||||
*/
|
||||
var Signals = keyMirror({
|
||||
DELAY: null,
|
||||
RESPONDER_GRANT: null,
|
||||
RESPONDER_RELEASE: null,
|
||||
RESPONDER_TERMINATED: null,
|
||||
ENTER_PRESS_RECT: null,
|
||||
LEAVE_PRESS_RECT: null,
|
||||
LONG_PRESS_DETECTED: null,
|
||||
});
|
||||
|
||||
/**
|
||||
* Mapping from States x Signals => States
|
||||
*/
|
||||
var Transitions = {
|
||||
NOT_RESPONDER: {
|
||||
DELAY: States.ERROR,
|
||||
RESPONDER_GRANT: States.RESPONDER_INACTIVE_PRESS_IN,
|
||||
RESPONDER_RELEASE: States.ERROR,
|
||||
RESPONDER_TERMINATED: States.ERROR,
|
||||
ENTER_PRESS_RECT: States.ERROR,
|
||||
LEAVE_PRESS_RECT: States.ERROR,
|
||||
LONG_PRESS_DETECTED: States.ERROR,
|
||||
},
|
||||
RESPONDER_INACTIVE_PRESS_IN: {
|
||||
DELAY: States.RESPONDER_ACTIVE_PRESS_IN,
|
||||
RESPONDER_GRANT: States.ERROR,
|
||||
RESPONDER_RELEASE: States.NOT_RESPONDER,
|
||||
RESPONDER_TERMINATED: States.NOT_RESPONDER,
|
||||
ENTER_PRESS_RECT: States.RESPONDER_INACTIVE_PRESS_IN,
|
||||
LEAVE_PRESS_RECT: States.RESPONDER_INACTIVE_PRESS_OUT,
|
||||
LONG_PRESS_DETECTED: States.ERROR,
|
||||
},
|
||||
RESPONDER_INACTIVE_PRESS_OUT: {
|
||||
DELAY: States.RESPONDER_ACTIVE_PRESS_OUT,
|
||||
RESPONDER_GRANT: States.ERROR,
|
||||
RESPONDER_RELEASE: States.NOT_RESPONDER,
|
||||
RESPONDER_TERMINATED: States.NOT_RESPONDER,
|
||||
ENTER_PRESS_RECT: States.RESPONDER_INACTIVE_PRESS_IN,
|
||||
LEAVE_PRESS_RECT: States.RESPONDER_INACTIVE_PRESS_OUT,
|
||||
LONG_PRESS_DETECTED: States.ERROR,
|
||||
},
|
||||
RESPONDER_ACTIVE_PRESS_IN: {
|
||||
DELAY: States.ERROR,
|
||||
RESPONDER_GRANT: States.ERROR,
|
||||
RESPONDER_RELEASE: States.NOT_RESPONDER,
|
||||
RESPONDER_TERMINATED: States.NOT_RESPONDER,
|
||||
ENTER_PRESS_RECT: States.RESPONDER_ACTIVE_PRESS_IN,
|
||||
LEAVE_PRESS_RECT: States.RESPONDER_ACTIVE_PRESS_OUT,
|
||||
LONG_PRESS_DETECTED: States.RESPONDER_ACTIVE_LONG_PRESS_IN,
|
||||
},
|
||||
RESPONDER_ACTIVE_PRESS_OUT: {
|
||||
DELAY: States.ERROR,
|
||||
RESPONDER_GRANT: States.ERROR,
|
||||
RESPONDER_RELEASE: States.NOT_RESPONDER,
|
||||
RESPONDER_TERMINATED: States.NOT_RESPONDER,
|
||||
ENTER_PRESS_RECT: States.RESPONDER_ACTIVE_PRESS_IN,
|
||||
LEAVE_PRESS_RECT: States.RESPONDER_ACTIVE_PRESS_OUT,
|
||||
LONG_PRESS_DETECTED: States.ERROR,
|
||||
},
|
||||
RESPONDER_ACTIVE_LONG_PRESS_IN: {
|
||||
DELAY: States.ERROR,
|
||||
RESPONDER_GRANT: States.ERROR,
|
||||
RESPONDER_RELEASE: States.NOT_RESPONDER,
|
||||
RESPONDER_TERMINATED: States.NOT_RESPONDER,
|
||||
ENTER_PRESS_RECT: States.RESPONDER_ACTIVE_LONG_PRESS_IN,
|
||||
LEAVE_PRESS_RECT: States.RESPONDER_ACTIVE_LONG_PRESS_OUT,
|
||||
LONG_PRESS_DETECTED: States.RESPONDER_ACTIVE_LONG_PRESS_IN,
|
||||
},
|
||||
RESPONDER_ACTIVE_LONG_PRESS_OUT: {
|
||||
DELAY: States.ERROR,
|
||||
RESPONDER_GRANT: States.ERROR,
|
||||
RESPONDER_RELEASE: States.NOT_RESPONDER,
|
||||
RESPONDER_TERMINATED: States.NOT_RESPONDER,
|
||||
ENTER_PRESS_RECT: States.RESPONDER_ACTIVE_LONG_PRESS_IN,
|
||||
LEAVE_PRESS_RECT: States.RESPONDER_ACTIVE_LONG_PRESS_OUT,
|
||||
LONG_PRESS_DETECTED: States.ERROR,
|
||||
},
|
||||
error: {
|
||||
DELAY: States.NOT_RESPONDER,
|
||||
RESPONDER_GRANT: States.RESPONDER_INACTIVE_PRESS_IN,
|
||||
RESPONDER_RELEASE: States.NOT_RESPONDER,
|
||||
RESPONDER_TERMINATED: States.NOT_RESPONDER,
|
||||
ENTER_PRESS_RECT: States.NOT_RESPONDER,
|
||||
LEAVE_PRESS_RECT: States.NOT_RESPONDER,
|
||||
LONG_PRESS_DETECTED: States.NOT_RESPONDER,
|
||||
}
|
||||
};
|
||||
|
||||
// ==== Typical Constants for integrating into UI components ====
|
||||
// var HIT_EXPAND_PX = 20;
|
||||
// var HIT_VERT_OFFSET_PX = 10;
|
||||
var HIGHLIGHT_DELAY_MS = 130;
|
||||
|
||||
var PRESS_EXPAND_PX = 20;
|
||||
|
||||
var LONG_PRESS_THRESHOLD = 500;
|
||||
|
||||
var LONG_PRESS_DELAY_MS = LONG_PRESS_THRESHOLD - HIGHLIGHT_DELAY_MS;
|
||||
|
||||
var LONG_PRESS_ALLOWED_MOVEMENT = 10;
|
||||
|
||||
// Default amount "active" region protrudes beyond box
|
||||
|
||||
/**
|
||||
* By convention, methods prefixed with underscores are meant to be @private,
|
||||
* and not @protected. Mixers shouldn't access them - not even to provide them
|
||||
* as callback handlers.
|
||||
*
|
||||
*
|
||||
* ========== Geometry =========
|
||||
* `Touchable` only assumes that there exists a `HitRect` node. The `PressRect`
|
||||
* is an abstract box that is extended beyond the `HitRect`.
|
||||
*
|
||||
* +--------------------------+
|
||||
* | | - "Start" events in `HitRect` cause `HitRect`
|
||||
* | +--------------------+ | to become the responder.
|
||||
* | | +--------------+ | | - `HitRect` is typically expanded around
|
||||
* | | | | | | the `VisualRect`, but shifted downward.
|
||||
* | | | VisualRect | | | - After pressing down, after some delay,
|
||||
* | | | | | | and before letting up, the Visual React
|
||||
* | | +--------------+ | | will become "active". This makes it eligible
|
||||
* | | HitRect | | for being highlighted (so long as the
|
||||
* | +--------------------+ | press remains in the `PressRect`).
|
||||
* | PressRect o |
|
||||
* +----------------------|---+
|
||||
* Out Region |
|
||||
* +-----+ This gap between the `HitRect` and
|
||||
* `PressRect` allows a touch to move far away
|
||||
* from the original hit rect, and remain
|
||||
* highlighted, and eligible for a "Press".
|
||||
* Customize this via
|
||||
* `touchableGetPressRectOffset()`.
|
||||
*
|
||||
*
|
||||
*
|
||||
* ======= State Machine =======
|
||||
*
|
||||
* +-------------+ <---+ RESPONDER_RELEASE
|
||||
* |NOT_RESPONDER|
|
||||
* +-------------+ <---+ RESPONDER_TERMINATED
|
||||
* +
|
||||
* | RESPONDER_GRANT (HitRect)
|
||||
* v
|
||||
* +---------------------------+ DELAY +-------------------------+ T + DELAY +------------------------------+
|
||||
* |RESPONDER_INACTIVE_PRESS_IN|+-------->|RESPONDER_ACTIVE_PRESS_IN| +------------> |RESPONDER_ACTIVE_LONG_PRESS_IN|
|
||||
* +---------------------------+ +-------------------------+ +------------------------------+
|
||||
* + ^ + ^ + ^
|
||||
* |LEAVE_ |ENTER_ |LEAVE_ |ENTER_ |LEAVE_ |ENTER_
|
||||
* |PRESS_RECT |PRESS_RECT |PRESS_RECT |PRESS_RECT |PRESS_RECT |PRESS_RECT
|
||||
* | | | | | |
|
||||
* v + v + v +
|
||||
* +----------------------------+ DELAY +--------------------------+ +-------------------------------+
|
||||
* |RESPONDER_INACTIVE_PRESS_OUT|+------->|RESPONDER_ACTIVE_PRESS_OUT| |RESPONDER_ACTIVE_LONG_PRESS_OUT|
|
||||
* +----------------------------+ +--------------------------+ +-------------------------------+
|
||||
*
|
||||
* T + DELAY => LONG_PRESS_DELAY_MS + DELAY
|
||||
*
|
||||
* Not drawn are the side effects of each transition. The most important side
|
||||
* effect is the `touchableHandlePress` abstract method invocation that occurs
|
||||
* when a responder is released while in either of the "Press" states.
|
||||
*
|
||||
* The other important side effects are the highlight abstract method
|
||||
* invocations (internal callbacks) to be implemented by the mixer.
|
||||
*
|
||||
*
|
||||
* @lends Touchable.prototype
|
||||
*/
|
||||
var TouchableMixin = {
|
||||
/**
|
||||
* Clear all timeouts on unmount
|
||||
*/
|
||||
componentWillUnmount: function() {
|
||||
this.touchableDelayTimeout && clearTimeout(this.touchableDelayTimeout);
|
||||
this.longPressDelayTimeout && clearTimeout(this.longPressDelayTimeout);
|
||||
this.pressOutDelayTimeout && clearTimeout(this.pressOutDelayTimeout);
|
||||
},
|
||||
|
||||
/**
|
||||
* It's prefer that mixins determine state in this way, having the class
|
||||
* explicitly mix the state in the one and only `getInitialState` method.
|
||||
*
|
||||
* @return {object} State object to be placed inside of
|
||||
* `this.state.touchable`.
|
||||
*/
|
||||
touchableGetInitialState: function() {
|
||||
return {
|
||||
touchable: {touchState: undefined, responderID: null}
|
||||
};
|
||||
},
|
||||
|
||||
// ==== Hooks to Gesture Responder system ====
|
||||
/**
|
||||
* Must return true if embedded in a native platform scroll view.
|
||||
*/
|
||||
touchableHandleResponderTerminationRequest: function() {
|
||||
return !this.props.rejectResponderTermination;
|
||||
},
|
||||
|
||||
/**
|
||||
* Must return true to start the process of `Touchable`.
|
||||
*/
|
||||
touchableHandleStartShouldSetResponder: function() {
|
||||
return !this.props.disabled;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return true to cancel press on long press.
|
||||
*/
|
||||
touchableLongPressCancelsPress: function () {
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Place as callback for a DOM element's `onResponderGrant` event.
|
||||
* @param {SyntheticEvent} e Synthetic event from event system.
|
||||
* @param {string} dispatchID ID of node that e was dispatched to.
|
||||
*
|
||||
*/
|
||||
touchableHandleResponderGrant: function(e, dispatchID) {
|
||||
// Since e is used in a callback invoked on another event loop
|
||||
// (as in setTimeout etc), we need to call e.persist() on the
|
||||
// event to make sure it doesn't get reused in the event object pool.
|
||||
e.persist();
|
||||
|
||||
this.pressOutDelayTimeout && clearTimeout(this.pressOutDelayTimeout);
|
||||
this.pressOutDelayTimeout = null;
|
||||
|
||||
this.state.touchable.touchState = States.NOT_RESPONDER;
|
||||
this.state.touchable.responderID = dispatchID;
|
||||
this._receiveSignal(Signals.RESPONDER_GRANT, e);
|
||||
var delayMS =
|
||||
this.touchableGetHighlightDelayMS !== undefined ?
|
||||
Math.max(this.touchableGetHighlightDelayMS(), 0) : HIGHLIGHT_DELAY_MS;
|
||||
delayMS = isNaN(delayMS) ? HIGHLIGHT_DELAY_MS : delayMS;
|
||||
if (delayMS !== 0) {
|
||||
this.touchableDelayTimeout = setTimeout(
|
||||
this._handleDelay.bind(this, e),
|
||||
delayMS
|
||||
);
|
||||
} else {
|
||||
this._handleDelay(e);
|
||||
}
|
||||
|
||||
var longDelayMS =
|
||||
this.touchableGetLongPressDelayMS !== undefined ?
|
||||
Math.max(this.touchableGetLongPressDelayMS(), 10) : LONG_PRESS_DELAY_MS;
|
||||
longDelayMS = isNaN(longDelayMS) ? LONG_PRESS_DELAY_MS : longDelayMS;
|
||||
this.longPressDelayTimeout = setTimeout(
|
||||
this._handleLongDelay.bind(this, e),
|
||||
longDelayMS + delayMS
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Place as callback for a DOM element's `onResponderRelease` event.
|
||||
*/
|
||||
touchableHandleResponderRelease: function(e) {
|
||||
this._receiveSignal(Signals.RESPONDER_RELEASE, e);
|
||||
},
|
||||
|
||||
/**
|
||||
* Place as callback for a DOM element's `onResponderTerminate` event.
|
||||
*/
|
||||
touchableHandleResponderTerminate: function(e) {
|
||||
this._receiveSignal(Signals.RESPONDER_TERMINATED, e);
|
||||
},
|
||||
|
||||
/**
|
||||
* Place as callback for a DOM element's `onResponderMove` event.
|
||||
*/
|
||||
touchableHandleResponderMove: function(e) {
|
||||
// Not enough time elapsed yet, wait for highlight -
|
||||
// this is just a perf optimization.
|
||||
if (this.state.touchable.touchState === States.RESPONDER_INACTIVE_PRESS_IN) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Measurement may not have returned yet.
|
||||
if (!this.state.touchable.positionOnActivate) {
|
||||
return;
|
||||
}
|
||||
|
||||
var positionOnActivate = this.state.touchable.positionOnActivate;
|
||||
var dimensionsOnActivate = this.state.touchable.dimensionsOnActivate;
|
||||
var pressRectOffset = this.touchableGetPressRectOffset ?
|
||||
this.touchableGetPressRectOffset() : {
|
||||
left: PRESS_EXPAND_PX,
|
||||
right: PRESS_EXPAND_PX,
|
||||
top: PRESS_EXPAND_PX,
|
||||
bottom: PRESS_EXPAND_PX
|
||||
};
|
||||
|
||||
var pressExpandLeft = pressRectOffset.left;
|
||||
var pressExpandTop = pressRectOffset.top;
|
||||
var pressExpandRight = pressRectOffset.right;
|
||||
var pressExpandBottom = pressRectOffset.bottom;
|
||||
|
||||
var hitSlop = this.touchableGetHitSlop ?
|
||||
this.touchableGetHitSlop() : null;
|
||||
|
||||
if (hitSlop) {
|
||||
pressExpandLeft += hitSlop.left;
|
||||
pressExpandTop += hitSlop.top;
|
||||
pressExpandRight += hitSlop.right;
|
||||
pressExpandBottom += hitSlop.bottom;
|
||||
}
|
||||
|
||||
var touch = TouchEventUtils.extractSingleTouch(e.nativeEvent);
|
||||
var pageX = touch && touch.pageX;
|
||||
var pageY = touch && touch.pageY;
|
||||
|
||||
if (this.pressInLocation) {
|
||||
var movedDistance = this._getDistanceBetweenPoints(pageX, pageY, this.pressInLocation.pageX, this.pressInLocation.pageY);
|
||||
if (movedDistance > LONG_PRESS_ALLOWED_MOVEMENT) {
|
||||
this._cancelLongPressDelayTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
var isTouchWithinActive =
|
||||
pageX > positionOnActivate.left - pressExpandLeft &&
|
||||
pageY > positionOnActivate.top - pressExpandTop &&
|
||||
pageX <
|
||||
positionOnActivate.left +
|
||||
dimensionsOnActivate.width +
|
||||
pressExpandRight &&
|
||||
pageY <
|
||||
positionOnActivate.top +
|
||||
dimensionsOnActivate.height +
|
||||
pressExpandBottom;
|
||||
if (isTouchWithinActive) {
|
||||
this._receiveSignal(Signals.ENTER_PRESS_RECT, e);
|
||||
var curState = this.state.touchable.touchState;
|
||||
if (curState === States.RESPONDER_INACTIVE_PRESS_IN) {
|
||||
// fix for t7967420
|
||||
this._cancelLongPressDelayTimeout();
|
||||
}
|
||||
} else {
|
||||
this._cancelLongPressDelayTimeout();
|
||||
this._receiveSignal(Signals.LEAVE_PRESS_RECT, e);
|
||||
}
|
||||
},
|
||||
|
||||
// ==== Abstract Application Callbacks ====
|
||||
|
||||
/**
|
||||
* Invoked when the item should be highlighted. Mixers should implement this
|
||||
* to visually distinguish the `VisualRect` so that the user knows that
|
||||
* releasing a touch will result in a "selection" (analog to click).
|
||||
*
|
||||
* @abstract
|
||||
* touchableHandleActivePressIn: function,
|
||||
*/
|
||||
|
||||
/**
|
||||
* Invoked when the item is "active" (in that it is still eligible to become
|
||||
* a "select") but the touch has left the `PressRect`. Usually the mixer will
|
||||
* want to unhighlight the `VisualRect`. If the user (while pressing) moves
|
||||
* back into the `PressRect` `touchableHandleActivePressIn` will be invoked
|
||||
* again and the mixer should probably highlight the `VisualRect` again. This
|
||||
* event will not fire on an `touchEnd/mouseUp` event, only move events while
|
||||
* the user is depressing the mouse/touch.
|
||||
*
|
||||
* @abstract
|
||||
* touchableHandleActivePressOut: function
|
||||
*/
|
||||
|
||||
/**
|
||||
* Invoked when the item is "selected" - meaning the interaction ended by
|
||||
* letting up while the item was either in the state
|
||||
* `RESPONDER_ACTIVE_PRESS_IN` or `RESPONDER_INACTIVE_PRESS_IN`.
|
||||
*
|
||||
* @abstract
|
||||
* touchableHandlePress: function
|
||||
*/
|
||||
|
||||
/**
|
||||
* Invoked when the item is long pressed - meaning the interaction ended by
|
||||
* letting up while the item was in `RESPONDER_ACTIVE_LONG_PRESS_IN`. If
|
||||
* `touchableHandleLongPress` is *not* provided, `touchableHandlePress` will
|
||||
* be called as it normally is. If `touchableHandleLongPress` is provided, by
|
||||
* default any `touchableHandlePress` callback will not be invoked. To
|
||||
* override this default behavior, override `touchableLongPressCancelsPress`
|
||||
* to return false. As a result, `touchableHandlePress` will be called when
|
||||
* lifting up, even if `touchableHandleLongPress` has also been called.
|
||||
*
|
||||
* @abstract
|
||||
* touchableHandleLongPress: function
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the number of millis to wait before triggering a highlight.
|
||||
*
|
||||
* @abstract
|
||||
* touchableGetHighlightDelayMS: function
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns the amount to extend the `HitRect` into the `PressRect`. Positive
|
||||
* numbers mean the size expands outwards.
|
||||
*
|
||||
* @abstract
|
||||
* touchableGetPressRectOffset: function
|
||||
*/
|
||||
|
||||
|
||||
|
||||
// ==== Internal Logic ====
|
||||
|
||||
/**
|
||||
* Measures the `HitRect` node on activation. The Bounding rectangle is with
|
||||
* respect to viewport - not page, so adding the `pageXOffset/pageYOffset`
|
||||
* should result in points that are in the same coordinate system as an
|
||||
* event's `globalX/globalY` data values.
|
||||
*
|
||||
* - Consider caching this for the lifetime of the component, or possibly
|
||||
* being able to share this cache between any `ScrollMap` view.
|
||||
*
|
||||
* @sideeffects
|
||||
* @private
|
||||
*/
|
||||
_remeasureMetricsOnActivation: function(e) {
|
||||
/* @edit begin */
|
||||
UIManager.measure(
|
||||
e.nativeEvent.target,
|
||||
this._handleQueryLayout
|
||||
);
|
||||
/* @edit end */
|
||||
},
|
||||
|
||||
_handleQueryLayout: function(l, t, w, h, globalX, globalY) {
|
||||
this.state.touchable.positionOnActivate &&
|
||||
Position.release(this.state.touchable.positionOnActivate);
|
||||
this.state.touchable.dimensionsOnActivate &&
|
||||
BoundingDimensions.release(this.state.touchable.dimensionsOnActivate);
|
||||
this.state.touchable.positionOnActivate = Position.getPooled(globalX, globalY);
|
||||
this.state.touchable.dimensionsOnActivate = BoundingDimensions.getPooled(w, h);
|
||||
},
|
||||
|
||||
_handleDelay: function(e) {
|
||||
this.touchableDelayTimeout = null;
|
||||
this._receiveSignal(Signals.DELAY, e);
|
||||
},
|
||||
|
||||
_handleLongDelay: function(e) {
|
||||
this.longPressDelayTimeout = null;
|
||||
var curState = this.state.touchable.touchState;
|
||||
if (curState !== States.RESPONDER_ACTIVE_PRESS_IN &&
|
||||
curState !== States.RESPONDER_ACTIVE_LONG_PRESS_IN) {
|
||||
console.error('Attempted to transition from state `' + curState + '` to `' +
|
||||
States.RESPONDER_ACTIVE_LONG_PRESS_IN + '`, which is not supported. This is ' +
|
||||
'most likely due to `Touchable.longPressDelayTimeout` not being cancelled.');
|
||||
} else {
|
||||
this._receiveSignal(Signals.LONG_PRESS_DETECTED, e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Receives a state machine signal, performs side effects of the transition
|
||||
* and stores the new state. Validates the transition as well.
|
||||
*
|
||||
* @param {Signals} signal State machine signal.
|
||||
* @throws Error if invalid state transition or unrecognized signal.
|
||||
* @sideeffects
|
||||
*/
|
||||
_receiveSignal: function(signal, e) {
|
||||
var responderID = this.state.touchable.responderID;
|
||||
var curState = this.state.touchable.touchState;
|
||||
var nextState = Transitions[curState] && Transitions[curState][signal];
|
||||
if (!responderID && signal === Signals.RESPONDER_RELEASE) {
|
||||
return;
|
||||
}
|
||||
if (!nextState) {
|
||||
throw new Error(
|
||||
'Unrecognized signal `' + signal + '` or state `' + curState +
|
||||
'` for Touchable responder `' + responderID + '`'
|
||||
);
|
||||
}
|
||||
if (nextState === States.ERROR) {
|
||||
throw new Error(
|
||||
'Touchable cannot transition from `' + curState + '` to `' + signal +
|
||||
'` for responder `' + responderID + '`'
|
||||
);
|
||||
}
|
||||
if (curState !== nextState) {
|
||||
this._performSideEffectsForTransition(curState, nextState, signal, e);
|
||||
this.state.touchable.touchState = nextState;
|
||||
}
|
||||
},
|
||||
|
||||
_cancelLongPressDelayTimeout: function () {
|
||||
this.longPressDelayTimeout && clearTimeout(this.longPressDelayTimeout);
|
||||
this.longPressDelayTimeout = null;
|
||||
},
|
||||
|
||||
_isHighlight: function (state) {
|
||||
return state === States.RESPONDER_ACTIVE_PRESS_IN ||
|
||||
state === States.RESPONDER_ACTIVE_LONG_PRESS_IN;
|
||||
},
|
||||
|
||||
_savePressInLocation: function(e) {
|
||||
var touch = TouchEventUtils.extractSingleTouch(e.nativeEvent);
|
||||
var pageX = touch && touch.pageX;
|
||||
var pageY = touch && touch.pageY;
|
||||
var locationX = touch && touch.locationX;
|
||||
var locationY = touch && touch.locationY;
|
||||
this.pressInLocation = {pageX, pageY, locationX, locationY};
|
||||
},
|
||||
|
||||
_getDistanceBetweenPoints: function (aX, aY, bX, bY) {
|
||||
var deltaX = aX - bX;
|
||||
var deltaY = aY - bY;
|
||||
return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
|
||||
},
|
||||
|
||||
/**
|
||||
* Will perform a transition between touchable states, and identify any
|
||||
* highlighting or unhighlighting that must be performed for this particular
|
||||
* transition.
|
||||
*
|
||||
* @param {States} curState Current Touchable state.
|
||||
* @param {States} nextState Next Touchable state.
|
||||
* @param {Signal} signal Signal that triggered the transition.
|
||||
* @param {Event} e Native event.
|
||||
* @sideeffects
|
||||
*/
|
||||
_performSideEffectsForTransition: function(curState, nextState, signal, e) {
|
||||
var curIsHighlight = this._isHighlight(curState);
|
||||
var newIsHighlight = this._isHighlight(nextState);
|
||||
|
||||
var isFinalSignal =
|
||||
signal === Signals.RESPONDER_TERMINATED ||
|
||||
signal === Signals.RESPONDER_RELEASE;
|
||||
|
||||
if (isFinalSignal) {
|
||||
this._cancelLongPressDelayTimeout();
|
||||
}
|
||||
|
||||
if (!IsActive[curState] && IsActive[nextState]) {
|
||||
this._remeasureMetricsOnActivation(e);
|
||||
}
|
||||
|
||||
if (IsPressingIn[curState] && signal === Signals.LONG_PRESS_DETECTED) {
|
||||
this.touchableHandleLongPress && this.touchableHandleLongPress(e);
|
||||
}
|
||||
|
||||
if (newIsHighlight && !curIsHighlight) {
|
||||
this._savePressInLocation(e);
|
||||
this.touchableHandleActivePressIn && this.touchableHandleActivePressIn(e);
|
||||
} else if (!newIsHighlight && curIsHighlight && this.touchableHandleActivePressOut) {
|
||||
if (this.touchableGetPressOutDelayMS && this.touchableGetPressOutDelayMS()) {
|
||||
this.pressOutDelayTimeout = setTimeout(() => {
|
||||
this.touchableHandleActivePressOut(e);
|
||||
}, this.touchableGetPressOutDelayMS());
|
||||
} else {
|
||||
this.touchableHandleActivePressOut(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (IsPressingIn[curState] && signal === Signals.RESPONDER_RELEASE) {
|
||||
var hasLongPressHandler = !!this.props.onLongPress;
|
||||
var pressIsLongButStillCallOnPress =
|
||||
IsLongPressingIn[curState] && ( // We *are* long pressing..
|
||||
!hasLongPressHandler || // But either has no long handler
|
||||
!this.touchableLongPressCancelsPress() // or we're told to ignore it.
|
||||
);
|
||||
|
||||
var shouldInvokePress = !IsLongPressingIn[curState] || pressIsLongButStillCallOnPress;
|
||||
if (shouldInvokePress && this.touchableHandlePress) {
|
||||
this.touchableHandlePress(e);
|
||||
}
|
||||
}
|
||||
|
||||
this.touchableDelayTimeout && clearTimeout(this.touchableDelayTimeout);
|
||||
this.touchableDelayTimeout = null;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
var Touchable = {
|
||||
Mixin: TouchableMixin
|
||||
};
|
||||
|
||||
module.exports = Touchable;
|
||||
164
src/components/Touchable/TouchableBounce.js
Normal file
164
src/components/Touchable/TouchableBounce.js
Normal file
@@ -0,0 +1,164 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* 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 TouchableBounce
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var Animated = require('../../apis/Animated');
|
||||
var EdgeInsetsPropType = require('../../apis/StyleSheet/EdgeInsetsPropType');
|
||||
var NativeMethodsMixin = require('../../modules/NativeMethodsMixin');
|
||||
var React = require('react');
|
||||
var StyleSheet = require('../../apis/StyleSheet');
|
||||
var Touchable = require('./Touchable');
|
||||
|
||||
type Event = Object;
|
||||
|
||||
type State = {
|
||||
animationID: ?number;
|
||||
scale: Animated.Value;
|
||||
};
|
||||
|
||||
var PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30};
|
||||
|
||||
/**
|
||||
* Example of using the `TouchableMixin` to play well with other responder
|
||||
* locking views including `ScrollView`. `TouchableMixin` provides touchable
|
||||
* hooks (`this.touchableHandle*`) that we forward events to. In turn,
|
||||
* `TouchableMixin` expects us to implement some abstract methods to handle
|
||||
* interesting interactions such as `handleTouchablePress`.
|
||||
*/
|
||||
var TouchableBounce = React.createClass({
|
||||
mixins: [Touchable.Mixin, NativeMethodsMixin],
|
||||
|
||||
propTypes: {
|
||||
onPress: React.PropTypes.func,
|
||||
onPressIn: React.PropTypes.func,
|
||||
onPressOut: React.PropTypes.func,
|
||||
// The function passed takes a callback to start the animation which should
|
||||
// be run after this onPress handler is done. You can use this (for example)
|
||||
// to update UI before starting the animation.
|
||||
onPressWithCompletion: React.PropTypes.func,
|
||||
// the function passed is called after the animation is complete
|
||||
onPressAnimationComplete: React.PropTypes.func,
|
||||
/**
|
||||
* When the scroll view is disabled, this defines how far your touch may
|
||||
* move off of the button, before deactivating the button. Once deactivated,
|
||||
* try moving it back and you'll see that the button is once again
|
||||
* reactivated! Move it back and forth several times while the scroll view
|
||||
* is disabled. Ensure you pass in a constant to reduce memory allocations.
|
||||
*/
|
||||
pressRetentionOffset: EdgeInsetsPropType,
|
||||
/**
|
||||
* This defines how far your touch can start away from the button. This is
|
||||
* added to `pressRetentionOffset` when moving off of the button.
|
||||
* ** NOTE **
|
||||
* The touch area never extends past the parent view bounds and the Z-index
|
||||
* of sibling views always takes precedence if a touch hits two overlapping
|
||||
* views.
|
||||
*/
|
||||
hitSlop: EdgeInsetsPropType,
|
||||
},
|
||||
|
||||
getInitialState: function(): State {
|
||||
return {
|
||||
...this.touchableGetInitialState(),
|
||||
scale: new Animated.Value(1),
|
||||
};
|
||||
},
|
||||
|
||||
bounceTo: function(
|
||||
value: number,
|
||||
velocity: number,
|
||||
bounciness: number,
|
||||
callback?: ?Function
|
||||
) {
|
||||
Animated.spring(this.state.scale, {
|
||||
toValue: value,
|
||||
velocity,
|
||||
bounciness,
|
||||
}).start(callback);
|
||||
},
|
||||
|
||||
/**
|
||||
* `Touchable.Mixin` self callbacks. The mixin will invoke these if they are
|
||||
* defined on your component.
|
||||
*/
|
||||
touchableHandleActivePressIn: function(e: Event) {
|
||||
this.bounceTo(0.93, 0.1, 0);
|
||||
this.props.onPressIn && this.props.onPressIn(e);
|
||||
},
|
||||
|
||||
touchableHandleActivePressOut: function(e: Event) {
|
||||
this.bounceTo(1, 0.4, 0);
|
||||
this.props.onPressOut && this.props.onPressOut(e);
|
||||
},
|
||||
|
||||
touchableHandlePress: function(e: Event) {
|
||||
var onPressWithCompletion = this.props.onPressWithCompletion;
|
||||
if (onPressWithCompletion) {
|
||||
onPressWithCompletion(() => {
|
||||
this.state.scale.setValue(0.93);
|
||||
this.bounceTo(1, 10, 10, this.props.onPressAnimationComplete);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.bounceTo(1, 10, 10, this.props.onPressAnimationComplete);
|
||||
this.props.onPress && this.props.onPress(e);
|
||||
},
|
||||
|
||||
touchableGetPressRectOffset: function(): typeof PRESS_RETENTION_OFFSET {
|
||||
return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET;
|
||||
},
|
||||
|
||||
touchableGetHitSlop: function(): ?Object {
|
||||
return this.props.hitSlop;
|
||||
},
|
||||
|
||||
touchableGetHighlightDelayMS: function(): number {
|
||||
return 0;
|
||||
},
|
||||
|
||||
render: function(): ReactElement {
|
||||
const scaleTransform = [{ scale: this.state.scale }];
|
||||
const propsTransform = this.props.style.transform;
|
||||
const transform = propsTransform && Array.isArray(propsTransform) ? propsTransform.concat(scaleTransform) : scaleTransform;
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
accessible={true}
|
||||
accessibilityLabel={this.props.accessibilityLabel}
|
||||
accessibilityRole={this.props.accessibilityRole || 'button'}
|
||||
testID={this.props.testID}
|
||||
hitSlop={this.props.hitSlop}
|
||||
onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
|
||||
onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}
|
||||
onResponderGrant={this.touchableHandleResponderGrant}
|
||||
onResponderMove={this.touchableHandleResponderMove}
|
||||
onResponderRelease={this.touchableHandleResponderRelease}
|
||||
onResponderTerminate={this.touchableHandleResponderTerminate}
|
||||
style={[styles.root, this.props.style, { transform }]}
|
||||
tabIndex='0'
|
||||
>
|
||||
{this.props.children}
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
root: {
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none'
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = TouchableBounce;
|
||||
274
src/components/Touchable/TouchableHighlight.js
Normal file
274
src/components/Touchable/TouchableHighlight.js
Normal file
@@ -0,0 +1,274 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* 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 TouchableHighlight
|
||||
* @noflow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
// Note (avik): add @flow when Flow supports spread properties in propTypes
|
||||
|
||||
var ColorPropType = require('../../apis/StyleSheet/ColorPropType');
|
||||
var NativeMethodsMixin = require('../../modules/NativeMethodsMixin');
|
||||
var React = require('react');
|
||||
var StyleSheet = require('../../apis/StyleSheet');
|
||||
var TimerMixin = require('react-timer-mixin');
|
||||
var Touchable = require('./Touchable');
|
||||
var TouchableWithoutFeedback = require('./TouchableWithoutFeedback');
|
||||
var View = require('../View');
|
||||
|
||||
var ensureComponentIsNative = require('./ensureComponentIsNative');
|
||||
var ensurePositiveDelayProps = require('./ensurePositiveDelayProps');
|
||||
var keyOf = require('fbjs/lib/keyOf');
|
||||
var merge = require('../../modules/merge');
|
||||
|
||||
type Event = Object;
|
||||
|
||||
var DEFAULT_PROPS = {
|
||||
activeOpacity: 0.8,
|
||||
underlayColor: 'black'
|
||||
};
|
||||
|
||||
var PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30};
|
||||
|
||||
/**
|
||||
* A wrapper for making views respond properly to touches.
|
||||
* On press down, the opacity of the wrapped view is decreased, which allows
|
||||
* the underlay color to show through, darkening or tinting the view. The
|
||||
* underlay comes from adding a view to the view hierarchy, which can sometimes
|
||||
* cause unwanted visual artifacts if not used correctly, for example if the
|
||||
* backgroundColor of the wrapped view isn't explicitly set to an opaque color.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* renderButton: function() {
|
||||
* return (
|
||||
* <TouchableHighlight onPress={this._onPressButton}>
|
||||
* <Image
|
||||
* style={styles.button}
|
||||
* source={require('image!myButton')}
|
||||
* />
|
||||
* </TouchableHighlight>
|
||||
* );
|
||||
* },
|
||||
* ```
|
||||
* > **NOTE**: TouchableHighlight supports only one child
|
||||
* >
|
||||
* > If you wish to have several child components, wrap them in a View.
|
||||
*/
|
||||
|
||||
var TouchableHighlight = React.createClass({
|
||||
propTypes: {
|
||||
...TouchableWithoutFeedback.propTypes,
|
||||
/**
|
||||
* Determines what the opacity of the wrapped view should be when touch is
|
||||
* active.
|
||||
*/
|
||||
activeOpacity: React.PropTypes.number,
|
||||
/**
|
||||
* The color of the underlay that will show through when the touch is
|
||||
* active.
|
||||
*/
|
||||
underlayColor: ColorPropType,
|
||||
style: View.propTypes.style,
|
||||
/**
|
||||
* Called immediately after the underlay is shown
|
||||
*/
|
||||
onShowUnderlay: React.PropTypes.func,
|
||||
/**
|
||||
* Called immediately after the underlay is hidden
|
||||
*/
|
||||
onHideUnderlay: React.PropTypes.func,
|
||||
},
|
||||
|
||||
mixins: [NativeMethodsMixin, TimerMixin, Touchable.Mixin],
|
||||
|
||||
getDefaultProps: () => DEFAULT_PROPS,
|
||||
|
||||
// Performance optimization to avoid constantly re-generating these objects.
|
||||
computeSyntheticState: function(props) {
|
||||
return {
|
||||
activeProps: {
|
||||
style: {
|
||||
opacity: props.activeOpacity,
|
||||
}
|
||||
},
|
||||
activeUnderlayProps: {
|
||||
style: {
|
||||
backgroundColor: props.underlayColor,
|
||||
}
|
||||
},
|
||||
underlayProps: {
|
||||
style: {
|
||||
backgroundColor: props.style.backgroundColor || null
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return merge(this.touchableGetInitialState(), this.computeSyntheticState(this.props))
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
ensurePositiveDelayProps(this.props);
|
||||
ensureComponentIsNative(this.refs[CHILD_REF]);
|
||||
},
|
||||
|
||||
componentDidUpdate: function() {
|
||||
ensureComponentIsNative(this.refs[CHILD_REF]);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
ensurePositiveDelayProps(nextProps);
|
||||
if (nextProps.activeOpacity !== this.props.activeOpacity ||
|
||||
nextProps.underlayColor !== this.props.underlayColor ||
|
||||
nextProps.style !== this.props.style) {
|
||||
this.setState(this.computeSyntheticState(nextProps));
|
||||
}
|
||||
},
|
||||
|
||||
// viewConfig: {
|
||||
// uiViewClassName: 'RCTView',
|
||||
// validAttributes: ReactNativeViewAttributes.RCTView
|
||||
// },
|
||||
|
||||
/**
|
||||
* `Touchable.Mixin` self callbacks. The mixin will invoke these if they are
|
||||
* defined on your component.
|
||||
*/
|
||||
touchableHandleActivePressIn: function(e: Event) {
|
||||
this.clearTimeout(this._hideTimeout);
|
||||
this._hideTimeout = null;
|
||||
this._showUnderlay();
|
||||
this.props.onPressIn && this.props.onPressIn(e);
|
||||
},
|
||||
|
||||
touchableHandleActivePressOut: function(e: Event) {
|
||||
if (!this._hideTimeout) {
|
||||
this._hideUnderlay();
|
||||
}
|
||||
this.props.onPressOut && this.props.onPressOut(e);
|
||||
},
|
||||
|
||||
touchableHandlePress: function(e: Event) {
|
||||
this.clearTimeout(this._hideTimeout);
|
||||
this._showUnderlay();
|
||||
this._hideTimeout = this.setTimeout(this._hideUnderlay,
|
||||
this.props.delayPressOut || 100);
|
||||
this.props.onPress && this.props.onPress(e);
|
||||
},
|
||||
|
||||
touchableHandleLongPress: function(e: Event) {
|
||||
this.props.onLongPress && this.props.onLongPress(e);
|
||||
},
|
||||
|
||||
touchableGetPressRectOffset: function() {
|
||||
return this.props.pressRetentionOffset || PRESS_RETENTION_OFFSET;
|
||||
},
|
||||
|
||||
touchableGetHitSlop: function() {
|
||||
return this.props.hitSlop;
|
||||
},
|
||||
|
||||
touchableGetHighlightDelayMS: function() {
|
||||
return this.props.delayPressIn;
|
||||
},
|
||||
|
||||
touchableGetLongPressDelayMS: function() {
|
||||
return this.props.delayLongPress;
|
||||
},
|
||||
|
||||
touchableGetPressOutDelayMS: function() {
|
||||
return this.props.delayPressOut;
|
||||
},
|
||||
|
||||
_showUnderlay: function() {
|
||||
if (!this.isMounted() || !this._hasPressHandler()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.refs[UNDERLAY_REF].setNativeProps(this.state.activeUnderlayProps);
|
||||
this.refs[CHILD_REF].setNativeProps(this.state.activeProps);
|
||||
this.props.onShowUnderlay && this.props.onShowUnderlay();
|
||||
},
|
||||
|
||||
_hideUnderlay: function() {
|
||||
this.clearTimeout(this._hideTimeout);
|
||||
this._hideTimeout = null;
|
||||
if (this._hasPressHandler() && this.refs[UNDERLAY_REF]) {
|
||||
this.refs[CHILD_REF].setNativeProps(INACTIVE_CHILD_PROPS);
|
||||
this.refs[UNDERLAY_REF].setNativeProps(this.state.underlayProps);
|
||||
this.props.onHideUnderlay && this.props.onHideUnderlay();
|
||||
}
|
||||
},
|
||||
|
||||
_hasPressHandler: function() {
|
||||
return !!(
|
||||
this.props.onPress ||
|
||||
this.props.onPressIn ||
|
||||
this.props.onPressOut ||
|
||||
this.props.onLongPress
|
||||
);
|
||||
},
|
||||
|
||||
_onKeyEnter(e, callback) {
|
||||
var ENTER = 13
|
||||
if (e.keyCode === ENTER) {
|
||||
callback && callback(e)
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<View
|
||||
accessible={true}
|
||||
accessibilityLabel={this.props.accessibilityLabel}
|
||||
accessibilityRole={this.props.accessibilityRole || this.props.accessibilityTraits || 'button'}
|
||||
hitSlop={this.props.hitSlop}
|
||||
onKeyDown={(e) => { this._onKeyEnter(e, this.touchableHandleActivePressIn) }}
|
||||
onKeyPress={(e) => { this._onKeyEnter(e, this.touchableHandlePress) }}
|
||||
onKeyUp={(e) => { this._onKeyEnter(e, this.touchableHandleActivePressOut) }}
|
||||
onLayout={this.props.onLayout}
|
||||
onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
|
||||
onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}
|
||||
onResponderGrant={this.touchableHandleResponderGrant}
|
||||
onResponderMove={this.touchableHandleResponderMove}
|
||||
onResponderRelease={this.touchableHandleResponderRelease}
|
||||
onResponderTerminate={this.touchableHandleResponderTerminate}
|
||||
ref={UNDERLAY_REF}
|
||||
style={[styles.root, this.props.style]}
|
||||
tabIndex='0'
|
||||
testID={this.props.testID}>
|
||||
{React.cloneElement(
|
||||
React.Children.only(this.props.children),
|
||||
{
|
||||
ref: CHILD_REF,
|
||||
}
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var CHILD_REF = keyOf({childRef: null});
|
||||
var UNDERLAY_REF = keyOf({underlayRef: null});
|
||||
var INACTIVE_CHILD_PROPS = {
|
||||
style: StyleSheet.create({x: {opacity: 1.0}}).x,
|
||||
};
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
root: {
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none'
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = TouchableHighlight;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user