├── .gitignore ├── .npmignore ├── README.md ├── bower.json ├── example ├── index.html ├── js │ ├── app.jsx │ └── main.js └── less │ └── site.less ├── gulpfile.js ├── package.json ├── src ├── js │ ├── scrollBar.jsx │ ├── scrollMouse.jsx │ ├── scrollStyles.js │ ├── scrollTouch.jsx │ └── scrollable.js └── less │ ├── scrollbar.less │ └── site.less └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | /dist/ 2 | /example/*.js 3 | /example/*.css 4 | /node_modules/ 5 | /bower_components/ 6 | /npm-debug.log 7 | /.publish 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /bower_components/ 3 | /npm-debug.log 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-inertia 2 | 3 | An inertia touch scroll [React](http://facebook.github.io/react/) component come up to native scrolling experience and performance. 4 | 5 | [Demo](http://souhe.github.io/reactScrollbar) 6 | 7 | ```bash 8 | npm install react-inertia --save 9 | ``` 10 | 11 | ## Usage examples 12 | 13 | ```js 14 | var React = require('react'); 15 | var ScrollArea = require('react-inertia'); 16 | 17 | var App = React.createClass({ 18 | render: function() { 19 | return ( 20 | 25 | ... lots of content here... 26 | 27 | ); 28 | } 29 | }); 30 | 31 | React.render(, document.body); 32 | ``` 33 | 34 | ### Run the example app 35 | 36 | ```bash 37 | git clone https://github.com/souhe/reactScrollbar.git 38 | cd reactScrollbar 39 | npm install 40 | gulp 41 | ``` 42 | 43 | then open [http://localhost:8003](http://localhost:80003). 44 | 45 | ## API 46 | 47 | ### Props 48 | 49 | ```js 50 | 57 | ``` 58 | 59 | #### speed 60 | Scroll speed applied to mouse wheel event. 61 | **Default: 1** 62 | 63 | #### className 64 | CSS class names added to main scroll area component. 65 | 66 | #### contentClassName 67 | CSS class names added to element with scroll area content. 68 | 69 | #### horizontal 70 | When set to false, horizontal scrollbar will not be available. 71 | **Default: true** 72 | 73 | #### vertical 74 | When set to false, vertical scrollbar will not be available, regardless of the content height. 75 | **Default: true** 76 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-inertia", 3 | "version": "0.0.1", 4 | "description": "An inertia touch scroll React component", 5 | "homepage": "https://github.com/garryyao/react-inertia", 6 | "authors": [ 7 | "Garry Yao " 8 | ], 9 | "keywords": [ 10 | "react", 11 | "react-component", 12 | "inertia", 13 | "momentum", 14 | "scroll", 15 | "touch" 16 | ], 17 | "license": "MIT", 18 | "dependencies": { 19 | "scroller": "~1.2.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Inertia Scroll React Demo 6 | 7 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /example/js/app.jsx: -------------------------------------------------------------------------------- 1 | require.ensure(['../../scrollable'], function () { 2 | var React = require('react'); 3 | var Scrollable = require('../../scrollable'); 4 | 5 | class App extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | } 9 | 10 | render() { 11 | var itemElements = []; 12 | 13 | var color; 14 | for (var i = 0; i < this.props.itemsCount; i++) { 15 | color = '#' + Math.floor(Math.random() * 16777215).toString(16); 16 | itemElements.push(
item {i}
); 17 | } 18 | 19 | return ( 20 |
21 | 22 | {itemElements} 23 | 24 |
25 | ); 26 | } 27 | } 28 | 29 | App.defaultProps = { 30 | itemsCount: 100 31 | }; 32 | 33 | module.exports = App; 34 | }); 35 | -------------------------------------------------------------------------------- /example/js/main.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from './app.jsx'; 3 | 4 | var css = require("style!css!../../css/scrollbar.css"); 5 | 6 | React.render(React.createElement(App, {itemsCount: 100}), document.getElementById("main")); 7 | -------------------------------------------------------------------------------- /example/less/site.less: -------------------------------------------------------------------------------- 1 | html, body, #main{ 2 | height: 100%; 3 | } 4 | body{ 5 | margin: 0; 6 | 7 | h1, h2, h3, h4, h5, h6 { 8 | font-weight: normal; 9 | } 10 | 11 | .container{ 12 | width: 100%; 13 | height: 100%; 14 | box-sizing: border-box; 15 | position: relative; 16 | background: white; 17 | padding: 30px; 18 | } 19 | 20 | .area{ 21 | margin: 0 auto; 22 | height: calc(~'100vh' - 60px); 23 | background: #e5e5e5; 24 | 25 | .content{ 26 | .item{ 27 | font-size: 5vh; 28 | background: #82bb95; 29 | width: 100%; 30 | height: 10vh; 31 | margin: 10px; 32 | float: left; 33 | padding: 8px; 34 | box-sizing: border-box; 35 | } 36 | } 37 | ul{ 38 | width: 700px; 39 | } 40 | } 41 | 42 | 43 | } 44 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var webpack = require('gulp-webpack'); 3 | var concat = require('gulp-concat'); 4 | var less = require('gulp-less'); 5 | var webpackConf = require('./webpack.config.js'); 6 | var babel = require('gulp-babel'); 7 | var connect = require('gulp-connect'); 8 | var uglify = require('gulp-uglify'); 9 | 10 | gulp.task('babel', function () { 11 | return gulp.src('src/js/*.*').pipe(babel()).pipe(gulp.dest('.')); 12 | }); 13 | 14 | gulp.task('build-less', function () { 15 | return gulp.src('./src/less/**/*.less').pipe(less()).pipe(gulp.dest('./css')).pipe(connect.reload()); 16 | }); 17 | 18 | gulp.task('build-less-example', function () { 19 | return gulp.src('./example/less/**/*.less').pipe(less()).pipe(gulp.dest('./example')).pipe(connect.reload()); 20 | }); 21 | 22 | gulp.task("webpack", function () { 23 | return webpack(webpackConf).pipe(gulp.dest('dist')).pipe(connect.reload()); 24 | }); 25 | 26 | gulp.task('confuse', function () { 27 | return gulp.src('dist/*.js') 28 | .pipe(uglify()) 29 | .pipe(gulp.dest('dist')); 30 | }); 31 | 32 | gulp.task("connect", function () { 33 | connect.server({ 34 | root: '.', 35 | livereload: true, 36 | port: 8003 37 | }); 38 | }); 39 | 40 | gulp.task('build', ['build-less', 'copy']); 41 | 42 | gulp.task('deploy', ['confuse'], function () { 43 | var gh_pages = require('gulp-gh-pages'); 44 | return gulp.src(['dist/**', 'example/**'], {base: '.'}) 45 | .pipe(gh_pages()); 46 | }); 47 | 48 | gulp.task('default', ['build-less', 'babel', 'build-less-example', 'webpack']); 49 | 50 | gulp.task('js', ['babel', 'webpack']); 51 | 52 | gulp.task('watch', function () { 53 | connect.server({ 54 | root: '.', 55 | livereload: true, 56 | port: 8003 57 | }); 58 | gulp.watch('src/**/*.less', ['build-less']); 59 | gulp.watch(['src/**/*.js', 'src/**/*.jsx'], ['babel', 'webpack']); 60 | gulp.watch(['example/js/**/*.js', 'example/**/*.jsx'], ['webpack']); 61 | gulp.watch('example/**/*.less', ['build-less-example']); 62 | }); 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-inertia", 3 | "version": "0.0.1", 4 | "description": "An inertia touch scroll React component", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/garryyao/react-scroll" 11 | }, 12 | "keywords": [ 13 | "react", 14 | "react-component", 15 | "inertia", 16 | "momentum", 17 | "scroll", 18 | "touch" 19 | ], 20 | "author": "yaojun85@gmail.com", 21 | "license": "MIT", 22 | "dependencies": { 23 | "classnames": "^1.2.0", 24 | "object.assign": "^2.0.1", 25 | "react": "^0.13.1" 26 | }, 27 | "devDependencies": { 28 | "babel-core": "^4.7.16", 29 | "babel-loader": "^4.2.0", 30 | "babel-runtime": "^5.0.8", 31 | "css-loader": "^0.10.1", 32 | "exports-loader": "^0.6.2", 33 | "gulp": "^3.8.11", 34 | "gulp-babel": "^5.1.0", 35 | "gulp-concat": "^2.5.2", 36 | "gulp-connect": "^2.2.0", 37 | "gulp-gh-pages": "^0.5.1", 38 | "gulp-less": "^3.0.2", 39 | "gulp-uglify": "^1.2.0", 40 | "gulp-webpack": "^1.3.0", 41 | "imports-loader": "^0.6.3", 42 | "jquery": "^2.1.4", 43 | "style-loader": "^0.10.2", 44 | "uglify-loader": "^1.2.0", 45 | "webpack": "^1.7.3" 46 | }, 47 | "bugs": { 48 | "url": "https://github.com/garryyao/react-inertia/issues" 49 | }, 50 | "homepage": "https://github.com/garryyao/react-inertia" 51 | } 52 | -------------------------------------------------------------------------------- /src/js/scrollBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import ScrollStyle from './scrollStyles.js'; 4 | import assign from 'object.assign'; 5 | 6 | 7 | class ScrollBar extends React.Component { 8 | constructor(props) { 9 | super(props); 10 | var newState = this.calculateState(props); 11 | this.state = { 12 | position: newState.position, 13 | scrollSize: newState.scrollSize, 14 | isDragging: false, 15 | isActive: false, 16 | lastClientPosition: 0 17 | } 18 | 19 | if (props.type === 'vertical') { 20 | this.bindedHandleMouseMove = this.handleMouseMoveForVertical.bind(this); 21 | } else { 22 | this.bindedHandleMouseMove = this.handleMouseMoveForHorizontal.bind(this); 23 | } 24 | 25 | this.bindedHandleMouseUp = this.handleMouseUp.bind(this); 26 | } 27 | 28 | componentDidMount() { 29 | document.addEventListener("mousemove", this.bindedHandleMouseMove); 30 | document.addEventListener("mouseup", this.bindedHandleMouseUp); 31 | } 32 | 33 | componentWillReceiveProps(nextProps) { 34 | var props = this.calculateState(nextProps); 35 | props.isActive = true; 36 | this.setState(props); 37 | clearTimeout(this.timer); 38 | this.timer = setTimeout(() => { 39 | this.setState({ 40 | isActive: false 41 | }); 42 | }, 200); 43 | } 44 | 45 | componentWillUnmount() { 46 | document.removeEventListener("mousemove", this.bindedHandleMouseMove); 47 | document.removeEventListener("mouseup", this.bindedHandleMouseUp); 48 | } 49 | 50 | calculateState(props) { 51 | var scrollSize = props.containerSize * props.containerSize / props.realSize; 52 | var multiplier = props.containerSize / props.realSize; 53 | var position = props.position * multiplier; 54 | 55 | return { 56 | scrollSize: scrollSize, 57 | position: position 58 | }; 59 | } 60 | 61 | render() { 62 | var scrollStyle = this.createScrollStyles(); 63 | var scrollbarClasses = classNames([ 64 | 'scrollbar-container', 65 | { 66 | 'active': this.state.isDragging || this.state.isActive, 67 | 'horizontal': this.props.type === 'horizontal', 68 | 'vertical': this.props.type === 'vertical' 69 | } 70 | ]); 71 | 72 | return ( 73 |
74 |
78 | 79 |
80 |
81 | ); 82 | } 83 | 84 | handleMouseMoveForHorizontal(e) { 85 | var multiplier = this.props.containerSize / this.props.realSize; 86 | if (this.state.isDragging) { 87 | e.preventDefault(); 88 | var deltaX = this.state.lastClientPosition - e.clientX; 89 | this.setState({lastClientPosition: e.clientX}); 90 | this.props.onMove(0, deltaX / multiplier); 91 | } 92 | } 93 | 94 | handleMouseMoveForVertical(e) { 95 | var multiplier = this.props.containerSize / this.props.realSize; 96 | if (this.state.isDragging) { 97 | e.preventDefault(); 98 | var deltaY = this.state.lastClientPosition - e.clientY; 99 | this.setState({lastClientPosition: e.clientY}); 100 | this.props.onMove(deltaY / multiplier, 0); 101 | } 102 | } 103 | 104 | handleMouseDown(e) { 105 | var lastClientPosition = this.props.type === 'vertical' 106 | ? e.clientY 107 | : e.clientX 108 | this.setState({isDragging: true, lastClientPosition: lastClientPosition}); 109 | } 110 | 111 | handleMouseUp(e) { 112 | this.setState({isDragging: false}); 113 | } 114 | 115 | createScrollStyles() { 116 | var style = {}; 117 | var left = 0, top = 0; 118 | if (this.props.type === 'vertical') { 119 | style.height = this.state.scrollSize; 120 | top = this.state.position; 121 | } else { 122 | style.width = this.state.scrollSize; 123 | left = this.state.position; 124 | } 125 | return assign(style, ScrollStyle(left, top)); 126 | } 127 | } 128 | 129 | ScrollBar.propTypes = { 130 | onMove: React.PropTypes.func, 131 | realSize: React.PropTypes.number, 132 | containerSize: React.PropTypes.number, 133 | position: React.PropTypes.number, 134 | type: React.PropTypes.oneOf(['vertical', 'horizontal']) 135 | }; 136 | 137 | ScrollBar.defaultProps = { 138 | type: 'vertical', 139 | useCssTransform: true 140 | } 141 | export default ScrollBar; 142 | -------------------------------------------------------------------------------- /src/js/scrollMouse.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Scrollable extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { 7 | // aka. scrollTop 8 | topPosition: 0, 9 | // aka. scrollLeft 10 | leftPosition: 0, 11 | // aka. scrollHeight 12 | realHeight: 0, 13 | // aka. clientHeight 14 | containerHeight: 0, 15 | // aka. scrollWidth 16 | realWidth: 0, 17 | // aka. clientWidth 18 | containerWidth: 0, 19 | // if content has horizontally overflowed 20 | scrollableX: false, 21 | // if content has vertically overflowed 22 | scrollableY: false 23 | }; 24 | 25 | this.bindedHandleWindowResize = this.handleWindowResize.bind(this); 26 | } 27 | 28 | componentDidMount() { 29 | window.addEventListener("resize", this.bindedHandleWindowResize); 30 | this.setSizesToState(); 31 | } 32 | 33 | componentWillUnmount() { 34 | window.removeEventListener("resize", this.bindedHandleWindowResize); 35 | } 36 | 37 | componentDidUpdate() { 38 | this.setSizesToState(); 39 | } 40 | 41 | render() { 42 | var style = { 43 | marginTop: this.state.topPosition, 44 | marginLeft: this.state.leftPosition 45 | }; 46 | 47 | var classes = 'scrollarea ' + this.props.className; 48 | var contentClasses = 'scrollarea-content ' + this.props.contentClassName 49 | return ( 50 |
51 |
52 | {this.props.children} 53 |
54 |
55 | ); 56 | } 57 | 58 | handleMove(deltaY, deltaX) { 59 | var newState = this.computeSizes(); 60 | if (this.canScrollY(newState)) { 61 | newState.topPosition = this.computeTopPosition(deltaY); 62 | } 63 | if (this.canScrollX(newState)) { 64 | newState.leftPosition = this.computeLeftPosition(deltaX); 65 | } 66 | this.setState(newState); 67 | } 68 | 69 | handleWheel(e) { 70 | var newState = this.computeSizes(); 71 | var deltaY = e.deltaY * this.props.speed; 72 | var deltaX = e.deltaX * this.props.speed; 73 | 74 | if (this.canScrollY(newState)) { 75 | newState.topPosition = this.computeTopPosition(-deltaY); 76 | } 77 | 78 | if (this.canScrollX(newState)) { 79 | newState.leftPosition = this.computeLeftPosition(-deltaX); 80 | } 81 | 82 | if (this.state.topPosition !== newState.topPosition || 83 | this.state.leftPosition !== newState.leftPosition) { 84 | e.preventDefault(); 85 | } 86 | 87 | this.setState(newState); 88 | } 89 | 90 | computeTopPosition(deltaY) { 91 | var newTopPosition = this.state.topPosition + deltaY; 92 | if (-newTopPosition > this.state.realHeight - this.state.containerHeight) { 93 | newTopPosition = -(this.state.realHeight - this.state.containerHeight); 94 | } 95 | else if (newTopPosition > 0) { 96 | newTopPosition = 0; 97 | } 98 | 99 | return newTopPosition; 100 | } 101 | 102 | computeLeftPosition(deltaX) { 103 | var newLeftPosition = this.state.leftPosition + deltaX; 104 | if (-newLeftPosition > this.state.realWidth - this.state.containerWidth) { 105 | newLeftPosition = -(this.state.realWidth - this.state.containerWidth); 106 | } 107 | else if (newLeftPosition > 0) { 108 | newLeftPosition = 0; 109 | } 110 | 111 | return newLeftPosition; 112 | } 113 | 114 | handleWindowResize() { 115 | var newState = this.computeSizes(); 116 | var bottomPosition = newState.realHeight - newState.containerHeight; 117 | if (-this.state.topPosition >= bottomPosition) { 118 | newState.topPosition = this.canScrollY(newState) ? -bottomPosition : 0; 119 | } 120 | 121 | var rightPosition = newState.realWidth - newState.containerWidth; 122 | if (-this.state.leftPosition >= rightPosition) { 123 | newState.leftPosition = this.canScrollX(newState) ? -rightPosition : 0; 124 | } 125 | 126 | this.setState(newState); 127 | } 128 | 129 | computeSizes() { 130 | var realHeight = React.findDOMNode(this.refs.content).offsetHeight; 131 | var containerHeight = React.findDOMNode(this).offsetHeight; 132 | var realWidth = React.findDOMNode(this.refs.content).offsetWidth; 133 | var containerWidth = React.findDOMNode(this).offsetWidth; 134 | var scrollableY = realHeight > containerHeight; 135 | var scrollableX = realWidth > containerWidth; 136 | 137 | return { 138 | realHeight: realHeight, 139 | containerHeight: containerHeight, 140 | realWidth: realWidth, 141 | containerWidth: containerWidth, 142 | scrollableX: scrollableX, 143 | scrollableY: scrollableY 144 | }; 145 | } 146 | 147 | setSizesToState() { 148 | var sizes = this.computeSizes(); 149 | if (sizes.realHeight !== this.state.realHeight || 150 | sizes.realWidth !== this.state.realWidth) { 151 | this.setState(sizes); 152 | } 153 | } 154 | 155 | canScrollY(state = this.state) { 156 | return state.scrollableY && this.props.vertical; 157 | } 158 | 159 | canScrollX(state = this.state) { 160 | return state.scrollableX && this.props.horizontal; 161 | } 162 | } 163 | 164 | Scrollable.propTypes = { 165 | className: React.PropTypes.string, 166 | speed: React.PropTypes.number, 167 | contentClassName: React.PropTypes.string, 168 | vertical: React.PropTypes.bool, 169 | horizontal: React.PropTypes.bool 170 | }; 171 | 172 | Scrollable.defaultProps = { 173 | speed: 1, 174 | vertical: true, 175 | horizontal: true 176 | }; 177 | 178 | export default Scrollable; 179 | -------------------------------------------------------------------------------- /src/js/scrollStyles.js: -------------------------------------------------------------------------------- 1 | var getScrollStyles = (function (global) { 2 | 3 | var docStyle = document.documentElement.style; 4 | 5 | var engine; 6 | if (global.opera && Object.prototype.toString.call(opera) === '[object Opera]') { 7 | engine = 'presto'; 8 | } else if ('MozAppearance' in docStyle) { 9 | engine = 'gecko'; 10 | } else if ('WebkitAppearance' in docStyle) { 11 | engine = 'webkit'; 12 | } else if (typeof navigator.cpuClass === 'string') { 13 | engine = 'trident'; 14 | } 15 | 16 | var vendorPrefix = ({ 17 | trident: 'ms', 18 | gecko: 'Moz', 19 | webkit: 'Webkit', 20 | presto: 'O' 21 | })[engine]; 22 | 23 | var helperElem = document.createElement('div'); 24 | var undef; 25 | 26 | var perspectiveProperty = vendorPrefix + 'Perspective'; 27 | var transformProperty = vendorPrefix + 'Transform'; 28 | 29 | if (helperElem.style[perspectiveProperty] !== undef) { 30 | 31 | return function (left, top) { 32 | var style = {}; 33 | style[transformProperty] = 'translate3d(' + -left + 'px,' + -top + 'px,0)'; 34 | return style; 35 | }; 36 | } else if (helperElem.style[transformProperty] !== undef) { 37 | 38 | return function (left, top) { 39 | var style = {}; 40 | style[transformProperty] = 'translate(' + -left + 'px,' + -top + 'px)'; 41 | return style; 42 | }; 43 | } else { 44 | return function (left, top) { 45 | var style = {}; 46 | style.marginLeft = left ? -left + 'px' : ''; 47 | style.marginTop = top ? -top + 'px' : ''; 48 | return style; 49 | }; 50 | } 51 | })(window); 52 | 53 | export default getScrollStyles; -------------------------------------------------------------------------------- /src/js/scrollTouch.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Scrollbar from './scrollBar.js'; 3 | import ScrollStyle from './scrollStyles.js'; 4 | 5 | require('imports?this=>window!scroller/src/Animate.js'); 6 | var Scroller = require('imports?core=>window.core!exports?Scroller!scroller/src/Scroller.js') 7 | 8 | class Scrollable extends React.Component { 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | leftPosition: 0, 13 | topPosition: 0, 14 | realHeight: 0, 15 | containerHeight: 0, 16 | realWidth: 0, 17 | containerWidth: 0, 18 | scrollableX: false, 19 | scrollableY: false 20 | }; 21 | this.bindedHandleWindowResize = this.handleWindowResize.bind(this); 22 | } 23 | 24 | componentDidMount() { 25 | window.addEventListener("resize", this.bindedHandleWindowResize); 26 | var scroller = this.scroller = new Scroller(this.handleScroll.bind(this), { 27 | // scroll options goes here 28 | }); 29 | this.updateScroll(); 30 | } 31 | 32 | componentWillUnmount() { 33 | window.removeEventListener("resize", this.bindedHandleWindowResize); 34 | } 35 | 36 | computeSizes() { 37 | var realHeight = React.findDOMNode(this.refs.content).offsetHeight; 38 | var realWidth = React.findDOMNode(this.refs.content).offsetWidth; 39 | var container = React.findDOMNode(this); 40 | var rect = container.getBoundingClientRect(); 41 | var containerLeft = rect.left + container.clientLeft; 42 | var containerTop = rect.top + container.clientTop; 43 | var containerHeight = container.offsetHeight; 44 | var containerWidth = container.offsetWidth; 45 | var scrollableY = realHeight > containerHeight; 46 | var scrollableX = realWidth > containerWidth; 47 | return { 48 | containerHeight: containerHeight, 49 | containerWidth: containerWidth, 50 | containerLeft: containerLeft, 51 | containerTop: containerTop, 52 | realHeight: realHeight, 53 | realWidth: realWidth, 54 | scrollableX: scrollableX, 55 | scrollableY: scrollableY 56 | }; 57 | } 58 | 59 | updateScroll() { 60 | var sizes = this.computeSizes(); 61 | var scroller = this.scroller; 62 | scroller.setPosition(sizes.containerLeft, sizes.containerTop); 63 | scroller.setDimensions(sizes.containerWidth, sizes.containerHeight, sizes.realWidth, sizes.realHeight); 64 | scroller.options.scrollingX = sizes.scrollableX; 65 | scroller.options.scrollingY = sizes.scrollableY; 66 | this.setState(sizes); 67 | } 68 | 69 | handleScroll(left, top) { 70 | this.setState({ 71 | leftPosition: left, 72 | topPosition: top 73 | }); 74 | } 75 | 76 | // Events 77 | // ====== 78 | 79 | handleTouchStart(e) { 80 | // Don't react if initial down happens on a form element 81 | if (e.target.tagName.match(/input|textarea|select/i)) { 82 | return; 83 | } 84 | 85 | this.scroller.doTouchStart(e.touches, e.timeStamp); 86 | e.preventDefault(); 87 | } 88 | 89 | handleTouchMove(e) { 90 | e.preventDefault(); 91 | this.scroller.doTouchMove(e.touches, e.timeStamp, e.scale); 92 | } 93 | 94 | handleTouchEnd(e) { 95 | this.scroller.doTouchEnd(e.timeStamp); 96 | } 97 | 98 | handleWindowResize() { 99 | this.updateScroll(); 100 | } 101 | 102 | canScrollY(state = this.state) { 103 | return state.scrollableY && this.props.vertical; 104 | } 105 | 106 | canScrollX(state = this.state) { 107 | return state.scrollableX && this.props.horizontal; 108 | } 109 | 110 | render() { 111 | 112 | var scrollbarY = this.canScrollY() ? ( 113 | 118 | ) : null; 119 | 120 | var scrollbarX = this.canScrollX() ? ( 121 | 126 | ) : null; 127 | 128 | var style = ScrollStyle(this.state.leftPosition, this.state.topPosition); 129 | 130 | var classes = 'scrollarea ' + this.props.className; 131 | var contentClasses = 'scrollarea-content ' + this.props.contentClassName 132 | return ( 133 |
137 |
138 | {this.props.children} 139 |
140 | {scrollbarY} 141 | {scrollbarX} 142 |
143 | ); 144 | } 145 | } 146 | 147 | Scrollable.propTypes = { 148 | className: React.PropTypes.string, 149 | speed: React.PropTypes.number, 150 | contentClassName: React.PropTypes.string, 151 | vertical: React.PropTypes.bool, 152 | horizontal: React.PropTypes.bool 153 | }; 154 | 155 | Scrollable.defaultProps = { 156 | speed: 1, 157 | vertical: true, 158 | horizontal: true, 159 | useCssTransform: true 160 | }; 161 | 162 | export default Scrollable; 163 | -------------------------------------------------------------------------------- /src/js/scrollable.js: -------------------------------------------------------------------------------- 1 | import scrollTouch from './scrollTouch.js'; 2 | import scrollMouse from './scrollMouse.js'; 3 | 4 | function isTouch() { 5 | return 'ontouchstart' in window || 'onmsgesturechange' in window; 6 | } 7 | 8 | export default isTouch() ? scrollTouch : scrollMouse; -------------------------------------------------------------------------------- /src/less/scrollbar.less: -------------------------------------------------------------------------------- 1 | .scrollarea-content { 2 | margin: 0; 3 | padding: 0; 4 | overflow: hidden; 5 | position: relative; 6 | } 7 | 8 | .scrollarea { 9 | position: relative; 10 | overflow: hidden; 11 | .scrollbar-container { 12 | 13 | .scrollbar { 14 | border-radius: 10px; 15 | } 16 | 17 | &.horizontal { 18 | width: 100%; 19 | height: 10px; 20 | left: 0; 21 | bottom: 5px; 22 | 23 | .scrollbar { 24 | height: 100%; 25 | height: 8px; 26 | background: black; 27 | margin-top: 1px; 28 | } 29 | } 30 | 31 | &.vertical { 32 | width: 10px; 33 | height: 100%; 34 | right: 5px; 35 | top: 0; 36 | 37 | .scrollbar { 38 | width: 100%; 39 | height: 20px; 40 | background: black; 41 | margin-left: 1px; 42 | } 43 | } 44 | 45 | position: absolute; 46 | background: none; 47 | opacity: 0; 48 | z-index: 9999; 49 | 50 | -webkit-transition: all .4s; 51 | -moz-transition: all .4s; 52 | -ms-transition: all .4s; 53 | -o-transition: all .4s; 54 | transition: all .4s; 55 | 56 | &:hover, &.active { 57 | opacity: .4 !important; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/less/site.less: -------------------------------------------------------------------------------- 1 | html, body, #main { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | 8 | h1, h2, h3, h4, h5, h6 { 9 | font-weight: normal; 10 | } 11 | 12 | .container { 13 | width: 100%; 14 | height: 100%; 15 | position: relative; 16 | } 17 | 18 | .content { 19 | width: 300px; 20 | height: 300px; 21 | background: #5fe151; 22 | overflow: hidden; 23 | position: relative; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | module.exports = { 4 | entry: { 5 | example: './example/js/main.js', 6 | main: ['./scrollable.js'], 7 | vendor: ['react'] 8 | }, 9 | output: { 10 | filename: '[name].js' 11 | }, 12 | resolve: { 13 | alias: { 14 | 'react': 'react/dist/react-with-addons.min' 15 | }, 16 | modulesDirectories: ['node_modules', 'bower_components'], 17 | }, 18 | plugins: [ 19 | new webpack.optimize.CommonsChunkPlugin( 20 | { 21 | name: ['vendor'] 22 | } 23 | ) 24 | ], 25 | module: { 26 | loaders: [ 27 | { 28 | test: /\.js|\.jsx$/, 29 | exclude: /node_modules|bower_components/, 30 | loader: 'babel-loader' 31 | }, { 32 | test: /\.less$/, 33 | loader: 'css?sourceMap!less?sourceMap' 34 | } 35 | ] 36 | } 37 | }; 38 | --------------------------------------------------------------------------------