UNPKG

react-image-slideshow

Version:

A simple image slideshow components with react.js

710 lines (658 loc) 29.6 kB
// Libs import React from 'react'; import TWEEN from 'tween.js'; import Portal from'react-portal'; // Style import styles from './app.css'; import jsStyles from './appStyle.js'; // Polyfill import polyfill from '../../utils/polyfill.js'; class Slideshow extends React.Component { constructor(props) { super(props); polyfill(); this.window = null; this.keyEvent = null; this.bodyAttr = null; this.resizeRatio = 0.90; this.onAnimate = false; this.tmpNowImage = null; this.minImageSize = null; this.originalSize = null; this.imageInZoom = false; this.imageZoomMargin = 100; this.imageMoveRange = null; this.state = { isOpened : false, nowImage: null, imageSize: null, imageSizeAnimate: null, imageMovePos: null, imageZoomQuit: null, showAction: false }; this.handleModalOpen = this.handleModalOpen.bind(this); this.handleBeforeModalOnOpen = this.handleBeforeModalOnOpen.bind(this); this.handleModalBeforeClose = this.handleModalBeforeClose.bind(this); this.handleModalClose = this.handleModalClose.bind(this); this.calAllImageSize = this.calAllImageSize.bind(this); this.getImageSize = this.getImageSize.bind(this); this.initImageSize = this.initImageSize.bind(this); this.calImageSize = this.calImageSize.bind(this); this.calSingleImageSize = this.calSingleImageSize.bind(this); this.calNowImageSize = this.calNowImageSize.bind(this); this.handleImageSliderToPreviousHover = this.handleImageSliderToPreviousHover.bind(this); this.handleImageSliderToNextHover = this.handleImageSliderToNextHover.bind(this); this.handleImageCloserHover = this.handleImageCloserHover.bind(this); this.handleImageKeySwitch = this.handleImageKeySwitch.bind(this); this.handleImageSliderToPrevious = this.handleImageSliderToPrevious.bind(this); this.handleImageSliderToNext = this.handleImageSliderToNext.bind(this); this.handleImageAnimate = this.handleImageAnimate.bind(this); this.getPrevIndex = this.getPrevIndex.bind(this); this.getNextIndex = this.getNextIndex.bind(this); this.handleImageZoom = this.handleImageZoom.bind(this); this.handleImageMove = this.handleImageMove.bind(this); this.handleImageOnComplete = this.handleImageOnComplete.bind(this); this.getWindow = this.getWindow.bind(this); this.preventSelect = this.preventSelect.bind(this); this.addEvent = this.addEvent.bind(this); this.enableBodyScroll = this.enableBodyScroll.bind(this); this.disableBodyScroll = this.disableBodyScroll.bind(this); this.listenKeyDown = this.listenKeyDown.bind(this); this.unListenKeyDown = this.unListenKeyDown.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); } componentWillMount() { this.window = this.getWindow(); } componentDidMount() { this.initImageSize(); this.addEvent(this.window, 'resize', this.calNowImageSize); } componentWillReceiveProps(nextProps) { if(nextProps.imgs.length !== this.props.imgs.length) { this.calNowImageSize(); } } shouldComponentUpdate(nextProps, nextState) { return true; } componentWillUpdate(nextProps, nextState) {} componentDidUpdate(prevProps, prevState) {} componentWillUnmount() {} // 模态框弹出 handleModalOpen(initImage) { this.onAnimate = true; if(initImage !== undefined && this.props.imgs !== undefined) { this.tmpNowImage = (initImage > this.props.imgs.length-1 ? this.props.imgs.length-1 : initImage<0 ? 0 : initImage); } else { this.tmpNowImage = 0; } this.setState({ nowImage: this.tmpNowImage, isOpened: true, showAction: false }); this.calSingleImageSize(this.tmpNowImage); this.disableBodyScroll(); this.listenKeyDown(); this.handleImageOnComplete(this.tmpNowImage); } // 模态框弹出执行钩子 handleBeforeModalOnOpen(nodeModal) { let self = this; let nodeNow = document.getElementById(`sliderShowImageOf${this.tmpNowImage}`); nodeNow.style.opacity = 1; new TWEEN.Tween({ opacity: 0 }) .to({ opacity: 1 }, 300) .easing(TWEEN.Easing.Cubic.In) .onUpdate(function() { nodeModal.style.opacity = this.opacity; }) .onComplete(function() { self.onAnimate = false; }) .start(); } // 模态框关闭 handleModalClose() { this.onAnimate = true; this.setState({ isOpened: false }); this.enableBodyScroll(); this.unListenKeyDown(); } // 模态框关闭执行钩子 handleModalBeforeClose(nodeModal, removeFromDom) { let self = this; new TWEEN.Tween({ opacity: 1 }) .to({ opacity: 0 }, 300) .easing(TWEEN.Easing.Cubic.In) .onUpdate(function() { nodeModal.style.opacity = this.opacity; }) .onComplete(function() { removeFromDom(); self.onAnimate = false; }) .start(); } // 计算图片缩放大小, 保持图片始终在视图内 // resizeRatio可以控制图片缩放比例 getImageSize(imageUrl) { let img = new Image(); img.src = imageUrl; return { width: img.width, height: img.height } } initImageSize() { let imgSizeData = new Array(this.props.imgs.length); imgSizeData.fill({ width : 0, height: 0 }); if(this.props.imgs) { this.setState({ imageSize: imgSizeData, imageSizeAnimate: { transition: '.3s height, .3s width' }, imageMovePos: { transformOrigin: `${this.imageZoomMargin}px ${this.imageZoomMargin}px 0px` } }); } } calImageSize(imageUrl, callBack) { let winWidth = this.window.innerWidth; let winHeight = this.window.innerHeight; let winWHRatio = winWidth / winHeight; let img = new Image(); img.onload = () => { let imgSizeData = null; let imgWHRatio = img.width / img.height; if (imgWHRatio > winWHRatio) { if (img.width >= winWidth) { imgSizeData = { width: winWidth * this.resizeRatio, height: winWidth * this.resizeRatio / imgWHRatio }; } else { if (img.width < winWidth * this.resizeRatio) { imgSizeData = { width: img.width, height: img.height }; } else { imgSizeData = { width: winWidth * this.resizeRatio, height: winWidth * this.resizeRatio / imgWHRatio }; } } } else { if (img.height >= winHeight) { imgSizeData = { height: winHeight * this.resizeRatio, width: winHeight * this.resizeRatio * imgWHRatio }; } else { if (img.height < winHeight * this.resizeRatio) { imgSizeData = { height: img.height, width: img.width }; } else { imgSizeData = { height: winHeight * this.resizeRatio, width: winHeight * this.resizeRatio * imgWHRatio }; } } } callBack(imgSizeData); }; img.src = imageUrl; } calAllImageSize() { let imgSizeData = JSON.parse(JSON.stringify(this.state.imageSize)); this.props.imgs ? this.props.imgs.map((imageData, urlIndex)=> { this.calImageSize(imageData.url, (size) => { imgSizeData[urlIndex] = size; this.setState({ imageSize: imgSizeData }); }); }) : null; } calSingleImageSize(urlIndex) { if(this.state.imageSize && this.props.imgs) { let imgSizeData = JSON.parse(JSON.stringify(this.state.imageSize)); this.calImageSize(this.props.imgs[urlIndex].url, (size) => { imgSizeData[urlIndex] = size; this.setState({ imageSize: imgSizeData }, () => { setTimeout(()=>{ this.setState({ imageSizeAnimate: { transition: '.3s height, .3s width' }, }) }, 500)} ); }); } else { console.warn('imageUrl数据为空, 无法计算图片尺寸'); } } calNowImageSize() { if(this.state.nowImage) { this.calSingleImageSize(this.state.nowImage); } } // 图片切换按钮动画 handleImageSliderToPreviousHover() { if(!this.onAnimate) { document.getElementById(`toPrevButton`).style.left = '40px'; document.getElementById(`toNextButton`).style.right = '30px'; } } handleImageSliderToNextHover() { if(!this.onAnimate) { document.getElementById(`toPrevButton`).style.left = '30px'; document.getElementById(`toNextButton`).style.right = '40px'; } } handleImageCloserHover() { if(!this.onAnimate) { document.getElementById(`toPrevButton`).style.left = '30px'; document.getElementById(`toNextButton`).style.right = '30px'; } } // 图片切换控制 handleImageKeySwitch(key) { if(key == 37 || key == 38) { this.handleImageSliderToPrevious(); } else if(key == 39 || key == 40) { this.handleImageSliderToNext(); } } handleImageSliderToPrevious() { if(!this.onAnimate) { this.onAnimate = true; let prevNodeIndex = this.getPrevIndex(); this.setState({ nowImage: prevNodeIndex, imageSizeAnimate: { transition: 'initial' }, showAction: false }); this.calSingleImageSize(prevNodeIndex); this.handleImageOnComplete(prevNodeIndex); let nodeNow = document.getElementById(`sliderShowImageOf${this.state.nowImage}`); let nodePrevious = document.getElementById(`sliderShowImageOf${prevNodeIndex}`); this.handleImageAnimate(nodeNow, nodePrevious); } } handleImageSliderToNext() { if(!this.onAnimate) { this.onAnimate = true; let nextNodeIndex = this.getNextIndex(); this.setState({ nowImage: nextNodeIndex, imageSizeAnimate: { transition: 'initial' }, showAction: false }); this.calSingleImageSize(nextNodeIndex); this.handleImageOnComplete(nextNodeIndex); let nodeNow = document.getElementById(`sliderShowImageOf${this.state.nowImage}`); let nodeNext = document.getElementById(`sliderShowImageOf${nextNodeIndex}`); this.handleImageAnimate(nodeNow, nodeNext); } } handleImageAnimate(outNode, inNode) { let self = this; let outComplete = false; let inComplete = false; new TWEEN.Tween({ opacity: 1 }) .to({ opacity: 0 }, 300) .easing(TWEEN.Easing.Cubic.In) .onUpdate(function() { if(outNode) outNode.style.opacity = this.opacity; }) .onComplete(function() { outComplete = true; if(outComplete && inComplete) { self.onAnimate = false; } }) .start(); new TWEEN.Tween({ opacity: 0 }) .to({ opacity: 1 }, 300) .easing(TWEEN.Easing.Cubic.In) .onUpdate(function() { if(inNode) inNode.style.opacity = this.opacity; }) .onComplete(function() { inComplete = true; if(outComplete && inComplete) { self.onAnimate = false; } }) .start(); } getPrevIndex(index) { let nowIndex = null; if (index) nowIndex = index; else nowIndex = this.state.nowImage; try { if (nowIndex > 0) return nowIndex - 1; else return this.props.imgs.length - 1; } catch (err) { console.warn(err); return 0; } } getNextIndex(index) { let nowIndex = null; if (index) nowIndex = index; else nowIndex = this.state.nowImage; try { if (nowIndex < this.props.imgs.length - 1) return nowIndex + 1; else return 0; } catch (err) { console.warn(err); return 0; } } // 缩放 handleImageZoom(imageIndex) { if(!this.imageInZoom) { this.tmpNowImage = imageIndex; this.minImageSize = this.state.imageSize[this.tmpNowImage]; this.originalSize = this.getImageSize(this.props.imgs[this.tmpNowImage].url); this.imageMoveRange = { x: (this.originalSize.width - this.window.innerWidth) + (2 * this.imageZoomMargin), y: (this.originalSize.height - this.window.innerHeight) + (2 * this.imageZoomMargin) }; let imgSizeData = JSON.parse(JSON.stringify(this.state.imageSize)); imgSizeData[this.tmpNowImage] = this.originalSize; this.addEvent(this.window, 'mousemove', this.handleImageMove); this.removeEvent(this.window, 'resize', this.calNowImageSize); this.setState({ imageSize: imgSizeData, imageZoomQuit: { zIndex: 200 }, showAction: false }); this.imageInZoom = true; } else { let imgSizeData = JSON.parse(JSON.stringify(this.state.imageSize)); imgSizeData[this.tmpNowImage] = this.minImageSize; this.removeEvent(this.window, 'mousemove', this.handleImageMove); this.addEvent(this.window, 'resize', this.calNowImageSize); this.setState({ imageMovePos: null, imageSize: imgSizeData, imageZoomQuit: { zIndex: -100 }, showAction: true }); this.imageInZoom = false; } } handleImageMove(e) { let imgPosX,imgPosY = null; if(this.originalSize.width > this.window.innerWidth) { imgPosX = this.imageZoomMargin - (this.imageMoveRange.x*(e.clientX/this.window.innerWidth)); } else { imgPosX = 0; } if(this.originalSize.height > this.window.innerHeight) { imgPosY = this.imageZoomMargin - (this.imageMoveRange.y*(e.clientY/this.window.innerHeight)) + this.originalSize.height/2 } else { imgPosY = (this.window.innerHeight - this.originalSize.height)/2 + this.originalSize.height/2; } this.setState({ imageMovePos: { transform: `translate(${imgPosX}px, ${imgPosY}px)` } }); } // 当前图片加载完成执行 handleImageOnComplete(nowImage) { clearInterval(this.checkNowImageLoaded); let img = new Image(); img.src = this.props.imgs[nowImage ? nowImage : this.state.nowImage ? this.state.nowImage : this.tmpNowImage].url; this.checkNowImageLoaded = setInterval(() => { if(img.complete) { this.setState({ showAction: true }); clearInterval(this.checkNowImageLoaded); } }, 250); } // 获取窗口 getWindow() { if(window.top) { return window.top } else { return window } } // 禁止选中 preventSelect() { return false } // 事件绑定 addEvent(object, type, callback) { if (object == null || typeof(object) == 'undefined') return; if (object.addEventListener) { object.addEventListener(type, callback, false); } else if (object.attachEvent) { object.attachEvent("on" + type, callback); } else { object["on"+type] = callback; } }; // 事件移除绑定 removeEvent(object, type, callback) { if (object == null || typeof(object) == 'undefined') return; if (object.removeEventListener) { object.removeEventListener(type, callback, false); } else if (object.detachEvent) { object.detachEvent("on" + type, callback); } else { object["on"+type] = callback; } } // 启用Body滚动 enableBodyScroll() { if(this.bodyAttr) { document.getElementsByTagName('body')[0].style.position = this.bodyAttr; this.bodyAttr = null; } else { document.getElementsByTagName('body')[0].style.position = ''; } } // 关闭Body滚动 disableBodyScroll() { if(!this.bodyAttr) { this.bodyAttr = document.getElementsByTagName('body')[0].style.position; document.getElementsByTagName('body')[0].style.position = 'fixed'; } else { document.getElementsByTagName('body')[0].style.position = 'fixed'; } } // 监听键盘事件 listenKeyDown() { if(!this.keyEvent) { this.keyEvent = document.onkeydown; document.onkeydown = this.handleKeyDown; } else { document.onkeydown = this.handleKeyDown; } } unListenKeyDown() { if(this.keyEvent) { document.onkeydown = this.keyEvent; this.keyEvent = null; } else { document.onkeydown = null; } } handleKeyDown(e){ let ie = false; if(document.all) ie = true; var key; if (ie) { key = event.keyCode; } else { key = e.keyCode; } this.handleImageKeySwitch(key); } render() { //Tween function animate(time) { requestAnimationFrame(animate); TWEEN.update(time); } requestAnimationFrame(animate); return ( <Portal className={styles.portalStyle} isOpened={this.state.isOpened} closeOnEsc={true} closeOnOutsideClick={false} onOpen={this.handleBeforeModalOnOpen} beforeClose={this.handleModalBeforeClose} > <div className={styles.slider}> <div className={styles.screenOverlay} onClickCapture={() => this.handleModalClose()}></div> <div className={styles.sliderZoomQuit} onClickCapture={() => this.handleImageZoom()} style={this.state.imageZoomQuit} > </div> { this.props.switchButton ? <div className={styles.toPreviousButton} onClickCapture={() => this.handleImageSliderToPrevious()} id="toPrevButton"> <div className={styles.switchButtonLayer}></div> <svg className={styles.switchButtonIcons} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path d="M15.41 16.09l-4.58-4.59 4.58-4.59L14 5.5l-6 6 6 6z"/> <path d="M0-.5h24v24H0z" fill="none"/> </svg> </div> : null } <div className={styles.sliderImageListWrapper}> { (this.props.imgs && this.state.imageSize) ? this.props.imgs.map((imageData,index)=> { let imageWrapperStyle = (index == this.state.nowImage ? jsStyles.imageWrapperShowStyle : jsStyles.imageWrapperHideStyle); let imageMovePos = (index == this.state.nowImage ? this.state.imageMovePos : null); let imageUrl = this.props.lazyLoad ? (index == this.state.nowImage ? imageData.url : (this.state.imageSize[index].width == 0 ? '' : imageData.url)) : imageData.url; let actionStyle = { top: this.window.innerHeight / 2 - this.state.imageSize[index].height / 2, right: this.window.innerWidth / 2 - this.state.imageSize[index].width / 2 - 48, display: (this.state.showAction ? 'block' : 'none') }; let loadingStyle = { display: (this.props.loading ? 'block' : 'none') }; return ( <div className={styles.sliderImageWrapper} key={index} style={imageWrapperStyle}> <div className={styles.sliderCloser} onClickCapture={() => this.handleModalClose()} onMouseOver={this.handleImageCloserHover} /> <div className={styles.sliderImageContainer} id={`sliderShowImageOf${index}`} style={imageMovePos}> <div className={styles.imageLoading} style={loadingStyle}> <svg className={styles.circleLoading} width="65px" height="65px" viewBox="0 0 66 66" xmlns="http://www.w3.org/2000/svg"><circle className={styles.circleLoadingPath} fill="none" strokeWidth="3" strokeLinecap="round" cx="33" cy="33" r="30"></circle></svg> </div> <img src={imageUrl} className={styles.sliderImage} style={Object.assign({}, this.state.imageSize[index], this.state.imageSizeAnimate)} onSelect={this.preventSelect} /> <div className={styles.imageAction} style={actionStyle} > { this.props.zoomButton ? <a className={styles.imageZoom} onClickCapture={() => this.handleImageZoom(index)}> <svg className={styles.actionButtonIcons} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/><path d="M0 0h24v24H0V0z" fill="none"/><path d="M12 10h-2v2H9v-2H7V9h2V7h1v2h2v1z"/></svg> </a> : null } { this.props.downloadButton ? <a className={styles.imageDownLoad} href={imageData.url} download={'保存图片'}> <svg className={styles.actionButtonIcons} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/><path d="M0 0h24v24H0z" fill="none"/></svg> </a> : null } </div> </div> <div className={styles.imageSwitch} style={this.state.imageSize[index]}> <div className={styles.switchOverlay} onMouseOver={this.handleImageCloserHover} /> <div className={styles.leftSwitch} onClickCapture={() => this.handleImageSliderToPrevious()} onMouseOver={this.handleImageSliderToPreviousHover} /> <div className={styles.rightSwitch} onClickCapture={() => this.handleImageSliderToNext()} onMouseOver={this.handleImageSliderToNextHover} /> </div> </div> ); }) : null } { this.props.indicator ? <div className={styles.imageIndicatorWrapper}> <div className={styles.imageIndicator}> <span className={styles.imageIndicatorLabel} onSelect={this.preventSelect}> {this.state.nowImage + 1} / {this.props.imgs.length} </span> </div> </div> : null } </div> { this.props.switchButton ? <div className={styles.toNextButton} onClickCapture={() => this.handleImageSliderToNext()} id="toNextButton"> <div className={styles.switchButtonLayer}></div> <svg className={styles.switchButtonIcons} viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z"/><path d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z"/><path d="M0-.25h24v24H0z" fill="none"/></svg> </div> : null } </div> </Portal> ); } } Slideshow.defaultProps = { lazyLoad: true, infinitySwitch: true, switchButton: true, downloadButton: true, loading: true, zoomButton: true, indicator: true, imgs: [] }; Slideshow.propTypes = { imgs: React.PropTypes.array.isRequired }; export default Slideshow; // TODO // 图片放大/Done // 图片下载/Done // Loading动画/Done // 带文字 // 懒加载/Done // 缩略图 // 更多动画 // BUGS // 修正按钮位移state问题/Done // 修正IE下的问题 // 修正放大/下载按钮图片未加载前就显示的问题/Done // Firefox下放大偏移/待测试