@momentum-ui/react
Version:
Cisco Momentum UI framework for ReactJs applications
604 lines (564 loc) • 18.1 kB
JavaScript
/** @component lightbox */
import React from 'react';
import PropTypes from 'prop-types';
import Modal from 'react-aria-modal';
import { Spinner, Tooltip, Icon } from '@momentum-ui/react';
class Lightbox extends React.Component {
state = {
viewportDimensions: {
width: 600,
height: 600
},
zoom: 1
}
componentDidMount() {
window.addEventListener('keydown', this.handleKeyDown, true);
window.addEventListener('resize', this.handleResize, true);
const { viewport } = this;
if (viewport) {
// eslint-disable-next-line react/no-did-mount-set-state
this.setState({
viewportDimensions: {
width: viewport.clientWidth,
height: viewport.clientHeight
}
});
}
}
componentDidUpdate(prevProps) {
if (prevProps.index !== this.props.index && this.state.zoom > 1 && this.imgWrapper) {
const viewportNode = this.viewport;
viewportNode.scrollTop = 0;
viewportNode.scrollLeft = (this.imgWrapper.offsetWidth - viewportNode.offsetWidth) / 2;
}
}
componentWillUnmount() {
window.removeEventListener('keydown', this.handleKeyDown, true);
window.removeEventListener('resize', this.handleResize, true);
}
handleResize = () => {
const { viewport } = this;
this.setState({
viewportDimensions: {
width: viewport.offsetWidth,
height: viewport.offsetHeight
}
});
}
handleKeyDown = e => {
const { index, pages } = this.props;
let newIndex;
switch (e.keyCode) {
// Escape
case 27:
this.handleClose();
return;
// left arrow & up arrow
case 37:
case 38:
newIndex = Math.max(index - 1, 0);
break;
// right arrow & down arrow
case 39:
case 40:
newIndex = Math.min(index + 1, pages.length - 1);
break;
// page up & home
case 33:
case 36:
newIndex = 0;
break;
// page down & end
case 34:
case 35:
newIndex = pages.length - 1;
break;
// 1 - 9
case 49:
case 50:
case 51:
case 52:
case 53:
case 54:
case 55:
case 56:
case 57:
newIndex = Math.min(e.keyCode - 49, pages.length - 1);
break;
default:
return;
}
this.triggerPageChange(newIndex, e);
}
handleThumbnailClick = index => {
const { onChange } = this.props;
onChange && onChange(index);
}
triggerPageChange = (index, e) => {
const { onChange, pages } = this.props;
const target = this.lightBox && this.lightBox.querySelector(`[data-index="${index}"]`);
if (index >= 0 && index <= pages.length - 1) {
onChange && onChange(index);
}
e.stopPropagation();
target && target.scrollIntoViewIfNeeded();
}
stopPropagation = e => {
e.stopPropagation();
}
setZoom = increment => {
const newZoom = this.state.zoom + increment;
this.setState({
zoom: newZoom < 0.25 ? 0.25 : newZoom
});
}
handleDownload = () => {
const { onDownload } = this.props;
onDownload && onDownload();
}
handleClose = () => {
const { onClose } = this.props;
onClose && onClose();
}
render() {
const { pages, index, width, height, tooltips, downloading, info, name, applicationId, imgClassName, isImageRotated, popoverProps } = this.props;
const { zoom, viewportDimensions } = this.state;
const currentPage = pages[index];
const showColumn = pages.length > 1;
const calculateAspectRatioFit = (srcWidth, srcHeight, maxWidth, maxHeight) => {
let maxW, maxH;
if (isImageRotated) {
maxW = maxHeight;
maxH = maxWidth;
}
else {
maxW = maxWidth;
maxH = maxHeight;
}
const ratio = Math.min(maxW / srcWidth, maxH / srcHeight, 1);
return {
width: Math.round(srcWidth * ratio),
height: Math.round(srcHeight * ratio),
ratio
};
};
const getThumbnails = () => {
const thumbnails = pages.map((page, idx) => {
let key = `${idx}:${page.thumb}`;
let body;
if (page.decrypting) {
const scale = width / 150;
const scaleY = height / scale;
const style = {
height: Math.round(scaleY)
};
key += ':decrypting';
body = (
<div
className={
`md-lightbox__thumbnail` +
`${(!!page.decrypting && ` md-lightbox__thumbnail--decrypting`) || ''}`
}
data-index={idx}
style={style}
>
<Icon className="md-lightbox__thumbnail--icon" name="secure_28" />
</div>
);
}
else {
body = (
<img
alt=""
className={
`md-lightbox__thumbnail` +
`${(!!page.decrypting && ` md-lightbox__thumbnail--decrypting`) || ''}`
}
data-index={idx}
draggable="false"
onDragStart={() => false}
src={page.thumb}
/>
);
}
return (
<div
className={
'md-lightbox__thumbnail-wrapper' +
`${(idx === index && ` md-lightbox__thumbnail-wrapper--selected`) || ''}`
}
key={key}
onClick={() => this.handleThumbnailClick(idx)}
onKeyPress={() => this.handleThumbnailClick(idx)}
role="button"
tabIndex="0"
>
{body}
<div>
{idx + 1}
</div>
</div>
);
});
return (
<div className="md-lightbox__list">
{thumbnails}
</div>
);
};
let newWidth = width;
let newHeight = height;
const getViewport = () => {
let viewport;
let imageContainerStyles;
if (currentPage.content) {
if (currentPage.fullView) {
imageContainerStyles = {
width: '100%',
height: '100%',
overflow: 'hidden'
};
}
viewport = (
<div
className="md-lightbox__viewport-content"
draggable="false"
onClick={this.stopPropagation}
onKeyPress={this.stopPropagation}
onDoubleClick={() => this.setZoom(0.25)}
onDragStart={() => false}
role="button"
tabIndex="0"
>
{currentPage.content}
</div>
);
}
else if (currentPage.image) {
if (zoom <= 1) {
const dimensions = calculateAspectRatioFit(
width * zoom,
height * zoom,
viewportDimensions.width,
viewportDimensions.height
);
newHeight = dimensions.height;
newWidth = dimensions.width;
imageContainerStyles = {
width: `${dimensions.width}px`,
height: `${dimensions.height}px`
};
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
viewport = (
<img
alt=""
className={"md-lightbox__viewport-image" + `${(imgClassName && ` ${imgClassName}`) || ''}`}
draggable="false"
onClick={this.stopPropagation}
onKeyPress={this.stopPropagation}
onDoubleClick={() => this.setZoom(0.25)}
onDragStart={() => false}
src={currentPage.image}
/>
);
}
else {
const dimensions = calculateAspectRatioFit(
width,
height,
viewportDimensions.width,
viewportDimensions.height
);
imageContainerStyles = {};
newHeight = dimensions.height * zoom;
newWidth = dimensions.width * zoom;
viewport = (
<img
alt=""
className={"md-lightbox__viewport-image" + `${(imgClassName && ` ${imgClassName}`) || ''}`}
draggable="false"
onClick={this.stopPropagation}
onKeyPress={this.stopPropagation}
onDoubleClick={() => this.setZoom(0.25)}
onDragStart={() => false}
src={currentPage.image}
style={{
maxHeight: newHeight,
maxWidth: newWidth,
minHeight: newHeight,
minWidth: newWidth
}}
/>
);
}
}
return (
<div
className="md-lightbox__viewport-wrapper"
ref={ref => this.imgWrapper = ref}
style={imageContainerStyles}
>
{viewport}
</div>
);
};
const leftArrowControl = (
<Tooltip tooltip={tooltips.previous} popoverProps={{direction: "right-center", isContained: true}}>
<div
className="md-lightbox__page-control md-lightbox__page-control-icon md-lightbox__page-controls--left"
role="button"
tabIndex="0"
onKeyPress={e => this.triggerPageChange(index - 1, e)}
onClick={e => this.triggerPageChange(index - 1, e)}
style={{transform: 'rotate(-180deg)'}}
>
<Icon name="arrow-right_16" />
</div>
</Tooltip>
);
const rightArrowControl = (
<Tooltip tooltip={tooltips.next} popoverProps={{direction: "left-center", isContained: true}}>
<div
className="md-lightbox__page-control md-lightbox__page-control-icon md-lightbox__page-controls--right"
role="button"
tabIndex="0"
onKeyPress={e => this.triggerPageChange(index + 1, e)}
onClick={e => this.triggerPageChange(index + 1, e)}
>
<Icon name="arrow-right_16" />
</div>
</Tooltip>
);
const viewportControls = () => {
const downloadButton = (
<div
className="md-lightbox__control md-lightbox__control-download"
tabIndex="0"
role="button"
onClick={this.handleDownload}
onKeyPress={this.handleDownload}
>
<Icon name="download_16"/>
</div>
);
const controlStyle = currentPage.content ? {
visibility: 'hidden'
} : {};
const pageControl = pages.length > 1 ? (
<div className="md-lightbox__controls md-lightbox__controls--center">
<Tooltip tooltip={tooltips.previous}>
<div className="md-lightbox__control"
onClick={e => this.triggerPageChange(index - 1, e)}
role="button"
tabIndex="0"
onKeyPress={e => this.triggerPageChange(index - 1, e)}
style={{transform: 'rotate(-180deg)'}}
>
<Icon name="arrow-right_16"/>
</div>
</Tooltip>
<span className="md-lightbox__control-value">{`${index + 1} / ${pages.length}`}</span>
<Tooltip tooltip={tooltips.next}>
<div className="md-lightbox__control"
role="button"
onClick={e => this.triggerPageChange(index + 1, e)}
tabIndex="0"
onKeyPress={e => this.triggerPageChange(index + 1, e)}
>
<Icon name="arrow-right_16"/>
</div>
</Tooltip>
</div>
) : (
<div className="md-lightbox__controls">
<span className="md-lightbox__control-value">{index + 1}</span>
</div>
);
return (
<div
className="md-lightbox__viewer-controls"
onClick={this.stopPropagation}
onKeyPress={this.stopPropagation}
role="button"
tabIndex="0"
>
<div className="md-lightbox__controls" style={controlStyle}>
<Tooltip tooltip={tooltips.zoomOut}>
<div className="md-lightbox__control"
onClick={() => this.setZoom(-0.25)}
role="button"
tabIndex="0"
onKeyPress={() => this.setZoom(-0.25)}
>
<Icon name="zoom-out_16"/>
</div>
</Tooltip>
<span className="md-lightbox__control-value">{Math.round((newHeight * 1.0)/height * 100)}%</span>
<Tooltip tooltip={tooltips.zoomIn}>
<div className="md-lightbox__control"
role="button"
onClick={() => this.setZoom(0.25)}
tabIndex="0"
onKeyPress={() => this.setZoom(0.25)}
>
<Icon name="zoom-in_16"/>
</div>
</Tooltip>
</div>
{pageControl}
<div className="md-lightbox__controls" style={controlStyle}>
<span className="md-lightbox__control-value">{info.size}</span>
<Tooltip tooltip={downloading ? tooltips.downloading : tooltips.download}>
{ downloading ?
<div className="md-lightbox__control md-lightbox__control-spinner">
<Spinner size={28}/>
</div> :
downloadButton
}
</Tooltip>
</div>
</div>
);
};
return (
<Modal
includeDefaultStyles={false}
getApplicationNode={() => document.querySelector(`#${applicationId}`)}
onExit={this.handleClose}
focusDialog={true}
titleId="md-lightbox"
dialogClass="md-lightbox"
underlayClass="md-lightbox__container"
>
<div className="md-lightbox__header" ref={ref => this.lightBox = ref}>
<div className="md-lightbox__header-item--left">
<div className="md-lightbox__header-meta">
<div className="md-lightbox__header-sharer">
{info.sharedBy}
</div>
<div className="md-lightbox__header-timestamp">
{info.sharedOn}
</div>
</div>
</div>
<div className="md-lightbox__header-item--center">
<div className="md-lightbox__header-name">
{name}
</div>
</div>
<div className="md-lightbox__header-item--right">
<Tooltip popoverProps={popoverProps} tooltip={tooltips.exit}>
<div className="md-lightbox__control"
onClick={this.handleClose}
role="button"
tabIndex="0"
onKeyPress={this.handleClose}>
<Icon name="cancel_16"/>
</div>
</Tooltip>
</div>
</div>
<div className="md-lightbox__body">
{showColumn && getThumbnails()}
<div
className="md-lightbox__content"
onClick={this.handleClose}
onKeyPress={this.handleClose}
role="button"
tabIndex="0"
>
<div
className={
`md-lightbox__viewport` +
`${(!!currentPage.decrypting && ` md-lightbox__viewport--decrypting`) || ''}`
}
ref={ref => this.viewport = ref}
>
{
pages[index].decrypting &&
<Spinner className="md-lightbox__decrypting-spinner" />
}
{getViewport()}
</div>
{showColumn && leftArrowControl}
{showColumn && rightArrowControl}
{viewportControls()}
</div>
</div>
</Modal>
);
}
}
Lightbox.propTypes = {
/** @prop ID for Lightbox query lookup */
applicationId: PropTypes.string.isRequired,
/** Determines if info is decrypting | false */
decrypting: PropTypes.bool,
/** @prop Optional downloading css styling | false */
downloading: PropTypes.bool,
/** @prop Set Height value of Lightbox */
height: PropTypes.number.isRequired,
/** @prop Classname appended to img viewport | '' */
imgClassName: PropTypes.string,
/** @prop Initial index of start page | 0 */
index: PropTypes.number,
/** @prop Lightbox information Object | {} */
info: PropTypes.shape({
sharedBy: PropTypes.string,
sharedOn: PropTypes.string,
size: PropTypes.string
}),
/** @prop Optional indication if image is rotated | false */
isImageRotated: PropTypes.bool,
/** @prop Required name prop for Lightbox */
name: PropTypes.string.isRequired,
/** @prop Callback function invoked by user when interact with Lightbox | null */
onChange: PropTypes.func,
/** @prop Callback function invoked by user closing the Lightbox | null */
onClose: PropTypes.func,
/** @prop Callback function invoked by the download action of Lightbox | null */
onDownload: PropTypes.func,
/** @prop Array of Lightbox pages */
pages: PropTypes.array.isRequired,
/** @prop tooltip style | {isContained:true, direction: 'bottom-right'} */
popoverProps: PropTypes.object,
/** @prop Collection of predefined tootips for various Lightbox actions | { download: 'Download', etc } */
tooltips: PropTypes.shape({
download: PropTypes.string,
downloading: PropTypes.string,
exit: PropTypes.string,
previous: PropTypes.string,
next: PropTypes.string,
zoomIn: PropTypes.string,
zoomOut: PropTypes.string
}),
/** @prop Set Width value for Lightbox */
width: PropTypes.number.isRequired,
};
Lightbox.defaultProps = {
decrypting: false,
downloading: false,
imgClassName: '',
index: 0,
info: {},
isImageRotated: false,
name: '',
onChange: null,
onClose: null,
onDownload: null,
popoverProps:{
isContained:true,
direction: 'bottom-right'
},
tooltips: {
download: 'Download',
downloading: 'Downloading...',
exit: 'Exit',
previous: 'Previous',
next: 'Next',
zoomIn: 'Zoom in',
zoomOut: 'Zoom out'
},
};
Lightbox.displayName = 'Lightbox';
export default Lightbox;