Last active
January 31, 2018 08:38
-
-
Save YuCJ/c5fdbc7fbdd5f14bdd924cf48fc5a9da to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint no-restricted-globals: 0, react/no-did-mount-set-state: 0 */ | |
import PropTypes from 'prop-types' | |
import React from 'react' | |
import styled, { css } from 'styled-components' | |
import Swipeable from 'react-swipeable' | |
// lodash | |
import get from 'lodash.get' | |
import throttle from 'lodash.throttle' | |
const _ = { | |
get, | |
throttle, | |
} | |
/* | |
`globalCssForViewport` will be added to global style via `injectGlobal` method in `styled-components`. | |
The content of `globalCssForViewport` is used to: | |
1. Set all ancestor elements of `Viewport` with `height: 100%`. | |
2. Prevent over-scroll effects on mobile devices. | |
3. Prevent highlight and outline of elements when user touch screen (focus on the element) on mobile device. | |
*/ | |
const reactRootSelector = '#root' | |
const globalCssForViewport = css` | |
html, body { | |
touch-action: manipulation; | |
overflow: hidden; | |
height: 100%; | |
width: 100%; | |
margin: 0; | |
padding: 0; | |
position: relative; | |
} | |
html, body, ${reactRootSelector} { | |
height: 100%; | |
overflow: hidden; | |
} | |
* { | |
box-sizing: border-box; | |
-webkit-tap-highlight-color: rgba(255, 255, 255, 0) !important; | |
-webkit-focus-ring-color: rgba(255, 255, 255, 0) !important; | |
outline: none !important; | |
} | |
` | |
const wheelThreshold = 0 | |
const onWheelThrottleWaitTime = 1600 | |
const pageChangeThrottleWaitTime = 900 | |
const Container = styled.div` | |
user-select: none; | |
box-sizing: border-box; | |
background-color: ${props => props.backgroundColor}; | |
width: 100%; | |
height: 100%; | |
overflow: hidden; | |
position: fixed; | |
top: 0; | |
left: 0; | |
` | |
class Viewport extends React.PureComponent { | |
constructor(props) { | |
super(props) | |
this.state = { | |
currentIndex: 0, | |
} | |
} | |
componentDidMount() { | |
this._jumpToPageViaHash() | |
} | |
/* | |
`_jumpToPageViaHash` is used to let developer jump to specific page via giving the page number with url hash. | |
Example: Requesting http://localhost:3000/#10 will make the component | |
jump to page 10 on client side when the compoenent be mounted. | |
(In chrome, you need to refresh the page to trigger the re-mount process.) | |
*/ | |
_jumpToPageViaHash() { | |
const { hash } = _.get(window, 'location') | |
if (hash) { | |
const targetIndex = parseInt(hash.substring(1), 10) | |
const { nOfIndex } = this.props | |
if (targetIndex >= 0 && targetIndex < nOfIndex) { | |
return this.setState({ | |
currentIndex: targetIndex, | |
}) | |
} | |
} | |
} | |
onKeyDown = (e) => { | |
switch (e.key) { | |
case 'PageDown': | |
case 'Down': | |
case 'Enter': | |
case ' ': | |
case 'ArrowRight': | |
case 'Right': | |
case 'ArrowDown': | |
case 'Spacebar': | |
e.preventDefault() | |
return this.changeIndex(this.state.currentIndex + 1) | |
case 'ArrowUp': | |
case 'Up': | |
case 'ArrowLeft': | |
case 'Left': | |
case 'PageUp': | |
e.preventDefault() | |
return this.changeIndex(this.state.currentIndex - 1) | |
default: | |
return null | |
} | |
} | |
/* | |
When a user swipes on laptop touchpad with two fingers once, | |
it will cause lots of wheeling events during about 1.6s. | |
So we need to throttle it. | |
*/ | |
onWheel = _.throttle((e) => { | |
if (Math.abs(e.deltaY) > wheelThreshold) { | |
if (e.deltaY > 0) { | |
return this.changeIndex(this.state.currentIndex + 1) | |
} | |
if (e.deltaY < 0) { | |
return this.changeIndex(this.state.currentIndex - 1) | |
} | |
} | |
}, onWheelThrottleWaitTime, { leading: true, trailing: false }) | |
_isIndexValueValid(index) { | |
const { nOfIndex } = this.props | |
return (index >= 0 && index < nOfIndex) | |
} | |
changeIndex = _.throttle((targetIndex) => { | |
if (this._isIndexValueValid(targetIndex)) { | |
if (targetIndex !== this.state.currentIndex) { | |
this.setState({ | |
currentIndex: targetIndex, | |
}) | |
} | |
} | |
}, pageChangeThrottleWaitTime, { leading: true, trailing: false }) | |
goToNextIndex = () => { | |
return this.changeIndex(this.state.currentIndex + 1) | |
} | |
goToPrevIndex = () => { | |
return this.changeIndex(this.state.currentIndex - 1) | |
} | |
/* | |
Use `_addPropsToChild` with `React.Children.map` we can add props to all children. | |
*/ | |
_addPropsToChild = (child) => { | |
return React.cloneElement(child, { | |
currentIndex: this.state.currentIndex, | |
goToNextIndex: this.goToNextIndex, | |
}) | |
} | |
render() { | |
const { backgroundColor, children } = this.props | |
return ( | |
<Container | |
backgroundColor={backgroundColor} | |
onKeyDown={this.onKeyDown} | |
onWheel={this.onWheel} | |
> | |
<Swipeable | |
tabIndex="0" | |
style={{ height: '100%', position: 'relative' }} | |
onSwipedDown={this.goToPrevIndex} | |
onSwipedUp={this.goToNextIndex} | |
> | |
{React.Children.map(children, this._addPropsToChild)} | |
</Swipeable> | |
</Container> | |
) | |
} | |
} | |
Viewport.propTypes = { | |
children: PropTypes.node.isRequired, | |
backgroundColor: PropTypes.string, | |
nOfIndex: PropTypes.number.isRequired, | |
} | |
Viewport.defaultProps = { | |
backgroundColor: '#1d1d1d', | |
} | |
export default Viewport | |
export { globalCssForViewport } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment