mirror of
https://github.com/zhigang1992/react-native-web.git
synced 2026-03-30 23:23:35 +08:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
44
README.md
44
README.md
@@ -2,12 +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:
|
||||
@@ -18,17 +35,6 @@ npm install --save react@0.14 react-dom@0.14 react-native-web
|
||||
|
||||
Read the [Client and Server rendering](docs/guides/rendering.md) guide.
|
||||
|
||||
## Overview
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Examples
|
||||
|
||||
Demos:
|
||||
@@ -37,7 +43,7 @@ Demos:
|
||||
* [TicTacToe](http://codepen.io/necolas/full/eJaLZd/)
|
||||
* [2048](http://codepen.io/necolas/full/wMVvxj/)
|
||||
|
||||
Example:
|
||||
Sample:
|
||||
|
||||
```js
|
||||
import React, { AppRegistry, Image, StyleSheet, Text, View } from 'react-native'
|
||||
@@ -53,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: {
|
||||
@@ -73,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
|
||||
@@ -96,8 +102,8 @@ 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
|
||||
|
||||
@@ -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 }))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -18,45 +18,53 @@ module.exports = {
|
||||
|
||||
## 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`:
|
||||
|
||||
```
|
||||
// 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
|
||||
// server.js
|
||||
// AppShell.js
|
||||
|
||||
import React, { AppRegistry } from 'react-native'
|
||||
import MyApp from './MyApp'
|
||||
import React from 'react-native'
|
||||
|
||||
// register the app
|
||||
AppRegistry.registerApp('MyApp', () => MyApp)
|
||||
|
||||
// prerender the app
|
||||
const { html, style } = AppRegistry.prerenderApplication('MyApp', { initialProps })
|
||||
|
||||
// construct full page markup
|
||||
const HtmlShell = (html, style) => (
|
||||
const AppShell = (html, style) => (
|
||||
<html>
|
||||
<head>
|
||||
<meta charSet="utf-8" />
|
||||
@@ -64,10 +72,26 @@ const HtmlShell = (html, style) => (
|
||||
{style}
|
||||
</head>
|
||||
<body>
|
||||
<div id="react-root" dangerouslySetInnerHTML={{ __html: html }} />
|
||||
<div id="react-app" dangerouslySetInnerHTML={{ __html: html }} />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
|
||||
React.renderToStaticMarkup(<HtmlShell html={html} style={style} />)
|
||||
export default AppShell
|
||||
```
|
||||
|
||||
```js
|
||||
// server.js
|
||||
|
||||
import React, { AppRegistry } from 'react-native'
|
||||
import App from './App'
|
||||
import AppShell from './AppShell'
|
||||
|
||||
// registers the app
|
||||
AppRegistry.registerComponent('App', () => App)
|
||||
|
||||
// prerenders the app
|
||||
const { html, style } = AppRegistry.prerenderApplication('App', { initialProps })
|
||||
|
||||
// renders the full-page markup
|
||||
const renderedApplicationHTML = React.renderToString(<AppShell html={html} style={style} />)
|
||||
```
|
||||
|
||||
12
package.json
12
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "react-native-web",
|
||||
"version": "0.0.17",
|
||||
"version": "0.0.18",
|
||||
"description": "React Native for Web",
|
||||
"main": "dist/index.js",
|
||||
"files": [
|
||||
@@ -10,17 +10,17 @@
|
||||
"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",
|
||||
"lint": "eslint config src",
|
||||
"prepublish": "npm run build && npm run build:umd",
|
||||
"test": "karma start config/karma.config.js",
|
||||
"test:watch": "npm run test -- --no-single-run"
|
||||
},
|
||||
"dependencies": {
|
||||
"fbjs": "^0.7.2",
|
||||
"inline-style-prefix-all": "^1.0.2",
|
||||
"lodash.debounce": "^3.1.1",
|
||||
"react-tappable": "^0.7.1",
|
||||
"react-textarea-autosize": "^3.1.0"
|
||||
"inline-style-prefix-all": "^1.0.3",
|
||||
"lodash.debounce": "^4.0.3",
|
||||
"react-textarea-autosize": "^3.1.0",
|
||||
"react-timer-mixin": "^0.13.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.3.17",
|
||||
|
||||
@@ -11,7 +11,7 @@ import Image from '../../components/Image'
|
||||
import Text from '../../components/Text'
|
||||
import View from '../../components/View'
|
||||
|
||||
export default {
|
||||
module.exports = {
|
||||
...AnimatedImplementation,
|
||||
View: AnimatedImplementation.createAnimatedComponent(View),
|
||||
Text: AnimatedImplementation.createAnimatedComponent(Text),
|
||||
|
||||
@@ -4,7 +4,7 @@ import ReactDOM from 'react-dom'
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -88,3 +88,5 @@ export default class AppRegistry {
|
||||
ReactDOM.unmountComponentAtNode(rootTag)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AppRegistry
|
||||
|
||||
@@ -3,7 +3,7 @@ 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
|
||||
|
||||
@@ -17,15 +17,17 @@ const dimensions = {
|
||||
},
|
||||
window: {
|
||||
fontScale: 1,
|
||||
get height() { return document.documentElement.clientHeight },
|
||||
get height() { return window.innerHeight },
|
||||
scale: window.devicePixelRatio || 1,
|
||||
get width() { return document.documentElement.clientWidth }
|
||||
get width() { return window.innerWidth }
|
||||
}
|
||||
}
|
||||
|
||||
export default class Dimensions {
|
||||
class Dimensions {
|
||||
static get(dimension: string): Object {
|
||||
invariant(dimensions[dimension], 'No dimension set for key ' + dimension)
|
||||
return dimensions[dimension]
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Dimensions
|
||||
|
||||
@@ -45,4 +45,4 @@ const InteractionManager = {
|
||||
addListener: () => {}
|
||||
}
|
||||
|
||||
export default InteractionManager
|
||||
module.exports = InteractionManager
|
||||
|
||||
@@ -76,4 +76,4 @@ const NetInfo = {
|
||||
}
|
||||
}
|
||||
|
||||
export default NetInfo
|
||||
module.exports = NetInfo
|
||||
|
||||
@@ -18,9 +18,11 @@ const {
|
||||
topTouchStart
|
||||
} = EventConstants.topLevelTypes
|
||||
|
||||
const endDependencies = [ topMouseUp, topTouchCancel, topTouchEnd ]
|
||||
const moveDependencies = [ topMouseMove, topTouchMove ]
|
||||
const startDependencies = [ topMouseDown, topTouchStart ]
|
||||
const supportsTouch = ('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
|
||||
@@ -28,7 +30,7 @@ const startDependencies = [ topMouseDown, topTouchStart ]
|
||||
ResponderEventPlugin.eventTypes.responderMove.dependencies = moveDependencies
|
||||
ResponderEventPlugin.eventTypes.responderEnd.dependencies = endDependencies
|
||||
ResponderEventPlugin.eventTypes.responderStart.dependencies = startDependencies
|
||||
ResponderEventPlugin.eventTypes.responderRelease.dependencies = []
|
||||
ResponderEventPlugin.eventTypes.responderRelease.dependencies = endDependencies
|
||||
ResponderEventPlugin.eventTypes.responderTerminationRequest.dependencies = []
|
||||
ResponderEventPlugin.eventTypes.responderGrant.dependencies = []
|
||||
ResponderEventPlugin.eventTypes.responderReject.dependencies = []
|
||||
@@ -42,7 +44,7 @@ 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) {
|
||||
if ((topLevelType === topMouseMove) && !ResponderTouchHistoryStore.touchHistory.touchBank.length) {
|
||||
return
|
||||
}
|
||||
originalRecordTouchTrack.call(ResponderTouchHistoryStore, topLevelType, normalizeNativeEvent(nativeEvent))
|
||||
|
||||
@@ -2,10 +2,16 @@
|
||||
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,
|
||||
@@ -41,9 +47,8 @@ function normalizeTouchEvent(nativeEvent) {
|
||||
event.identifier = changedTouches[0].identifier
|
||||
event.pageX = changedTouches[0].pageX
|
||||
event.pageY = changedTouches[0].pageY
|
||||
const rect = changedTouches[0].target.getBoundingClientRect()
|
||||
event.locationX = changedTouches[0].pageX - rect.left
|
||||
event.locationY = changedTouches[0].pageY - rect.top
|
||||
event.locationX = changedTouches[0].locationX
|
||||
event.locationY = changedTouches[0].locationY
|
||||
}
|
||||
|
||||
return event
|
||||
@@ -54,6 +59,8 @@ function normalizeMouseEvent(nativeEvent) {
|
||||
clientX: nativeEvent.clientX,
|
||||
clientY: nativeEvent.clientY,
|
||||
force: nativeEvent.force,
|
||||
locationX: nativeEvent.clientX,
|
||||
locationY: nativeEvent.clientY,
|
||||
identifier: 0,
|
||||
pageX: nativeEvent.pageX,
|
||||
pageY: nativeEvent.pageY,
|
||||
@@ -81,4 +88,4 @@ function normalizeNativeEvent(nativeEvent) {
|
||||
return mouse ? normalizeMouseEvent(nativeEvent) : normalizeTouchEvent(nativeEvent)
|
||||
}
|
||||
|
||||
export default normalizeNativeEvent
|
||||
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
|
||||
|
||||
@@ -5,4 +5,4 @@ const Platform = {
|
||||
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
|
||||
|
||||
@@ -59,4 +59,4 @@ var colorPropType = function(isRequired, props, propName, componentName, locatio
|
||||
var ColorPropType = colorPropType.bind(null, false /* isRequired */);
|
||||
ColorPropType.isRequired = colorPropType.bind(null, true /* isRequired */);
|
||||
|
||||
export default ColorPropType
|
||||
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,7 +1,7 @@
|
||||
import prefixAll from 'inline-style-prefix-all'
|
||||
import hyphenate from './hyphenate'
|
||||
|
||||
export default class Store {
|
||||
class Store {
|
||||
constructor(
|
||||
initialState:Object = {},
|
||||
options:Object = { obfuscateClassNames: false }
|
||||
@@ -95,3 +95,5 @@ export default class Store {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Store
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,14 +8,15 @@
|
||||
|
||||
import prefixAll from 'inline-style-prefix-all'
|
||||
import flattenStyle from './flattenStyle'
|
||||
import processTransform from './processTransform'
|
||||
|
||||
export default class StyleSheetRegistry {
|
||||
class StyleSheetRegistry {
|
||||
static registerStyle(style: Object, store): number {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
Object.freeze(style)
|
||||
}
|
||||
|
||||
const normalizedStyle = flattenStyle(style)
|
||||
const normalizedStyle = processTransform(flattenStyle(style))
|
||||
Object.keys(normalizedStyle).forEach((prop) => {
|
||||
// add each declaration to the store
|
||||
store.set(prop, normalizedStyle[prop])
|
||||
@@ -26,7 +27,7 @@ export default class StyleSheetRegistry {
|
||||
let _className
|
||||
let _style = {}
|
||||
const classList = []
|
||||
const normalizedStyle = flattenStyle(style)
|
||||
const normalizedStyle = processTransform(flattenStyle(style))
|
||||
|
||||
for (const prop in normalizedStyle) {
|
||||
let styleClass = store.get(prop, normalizedStyle[prop])
|
||||
@@ -44,3 +45,5 @@ export default class StyleSheetRegistry {
|
||||
return { className: _className, style: _style }
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = StyleSheetRegistry
|
||||
|
||||
@@ -12,7 +12,7 @@ import TextStylePropTypes from '../../components/Text/TextStylePropTypes'
|
||||
import ViewStylePropTypes from '../../components/View/ViewStylePropTypes'
|
||||
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) {
|
||||
@@ -66,3 +66,5 @@ StyleSheetValidation.addValidStylePropTypes({
|
||||
listStyle: PropTypes.string,
|
||||
verticalAlign: 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
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import invariant from 'fbjs/lib/invariant'
|
||||
import ReactPropTypeLocationNames from 'react/lib/ReactPropTypeLocationNames'
|
||||
|
||||
export default function createStrictShapeTypeChecker(shapeTypes) {
|
||||
module.exports = function createStrictShapeTypeChecker(shapeTypes) {
|
||||
function checkType(isRequired, props, propName, componentName, location?) {
|
||||
if (!props[propName]) {
|
||||
if (isRequired) {
|
||||
|
||||
@@ -56,4 +56,4 @@ const expandStyle = (style) => {
|
||||
}, {})
|
||||
}
|
||||
|
||||
export default expandStyle
|
||||
module.exports = expandStyle
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import invariant from 'fbjs/lib/invariant'
|
||||
import expandStyle from './expandStyle'
|
||||
|
||||
export default function flattenStyle(style): ?Object {
|
||||
module.exports = function flattenStyle(style): ?Object {
|
||||
if (!style) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -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-')
|
||||
|
||||
@@ -65,7 +65,7 @@ const resolve = ({ style = {} }) => {
|
||||
return StyleSheetRegistry.getStyleAsNativeProps(style, store)
|
||||
}
|
||||
|
||||
export default {
|
||||
module.exports = {
|
||||
_destroy,
|
||||
_renderToString,
|
||||
create,
|
||||
|
||||
@@ -30,4 +30,4 @@ const normalizeValue = (property, value) => {
|
||||
return value
|
||||
}
|
||||
|
||||
export default normalizeValue
|
||||
module.exports = normalizeValue
|
||||
|
||||
@@ -3,11 +3,10 @@
|
||||
*/
|
||||
export const resetCSS =
|
||||
`/* React Native Web */
|
||||
html {font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}
|
||||
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 {-webkit-appearance:none}`
|
||||
|
||||
/**
|
||||
* Custom pointer event styles
|
||||
|
||||
24
src/apis/StyleSheet/processTransform.js
Normal file
24
src/apis/StyleSheet/processTransform.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// { 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.transformMatrix = convertTransformMatrix(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,6 +1,8 @@
|
||||
import CSSPropertyOperations from 'react/lib/CSSPropertyOperations'
|
||||
import flattenStyle from '../StyleSheet/flattenStyle'
|
||||
import processTransform from '../StyleSheet/processTransform'
|
||||
|
||||
const measureAll = (node, callback, relativeToNativeNode) => {
|
||||
const _measureLayout = (node, relativeToNativeNode, callback) => {
|
||||
const relativeNode = relativeToNativeNode || node.parentNode
|
||||
const relativeRect = relativeNode.getBoundingClientRect()
|
||||
const { height, left, top, width } = node.getBoundingClientRect()
|
||||
@@ -10,26 +12,55 @@ const measureAll = (node, callback, relativeToNativeNode) => {
|
||||
}
|
||||
|
||||
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,4 +1,4 @@
|
||||
import { NativeMethodsDecorator } from '../../modules/NativeMethodsMixin'
|
||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
@@ -20,7 +20,7 @@ const keyframeEffects = [
|
||||
]
|
||||
|
||||
@NativeMethodsDecorator
|
||||
export default class ActivityIndicator extends Component {
|
||||
class ActivityIndicator extends Component {
|
||||
static propTypes = {
|
||||
animating: PropTypes.bool,
|
||||
color: PropTypes.string,
|
||||
@@ -107,3 +107,5 @@ const indicatorStyles = StyleSheet.create({
|
||||
height: 36
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = ActivityIndicator
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NativeMethodsDecorator } from '../../modules/NativeMethodsMixin'
|
||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
|
||||
@@ -19,7 +19,7 @@ const roleComponents = {
|
||||
}
|
||||
|
||||
@NativeMethodsDecorator
|
||||
export default class CoreComponent extends Component {
|
||||
class CoreComponent extends Component {
|
||||
static propTypes = {
|
||||
accessibilityLabel: PropTypes.string,
|
||||
accessibilityLiveRegion: PropTypes.oneOf([ 'assertive', 'off', 'polite' ]),
|
||||
@@ -64,3 +64,5 @@ export default class CoreComponent extends Component {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CoreComponent
|
||||
|
||||
@@ -7,4 +7,4 @@ const ImageResizeMode = keyMirror({
|
||||
stretch: null
|
||||
})
|
||||
|
||||
export default ImageResizeMode
|
||||
module.exports = ImageResizeMode
|
||||
|
||||
@@ -6,7 +6,7 @@ import ImageResizeMode from './ImageResizeMode'
|
||||
|
||||
const hiddenOrVisible = PropTypes.oneOf([ 'hidden', 'visible' ])
|
||||
|
||||
export default {
|
||||
module.exports = {
|
||||
...LayoutPropTypes,
|
||||
...TransformPropTypes,
|
||||
backfaceVisibility: hiddenOrVisible,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* global window */
|
||||
import { NativeMethodsDecorator } from '../../modules/NativeMethodsMixin'
|
||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
||||
import resolveAssetSource from './resolveAssetSource'
|
||||
import CoreComponent from '../CoreComponent'
|
||||
import ImageResizeMode from './ImageResizeMode'
|
||||
@@ -23,7 +23,7 @@ const ImageSourcePropType = PropTypes.oneOfType([
|
||||
])
|
||||
|
||||
@NativeMethodsDecorator
|
||||
export default class Image extends Component {
|
||||
class Image extends Component {
|
||||
static propTypes = {
|
||||
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
|
||||
accessible: CoreComponent.propTypes.accessible,
|
||||
@@ -217,3 +217,5 @@ const resizeModeStyles = StyleSheet.create({
|
||||
backgroundSize: '100% 100%'
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = Image
|
||||
|
||||
@@ -2,4 +2,4 @@ function resolveAssetSource(source) {
|
||||
return ((typeof source === 'object') ? source.uri : source) || null
|
||||
}
|
||||
|
||||
export default resolveAssetSource
|
||||
module.exports = resolveAssetSource
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { NativeMethodsDecorator } from '../../modules/NativeMethodsMixin'
|
||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import ScrollView from '../ScrollView'
|
||||
|
||||
@NativeMethodsDecorator
|
||||
export default class ListView extends Component {
|
||||
class ListView extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.any,
|
||||
style: ScrollView.propTypes.style
|
||||
@@ -19,3 +19,5 @@ export default class ListView extends Component {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ListView
|
||||
|
||||
@@ -19,7 +19,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
|
||||
};
|
||||
@@ -154,3 +154,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,130 +1,227 @@
|
||||
import { NativeMethodsDecorator } from '../../modules/NativeMethodsMixin'
|
||||
import debounce from 'lodash.debounce'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
import View from '../View'
|
||||
/**
|
||||
* Copyright (c) 2016-present, Nicolas Gallagher.
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
@NativeMethodsDecorator
|
||||
export default class ScrollView extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.any,
|
||||
contentContainerStyle: View.propTypes.style,
|
||||
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: View.propTypes.style
|
||||
};
|
||||
style: StyleSheetPropType(ViewStylePropTypes)
|
||||
},
|
||||
|
||||
static defaultProps = {
|
||||
contentContainerStyle: {},
|
||||
horizontal: false,
|
||||
scrollEnabled: true,
|
||||
scrollEventThrottle: 0,
|
||||
style: {}
|
||||
};
|
||||
mixins: [ScrollResponder.Mixin],
|
||||
|
||||
constructor(...args) {
|
||||
super(...args)
|
||||
this._debouncedOnScrollEnd = debounce(this._onScrollEnd, 100)
|
||||
this.state = {
|
||||
isScrolling: false
|
||||
}
|
||||
}
|
||||
getInitialState() {
|
||||
return this.scrollResponderMixinGetInitialState()
|
||||
},
|
||||
|
||||
_onScroll(e) {
|
||||
const { scrollEventThrottle } = this.props
|
||||
const { isScrolling, scrollLastTick } = this.state
|
||||
setNativeProps(props: Object) {
|
||||
this.refs[SCROLLVIEW].setNativeProps(props)
|
||||
},
|
||||
|
||||
// A scroll happened, so the scroll bumps the debounce.
|
||||
this._debouncedOnScrollEnd(e)
|
||||
/**
|
||||
* 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
|
||||
},
|
||||
|
||||
if (isScrolling) {
|
||||
// Scroll last tick may have changed, check if we need to notify
|
||||
if (this._shouldEmitScrollEvent(scrollLastTick, scrollEventThrottle)) {
|
||||
this._onScrollTick(e)
|
||||
}
|
||||
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 {
|
||||
// Weren't scrolling, so we must have just started
|
||||
this._onScrollStart(e)
|
||||
({x, y, animated} = y || {})
|
||||
}
|
||||
}
|
||||
|
||||
_onScrollStart() {
|
||||
this.setState({
|
||||
isScrolling: true,
|
||||
scrollLastTick: Date.now()
|
||||
})
|
||||
}
|
||||
this.getScrollResponder().scrollResponderScrollTo({x: x || 0, y: y || 0, animated: animated !== false})
|
||||
},
|
||||
|
||||
_onScrollTick(e) {
|
||||
const { onScroll } = this.props
|
||||
this.setState({
|
||||
scrollLastTick: Date.now()
|
||||
})
|
||||
if (onScroll) onScroll(e)
|
||||
}
|
||||
/**
|
||||
* 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})
|
||||
},
|
||||
|
||||
_onScrollEnd(e) {
|
||||
const { onScroll } = this.props
|
||||
this.setState({
|
||||
isScrolling: false
|
||||
})
|
||||
if (onScroll) onScroll(e)
|
||||
}
|
||||
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.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_shouldEmitScrollEvent(lastTick, eventThrottle) {
|
||||
const timeSinceLastTick = Date.now() - lastTick
|
||||
return (eventThrottle > 0 && timeSinceLastTick >= (1000 / eventThrottle))
|
||||
}
|
||||
if (this.props.keyboardDismissMode === 'on-drag') {
|
||||
dismissKeyboard()
|
||||
}
|
||||
|
||||
_maybePreventScroll(e) {
|
||||
const { scrollEnabled } = this.props
|
||||
if (!scrollEnabled) e.preventDefault()
|
||||
}
|
||||
this.scrollResponderHandleScroll(e)
|
||||
},
|
||||
|
||||
_handleContentOnLayout(e: Object) {
|
||||
const { width, height } = e.nativeEvent.layout
|
||||
this.props.onContentSizeChange && this.props.onContentSizeChange(width, height)
|
||||
},
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
contentContainerStyle,
|
||||
horizontal,
|
||||
style
|
||||
} = this.props
|
||||
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 (
|
||||
<View
|
||||
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>
|
||||
<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'
|
||||
}
|
||||
})
|
||||
|
||||
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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { NativeMethodsDecorator } from '../../modules/NativeMethodsMixin'
|
||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
||||
import CoreComponent from '../CoreComponent'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
@@ -6,7 +6,7 @@ import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType'
|
||||
import TextStylePropTypes from './TextStylePropTypes'
|
||||
|
||||
@NativeMethodsDecorator
|
||||
export default class Text extends Component {
|
||||
class Text extends Component {
|
||||
static propTypes = {
|
||||
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
|
||||
accessibilityRole: CoreComponent.propTypes.accessibilityRole,
|
||||
@@ -66,3 +66,5 @@ const styles = StyleSheet.create({
|
||||
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,14 +1,15 @@
|
||||
import { NativeMethodsDecorator } from '../../modules/NativeMethodsMixin'
|
||||
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'
|
||||
|
||||
@NativeMethodsDecorator
|
||||
export default class TextInput extends Component {
|
||||
class TextInput extends Component {
|
||||
static propTypes = {
|
||||
...View.propTypes,
|
||||
autoComplete: PropTypes.bool,
|
||||
@@ -50,11 +51,15 @@ export default class TextInput extends Component {
|
||||
}
|
||||
|
||||
blur() {
|
||||
this.refs.input.blur()
|
||||
TextInputState.blurTextInput(ReactDOM.findDOMNode(this.refs.input))
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.setNativeProps({ text: '' })
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.refs.input.focus()
|
||||
TextInputState.focusTextInput(ReactDOM.findDOMNode(this.refs.input))
|
||||
}
|
||||
|
||||
setNativeProps(props) {
|
||||
@@ -63,27 +68,34 @@ export default class TextInput extends Component {
|
||||
|
||||
_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) {
|
||||
@@ -229,3 +241,5 @@ const styles = StyleSheet.create({
|
||||
whiteSpace: 'pre'
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = TextInput
|
||||
|
||||
@@ -340,7 +340,7 @@ var TouchableMixin = {
|
||||
* Must return true to start the process of `Touchable`.
|
||||
*/
|
||||
touchableHandleStartShouldSetResponder: function() {
|
||||
return true;
|
||||
return !this.props.disabled;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -558,10 +558,10 @@ var TouchableMixin = {
|
||||
* @sideeffects
|
||||
* @private
|
||||
*/
|
||||
_remeasureMetricsOnActivation: function() {
|
||||
_remeasureMetricsOnActivation: function(e) {
|
||||
/* @edit begin */
|
||||
UIManager.measure(
|
||||
this.state.touchable.responderID,
|
||||
e.nativeEvent.target,
|
||||
this._handleQueryLayout
|
||||
);
|
||||
/* @edit end */
|
||||
@@ -603,18 +603,22 @@ var TouchableMixin = {
|
||||
* @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 `' + this.state.touchable.responderID + '`'
|
||||
'` for Touchable responder `' + responderID + '`'
|
||||
);
|
||||
}
|
||||
if (nextState === States.ERROR) {
|
||||
throw new Error(
|
||||
'Touchable cannot transition from `' + curState + '` to `' + signal +
|
||||
'` for responder `' + this.state.touchable.responderID + '`'
|
||||
'` for responder `' + responderID + '`'
|
||||
);
|
||||
}
|
||||
if (curState !== nextState) {
|
||||
@@ -672,7 +676,7 @@ var TouchableMixin = {
|
||||
}
|
||||
|
||||
if (!IsActive[curState] && IsActive[nextState]) {
|
||||
this._remeasureMetricsOnActivation();
|
||||
this._remeasureMetricsOnActivation(e);
|
||||
}
|
||||
|
||||
if (IsPressingIn[curState] && signal === Signals.LONG_PRESS_DETECTED) {
|
||||
|
||||
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;
|
||||
278
src/components/Touchable/TouchableHighlight.js
Normal file
278
src/components/Touchable/TouchableHighlight.js
Normal file
@@ -0,0 +1,278 @@
|
||||
/* 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,
|
||||
}
|
||||
},
|
||||
underlayStyle: [
|
||||
INACTIVE_UNDERLAY_PROPS.style
|
||||
]
|
||||
};
|
||||
},
|
||||
|
||||
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({
|
||||
...INACTIVE_UNDERLAY_PROPS,
|
||||
style: this.state.underlayStyle,
|
||||
});
|
||||
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'}
|
||||
ref={UNDERLAY_REF}
|
||||
style={[styles.root, this.state.underlayStyle, this.props.style]}
|
||||
onLayout={this.props.onLayout}
|
||||
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) }}
|
||||
onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
|
||||
onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}
|
||||
onResponderGrant={this.touchableHandleResponderGrant}
|
||||
onResponderMove={this.touchableHandleResponderMove}
|
||||
onResponderRelease={this.touchableHandleResponderRelease}
|
||||
onResponderTerminate={this.touchableHandleResponderTerminate}
|
||||
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 INACTIVE_UNDERLAY_PROPS = {
|
||||
style: {backgroundColor: null}
|
||||
};
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
root: {
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none'
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = TouchableHighlight;
|
||||
200
src/components/Touchable/TouchableOpacity.js
Normal file
200
src/components/Touchable/TouchableOpacity.js
Normal file
@@ -0,0 +1,200 @@
|
||||
/* 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 TouchableOpacity
|
||||
* @noflow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
// Note (avik): add @flow when Flow supports spread properties in propTypes
|
||||
|
||||
var Animated = require('../../apis/Animated');
|
||||
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 ensurePositiveDelayProps = require('./ensurePositiveDelayProps');
|
||||
var flattenStyle = require('../../apis/StyleSheet/flattenStyle');
|
||||
|
||||
type Event = Object;
|
||||
|
||||
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, dimming it.
|
||||
* This is done without actually changing the view hierarchy, and in general is
|
||||
* easy to add to an app without weird side-effects.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* ```
|
||||
* renderButton: function() {
|
||||
* return (
|
||||
* <TouchableOpacity onPress={this._onPressButton}>
|
||||
* <Image
|
||||
* style={styles.button}
|
||||
* source={require('image!myButton')}
|
||||
* />
|
||||
* </TouchableOpacity>
|
||||
* );
|
||||
* },
|
||||
* ```
|
||||
*/
|
||||
var TouchableOpacity = React.createClass({
|
||||
mixins: [TimerMixin, Touchable.Mixin, NativeMethodsMixin],
|
||||
|
||||
propTypes: {
|
||||
...TouchableWithoutFeedback.propTypes,
|
||||
/**
|
||||
* Determines what the opacity of the wrapped view should be when touch is
|
||||
* active.
|
||||
*/
|
||||
activeOpacity: React.PropTypes.number,
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
return {
|
||||
activeOpacity: 0.2,
|
||||
};
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
...this.touchableGetInitialState(),
|
||||
anim: new Animated.Value(1),
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
ensurePositiveDelayProps(this.props);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
ensurePositiveDelayProps(nextProps);
|
||||
},
|
||||
|
||||
setOpacityTo: function(value) {
|
||||
Animated.timing(
|
||||
this.state.anim,
|
||||
{toValue: value, duration: 150}
|
||||
).start();
|
||||
},
|
||||
|
||||
/**
|
||||
* `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._opacityActive();
|
||||
this.props.onPressIn && this.props.onPressIn(e);
|
||||
},
|
||||
|
||||
touchableHandleActivePressOut: function(e: Event) {
|
||||
if (!this._hideTimeout) {
|
||||
this._opacityInactive();
|
||||
}
|
||||
this.props.onPressOut && this.props.onPressOut(e);
|
||||
},
|
||||
|
||||
touchableHandlePress: function(e: Event) {
|
||||
this.clearTimeout(this._hideTimeout);
|
||||
this._opacityActive();
|
||||
this._hideTimeout = this.setTimeout(
|
||||
this._opacityInactive,
|
||||
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 || 0;
|
||||
},
|
||||
|
||||
touchableGetLongPressDelayMS: function() {
|
||||
return this.props.delayLongPress === 0 ? 0 :
|
||||
this.props.delayLongPress || 500;
|
||||
},
|
||||
|
||||
touchableGetPressOutDelayMS: function() {
|
||||
return this.props.delayPressOut;
|
||||
},
|
||||
|
||||
_opacityActive: function() {
|
||||
this.setOpacityTo(this.props.activeOpacity);
|
||||
},
|
||||
|
||||
_opacityInactive: function() {
|
||||
this.clearTimeout(this._hideTimeout);
|
||||
this._hideTimeout = null;
|
||||
var childStyle = flattenStyle(this.props.style) || {};
|
||||
this.setOpacityTo(
|
||||
childStyle.opacity === undefined ? 1 : childStyle.opacity
|
||||
);
|
||||
},
|
||||
|
||||
_onKeyEnter(e, callback) {
|
||||
var ENTER = 13
|
||||
if (e.keyCode === ENTER) {
|
||||
callback && callback(e)
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<Animated.View
|
||||
accessible={true}
|
||||
accessibilityLabel={this.props.accessibilityLabel}
|
||||
accessibilityRole={this.props.accessibilityRole || 'button'}
|
||||
style={[styles.root, this.props.style, {opacity: this.state.anim}]}
|
||||
testID={this.props.testID}
|
||||
onLayout={this.props.onLayout}
|
||||
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) }}
|
||||
onStartShouldSetResponder={this.touchableHandleStartShouldSetResponder}
|
||||
onResponderTerminationRequest={this.touchableHandleResponderTerminationRequest}
|
||||
onResponderGrant={this.touchableHandleResponderGrant}
|
||||
onResponderMove={this.touchableHandleResponderMove}
|
||||
onResponderRelease={this.touchableHandleResponderRelease}
|
||||
onResponderTerminate={this.touchableHandleResponderTerminate}
|
||||
tabIndex='0'
|
||||
>
|
||||
{this.props.children}
|
||||
</Animated.View>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
var styles = StyleSheet.create({
|
||||
root: {
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none'
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = TouchableOpacity;
|
||||
166
src/components/Touchable/TouchableWithoutFeedback.js
Normal file
166
src/components/Touchable/TouchableWithoutFeedback.js
Normal file
@@ -0,0 +1,166 @@
|
||||
/* 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 TouchableWithoutFeedback
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var EdgeInsetsPropType = require('../../apis/StyleSheet/EdgeInsetsPropType');
|
||||
var React = require('react');
|
||||
var TimerMixin = require('react-timer-mixin');
|
||||
var Touchable = require('./Touchable');
|
||||
var View = require('../View');
|
||||
var ensurePositiveDelayProps = require('./ensurePositiveDelayProps');
|
||||
|
||||
type Event = Object;
|
||||
|
||||
var PRESS_RETENTION_OFFSET = {top: 20, left: 20, right: 20, bottom: 30};
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
var TouchableWithoutFeedback = React.createClass({
|
||||
mixins: [TimerMixin, Touchable.Mixin],
|
||||
|
||||
propTypes: {
|
||||
accessible: React.PropTypes.bool,
|
||||
accessibilityLabel: View.propTypes.accessibilityLabel,
|
||||
accessibilityRole: View.propTypes.accessibilityRole,
|
||||
/**
|
||||
* If true, disable all interactions for this component.
|
||||
*/
|
||||
disabled: React.PropTypes.bool,
|
||||
/**
|
||||
* Called when the touch is released, but not if cancelled (e.g. by a scroll
|
||||
* that steals the responder lock).
|
||||
*/
|
||||
onPress: React.PropTypes.func,
|
||||
onPressIn: React.PropTypes.func,
|
||||
onPressOut: React.PropTypes.func,
|
||||
/**
|
||||
* Invoked on mount and layout changes with
|
||||
*
|
||||
* `{nativeEvent: {layout: {x, y, width, height}}}`
|
||||
*/
|
||||
onLayout: React.PropTypes.func,
|
||||
|
||||
onLongPress: React.PropTypes.func,
|
||||
|
||||
/**
|
||||
* Delay in ms, from the start of the touch, before onPressIn is called.
|
||||
*/
|
||||
delayPressIn: React.PropTypes.number,
|
||||
/**
|
||||
* Delay in ms, from the release of the touch, before onPressOut is called.
|
||||
*/
|
||||
delayPressOut: React.PropTypes.number,
|
||||
/**
|
||||
* Delay in ms, from onPressIn, before onLongPress is called.
|
||||
*/
|
||||
delayLongPress: React.PropTypes.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.
|
||||
*/
|
||||
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() {
|
||||
return this.touchableGetInitialState();
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
ensurePositiveDelayProps(this.props);
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps: Object) {
|
||||
ensurePositiveDelayProps(nextProps);
|
||||
},
|
||||
|
||||
/**
|
||||
* `Touchable.Mixin` self callbacks. The mixin will invoke these if they are
|
||||
* defined on your component.
|
||||
*/
|
||||
touchableHandlePress: function(e: Event) {
|
||||
this.props.onPress && this.props.onPress(e);
|
||||
},
|
||||
|
||||
touchableHandleActivePressIn: function(e: Event) {
|
||||
this.props.onPressIn && this.props.onPressIn(e);
|
||||
},
|
||||
|
||||
touchableHandleActivePressOut: function(e: Event) {
|
||||
this.props.onPressOut && this.props.onPressOut(e);
|
||||
},
|
||||
|
||||
touchableHandleLongPress: function(e: Event) {
|
||||
this.props.onLongPress && this.props.onLongPress(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 this.props.delayPressIn || 0;
|
||||
},
|
||||
|
||||
touchableGetLongPressDelayMS: function(): number {
|
||||
return this.props.delayLongPress === 0 ? 0 :
|
||||
this.props.delayLongPress || 500;
|
||||
},
|
||||
|
||||
touchableGetPressOutDelayMS: function(): number {
|
||||
return this.props.delayPressOut || 0;
|
||||
},
|
||||
|
||||
render: function(): ReactElement {
|
||||
// Note(avik): remove dynamic typecast once Flow has been upgraded
|
||||
return (React: any).cloneElement(React.children.only(this.props.children), {
|
||||
accessible: this.props.accessible !== false,
|
||||
accessibilityLabel: this.props.accessibilityLabel,
|
||||
accessibilityRole: this.props.accessibilityRole,
|
||||
testID: this.props.testID,
|
||||
onLayout: this.props.onLayout,
|
||||
hitSlop: this.props.hitSlop,
|
||||
onStartShouldSetResponder: this.touchableHandleStartShouldSetResponder,
|
||||
onResponderTerminationRequest: this.touchableHandleResponderTerminationRequest,
|
||||
onResponderGrant: this.touchableHandleResponderGrant,
|
||||
onResponderMove: this.touchableHandleResponderMove,
|
||||
onResponderRelease: this.touchableHandleResponderRelease,
|
||||
onResponderTerminate: this.touchableHandleResponderTerminate,
|
||||
tabIndex: '0'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = TouchableWithoutFeedback;
|
||||
@@ -1,35 +1,5 @@
|
||||
/* eslint-env mocha */
|
||||
|
||||
import * as utils from '../../../modules/specHelpers'
|
||||
import assert from 'assert'
|
||||
import React from 'react'
|
||||
|
||||
import Touchable from '../'
|
||||
|
||||
const children = <span style={{}}>children</span>
|
||||
const requiredProps = { children }
|
||||
|
||||
suite('components/Touchable', () => {
|
||||
test('prop "accessibilityLabel"', () => {
|
||||
const accessibilityLabel = 'accessibilityLabel'
|
||||
const result = utils.shallowRender(<Touchable {...requiredProps} accessibilityLabel={accessibilityLabel} />)
|
||||
assert.equal(result.props.accessibilityLabel, accessibilityLabel)
|
||||
})
|
||||
|
||||
test('prop "accessibilityRole"', () => {
|
||||
const accessibilityRole = 'accessibilityRole'
|
||||
const result = utils.shallowRender(<Touchable {...requiredProps} accessibilityRole={accessibilityRole} />)
|
||||
assert.equal(result.props.accessibilityRole, accessibilityRole)
|
||||
})
|
||||
|
||||
test('prop "accessible"', () => {
|
||||
const accessible = false
|
||||
const result = utils.shallowRender(<Touchable {...requiredProps} accessible={accessible} />)
|
||||
assert.equal(result.props.accessible, accessible)
|
||||
})
|
||||
|
||||
test('prop "children"', () => {
|
||||
const result = utils.shallowRender(<Touchable {...requiredProps} />)
|
||||
assert.deepEqual(result.props.children, <span style={[{}, false]}>children</span>)
|
||||
})
|
||||
test.skip('NO TEST COVERAGE', () => {})
|
||||
})
|
||||
|
||||
25
src/components/Touchable/ensureComponentIsNative.js
Normal file
25
src/components/Touchable/ensureComponentIsNative.js
Normal file
@@ -0,0 +1,25 @@
|
||||
/* 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 ensureComponentIsNative
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var invariant = require('fbjs/lib/invariant');
|
||||
|
||||
var ensureComponentIsNative = function(component: any) {
|
||||
invariant(
|
||||
component && typeof component.setNativeProps === 'function',
|
||||
'Touchable child must either be native or forward setNativeProps to a ' +
|
||||
'native component'
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = ensureComponentIsNative;
|
||||
25
src/components/Touchable/ensurePositiveDelayProps.js
Normal file
25
src/components/Touchable/ensurePositiveDelayProps.js
Normal file
@@ -0,0 +1,25 @@
|
||||
/* 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 ensurePositiveDelayProps
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var invariant = require('fbjs/lib/invariant');
|
||||
|
||||
var ensurePositiveDelayProps = function(props: any) {
|
||||
invariant(
|
||||
!(props.delayPressIn < 0 || props.delayPressOut < 0 ||
|
||||
props.delayLongPress < 0),
|
||||
'Touchable components cannot have negative delay properties'
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = ensurePositiveDelayProps;
|
||||
@@ -1,131 +0,0 @@
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
import Tappable from 'react-tappable'
|
||||
import View from '../View'
|
||||
|
||||
export default class Touchable extends Component {
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this.state = {
|
||||
isActive: false
|
||||
}
|
||||
|
||||
this._onLongPress = this._onLongPress.bind(this)
|
||||
this._onPress = this._onPress.bind(this)
|
||||
this._onPressIn = this._onPressIn.bind(this)
|
||||
this._onPressOut = this._onPressOut.bind(this)
|
||||
}
|
||||
|
||||
static propTypes = {
|
||||
accessibilityLabel: View.propTypes.accessibilityLabel,
|
||||
accessibilityRole: View.propTypes.accessibilityRole,
|
||||
accessible: View.propTypes.accessible,
|
||||
activeOpacity: PropTypes.number,
|
||||
activeUnderlayColor: PropTypes.string,
|
||||
children: PropTypes.element,
|
||||
delayLongPress: PropTypes.number,
|
||||
delayPressIn: PropTypes.number,
|
||||
delayPressOut: PropTypes.number,
|
||||
onLongPress: PropTypes.func,
|
||||
onPress: PropTypes.func,
|
||||
onPressIn: PropTypes.func,
|
||||
onPressOut: PropTypes.func,
|
||||
style: View.propTypes.style
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
accessibilityRole: 'button',
|
||||
activeOpacity: 0.8,
|
||||
activeUnderlayColor: 'black',
|
||||
delayLongPress: 500,
|
||||
delayPressIn: 0,
|
||||
delayPressOut: 100,
|
||||
style: {}
|
||||
};
|
||||
|
||||
_getChildren() {
|
||||
const { activeOpacity, children } = this.props
|
||||
return React.cloneElement(React.Children.only(children), {
|
||||
style: [
|
||||
children.props.style,
|
||||
this.state.isActive && { opacity: activeOpacity }
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
_onKeyEnter(e, callback) {
|
||||
var ENTER = 13
|
||||
if (e.keyCode === ENTER) {
|
||||
callback(e)
|
||||
}
|
||||
}
|
||||
|
||||
_onLongPress(e) {
|
||||
if (this.props.onLongPress) this.props.onLongPress(e)
|
||||
}
|
||||
|
||||
_onPress(e) {
|
||||
if (this.props.onPress) this.props.onPress(e)
|
||||
}
|
||||
|
||||
_onPressIn(e) {
|
||||
this.setState({ isActive: true })
|
||||
if (this.props.onPressIn) this.props.onPressIn(e)
|
||||
}
|
||||
|
||||
_onPressOut(e) {
|
||||
this.setState({ isActive: false })
|
||||
if (this.props.onPressOut) this.props.onPressOut(e)
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
accessibilityLabel,
|
||||
accessibilityRole,
|
||||
accessible,
|
||||
activeUnderlayColor,
|
||||
delayLongPress,
|
||||
style
|
||||
} = this.props
|
||||
|
||||
/**
|
||||
* Creates a wrapping element that can receive keyboard focus. The
|
||||
* highlight is applied as a background color on this wrapper. The opacity
|
||||
* is set on the child element, allowing it to have its own background
|
||||
* color.
|
||||
*/
|
||||
return (
|
||||
<Tappable
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
accessibilityRole={accessibilityRole}
|
||||
accessible={accessible}
|
||||
children={this._getChildren()}
|
||||
component={View}
|
||||
onKeyDown={(e) => { this._onKeyEnter(e, this._onPressIn) }}
|
||||
onKeyPress={this._onPress}
|
||||
onKeyUp={(e) => { this._onKeyEnter(e, this._onPressOut) }}
|
||||
onMouseDown={this._onPressIn}
|
||||
onMouseUp={this._onPressOut}
|
||||
onPress={this._onLongPress}
|
||||
onTap={this._onPress}
|
||||
onTouchEnd={this._onPressOut}
|
||||
onTouchStart={this._onPressIn}
|
||||
pressDelay={delayLongPress}
|
||||
pressMoveThreshold={5}
|
||||
style={StyleSheet.flatten([
|
||||
styles.initial,
|
||||
style,
|
||||
activeUnderlayColor && this.state.isActive && { backgroundColor: activeUnderlayColor }
|
||||
])}
|
||||
tabIndex='0'
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
initial: {
|
||||
cursor: 'pointer',
|
||||
userSelect: undefined
|
||||
}
|
||||
})
|
||||
@@ -8,7 +8,7 @@ const { number, oneOf, string } = PropTypes
|
||||
const autoOrHiddenOrVisible = oneOf([ 'auto', 'hidden', 'visible' ])
|
||||
const hiddenOrVisible = oneOf([ 'hidden', 'visible' ])
|
||||
|
||||
export default {
|
||||
module.exports = {
|
||||
...BorderPropTypes,
|
||||
...LayoutPropTypes,
|
||||
...TransformPropTypes,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NativeMethodsDecorator } from '../../modules/NativeMethodsMixin'
|
||||
import NativeMethodsDecorator from '../../modules/NativeMethodsDecorator'
|
||||
import normalizeNativeEvent from '../../apis/PanResponder/normalizeNativeEvent'
|
||||
import CoreComponent from '../CoreComponent'
|
||||
import React, { Component, PropTypes } from 'react'
|
||||
import StyleSheet from '../../apis/StyleSheet'
|
||||
@@ -6,7 +7,7 @@ import StyleSheetPropType from '../../apis/StyleSheet/StyleSheetPropType'
|
||||
import ViewStylePropTypes from './ViewStylePropTypes'
|
||||
|
||||
@NativeMethodsDecorator
|
||||
export default class View extends Component {
|
||||
class View extends Component {
|
||||
static propTypes = {
|
||||
accessibilityLabel: CoreComponent.propTypes.accessibilityLabel,
|
||||
accessibilityLiveRegion: CoreComponent.propTypes.accessibilityLiveRegion,
|
||||
@@ -44,16 +45,7 @@ export default class View extends Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context)
|
||||
this._handleClick = this._handleClick.bind(this)
|
||||
this._handleClickCapture = this._handleClickCapture.bind(this)
|
||||
this._handleTouchCancel = this._handleTouchCancel.bind(this)
|
||||
this._handleTouchCancelCapture = this._handleTouchCancelCapture.bind(this)
|
||||
this._handleTouchEnd = this._handleTouchEnd.bind(this)
|
||||
this._handleTouchEndCapture = this._handleTouchEndCapture.bind(this)
|
||||
this._handleTouchMove = this._handleTouchMove.bind(this)
|
||||
this._handleTouchMoveCapture = this._handleTouchMoveCapture.bind(this)
|
||||
this._handleTouchStart = this._handleTouchStart.bind(this)
|
||||
this._handleTouchStartCapture = this._handleTouchStartCapture.bind(this)
|
||||
this._normalizeEventForHandler = this._normalizeEventForHandler.bind(this)
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -69,15 +61,15 @@ export default class View extends Component {
|
||||
<CoreComponent
|
||||
{...other}
|
||||
onClick={this._handleClick}
|
||||
onClickCapture={this._handleClickCapture}
|
||||
onTouchCancel={this._handleTouchCancel}
|
||||
onTouchCancelCapture={this._handleTouchCancelCapture}
|
||||
onTouchEnd={this._handleTouchEnd}
|
||||
onTouchEndCapture={this._handleTouchEndCapture}
|
||||
onTouchMove={this._handleTouchMove}
|
||||
onTouchMoveCapture={this._handleTouchMoveCapture}
|
||||
onTouchStart={this._handleTouchStart}
|
||||
onTouchStartCapture={this._handleTouchStartCapture}
|
||||
onClickCapture={this._normalizeEventForHandler(this.props.onClickCapture)}
|
||||
onTouchCancel={this._normalizeEventForHandler(this.props.onTouchCancel)}
|
||||
onTouchCancelCapture={this._normalizeEventForHandler(this.props.onTouchCancelCapture)}
|
||||
onTouchEnd={this._normalizeEventForHandler(this.props.onTouchEnd)}
|
||||
onTouchEndCapture={this._normalizeEventForHandler(this.props.onTouchEndCapture)}
|
||||
onTouchMove={this._normalizeEventForHandler(this.props.onTouchMove)}
|
||||
onTouchMoveCapture={this._normalizeEventForHandler(this.props.onTouchMoveCapture)}
|
||||
onTouchStart={this._normalizeEventForHandler(this.props.onTouchStart)}
|
||||
onTouchStartCapture={this._normalizeEventForHandler(this.props.onTouchStartCapture)}
|
||||
style={[
|
||||
styles.initial,
|
||||
style,
|
||||
@@ -91,73 +83,13 @@ export default class View extends Component {
|
||||
* React Native expects `pageX` and `pageY` to be on the `nativeEvent`, but
|
||||
* React doesn't include them for touch events.
|
||||
*/
|
||||
_normalizeTouchEvent(event) {
|
||||
const { pageX, changedTouches } = event.nativeEvent
|
||||
if (pageX === undefined) {
|
||||
const { pageX, pageY } = changedTouches[0]
|
||||
event.nativeEvent.pageX = pageX
|
||||
event.nativeEvent.pageY = pageY
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
_handleClick(e) {
|
||||
if (this.props.onClick) {
|
||||
this.props.onClick(this._normalizeTouchEvent(e))
|
||||
}
|
||||
}
|
||||
|
||||
_handleClickCapture(e) {
|
||||
if (this.props.onClickCapture) {
|
||||
this.props.onClickCapture(this._normalizeTouchEvent(e))
|
||||
}
|
||||
}
|
||||
|
||||
_handleTouchCancel(e) {
|
||||
if (this.props.onTouchCancel) {
|
||||
this.props.onTouchCancel(this._normalizeTouchEvent(e))
|
||||
}
|
||||
}
|
||||
|
||||
_handleTouchCancelCapture(e) {
|
||||
if (this.props.onTouchCancelCapture) {
|
||||
this.props.onTouchCancelCapture(this._normalizeTouchEvent(e))
|
||||
}
|
||||
}
|
||||
|
||||
_handleTouchEnd(e) {
|
||||
if (this.props.onTouchEnd) {
|
||||
this.props.onTouchEnd(this._normalizeTouchEvent(e))
|
||||
}
|
||||
}
|
||||
|
||||
_handleTouchEndCapture(e) {
|
||||
if (this.props.onTouchEndCapture) {
|
||||
this.props.onTouchEndCapture(this._normalizeTouchEvent(e))
|
||||
}
|
||||
}
|
||||
|
||||
_handleTouchMove(e) {
|
||||
if (this.props.onTouchMove) {
|
||||
this.props.onTouchMove(this._normalizeTouchEvent(e))
|
||||
}
|
||||
}
|
||||
|
||||
_handleTouchMoveCapture(e) {
|
||||
if (this.props.onTouchMoveCapture) {
|
||||
this.props.onTouchMoveCapture(this._normalizeTouchEvent(e))
|
||||
}
|
||||
}
|
||||
|
||||
_handleTouchStart(e) {
|
||||
if (this.props.onTouchStart) {
|
||||
this.props.onTouchStart(this._normalizeTouchEvent(e))
|
||||
}
|
||||
}
|
||||
|
||||
_handleTouchStartCapture(e) {
|
||||
if (this.props.onTouchStartCapture) {
|
||||
this.props.onTouchStartCapture(this._normalizeTouchEvent(e))
|
||||
_normalizeEventForHandler(handler) {
|
||||
return (e) => {
|
||||
const { pageX } = e.nativeEvent
|
||||
if (pageX === undefined) {
|
||||
e.nativeEvent = normalizeNativeEvent(e.nativeEvent)
|
||||
}
|
||||
handler && handler(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -173,15 +105,22 @@ const styles = StyleSheet.create({
|
||||
flexBasis: 'auto',
|
||||
flexDirection: 'column',
|
||||
flexShrink: 0,
|
||||
listStyle: 'none',
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
position: 'relative',
|
||||
textDecoration: 'none',
|
||||
// button reset
|
||||
// button and anchor reset
|
||||
backgroundColor: 'transparent',
|
||||
color: 'inherit',
|
||||
font: 'inherit',
|
||||
textAlign: 'inherit'
|
||||
textAlign: 'inherit',
|
||||
textDecoration: 'none',
|
||||
// list reset
|
||||
listStyle: 'none',
|
||||
// fix flexbox bugs
|
||||
maxWidth: '100%',
|
||||
minHeight: 0,
|
||||
minWidth: 0
|
||||
}
|
||||
})
|
||||
|
||||
module.exports = View
|
||||
|
||||
28
src/index.js
28
src/index.js
@@ -17,6 +17,7 @@ import PanResponder from './apis/PanResponder'
|
||||
import PixelRatio from './apis/PixelRatio'
|
||||
import Platform from './apis/Platform'
|
||||
import StyleSheet from './apis/StyleSheet'
|
||||
import UIManager from './apis/UIManager'
|
||||
|
||||
// components
|
||||
import ActivityIndicator from './components/ActivityIndicator'
|
||||
@@ -26,12 +27,22 @@ import Portal from './components/Portal'
|
||||
import ScrollView from './components/ScrollView'
|
||||
import Text from './components/Text'
|
||||
import TextInput from './components/TextInput'
|
||||
import Touchable from './components/Touchable'
|
||||
import Touchable from './components/Touchable/Touchable'
|
||||
import TouchableBounce from './components/Touchable/TouchableBounce'
|
||||
import TouchableHighlight from './components/Touchable/TouchableHighlight'
|
||||
import TouchableOpacity from './components/Touchable/TouchableOpacity'
|
||||
import TouchableWithoutFeedback from './components/Touchable/TouchableWithoutFeedback'
|
||||
import View from './components/View'
|
||||
|
||||
// modules
|
||||
import NativeModules from './modules/NativeModules'
|
||||
|
||||
// propTypes
|
||||
|
||||
import ColorPropType from './apis/StyleSheet/ColorPropType'
|
||||
import EdgeInsetsPropType from './apis/StyleSheet/EdgeInsetsPropType'
|
||||
import PointPropType from './apis/StyleSheet/PointPropType'
|
||||
|
||||
const ReactNative = {
|
||||
// apis
|
||||
Animated,
|
||||
@@ -46,6 +57,7 @@ const ReactNative = {
|
||||
PixelRatio,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
UIManager,
|
||||
|
||||
// components
|
||||
ActivityIndicator,
|
||||
@@ -55,15 +67,21 @@ const ReactNative = {
|
||||
ScrollView,
|
||||
Text,
|
||||
TextInput,
|
||||
TouchableBounce: Touchable,
|
||||
TouchableHighlight: Touchable,
|
||||
TouchableOpacity: Touchable,
|
||||
TouchableWithoutFeedback: Touchable,
|
||||
Touchable,
|
||||
TouchableBounce,
|
||||
TouchableHighlight,
|
||||
TouchableOpacity,
|
||||
TouchableWithoutFeedback,
|
||||
View,
|
||||
|
||||
// modules
|
||||
NativeModules,
|
||||
|
||||
// propTypes
|
||||
ColorPropType,
|
||||
EdgeInsetsPropType,
|
||||
PointPropType,
|
||||
|
||||
// React
|
||||
...React,
|
||||
...ReactDOM,
|
||||
|
||||
19
src/modules/NativeMethodsDecorator/index.js
Normal file
19
src/modules/NativeMethodsDecorator/index.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Nicolas Gallagher.
|
||||
* All rights reserved.
|
||||
*
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import NativeMethodsMixin from '../NativeMethodsMixin'
|
||||
|
||||
const NativeMethodsDecorator = (Component) => {
|
||||
Object.keys(NativeMethodsMixin).forEach((method) => {
|
||||
if (!Component.prototype[method]) {
|
||||
Component.prototype[method] = NativeMethodsMixin[method]
|
||||
}
|
||||
})
|
||||
return Component
|
||||
}
|
||||
|
||||
module.exports = NativeMethodsDecorator
|
||||
@@ -10,13 +10,11 @@ import { Component } from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import UIManager from '../../apis/UIManager'
|
||||
|
||||
type MeasureOnSuccessCallback = (
|
||||
type MeasureInWindowOnSuccessCallback = (
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
pageX: number,
|
||||
pageY: number
|
||||
) => void
|
||||
|
||||
type MeasureLayoutOnSuccessCallback = (
|
||||
@@ -26,12 +24,21 @@ type MeasureLayoutOnSuccessCallback = (
|
||||
height: number
|
||||
) => void
|
||||
|
||||
type MeasureOnSuccessCallback = (
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
pageX: number,
|
||||
pageY: number
|
||||
) => void
|
||||
|
||||
const NativeMethodsMixin = {
|
||||
/**
|
||||
* Removes focus from an input or view. This is the opposite of `focus()`.
|
||||
*/
|
||||
blur() {
|
||||
ReactDOM.findDOMNode(this).blur()
|
||||
UIManager.blur(ReactDOM.findDOMNode(this))
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -39,7 +46,7 @@ const NativeMethodsMixin = {
|
||||
* The exact behavior triggered will depend the type of view.
|
||||
*/
|
||||
focus() {
|
||||
ReactDOM.findDOMNode(this).focus()
|
||||
UIManager.focus(ReactDOM.findDOMNode(this))
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -52,11 +59,33 @@ const NativeMethodsMixin = {
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines the location of the given view in the window and returns the
|
||||
* values via an async callback. If the React root view is embedded in
|
||||
* another native view, this will give you the absolute coordinates. If
|
||||
* successful, the callback will be called be called with the following
|
||||
* arguments:
|
||||
*
|
||||
* - x
|
||||
* - y
|
||||
* - width
|
||||
* - height
|
||||
*
|
||||
* Note that these measurements are not available until after the rendering
|
||||
* has been completed in native.
|
||||
*/
|
||||
measureInWindow(callback: MeasureInWindowOnSuccessCallback) {
|
||||
UIManager.measureInWindow(
|
||||
ReactDOM.findDOMNode(this),
|
||||
mountSafeCallback(this, callback)
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Measures the view relative to another view (usually an ancestor)
|
||||
*/
|
||||
measureLayout(
|
||||
relativeToNativeNode: number,
|
||||
relativeToNativeNode: Object,
|
||||
onSuccess: MeasureLayoutOnSuccessCallback,
|
||||
onFail: () => void /* currently unused */
|
||||
) {
|
||||
@@ -90,11 +119,4 @@ const mountSafeCallback = (context: Component, callback: ?Function) => () => {
|
||||
return callback.apply(context, arguments)
|
||||
}
|
||||
|
||||
export const NativeMethodsDecorator = (Component) => {
|
||||
Object.keys(NativeMethodsMixin).forEach((method) => {
|
||||
Component.prototype[method] = NativeMethodsMixin[method]
|
||||
})
|
||||
return Component
|
||||
}
|
||||
|
||||
export default NativeMethodsMixin
|
||||
module.exports = NativeMethodsMixin
|
||||
|
||||
@@ -1,3 +1,2 @@
|
||||
// NativeModules shim
|
||||
const NativeModules = {}
|
||||
export default NativeModules
|
||||
module.exports = {}
|
||||
|
||||
538
src/modules/ScrollResponder/index.js
Normal file
538
src/modules/ScrollResponder/index.js
Normal file
@@ -0,0 +1,538 @@
|
||||
/* 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 ScrollResponder
|
||||
* @flow
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var Dimensions = require('../../apis/Dimensions');
|
||||
var Platform = require('../../apis/Platform');
|
||||
var React = require('react');
|
||||
var ReactDOM = require('react-dom');
|
||||
// var Subscribable = require('../Subscribable');
|
||||
var TextInputState = require('../../components/TextInput/TextInputState');
|
||||
var UIManager = require('../../apis/UIManager');
|
||||
|
||||
// var { ScrollViewManager } = require('../../modules/NativeModules');
|
||||
|
||||
var invariant = require('fbjs/lib/invariant');
|
||||
var warning = require('fbjs/lib/warning');
|
||||
|
||||
// type Component = React.Component
|
||||
|
||||
/**
|
||||
* Mixin that can be integrated in order to handle scrolling that plays well
|
||||
* with `ResponderEventPlugin`. Integrate with your platform specific scroll
|
||||
* views, or even your custom built (every-frame animating) scroll views so that
|
||||
* all of these systems play well with the `ResponderEventPlugin`.
|
||||
*
|
||||
* iOS scroll event timing nuances:
|
||||
* ===============================
|
||||
*
|
||||
*
|
||||
* Scrolling without bouncing, if you touch down:
|
||||
* -------------------------------
|
||||
*
|
||||
* 1. `onMomentumScrollBegin` (when animation begins after letting up)
|
||||
* ... physical touch starts ...
|
||||
* 2. `onTouchStartCapture` (when you press down to stop the scroll)
|
||||
* 3. `onTouchStart` (same, but bubble phase)
|
||||
* 4. `onResponderRelease` (when lifting up - you could pause forever before * lifting)
|
||||
* 5. `onMomentumScrollEnd`
|
||||
*
|
||||
*
|
||||
* Scrolling with bouncing, if you touch down:
|
||||
* -------------------------------
|
||||
*
|
||||
* 1. `onMomentumScrollBegin` (when animation begins after letting up)
|
||||
* ... bounce begins ...
|
||||
* ... some time elapses ...
|
||||
* ... physical touch during bounce ...
|
||||
* 2. `onMomentumScrollEnd` (Makes no sense why this occurs first during bounce)
|
||||
* 3. `onTouchStartCapture` (immediately after `onMomentumScrollEnd`)
|
||||
* 4. `onTouchStart` (same, but bubble phase)
|
||||
* 5. `onTouchEnd` (You could hold the touch start for a long time)
|
||||
* 6. `onMomentumScrollBegin` (When releasing the view starts bouncing back)
|
||||
*
|
||||
* So when we receive an `onTouchStart`, how can we tell if we are touching
|
||||
* *during* an animation (which then causes the animation to stop)? The only way
|
||||
* to tell is if the `touchStart` occurred immediately after the
|
||||
* `onMomentumScrollEnd`.
|
||||
*
|
||||
* This is abstracted out for you, so you can just call this.scrollResponderIsAnimating() if
|
||||
* necessary
|
||||
*
|
||||
* `ScrollResponder` also includes logic for blurring a currently focused input
|
||||
* if one is focused while scrolling. The `ScrollResponder` is a natural place
|
||||
* to put this logic since it can support not dismissing the keyboard while
|
||||
* scrolling, unless a recognized "tap"-like gesture has occurred.
|
||||
*
|
||||
* The public lifecycle API includes events for keyboard interaction, responder
|
||||
* interaction, and scrolling (among others). The keyboard callbacks
|
||||
* `onKeyboardWill/Did/*` are *global* events, but are invoked on scroll
|
||||
* responder's props so that you can guarantee that the scroll responder's
|
||||
* internal state has been updated accordingly (and deterministically) by
|
||||
* the time the props callbacks are invoke. Otherwise, you would always wonder
|
||||
* if the scroll responder is currently in a state where it recognizes new
|
||||
* keyboard positions etc. If coordinating scrolling with keyboard movement,
|
||||
* *always* use these hooks instead of listening to your own global keyboard
|
||||
* events.
|
||||
*
|
||||
* Public keyboard lifecycle API: (props callbacks)
|
||||
*
|
||||
* Standard Keyboard Appearance Sequence:
|
||||
*
|
||||
* this.props.onKeyboardWillShow
|
||||
* this.props.onKeyboardDidShow
|
||||
*
|
||||
* `onScrollResponderKeyboardDismissed` will be invoked if an appropriate
|
||||
* tap inside the scroll responder's scrollable region was responsible
|
||||
* for the dismissal of the keyboard. There are other reasons why the
|
||||
* keyboard could be dismissed.
|
||||
*
|
||||
* this.props.onScrollResponderKeyboardDismissed
|
||||
*
|
||||
* Standard Keyboard Hide Sequence:
|
||||
*
|
||||
* this.props.onKeyboardWillHide
|
||||
* this.props.onKeyboardDidHide
|
||||
*/
|
||||
|
||||
var IS_ANIMATING_TOUCH_START_THRESHOLD_MS = 16;
|
||||
|
||||
type State = {
|
||||
isTouching: boolean;
|
||||
lastMomentumScrollBeginTime: number;
|
||||
lastMomentumScrollEndTime: number;
|
||||
observedScrollSinceBecomingResponder: boolean;
|
||||
becameResponderWhileAnimating: boolean;
|
||||
};
|
||||
type Event = Object;
|
||||
|
||||
var ScrollResponderMixin = {
|
||||
// mixins: [Subscribable.Mixin],
|
||||
scrollResponderMixinGetInitialState: function(): State {
|
||||
return {
|
||||
isTouching: false,
|
||||
lastMomentumScrollBeginTime: 0,
|
||||
lastMomentumScrollEndTime: 0,
|
||||
|
||||
// Reset to false every time becomes responder. This is used to:
|
||||
// - Determine if the scroll view has been scrolled and therefore should
|
||||
// refuse to give up its responder lock.
|
||||
// - Determine if releasing should dismiss the keyboard when we are in
|
||||
// tap-to-dismiss mode (!this.props.keyboardShouldPersistTaps).
|
||||
observedScrollSinceBecomingResponder: false,
|
||||
becameResponderWhileAnimating: false,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke this from an `onScroll` event.
|
||||
*/
|
||||
scrollResponderHandleScrollShouldSetResponder: function(): boolean {
|
||||
return this.state.isTouching;
|
||||
},
|
||||
|
||||
/**
|
||||
* Merely touch starting is not sufficient for a scroll view to become the
|
||||
* responder. Being the "responder" means that the very next touch move/end
|
||||
* event will result in an action/movement.
|
||||
*
|
||||
* Invoke this from an `onStartShouldSetResponder` event.
|
||||
*
|
||||
* `onStartShouldSetResponder` is used when the next move/end will trigger
|
||||
* some UI movement/action, but when you want to yield priority to views
|
||||
* nested inside of the view.
|
||||
*
|
||||
* There may be some cases where scroll views actually should return `true`
|
||||
* from `onStartShouldSetResponder`: Any time we are detecting a standard tap
|
||||
* that gives priority to nested views.
|
||||
*
|
||||
* - If a single tap on the scroll view triggers an action such as
|
||||
* recentering a map style view yet wants to give priority to interaction
|
||||
* views inside (such as dropped pins or labels), then we would return true
|
||||
* from this method when there is a single touch.
|
||||
*
|
||||
* - Similar to the previous case, if a two finger "tap" should trigger a
|
||||
* zoom, we would check the `touches` count, and if `>= 2`, we would return
|
||||
* true.
|
||||
*
|
||||
*/
|
||||
scrollResponderHandleStartShouldSetResponder: function(): boolean {
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* There are times when the scroll view wants to become the responder
|
||||
* (meaning respond to the next immediate `touchStart/touchEnd`), in a way
|
||||
* that *doesn't* give priority to nested views (hence the capture phase):
|
||||
*
|
||||
* - Currently animating.
|
||||
* - Tapping anywhere that is not the focused input, while the keyboard is
|
||||
* up (which should dismiss the keyboard).
|
||||
*
|
||||
* Invoke this from an `onStartShouldSetResponderCapture` event.
|
||||
*/
|
||||
scrollResponderHandleStartShouldSetResponderCapture: function(e: Event): boolean {
|
||||
// First see if we want to eat taps while the keyboard is up
|
||||
// var currentlyFocusedTextInput = TextInputState.currentlyFocusedField();
|
||||
// if (!this.props.keyboardShouldPersistTaps &&
|
||||
// currentlyFocusedTextInput != null &&
|
||||
// e.target !== currentlyFocusedTextInput) {
|
||||
// return true;
|
||||
// }
|
||||
return this.scrollResponderIsAnimating();
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke this from an `onResponderReject` event.
|
||||
*
|
||||
* Some other element is not yielding its role as responder. Normally, we'd
|
||||
* just disable the `UIScrollView`, but a touch has already began on it, the
|
||||
* `UIScrollView` will not accept being disabled after that. The easiest
|
||||
* solution for now is to accept the limitation of disallowing this
|
||||
* altogether. To improve this, find a way to disable the `UIScrollView` after
|
||||
* a touch has already started.
|
||||
*/
|
||||
scrollResponderHandleResponderReject: function() {
|
||||
warning(false, "ScrollView doesn't take rejection well - scrolls anyway");
|
||||
},
|
||||
|
||||
/**
|
||||
* We will allow the scroll view to give up its lock iff it acquired the lock
|
||||
* during an animation. This is a very useful default that happens to satisfy
|
||||
* many common user experiences.
|
||||
*
|
||||
* - Stop a scroll on the left edge, then turn that into an outer view's
|
||||
* backswipe.
|
||||
* - Stop a scroll mid-bounce at the top, continue pulling to have the outer
|
||||
* view dismiss.
|
||||
* - However, without catching the scroll view mid-bounce (while it is
|
||||
* motionless), if you drag far enough for the scroll view to become
|
||||
* responder (and therefore drag the scroll view a bit), any backswipe
|
||||
* navigation of a swipe gesture higher in the view hierarchy, should be
|
||||
* rejected.
|
||||
*/
|
||||
scrollResponderHandleTerminationRequest: function(): boolean {
|
||||
return !this.state.observedScrollSinceBecomingResponder;
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke this from an `onTouchEnd` event.
|
||||
*
|
||||
* @param {SyntheticEvent} e Event.
|
||||
*/
|
||||
scrollResponderHandleTouchEnd: function(e: Event) {
|
||||
var nativeEvent = e.nativeEvent;
|
||||
this.state.isTouching = nativeEvent.touches.length !== 0;
|
||||
this.props.onTouchEnd && this.props.onTouchEnd(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke this from an `onResponderRelease` event.
|
||||
*/
|
||||
scrollResponderHandleResponderRelease: function(e: Event) {
|
||||
this.props.onResponderRelease && this.props.onResponderRelease(e);
|
||||
|
||||
// By default scroll views will unfocus a textField
|
||||
// if another touch occurs outside of it
|
||||
var currentlyFocusedTextInput = TextInputState.currentlyFocusedField();
|
||||
if (!this.props.keyboardShouldPersistTaps &&
|
||||
currentlyFocusedTextInput != null &&
|
||||
e.target !== currentlyFocusedTextInput &&
|
||||
!this.state.observedScrollSinceBecomingResponder &&
|
||||
!this.state.becameResponderWhileAnimating) {
|
||||
this.props.onScrollResponderKeyboardDismissed &&
|
||||
this.props.onScrollResponderKeyboardDismissed(e);
|
||||
TextInputState.blurTextInput(currentlyFocusedTextInput);
|
||||
}
|
||||
},
|
||||
|
||||
scrollResponderHandleScroll: function(e: Event) {
|
||||
this.state.observedScrollSinceBecomingResponder = true;
|
||||
this.props.onScroll && this.props.onScroll(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke this from an `onResponderGrant` event.
|
||||
*/
|
||||
scrollResponderHandleResponderGrant: function(e: Event) {
|
||||
this.state.observedScrollSinceBecomingResponder = false;
|
||||
this.props.onResponderGrant && this.props.onResponderGrant(e);
|
||||
this.state.becameResponderWhileAnimating = this.scrollResponderIsAnimating();
|
||||
},
|
||||
|
||||
/**
|
||||
* Unfortunately, `onScrollBeginDrag` also fires when *stopping* the scroll
|
||||
* animation, and there's not an easy way to distinguish a drag vs. stopping
|
||||
* momentum.
|
||||
*
|
||||
* Invoke this from an `onScrollBeginDrag` event.
|
||||
*/
|
||||
scrollResponderHandleScrollBeginDrag: function(e: Event) {
|
||||
this.props.onScrollBeginDrag && this.props.onScrollBeginDrag(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke this from an `onScrollEndDrag` event.
|
||||
*/
|
||||
scrollResponderHandleScrollEndDrag: function(e: Event) {
|
||||
this.props.onScrollEndDrag && this.props.onScrollEndDrag(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke this from an `onMomentumScrollBegin` event.
|
||||
*/
|
||||
scrollResponderHandleMomentumScrollBegin: function(e: Event) {
|
||||
this.state.lastMomentumScrollBeginTime = Date.now();
|
||||
this.props.onMomentumScrollBegin && this.props.onMomentumScrollBegin(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke this from an `onMomentumScrollEnd` event.
|
||||
*/
|
||||
scrollResponderHandleMomentumScrollEnd: function(e: Event) {
|
||||
this.state.lastMomentumScrollEndTime = Date.now();
|
||||
this.props.onMomentumScrollEnd && this.props.onMomentumScrollEnd(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke this from an `onTouchStart` event.
|
||||
*
|
||||
* Since we know that the `SimpleEventPlugin` occurs later in the plugin
|
||||
* order, after `ResponderEventPlugin`, we can detect that we were *not*
|
||||
* permitted to be the responder (presumably because a contained view became
|
||||
* responder). The `onResponderReject` won't fire in that case - it only
|
||||
* fires when a *current* responder rejects our request.
|
||||
*
|
||||
* @param {SyntheticEvent} e Touch Start event.
|
||||
*/
|
||||
scrollResponderHandleTouchStart: function(e: Event) {
|
||||
this.state.isTouching = true;
|
||||
this.props.onTouchStart && this.props.onTouchStart(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* Invoke this from an `onTouchMove` event.
|
||||
*
|
||||
* Since we know that the `SimpleEventPlugin` occurs later in the plugin
|
||||
* order, after `ResponderEventPlugin`, we can detect that we were *not*
|
||||
* permitted to be the responder (presumably because a contained view became
|
||||
* responder). The `onResponderReject` won't fire in that case - it only
|
||||
* fires when a *current* responder rejects our request.
|
||||
*
|
||||
* @param {SyntheticEvent} e Touch Start event.
|
||||
*/
|
||||
scrollResponderHandleTouchMove: function(e: Event) {
|
||||
this.props.onTouchMove && this.props.onTouchMove(e);
|
||||
},
|
||||
|
||||
/**
|
||||
* A helper function for this class that lets us quickly determine if the
|
||||
* view is currently animating. This is particularly useful to know when
|
||||
* a touch has just started or ended.
|
||||
*/
|
||||
scrollResponderIsAnimating: function(): boolean {
|
||||
var now = Date.now();
|
||||
var timeSinceLastMomentumScrollEnd = now - this.state.lastMomentumScrollEndTime;
|
||||
var isAnimating = timeSinceLastMomentumScrollEnd < IS_ANIMATING_TOUCH_START_THRESHOLD_MS ||
|
||||
this.state.lastMomentumScrollEndTime < this.state.lastMomentumScrollBeginTime;
|
||||
return isAnimating;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the node that represents native view that can be scrolled.
|
||||
* Components can pass what node to use by defining a `getScrollableNode`
|
||||
* function otherwise `this` is used.
|
||||
*/
|
||||
scrollResponderGetScrollableNode: function(): any {
|
||||
return this.getScrollableNode ?
|
||||
this.getScrollableNode() :
|
||||
ReactDOM.findDOMNode(this);
|
||||
},
|
||||
|
||||
/**
|
||||
* A helper function to scroll to a specific point in the scrollview.
|
||||
* This is currently used to help focus on child textviews, but can also
|
||||
* be used to quickly scroll to any element we want to focus. Syntax:
|
||||
*
|
||||
* scrollResponderScrollTo(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.
|
||||
*/
|
||||
scrollResponderScrollTo: function(
|
||||
x?: number | { x?: number; y?: number; animated?: boolean },
|
||||
y?: number,
|
||||
animated?: boolean
|
||||
) {
|
||||
if (typeof x === 'number') {
|
||||
console.warn('`scrollResponderScrollTo(x, y, animated)` is deprecated. Use `scrollResponderScrollTo({x: 5, y: 5, animated: true})` instead.');
|
||||
} else {
|
||||
({x, y, animated} = x || {});
|
||||
}
|
||||
const node = this.scrollResponderGetScrollableNode()
|
||||
node.scrollLeft = x || 0
|
||||
node.scrollTop = y || 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Deprecated, do not use.
|
||||
*/
|
||||
scrollResponderScrollWithoutAnimationTo: function(offsetX: number, offsetY: number) {
|
||||
console.warn('`scrollResponderScrollWithoutAnimationTo` is deprecated. Use `scrollResponderScrollTo` instead');
|
||||
this.scrollResponderScrollTo({x: offsetX, y: offsetY, animated: false});
|
||||
},
|
||||
|
||||
/**
|
||||
* A helper function to zoom to a specific rect in the scrollview. The argument has the shape
|
||||
* {x: number; y: number; width: number; height: number; animated: boolean = true}
|
||||
*
|
||||
* @platform ios
|
||||
*/
|
||||
scrollResponderZoomTo: function(
|
||||
rect: { x: number; y: number; width: number; height: number; animated?: boolean },
|
||||
animated?: boolean // deprecated, put this inside the rect argument instead
|
||||
) {
|
||||
if (Platform.OS !== 'ios') {
|
||||
invariant('zoomToRect is not implemented');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* This method should be used as the callback to onFocus in a TextInputs'
|
||||
* parent view. Note that any module using this mixin needs to return
|
||||
* the parent view's ref in getScrollViewRef() in order to use this method.
|
||||
* @param {any} nodeHandle The TextInput node handle
|
||||
* @param {number} additionalOffset The scroll view's top "contentInset".
|
||||
* Default is 0.
|
||||
* @param {bool} preventNegativeScrolling Whether to allow pulling the content
|
||||
* down to make it meet the keyboard's top. Default is false.
|
||||
*/
|
||||
scrollResponderScrollNativeHandleToKeyboard: function(nodeHandle: any, additionalOffset?: number, preventNegativeScrollOffset?: bool) {
|
||||
this.additionalScrollOffset = additionalOffset || 0;
|
||||
this.preventNegativeScrollOffset = !!preventNegativeScrollOffset;
|
||||
UIManager.measureLayout(
|
||||
nodeHandle,
|
||||
ReactDOM.findDOMNode(this.getInnerViewNode()),
|
||||
this.scrollResponderTextInputFocusError,
|
||||
this.scrollResponderInputMeasureAndScrollToKeyboard
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* The calculations performed here assume the scroll view takes up the entire
|
||||
* screen - even if has some content inset. We then measure the offsets of the
|
||||
* keyboard, and compensate both for the scroll view's "contentInset".
|
||||
*
|
||||
* @param {number} left Position of input w.r.t. table view.
|
||||
* @param {number} top Position of input w.r.t. table view.
|
||||
* @param {number} width Width of the text input.
|
||||
* @param {number} height Height of the text input.
|
||||
*/
|
||||
scrollResponderInputMeasureAndScrollToKeyboard: function(left: number, top: number, width: number, height: number) {
|
||||
var keyboardScreenY = Dimensions.get('window').height;
|
||||
if (this.keyboardWillOpenTo) {
|
||||
keyboardScreenY = this.keyboardWillOpenTo.endCoordinates.screenY;
|
||||
}
|
||||
var scrollOffsetY = top - keyboardScreenY + height + this.additionalScrollOffset;
|
||||
|
||||
// By default, this can scroll with negative offset, pulling the content
|
||||
// down so that the target component's bottom meets the keyboard's top.
|
||||
// If requested otherwise, cap the offset at 0 minimum to avoid content
|
||||
// shifting down.
|
||||
if (this.preventNegativeScrollOffset) {
|
||||
scrollOffsetY = Math.max(0, scrollOffsetY);
|
||||
}
|
||||
this.scrollResponderScrollTo({x: 0, y: scrollOffsetY, animated: true});
|
||||
|
||||
this.additionalOffset = 0;
|
||||
this.preventNegativeScrollOffset = false;
|
||||
},
|
||||
|
||||
scrollResponderTextInputFocusError: function(e: Event) {
|
||||
console.error('Error measuring text field: ', e);
|
||||
},
|
||||
|
||||
/**
|
||||
* `componentWillMount` is the closest thing to a standard "constructor" for
|
||||
* React components.
|
||||
*
|
||||
* The `keyboardWillShow` is called before input focus.
|
||||
*/
|
||||
componentWillMount: function() {
|
||||
this.keyboardWillOpenTo = null;
|
||||
this.additionalScrollOffset = 0;
|
||||
// this.addListenerOn(RCTDeviceEventEmitter, 'keyboardWillShow', this.scrollResponderKeyboardWillShow);
|
||||
// this.addListenerOn(RCTDeviceEventEmitter, 'keyboardWillHide', this.scrollResponderKeyboardWillHide);
|
||||
// this.addListenerOn(RCTDeviceEventEmitter, 'keyboardDidShow', this.scrollResponderKeyboardDidShow);
|
||||
// this.addListenerOn(RCTDeviceEventEmitter, 'keyboardDidHide', this.scrollResponderKeyboardDidHide);
|
||||
},
|
||||
|
||||
/**
|
||||
* Warning, this may be called several times for a single keyboard opening.
|
||||
* It's best to store the information in this method and then take any action
|
||||
* at a later point (either in `keyboardDidShow` or other).
|
||||
*
|
||||
* Here's the order that events occur in:
|
||||
* - focus
|
||||
* - willShow {startCoordinates, endCoordinates} several times
|
||||
* - didShow several times
|
||||
* - blur
|
||||
* - willHide {startCoordinates, endCoordinates} several times
|
||||
* - didHide several times
|
||||
*
|
||||
* The `ScrollResponder` providesModule callbacks for each of these events.
|
||||
* Even though any user could have easily listened to keyboard events
|
||||
* themselves, using these `props` callbacks ensures that ordering of events
|
||||
* is consistent - and not dependent on the order that the keyboard events are
|
||||
* subscribed to. This matters when telling the scroll view to scroll to where
|
||||
* the keyboard is headed - the scroll responder better have been notified of
|
||||
* the keyboard destination before being instructed to scroll to where the
|
||||
* keyboard will be. Stick to the `ScrollResponder` callbacks, and everything
|
||||
* will work.
|
||||
*
|
||||
* WARNING: These callbacks will fire even if a keyboard is displayed in a
|
||||
* different navigation pane. Filter out the events to determine if they are
|
||||
* relevant to you. (For example, only if you receive these callbacks after
|
||||
* you had explicitly focused a node etc).
|
||||
*/
|
||||
scrollResponderKeyboardWillShow: function(e: Event) {
|
||||
this.keyboardWillOpenTo = e;
|
||||
this.props.onKeyboardWillShow && this.props.onKeyboardWillShow(e);
|
||||
},
|
||||
|
||||
scrollResponderKeyboardWillHide: function(e: Event) {
|
||||
this.keyboardWillOpenTo = null;
|
||||
this.props.onKeyboardWillHide && this.props.onKeyboardWillHide(e);
|
||||
},
|
||||
|
||||
scrollResponderKeyboardDidShow: function(e: Event) {
|
||||
// TODO(7693961): The event for DidShow is not available on iOS yet.
|
||||
// Use the one from WillShow and do not assign.
|
||||
if (e) {
|
||||
this.keyboardWillOpenTo = e;
|
||||
}
|
||||
this.props.onKeyboardDidShow && this.props.onKeyboardDidShow(e);
|
||||
},
|
||||
|
||||
scrollResponderKeyboardDidHide: function(e: Event) {
|
||||
this.keyboardWillOpenTo = null;
|
||||
this.props.onKeyboardDidHide && this.props.onKeyboardDidHide(e);
|
||||
}
|
||||
};
|
||||
|
||||
var ScrollResponder = {
|
||||
Mixin: ScrollResponderMixin,
|
||||
};
|
||||
|
||||
module.exports = ScrollResponder;
|
||||
7
src/modules/dismissKeyboard/index.js
Normal file
7
src/modules/dismissKeyboard/index.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import TextInputState from '../../components/TextInput/TextInputState'
|
||||
|
||||
const dismissKeyboard = () => {
|
||||
TextInputState.blurTextInput(TextInputState.currentlyFocusedField())
|
||||
}
|
||||
|
||||
module.exports = dismissKeyboard
|
||||
222
src/modules/merge/index.js
Normal file
222
src/modules/merge/index.js
Normal file
@@ -0,0 +1,222 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* @generated SignedSource<<b68d78236d45828b3f7f7fcc740782a9>>
|
||||
*
|
||||
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
* !! This file is a check-in of a static_upstream project! !!
|
||||
* !! !!
|
||||
* !! You should not modify this file directly. Instead: !!
|
||||
* !! 1) Use `fjs use-upstream` to temporarily replace this with !!
|
||||
* !! the latest version from upstream. !!
|
||||
* !! 2) Make your changes, test them, etc. !!
|
||||
* !! 3) Use `fjs push-upstream` to copy your changes back to !!
|
||||
* !! static_upstream. !!
|
||||
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
*
|
||||
* Copyright 2013-2014 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule mergeHelpers
|
||||
*
|
||||
* requiresPolyfills: Array.isArray
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
var invariant = require('fbjs/lib/invariant');
|
||||
var keyMirror = require('fbjs/lib/keyMirror');
|
||||
|
||||
/**
|
||||
* Maximum number of levels to traverse. Will catch circular structures.
|
||||
* @const
|
||||
*/
|
||||
var MAX_MERGE_DEPTH = 36;
|
||||
|
||||
/**
|
||||
* We won't worry about edge cases like new String('x') or new Boolean(true).
|
||||
* Functions are considered terminals, and arrays are not.
|
||||
* @param {*} o The item/object/value to test.
|
||||
* @return {boolean} true iff the argument is a terminal.
|
||||
*/
|
||||
var isTerminal = function(o) {
|
||||
return typeof o !== 'object' || o === null;
|
||||
};
|
||||
|
||||
var mergeHelpers = {
|
||||
|
||||
MAX_MERGE_DEPTH: MAX_MERGE_DEPTH,
|
||||
|
||||
isTerminal: isTerminal,
|
||||
|
||||
/**
|
||||
* Converts null/undefined values into empty object.
|
||||
*
|
||||
* @param {?Object=} arg Argument to be normalized (nullable optional)
|
||||
* @return {!Object}
|
||||
*/
|
||||
normalizeMergeArg: function(arg) {
|
||||
return arg === undefined || arg === null ? {} : arg;
|
||||
},
|
||||
|
||||
/**
|
||||
* If merging Arrays, a merge strategy *must* be supplied. If not, it is
|
||||
* likely the caller's fault. If this function is ever called with anything
|
||||
* but `one` and `two` being `Array`s, it is the fault of the merge utilities.
|
||||
*
|
||||
* @param {*} one Array to merge into.
|
||||
* @param {*} two Array to merge from.
|
||||
*/
|
||||
checkMergeArrayArgs: function(one, two) {
|
||||
invariant(
|
||||
Array.isArray(one) && Array.isArray(two),
|
||||
'Tried to merge arrays, instead got %s and %s.',
|
||||
one,
|
||||
two
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {*} one Object to merge into.
|
||||
* @param {*} two Object to merge from.
|
||||
*/
|
||||
checkMergeObjectArgs: function(one, two) {
|
||||
mergeHelpers.checkMergeObjectArg(one);
|
||||
mergeHelpers.checkMergeObjectArg(two);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {*} arg
|
||||
*/
|
||||
checkMergeObjectArg: function(arg) {
|
||||
invariant(
|
||||
!isTerminal(arg) && !Array.isArray(arg),
|
||||
'Tried to merge an object, instead got %s.',
|
||||
arg
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {*} arg
|
||||
*/
|
||||
checkMergeIntoObjectArg: function(arg) {
|
||||
invariant(
|
||||
(!isTerminal(arg) || typeof arg === 'function') && !Array.isArray(arg),
|
||||
'Tried to merge into an object, instead got %s.',
|
||||
arg
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks that a merge was not given a circular object or an object that had
|
||||
* too great of depth.
|
||||
*
|
||||
* @param {number} Level of recursion to validate against maximum.
|
||||
*/
|
||||
checkMergeLevel: function(level) {
|
||||
invariant(
|
||||
level < MAX_MERGE_DEPTH,
|
||||
'Maximum deep merge depth exceeded. You may be attempting to merge ' +
|
||||
'circular structures in an unsupported way.'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks that the supplied merge strategy is valid.
|
||||
*
|
||||
* @param {string} Array merge strategy.
|
||||
*/
|
||||
checkArrayStrategy: function(strategy) {
|
||||
invariant(
|
||||
strategy === undefined || strategy in mergeHelpers.ArrayStrategies,
|
||||
'You must provide an array strategy to deep merge functions to ' +
|
||||
'instruct the deep merge how to resolve merging two arrays.'
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Set of possible behaviors of merge algorithms when encountering two Arrays
|
||||
* that must be merged together.
|
||||
* - `clobber`: The left `Array` is ignored.
|
||||
* - `indexByIndex`: The result is achieved by recursively deep merging at
|
||||
* each index. (not yet supported.)
|
||||
*/
|
||||
ArrayStrategies: keyMirror({
|
||||
Clobber: true,
|
||||
IndexByIndex: true
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* @generated SignedSource<<d3caa35be27b17ea4dd4c76bef72d1ab>>
|
||||
*
|
||||
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
* !! This file is a check-in of a static_upstream project! !!
|
||||
* !! !!
|
||||
* !! You should not modify this file directly. Instead: !!
|
||||
* !! 1) Use `fjs use-upstream` to temporarily replace this with !!
|
||||
* !! the latest version from upstream. !!
|
||||
* !! 2) Make your changes, test them, etc. !!
|
||||
* !! 3) Use `fjs push-upstream` to copy your changes back to !!
|
||||
* !! static_upstream. !!
|
||||
* !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
*
|
||||
* Copyright 2013-2014 Facebook, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @providesModule mergeInto
|
||||
* @typechecks static-only
|
||||
*/
|
||||
|
||||
var checkMergeObjectArg = mergeHelpers.checkMergeObjectArg;
|
||||
var checkMergeIntoObjectArg = mergeHelpers.checkMergeIntoObjectArg;
|
||||
|
||||
/**
|
||||
* Shallow merges two structures by mutating the first parameter.
|
||||
*
|
||||
* @param {object|function} one Object to be merged into.
|
||||
* @param {?object} two Optional object with properties to merge from.
|
||||
*/
|
||||
function mergeInto(one, two) {
|
||||
checkMergeIntoObjectArg(one);
|
||||
if (two != null) {
|
||||
checkMergeObjectArg(two);
|
||||
for (var key in two) {
|
||||
if (!two.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
one[key] = two[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var merge = function(one, two) {
|
||||
var result = {};
|
||||
mergeInto(result, one);
|
||||
mergeInto(result, two);
|
||||
return result;
|
||||
};
|
||||
|
||||
module.exports = merge;
|
||||
Reference in New Issue
Block a user