react-awesome-lightbox
Version:
lightbox image viewer for react with zoom, rotate. Single or multi image mode and touch support
270 lines (269 loc) • 11.3 kB
JavaScript
import React from "react";
const DEFAULT_ZOOM_STEP = 0.3;
const DEFAULT_LARGE_ZOOM = 4;
function getXY(e){
let x = 0;
let y = 0;
if(e.touches && e.touches.length){
x = e.touches[0].pageX;
y = e.touches[0].pageY;
}
else{
x = e.pageX;
y = e.pageY;
}
return {x,y}
}
function Cond(props){
if(!props.condition) return null;
return (
<React.Fragment>
{props.children}
</React.Fragment>
);
}
export default class Lightbox extends React.Component {
initX = 0;
initY = 0;
lastX = 0;
lastY = 0;
_cont = React.createRef();
state = {
x : 0,
y : 0,
zoom : 1,
rotate : 0,
loading : true,
moving : false,
current : this.props?.startIndex ?? 0,
multi : this.props?.images?.length? true: false
}
createTransform = (x,y,zoom,rotate) => `translate3d(${x}px,${y}px,0px) scale(${zoom}) rotate(${rotate}deg)`;
stopSideEffect = (e) => e.stopPropagation();
getCurrentImage = (s,p) => {
if(!s.multi) return p.image ?? "";
return p.images[s.current]?.url ?? p.images?.[s.current] ?? "";
}
getCurrentTitle = (s,p) => {
if(!s.multi) return p.title ?? "";
return p.images?.[s.current]?.title ?? "";
}
resetZoom = () => this.setState({x:0, y:0, zoom:1});
shockZoom = e =>{
let {
zoomStep = DEFAULT_ZOOM_STEP,
allowZoom = true,
doubleClickZoom = DEFAULT_LARGE_ZOOM
} = this.props;
if(!allowZoom || !doubleClickZoom) return false;
this.stopSideEffect(e);
if(this.state.zoom > 1) return this.resetZoom();
const _z = ((zoomStep<1)?Math.ceil(doubleClickZoom / zoomStep):zoomStep) * zoomStep;
const _xy = getXY(e);
const _cbr = this._cont.current?.getBoundingClientRect?.();
const _ccx = _cbr.x + (_cbr.width / 2);
const _ccy = _cbr.y + (_cbr.height / 2);
const x = (_xy.x - _ccx) * -1 * _z;
const y = (_xy.y - _ccy) * -1 * _z;
this.setState({x, y, zoom: _z});
}
navigateImage = (direction, e) =>{
this.stopSideEffect(e);
let current = 0;
switch(direction){
case "next":
current = this.state.current + 1;
break;
case "prev":
current = this.state.current - 1;
break;
}
if(current >= this.props.images.length) current = 0;
else if (current < 0) current = this.props.images.length -1;
this.setState({current, x: 0, y: 0, zoom: 1, rotate: 0, loading: true});
if(typeof this.props.onNavigateImage === 'function') {
this.props.onNavigateImage(current)
}
}
startMove = (e) => {
if(this.state.zoom <= 1) return false;
this.setState({moving: true});
let xy = getXY(e);
this.initX = xy.x - this.lastX;
this.initY = xy.y - this.lastY;
}
duringMove = (e) => {
if(!this.state.moving) return false;
let xy = getXY(e);
this.lastX = xy.x - this.initX;
this.lastY = xy.y - this.initY;
this.setState({
x: xy.x - this.initX,
y: xy.y - this.initY
});
}
endMove = (e) => this.setState({moving: false});
applyZoom = (type) => {
let {zoomStep = DEFAULT_ZOOM_STEP} = this.props;
switch(type){
case "in":
this.setState({zoom: this.state.zoom + zoomStep});
break;
case "out":
let newZoom = this.state.zoom - zoomStep;
if(newZoom < 1) break;
else if(newZoom === 1) this.setState({x:0, y:0, zoom: 1});
else this.setState({zoom: newZoom});
break;
case "reset":
this.resetZoom();
break;
}
}
applyRotate = (type) => {
switch(type){
case "cw":
this.setState({rotate: this.state.rotate + 90});
break;
case "acw":
this.setState({rotate: this.state.rotate - 90});
break;
}
}
reset = e => {
this.stopSideEffect(e);
this.setState({x:0,y:0,zoom:1,rotate:0});
};
exit = (e) =>{
if(typeof this.props.onClose === "function") return this.props.onClose(e);
console.error("No Exit function passed on prop: onClose. Clicking the close button will do nothing");
}
shouldShowReset = () => (this.state.x || this.state.y || this.state.zoom !== 1 || this.state.rotate !== 0);
canvasClick = (e) => {
let {clickOutsideToExit = true} = this.props;
if(clickOutsideToExit && this.state.zoom <=1) return this.exit(e);
}
keyboardNavigation = e => {
let {allowZoom = true, allowReset = true} = this.props;
let {multi, x, y, zoom} = this.state;
switch(e.key){
case "ArrowLeft":
if(multi && zoom === 1) this.navigateImage("prev", e);
else if(zoom > 1) this.setState({x: x - 20});
break;
case "ArrowRight":
if(multi && zoom === 1) this.navigateImage("next", e);
else if(zoom > 1) this.setState({x: x + 20});
break;
case "ArrowUp":
if(zoom > 1) this.setState({y: y + 20});
break;
case "ArrowDown":
if(zoom > 1) this.setState({y: y - 20});
break;
case "+":
if(allowZoom) this.applyZoom("in");
break;
case "-":
if(allowZoom) this.applyZoom("out");
break;
case "Escape":
if(allowReset && this.shouldShowReset()) this.reset(e);
else this.exit(e);
break;
}
}
componentDidMount(){
document.body.classList.add("lb-open-lightbox");
let {keyboardInteraction = true} = this.props;
if(keyboardInteraction) document.addEventListener("keyup", this.keyboardNavigation);
}
componentWillUnmount(){
document.body.classList.remove("lb-open-lightbox");
let {keyboardInteraction = true} = this.props;
if(keyboardInteraction) document.removeEventListener("keyup", this.keyboardNavigation);
}
render(){
let image = this.getCurrentImage(this.state,this.props);
let title = this.getCurrentTitle(this.state,this.props);
if(!image){
console.warn("Not showing lightbox because no image(s) was supplied");
return null;
}
let {
allowZoom = true,
allowRotate = true,
buttonAlign = "flex-end",
showTitle = true,
allowReset = true
} = this.props;
let {x, y, zoom, rotate, multi, loading, moving} = this.state;
let _reset = allowReset && this.shouldShowReset();
return (
<div className="lb-container">
<div className="lb-header" style={{justifyContent: buttonAlign}}>
<Cond condition = {showTitle && title}>
<div className="lb-title"
style={{
display : buttonAlign === "center"?"none":"flex",
order : buttonAlign === "flex-start"?"2":"unset"
}}>
<span title={title} style= {{textAlign : buttonAlign === "flex-start"?"right":"left"}}>{title}</span>
</div>
</Cond>
<Cond condition={buttonAlign === "center" || _reset}>
<div title="Reset"
style={{order : buttonAlign === "flex-start"?"1":"unset"}}
className={`lb-button lb-icon-reset lb-hide-mobile reload ${_reset?"":"lb-disabled"}`}
onClick={this.reset}></div>
</Cond>
<Cond condition = {multi}>
<div title="Previous" className="lb-button lb-icon-arrow prev lb-hide-mobile" onClick={e=>this.navigateImage("prev", e)}></div>
<div title="Next" className="lb-button lb-icon-arrow next lb-hide-mobile" onClick={e=>this.navigateImage("next", e)}></div>
</Cond>
<Cond condition = {allowZoom}>
<div title="Zoom In" className="lb-button lb-icon-zoomin zoomin" onClick={()=>this.applyZoom("in")}></div>
<div title="Zoom Out"
className={`lb-button lb-icon-zoomout zoomout ${zoom<=1?"lb-disabled":""}`}
onClick={()=>this.applyZoom("out")}></div>
</Cond>
<Cond condition = {allowRotate}>
<div title="Rotate left" className="lb-button lb-icon-rotate rotatel" onClick={()=>this.applyRotate("acw")}></div>
<div title="Rotate right" className="lb-button lb-icon-rotate rotater" onClick={()=>this.applyRotate("cw")}></div>
</Cond>
<div title="Close" className="lb-button lb-icon-close close" style={{order: buttonAlign === "flex-start"?"-1":"unset"}} onClick={e=>this.exit(e)}></div>
</div>
<div
className={`lb-canvas${loading?" lb-loading":""}`}
ref={this._cont}
onClick={e=>this.canvasClick(e)}>
<img
draggable = "false"
style={{
transform : this.createTransform(x,y,zoom,rotate),
cursor : zoom>1?"grab":"unset",
transition : moving?"none":"all 0.1s"
}}
onMouseDown={e=>this.startMove(e)}
onTouchStart={e=>this.startMove(e)}
onMouseMove={e=>this.duringMove(e)}
onTouchMove={e=>this.duringMove(e)}
onMouseUp={e=>this.endMove(e)}
onMouseLeave={e=>this.endMove(e)}
onTouchEnd={e=>this.endMove(e)}
onClick={e=>this.stopSideEffect(e)}
onDoubleClick={e=>this.shockZoom(e)}
onLoad={e=>this.setState({loading: false})}
className={`lb-img${loading?" lb-loading":""}`}
title={title}
src={image} alt={title}/>
<div className="mobile-controls lb-show-mobile">
{multi?<div title="Previous" className="lb-button lb-icon-arrow prev" onClick={e=>this.navigateImage("prev", e)}></div>:null}
{_reset?<div title="Reset" className="lb-button lb-icon-reset reload" onClick={this.reset}></div>:null}
{multi?<div title="Next" className="lb-button lb-icon-arrow next" onClick={e=>this.navigateImage("next", e)}></div>:null}
</div>
</div>
</div>
)
}
}