5 images not suported yet
97 | }, 98 | }; 99 | 100 | 101 | 102 | 103 | export default Helper 104 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Helper from './helper.jsx'; 4 | 5 | const DEFAULT_WIDTH = 500; 6 | const DEFAULT_HEIGHT = 500; 7 | const MAX_IMAGES = 4; // currently supprt layout upto 4 images 8 | 9 | class ImageStory extends React.Component { 10 | 11 | constructor(props) { 12 | super(props) 13 | this.shouldPrepareImages = this.shouldPrepareImages.bind(this); 14 | this.prepareImages = this.prepareImages.bind(this); 15 | this.imagesPrepared = this.imagesPrepared.bind(this); 16 | this.getArrangedImages = this.getArrangedImages.bind(this); 17 | this.getStyles = this.getStyles.bind(this); 18 | } 19 | 20 | 21 | componentWillMount() { 22 | let { onLoad, images } = this.props; 23 | 24 | this.prepareImages(images, _ => { 25 | if (!this._isMounted) 26 | return 27 | this.forceUpdate(); 28 | onLoad && onLoad(); 29 | }); 30 | } 31 | 32 | 33 | componentDidMount() { 34 | this._isMounted = true; 35 | } 36 | 37 | 38 | componentWillReceiveProps(nextProps) { 39 | let { images = []} = this.props; 40 | let nextImages = nextProps.images; 41 | if (this.shouldPrepareImages(images, nextImages)) { 42 | this.prepareImages(nextImages, _ => { 43 | this._isMounted && this.forceUpdate() 44 | }); 45 | } 46 | } 47 | 48 | 49 | componentWillUnmount() { 50 | this._isMounted = false; 51 | } 52 | 53 | 54 | shouldPrepareImages(currentImages = [], nextImages = []) { 55 | if (currentImages.length !== nextImages.length) return true; 56 | let result = false; 57 | nextImages.forEach(img => { 58 | if (currentImages.indexOf(img) === -1) result = true; 59 | }) 60 | return result; 61 | } 62 | 63 | 64 | prepareImages(images, cb) { 65 | if (!images || !images.length) return; 66 | let oldImages = this.images || []; 67 | 68 | this.images = images.map((img, index) => { 69 | let oldImg = oldImages.find(i => i.src === img); 70 | return { 71 | src: img, 72 | loading: !oldImg && index < MAX_IMAGES, 73 | width: oldImg ? oldImg.width : null, 74 | height: oldImg ? oldImg.height : null, 75 | } 76 | }); 77 | 78 | 79 | this.images.forEach((img, index) => { 80 | if (!img.loading) return; 81 | let i = new Image(); 82 | i.onload = () => { 83 | this.images[index].width = i.width; 84 | this.images[index].height = i.height; 85 | this.images[index].loading = false; 86 | this.imagesPrepared() && cb(); 87 | } 88 | i.onerror = () => { 89 | this.images.splice(index, 1); 90 | this.imagesPrepared() && cb(); 91 | } 92 | i.src = img.src; 93 | }) 94 | } 95 | 96 | 97 | imagesPrepared() { 98 | if (!this.images || !this.images.length) return; 99 | return !this.images.some((img) => img.loading) 100 | } 101 | 102 | 103 | getArrangedImages() { 104 | if (!this.imagesPrepared()) return null; 105 | let result = null; 106 | let style = this.getStyles(); 107 | let images = this.images && this.images.filter(img => !img.loading); 108 | if (!images || !images.length) return null; 109 | let { onImageSelect } = this.props; 110 | style.img.cursor = onImageSelect ? 'pointer' : null; 111 | switch (images.length) { 112 | case 1: result = Helper.getOneImageLayout(images.slice(0, 1), style, onImageSelect); break; 113 | case 2: result = Helper.getTwoImageLayout(images.slice(0, 2), style, onImageSelect); break; 114 | case 3: result = Helper.getThreeImageLayout(images.slice(0, 3), style, onImageSelect); break; 115 | case 4: result = Helper.getFourImageLayout(images.slice(0, 4), style, null, onImageSelect); break; 116 | default: result = Helper.getFourImageLayout(images.slice(0, 4), style, images.slice(4), onImageSelect); break; 117 | } 118 | return result 119 | } 120 | 121 | 122 | getStyles() { 123 | let { width, height, rootStyle = {}} = this.props; 124 | 125 | let styles = { 126 | root: { 127 | backgroundColor: '#f5f5f5', 128 | ...rootStyle, 129 | width: width || DEFAULT_WIDTH, 130 | height: height || DEFAULT_HEIGHT, 131 | }, 132 | img: { 133 | boxSizing: 'border-box', 134 | border: 'solid 2px #fff', 135 | float: 'left', 136 | backgroundSize: 'cover', 137 | backgroundRepeat: 'no-repeat', 138 | backgroundPosition: 'center', 139 | }, 140 | more: { 141 | width: '100%', 142 | height: '100%', 143 | display: 'flex', 144 | color: '#fff', 145 | fontSize: '38px', 146 | alignItems: 'center', 147 | justifyContent: 'center', 148 | backgroundColor: 'rgba(0, 0, 0, .4)', 149 | }, 150 | }; 151 | 152 | return styles; 153 | } 154 | 155 | 156 | render() { 157 | return this.getArrangedImages() 158 | } 159 | } 160 | 161 | 162 | ImageStory.propTypes = { 163 | images: PropTypes.arrayOf(PropTypes.string), 164 | width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 165 | height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 166 | rootStyle: PropTypes.object, 167 | onImageSelect: PropTypes.func, 168 | onLoad: PropTypes.func, 169 | } 170 | 171 | 172 | export default ImageStory; 173 | -------------------------------------------------------------------------------- /src/layouts.js: -------------------------------------------------------------------------------- 1 | const Layouts = { 2 | 3 | 4 | _l1_1: {}, 5 | 6 | 7 | _l2_1: { 8 | getScore(imgs) { 9 | let img1Score = Layouts._getCroppedPixel(imgs[0].width, imgs[0].height, 1, 1/2) 10 | let img2Score = Layouts._getCroppedPixel(imgs[1].width, imgs[1].height, 1, 1/2) 11 | return img1Score + img2Score; 12 | }, 13 | getParams() { 14 | return [ 15 | {width: 1 * 100,height: 1 / 2 * 100}, 16 | {width: 1 * 100,height: 1 / 2 * 100}, 17 | ] 18 | }, 19 | }, 20 | 21 | 22 | _l2_2: { 23 | getScore(imgs) { 24 | let img1Score = Layouts._getCroppedPixel(imgs[0].width, imgs[0].height, 1/2, 1/2) 25 | let img2Score = Layouts._getCroppedPixel(imgs[1].width, imgs[1].height, 1/2, 1/2) 26 | return img1Score + img2Score; 27 | }, 28 | getParams() { 29 | return [ 30 | {width: 1 / 2 * 100,height: 1 * 100}, 31 | {width: 1 / 2 * 100,height: 1 * 100}, 32 | ] 33 | }, 34 | }, 35 | 36 | 37 | _l3_1: { 38 | getScore(imgs) { 39 | let img1Score = Layouts._getCroppedPixel(imgs[0].width, imgs[0].height, 1, 1/2) 40 | let img2Score = Layouts._getCroppedPixel(imgs[1].width, imgs[1].height, 1/2, 1/2) 41 | let img3Score = Layouts._getCroppedPixel(imgs[2].width, imgs[2].height, 1/2, 1/2) 42 | return img1Score + img2Score + img3Score; 43 | }, 44 | getParams() { 45 | return [ 46 | {width:1*100,height:1/2*100}, 47 | {width:1/2*100,height:1/2*100}, 48 | {width:1/2*100,height:1/2*100}, 49 | ] 50 | } 51 | }, 52 | 53 | 54 | _l3_2: { 55 | getScore(imgs) { 56 | let img1Score = Layouts._getCroppedPixel(imgs[0].width, imgs[0].height, 1, 2/3) 57 | let img2Score = Layouts._getCroppedPixel(imgs[1].width, imgs[1].height, 1/2, 1/3) 58 | let img3Score = Layouts._getCroppedPixel(imgs[2].width, imgs[2].height, 1/2, 1/3) 59 | return img1Score + img2Score + img3Score; 60 | }, 61 | getParams() { 62 | return [ 63 | {width:1*100,height:2/3*100}, 64 | {width:1/2*100,height:1/3*100}, 65 | {width:1/2*100,height:1/3*100}, 66 | ] 67 | } 68 | }, 69 | 70 | 71 | _l3_3: { 72 | getScore(imgs) { 73 | let img1Score = Layouts._getCroppedPixel(imgs[0].width, imgs[0].height, 1/2, 2/3) 74 | let img2Score = Layouts._getCroppedPixel(imgs[1].width, imgs[1].height, 1/2, 2/3) 75 | let img3Score = Layouts._getCroppedPixel(imgs[2].width, imgs[2].height, 1, 1/3) 76 | return img1Score + img2Score + img3Score; 77 | }, 78 | getParams() { 79 | return [ 80 | {width:1/2*100,height:2/3*100}, 81 | {width:1/2*100,height:2/3*100}, 82 | {width:1*100,height:1/3*100}, 83 | ] 84 | } 85 | }, 86 | 87 | _l4_1: { 88 | getScore(imgs) { 89 | let img1Score = Layouts._getCroppedPixel(imgs[0].width, imgs[0].height, 1/2, 1/2) 90 | let img2Score = Layouts._getCroppedPixel(imgs[1].width, imgs[1].height, 1/2, 1/2) 91 | let img3Score = Layouts._getCroppedPixel(imgs[2].width, imgs[2].height, 1/2, 1/2) 92 | let img4Score = Layouts._getCroppedPixel(imgs[3].width, imgs[3].height, 1/2, 1/2) 93 | return img1Score + img2Score + img3Score + img4Score; 94 | }, 95 | getParams() { 96 | return [ 97 | {width:1/2*100,height:1/2*100}, 98 | {width:1/2*100,height:1/2*100}, 99 | {width:1/2*100,height:1/2*100}, 100 | {width:1/2*100,height:1/2*100}, 101 | ] 102 | } 103 | }, 104 | _l4_2: { 105 | getScore(imgs) { 106 | let img1Score = Layouts._getCroppedPixel(imgs[0].width, imgs[0].height, 1, 2/3) 107 | let img2Score = Layouts._getCroppedPixel(imgs[1].width, imgs[1].height, 1/3, 1/3) 108 | let img3Score = Layouts._getCroppedPixel(imgs[2].width, imgs[2].height, 1/3, 1/3) 109 | let img4Score = Layouts._getCroppedPixel(imgs[3].width, imgs[3].height, 1/3, 1/3) 110 | return img1Score + img2Score + img3Score + img4Score; 111 | }, 112 | getParams() { 113 | return [ 114 | {width:1*100,height:2/3*100}, 115 | {width:1/3*100,height:1/3*100}, 116 | {width:1/3*100,height:1/3*100}, 117 | {width:1/3*100,height:1/3*100}, 118 | ] 119 | } 120 | }, 121 | 122 | _l4_3: { 123 | getScore(imgs) { 124 | let img1Score = Layouts._getCroppedPixel(imgs[0].width, imgs[0].height, 2/3, 1) 125 | let img2Score = Layouts._getCroppedPixel(imgs[1].width, imgs[1].height, 1/3, 1/3) 126 | let img3Score = Layouts._getCroppedPixel(imgs[2].width, imgs[2].height, 1/3, 1/3) 127 | let img4Score = Layouts._getCroppedPixel(imgs[3].width, imgs[3].height, 1/3, 1/3) 128 | return img1Score + img2Score + img3Score + img4Score; 129 | }, 130 | getParams() { 131 | return [ 132 | {width:2/3*100,height:1*100}, 133 | {width:1/3*100,height:1/3*100}, 134 | {width:1/3*100,height:1/3*100}, 135 | {width:1/3*100,height:1/3*100}, 136 | ] 137 | } 138 | }, 139 | 140 | _getCroppedPixel(imgWidth, imgHeight, cWidth, cHeight) { 141 | let rw = (imgWidth * (cHeight))/imgHeight; 142 | if(rw >= cWidth) return rw - cWidth 143 | return ((imgHeight * cWidth)/imgWidth) - cHeight; 144 | }, 145 | } 146 | 147 | 148 | export default Layouts 149 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var webpack = require('webpack'); 3 | 4 | 5 | var DIST_DIR = path.resolve(__dirname, 'dist'); 6 | var SRC_DIR = path.resolve(__dirname, 'src'); 7 | var DEMO_DIR = path.resolve(__dirname, 'demo'); 8 | 9 | var config = { 10 | entry: SRC_DIR + '/index.jsx', 11 | output: { 12 | path: DIST_DIR, 13 | filename: 'image-story.js', 14 | libraryTarget: 'commonjs2', 15 | }, 16 | module: { 17 | loaders: [ 18 | { 19 | test: /.jsx?$/, 20 | include: SRC_DIR, 21 | loader: 'babel', 22 | exclude: /node_modules/, 23 | }, 24 | ], 25 | }, 26 | externals: { 27 | react: 'react', 28 | 'react-dom': 'react-dom', 29 | }, 30 | }; 31 | 32 | demoConfig = { 33 | entry: DEMO_DIR + '/src/index.jsx', 34 | output: { 35 | path: DEMO_DIR, 36 | filename: 'index.js', 37 | }, 38 | module: { 39 | loaders: [ 40 | { 41 | test: /.jsx?$/, 42 | include: [ 43 | DEMO_DIR + '/src', 44 | SRC_DIR, 45 | ], 46 | loader: 'babel-loader', 47 | exclude: /node_modules/, 48 | }, 49 | ], 50 | }, 51 | plugins: [ 52 | new webpack.DefinePlugin({ 53 | 'process.env': { 54 | NODE_ENV: JSON.stringify('production'), 55 | }, 56 | }), 57 | new webpack.optimize.UglifyJsPlugin(), 58 | ], 59 | 60 | } 61 | 62 | module.exports = [config, demoConfig]; 63 | --------------------------------------------------------------------------------