react-modal-slideshow
Version:
A simple React modal media slideshow with CSS powered animations
386 lines (322 loc) • 9.21 kB
JSX
import React, { Component } from "react";
import ReactCSSTransitionGroup from "react-addons-css-transition-group";
import debug from "debug";
import PropTypes from "prop-types";
import SimpleModal from "./SimpleModal.jsx";
const log = debug("modal-slideshow:log");
export default class SimpleModalSlideshow extends Component {
constructor(props) {
super(props);
// Initial state
this.state = {
open: false,
currentSlide: 0,
slideAnimationDirection: "left"
};
// Prepare keyboard event listener
this._handleKeyboardInput = this._handleKeyboardInput.bind(this);
// Bind API
this.goTo = this.goTo.bind(this);
this.goToNext = this.goToNext.bind(this);
this.goToPrev = this.goToPrev.bind(this);
this.open = this.open.bind(this);
this.close = this.close.bind(this);
}
componentDidMount() {
// Initially set current slide
if (this.props.currentSlide) {
this._goTo(this.props.currentSlide);
}
// Initially open the modal
if (this.props.open) {
this._open();
}
}
componentWillReceiveProps(nextProps) {
// Listen "open" / "currentSlide" props only if the API is disabled
if (!nextProps.enableApi) {
// Update current slide
this._goTo(nextProps.currentSlide);
// Make sure to open/close only when needed
if (!this.props.open && nextProps.open) {
this._open();
} else if (this.props.open && !nextProps.open) {
this._close();
}
}
// If classNamePrefix is updated, make sure to update flag on html tag if set
if (nextProps.classNamePrefix !== this.props.classNamePrefix) {
document.documentElement.classList.remove(
`${this.props.classNamePrefix}--opened`
);
document.documentElement.classList.add(
`${nextProps.classNamePrefix}--opened`
);
}
}
/**
* Keyboard event listener
*
* @param {Event} event Keyboard event
*/
_handleKeyboardInput(event) {
log(`Keyboard input captured (keyCode=${event.keyCode})`);
// Left arrow
if (event.keyCode === 37) {
this._handlePrev();
// Right arrow
} else if (event.keyCode === 39) {
this._handleNext();
// Esc
} else if (event.keyCode === 27) {
this._handleClose();
}
}
/**
* Handle close event
*/
_handleClose() {
log("Handle Close");
// If props.onClose is supplied, call it and stop here
if (this.props.onClose) {
this.props.onClose();
return;
}
// If API is disabled only the parent can control the slideshow
// through props.onClose
if (this.props.enableApi) {
this.close();
}
}
/**
* Handle prev event
*/
_handlePrev() {
const currentSlide = this.state.currentSlide;
const prevSlide = currentSlide - 1;
// Make sure there's a slide before the current one
if (currentSlide <= 0) {
return;
}
log("Handle Prev");
// If props.onPrev is supplied, call it and stop here
if (this.props.onPrev) {
this.props.onPrev(prevSlide);
return;
}
// If API is disabled only the parent can control the slideshow
// through props.onPrev
if (this.props.enableApi) {
this._goTo(prevSlide);
}
}
/**
* Handle next event
*/
_handleNext() {
const currentSlide = this.state.currentSlide;
const nextSlide = currentSlide + 1;
// Make sure there's a slide after the current one
if (nextSlide >= this.props.slides.length) {
return;
}
log("Handle Next");
// If props.onNext is supplied, call it and stop here
if (this.props.onNext) {
this.props.onNext(nextSlide);
return;
}
// If API is disabled, only the parent component can control the slideshow
// through props.onNext
if (this.props.enableApi) {
this._goTo(nextSlide);
}
}
_goToPrev() {
const currentSlide = this.state.currentSlide;
const prevSlide = currentSlide - 1;
if (currentSlide <= 0) {
return;
}
this._goTo(prevSlide);
}
_goToNext() {
const currentSlide = this.state.currentSlide;
const nextSlide = currentSlide + 1;
if (nextSlide >= this.props.slides.length) {
return;
}
this._goTo(nextSlide);
}
_goTo(index) {
if (this.props.slides.length === 0) {
return;
}
// Make sure the slide exists
if (index < 0 || index >= this.props.slides.length) {
throw new Error(
`Invalid index '${index}' supplied to SimpleModalSlideshow::goTo()`
);
}
log(`Go to slide ${index}`);
// Update state with the new index and animation direction
this.setState({
currentSlide: index,
slideAnimationDirection:
index < this.state.currentSlide ? "right" : "left"
});
}
_close() {
log("Close");
// Remove class from html tag
document.documentElement.classList.remove(
`${this.props.classNamePrefix}--opened`
);
// Remove keyboard event listener
document.removeEventListener("keydown", this._handleKeyboardInput);
// Update state
this.setState({
open: false
});
}
_open(index) {
log("Open");
// Add a class to the body (to disable background scroll)
document.documentElement.classList.add(
`${this.props.classNamePrefix}--opened`
);
// Enable keyboard event listener
document.addEventListener("keydown", this._handleKeyboardInput);
// If an index is given, jump to the slide before opening
if (index !== undefined) {
this._goTo(index);
}
// Update state
this.setState({
open: true
});
}
/**
* Go to previous slide
*/
goToPrev() {
if (!this.props.enableApi) {
throw new Error("SimpleModalSlideshow API is disabled");
}
this._goToPrev();
}
/**
* Go to next slide
*/
goToNext() {
if (!this.props.enableApi) {
throw new Error("SimpleModalSlideshow API is disabled");
}
this._goToNext();
}
/**
* Go to a specific slide
*
* @param {Int} index Slide index to go to
*/
goTo(index) {
if (!this.props.enableApi) {
throw new Error("SimpleModalSlideshow API is disabled");
}
this._goTo(index);
}
/**
* Open the slideshow
*
* @param {Int} index If specified, the given slide will be opened
*/
open(index) {
if (!this.props.enableApi) {
throw new Error("SimpleModalSlideshow API is disabled");
}
this._open(index);
}
/**
* Close the slideshow
*/
close() {
if (!this.props.enableApi) {
throw new Error("SimpleModalSlideshow API is disabled");
}
this._close();
}
render() {
let modal = null;
// Modal is added to the DOM only if it has a slide to display
if (this.props.slides.length > 0 && this.state.open) {
modal = (
<SimpleModal
key="modal-slideshow"
slide={this.props.slides[this.state.currentSlide]}
slideIndex={this.state.currentSlide}
slideAnimations={this.props.slideAnimations}
slideAnimationDirection={this.state.slideAnimationDirection}
slideTransitionEnterTimeout={this.props.slideTransitionEnterTimeout}
slideTransitionLeaveTimeout={this.props.slideTransitionLeaveTimeout}
showPrev={this.state.currentSlide > 0}
showNext={this.state.currentSlide < this.props.slides.length - 1}
onPrev={this._handlePrev.bind(this)}
onNext={this._handleNext.bind(this)}
onClose={this._handleClose.bind(this)}
classNamePrefix={this.props.classNamePrefix}
/>
);
}
return (
<ReactCSSTransitionGroup
component="div"
transitionName={`${this.props.classNamePrefix}__modal`}
transitionEnter={this.props.modalAnimations}
transitionLeave={this.props.modalAnimations}
transitionEnterTimeout={this.props.modalTransitionEnterTimeout}
transitionLeaveTimeout={this.props.modalTransitionLeaveTimeout}
>
{modal}
</ReactCSSTransitionGroup>
);
}
}
SimpleModalSlideshow.propTypes = {
// Slides
slides: PropTypes.arrayOf(
PropTypes.shape({
media: PropTypes.node.isRequired,
content: PropTypes.node,
title: PropTypes.node
})
).isRequired,
// Controls
open: PropTypes.bool,
currentSlide: PropTypes.number,
// Options
enableApi: PropTypes.bool,
classNamePrefix: PropTypes.string,
// Animations
modalAnimations: PropTypes.bool,
slideAnimations: PropTypes.bool,
modalTransitionEnterTimeout: PropTypes.number,
modalTransitionLeaveTimeout: PropTypes.number,
slideTransitionEnterTimeout: PropTypes.number,
slideTransitionLeaveTimeout: PropTypes.number,
// Event listeners
onClose: PropTypes.func,
onPrev: PropTypes.func,
onNext: PropTypes.func
};
SimpleModalSlideshow.defaultProps = {
open: false,
currentSlide: 0,
enableApi: false,
classNamePrefix: "modal-slideshow",
modalAnimations: true,
slideAnimations: true,
modalTransitionEnterTimeout: 300,
modalTransitionLeaveTimeout: 300,
slideTransitionEnterTimeout: 300,
slideTransitionLeaveTimeout: 300
};