impress.js-react
Version:
Create Impress presentation with React and JSX
251 lines (214 loc) • 7.84 kB
JavaScript
import React, {Component} from 'react'
import {noop, $, css, perspective, scale, toNumber, translate, rotate} from '../helpers'
import Events from '../events'
class Impress extends Component{
constructor(props){
super(props)
let body = document.body
// First we set up the viewport for mobile devices.
// For some reason iPad goes nuts when it is not done properly.
let meta = $("meta[name='viewport']") || document.createElement("meta")
meta.content = "width=device-width, minimum-scale=1, maximum-scale=1, user-scalable=no"
if(meta.parentNode !== document.head){
meta.name = 'viewport'
document.head.appendChild(meta)
}
this.windowScale = this.computeWindowScale()
// set initial styles
document.documentElement.style.height = "100%"
css(body, {
height: "100%",
overflow: "hidden"
})
let canvasStyles = {
position: "absolute",
transformOrigin: "top left",
transition: "all 0s ease-in-out",
transformStyle: "preserve-3d",
}
body.classList.remove("impress-disabled")
body.classList.add("impress-enabled")
this.styles = {
rootStyles: {
...canvasStyles,
top: "50%",
left: "50%",
transform: perspective(props.perspective/this.windowScale) + scale(this.windowScale)
},
canvasStyles,
}
this.state = {
currentStep: 0
}
this.currentState = {
translate: { x: 0, y: 0, z: 0 },
rotate: { x: 0, y: 0, z: 0 },
scale: 1
}
this.API = {
goTo: this.goTo.bind(this),
next: this.next.bind(this),
prev: this.prev.bind(this)
}
this.props.onGotAPI(this.API)
this.events = Events(this.API)
}
// `computeWindowScale` counts the scale factor between window size and size
// defined for the presentation in the config.
computeWindowScale = () => {
let hScale = window.innerHeight / this.props.height,
wScale = window.innerWidth / this.props.width,
scale = hScale > wScale ? wScale : hScale
if (this.props.maxScale && scale > this.props.maxScale) {
scale = this.props.maxScale
}
if (this.props.minScale && scale < this.props.minScale) {
scale = this.props.minScale
}
return scale
}
calculateStyles = () => {
if(!this.props.children)
return
// Sometimes it's possible to trigger focus on first link with some keyboard action.
// Browser in such a case tries to scroll the page to make this element visible
// (even that body overflow is set to hidden) and it breaks our careful positioning.
//
// So, as a lousy (and lazy) workaround we will make the page scroll back to the top
// whenever slide is selected
//
// If you are reading this and know any better way to handle it, I'll be glad to hear about it!
window.scrollTo(0, 0)
let id = this.state.currentStep
let step = this.props.children[id]
if(!step)
step = this.props.children
let duration = this.duration
// Compute target state of the canvas based on given step
let target = {
rotate: {
x: -toNumber(step.props.rotateX, 0),
y: -toNumber(step.props.rotateY, 0),
z: -toNumber((step.props.rotateZ || step.props.rotate), 0)
},
translate: {
x: -toNumber(step.props.x, 0),
y: -toNumber(step.props.y, 0),
z: -toNumber(step.props.z, 0)
},
scale: 1 / toNumber(step.props.scale, 1)
}
// Check if the transition is zooming in or not.
//
// This information is used to alter the transition style:
// when we are zooming in - we start with move and rotate transition
// and the scaling is delayed, but when we are zooming out we start
// with scaling down and move and rotation are delayed.
let zoomin = target.scale >= this.state.scale
let delay = (duration / 2)
this.windowScale = this.computeWindowScale()
let targetScale = target.scale * this.windowScale
// Here is a tricky part...
//
// If there is no change in scale or no change in rotation and translation, it means there was actually
// no delay - because there was no transition on `root` or `canvas` elements.
// We want to trigger `impress:stepenter` event in the correct moment, so here we compare the current
// and target values to check if delay should be taken into account.
//
// I know that this `if` statement looks scary, but it's pretty simple when you know what is going on
// - it's simply comparing all the values.
if ( this.currentState.scale === target.scale ||
(this.currentState.rotate.x === target.rotate.x && this.currentState.rotate.y === target.rotate.y &&
this.currentState.rotate.z === target.rotate.z && this.currentState.translate.x === target.translate.x &&
this.currentState.translate.y === target.translate.y && this.currentState.translate.z === target.translate.z) ) {
delay = 0
}
// store current state
this.currentState = target
clearTimeout(this.stepEnterTimeout)
this.stepEnterTimeout = setTimeout(function() {
// onStepEnter(activeStep);
}, duration + delay)
// Now we alter transforms of `root` and `canvas` to trigger transitions.
//
// And here is why there are two elements: `root` and `canvas` - they are
// being animated separately:
// `root` is used for scaling and `canvas` for translate and rotations.
// Transitions on them are triggered with different delays (to make
// visually nice and 'natural' looking transitions), so we need to know
// that both of them are finished.
this.styles = {
rootStyles: {
...this.styles.rootStyles,
// to keep the perspective look similar for different scales
// we need to 'scale' the perspective, too
transform: perspective(this.props.perspective / targetScale) + scale(targetScale),
transitionDuration: duration + "ms",
transitionDelay: (zoomin ? delay : 0) + "ms"
},
canvasStyles: {
...this.styles.canvasStyles,
transform: rotate(target.rotate, true) + translate(target.translate),
transitionDuration: duration + "ms",
transitionDelay: (zoomin ? 0 : delay) + "ms"
}
}
}
goTo = (idOrEl, duration) => {
if(typeof idOrEl !== 'number'){
idOrEl = parseInt(idOrEl.dataset.id)
if(isNaN(idOrEl)){
return
}
}
this.duration = toNumber(duration, this.props.transitionDuration)
this.setState({
currentStep: idOrEl
})
}
next = (duration) => {
if(!this.props.children || !this.props.children.length)
return
let step = (this.state.currentStep + 1) % this.props.children.length
return this.goTo(step, duration)
}
prev = (duration) => {
if(!this.props.children || !this.props.children.length)
return
let step = this.state.currentStep - 1
return this.goTo(step < 0 ? this.props.children.length - 1 : step, duration)
}
componentWillMount(){
this.events.listen()
}
componentWillUnmount(){
clearTimeout(this.stepEnterTimeout)
this.events.remove()
}
render(){
this.calculateStyles()
let childrenWithProps = React.Children.map(this.props.children, (child, id) =>
React.cloneElement(child, {
active: id == this.state.currentStep,
id: id
})
)
return (
<div style={this.styles.rootStyles}>
<div style={this.styles.canvasStyles}>
{childrenWithProps}
</div>
</div>
)
}
}
Impress.defaultProps = {
width: 1024,
height: 768,
maxScale: 1,
minScale: 0,
perspective: 1000,
transitionDuration: 1000,
onGotAPI: API => null
}
export default Impress