Skip to content

Instantly share code, notes, and snippets.

@YuCJ
Last active January 31, 2018 08:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save YuCJ/c5fdbc7fbdd5f14bdd924cf48fc5a9da to your computer and use it in GitHub Desktop.
Save YuCJ/c5fdbc7fbdd5f14bdd924cf48fc5a9da to your computer and use it in GitHub Desktop.
/* 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