UNPKG

react-trilogo-images

Version:

A simple, responsive lightbox component for displaying an array of images with React.js with extended features

743 lines (640 loc) 22.5 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _propTypes = require('prop-types'); var _propTypes2 = _interopRequireDefault(_propTypes); var _react = require('react'); var _react2 = _interopRequireDefault(_react); var _aphrodite = require('aphrodite'); var _reactScrolllock = require('react-scrolllock'); var _reactScrolllock2 = _interopRequireDefault(_reactScrolllock); var _theme = require('./theme'); var _theme2 = _interopRequireDefault(_theme); var _Arrow = require('./components/Arrow'); var _Arrow2 = _interopRequireDefault(_Arrow); var _Container = require('./components/Container'); var _Container2 = _interopRequireDefault(_Container); var _Footer = require('./components/Footer'); var _Footer2 = _interopRequireDefault(_Footer); var _Header = require('./components/Header'); var _Header2 = _interopRequireDefault(_Header); var _PaginatedThumbnails = require('./components/PaginatedThumbnails'); var _PaginatedThumbnails2 = _interopRequireDefault(_PaginatedThumbnails); var _Portal = require('./components/Portal'); var _Portal2 = _interopRequireDefault(_Portal); var _Spinner = require('./components/Spinner'); var _Spinner2 = _interopRequireDefault(_Spinner); var _bindFunctions = require('./utils/bindFunctions'); var _bindFunctions2 = _interopRequireDefault(_bindFunctions); var _canUseDom = require('./utils/canUseDom'); var _canUseDom2 = _interopRequireDefault(_canUseDom); var _deepMerge = require('./utils/deepMerge'); var _deepMerge2 = _interopRequireDefault(_deepMerge); var _Icon = require('./components/Icon'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } // consumers sometimes provide incorrect type or casing function normalizeSourceSet(data) { var sourceSet = data.srcSet || data.srcset; if (Array.isArray(sourceSet)) { return sourceSet.join(); } return sourceSet; } var Lightbox = function (_Component) { _inherits(Lightbox, _Component); function Lightbox(props) { _classCallCheck(this, Lightbox); var _this = _possibleConstructorReturn(this, (Lightbox.__proto__ || Object.getPrototypeOf(Lightbox)).call(this, props)); _this.theme = (0, _deepMerge2.default)(_theme2.default, props.theme); _this.classes = _aphrodite.StyleSheet.create((0, _deepMerge2.default)(defaultStyles, _this.theme)); _this.state = { imageLoaded: false, left: null, top: 15, width: 0, height: 0, rotate: 0, imageWidth: 0, imageHeight: 0, scaleX: 1, scaleY: 1 }; _this.containerWidth = 0; _this.containerHeight = 0; _this.footerHeight = 84; _this.setContainerWidthHeight(); _bindFunctions2.default.call(_this, ['gotoNext', 'gotoPrev', 'closeBackdrop', 'handleKeyboardInput', 'handleImageLoaded', 'handleAction', 'getImageCenterXY', 'handleZoom', 'handleRotate']); return _this; } _createClass(Lightbox, [{ key: 'getChildContext', value: function getChildContext() { return { theme: this.theme }; } }, { key: 'componentDidMount', value: function componentDidMount() { if (this.props.isOpen) { if (this.props.enableKeyboardInput) { window.addEventListener('keydown', this.handleKeyboardInput); } if (typeof this.props.currentImage === 'number') { this.preloadImage(this.props.currentImage); } } } }, { key: 'componentWillReceiveProps', value: function componentWillReceiveProps(nextProps) { if (!_canUseDom2.default) return; if (this.props.isOpen && nextProps.isOpen) { this.preloadImage(this.props.currentImage, nextProps); } // preload current image if (this.props.currentImage !== nextProps.currentImage || !this.props.isOpen && nextProps.isOpen) { var img = this.preloadImage(nextProps.currentImage, nextProps); if (img && img.complete) { this.setState({ imageLoaded: img.complete }); } } // add/remove event listeners if (!this.props.isOpen && nextProps.isOpen && nextProps.enableKeyboardInput) { window.addEventListener('keydown', this.handleKeyboardInput); } if (!nextProps.isOpen && nextProps.enableKeyboardInput) { window.removeEventListener('keydown', this.handleKeyboardInput); } } }, { key: 'componentWillUnmount', value: function componentWillUnmount() { if (this.props.enableKeyboardInput) { window.removeEventListener('keydown', this.handleKeyboardInput); } } // ============================== // METHODS // ============================== }, { key: 'setContainerWidthHeight', value: function setContainerWidthHeight() { this.containerWidth = window.innerWidth; this.containerHeight = window.innerHeight; } }, { key: 'getImageCenterXY', value: function getImageCenterXY() { return { x: this.state.left + this.state.width / 2, y: this.state.top + this.state.height / 2 }; } }, { key: 'getImgWidthHeight', value: function getImgWidthHeight(imgWidth, imgHeight) { var width = 0; var height = 0; var maxWidth = this.containerWidth * 0.8; var maxHeight = (this.containerHeight - this.footerHeight) * 0.8; width = Math.min(maxWidth, imgWidth); height = width / imgWidth * imgHeight; if (height > maxHeight) { height = maxHeight; width = height / imgHeight * imgWidth; } return [width, height]; } }, { key: 'handleAction', value: function handleAction(type) { switch (type) { case _Icon.ActionType.prev: if (this.state.activeIndex - 1 >= 0) { this.handleChangeImg(this.state.activeIndex - 1); } break; case _Icon.ActionType.next: if (this.state.activeIndex + 1 < this.props.images.length) { this.handleChangeImg(this.state.activeIndex + 1); } break; case _Icon.ActionType.zoomIn: var imgCenterXY = this.getImageCenterXY(); this.handleZoom(imgCenterXY.x, imgCenterXY.y, 1, 0.1); this.props.onZoomIn && this.props.onZoomIn(); break; case _Icon.ActionType.zoomOut: var imgCenterXY2 = this.getImageCenterXY(); this.handleZoom(imgCenterXY2.x, imgCenterXY2.y, -1, 0.1); this.props.onZoomOut && this.props.onZoomOut(); break; case _Icon.ActionType.rotateLeft: this.handleRotate(); this.props.onRotateLeft && this.props.onRotateLeft(); break; case _Icon.ActionType.rotateRight: this.handleRotate(true); this.props.onRotateRight && this.props.onRotateRight(); break; case _Icon.ActionType.reset: this.loadImg(this.state.activeIndex); break; case _Icon.ActionType.scaleX: this.handleScaleX(-1); break; case _Icon.ActionType.scaleY: this.handleScaleY(-1); break; case _Icon.ActionType.save: this.props.onSave && this.props.onSave(this.props.currentImage, { zoom: this.state.scaleX, rotation: this.state.rotate }); break; default: break; } } }, { key: 'handleZoom', value: function handleZoom(targetX, targetY, direct, scale) { var imgCenterXY = this.getImageCenterXY(); var diffX = targetX - imgCenterXY.x; var diffY = targetY - imgCenterXY.y; // when image width is 0, set original width var top = 0; var left = 0; var width = 0; var height = 0; var scaleX = 0; var scaleY = 0; if (this.state.width === 0) { var _getImgWidthHeight = this.getImgWidthHeight(this.state.imageWidth, this.state.imageHeight), _getImgWidthHeight2 = _slicedToArray(_getImgWidthHeight, 2), imgWidth = _getImgWidthHeight2[0], imgHeight = _getImgWidthHeight2[1]; left = (this.containerWidth - imgWidth) / 2; top = (this.containerHeight - this.footerHeight - imgHeight) / 2; width = this.state.width + imgWidth; height = this.state.height + imgHeight; scaleX = scaleY = 1; } else { var directX = this.state.scaleX > 0 ? 1 : -1; var directY = this.state.scaleY > 0 ? 1 : -1; scaleX = this.state.scaleX + scale * direct * directX; scaleY = this.state.scaleY + scale * direct * directY; if (Math.abs(scaleX) < 0.1 || Math.abs(scaleY) < 0.1) { return; } top = this.state.top + -direct * diffY / this.state.scaleX * scale * directX; left = this.state.left + -direct * diffX / this.state.scaleY * scale * directY; width = this.state.width; height = this.state.height; } this.setState({ width: width, scaleX: Math.floor(scaleX * 10) / 10, scaleY: Math.floor(scaleY * 10) / 10, height: height, top: top, left: left, loading: false }); } }, { key: 'handleRotate', value: function handleRotate() { var isRight = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; this.setState({ rotate: (this.state.rotate + 90 * (isRight ? 1 : -1)) % 360 }); } }, { key: 'preloadImage', value: function preloadImage(idx, nextProps) { var _this2 = this; var data = nextProps ? nextProps.images[idx] : this.props.images[idx]; if (!data) return; var img = new Image(); var sourceSet = normalizeSourceSet(data); // TODO: add error handling for missing images img.onerror = function () { _this2.setState({ imageLoaded: true }); }; img.onload = function () { var imgWidth = img.width; var imgHeight = img.height; var _getImgWidthHeight3 = _this2.getImgWidthHeight(imgWidth, imgHeight), _getImgWidthHeight4 = _slicedToArray(_getImgWidthHeight3, 2), width = _getImgWidthHeight4[0], height = _getImgWidthHeight4[1]; var left = (_this2.containerWidth - width) / 2; var top = (_this2.containerHeight - height - _this2.footerHeight) / 2; _this2.setState({ width: width, height: height, left: left, top: top, imageWidth: imgWidth, imageHeight: imgHeight, rotate: data.initialRotation || 0, scaleX: data.initialZoom || 1, scaleY: data.initialZoom || 1, imageLoaded: true }); }; img.src = data.src; if (sourceSet) img.srcset = sourceSet; return img; } }, { key: 'gotoNext', value: function gotoNext(event) { var _props = this.props, currentImage = _props.currentImage, images = _props.images; var imageLoaded = this.state.imageLoaded; if (!imageLoaded || currentImage === images.length - 1) return; if (event) { event.preventDefault(); event.stopPropagation(); } this.props.onClickNext(); } }, { key: 'gotoPrev', value: function gotoPrev(event) { var currentImage = this.props.currentImage; var imageLoaded = this.state.imageLoaded; if (!imageLoaded || currentImage === 0) return; if (event) { event.preventDefault(); event.stopPropagation(); } this.props.onClickPrev(); } }, { key: 'closeBackdrop', value: function closeBackdrop(event) { // make sure event only happens if they click the backdrop // and if the caption is widening the figure element let that respond too if (event.target.id === 'lightboxBackdrop' || event.target.tagName === 'FIGURE') { this.props.onClose(); } } }, { key: 'handleKeyboardInput', value: function handleKeyboardInput(event) { if (event.keyCode === 37) { // left this.gotoPrev(event); return true; } else if (event.keyCode === 39) { // right this.gotoNext(event); return true; } else if (event.keyCode === 27) { // esc this.props.onClose(); return true; } return false; } }, { key: 'handleImageLoaded', value: function handleImageLoaded() { this.setState({ imageLoaded: true }); } // ============================== // RENDERERS // ============================== }, { key: 'renderArrowPrev', value: function renderArrowPrev() { if (this.props.currentImage === 0) return null; return _react2.default.createElement(_Arrow2.default, { direction: 'left', icon: 'arrowLeft', onClick: this.gotoPrev, title: this.props.leftArrowTitle, type: 'button' }); } }, { key: 'renderArrowNext', value: function renderArrowNext() { if (this.props.currentImage === this.props.images.length - 1) return null; return _react2.default.createElement(_Arrow2.default, { direction: 'right', icon: 'arrowRight', onClick: this.gotoNext, title: this.props.rightArrowTitle, type: 'button' }); } }, { key: 'renderDialog', value: function renderDialog() { var _props2 = this.props, backdropClosesModal = _props2.backdropClosesModal, isOpen = _props2.isOpen, showThumbnails = _props2.showThumbnails, width = _props2.width; var imageLoaded = this.state.imageLoaded; if (!isOpen) return _react2.default.createElement('span', { key: 'closed' }); var offsetThumbnails = 0; if (showThumbnails) { offsetThumbnails = this.theme.thumbnail.size + this.theme.container.gutter.vertical; } return _react2.default.createElement( _Container2.default, { key: 'open', onClick: backdropClosesModal && this.closeBackdrop, onTouchEnd: backdropClosesModal && this.closeBackdrop }, _react2.default.createElement( 'div', null, _react2.default.createElement( 'div', { className: (0, _aphrodite.css)(this.classes.content), style: { marginBottom: offsetThumbnails, maxWidth: width } }, imageLoaded && this.renderHeader(), this.renderImages(), this.renderSpinner(), imageLoaded && this.renderFooter() ), imageLoaded && this.renderThumbnails(), imageLoaded && this.renderArrowPrev(), imageLoaded && this.renderArrowNext(), this.props.preventScroll && _react2.default.createElement(_reactScrolllock2.default, null) ) ); } }, { key: 'renderImages', value: function renderImages() { var _this3 = this; var _props3 = this.props, currentImage = _props3.currentImage, images = _props3.images, onClickImage = _props3.onClickImage; var imageLoaded = this.state.imageLoaded; if (!images || !images.length) return null; var image = images[currentImage]; var sourceSet = normalizeSourceSet(image); var sizes = sourceSet ? '100vw' : null; var imgStyle = { width: this.state.width + 'px', height: 'auto', transform: 'rotate(' + this.state.rotate + 'deg) scaleX(' + this.state.scaleX + ') scaleY(' + this.state.scaleY + ')' }; return _react2.default.createElement( 'figure', { className: (0, _aphrodite.css)(this.classes.figure) }, _react2.default.createElement('img', { className: (0, _aphrodite.css)(this.classes.image, imageLoaded && this.classes.imageLoaded), onClick: onClickImage, ref: function ref(image) { _this3.image = image; }, sizes: sizes, alt: image.alt, src: image.src, srcSet: sourceSet, style: imgStyle }) ); } }, { key: 'renderThumbnails', value: function renderThumbnails() { var _props4 = this.props, images = _props4.images, currentImage = _props4.currentImage, onClickThumbnail = _props4.onClickThumbnail, showThumbnails = _props4.showThumbnails, thumbnailOffset = _props4.thumbnailOffset; if (!showThumbnails) return; return _react2.default.createElement(_PaginatedThumbnails2.default, { currentImage: currentImage, images: images, offset: thumbnailOffset, onClickThumbnail: onClickThumbnail }); } }, { key: 'renderHeader', value: function renderHeader() { var _props5 = this.props, closeButtonTitle = _props5.closeButtonTitle, customControls = _props5.customControls, onClose = _props5.onClose, showCloseButton = _props5.showCloseButton; return _react2.default.createElement(_Header2.default, { customControls: customControls, onClose: onClose, showCloseButton: showCloseButton, closeButtonTitle: closeButtonTitle }); } }, { key: 'renderFooter', value: function renderFooter() { var _props6 = this.props, currentImage = _props6.currentImage, images = _props6.images, imageCountSeparator = _props6.imageCountSeparator, showImageCount = _props6.showImageCount, rotatable = _props6.rotatable, zoomable = _props6.zoomable, onSave = _props6.onSave; if (!images || !images.length) return null; return _react2.default.createElement(_Footer2.default, { caption: images[currentImage].caption, countCurrent: currentImage + 1, countSeparator: imageCountSeparator, countTotal: images.length, onAction: this.handleAction, rotatable: rotatable, showCount: showImageCount, zoomable: zoomable, savable: !!onSave }); } }, { key: 'renderSpinner', value: function renderSpinner() { var _props7 = this.props, spinner = _props7.spinner, spinnerColor = _props7.spinnerColor, spinnerSize = _props7.spinnerSize; var imageLoaded = this.state.imageLoaded; var Spinner = spinner; return _react2.default.createElement( 'div', { className: (0, _aphrodite.css)(this.classes.spinner, !imageLoaded && this.classes.spinnerActive) }, _react2.default.createElement(Spinner, { color: spinnerColor, size: spinnerSize }) ); } }, { key: 'render', value: function render() { return _react2.default.createElement( _Portal2.default, null, this.renderDialog() ); } }]); return Lightbox; }(_react.Component); Lightbox.propTypes = { backdropClosesModal: _propTypes2.default.bool, closeButtonTitle: _propTypes2.default.string, currentImage: _propTypes2.default.number, customControls: _propTypes2.default.arrayOf(_propTypes2.default.node), enableKeyboardInput: _propTypes2.default.bool, imageCountSeparator: _propTypes2.default.string, images: _propTypes2.default.arrayOf(_propTypes2.default.shape({ src: _propTypes2.default.string.isRequired, srcSet: _propTypes2.default.array, caption: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.element]), thumbnail: _propTypes2.default.string })).isRequired, isOpen: _propTypes2.default.bool, leftArrowTitle: _propTypes2.default.string, onClickImage: _propTypes2.default.func, onClickNext: _propTypes2.default.func, onClickPrev: _propTypes2.default.func, onClose: _propTypes2.default.func.isRequired, onRotateLeft: _propTypes2.default.func, onRotateRight: _propTypes2.default.func, onZoomIn: _propTypes2.default.func, onZoomOut: _propTypes2.default.func, preloadNextImage: _propTypes2.default.bool, preventScroll: _propTypes2.default.bool, rightArrowTitle: _propTypes2.default.string, showCloseButton: _propTypes2.default.bool, showImageCount: _propTypes2.default.bool, showThumbnails: _propTypes2.default.bool, spinner: _propTypes2.default.func, spinnerColor: _propTypes2.default.string, spinnerSize: _propTypes2.default.number, theme: _propTypes2.default.object, thumbnailOffset: _propTypes2.default.number, width: _propTypes2.default.number }; Lightbox.defaultProps = { closeButtonTitle: 'Close (Esc)', currentImage: 0, enableKeyboardInput: true, imageCountSeparator: ' of ', leftArrowTitle: 'Previous (Left arrow key)', onClickShowNextImage: true, preloadNextImage: true, preventScroll: true, rightArrowTitle: 'Next (Right arrow key)', showCloseButton: true, showImageCount: true, spinner: _Spinner2.default, spinnerColor: 'white', spinnerSize: 100, theme: {}, thumbnailOffset: 2, width: 1024 }; Lightbox.childContextTypes = { theme: _propTypes2.default.object.isRequired }; var defaultStyles = { content: { position: 'relative', width: '100vw', height: '100vh' }, figure: { margin: 0, // remove browser default width: 'auto', height: 'auto', position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }, image: { display: 'block', // removes browser default gutter margin: '0 auto', // maintain center on very short screens OR very narrow image maxWidth: '100%', // disable user select WebkitTouchCallout: 'none', userSelect: 'none', // opacity animation on image load opacity: 0, transition: 'opacity 0.3s' }, imageLoaded: { opacity: 1 }, spinner: { position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', // opacity animation to make spinner appear with delay opacity: 0, transition: 'opacity 0.3s' }, spinnerActive: { opacity: 1 } }; exports.default = Lightbox;