UNPKG

mk-react-images

Version:

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

1,310 lines (1,115 loc) 34.6 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('prop-types'), require('react'), require('aphrodite'), require('react-scrolllock'), require('aphrodite/no-important'), require('react-transition-group'), require('react-dom')) : typeof define === 'function' && define.amd ? define(['prop-types', 'react', 'aphrodite', 'react-scrolllock', 'aphrodite/no-important', 'react-transition-group', 'react-dom'], factory) : (global.Lightbox = factory(global.PropTypes,global.React,global.aphrodite,global.ScrollLock,global.aphrodite,global.ReactTransitionGroup,global.ReactDOM)); }(this, (function (PropTypes,React,aphrodite,reactScrolllock,noImportant,reactTransitionGroup,reactDom) { 'use strict'; PropTypes = PropTypes && PropTypes.hasOwnProperty('default') ? PropTypes['default'] : PropTypes; var React__default = 'default' in React ? React['default'] : React; reactScrolllock = reactScrolllock && reactScrolllock.hasOwnProperty('default') ? reactScrolllock['default'] : reactScrolllock; // ============================== // THEME // ============================== var theme = {}; // container theme.container = { background: 'rgba(0, 0, 0, 0.8)', gutter: { horizontal: 10, vertical: 10 }, zIndex: 2001 }; // header theme.header = { height: 40 }; theme.close = { fill: 'white' }; // footer theme.footer = { color: 'white', count: { color: 'rgba(255, 255, 255, 0.75)', fontSize: '0.85em' }, height: 40, gutter: { horizontal: 0, vertical: 5 } }; // thumbnails theme.thumbnail = { activeBorderColor: 'white', size: 50, gutter: 2 }; // arrow theme.arrow = { background: 'none', fill: 'white', height: 120 }; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var asyncGenerator = function () { function AwaitValue(value) { this.value = value; } function AsyncGenerator(gen) { var front, back; function send(key, arg) { return new Promise(function (resolve, reject) { var request = { key: key, arg: arg, resolve: resolve, reject: reject, next: null }; if (back) { back = back.next = request; } else { front = back = request; resume(key, arg); } }); } function resume(key, arg) { try { var result = gen[key](arg); var value = result.value; if (value instanceof AwaitValue) { Promise.resolve(value.value).then(function (arg) { resume("next", arg); }, function (arg) { resume("throw", arg); }); } else { settle(result.done ? "return" : "normal", result.value); } } catch (err) { settle("throw", err); } } function settle(type, value) { switch (type) { case "return": front.resolve({ value: value, done: true }); break; case "throw": front.reject(value); break; default: front.resolve({ value: value, done: false }); break; } front = front.next; if (front) { resume(front.key, front.arg); } else { back = null; } } this._invoke = send; if (typeof gen.return !== "function") { this.return = undefined; } } if (typeof Symbol === "function" && Symbol.asyncIterator) { AsyncGenerator.prototype[Symbol.asyncIterator] = function () { return this; }; } AsyncGenerator.prototype.next = function (arg) { return this._invoke("next", arg); }; AsyncGenerator.prototype.throw = function (arg) { return this._invoke("throw", arg); }; AsyncGenerator.prototype.return = function (arg) { return this._invoke("return", arg); }; return { wrap: function (fn) { return function () { return new AsyncGenerator(fn.apply(this, arguments)); }; }, await: function (value) { return new AwaitValue(value); } }; }(); var classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; 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 _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var inherits = function (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; }; var objectWithoutProperties = function (obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; }; var possibleConstructorReturn = function (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 deepMerge(target) { var source = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var extended = Object.assign({}, target); Object.keys(source).forEach(function (key) { if (_typeof(source[key]) !== 'object' || !source[key]) { extended[key] = source[key]; } else { if (!target[key]) { extended[key] = source[key]; } else { extended[key] = deepMerge(target[key], source[key]); } } }); return extended; } var arrowLeft = (function (fill) { return "<svg fill=\"" + fill + "\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\" width=\"100%\" height=\"100%\" viewBox=\"0 0 512 512\" xml:space=\"preserve\">\n\t\t<path d=\"M213.7,256L213.7,256L213.7,256L380.9,81.9c4.2-4.3,4.1-11.4-0.2-15.8l-29.9-30.6c-4.3-4.4-11.3-4.5-15.5-0.2L131.1,247.9 c-2.2,2.2-3.2,5.2-3,8.1c-0.1,3,0.9,5.9,3,8.1l204.2,212.7c4.2,4.3,11.2,4.2,15.5-0.2l29.9-30.6c4.3-4.4,4.4-11.5,0.2-15.8 L213.7,256z\"/>\n\t</svg>"; }); var arrowRight = (function (fill) { return "<svg fill=\"" + fill + "\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\" width=\"100%\" height=\"100%\" viewBox=\"0 0 512 512\" xml:space=\"preserve\">\n\t\t<path d=\"M298.3,256L298.3,256L298.3,256L131.1,81.9c-4.2-4.3-4.1-11.4,0.2-15.8l29.9-30.6c4.3-4.4,11.3-4.5,15.5-0.2l204.2,212.7 c2.2,2.2,3.2,5.2,3,8.1c0.1,3-0.9,5.9-3,8.1L176.7,476.8c-4.2,4.3-11.2,4.2-15.5-0.2L131.3,446c-4.3-4.4-4.4-11.5-0.2-15.8 L298.3,256z\"/>\n\t</svg>"; }); var close = (function (fill) { return "<svg fill=\"" + fill + "\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" x=\"0px\" y=\"0px\" width=\"100%\" height=\"100%\" viewBox=\"0 0 512 512\" style=\"enable-background:new 0 0 512 512;\" xml:space=\"preserve\">\n\t\t<path d=\"M443.6,387.1L312.4,255.4l131.5-130c5.4-5.4,5.4-14.2,0-19.6l-37.4-37.6c-2.6-2.6-6.1-4-9.8-4c-3.7,0-7.2,1.5-9.8,4 L256,197.8L124.9,68.3c-2.6-2.6-6.1-4-9.8-4c-3.7,0-7.2,1.5-9.8,4L68,105.9c-5.4,5.4-5.4,14.2,0,19.6l131.5,130L68.4,387.1 c-2.6,2.6-4.1,6.1-4.1,9.8c0,3.7,1.4,7.2,4.1,9.8l37.4,37.6c2.7,2.7,6.2,4.1,9.8,4.1c3.5,0,7.1-1.3,9.8-4.1L256,313.1l130.7,131.1 c2.7,2.7,6.2,4.1,9.8,4.1c3.5,0,7.1-1.3,9.8-4.1l37.4-37.6c2.6-2.6,4.1-6.1,4.1-9.8C447.7,393.2,446.2,389.7,443.6,387.1z\"/>\n\t</svg>"; }); var icons = { arrowLeft: arrowLeft, arrowRight: arrowRight, close: close }; var Icon = function Icon(_ref) { var fill = _ref.fill, type = _ref.type, props = objectWithoutProperties(_ref, ['fill', 'type']); var icon = icons[type]; return React__default.createElement('span', _extends({ dangerouslySetInnerHTML: { __html: icon(fill) } }, props)); }; Icon.propTypes = { fill: PropTypes.string, type: PropTypes.oneOf(Object.keys(icons)) }; Icon.defaultProps = { fill: 'white' }; function Arrow(_ref, _ref2) { var theme$$1 = _ref2.theme; var direction = _ref.direction, icon = _ref.icon, onClick = _ref.onClick, size = _ref.size, props = objectWithoutProperties(_ref, ['direction', 'icon', 'onClick', 'size']); var classes = noImportant.StyleSheet.create(deepMerge(defaultStyles$1, theme$$1)); return React__default.createElement( 'button', _extends({ type: 'button', className: noImportant.css(classes.arrow, classes['arrow__direction__' + direction], size && classes['arrow__size__' + size]), onClick: onClick, onTouchEnd: onClick }, props), React__default.createElement(Icon, { fill: !!theme$$1.arrow && theme$$1.arrow.fill || theme.arrow.fill, type: icon }) ); } Arrow.propTypes = { direction: PropTypes.oneOf(['left', 'right']), icon: PropTypes.string, onClick: PropTypes.func.isRequired, size: PropTypes.oneOf(['medium', 'small']).isRequired }; Arrow.defaultProps = { size: 'medium' }; Arrow.contextTypes = { theme: PropTypes.object.isRequired }; var defaultStyles$1 = { arrow: { background: 'none', border: 'none', borderRadius: 4, cursor: 'pointer', outline: 'none', padding: 10, // increase hit area position: 'absolute', top: '50%', // disable user select WebkitTouchCallout: 'none', userSelect: 'none' }, // sizes arrow__size__medium: { height: theme.arrow.height, marginTop: theme.arrow.height / -2, width: 40, '@media (min-width: 768px)': { width: 70 } }, arrow__size__small: { height: theme.thumbnail.size, marginTop: theme.thumbnail.size / -2, width: 30, '@media (min-width: 500px)': { width: 40 } }, // direction arrow__direction__right: { right: theme.container.gutter.horizontal }, arrow__direction__left: { left: theme.container.gutter.horizontal } }; function Container(_ref, _ref2) { var theme$$1 = _ref2.theme; var props = objectWithoutProperties(_ref, []); var classes = noImportant.StyleSheet.create(deepMerge(defaultStyles$2, theme$$1)); return React__default.createElement('div', _extends({ id: 'lightboxBackdrop', className: noImportant.css(classes.container) }, props)); } Container.contextTypes = { theme: PropTypes.object.isRequired }; var defaultStyles$2 = { container: { alignItems: 'center', backgroundColor: theme.container.background, boxSizing: 'border-box', display: 'flex', height: '100%', justifyContent: 'center', left: 0, paddingBottom: theme.container.gutter.vertical, paddingLeft: theme.container.gutter.horizontal, paddingRight: theme.container.gutter.horizontal, paddingTop: theme.container.gutter.vertical, position: 'fixed', top: 0, width: '100%', zIndex: theme.container.zIndex } }; function Footer(_ref, _ref2) { var theme$$1 = _ref2.theme; var caption = _ref.caption, countCurrent = _ref.countCurrent, countSeparator = _ref.countSeparator, countTotal = _ref.countTotal, showCount = _ref.showCount, props = objectWithoutProperties(_ref, ['caption', 'countCurrent', 'countSeparator', 'countTotal', 'showCount']); if (!caption && !showCount) return null; var classes = noImportant.StyleSheet.create(deepMerge(defaultStyles$3, theme$$1)); var imageCount = showCount ? React__default.createElement( 'div', { className: noImportant.css(classes.footerCount) }, countCurrent, countSeparator, countTotal ) : React__default.createElement('span', null); return React__default.createElement( 'div', _extends({ className: noImportant.css(classes.footer) }, props), caption ? React__default.createElement( 'figcaption', { className: noImportant.css(classes.footerCaption) }, caption ) : React__default.createElement('span', null), imageCount ); } Footer.propTypes = { caption: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), countCurrent: PropTypes.number, countSeparator: PropTypes.string, countTotal: PropTypes.number, showCount: PropTypes.bool }; Footer.contextTypes = { theme: PropTypes.object.isRequired }; var defaultStyles$3 = { footer: { boxSizing: 'border-box', color: theme.footer.color, cursor: 'auto', display: 'flex', justifyContent: 'space-between', left: 0, lineHeight: 1.3, paddingBottom: theme.footer.gutter.vertical, paddingLeft: theme.footer.gutter.horizontal, paddingRight: theme.footer.gutter.horizontal, paddingTop: theme.footer.gutter.vertical }, footerCount: { color: theme.footer.count.color, fontSize: theme.footer.count.fontSize, paddingLeft: '1em' // add a small gutter for the caption }, footerCaption: { flex: '1 1 0' } }; function Header(_ref, _ref2) { var theme$$1 = _ref2.theme; var customControls = _ref.customControls, onClose = _ref.onClose, showCloseButton = _ref.showCloseButton, closeButtonTitle = _ref.closeButtonTitle, props = objectWithoutProperties(_ref, ['customControls', 'onClose', 'showCloseButton', 'closeButtonTitle']); var classes = noImportant.StyleSheet.create(deepMerge(defaultStyles$4, theme$$1)); return React__default.createElement( 'div', _extends({ className: noImportant.css(classes.header) }, props), customControls ? customControls : React__default.createElement('span', null), !!showCloseButton && React__default.createElement( 'button', { title: closeButtonTitle, className: noImportant.css(classes.close), onClick: onClose }, React__default.createElement(Icon, { fill: !!theme$$1.close && theme$$1.close.fill || theme.close.fill, type: 'close' }) ) ); } Header.propTypes = { customControls: PropTypes.array, onClose: PropTypes.func.isRequired, showCloseButton: PropTypes.bool }; Header.contextTypes = { theme: PropTypes.object.isRequired }; var defaultStyles$4 = { header: { display: 'flex', justifyContent: 'space-between', height: theme.header.height }, close: { background: 'none', border: 'none', cursor: 'pointer', outline: 'none', position: 'relative', top: 0, verticalAlign: 'bottom', // increase hit area height: 40, marginRight: -10, padding: 10, width: 40 } }; function Thumbnail(_ref, _ref2) { var index = _ref.index, src = _ref.src, thumbnail = _ref.thumbnail, active = _ref.active, _onClick = _ref.onClick; var theme$$1 = _ref2.theme; var url = thumbnail ? thumbnail : src; var classes = noImportant.StyleSheet.create(deepMerge(defaultStyles$5, theme$$1)); return React__default.createElement('div', { className: noImportant.css(classes.thumbnail, active && classes.thumbnail__active), onClick: function onClick(e) { e.preventDefault(); e.stopPropagation(); _onClick(index); }, style: { backgroundImage: 'url("' + url + '")' } }); } Thumbnail.propTypes = { active: PropTypes.bool, index: PropTypes.number, onClick: PropTypes.func.isRequired, src: PropTypes.string, thumbnail: PropTypes.string }; Thumbnail.contextTypes = { theme: PropTypes.object.isRequired }; var defaultStyles$5 = { thumbnail: { backgroundPosition: 'center', backgroundSize: 'cover', borderRadius: 2, boxShadow: 'inset 0 0 0 1px hsla(0,0%,100%,.2)', cursor: 'pointer', display: 'inline-block', height: theme.thumbnail.size, margin: theme.thumbnail.gutter, overflow: 'hidden', width: theme.thumbnail.size }, thumbnail__active: { boxShadow: 'inset 0 0 0 2px ' + theme.thumbnail.activeBorderColor } }; var classes = noImportant.StyleSheet.create({ paginatedThumbnails: { bottom: theme.container.gutter.vertical, height: theme.thumbnail.size, padding: '0 50px', position: 'absolute', textAlign: 'center', whiteSpace: 'nowrap', left: '50%', transform: 'translateX(-50%)' } }); var arrowStyles = { height: theme.thumbnail.size + theme.thumbnail.gutter * 2, width: 40 }; var PaginatedThumbnails = function (_Component) { inherits(PaginatedThumbnails, _Component); function PaginatedThumbnails(props) { classCallCheck(this, PaginatedThumbnails); var _this = possibleConstructorReturn(this, (PaginatedThumbnails.__proto__ || Object.getPrototypeOf(PaginatedThumbnails)).call(this, props)); _this.state = { hasCustomPage: false }; _this.gotoPrev = _this.gotoPrev.bind(_this); _this.gotoNext = _this.gotoNext.bind(_this); return _this; } createClass(PaginatedThumbnails, [{ key: 'componentWillReceiveProps', value: function componentWillReceiveProps(nextProps) { // Component should be controlled, flush state when currentImage changes if (nextProps.currentImage !== this.props.currentImage) { this.setState({ hasCustomPage: false }); } } // ============================== // METHODS // ============================== }, { key: 'getFirst', value: function getFirst() { var _props = this.props, currentImage = _props.currentImage, offset = _props.offset; if (this.state.hasCustomPage) { return this.clampFirst(this.state.first); } return this.clampFirst(currentImage - offset); } }, { key: 'setFirst', value: function setFirst(event, newFirst) { var first = this.state.first; if (event) { event.preventDefault(); event.stopPropagation(); } if (first === newFirst) return; this.setState({ hasCustomPage: true, first: newFirst }); } }, { key: 'gotoPrev', value: function gotoPrev(event) { this.setFirst(event, this.getFirst() - this.props.offset); } }, { key: 'gotoNext', value: function gotoNext(event) { this.setFirst(event, this.getFirst() + this.props.offset); } }, { key: 'clampFirst', value: function clampFirst(value) { var _props2 = this.props, images = _props2.images, offset = _props2.offset; var totalCount = 2 * offset + 1; // show $offset extra thumbnails on each side if (value < 0) { return 0; } else if (value + totalCount > images.length) { // Too far return images.length - totalCount; } else { return value; } } // ============================== // RENDERERS // ============================== }, { key: 'renderArrowPrev', value: function renderArrowPrev() { if (this.getFirst() <= 0) return null; return React__default.createElement(Arrow, { direction: 'left', size: 'small', icon: 'arrowLeft', onClick: this.gotoPrev, style: arrowStyles, title: 'Previous (Left arrow key)', type: 'button' }); } }, { key: 'renderArrowNext', value: function renderArrowNext() { var _props3 = this.props, offset = _props3.offset, images = _props3.images; var totalCount = 2 * offset + 1; if (this.getFirst() + totalCount >= images.length) return null; return React__default.createElement(Arrow, { direction: 'right', size: 'small', icon: 'arrowRight', onClick: this.gotoNext, style: arrowStyles, title: 'Next (Right arrow key)', type: 'button' }); } }, { key: 'render', value: function render$$1() { var _props4 = this.props, images = _props4.images, currentImage = _props4.currentImage, onClickThumbnail = _props4.onClickThumbnail, offset = _props4.offset; var totalCount = 2 * offset + 1; // show $offset extra thumbnails on each side var thumbnails = []; var baseOffset = 0; if (images.length <= totalCount) { thumbnails = images; } else { // Try to center current image in list baseOffset = this.getFirst(); thumbnails = images.slice(baseOffset, baseOffset + totalCount); } return React__default.createElement( 'div', { className: noImportant.css(classes.paginatedThumbnails) }, this.renderArrowPrev(), thumbnails.map(function (img, idx) { return React__default.createElement(Thumbnail, _extends({ key: baseOffset + idx }, img, { index: baseOffset + idx, onClick: onClickThumbnail, active: baseOffset + idx === currentImage })); }), this.renderArrowNext() ); } }]); return PaginatedThumbnails; }(React.Component); PaginatedThumbnails.propTypes = { currentImage: PropTypes.number, images: PropTypes.array, offset: PropTypes.number, onClickThumbnail: PropTypes.func.isRequired }; // Pass the Lightbox context through to the Portal's descendents // StackOverflow discussion http://goo.gl/oclrJ9 var PassContext = function (_Component) { inherits(PassContext, _Component); function PassContext() { classCallCheck(this, PassContext); return possibleConstructorReturn(this, (PassContext.__proto__ || Object.getPrototypeOf(PassContext)).apply(this, arguments)); } createClass(PassContext, [{ key: 'getChildContext', value: function getChildContext() { return this.props.context; } }, { key: 'render', value: function render$$1() { return React.Children.only(this.props.children); } }]); return PassContext; }(React.Component); PassContext.propTypes = { context: PropTypes.object.isRequired }; PassContext.childContextTypes = { theme: PropTypes.object }; var Portal = function (_Component) { inherits(Portal, _Component); function Portal() { classCallCheck(this, Portal); var _this = possibleConstructorReturn(this, (Portal.__proto__ || Object.getPrototypeOf(Portal)).call(this)); _this.portalElement = null; return _this; } createClass(Portal, [{ key: 'componentDidMount', value: function componentDidMount() { var p = document.createElement('div'); document.body.appendChild(p); this.portalElement = p; this.componentDidUpdate(); } }, { key: 'componentDidUpdate', value: function componentDidUpdate() { // Animate fade on mount/unmount var duration = 200; var styles = '\n\t\t\t\t.fade-enter { opacity: 0.01; }\n\t\t\t\t.fade-enter.fade-enter-active { opacity: 1; transition: opacity ' + duration + 'ms; }\n\t\t\t\t.fade-leave { opacity: 1; }\n\t\t\t\t.fade-leave.fade-leave-active { opacity: 0.01; transition: opacity ' + duration + 'ms; }\n\t\t'; reactDom.render(React__default.createElement( PassContext, { context: this.context }, React__default.createElement( 'div', null, React__default.createElement( 'style', null, styles ), React__default.createElement(reactTransitionGroup.CSSTransitionGroup, _extends({ component: 'div', transitionName: 'fade', transitionEnterTimeout: duration, transitionLeaveTimeout: duration }, this.props)) ) ), this.portalElement); } }, { key: 'componentWillUnmount', value: function componentWillUnmount() { document.body.removeChild(this.portalElement); } }, { key: 'render', value: function render$$1() { return null; } }]); return Portal; }(React.Component); Portal.contextTypes = { theme: PropTypes.object.isRequired }; /** Bind multiple component methods: * @param {this} context * @param {Array} functions constructor() { ... bindFunctions.call(this, ['handleClick', 'handleOther']); } */ function bindFunctions(functions) { var _this = this; functions.forEach(function (f) { return _this[f] = _this[f].bind(_this); }); } // Return true if window + document var canUseDom = !!(typeof window !== 'undefined' && window.document && window.document.createElement); 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 = deepMerge(theme, props.theme); _this.classes = aphrodite.StyleSheet.create(deepMerge(defaultStyles, _this.theme)); bindFunctions.call(_this, ['gotoNext', 'gotoPrev', 'closeBackdrop', 'handleKeyboardInput']); return _this; } createClass(Lightbox, [{ key: 'getChildContext', value: function getChildContext() { return { theme: this.theme }; } }, { key: 'componentDidMount', value: function componentDidMount() { if (this.props.isOpen && this.props.enableKeyboardInput) { window.addEventListener('keydown', this.handleKeyboardInput); } } }, { key: 'componentWillReceiveProps', value: function componentWillReceiveProps(nextProps) { if (!canUseDom) return; // preload images if (nextProps.preloadNextImage) { var currentIndex = this.props.currentImage; var nextIndex = nextProps.currentImage + 1; var prevIndex = nextProps.currentImage - 1; var preloadIndex = void 0; if (currentIndex && nextProps.currentImage > currentIndex) { preloadIndex = nextIndex; } else if (currentIndex && nextProps.currentImage < currentIndex) { preloadIndex = prevIndex; } // if we know the user's direction just get one image // otherwise, to be safe, we need to grab one in each direction if (preloadIndex) { this.preloadImage(preloadIndex); } else { this.preloadImage(prevIndex); this.preloadImage(nextIndex); } } // 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: 'preloadImage', value: function preloadImage(idx) { var image = this.props.images[idx]; if (!image) return; var img = new Image(); img.src = image.src; img.srcset = img.srcSet || img.srcset; if (image.srcset) { img.srcset = image.srcset.join(); } } }, { key: 'gotoNext', value: function gotoNext(event) { if (this.props.currentImage === this.props.images.length - 1) return; if (event) { event.preventDefault(); event.stopPropagation(); } this.props.onClickNext(); } }, { key: 'gotoPrev', value: function gotoPrev(event) { if (this.props.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; } // ============================== // RENDERERS // ============================== }, { key: 'renderArrowPrev', value: function renderArrowPrev() { if (this.props.currentImage === 0) return null; return React__default.createElement(Arrow, { 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 React__default.createElement(Arrow, { direction: 'right', icon: 'arrowRight', onClick: this.gotoNext, title: this.props.rightArrowTitle, type: 'button' }); } }, { key: 'renderDialog', value: function renderDialog() { var _props = this.props, backdropClosesModal = _props.backdropClosesModal, customControls = _props.customControls, isOpen = _props.isOpen, onClose = _props.onClose, showCloseButton = _props.showCloseButton, showThumbnails = _props.showThumbnails, width = _props.width; if (!isOpen) return React__default.createElement('span', { key: 'closed' }); var offsetThumbnails = 0; if (showThumbnails) { offsetThumbnails = this.theme.thumbnail.size + this.theme.container.gutter.vertical; } return React__default.createElement( Container, { key: 'open', onClick: backdropClosesModal && this.closeBackdrop, onTouchEnd: backdropClosesModal && this.closeBackdrop }, React__default.createElement( 'div', { className: aphrodite.css(this.classes.content), style: { marginBottom: offsetThumbnails, maxWidth: width } }, React__default.createElement(Header, { customControls: customControls, onClose: onClose, showCloseButton: showCloseButton, closeButtonTitle: this.props.closeButtonTitle }), this.renderImages() ), this.renderThumbnails(), this.renderArrowPrev(), this.renderArrowNext() ); } }, { key: 'renderImages', value: function renderImages() { var _props2 = this.props, currentImage = _props2.currentImage, images = _props2.images, imageCountSeparator = _props2.imageCountSeparator, onClickImage = _props2.onClickImage, showImageCount = _props2.showImageCount, showThumbnails = _props2.showThumbnails; if (!images || !images.length) return null; var image = images[currentImage]; image.srcset = image.srcSet || image.srcset; var srcset = void 0; var sizes = void 0; if (image.srcset) { srcset = image.srcset.join(); sizes = '100vw'; } var thumbnailsSize = showThumbnails ? this.theme.thumbnail.size : 0; var heightOffset = this.theme.header.height + this.theme.footer.height + thumbnailsSize + this.theme.container.gutter.vertical + 'px'; return React__default.createElement( 'figure', { className: aphrodite.css(this.classes.figure) }, React__default.createElement('img', { className: aphrodite.css(this.classes.image), onClick: !!onClickImage && onClickImage, sizes: sizes, alt: image.alt, src: image.src, srcSet: srcset, style: { cursor: this.props.onClickImage ? 'pointer' : 'auto', maxHeight: 'calc(100vh - ' + heightOffset + ')' } }), React__default.createElement(Footer, { caption: images[currentImage].caption, countCurrent: currentImage + 1, countSeparator: imageCountSeparator, countTotal: images.length, showCount: showImageCount }) ); } }, { key: 'renderThumbnails', value: function renderThumbnails() { var _props3 = this.props, images = _props3.images, currentImage = _props3.currentImage, onClickThumbnail = _props3.onClickThumbnail, showThumbnails = _props3.showThumbnails, thumbnailOffset = _props3.thumbnailOffset; if (!showThumbnails) return; return React__default.createElement(PaginatedThumbnails, { currentImage: currentImage, images: images, offset: thumbnailOffset, onClickThumbnail: onClickThumbnail }); } }, { key: 'render', value: function render$$1() { return React__default.createElement( Portal, null, this.renderDialog() ); } }]); return Lightbox; }(React.Component); Lightbox.propTypes = { backdropClosesModal: PropTypes.bool, closeButtonTitle: PropTypes.string, currentImage: PropTypes.number, customControls: PropTypes.arrayOf(PropTypes.node), enableKeyboardInput: PropTypes.bool, imageCountSeparator: PropTypes.string, images: PropTypes.arrayOf(PropTypes.shape({ src: PropTypes.string.isRequired, srcset: PropTypes.array, caption: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), thumbnail: PropTypes.string })).isRequired, isOpen: PropTypes.bool, leftArrowTitle: PropTypes.string, onClickImage: PropTypes.func, onClickNext: PropTypes.func, onClickPrev: PropTypes.func, onClose: PropTypes.func.isRequired, preloadNextImage: PropTypes.bool, rightArrowTitle: PropTypes.string, showCloseButton: PropTypes.bool, showImageCount: PropTypes.bool, showThumbnails: PropTypes.bool, theme: PropTypes.object, thumbnailOffset: PropTypes.number, width: PropTypes.number }; Lightbox.defaultProps = { closeButtonTitle: 'Close (Esc)', currentImage: 0, enableKeyboardInput: true, imageCountSeparator: ' of ', leftArrowTitle: 'Previous (Left arrow key)', onClickShowNextImage: true, preloadNextImage: true, rightArrowTitle: 'Next (Right arrow key)', showCloseButton: true, showImageCount: true, theme: {}, thumbnailOffset: 2, width: 1024 }; Lightbox.childContextTypes = { theme: PropTypes.object.isRequired }; var defaultStyles = { content: { position: 'relative' }, figure: { margin: 0 // remove browser default }, image: { display: 'block', // removes browser default gutter height: 'auto', margin: '0 auto', // maintain center on very short screens OR very narrow image maxWidth: '100%', // disable user select WebkitTouchCallout: 'none', userSelect: 'none' } }; return Lightbox; })));