├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── examples ├── index.css ├── index.html └── index.js ├── package.json ├── server.js ├── src ├── Arrows.js ├── Dots.js ├── Slider.js ├── Slides.js ├── index.js └── util.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0, 3 | "plugins": [ 4 | "react-transform" 5 | ], 6 | "extra": { 7 | "react-transform": [{ 8 | "target": "react-transform-hmr", 9 | "imports": ["react"], 10 | "locals": ["module"] 11 | }, { 12 | "target": "react-transform-catch-errors", 13 | "imports": ["react", "redbox-react"] 14 | }] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "jsx": true, 4 | "modules": true 5 | }, 6 | "env": { 7 | "browser": true, 8 | "node": true 9 | }, 10 | "extends": "airbnb", 11 | "parser": "babel-eslint", 12 | "rules": { 13 | "quotes": [2, "single"], 14 | "strict": [2, "never"], 15 | "comma-dangle": [2, "never"], 16 | "no-undef": 2, 17 | "func-names": 0, 18 | 19 | "react/jsx-uses-react": 2, 20 | "react/jsx-uses-vars": 2, 21 | "react/react-in-jsx-scope": 2, 22 | "react/no-multi-comp": 0 23 | }, 24 | "plugins": [ 25 | "react" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .DS_Store 4 | lib 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | examples 3 | node_modules 4 | .babelrc 5 | .dev.babelrc 6 | .eslintrc 7 | .eslintignore 8 | index.html 9 | .gitignore 10 | server.js 11 | webpack.config.js 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ramana Venkata 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # THIS PROJECT IS NOT MAINTAINED! 2 | 3 | React Flex Slick 4 | ================ 5 | 6 | This is library aims to replace [react-slick][react-slick]. 7 | 8 | Uses **flexbox** exclusively, so no support for older browsers :cry: :cry: 9 | 10 | On the bright side, it is extremly flexible. Currently it supports `>=react-0.14` including rc's 11 | 12 | Any questions?? Join here [![react-flex-slick on slack](https://img.shields.io/badge/slack-react--flex--slick%40reactiflux-61DAFB.svg?style=flat-square)](http://www.reactiflux.com) 13 | 14 | ## Examples 15 | 16 | To run the examples: 17 | 18 | 1. `git clone http://github.com/vramana/react-flex-slick` 19 | 2. `cd react-flex-slick && npm i` 20 | 3. `npm start` 21 | 22 | ## What works 23 | 24 | - Infinite and Non-infinite Mode 25 | - Pages 26 | - Horizontal and Vertical Sliding 27 | - Multiple Slides per page (slidesToScroll = slidesToShow) 28 | - Left and Right Arrows to go back and forth 29 | - Arrows have active and inactive classes. 30 | - Custom Arrows 31 | - **Touch Scrolling/Mouse Dragging** 32 | - Edge Friction 33 | 34 | ## TODO 35 | 36 | - Feature Parity with [Slick Carousel][slick] 37 | - Examples 38 | - Documentation 39 | - Tests 40 | 41 | ## Comparision with Slick 42 | 43 | ### Settings 44 | 45 | - cssEase - renamed to transitionTimingFn 46 | - speed - renamed to transitionSpeed 47 | - easing - Not supported: Minimum browser support ensure CSS Transistions are present. 48 | - arrows - Alternate way: Instead of arrows pass empty `
` to the Slider 49 | - appendArrows, appendDots - Not supported due the architecture of component 50 | - mobileFirst - Alternate way: Control the size of Slider just usign css on the parent class 51 | - prevArrow, nextArrow - Alternate way: Just put a ref on prevArrow and nextArrow 52 | - infinite - works as expected 53 | - initialSlide - works as expected 54 | - rows - Alternate way: Pass pages of slides instead of slides 55 | - slidesPerRow - Alternate Way: Pass pages of slides instead of slides 56 | - vertical - works as expected 57 | - swipe, verticalSwiping - merged into swipe - works as expected 58 | - touchMove - works as expected 59 | - draggable - works as expected 60 | - edgeFriction - works as expected 61 | - touchThreshold - fraction by which you should slide for slide to change - lies between 0 and 1 62 | - autoPlay, autoPlaySpeed - works as expected. *Bonus*: If the mounted component with `autoPlay` recieve 63 | `autoPlay={false}` then it will pause the slider. 64 | - zIndex - Not supported: No support for IE 9 itself requires atleast IE11 65 | - centerPadding - Alternate way: Use css on pages to manipulate this 66 | - customPaging - Alternate way: Just pass your slides in pages whatever way you want 67 | - waitForAnimate - This is the default behaviour of current slider. Otherwise behaviour 68 | is not implemented yet. 69 | - useCSS - Not supported: We don't have to fallback for jQuery animations for ancient browsers 70 | because we dont support them in first place. 71 | - dots, dotClass - works almost as expected. Instead of dotClass on slider give it as 72 | className in the `Dots` component 73 | 74 | **Remaining**: slidesToShow, slidesToScroll, accessibility, rtl, pauseOnHover, 75 | pauseOnDotsHover, responsive, swipeToSlide, slide, variableWidth, centerMode, 76 | fade, lazyLoad, respondTo 77 | 78 | **Progress** - Total: 43 Current: 29 79 | 80 | ### Events 81 | 82 | - beforeChange - `beforeChange(prevSlide, currSlide)` but doesn't have the event handler 83 | - afterChange - `afterChange(prevSlide, currSlide)` but doesn't have the event handler 84 | - swipeEvent - `swipeEvent(direction)` but doesn't have the event handler 85 | - edgeEvent - `edgeEvent(direction)` but doesn't have the event handler 86 | - init, reInit - Alternate way: Can be invoke in parent components lifecycle methods. 87 | - destroy - Alternate way: Can be invoke in parent components lifecycle methods when the slider is taken out of the render tree. 88 | - setPosition - Not positions are calculated from the DOM. So, doesn't make sense. 89 | - breakpoint - Not implemented yet 90 | 91 | **Progress** - Total: 9 Current: 8 92 | 93 | ### Methods 94 | 95 | No slick method will be supported because they encourage anti-patterns in react i.e, changing 96 | the state of child component via a parent component directly or indirectly using setState. 97 | You can add all this by passing props to the Slider component. Detailed examples will be written 98 | showing how. 99 | 100 | ## Inspiration 101 | 102 | - [Slick Carousel][slick] 103 | - [React Slick][react-slick] 104 | - [React SoundPlayer][react-soundplayer] 105 | - [Smart and Dumb Componenst][smart-dumb] 106 | 107 | ## License 108 | 109 | MIT 110 | 111 | [react-slick]: https://github.com/akiran/react-slick 112 | [react-soundplayer]: https://github.com/soundblogs/react-soundplayer 113 | [smart-dumb]: https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0 114 | [slick]: https://github.com/kenwheeler/slick 115 | -------------------------------------------------------------------------------- /examples/index.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | .banner { 7 | height: 5rem; 8 | display: flex; 9 | justify-content: center; 10 | align-items: center; 11 | font-size: 2rem; 12 | background-color: #1282DE; 13 | color: white; 14 | } 15 | 16 | h2 { 17 | color: #333; 18 | } 19 | 20 | 21 | .non-infinite-left--active { 22 | width: 0; 23 | height: 0; 24 | border-bottom: solid 30px transparent; 25 | border-top: solid 30px transparent; 26 | border-right: solid 40px #795548; 27 | } 28 | 29 | .non-infinite-left--inactive { 30 | width: 0; 31 | height: 0; 32 | border-bottom: solid 30px transparent; 33 | border-top: solid 30px transparent; 34 | border-right: solid 40px rgba(121, 85, 72, 0.51); 35 | } 36 | 37 | .non-infinite-right--active { 38 | width: 0; 39 | height: 0; 40 | border-bottom: solid 30px transparent; 41 | border-top: solid 30px transparent; 42 | border-left: solid 40px #795548; 43 | } 44 | 45 | .non-infinite-right--inactive { 46 | width: 0; 47 | height: 0; 48 | border-bottom: solid 30px transparent; 49 | border-top: solid 30px transparent; 50 | border-left: solid 40px rgba(121, 85, 72, 0.51); 51 | } 52 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | React Flex Slick 4 | 5 | 6 | 7 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Slider, Slides, PrevArrow, NextArrow, Dots } from '../src'; 4 | 5 | import './index.css'; 6 | 7 | class App extends Component { 8 | 9 | render() { 10 | return ( 11 |
12 |
13 |

Non Infinite

14 | 15 |
16 |
17 |

Infinite

18 | 19 |
20 |
21 |

Custom arrows

22 | 23 |
24 |
25 |

Control Play (will autoPlay after 5s)

26 | 27 |
28 |
29 |

Set slide to show (will show the 6th slide after 5s)

30 | 31 |
32 |
33 | ); 34 | } 35 | } 36 | 37 | const slideStyle = { 38 | width: 540, 39 | height: 125, 40 | backgroundColor: 'slateblue', 41 | color: 'white', 42 | display: 'flex', 43 | justifyContent: 'center', 44 | alignItems: 'center' 45 | }; 46 | 47 | class NonInfinite extends Component { 48 | 49 | render() { 50 | return ( 51 | 52 | 54 | 55 |

1

56 |

2

57 |

3

58 |

4

59 |

5

60 |

6

61 |
62 | 64 | 65 |
66 | ); 67 | } 68 | } 69 | 70 | class Infinite extends Component { 71 | 72 | render() { 73 | return ( 74 | 75 | 76 | 77 |

1

78 |

2

79 |

3

80 |

4

81 |

5

82 |

6

83 |
84 | 85 | 86 |
87 | ); 88 | } 89 | } 90 | 91 | class CustomArrows extends Component { 92 | 93 | render() { 94 | return ( 95 | 96 | 97 | 98 |

1

99 |

2

100 |

3

101 |

4

102 |

5

103 |

6

104 |
105 | 106 |
107 | ); 108 | } 109 | } 110 | 111 | class ControlPlay extends Component { 112 | 113 | constructor(props) { 114 | super(props); 115 | this.state = { playing: false }; 116 | } 117 | 118 | componentDidMount() { 119 | this.timeout = setTimeout(() => { 120 | this.setState({ playing: true }); 121 | }, 5000); 122 | } 123 | 124 | componentWillUnmount() { 125 | clearTimeout(this.timeout); 126 | } 127 | 128 | render() { 129 | return ( 130 | 131 | 132 | 133 |

1

134 |

2

135 |

3

136 |

4

137 |

5

138 |

6

139 |
140 | 141 |
142 | ); 143 | } 144 | } 145 | 146 | class SetSlide extends Component { 147 | 148 | constructor(props) { 149 | super(props); 150 | this.state = { currentSlide: 0 }; 151 | } 152 | 153 | componentDidMount() { 154 | this.timeout = setTimeout(() => { 155 | this.setState({ currentSlide: 5 }); 156 | }, 5000); 157 | } 158 | 159 | componentWillUnmount() { 160 | clearTimeout(this.timeout); 161 | } 162 | 163 | render() { 164 | return ( 165 | 166 | 167 | 168 |

1

169 |

2

170 |

3

171 |

4

172 |

5

173 |

6

174 |
175 | 176 |
177 | ); 178 | } 179 | } 180 | 181 | export default App; 182 | 183 | ReactDOM.render(, document.getElementById('root')); 184 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-flex-slick2", 3 | "version": "0.5.0", 4 | "description": "Slick carousel using Flexbox", 5 | "main": "./lib/index.js", 6 | "scripts": { 7 | "start": "node server.js", 8 | "lint": "eslint .", 9 | "prepublish": "rm .babelrc && babel src --out-dir lib --stage 0 -L all && git checkout -- ." 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/vramana/react-flex-slick.git" 14 | }, 15 | "keywords": [ 16 | "react", 17 | "reactjs", 18 | "component", 19 | "slick", 20 | "carousel", 21 | "react-slick", 22 | "react-flex-slick", 23 | "react-component" 24 | ], 25 | "author": "Ramana Venkata (http://github.com/vramana)", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "http://github.com/vramana/react-flex-slick/issues" 29 | }, 30 | "homepage": "http://github.com/vramana/react-flex-slick", 31 | "devDependencies": { 32 | "babel": "^5.8.23", 33 | "babel-core": "^5.8.25", 34 | "babel-eslint": "^4.1.3", 35 | "babel-loader": "^5.3.2", 36 | "babel-plugin-react-transform": "^1.0.5", 37 | "css-loader": "^0.18.0", 38 | "eslint": "^1.5.1", 39 | "eslint-config-airbnb": "0.0.8", 40 | "eslint-plugin-react": "^3.4.2", 41 | "express": "^4.13.3", 42 | "react": "^0.14.0-rc1", 43 | "react-dom": "^0.14.0-rc1", 44 | "react-transform-catch-errors": "^1.0.0", 45 | "react-transform-hmr": "^1.0.0", 46 | "redbox-react": "^1.1.0", 47 | "style-loader": "^0.12.4", 48 | "webpack": "^1.12.2", 49 | "webpack-dev-middleware": "^1.2.0", 50 | "webpack-hot-middleware": "^2.2.0" 51 | }, 52 | "peerDependencies": { 53 | "react": ">=0.14 || ^0.14.0-alpha" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const express = require('express'); 3 | const webpack = require('webpack'); 4 | const config = require('./webpack.config'); 5 | 6 | const app = express(); 7 | const compiler = webpack(config); 8 | 9 | app.use(require('webpack-dev-middleware')(compiler, { 10 | stats: { 11 | colors: true 12 | }, 13 | publicPath: config.output.publicPath 14 | })); 15 | 16 | app.use(require('webpack-hot-middleware')(compiler)); 17 | 18 | app.get('*', function(req, res) { 19 | res.sendFile(path.join(__dirname, 'examples/index.html')); 20 | }); 21 | 22 | app.listen(3000, 'localhost'); 23 | -------------------------------------------------------------------------------- /src/Arrows.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | class PrevArrow extends Component { 4 | static propTypes = { 5 | handleClick: PropTypes.func, 6 | currentSlide: PropTypes.number, 7 | activeClassName: PropTypes.string, 8 | inactiveClassName: PropTypes.string, 9 | infinite: PropTypes.bool 10 | } 11 | 12 | static defaultProps = { 13 | activeClassName: '', 14 | inactiveClassName: '' 15 | } 16 | 17 | render() { 18 | const { activeClassName, inactiveClassName, currentSlide, infinite } = this.props; 19 | 20 | const className = currentSlide === 0 && infinite === false ? inactiveClassName : activeClassName; 21 | const style = className !== '' ? null : { 22 | width: 0, 23 | height: 0, 24 | borderBottom: 'solid 30px transparent', 25 | borderTop: 'solid 30px transparent', 26 | borderRight: 'solid 40px #795548' 27 | }; 28 | 29 | return ( 30 |
31 |
32 | ); 33 | } 34 | } 35 | 36 | class NextArrow extends Component { 37 | static propTypes = { 38 | handleClick: PropTypes.func, 39 | currentSlide: PropTypes.number, 40 | activeClassName: PropTypes.string, 41 | inactiveClassName: PropTypes.string, 42 | infinite: PropTypes.bool, 43 | slideCount: PropTypes.number 44 | } 45 | 46 | static defaultProps = { 47 | activeClassName: '', 48 | inactiveClassName: '' 49 | } 50 | 51 | render() { 52 | const { activeClassName, inactiveClassName, currentSlide } = this.props; 53 | const { infinite, slideCount } = this.props; 54 | 55 | const className = (currentSlide + 1) === slideCount && infinite === false ? inactiveClassName : activeClassName; 56 | const style = className !== '' ? null : { 57 | width: 0, 58 | height: 0, 59 | borderBottom: 'solid 30px transparent', 60 | borderTop: 'solid 30px transparent', 61 | borderLeft: 'solid 40px #795548' 62 | }; 63 | 64 | return ( 65 |
66 | ); 67 | } 68 | } 69 | 70 | export { PrevArrow, NextArrow }; 71 | -------------------------------------------------------------------------------- /src/Dots.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | const range = n => [...Array(n)].map((_, i) => i); 4 | 5 | class Dots extends Component { 6 | static propTypes = { 7 | className: PropTypes.string, 8 | currentSlide: PropTypes.number, 9 | slideCount: PropTypes.number, 10 | handleSlideShift: PropTypes.func 11 | } 12 | 13 | render() { 14 | const { className, slideCount, currentSlide } = this.props; 15 | const dotStyle = { display: 'flex', justifyContent: 'center'}; 16 | 17 | const dots = range(slideCount).map((x, i) => { 18 | const isActive = i === currentSlide; 19 | const style = { 20 | width: 8, 21 | height: 8, 22 | borderRadius: 8, 23 | backgroundColor: 'black', 24 | opacity: isActive ? 1 : 0.2, 25 | margin: 3 26 | }; 27 | 28 | return ( 29 |
30 | ); 31 | }); 32 | 33 | return ( 34 |
35 | {dots} 36 |
37 | ); 38 | } 39 | } 40 | 41 | export default Dots; 42 | -------------------------------------------------------------------------------- /src/Slider.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes, cloneElement, Children } from 'react'; 2 | import { swipeDirection, swipeDistance } from './util.js'; 3 | import { SWIPE_UP, SWIPE_DOWN, SWIPE_RIGHT, SWIPE_LEFT } from './util.js'; 4 | 5 | class Slider extends Component { 6 | static propTypes = { 7 | children: PropTypes.any, 8 | initialSlide: PropTypes.number, 9 | currentSlide: PropTypes.number, 10 | infinite: PropTypes.bool, 11 | vertical: PropTypes.bool, 12 | transitionSpeed: PropTypes.number, 13 | transitionTimingFn: PropTypes.string, 14 | beforeChange: PropTypes.func, 15 | afterChange: PropTypes.func, 16 | swipe: PropTypes.bool, 17 | draggable: PropTypes.bool, 18 | edgeFriction: PropTypes.number, 19 | touchThreshold: PropTypes.number, 20 | edgeEvent: PropTypes.func, 21 | swipeEvent: PropTypes.func, 22 | touchMove: PropTypes.bool, 23 | autoPlay: PropTypes.bool, 24 | autoPlaySpeed: PropTypes.number 25 | } 26 | // May be move most of the props from here to Slider. and copy them to state while 27 | // componentWillMount 28 | 29 | static defaultProps = { 30 | initialSlide: 0, 31 | vertical: false, 32 | transitionSpeed: 500, 33 | transitionTimingFn: 'ease', 34 | swipe: false, 35 | draggable: false, 36 | infinite: false, 37 | edgeFriction: 0.35, 38 | touchThreshold: 0.2, 39 | touchMove: true, 40 | autoPlay: false, 41 | autoPlaySpeed: 3000 42 | } 43 | 44 | /** 45 | * @constructor 46 | */ 47 | constructor(props, context) { 48 | super(props, context); 49 | const { currentSlide, initialSlide } = props; 50 | this.state = { 51 | currentSlide: currentSlide !== undefined ? currentSlide : initialSlide, 52 | animating: false, 53 | translateXOffset: 0, 54 | translateYOffset: 0 55 | }; 56 | } 57 | 58 | componentDidMount() { 59 | if (this.props.autoPlay && this.props.infinite) { 60 | this.autoPlayTransistionCallback = 61 | setInterval(() => { this.handleSlideShift(1); }, this.props.autoPlaySpeed); 62 | } 63 | } 64 | 65 | componentWillReceiveProps(nextProps) { 66 | const { currentSlide, autoPlay } = nextProps; 67 | if (currentSlide !== undefined && currentSlide !== this.state.currentSlide) { 68 | this.setState({ currentSlide }); 69 | } 70 | 71 | if (this.props.autoPlay && !autoPlay && this.autoPlayTransistionCallback) { 72 | clearInterval(this.autoPlayTransistionCallback); 73 | } 74 | } 75 | 76 | componentDidUpdate(prevProps) { 77 | const { autoPlay } = prevProps; 78 | 79 | if (this.props.autoPlay && !autoPlay && this.props.infinite) { 80 | this.autoPlayTransistionCallback = 81 | setInterval(() => { this.handleSlideShift(1); }, this.props.autoPlaySpeed); 82 | } 83 | } 84 | 85 | componentWillUnmount() { 86 | if (this.transitionEndCallback) { 87 | clearTimeout(this.transitionEndCallback); 88 | } 89 | if (this.autoPlayTransistionCallback) { 90 | clearInterval(this.autoPlayTransistionCallback); 91 | } 92 | } 93 | 94 | /** 95 | * Change the position of a slider by `delta` 96 | * @method 97 | * @private 98 | * @param {number} delta - Move forward the slide by delta (delta can be negative) 99 | */ 100 | handleSlideShift(delta) { 101 | const { currentSlide, animating } = this.state; 102 | const { transitionSpeed, infinite } = this.props; 103 | if (animating === true || delta === 0) { 104 | return; 105 | } 106 | 107 | const slides = this.props.children[1]; 108 | const slideCount = Children.count(slides.props.children); 109 | 110 | let newNextSlide; 111 | if (infinite === true) { 112 | if (currentSlide + 1 === slideCount && delta > 0) { 113 | newNextSlide = 0; 114 | } else if (currentSlide === 0 && delta < 0) { 115 | newNextSlide = slideCount - 1; 116 | } 117 | } 118 | 119 | // EndEventListeners are not reliable. So,we use setTimeout 120 | // See react 0.14.0-rc1 blog post. 121 | // FIXME PERFORMANCE BOTTLENECK 122 | this.transitionEndCallback = () => { 123 | this.setState({ 124 | currentSlide: newNextSlide === undefined ? currentSlide + delta : newNextSlide, 125 | animating: false 126 | }); 127 | delete this.transitionEndCallback; 128 | }; 129 | 130 | const nonInfiniteCondition = 131 | infinite === false && 132 | ((delta < 0 && currentSlide > 0) || (delta > 0 && currentSlide + 1 < slideCount)); 133 | 134 | if (nonInfiniteCondition || infinite === true) { 135 | this.setState({ 136 | currentSlide: currentSlide + delta, 137 | animating: true, 138 | translateXOffset: 0, 139 | translateYOffset: 0 140 | }, () => { 141 | setTimeout(this.transitionEndCallback, transitionSpeed); 142 | }); 143 | } 144 | } 145 | 146 | handleSwipeStart(e) { 147 | const { swipe, draggable, edgeEvent, swipeEvent, touchThreshold, vertical } = this.props; 148 | if (swipe === false && (draggable === false && e.type.indexOf('mouse') === -1)) { 149 | return; 150 | } 151 | 152 | const posX = (e.touches !== undefined) ? e.touches[0].pageX : e.clientX; 153 | const posY = (e.touches !== undefined) ? e.touches[0].pageY : e.clientY; 154 | 155 | // FIXME Breaks compatibility with react-0.13 may be use id to this. 156 | // Doesn't require a style recalc as the actions happens after component is 157 | // mounted or updated 158 | const slider = this.refs.slider; 159 | const sliderRect = slider.getBoundingClientRect(); 160 | const trackRect = slider.children[1].children[0].getBoundingClientRect(); 161 | const maxSwipeLength = vertical === false ? trackRect.width : trackRect.height; 162 | const minSwipe = (vertical === false ? sliderRect.width : sliderRect.height) * touchThreshold; 163 | 164 | this.setState({ 165 | swiping: true, 166 | touchObject: { 167 | startX: posX, 168 | startY: posY, 169 | currX: posX, 170 | currY: posY, 171 | swipeLength: 0, 172 | edgeEventFired: edgeEvent === undefined ? true : false, 173 | swipeEventFired: swipeEvent === undefined ? true : false, 174 | minSwipe, 175 | maxSwipeLength 176 | } 177 | }); 178 | } 179 | 180 | handleSwipeMove(e) { 181 | if (this.state.animating === true || this.state.swiping === false) { 182 | return; 183 | } 184 | 185 | const { infinite, edgeEvent, children, swipeEvent, vertical, touchMove } = this.props; 186 | const { touchObject, currentSlide } = this.state; 187 | 188 | touchObject.currY = e.touches !== undefined ? e.touches[0].pageY : e.clientY; 189 | touchObject.currX = e.touches !== undefined ? e.touches[0].pageX : e.clientX; 190 | 191 | touchObject.swipeLength = swipeDistance(touchObject); 192 | 193 | const direction = swipeDirection(touchObject); 194 | const horizontalPrev = vertical === false && direction === SWIPE_RIGHT; 195 | const horizontalNext = vertical === false && direction === SWIPE_LEFT; 196 | 197 | const verticalPrev = vertical === true && direction === SWIPE_DOWN; 198 | const verticalNext = vertical === true && direction === SWIPE_UP; 199 | 200 | const slideCount = Children.count(children[1].props.children); 201 | 202 | const edgeSwipePrev = currentSlide === 0 && 203 | infinite === false && (horizontalPrev || verticalPrev); 204 | const edgeSwipeNext = (currentSlide + 1) === slideCount && 205 | infinite === false && (horizontalNext || verticalNext); 206 | 207 | const edgeFriction = (edgeSwipePrev || edgeSwipeNext) ? 208 | this.props.edgeFriction : 1; 209 | 210 | // TODO refactor this into a function. 211 | if (touchObject.edgeEventFired === false) { 212 | if (edgeSwipePrev || edgeSwipeNext) { 213 | edgeEvent(direction); 214 | touchObject.edgeEventFired = true; 215 | } 216 | } 217 | 218 | if (touchObject.swipeEventFired === false) { 219 | swipeEvent(direction); 220 | touchObject.swipeEventFired = true; 221 | } 222 | 223 | const translateXOffset = vertical === false && touchMove === true ? 224 | ((touchObject.currX - touchObject.startX) * 100 * edgeFriction) / touchObject.maxSwipeLength : 0; 225 | const translateYOffset = vertical === true && touchMove === true ? 226 | ((touchObject.currY - touchObject.startY) * 100 * edgeFriction) / touchObject.maxSwipeLength : 0; 227 | 228 | // FIXME PERFORMANCE BOTTLENECK 229 | this.setState({ 230 | touchObject: {...touchObject}, 231 | translateXOffset, 232 | translateYOffset 233 | }); 234 | 235 | // Don't cancel scrolling in the cross-axis to the slider 236 | const verticalScroll = vertical === false && (direction === SWIPE_UP || direction === SWIPE_DOWN); 237 | const horizontalScroll = vertical === true && (direction === SWIPE_LEFT || direction === SWIPE_RIGHT); 238 | if (verticalScroll || horizontalScroll) { 239 | return; 240 | } 241 | 242 | // Don't preventDefault for small movement helps in clicking links etc., 243 | // Refer to react-slick#26 244 | if (touchObject.swipeLength > 4) { 245 | e.preventDefault(); 246 | } 247 | } 248 | 249 | handleSwipeEnd(e) { 250 | if (!this.state.swiping) { 251 | return; 252 | } 253 | const { touchObject } = this.state; 254 | const { vertical } = this.props; 255 | const direction = swipeDirection(touchObject); 256 | 257 | this.setState({ 258 | swiping: false, 259 | touchObject: {}, 260 | translateXOffset: 0, 261 | translateYOffset: 0 262 | }); 263 | 264 | const horizontalPrev = vertical === false && direction === SWIPE_RIGHT; 265 | const horizontalNext = vertical === false && direction === SWIPE_LEFT; 266 | 267 | const verticalPrev = vertical === true && direction === SWIPE_DOWN; 268 | const verticalNext = vertical === true && direction === SWIPE_UP; 269 | 270 | if (touchObject.swipeLength > touchObject.minSwipe) { 271 | e.preventDefault(); 272 | if (horizontalPrev || verticalPrev) { 273 | this.handleSlideShift(-1); 274 | } else if (horizontalNext || verticalNext) { 275 | this.handleSlideShift(1); 276 | } 277 | } 278 | } 279 | 280 | render() { 281 | const { children, vertical, infinite, swipe, draggable } = this.props; 282 | const { transitionSpeed, transitionTimingFn } = this.props; 283 | const { beforeChange, afterChange } = this.props; 284 | const [ leftArrow, slides, rightArrow, customComponent ] = children; 285 | const { currentSlide, translateXOffset, translateYOffset } = this.state; 286 | const slideCount = Children.count(slides.props.children); 287 | 288 | // onClick is passed as a props so that dom elements can be custom arrows 289 | 290 | const newLeftArrow = leftArrow !== undefined ? cloneElement(leftArrow, { 291 | key: 0, 292 | handleClick: () => { this.handleSlideShift(-1); }, 293 | onClick: () => { this.handleSlideShift(-1); }, 294 | currentSlide, 295 | infinite 296 | }) : null; 297 | 298 | // Need to pass slideCount to check if end of slide has been reached. 299 | const newRightArrow = rightArrow !== undefined ? cloneElement(rightArrow, { 300 | key: 2, 301 | handleClick: () => { this.handleSlideShift(1); }, 302 | onClick: () => { this.handleSlideShift(1); }, 303 | currentSlide, 304 | infinite, 305 | slideCount 306 | }) : null; 307 | 308 | // TODO Show a warning if transitionSpeed prop is declared on Slides. 309 | const newSlides = cloneElement(slides, { 310 | key: 1, 311 | currentSlide, 312 | infinite, 313 | swipe, 314 | draggable, 315 | transitionSpeed, 316 | transitionTimingFn, 317 | vertical, 318 | onMouseDown: ::this.handleSwipeStart, 319 | onMouseMove: this.state.swiping ? ::this.handleSwipeMove : null, 320 | onMouseUp: ::this.handleSwipeEnd, 321 | onMouseLeave: this.state.swiping ? ::this.handleSwipeEnd : null, 322 | onTouchStart: ::this.handleSwipeStart, 323 | onTouchMove: this.state.swiping ? ::this.handleSwipeMove : null, 324 | onTouchEnd: ::this.handleSwipeEnd, 325 | onTouchCancel: this.state.swiping ? ::this.handleSwipeEnd : null, 326 | beforeChange, 327 | afterChange, 328 | translateXOffset, 329 | translateYOffset 330 | }); 331 | 332 | const newCustomComponent = customComponent !== undefined ? cloneElement(customComponent, { 333 | currentSlide, 334 | slideCount, 335 | handleSlideShift: ::this.handleSlideShift 336 | }) : null; 337 | 338 | return ( 339 |
340 |
341 | {newLeftArrow} 342 | {newSlides} 343 | {newRightArrow} 344 |
345 | {newCustomComponent} 346 |
347 | ); 348 | } 349 | } 350 | 351 | export default Slider; 352 | -------------------------------------------------------------------------------- /src/Slides.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes, Children, cloneElement } from 'react'; 2 | 3 | class Page extends Component { 4 | static propTypes = { 5 | children: PropTypes.any, 6 | className: PropTypes.string, 7 | pageStyle: PropTypes.any 8 | } 9 | 10 | render() { 11 | const { pageStyle, className } = this.props; 12 | 13 | return ( 14 |
15 | {this.props.children} 16 |
17 | ); 18 | } 19 | } 20 | 21 | // TODO Possible PERF OPTIMIZATION remove translateXOffset and translateYOffset from 22 | // here and do imperative DOM operations (translate) inside Slider. 23 | 24 | // This is posibly blocked until the above todo. 25 | // Implement shouldComponentUpdate so that setTimeout state change only effects 26 | // on the edge. May be have an edge key inside state?? 27 | class Track extends Component { 28 | 29 | static propTypes = { 30 | children: PropTypes.any, 31 | infinite: PropTypes.bool.isRequired, 32 | vertical: PropTypes.bool.isRequired, 33 | swipe: PropTypes.bool.isRequired, 34 | draggable: PropTypes.bool.isRequired, 35 | currentSlide: PropTypes.number.isRequired, 36 | pageClass: PropTypes.string, 37 | transitionSpeed: PropTypes.number.isRequired, 38 | transitionTimingFn: PropTypes.string.isRequired, 39 | beforeChange: PropTypes.func, 40 | afterChange: PropTypes.func, 41 | translateXOffset: PropTypes.number, 42 | translateYOffset: PropTypes.number 43 | } 44 | 45 | constructor(props, context) { 46 | super(props, context); 47 | this.state = { 48 | previousSlide: undefined 49 | }; 50 | } 51 | 52 | componentWillReceiveProps() { 53 | // TODO May be move this to Slider 54 | const { currentSlide } = this.props; 55 | const { previousSlide } = this.state; 56 | this.setState({ 57 | previousSlide: previousSlide !== currentSlide ? currentSlide : previousSlide 58 | }); 59 | } 60 | 61 | shouldComponentUpdate(nextProps) { 62 | const { swipe, draggable } = this.props; 63 | 64 | if (swipe === false && draggable === false) { 65 | return this.state.previousSlide !== nextProps.currentSlide; 66 | } 67 | 68 | return true; 69 | } 70 | 71 | componentWillUpdate() { 72 | if (this.props.beforeChange !== undefined) { 73 | this.props.beforeChange(this.state.previousSlide, this.props.currentSlide); 74 | } 75 | } 76 | 77 | 78 | componentDidUpdate() { 79 | if (this.props.afterChange !== undefined) { 80 | this.props.afterChange(this.state.previousSlide, this.props.currentSlide); 81 | } 82 | } 83 | 84 | computeTrackStyle() { 85 | const { vertical, currentSlide, infinite, 86 | translateXOffset, translateYOffset, 87 | transitionSpeed, transitionTimingFn } = this.props; 88 | const slideCount = Children.count(this.props.children); 89 | const totalCount = slideCount + (infinite === true ? 2 : 0); 90 | const { previousSlide } = this.state; 91 | const preSlideCount = infinite === true ? 1 : 0; 92 | 93 | const trackWidth = vertical ? '100%' : `${100 * totalCount}%`; 94 | const trackHeight = vertical ? `${100 * totalCount}%` : '100%'; 95 | const translate = (100 * (currentSlide + preSlideCount)) / totalCount; 96 | const translateX = vertical === false ? translate - translateXOffset : 0; 97 | const translateY = vertical === true ? translate - translateYOffset : 0; 98 | const trackTransform = `translate3d(${-translateX}%, ${-translateY}%, 0)`; 99 | const trackTransition = 100 | infinite === true && ((previousSlide === -1 && (currentSlide === slideCount - 1)) || 101 | ((previousSlide === slideCount) && currentSlide === 0)) || 102 | (translateXOffset !== 0 || translateYOffset !== 0) ? '' : 103 | `all ${transitionSpeed}ms ${transitionTimingFn}`; 104 | const flexDirection = vertical ? 'column' : 'row'; 105 | 106 | const trackStyle = { 107 | width: trackWidth, 108 | height: trackHeight, 109 | display: 'flex', 110 | flexDirection, 111 | flexShrink: 0, 112 | transform: trackTransform, 113 | transition: trackTransition 114 | }; 115 | 116 | return trackStyle; 117 | } 118 | 119 | computePageStyle() { 120 | const { vertical, infinite } = this.props; 121 | 122 | const slideCount = Children.count(this.props.children); 123 | const totalCount = slideCount + (infinite === true ? 2 : 0); 124 | 125 | const pageWidth = vertical ? '100%' : `${100 / totalCount}%`; 126 | const pageHeight = vertical ? `${100 / totalCount}%` : '100%'; 127 | const pageStyle = { 128 | width: pageWidth, 129 | height: pageHeight, 130 | display: 'flex', 131 | justifyContent: 'center', 132 | alignItems: 'center' 133 | }; 134 | 135 | return pageStyle; 136 | } 137 | 138 | render() { 139 | const { pageClass, infinite } = this.props; 140 | const slideCount = Children.count(this.props.children); 141 | const totalCount = slideCount + (infinite === true ? 2 : 0); 142 | 143 | const trackStyle = this.computeTrackStyle(); 144 | const pageStyle = this.computePageStyle(); 145 | 146 | const slides = Children.map(this.props.children, (child, i) => 147 | 148 | {cloneElement(child, { key: i })} 149 | 150 | ); 151 | 152 | const preSlides = slideCount === 1 || infinite === false ? null : 153 | 154 | {cloneElement(this.props.children[slideCount - 1], { key: -1 })} 155 | ; 156 | 157 | const postSlides = slideCount === 1 || infinite === false ? null : 158 | 159 | {cloneElement(this.props.children[0], { key: totalCount })} 160 | ; 161 | 162 | return ( 163 |
164 | {preSlides} 165 | {slides} 166 | {postSlides} 167 |
168 | ); 169 | } 170 | } 171 | 172 | class Slides extends Component { 173 | static propTypes = { 174 | children: PropTypes.any, 175 | width: PropTypes.number, 176 | height: PropTypes.number, 177 | currentSlide: PropTypes.number, 178 | infinite: PropTypes.bool, 179 | vertical: PropTypes.bool, 180 | swipe: PropTypes.bool, 181 | draggable: PropTypes.bool, 182 | pageClass: PropTypes.string, 183 | transitionSpeed: PropTypes.number, 184 | transitionTimingFn: PropTypes.string, 185 | onMouseDown: PropTypes.func, 186 | onMouseMove: PropTypes.func, 187 | onMouseUp: PropTypes.func, 188 | onMouseLeave: PropTypes.func, 189 | onTouchStart: PropTypes.func, 190 | onTouchMove: PropTypes.func, 191 | onTouchEnd: PropTypes.func, 192 | onTouchCancel: PropTypes.func, 193 | beforeChange: PropTypes.func, 194 | afterChange: PropTypes.func, 195 | translateXOffset: PropTypes.number, 196 | translateYOffset: PropTypes.number 197 | } 198 | 199 | static defaultProps = { 200 | width: 0, 201 | height: 0 202 | } 203 | 204 | render() { 205 | const { width, height, children, 206 | onMouseDown, onMouseMove, onMouseUp, onMouseLeave, 207 | onTouchStart, onTouchMove, onTouchEnd, onTouchCancel, ...props } = this.props; 208 | 209 | const containerWidth = width === 0 ? '100%' : width; 210 | const containerHeight = height === 0 ? '100%' : height; 211 | const containerStyle = { 212 | width: containerWidth, 213 | height: containerHeight, 214 | display: 'flex', 215 | overflow: 'hidden' 216 | }; 217 | 218 | return ( 219 |
228 | 229 | {this.props.children} 230 | 231 |
232 | ); 233 | } 234 | } 235 | 236 | export default Slides; 237 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export Slider from './Slider.js'; 2 | export Slides from './Slides.js'; 3 | export { PrevArrow, NextArrow } from './Arrows.js'; 4 | export Dots from './Dots.js'; 5 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | const SWIPE_UP = 'SWIPE_UP'; 2 | const SWIPE_DOWN = 'SWIPE_DOWN'; 3 | const SWIPE_LEFT = 'SWIPE_LEFT'; 4 | const SWIPE_RIGHT = 'SWIPE_RIGHT'; 5 | 6 | const swipeDirection = touchObject => { 7 | const { startX, startY, currX, currY } = touchObject; 8 | 9 | const deltaX = currX - startX; 10 | const deltaY = currY - startY; 11 | 12 | const angle = (Math.atan2(deltaY, deltaX) * 180) / Math.PI; 13 | if (angle < 45 && angle > -45) { 14 | return SWIPE_RIGHT; 15 | } else if ((angle <= 180 && angle > 135) || (angle < -135 && angle > -180)) { 16 | return SWIPE_LEFT; 17 | } else if (angle > 45 && angle < 135 ) { 18 | return SWIPE_DOWN; 19 | } else if (angle < -45 && angle > -135 ) { 20 | return SWIPE_UP; 21 | } 22 | }; 23 | 24 | const swipeDistance = touchObject => { 25 | const { startX, startY, currX, currY } = touchObject; 26 | return Math.round(Math.sqrt(Math.pow(currX - startX, 2) + Math.pow(currY - startY, 2))); 27 | }; 28 | 29 | export { 30 | swipeDirection, 31 | swipeDistance, 32 | SWIPE_UP, 33 | SWIPE_DOWN, 34 | SWIPE_LEFT, 35 | SWIPE_RIGHT 36 | }; 37 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | module.exports = { 5 | devtool: '#source-map', 6 | entry: [ 7 | 'webpack-hot-middleware/client', 8 | './examples/index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/static/' 14 | }, 15 | plugins: [ 16 | new webpack.HotModuleReplacementPlugin(), 17 | new webpack.NoErrorsPlugin() 18 | ], 19 | module: { 20 | loaders: [ 21 | { 22 | test: /\.js$/, 23 | include: [path.join(__dirname, 'examples'), path.join(__dirname, 'src')], 24 | loaders: ['babel'] 25 | }, 26 | { 27 | test: /\.css$/, 28 | include: [path.join(__dirname, 'examples')], 29 | loaders: ['style', 'css'] 30 | } 31 | ] 32 | } 33 | }; 34 | --------------------------------------------------------------------------------