react-image-lightbox
Version:
A lightbox component for React.js
729 lines (620 loc) • 25.9 kB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.ReactImageLightbox = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
(function (global){
/*
* react-image-lightbox 1.0.0
* Copyright 2016 Chris Fritz All rights reserved.
* @license Open source under the MIT License
*/
'use strict';
var React = (typeof window !== "undefined" ? window['React'] : typeof global !== "undefined" ? global['React'] : null);
var radium = (typeof window !== "undefined" ? window['Radium'] : typeof global !== "undefined" ? global['Radium'] : null);
var styles = require('./styles');
var ReactImageLightbox = React.createClass({
displayName: 'ReactImageLightbox',
propTypes: {
///////////////////////////////
// Image sources
///////////////////////////////
// Main display image url
mainSrc: React.PropTypes.string.isRequired,
// Previous display image url (displayed to the left)
// If left undefined, movePrev actions will not be performed, and the button not displayed
prevSrc: React.PropTypes.string,
// Next display image url (displayed to the right)
// If left undefined, moveNext actions will not be performed, and the button not displayed
nextSrc: React.PropTypes.string,
///////////////////////////////
// Image thumbnail sources
///////////////////////////////
// Thumbnail image url corresponding to props.mainSrc
mainSrcThumbnail: React.PropTypes.string,
// Thumbnail image url corresponding to props.prevSrc
prevSrcThumbnail: React.PropTypes.string,
// Thumbnail image url corresponding to props.nextSrc
nextSrcThumbnail: React.PropTypes.string,
///////////////////////////////
// Event Handlers
///////////////////////////////
// Close window event
// Should change the parent state such that the lightbox is not rendered
onCloseRequest: React.PropTypes.func.isRequired,
// Move to previous image event
// Should change the parent state such that props.prevSrc becomes props.mainSrc,
// props.mainSrc becomes props.nextSrc, etc.
onMovePrevRequest: React.PropTypes.func,
// Move to next image event
// Should change the parent state such that props.nextSrc becomes props.mainSrc,
// props.mainSrc becomes props.prevSrc, etc.
onMoveNextRequest: React.PropTypes.func,
///////////////////////////////
// Download discouragement settings
///////////////////////////////
// Enable download discouragement (prevents [right-click -> Save Image As...])
discourageDownloads: React.PropTypes.bool,
///////////////////////////////
// Animation settings
///////////////////////////////
// Disable all animation
animationDisabled: React.PropTypes.bool,
// Disable animation on actions performed with keyboard shortcuts
animationOnKeyInput: React.PropTypes.bool,
// Animation duration (ms)
animationDuration: React.PropTypes.number,
///////////////////////////////
// Keyboard shortcut settings
///////////////////////////////
// Required interval of time (ms) between key actions
// (prevents excessively fast navigation of images)
keyRepeatLimit: React.PropTypes.number,
// Amount of time (ms) restored after each keyup
// (makes rapid key presses slightly faster than holding down the key to navigate images)
keyRepeatKeyupBonus: React.PropTypes.number,
///////////////////////////////
// Image info
///////////////////////////////
// Image title
imageTitle: React.PropTypes.string,
///////////////////////////////
// Other
///////////////////////////////
// Array of custom toolbar buttons
toolbarButtons: React.PropTypes.arrayOf(React.PropTypes.node),
// Padding (px) between the edge of the window and the lightbox
imagePadding: React.PropTypes.number
},
getDefaultProps: function getDefaultProps() {
return {
onMovePrevRequest: function onMovePrevRequest() {},
onMoveNextRequest: function onMoveNextRequest() {},
discourageDownloads: false,
animationDisabled: false,
animationOnKeyInput: false,
animationDuration: 300,
keyRepeatLimit: 180,
keyRepeatKeyupBonus: 40,
imagePadding: 10
};
},
getInitialState: function getInitialState() {
return {
///////////////////////////////
// Animation
///////////////////////////////
// Lightbox is closing
// When Lightbox is mounted, if animation is enabled it will open with the reverse of the closing animation
isClosing: !this.props.animationDisabled,
// Main image is being replaced by the previous image
isMovingToPrev: false,
// Main image is being replaced by the next image
isMovingToNext: false
};
},
componentWillMount: function componentWillMount() {
// Whether event listeners for keyboard and mouse input have been attached or not
this.listenersAttached = false;
// Used to disable animation when changing props.mainSrc|nextSrc|prevSrc
this.keyPressed = false;
// Used to store load state / dimensions of images
this.imageCache = {};
// Time the last keydown event was called (used in keyboard action rate limiting)
this.lastKeyDownTime = 0;
},
componentDidMount: function componentDidMount() {
this.attachListeners();
if (!this.props.animationDisabled) {
// Make opening animation play
this.setState({ isClosing: false });
}
this.loadAllImages();
},
componentWillReceiveProps: function componentWillReceiveProps(nextProps) {
var sourcesChanged = this.getSrcTypes().some((function (srcType) {
return this.props[srcType] != nextProps[srcType];
}).bind(this));
if (sourcesChanged) {
// Enable animated states
if (!this.props.animationDisabled && (!this.keyPressed || this.props.animationOnKeyInput)) {
var movedToPrev = this.props.mainSrc == nextProps.nextSrc;
if (this.props.mainSrc == nextProps.nextSrc) {
this.setState({ isMovingToPrev: true });
setTimeout((function () {
this.setState({ isMovingToPrev: false });
}).bind(this), this.props.animationDuration);
} else if (this.props.mainSrc == nextProps.prevSrc) {
this.setState({ isMovingToNext: true });
setTimeout((function () {
this.setState({ isMovingToNext: false });
}).bind(this), this.props.animationDuration);
}
}
this.keyPressed = false;
// Load any new images
this.loadAllImages(nextProps);
}
},
componentWillUnmount: function componentWillUnmount() {
this.detachListeners();
},
// Handle user keyboard actions
handleKeyInput: function handleKeyInput(event) {
event.stopPropagation();
// Ignore key input during animations
if (this.isAnimating()) {
return;
}
// Allow slightly faster navigation through the images when user presses keys repeatedly
if (event.type === 'keyup') {
this.lastKeyDownTime -= this.props.keyRepeatKeyupBonus;
return;
}
var keyCode = event.which || event.keyCode;
var key = {
esc: 27,
leftArrow: 37,
rightArrow: 39
};
// Ignore key presses that happen too close to each other (when rapid fire key pressing or holding down the key)
// But allow it if it's a lightbox closing action
var currentTime = new Date();
if (currentTime.getTime() - this.lastKeyDownTime < this.props.keyRepeatLimit && keyCode != key.esc) {
return;
}
this.lastKeyDownTime = currentTime.getTime();
switch (keyCode) {
// ESC key closes the lightbox
case key.esc:
event.preventDefault();
if (this.props.animationDisabled || !this.props.animationOnKeyInput) {
this.props.onCloseRequest(event); // immediate
} else {
this.requestClose(event); // animated
}
break;
// Left arrow key moves to previous image
case key.leftArrow:
if (!this.props.prevSrc) {
return;
}
event.preventDefault();
this.keyPressed = true;
this.props.onMovePrevRequest(event);
break;
// Right arrow key moves to next image
case key.rightArrow:
if (!this.props.nextSrc) {
return;
}
event.preventDefault();
this.keyPressed = true;
this.props.onMoveNextRequest(event);
break;
default:
}
},
// Request that the lightbox be closed
requestClose: function requestClose(event) {
var closeLightbox = (function () {
// Call the parent close request
this.props.onCloseRequest(event);
}).bind(this);
if (!this.props.animationDisabled) {
// Start closing animation
this.setState({ isClosing: true });
// Perform the actual closing at the end of the animation
setTimeout(closeLightbox, this.props.animationDuration);
} else {
closeLightbox();
}
},
// Request to transition to the previous image
requestMovePrev: function requestMovePrev(event) {
this.props.onMovePrevRequest(event);
},
// Request to transition to the next image
requestMoveNext: function requestMoveNext(event) {
this.props.onMoveNextRequest(event);
},
// Attach key and mouse input events
attachListeners: function attachListeners() {
if (!this.listenersAttached) {
document.addEventListener('keydown', this.handleKeyInput);
document.addEventListener('keyup', this.handleKeyInput);
this.listenersAttached = true;
}
},
// Detach key and mouse input events
detachListeners: function detachListeners() {
if (this.listenersAttached) {
document.removeEventListener('keydown', this.handleKeyInput);
document.removeEventListener('keyup', this.handleKeyInput);
this.listenersAttached = false;
}
},
// Get image src types
getSrcTypes: function getSrcTypes() {
return ['mainSrc', 'mainSrcThumbnail', 'nextSrc', 'nextSrcThumbnail', 'prevSrc', 'prevSrcThumbnail'];
},
// Get sizing for when an image is larger than the window
getFitSizes: function getFitSizes(width, height) {
var windowHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
var windowWidth = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
var maxHeight = Math.min(windowHeight - this.props.imagePadding * 2, height);
var maxWidth = Math.min(windowWidth - this.props.imagePadding * 2, width);
var maxRatio = maxWidth / maxHeight;
var srcRatio = width / height;
var fitSizes = {};
if (maxRatio > srcRatio) {
// height is the constraining dimension of the photo
fitSizes.width = width * maxHeight / height;
fitSizes.height = maxHeight;
} else {
fitSizes.width = maxWidth;
fitSizes.height = height * maxWidth / width;
}
return fitSizes;
},
// Detach key and mouse input events
isAnimating: function isAnimating() {
return this.state.isMovingToNext || this.state.isMovingToPrev || this.state.isClosing;
},
// Load image from src and call callback with image width and height on load
loadImage: function loadImage(imageSrc, callback) {
// Return the image info if it is already cached
if (this.isImageLoaded(imageSrc)) {
setTimeout(function () {
callback(null, this.imageCache[imageSrc].width, this.imageCache[imageSrc].height);
}, 1);
return;
}
var that = this;
var inMemoryImage = new Image();
inMemoryImage.onerror = function () {
callback('image load error');
};
inMemoryImage.onload = function () {
that.imageCache[imageSrc] = {
loaded: true,
width: this.width,
height: this.height
};
callback(null, this.width, this.height);
};
inMemoryImage.src = imageSrc;
},
// Load all images and their thumbnails
loadAllImages: function loadAllImages(props) {
props = props || this.props;
var generateImageLoadedCallback = (function (srcType, imageSrc) {
return (function (err) {
// Give up showing image on error
if (err) {
return;
}
// Don't rerender if the src is not the same as when the load started
if (this.props[srcType] != imageSrc) {
return;
}
// Force rerender with the new image
this.forceUpdate();
}).bind(this);
}).bind(this);
// Load the images
this.getSrcTypes().forEach((function (srcType) {
// Load unloaded images
if (props[srcType] && !this.isImageLoaded(props[srcType])) {
this.loadImage(props[srcType], generateImageLoadedCallback(srcType, props[srcType]));
}
}).bind(this));
},
// Load image from src and call callback with image width and height on load
isImageLoaded: function isImageLoaded(imageSrc) {
return imageSrc && imageSrc in this.imageCache && this.imageCache[imageSrc].loaded;
},
render: function render() {
// Transition settings for sliding animations
var transitionStyle = {};
if (!this.props.animationDisabled && this.isAnimating()) {
transitionStyle = styles.imageAnimating(this.props.animationDuration);
}
// Images to be displayed
var images = [];
var addImage = (function (srcType, imageClass, baseStyle) {
var imageSrc = this.props[srcType];
if (!imageSrc) {
return;
}
var imageStyle = [styles.image, baseStyle, transitionStyle];
var fitSizes = {};
if (this.isImageLoaded(imageSrc)) {
fitSizes = this.getFitSizes(this.imageCache[imageSrc].width, this.imageCache[imageSrc].height);
} else if (this.isImageLoaded(this.props[srcType + 'Thumbnail'])) {
// Fall back to using thumbnail if the image has not been loaded
imageSrc = this.props[srcType + 'Thumbnail'];
fitSizes = this.getFitSizes(this.imageCache[imageSrc].width, this.imageCache[imageSrc].height);
} else {
// Fall back to loading icon if the thumbnail has not been loaded
images.push(React.createElement('div', {
className: imageClass + ' not-loaded',
style: imageStyle,
key: imageSrc
}));
return;
}
imageStyle.push({
width: fitSizes.width,
height: fitSizes.height
});
if (this.props.discourageDownloads) {
imageStyle.push({ backgroundImage: 'url(\'' + imageSrc + '\')' });
imageStyle.push(styles.imageDiscourager);
images.push(React.createElement(
'div',
{
className: imageClass,
style: imageStyle,
key: imageSrc
},
React.createElement('div', { className: 'download-blocker', style: [styles.image.downloadBlocker] })
));
} else {
images.push(React.createElement('img', {
className: imageClass,
style: imageStyle,
src: imageSrc,
key: imageSrc
}));
}
}).bind(this);
// Next Image (displayed on the right)
addImage('nextSrc', 'image-next', styles.imageNext);
// Main Image
addImage('mainSrc', 'image-current', styles.imageCurrent);
// Previous Image (displayed on the left)
addImage('prevSrc', 'image-prev', styles.imagePrev);
var noop = function noop() {};
return React.createElement(
'div',
{ // Floating modal with closing animations
className: "outer" + (this.state.isClosing ? ' closing' : ''),
style: [styles.outer, styles.outer.animating(this.props.animationDuration), this.state.isClosing ? styles.outerClosing : {}]
},
React.createElement(
'div',
{ // Image holder
className: 'inner',
style: [styles.inner]
},
images
),
!this.props.prevSrc ? '' : React.createElement('button', { // Move to previous image button
type: 'button',
className: 'prev-button',
key: 'prev',
style: [styles.navButtons, styles.navButtonPrev],
onClick: !this.isAnimating() ? this.requestMovePrev : noop // Ignore clicks during animation
}),
!this.props.nextSrc ? '' : React.createElement('button', { // Move to next image button
type: 'button',
className: 'next-button',
key: 'next',
style: [styles.navButtons, styles.navButtonNext],
onClick: !this.isAnimating() ? this.requestMoveNext : noop // Ignore clicks during animation
}),
React.createElement(
'div',
{ // Lightbox toolbar
className: 'toolbar',
style: [styles.toolbar]
},
React.createElement(
'ul',
{ className: 'toolbar-left', style: [styles.toolbarSide, styles.toolbarLeftSide] },
React.createElement(
'li',
{ style: [styles.toolbarItem] },
React.createElement(
'span',
{ style: [styles.toolbarItemChild] },
this.props.imageTitle
)
)
),
React.createElement(
'ul',
{ className: 'toolbar-right', style: [styles.toolbarSide, styles.toolbarRightSide] },
!this.props.toolbarButtons ? '' : this.props.toolbarButtons.map(function (button, i) {
return React.createElement(
'li',
{ key: i, style: [styles.toolbarItem] },
button
);
}),
React.createElement(
'li',
{ style: [styles.toolbarItem] },
React.createElement('button', { // Lightbox close button
type: 'button',
className: 'close',
style: [styles.toolbarItemChild, styles.closeButton],
onClick: !this.isAnimating() ? this.requestClose : noop // Ignore clicks during animation
})
)
)
)
);
}
});
module.exports = radium(ReactImageLightbox);
}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"./styles":2}],2:[function(require,module,exports){
'use strict';
var toolbarHeight = '50px';
var styles = {
outer: {
backgroundColor: 'rgba(0, 0, 0, 0.85)',
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 1000,
width: '100%',
height: '100%',
animating: function animating(duration) {
return {
transition: 'opacity ' + String(duration) + 'ms'
};
}
},
outerClosing: {
opacity: 0
},
inner: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0
},
image: {
position: 'absolute',
top: 0,
bottom: 0,
margin: 'auto',
maxWidth: '100%',
maxHeight: '100%'
},
imagePrev: {
left: '-100%',
right: '100%'
},
imageNext: {
left: '100%',
right: '-100%'
},
imageCurrent: {
left: 0,
right: 0
},
imageDiscourager: {
backgroundRepeat: 'no-repeat',
backgroundPosition: 'center',
backgroundSize: 'contain'
},
imageAnimating: function imageAnimating(duration) {
return {
transition: ['left ' + String(duration) + 'ms', 'right ' + String(duration) + 'ms'].join(', ')
};
},
navButtons: {
border: 'none',
position: 'absolute',
top: 0,
bottom: 0,
width: '20px',
height: '34px',
padding: '40px 40px',
margin: 'auto',
cursor: 'pointer',
opacity: 0.7,
':hover': {
opacity: 1
},
':active': {
opacity: 0.7
}
},
navButtonPrev: {
left: 0,
background: 'url(\'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgd2lkdGg9IjIwIiBoZWlnaHQ9IjM0Ij48cGF0aCBkPSJtIDE5LDMgLTIsLTIgLTE2LDE2IDE2LDE2IDEsLTEgLTE1LC0xNSAxNSwtMTUgeiIgZmlsbD0iI0ZGRiIvPjwvc3ZnPg==\') no-repeat center'
},
navButtonNext: {
right: 0,
background: 'url(\'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgd2lkdGg9IjIwIiBoZWlnaHQ9IjM0Ij48cGF0aCBkPSJtIDEsMyAyLC0yIDE2LDE2IC0xNiwxNiAtMSwtMSAxNSwtMTUgLTE1LC0xNSB6IiBmaWxsPSIjRkZGIi8+PC9zdmc+\') no-repeat center'
},
downloadBlocker: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundImage: 'url(\'../img/please-use-download-button.gif\')',
backgroundSize: 'cover'
},
toolbar: {
backgroundColor: 'rgba(0, 0, 0, 0.5)',
position: 'absolute',
left: 0,
top: 0,
right: 0,
height: toolbarHeight
},
toolbarSide: {
lineHeight: toolbarHeight,
position: 'absolute',
top: 0,
bottom: 0,
margin: 0,
maxWidth: '48%'
},
toolbarLeftSide: {
paddingLeft: '20px',
paddingRight: 0,
left: 0
},
toolbarRightSide: {
paddingLeft: 0,
paddingRight: '20px',
right: 0
},
toolbarItem: {
display: 'inline-block',
lineHeight: toolbarHeight,
padding: '0 6px',
color: '#FFFFFF',
fontSize: '120%',
maxWidth: '100%',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
},
toolbarItemChild: {
verticalAlign: 'middle'
},
closeButton: {
width: '25px',
height: '25px',
cursor: 'pointer',
border: 'none',
background: 'url(\'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIj48cGF0aCBkPSJtIDEsMyAxLjI1LC0xLjI1IDcuNSw3LjUgNy41LC03LjUgMS4yNSwxLjI1IC03LjUsNy41IDcuNSw3LjUgLTEuMjUsMS4yNSAtNy41LC03LjUgLTcuNSw3LjUgLTEuMjUsLTEuMjUgNy41LC03LjUgLTcuNSwtNy41IHoiIGZpbGw9IiNGRkYiLz48L3N2Zz4=\') no-repeat center',
':hover': {
opacity: 0.7
},
':active': {
outline: 'none'
}
}
};
module.exports = styles;
},{}]},{},[1])(1)
});