├── .editorconfig ├── .gitignore ├── .jshintrc ├── .yo-rc.json ├── Gruntfile.js ├── GulpFile.js ├── README.md ├── dist ├── assets │ └── react-infinite-list.js ├── favicon.ico ├── images │ └── yeoman.png └── index.html ├── karma.conf.js ├── npm-debug.log ├── package.json ├── src ├── favicon.ico ├── images │ └── yeoman.png ├── index.html ├── scripts │ ├── examples │ │ ├── ReactInfiniteListAppExample.js │ │ └── data │ │ │ └── people-dataset.js │ ├── infinite-list-item.js │ ├── react-infinite-list-beta.js │ └── react-infinite-list.js └── styles │ ├── main.css │ └── normalize.css ├── test ├── .jshintrc ├── helpers │ ├── phantomjs-shims.js │ └── react │ │ └── addons.js └── spec │ └── components │ └── ReactInfiniteListApp.js ├── webpack.config.js └── webpack.dist.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bower_components 2 | /node_modules -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": false, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "false", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": false, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "white": true, 22 | "newcap": false, 23 | "globals": { 24 | "React": true 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /.yo-rc.json: -------------------------------------------------------------------------------- 1 | { 2 | "generator-react-webpack": { 3 | "styles-language": "sass" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var mountFolder = function (connect, dir) { 4 | return connect.static(require('path').resolve(dir)); 5 | }; 6 | 7 | var webpackDistConfig = require('./webpack.dist.config.js'), 8 | webpackDevConfig = require('./webpack.config.js'); 9 | 10 | module.exports = function (grunt) { 11 | // Let *load-grunt-tasks* require everything 12 | require('load-grunt-tasks')(grunt); 13 | 14 | // Read configuration from package.json 15 | var pkgConfig = grunt.file.readJSON('package.json'); 16 | 17 | grunt.initConfig({ 18 | pkg: pkgConfig, 19 | 20 | webpack: { 21 | options: webpackDistConfig, 22 | 23 | dist: { 24 | cache: false 25 | } 26 | }, 27 | 28 | 'webpack-dev-server': { 29 | options: { 30 | hot: true, 31 | port: 8000, 32 | webpack: webpackDevConfig, 33 | publicPath: '/assets/', 34 | contentBase: './<%= pkg.src %>/', 35 | }, 36 | 37 | start: { 38 | keepAlive: true, 39 | } 40 | }, 41 | 42 | connect: { 43 | options: { 44 | port: 8000 45 | }, 46 | 47 | dist: { 48 | options: { 49 | keepalive: true, 50 | middleware: function (connect) { 51 | return [ 52 | mountFolder(connect, pkgConfig.dist) 53 | ]; 54 | } 55 | } 56 | } 57 | }, 58 | 59 | open: { 60 | options: { 61 | delay: 500 62 | }, 63 | dev: { 64 | path: 'http://localhost:<%= connect.options.port %>/webpack-dev-server/' 65 | }, 66 | dist: { 67 | path: 'http://localhost:<%= connect.options.port %>/' 68 | } 69 | }, 70 | 71 | karma: { 72 | unit: { 73 | configFile: 'karma.conf.js' 74 | } 75 | }, 76 | 77 | copy: { 78 | dist: { 79 | files: [ 80 | // includes files within path 81 | { 82 | flatten: true, 83 | expand: true, 84 | src: ['<%= pkg.src %>/*'], 85 | dest: '<%= pkg.dist %>/', 86 | filter: 'isFile' 87 | }, 88 | { 89 | flatten: true, 90 | expand: true, 91 | src: ['<%= pkg.src %>/images/*'], 92 | dest: '<%= pkg.dist %>/images/' 93 | }, 94 | ] 95 | } 96 | }, 97 | 98 | clean: { 99 | dist: { 100 | files: [{ 101 | dot: true, 102 | src: [ 103 | '<%= pkg.dist %>' 104 | ] 105 | }] 106 | } 107 | } 108 | }); 109 | 110 | grunt.registerTask('serve', function (target) { 111 | if (target === 'dist') { 112 | return grunt.task.run(['build', 'open:dist', 'connect:dist']); 113 | } 114 | 115 | grunt.task.run([ 116 | 'open:dev', 117 | 'webpack-dev-server' 118 | ]); 119 | }); 120 | 121 | grunt.registerTask('test', ['karma']); 122 | 123 | grunt.registerTask('build', ['clean', 'copy', 'webpack']); 124 | 125 | grunt.registerTask('default', []); 126 | }; 127 | -------------------------------------------------------------------------------- /GulpFile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'), 2 | given = require('gulp-if'), 3 | jsx = require('gulp-react'), 4 | rename = require('gulp-rename'), 5 | minifyjs = require('gulp-uglify'), 6 | sourcemaps = require('gulp-sourcemaps'); 7 | 8 | gulp.task('build', function() { 9 | gulp.src('./src/react-infinite-list.jsx') 10 | .pipe(sourcemaps.init()) 11 | .pipe(jsx()) 12 | .pipe(sourcemaps.write('.')) 13 | .pipe(gulp.dest('dist')); 14 | }); 15 | 16 | gulp.task('buildproduction', function() { 17 | gulp.src("./src/react-infinite-list.jsx") 18 | .pipe(rename("react-infinite-list.min.jsx")) 19 | .pipe(jsx()) 20 | .pipe(minifyjs()) 21 | .pipe(gulp.dest('dist')); 22 | }) 23 | 24 | gulp.task('default', ['build', 'buildproduction']); 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [WARNING] Deprecated 2 | See other more up to date solutions : 3 | For example : https://github.com/bvaughn/react-virtualized 4 | 5 | react-infinite-list 6 | =================== 7 | 8 | This react component turn any list of react components into a infinite list. 9 | 10 | This idea is to keep the dom as small as possible by rendering only the elements that are currently in the viewport. 11 | The list accepts element with different sizes, that the tricky part. 12 | 13 | Tested on : 14 | - chrome latest 15 | - firefox latest 16 | - Safari mobile 7-8 17 | - Chrome Mobile 18 | - Android browser 19 | 20 | It perform better in browsers that trigger scroll events during scroll momentum 21 | 22 | 23 | 24 | Warning : The bigger the size difference between elements is, the more edge cases you will hit. 25 | 26 | 27 | Contribute & Build 28 | ------------------------- 29 | 30 | Build : grunt build 31 | 32 | Develop : grunt serve 33 | 34 | How does it work : 35 | ----------------- 36 | 37 | The very basic : 38 | Only element in the current viewport are in the dom. 39 | 40 | In details : 41 | On each rendering frame, the list check the scroll position if it's before the first rendered element it adds item before if it's after it add elements after. 42 | If it's way different, ( the new rendered elements and the previous ones do not overlap, it renders at an appoximation of the position based on items mean height. 43 | 44 | See demo here : http://ganmor.github.io/react-core-list 45 | 46 | 47 | 48 | Example of use : 49 | -------------- 50 | ``` 51 | 52 | {myListOfReactComponents} 53 | 54 | ``` 55 | Do not forget to add a key on your components ! 56 | 57 | Caveats : 58 | ------ 59 | - When hiting the bottom of the list we need to adjust this size according to real elements height 60 | - no support of ie, it just a matter of adding the ms prefix in the transform 61 | 62 | -------------------------------------------------------------------------------- /dist/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ganmor/react-core-list/5d3541488a8ffddd03b8f0fb4a58e13cc6dd4942/dist/favicon.ico -------------------------------------------------------------------------------- /dist/images/yeoman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ganmor/react-core-list/5d3541488a8ffddd03b8f0fb4a58e13cc6dd4942/dist/images/yeoman.png -------------------------------------------------------------------------------- /dist/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 |
15 |

If you can see this, something is broken (or JS is not enabled)!!.

16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (config) { 4 | config.set({ 5 | basePath: '', 6 | frameworks: ['jasmine'], 7 | files: [ 8 | 'test/helpers/**/*.js', 9 | 'test/spec/components/**/*.js' 10 | ], 11 | preprocessors: { 12 | 'test/spec/components/**/*.js': ['webpack'] 13 | }, 14 | webpack: { 15 | cache: true, 16 | module: { 17 | loaders: [{ 18 | test: /\.gif/, 19 | loader: 'url-loader?limit=10000&mimetype=image/gif' 20 | }, { 21 | test: /\.jpg/, 22 | loader: 'url-loader?limit=10000&mimetype=image/jpg' 23 | }, { 24 | test: /\.png/, 25 | loader: 'url-loader?limit=10000&mimetype=image/png' 26 | }, { 27 | test: /\.js$/, 28 | loader: 'jsx-loader?harmony' 29 | }, { 30 | test: /\.sass/, 31 | loader: 'style-loader!css-loader!sass-loader?outputStyle=expanded' 32 | }, { 33 | test: /\.css$/, 34 | loader: 'style-loader!css-loader' 35 | }] 36 | }, 37 | resolve: { 38 | alias: { 39 | 'styles': './src/styles', 40 | 'components': './src/scripts/components/' 41 | } 42 | } 43 | }, 44 | webpackServer: { 45 | stats: { 46 | colors: true 47 | } 48 | }, 49 | exclude: [], 50 | port: 8080, 51 | logLevel: config.LOG_INFO, 52 | colors: true, 53 | autoWatch: false, 54 | // Start these browsers, currently available: 55 | // - Chrome 56 | // - ChromeCanary 57 | // - Firefox 58 | // - Opera 59 | // - Safari (only Mac) 60 | // - PhantomJS 61 | // - IE (only Windows) 62 | browsers: ['PhantomJS'], 63 | reporters: ['progress'], 64 | captureTimeout: 60000, 65 | singleRun: true 66 | }); 67 | }; 68 | -------------------------------------------------------------------------------- /npm-debug.log: -------------------------------------------------------------------------------- 1 | 0 info it worked if it ends with ok 2 | 1 verbose cli [ 'C:\\Program Files\\nodejs\\\\node.exe', 3 | 1 verbose cli 'C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js', 4 | 1 verbose cli 'install' ] 5 | 2 info using npm@1.4.28 6 | 3 info using node@v0.10.33 7 | 4 verbose node symlink C:\Program Files\nodejs\\node.exe 8 | 5 error install Couldn't read dependencies 9 | 6 error Failed to parse json 10 | 6 error Unexpected token } 11 | 7 error File: c:\DEV\react-infinite-list\package.json 12 | 8 error Failed to parse package.json data. 13 | 8 error package.json must be actual JSON, not just JavaScript. 14 | 8 error 15 | 8 error This is not a bug in npm. 16 | 8 error Tell the package author to fix their package.json file. JSON.parse 17 | 9 error System Windows_NT 6.1.7601 18 | 10 error command "C:\\Program Files\\nodejs\\\\node.exe" "C:\\Program Files\\nodejs\\node_modules\\npm\\bin\\npm-cli.js" "install" 19 | 11 error cwd c:\DEV\react-infinite-list 20 | 12 error node -v v0.10.33 21 | 13 error npm -v 1.4.28 22 | 14 error file c:\DEV\react-infinite-list\package.json 23 | 15 error code EJSONPARSE 24 | 16 verbose exit [ 1, true ] 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-core-list", 3 | "author":"morganlaupies", 4 | "author": "Morgan Laupies (http://github.com/ganmor)", 5 | "version": "0.1.0", 6 | "description": "Yet another infinte list component. Handle elements of differents height, works well on mobile devices", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/ganmor/react-core-list.git" 10 | }, 11 | "keywords": [ 12 | "react", 13 | "infinite", 14 | "scroll", 15 | "list", 16 | "html5", 17 | "mixin", 18 | "javascript", 19 | "react-component" 20 | ], 21 | "private": true, 22 | "src": "src", 23 | "test": "test", 24 | "dist": "dist", 25 | "mainInput": "src/scripts/react-infinite-list", 26 | "mainOutput": "dist/assets/react-infinite-list", 27 | "homepage": "http://ganmor.github.io/react-infinite-list", 28 | "dependencies": { 29 | "json-loader": "^0.5.1", 30 | "react": "~0.12.2", 31 | "underscore": "^1.8.2" 32 | }, 33 | "devDependencies": { 34 | "grunt": "~0.4.5", 35 | "load-grunt-tasks": "~0.6.0", 36 | "grunt-contrib-connect": "~0.8.0", 37 | "webpack": "~1.4.3", 38 | "jsx-loader": "~0.12.2", 39 | "grunt-webpack": "~1.0.8", 40 | "style-loader": "~0.8.0", 41 | "url-loader": "~0.5.5", 42 | "css-loader": "~0.9.0", 43 | "karma-script-launcher": "~0.1.0", 44 | "karma-chrome-launcher": "~0.1.4", 45 | "karma-firefox-launcher": "~0.1.3", 46 | "karma-jasmine": "~0.1.5", 47 | "karma-phantomjs-launcher": "~0.1.3", 48 | "karma": "~0.12.21", 49 | "grunt-karma": "~0.8.3", 50 | "karma-webpack": "~1.2.2", 51 | "webpack-dev-server": "~1.6.5", 52 | "grunt-open": "~0.2.3", 53 | "jshint-loader": "~0.8.0", 54 | "jsxhint-loader": "~0.2.0", 55 | "grunt-contrib-copy": "~0.5.0", 56 | "grunt-contrib-clean": "~0.6.0", 57 | "sass-loader": "^0.3.1", 58 | "react-hot-loader": "^1.0.7" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ganmor/react-core-list/5d3541488a8ffddd03b8f0fb4a58e13cc6dd4942/src/favicon.ico -------------------------------------------------------------------------------- /src/images/yeoman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ganmor/react-core-list/5d3541488a8ffddd03b8f0fb4a58e13cc6dd4942/src/images/yeoman.png -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 14 |
15 |

If you can see this, something is broken (or JS is not enabled)!!.

16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/scripts/examples/ReactInfiniteListAppExample.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var React = require('react/addons'); 4 | var ReactTransitionGroup = React.addons.TransitionGroup; 5 | 6 | // CSS 7 | require('../../styles/normalize.css'); 8 | require('../../styles/main.css'); 9 | 10 | var ReactInfiniteList = require('../react-infinite-list'); 11 | var _ = require('underscore'); 12 | 13 | 14 | var UpdateButton = React.createClass({ 15 | render : function () { 16 | return ( 17 | 18 | ); 19 | } 20 | 21 | }); 22 | 23 | var Person = React.createClass({ 24 | 25 | render : function () { 26 | 27 | var height = 75 + Math.random()* 40; 28 | this.color = this.props.element.id%2==0 ? this.props.colors[0] : this.props.element.id%3==0 ? this.props.colors[1] : this.props.colors[2] ; 29 | 30 | return (
31 |
32 | 33 | {this.props.element.name} - {this.props.element.company} 34 |
{this.props.element.about}
35 |
36 |
); 37 | } 38 | }); 39 | 40 | 41 | function getRandomColor() { 42 | var letters = '0123456789ABCDEF'.split(''); 43 | var color = '#'; 44 | for (var i = 0; i < 6; i++ ) { 45 | color += letters[Math.floor(Math.random() * 16)]; 46 | } 47 | return color; 48 | } 49 | 50 | var PeopleList = React.createClass({ 51 | getInitialState : function () { 52 | return { colors : ['#2a3fe5', '#949DEA', 'white']}; 53 | }, 54 | shuffleColors : function () { 55 | this.setState({ 56 | colors : [getRandomColor(), getRandomColor(), getRandomColor()] 57 | }); 58 | }, 59 | render : function () { 60 | 61 | var elements = _.map(this.props.jsonList, function (element) { 62 | return (); 63 | }, this); 64 | 65 | var height = window.innerHeight; 66 | 67 | return ( 68 |
69 | 70 | {elements} 71 | 72 |
73 | ); 74 | } 75 | 76 | }); 77 | 78 | var jsonData = require('./data/people-dataset'); 79 | var mountNode = document.querySelector('#placeholder'); 80 | React.render(, document.getElementById('content')); // jshint ignore:line 81 | 82 | module.exports = PeopleList; 83 | -------------------------------------------------------------------------------- /src/scripts/infinite-list-item.js: -------------------------------------------------------------------------------- 1 | var React = require('react'), 2 | _ = require('underscore'); 3 | 4 | 5 | /* 6 | * Wrapper for children components 7 | * Used to cache the size of elements 8 | */ 9 | var InfiniteListItem = React.createClass({ 10 | 11 | 12 | getInitialState : function () { 13 | return {}; 14 | }, 15 | render : function () { 16 | 17 | var style; 18 | 19 | if (!this.props.rendered) { return false; } 20 | 21 | style = {}; 22 | style.overflow = 'hidden' 23 | 24 | return (
{this.props.children}
); 25 | } 26 | }); 27 | 28 | 29 | module.exports = InfiniteListItem; 30 | -------------------------------------------------------------------------------- /src/scripts/react-infinite-list-beta.js: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | 3 | 4 | var AbsoluteElement = React.createClass({ 5 | 6 | proptTypes : { 7 | startRender : React.proptType.Number, // The minium rendereded position 8 | endeRender : React.proptType.Number // The maximum rendered position 9 | }, 10 | 11 | componentDidMount : function () { 12 | // TODO report size ? 13 | this.props. 14 | 15 | }, 16 | 17 | render : function { 18 | return ( 19 |
20 | 21 |
); 22 | } 23 | }); 24 | 25 | 26 | var InfiniteList = React.createClass({ 27 | 28 | // Local cache of scrollTop 29 | _scrollTop: 0, 30 | 31 | ready: function() { 32 | this._boundScrollHandler = this.scrollHandler.bind(this); 33 | this._boundPositionItems = this._positionItems.bind(this); 34 | this._oldMulti = this.multi; 35 | this._oldSelectionEnabled = this.selectionEnabled; 36 | this._virtualStart = 0; 37 | this._virtualCount = 0; 38 | this._physicalStart = 0; 39 | this._physicalOffset = 0; 40 | this._physicalSize = 0; 41 | this._physicalSizes = []; 42 | this._physicalAverage = 0; 43 | this._itemSizes = []; 44 | this._dividerSizes = []; 45 | this._repositionedItems = []; 46 | this._aboveSize = 0; 47 | this._nestedGroups = false; 48 | this._groupStart = 0; 49 | this._groupStartIndex = 0; 50 | }, 51 | 52 | attached: function() { 53 | this.isAttached = true; 54 | this.template = this.querySelector('template'); 55 | if (!this.template.bindingDelegate) { 56 | this.template.bindingDelegate = this.element.syntax; 57 | } 58 | this.resizableAttachedHandler(); 59 | }, 60 | 61 | detached: function() { 62 | this.isAttached = false; 63 | if (this._target) { 64 | this._target.removeEventListener('scroll', this._boundScrollHandler); 65 | } 66 | this.resizableDetachedHandler(); 67 | }, 68 | 69 | /** 70 | * To be called by the user when the list is manually resized 71 | * or shown after being hidden. 72 | * 73 | * @method updateSize 74 | */ 75 | 76 | updateSize: function() { 77 | if (!this._positionPending && !this._needItemInit) { 78 | this._resetIndex(this._getFirstVisibleIndex() || 0); 79 | this.initialize(); 80 | } 81 | }, 82 | 83 | _resetSelection: function() { 84 | if (((this._oldMulti != this.multi) && !this.multi) || 85 | ((this._oldSelectionEnabled != this.selectionEnabled) && 86 | !this.selectionEnabled)) { 87 | this._clearSelection(); 88 | this.refresh(); 89 | } else { 90 | this.selection = this.$.selection.getSelection(); 91 | } 92 | this._oldMulti = this.multi; 93 | this._oldSelectionEnabled = this.selectionEnabled; 94 | }, 95 | 96 | // Adjust virtual start index based on changes to backing data 97 | _adjustVirtualIndex: function(splices, group) { 98 | if (this._targetSize === 0) { 99 | return; 100 | } 101 | var totalDelta = 0; 102 | for (var i=0; i= this._virtualStart) { 112 | break; 113 | } 114 | var delta = Math.max(s.addedCount - s.removed.length, idx - this._virtualStart); 115 | totalDelta += delta; 116 | this._physicalStart += delta; 117 | this._virtualStart += delta; 118 | if (this._grouped) { 119 | if (group) { 120 | gitem = s.index; 121 | } else { 122 | var g = this.groupForVirtualIndex(s.index); 123 | gidx = g.group; 124 | gitem = g.groupIndex; 125 | } 126 | if (gidx == this._groupStart && gitem < this._groupStartIndex) { 127 | this._groupStartIndex += delta; 128 | } 129 | } 130 | } 131 | // Adjust offset/scroll position based on total number of items changed 132 | if (this._virtualStart < this._physicalCount) { 133 | this._resetIndex(this._getFirstVisibleIndex() || 0); 134 | } else { 135 | totalDelta = Math.max((totalDelta / this._rowFactor) * this._physicalAverage, -this._physicalOffset); 136 | this._physicalOffset += totalDelta; 137 | this._scrollTop = this.setScrollTop(this._scrollTop + totalDelta); 138 | } 139 | }, 140 | _updateSelection: function(splices) { 141 | for (var i=0; i 0) { 483 | var groupMax = this.getGroupLen() - this._groupStartIndex - 1; 484 | if (inc > groupMax) { 485 | inc -= (groupMax + 1); 486 | this._groupStart++; 487 | this._groupStartIndex = 0; 488 | } else { 489 | this._groupStartIndex += inc; 490 | inc = 0; 491 | } 492 | } 493 | while (inc < 0) { 494 | if (-inc > this._groupStartIndex) { 495 | inc += this._groupStartIndex; 496 | this._groupStart--; 497 | this._groupStartIndex = this.getGroupLen(); 498 | } else { 499 | this._groupStartIndex += inc; 500 | inc = this.getGroupLen(); 501 | } 502 | } 503 | } 504 | // In grid mode, virtualIndex must alway start on a row start! 505 | if (this.grid) { 506 | if (this._grouped) { 507 | inc = this._groupStartIndex % this._rowFactor; 508 | } else { 509 | inc = this._virtualStart % this._rowFactor; 510 | } 511 | if (inc) { 512 | this.changeStartIndex(-inc); 513 | } 514 | } 515 | }, 516 | getRowCount: function(dir) { 517 | if (!this.grid) { 518 | return dir; 519 | } else if (!this._grouped) { 520 | return dir * this._rowFactor; 521 | } else { 522 | if (dir < 0) { 523 | if (this._groupStartIndex > 0) { 524 | return -Math.min(this._rowFactor, this._groupStartIndex); 525 | } else { 526 | var prevLen = this.getGroupLen(this._groupStart-1); 527 | return -Math.min(this._rowFactor, prevLen % this._rowFactor || this._rowFactor); 528 | } 529 | } else { 530 | return Math.min(this._rowFactor, this.getGroupLen() - this._groupStartIndex); 531 | } 532 | } 533 | }, 534 | _virtualToPhysical: function(virtualIndex) { 535 | var physicalIndex = (virtualIndex - this._physicalStart) % this._physicalCount; 536 | return physicalIndex < 0 ? this._physicalCount + physicalIndex : physicalIndex; 537 | }, 538 | groupForVirtualIndex: function(virtual) { 539 | if (!this._grouped) { 540 | return {}; 541 | } else { 542 | var group; 543 | for (group=0; group virtual) { 546 | break; 547 | } else { 548 | virtual -= groupLen; 549 | } 550 | } 551 | return {group: group, groupIndex: virtual }; 552 | } 553 | }, 554 | virtualIndexForGroup: function(group, groupIndex) { 555 | groupIndex = groupIndex ? Math.min(groupIndex, this.getGroupLen(group)) : 0; 556 | group--; 557 | while (group >= 0) { 558 | groupIndex += this.getGroupLen(group--); 559 | } 560 | return groupIndex; 561 | }, 562 | dataForIndex: function(virtual, group, groupIndex) { 563 | if (this.data) { 564 | if (this._nestedGroups) { 565 | if (virtual < this._virtualCount) { 566 | return this.data[group][groupIndex]; 567 | } 568 | } else { 569 | return this.data[virtual]; 570 | } 571 | } 572 | }, 573 | // Refresh the list at the current scroll position. 574 | refresh: function() { 575 | var i, deltaCount; 576 | // Determine scroll position & any scrollDelta that may have occurred 577 | var lastScrollTop = this._scrollTop; 578 | this._scrollTop = this.getScrollTop(); 579 | var scrollDelta = this._scrollTop - lastScrollTop; 580 | this._dir = scrollDelta < 0 ? -1 : scrollDelta > 0 ? 1 : 0; 581 | // Adjust virtual items and positioning offset if scroll occurred 582 | if (Math.abs(scrollDelta) > Math.max(this._physicalSize, this._targetSize)) { 583 | // Random access to point in list: guess new index based on average size 584 | deltaCount = Math.round((scrollDelta / this._physicalAverage) * this._rowFactor); 585 | deltaCount = Math.max(deltaCount, -this._virtualStart); 586 | deltaCount = Math.min(deltaCount, this._virtualCount - this._virtualStart - 1); 587 | this._physicalOffset += Math.max(scrollDelta, -this._physicalOffset); 588 | this.changeStartIndex(deltaCount); 589 | // console.log(this._scrollTop, 'Random access to ' + this._virtualStart, this._physicalOffset); 590 | } else { 591 | // Incremental movement: adjust index by flipping items 592 | var base = this._aboveSize + this._physicalOffset; 593 | var margin = 0.3 * Math.max((this._physicalSize - this._targetSize, this._physicalSize)); 594 | this._upperBound = base + margin; 595 | this._lowerBound = base + this._physicalSize - this._targetSize - margin; 596 | var flipBound = this._dir > 0 ? this._upperBound : this._lowerBound; 597 | if (((this._dir > 0 && this._scrollTop > flipBound) || 598 | (this._dir < 0 && this._scrollTop < flipBound))) { 599 | var flipSize = Math.abs(this._scrollTop - flipBound); 600 | for (i=0; (i 0) && 601 | ((this._dir < 0 && this._virtualStart > 0) || 602 | (this._dir > 0 && this._virtualStart < this._virtualCount-this._physicalCount)); i++) { 603 | var idx = this._virtualToPhysical(this._dir > 0 ? 604 | this._virtualStart : 605 | this._virtualStart + this._physicalCount -1); 606 | var size = this._physicalSizes[idx]; 607 | flipSize -= size; 608 | var cnt = this.getRowCount(this._dir); 609 | // console.log(this._scrollTop, 'flip ' + (this._dir > 0 ? 'down' : 'up'), cnt, this._virtualStart, this._physicalOffset); 610 | if (this._dir > 0) { 611 | // When scrolling down, offset is adjusted based on previous item's size 612 | this._physicalOffset += size; 613 | // console.log(' ->', this._virtualStart, size, this._physicalOffset); 614 | } 615 | this.changeStartIndex(cnt); 616 | if (this._dir < 0) { 617 | this._repositionedItems.push(this._virtualStart); 618 | } 619 | } 620 | } 621 | } 622 | // Assign data to items lazily if scrolling, otherwise force 623 | if (this._updateItems(!scrollDelta)) { 624 | // Position items after bindings resolve (method varies based on O.o impl) 625 | if (Observer.hasObjectObserve) { 626 | this.async(this._boundPositionItems); 627 | } else { 628 | Platform.flush(); 629 | Platform.endOfMicrotask(this._boundPositionItems); 630 | } 631 | } 632 | }, 633 | _updateItems: function(force) { 634 | var i, virtualIndex, physicalIndex; 635 | var needsReposition = false; 636 | var groupIndex = this._groupStart; 637 | var groupItemIndex = this._groupStartIndex; 638 | for (i = 0; i < this._physicalCount; ++i) { 639 | virtualIndex = this._virtualStart + i; 640 | physicalIndex = this._virtualToPhysical(virtualIndex); 641 | // Update physical item with new user data and list metadata 642 | needsReposition = 643 | this._updateItemData(force, physicalIndex, virtualIndex, groupIndex, groupItemIndex) || needsReposition; 644 | // Increment 645 | groupItemIndex++; 646 | if (this.groups && groupIndex < this.groups.length - 1) { 647 | if (groupItemIndex >= this.getGroupLen(groupIndex)) { 648 | groupItemIndex = 0; 649 | groupIndex++; 650 | } 651 | } 652 | } 653 | return needsReposition; 654 | }, 655 | _positionItems: function() { 656 | var i, virtualIndex, physicalIndex, physicalItem; 657 | // Measure 658 | this.updateMetrics(); 659 | // Pre-positioning tasks 660 | if (this._dir < 0) { 661 | // When going up, remove offset after measuring size for 662 | // new data for item being moved from bottom to top 663 | while (this._repositionedItems.length) { 664 | virtualIndex = this._repositionedItems.pop(); 665 | physicalIndex = this._virtualToPhysical(virtualIndex); 666 | this._physicalOffset -= this._physicalSizes[physicalIndex]; 667 | // console.log(' <-', virtualIndex, this._physicalSizes[physicalIndex], this._physicalOffset); 668 | } 669 | // Adjust scroll position to home into top when going up 670 | if (this._scrollTop + this._targetSize < this._viewportSize) { 671 | this._updateScrollPosition(this._scrollTop); 672 | } 673 | } 674 | // Position items 675 | var divider, upperBound, lowerBound; 676 | var rowx = 0; 677 | var x = this._rowMargin; 678 | var y = this._physicalOffset; 679 | var lastHeight = 0; 680 | for (i = 0; i < this._physicalCount; ++i) { 681 | // Calculate indices 682 | virtualIndex = this._virtualStart + i; 683 | physicalIndex = this._virtualToPhysical(virtualIndex); 684 | physicalItem = this._physicalItems[physicalIndex]; 685 | // Position divider 686 | if (physicalItem._isDivider) { 687 | if (rowx !== 0) { 688 | y += lastHeight; 689 | rowx = 0; 690 | } 691 | divider = this._physicalDividers[physicalIndex]; 692 | x = this._rowMargin; 693 | if (divider && (divider._translateX != x || divider._translateY != y)) { 694 | divider.style.opacity = 1; 695 | if (this.grid) { 696 | divider.style.width = this.width * this._rowFactor + 'px'; 697 | } 698 | divider.style.transform = divider.style.webkitTransform = 699 | 'translate3d(' + x + 'px,' + y + 'px,0)'; 700 | divider._translateX = x; 701 | divider._translateY = y; 702 | } 703 | y += this._dividerSizes[physicalIndex]; 704 | } 705 | // Position item 706 | if (physicalItem._translateX != x || physicalItem._translateY != y) { 707 | physicalItem.style.opacity = 1; 708 | physicalItem.style.transform = physicalItem.style.webkitTransform = 709 | 'translate3d(' + x + 'px,' + y + 'px,0)'; 710 | physicalItem._translateX = x; 711 | physicalItem._translateY = y; 712 | } 713 | // Increment offsets 714 | lastHeight = this._itemSizes[physicalIndex]; 715 | if (this.grid) { 716 | rowx++; 717 | if (rowx >= this._rowFactor) { 718 | rowx = 0; 719 | y += lastHeight; 720 | } 721 | x = this._rowMargin + rowx * this.width; 722 | } else { 723 | y += lastHeight; 724 | } 725 | } 726 | if (this._scrollTop >= 0) { 727 | this._updateViewportHeight(); 728 | } 729 | }, 730 | _updateViewportHeight: function() { 731 | var remaining = Math.max(this._virtualCount - this._virtualStart - this._physicalCount, 0); 732 | remaining = Math.ceil(remaining / this._rowFactor); 733 | var vs = this._physicalOffset + this._physicalSize + remaining * this._physicalAverage; 734 | if (this._viewportSize != vs) { 735 | // console.log(this._scrollTop, 'adjusting viewport height', vs - this._viewportSize, vs); 736 | this._viewportSize = vs; 737 | this.$.viewport.style.height = this._viewportSize + 'px'; 738 | this.syncScroller(); 739 | } 740 | }, 741 | _updateScrollPosition: function(scrollTop) { 742 | var deltaHeight = this._virtualStart === 0 ? this._physicalOffset : 743 | Math.min(scrollTop + this._physicalOffset, 0); 744 | if (deltaHeight) { 745 | // console.log(scrollTop, 'adjusting scroll pos', this._virtualStart, -deltaHeight, scrollTop - deltaHeight); 746 | if (this.adjustPositionAllowed) { 747 | this._scrollTop = this.setScrollTop(scrollTop - deltaHeight); 748 | } 749 | this._physicalOffset -= deltaHeight; 750 | } 751 | }, 752 | // list selection 753 | tapHandler: function(e) { 754 | var n = e.target; 755 | var p = e.path; 756 | if (!this.selectionEnabled || (n === this)) { 757 | return; 758 | } 759 | requestAnimationFrame(function() { 760 | // Gambit: only select the item if the tap wasn't on a focusable child 761 | // of the list (since anything with its own action should be focusable 762 | // and not result in result in list selection). To check this, we 763 | // asynchronously check that shadowRoot.activeElement is null, which 764 | // means the tapped item wasn't focusable. On polyfill where 765 | // activeElement doesn't follow the data-hinding part of the spec, we 766 | // can check that document.activeElement is the list itself, which will 767 | // catch focus in lieu of the tapped item being focusable, as we make 768 | // the list focusable (tabindex="-1") for this purpose. Note we also 769 | // allow the list items themselves to be focusable if desired, so those 770 | // are excluded as well. 771 | var active = window.ShadowDOMPolyfill ? 772 | wrap(document.activeElement) : this.shadowRoot.activeElement; 773 | if (active && (active != this) && (active.parentElement != this) && 774 | (document.activeElement != document.body)) { 775 | return; 776 | } 777 | // Unfortunately, Safari does not focus certain form controls via mouse, 778 | // so we also blacklist input, button, & select 779 | // (https://bugs.webkit.org/show_bug.cgi?id=118043) 780 | if ((p[0].localName == 'input') || 781 | (p[0].localName == 'button') || 782 | (p[0].localName == 'select')) { 783 | return; 784 | } 785 | var model = n.templateInstance && n.templateInstance.model; 786 | if (model) { 787 | var data = this.dataForIndex(model.index, model.groupIndex, model.groupItemIndex); 788 | var item = this._physicalItems[model.physicalIndex]; 789 | if (!this.multi && data == this.selection) { 790 | this.$.selection.select(null); 791 | } else { 792 | this.$.selection.select(data); 793 | } 794 | this.asyncFire('core-activate', {data: data, item: item}); 795 | } 796 | }.bind(this)); 797 | }, 798 | selectedHandler: function(e, detail) { 799 | this.selection = this.$.selection.getSelection(); 800 | var id = this.indexesForData(detail.item); 801 | // TODO(sorvell): we should be relying on selection to store the 802 | // selected data but we want to optimize for lookup. 803 | this._selectedData.set(detail.item, detail.isSelected); 804 | if (id.physical >= 0 && id.virtual >= 0) { 805 | this.refresh(); 806 | } 807 | }, 808 | /** 809 | * Select the list item at the given index. 810 | * 811 | * @method selectItem 812 | * @param {number} index 813 | */ 814 | selectItem: function(index) { 815 | if (!this.selectionEnabled) { 816 | return; 817 | } 818 | var data = this.data[index]; 819 | if (data) { 820 | this.$.selection.select(data); 821 | } 822 | }, 823 | /** 824 | * Set the selected state of the list item at the given index. 825 | * 826 | * @method setItemSelected 827 | * @param {number} index 828 | * @param {boolean} isSelected 829 | */ 830 | setItemSelected: function(index, isSelected) { 831 | var data = this.data[index]; 832 | if (data) { 833 | this.$.selection.setItemSelected(data, isSelected); 834 | } 835 | }, 836 | indexesForData: function(data) { 837 | var virtual = -1; 838 | var groupsLen = 0; 839 | if (this._nestedGroups) { 840 | for (var i=0; i= this._scrollTop - this._aboveSize) { 883 | return virtualIndex; 884 | } 885 | } 886 | }, 887 | _resetIndex: function(index) { 888 | index = Math.min(index, this._virtualCount-1); 889 | index = Math.max(index, 0); 890 | this.changeStartIndex(index - this._virtualStart); 891 | this._scrollTop = this.setScrollTop(this._aboveSize + (index / this._rowFactor) * this._physicalAverage); 892 | this._physicalOffset = this._scrollTop - this._aboveSize; 893 | this._dir = 0; 894 | }, 895 | /** 896 | * Scroll to an item. 897 | * 898 | * Note, when grouping is used, the index is based on the 899 | * total flattened number of items. For scrolling to an item 900 | * within a group, use the `scrollToGroupItem` API. 901 | * 902 | * @method scrollToItem 903 | * @param {number} index 904 | */ 905 | scrollToItem: function(index) { 906 | this.scrollToGroupItem(null, index); 907 | }, 908 | /** 909 | * Scroll to a group. 910 | * 911 | * @method scrollToGroup 912 | * @param {number} group 913 | */ 914 | scrollToGroup: function(group) { 915 | this.scrollToGroupItem(group, 0); 916 | }, 917 | /** 918 | * Scroll to an item within a group. 919 | * 920 | * @method scrollToGroupItem 921 | * @param {number} group 922 | * @param {number} index 923 | */ 924 | scrollToGroupItem: function(group, index) { 925 | if (group != null) { 926 | index = this.virtualIndexForGroup(group, index); 927 | } 928 | this._resetIndex(index); 929 | this.refresh(); 930 | } 931 | 932 | render : function () { 933 | 934 | var height = (this.props.children.length * this.props.itemHeight || this.defaultItemHeight); 935 | var containerSize = { 936 | height : height 937 | }; 938 | 939 | return ( 940 |
941 | {this.props.children.length} {containerSize.height} 942 |
); 943 | } 944 | }); 945 | 946 | module.exports = InfiniteList; 947 | 948 | 949 | -------------------------------------------------------------------------------- /src/scripts/react-infinite-list.js: -------------------------------------------------------------------------------- 1 | var getWindowHeight, 2 | nextFrame, 3 | cancelFrame, 4 | DEFAULTS, 5 | InfiniteListComponent, 6 | InfiniteListItem = require('./infinite-list-item.js'), 7 | React = require('react'), 8 | _ = require('underscore'); 9 | 10 | 11 | getWindowHeight = function() { return window.innerHeight }; 12 | 13 | nextFrame = (function () { 14 | return window.requestAnimationFrame || 15 | window.webkitRequestAnimationFrame || 16 | window.mozRequestAnimationFrame || 17 | window.oRequestAnimationFrame || 18 | window.msRequestAnimationFrame || function (callback) { 19 | return window.setTimeout(callback, 1); 20 | }; 21 | })(); 22 | 23 | cancelFrame = (function () { 24 | return window.cancelRequestAnimationFrame || 25 | window.webkitCancelAnimationFrame || 26 | window.webkitCancelRequestAnimationFrame || 27 | window.mozCancelRequestAnimationFrame || 28 | window.oCancelRequestAnimationFrame || 29 | window.msCancelRequestAnimationFrame || 30 | window.clearTimeout; 31 | })(); 32 | 33 | 34 | // Default Config 35 | DEFAULTS = {}; 36 | DEFAULTS.DEFAULT_ITEM_HEIGHT = 95; 37 | DEFAULTS.MARGIN_BOTTOM = 0; 38 | DEFAULTS.MARGIN_OUT_SCREEN = 10 * DEFAULTS.DEFAULT_ITEM_HEIGHT; 39 | 40 | 41 | function getScrollParent(target) { 42 | if (target === window) { 43 | return window; 44 | } 45 | 46 | for (var el = target; el; el = el.parentElement) { 47 | var overflowY = window.getComputedStyle(el).overflowY; 48 | if (overflowY === 'auto' || overflowY === 'scroll') { return el; } 49 | } 50 | return window; 51 | } 52 | 53 | 54 | 55 | var InfiniteListComponent = React.createClass({ 56 | 57 | // 58 | // Life Cycle 59 | // 60 | 61 | componentDidMount : function () { 62 | this._configuration = _.extend(DEFAULTS, this.props.config); 63 | this.setState({ 64 | initialOffsetTop : this.getDOMNode().offsetTop, 65 | viewPortHeight : (getWindowHeight() - 60 - this._configuration.MARGIN_BOTTOM), 66 | listHeight : this.getListFullHeight() 67 | }); 68 | 69 | this.keepDisplayInSync(); 70 | }, 71 | 72 | componentWillUnmount : function () { 73 | this.tearDownDisplayUpdate(); 74 | }, 75 | 76 | getInitialState : function (){ 77 | return { 78 | startIdx : 0, 79 | endIdx : 30, /* FIXME */ 80 | bufferStartIdx : 0, 81 | bufferEndIdx : 40, 82 | bufferLength : 10, 83 | offsetTop : 0, 84 | differential : false, 85 | realSizes : {}, 86 | oldScroll : 0 87 | }; 88 | }, 89 | 90 | componentWillReceiveProps : function () { 91 | this.setState({ 92 | viewPortHeight : (getWindowHeight() - this.getDOMNode().offsetTop - this._configuration.MARGIN_BOTTOM), 93 | listHeight : this.getListFullHeight() 94 | }); 95 | }, 96 | 97 | componentWillUpdate : function (nextProps, nextState) { 98 | 99 | var bufferheight, removeElementsHeight, offsetCorrection; 100 | 101 | removeElementsHeight = 0; 102 | 103 | // Not in diff mode, nothing to do 104 | if (!nextState.differential) { 105 | nextState.startIdx = Math.max(0, nextState.startIdx); 106 | if (nextState.offsetTop > 0 && nextState.startIdx <= 0) { 107 | nextState.offsetTop = 0; 108 | } 109 | return; 110 | } 111 | 112 | // #1: Get buffer size 113 | bufferheight = this.refs['buffered-elements'].getDOMNode().offsetHeight; 114 | 115 | // #2: Get the remove elements height 116 | if (nextState.direction === 'up') { 117 | 118 | var removeElements = this.props.children.slice(nextState.endIdx, this.state.endIdx); 119 | _.each(removeElements, function (el, idx) { 120 | var refName = "infinite-list-item" + idx; 121 | el = this.refs[refName]; 122 | try { 123 | removeElementsHeight = removeElementsHeight + el.getDOMNode().offsetHeight; 124 | } catch(e) { 125 | console.log(e); 126 | } 127 | }, this); 128 | 129 | nextState.offsetTop = this.state.offsetTop - bufferheight; 130 | // console.log('Going up, new offset top will be : ' + nextState.offsetTop + ' ( ' + this.state.offsetTop + ',' + bufferheight + ')'); 131 | 132 | } else if (nextState.direction === 'down') { 133 | 134 | var removeElements = this.props.children.slice(this.state.startIdx, nextState.startIdx); 135 | _.each(removeElements, function (el, idx) { 136 | var refName = "infinite-list-item" + idx; 137 | el = this.refs[refName]; 138 | 139 | // console.log('Will remove item with index ' + idx); 140 | removeElementsHeight = removeElementsHeight + el.getDOMNode().offsetHeight; 141 | }, this); 142 | 143 | nextState.offsetTop = this.state.offsetTop + removeElementsHeight; 144 | 145 | // console.log('Going down, new offset top will be : ' + nextState.offsetTop + ' ( ' + this.state.offsetTop + ',' + removeElementsHeight + ')'); 146 | // console.log('New start idx will be ' + nextState.startIdx); 147 | } 148 | 149 | //offsetCorrection = (nextState.startIdx < this.state.startIdx) ? -bufferSize + removeElementsSize 150 | 151 | // Cleanup 152 | nextState.differential = false; 153 | nextState.direction = null; 154 | 155 | // Edges cases to be handled in a much cleaner way 156 | if (nextState.offsetTop < 0) { 157 | return this.approximateInsertion(0); 158 | } 159 | 160 | nextState.startIdx = Math.max(0, nextState.startIdx); 161 | if (nextState.offsetTop > 0 && nextState.startIdx <= 0) { 162 | nextState.offsetTop = 0; 163 | } 164 | 165 | }, 166 | 167 | componentDidUpdate : function (prevProps, prevState) { 168 | // Must result for next compare 169 | if (this.isMounted() && this.refs['list-rendered'] && this.refs['list-rendered'].getDOMNode()) { 170 | this.state.listRealHeight = this.refs['list-rendered'].getDOMNode().scrollHeight; 171 | this.state.offsetBottom = this.state.offsetTop + this.state.listRealHeight; 172 | } 173 | }, 174 | 175 | render : function () { 176 | 177 | var wrapperStyle, positionningDivStyle, listSizerStyle, startIdx, endIdx, instance_; 178 | 179 | if (!this.state.viewPortHeight) { return (
); } 180 | 181 | instance_ = this; 182 | 183 | wrapperStyle = { 184 | width:'100%', 185 | position:'relative' 186 | }; 187 | 188 | positionningDivStyle = { 189 | top: (this.state.offsetTop||0) + 'px', 190 | position:'absolute', 191 | width:'100%', 192 | WebkitTransform:'translateZ(0)' 193 | }; 194 | 195 | listSizerStyle = { 196 | height : this.state.listHeight 197 | }; 198 | 199 | startIdx = this.state.startIdx; 200 | endIdx = this.state.endIdx; 201 | 202 | var renderedElements = this.props.children.slice(startIdx, endIdx); 203 | var bufferedElements = this.props.children.slice(this.state.startBufferIdx||0, this.state.endBufferIdx||0); 204 | 205 | var infiniteChildren = _.map(renderedElements, function (ReactCpnt, idx) { 206 | var refName = "infinite-list-item" + idx; 207 | return ( 208 | 209 | {ReactCpnt} 210 | 211 | ); 212 | }, this); 213 | 214 | var bufferedElements = _.map(bufferedElements, function (ReactCpnt, idx) { 215 | return ( 216 | 217 | {ReactCpnt} 218 | 219 | ); 220 | }, this); 221 | 222 | var bufferStyle = { 223 | position : 'absolute', 224 | top : '-10000px;', 225 | padding: '0', 226 | margin: '0', 227 | border: '0', 228 | }; 229 | 230 | return ( 231 |
232 |
233 |
234 | {infiniteChildren} 235 |
236 |
237 |
{bufferedElements}
238 |
); 239 | }, 240 | 241 | 242 | /* 243 | * Read the scroll position perodically to update display when neededed 244 | * Synchronise update with browser frames 245 | */ 246 | keepDisplayInSync : function () { 247 | var instance_ = this; 248 | getScrollParent(this.getDOMNode()).addEventListener('scroll', this.checkDisplay); 249 | getScrollParent(this.getDOMNode()).addEventListener('touchend', this.checkDisplay); 250 | }, 251 | 252 | tearDownDisplayUpdate : function () { 253 | var instance_ = this; 254 | getScrollParent(this.getDOMNode()).removeEventListener('scroll', this.checkDisplay); 255 | getScrollParent(this.getDOMNode()).removeEventListener('touchend', this.checkDisplay); 256 | }, 257 | 258 | checkDisplay : function () { 259 | var instance_ = this; 260 | if (!this.isMounted()){ return; } 261 | instance_.rePositionList.call(instance_, instance_.getScroll()); 262 | }, 263 | 264 | getScroll : function () { 265 | return getScrollParent(this.getDOMNode()).scrollTop; 266 | }, 267 | 268 | getListFullHeight: function () { 269 | var fullLength = this.props.children.length; 270 | return (fullLength * this._configuration.DEFAULT_ITEM_HEIGHT) || 0; 271 | }, 272 | 273 | // 274 | // Real impl 275 | // 276 | rePositionList: function (newScroll) { 277 | 278 | var isGoingUp = newScroll < this.state.oldScroll; 279 | var isGoingDown = newScroll > this.state.oldScroll; 280 | var isGoingFlat = newScroll === this.state.oldScroll; 281 | 282 | 283 | 284 | var isBeforeTop = (newScroll < this.state.offsetTop); 285 | var isAfterBottom = ((newScroll + this.state.viewPortHeight) > this.state.offsetBottom); 286 | 287 | var hasReachedTopOfTheList = (this.state.startIdx == 0); 288 | var hasReachedBottomOfTheList = (this.state.endIdx >= (this.props.children.length)); 289 | 290 | 291 | if ((isGoingUp || isGoingFlat) && hasReachedTopOfTheList) { 292 | return; 293 | } 294 | 295 | if ((isGoingDown || isGoingFlat) && hasReachedBottomOfTheList) { 296 | return; 297 | } 298 | 299 | 300 | if ( (isBeforeTop && isGoingUp) || (isAfterBottom && isGoingDown) ) { 301 | this.approximateInsertion(newScroll); 302 | } else if (isGoingDown) { 303 | this.repositionListDown(newScroll); 304 | } else if (isGoingUp) { 305 | this.repositionListUp(newScroll); 306 | } 307 | 308 | }, 309 | 310 | repositionListUp: function (newScroll) { 311 | 312 | var targetOffsetTop, move, maxMove, shouldMove, willReachTop, isTooCloseToBottom, instance_; 313 | 314 | instance_ = this; 315 | 316 | 317 | // Compute expected destination 318 | shouldMove = newScroll < (this.state.offsetTop + 2 * this._configuration.DEFAULT_ITEM_HEIGHT); 319 | targetOffsetTop = Math.max((newScroll - this._configuration.MARGIN_OUT_SCREEN), 0); 320 | 321 | // Compute the move 322 | move = (this.state.offsetTop - targetOffsetTop) / this._configuration.DEFAULT_ITEM_HEIGHT; 323 | 324 | // Beginning of line 325 | willReachTop = (this.state.startIdx - move) <= 0; 326 | 327 | if (shouldMove || willReachTop) { 328 | move = Math.min(move, this.state.startIdx); // Easy one 329 | this.launchRenderingCycle('up', move); 330 | } 331 | 332 | }, 333 | 334 | 335 | repositionListDown: function (newScroll) { 336 | 337 | var instance_, targetOffsetBottom, move, shouldMove, maxMove, isTooCloseToTop, willReachedBottom, maximumMove; 338 | 339 | instance_ = this; 340 | 341 | 342 | // Where list is supposed to end now 343 | targetOffsetBottom = newScroll + this.state.viewPortHeight + this._configuration.MARGIN_OUT_SCREEN; 344 | 345 | 346 | shouldMove = newScroll + this.state.viewPortHeight > this.state.offsetBottom - 2 * this._configuration.DEFAULT_ITEM_HEIGHT; 347 | 348 | move = (targetOffsetBottom - this.state.offsetBottom) / this._configuration.DEFAULT_ITEM_HEIGHT; 349 | 350 | maxMove = (newScroll - this.state.offsetTop) / this._configuration.DEFAULT_ITEM_HEIGHT; 351 | move = Math.min(maxMove, move); 352 | 353 | // Another move 354 | willReachedBottom = (this.state.endIdx + move) >= this.props.children.length; 355 | 356 | var notAllAreDisplayed = this.state.endIdx < this.props.children.length; 357 | 358 | if (shouldMove && notAllAreDisplayed) { 359 | 360 | move = Math.min(move, (this.props.children.length) - this.state.endIdx); 361 | 362 | this.launchRenderingCycle('down', move); 363 | 364 | } 365 | }, 366 | 367 | 368 | approximateInsertion: function (newScroll) { 369 | 370 | var fullLength, fullSize, beginOffset, endOffset, startPosition, endPosition; 371 | var NB_MARGIN_TOP = 10; 372 | 373 | fullLength = this.props.children.length; 374 | 375 | 376 | beginOffset = Math.max(0, newScroll - this._configuration.MARGIN_OUT_SCREEN); 377 | 378 | startPosition = Math.round(beginOffset / this._configuration.DEFAULT_ITEM_HEIGHT); 379 | endPosition = Math.round(startPosition + ((this.state.viewPortHeight + 2 * this._configuration.MARGIN_OUT_SCREEN) / this._configuration.DEFAULT_ITEM_HEIGHT)); 380 | 381 | startPosition = Math.max(0, startPosition); 382 | endPosition = Math.min(endPosition, fullLength); 383 | 384 | var newOffsetTop = startPosition * this._configuration.DEFAULT_ITEM_HEIGHT; 385 | 386 | newOffsetTop = (newOffsetTop <= 0) ? 0 : newOffsetTop; 387 | 388 | // console.log('Approximate insertion to scroll ' + newScroll + 'new offset top will be ' + newOffsetTop + 'New start will be ' + startPosition); 389 | 390 | this.setState({ 391 | startIdx : startPosition, 392 | endIdx : endPosition, 393 | offsetTop : newOffsetTop, 394 | phase : 'rendering', 395 | oldScroll : this.getScroll() 396 | }); 397 | 398 | }, 399 | 400 | /* 401 | * From a move, compute the new position 402 | * Measure the size of the newly added elements and recompute offset top 403 | * Next rendering cycle will use that position, cache size in items when they are rendered 404 | */ 405 | launchRenderingCycle : function (direction, move, newScroll) { 406 | 407 | var instance_, startRenderIdx, endRenderIdx, toRender, rendering, representativeHeight, nextStartIdx, nextEndIdx, marginStart, oldScroll; 408 | 409 | instance_ = this; 410 | representativeHeight = 0; 411 | 412 | if (direction === 'up') { 413 | 414 | // Compute new indexes 415 | nextStartIdx = this.state.startIdx - move; 416 | nextEndIdx = this.state.endIdx - move; 417 | 418 | startRenderIdx = this.state.startIdx - move; 419 | endRenderIdx = this.state.startIdx; 420 | 421 | 422 | } else if (direction === 'down') { 423 | 424 | nextStartIdx = this.state.startIdx + move; 425 | nextEndIdx = this.state.endIdx + move; 426 | 427 | // Compute new element that will be rendered 428 | startRenderIdx = this.state.endIdx; 429 | endRenderIdx = this.state.endIdx + move; 430 | 431 | } 432 | 433 | nextStartIdx = Math.round(nextStartIdx); 434 | nextEndIdx = Math.round(nextEndIdx); 435 | startRenderIdx = Math.round(startRenderIdx); 436 | endRenderIdx = Math.round(endRenderIdx); 437 | 438 | oldScroll = this.getScroll(); 439 | 440 | instance_.setState({ 441 | startBufferIdx : startRenderIdx, 442 | endBufferIdx : endRenderIdx, 443 | buffering : true, 444 | oldScroll : oldScroll 445 | }, function () { 446 | // New items have been buffered, render new part of the list 447 | // TODO: debounce 448 | instance_.setState({ 449 | startIdx : nextStartIdx, 450 | endIdx : nextEndIdx, 451 | differential : true, 452 | oldScroll : oldScroll, 453 | direction : direction 454 | }); 455 | }); 456 | 457 | } 458 | 459 | }); 460 | 461 | 462 | 463 | module.exports = InfiniteListComponent; 464 | 465 | 466 | -------------------------------------------------------------------------------- /src/styles/main.css: -------------------------------------------------------------------------------- 1 | /* Stiziles */ 2 | 3 | html, body { 4 | background: #fff; 5 | } 6 | 7 | /* main */ 8 | 9 | .main { 10 | width: 100%; 11 | height: 100%; 12 | background: #fff; 13 | color: #fff; 14 | } 15 | 16 | .main img { 17 | width: 103px; 18 | height: 89px; 19 | margin-bottom: 10px; 20 | text-align: center; 21 | } 22 | 23 | /* transitions */ 24 | 25 | .fade-enter { 26 | opacity: 0.01; 27 | transition: opacity .5s ease-in; 28 | } 29 | 30 | .fade-enter.fade-enter-active { 31 | opacity: 1; 32 | } 33 | 34 | .fade-leave { 35 | opacity: 1; 36 | transition: opacity .5s ease-in; 37 | } 38 | 39 | .fade-leave.fade-leave-active { 40 | opacity: 0.01; 41 | } 42 | 43 | .item-line-style { 44 | padding-top:5px; 45 | padding-bottom:5px; 46 | } 47 | -------------------------------------------------------------------------------- /src/styles/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.1 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 and Firefox. 29 | * Correct `block` display not defined for `main` in IE 11. 30 | */ 31 | 32 | article, 33 | aside, 34 | details, 35 | figcaption, 36 | figure, 37 | footer, 38 | header, 39 | hgroup, 40 | main, 41 | nav, 42 | section, 43 | summary { 44 | display: block; 45 | } 46 | 47 | /** 48 | * 1. Correct `inline-block` display not defined in IE 8/9. 49 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 50 | */ 51 | 52 | audio, 53 | canvas, 54 | progress, 55 | video { 56 | display: inline-block; /* 1 */ 57 | vertical-align: baseline; /* 2 */ 58 | } 59 | 60 | /** 61 | * Prevent modern browsers from displaying `audio` without controls. 62 | * Remove excess height in iOS 5 devices. 63 | */ 64 | 65 | audio:not([controls]) { 66 | display: none; 67 | height: 0; 68 | } 69 | 70 | /** 71 | * Address `[hidden]` styling not present in IE 8/9/10. 72 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 73 | */ 74 | 75 | [hidden], 76 | template { 77 | display: none; 78 | } 79 | 80 | /* Links 81 | ========================================================================== */ 82 | 83 | /** 84 | * Remove the gray background color from active links in IE 10. 85 | */ 86 | 87 | a { 88 | background: transparent; 89 | } 90 | 91 | /** 92 | * Improve readability when focused and also mouse hovered in all browsers. 93 | */ 94 | 95 | a:active, 96 | a:hover { 97 | outline: 0; 98 | } 99 | 100 | /* Text-level semantics 101 | ========================================================================== */ 102 | 103 | /** 104 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 105 | */ 106 | 107 | abbr[title] { 108 | border-bottom: 1px dotted; 109 | } 110 | 111 | /** 112 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 113 | */ 114 | 115 | b, 116 | strong { 117 | font-weight: bold; 118 | } 119 | 120 | /** 121 | * Address styling not present in Safari and Chrome. 122 | */ 123 | 124 | dfn { 125 | font-style: italic; 126 | } 127 | 128 | /** 129 | * Address variable `h1` font-size and margin within `section` and `article` 130 | * contexts in Firefox 4+, Safari, and Chrome. 131 | */ 132 | 133 | h1 { 134 | font-size: 2em; 135 | margin: 0.67em 0; 136 | } 137 | 138 | /** 139 | * Address styling not present in IE 8/9. 140 | */ 141 | 142 | mark { 143 | background: #ff0; 144 | color: #000; 145 | } 146 | 147 | /** 148 | * Address inconsistent and variable font size in all browsers. 149 | */ 150 | 151 | small { 152 | font-size: 80%; 153 | } 154 | 155 | /** 156 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 157 | */ 158 | 159 | sub, 160 | sup { 161 | font-size: 75%; 162 | line-height: 0; 163 | position: relative; 164 | vertical-align: baseline; 165 | } 166 | 167 | sup { 168 | top: -0.5em; 169 | } 170 | 171 | sub { 172 | bottom: -0.25em; 173 | } 174 | 175 | /* Embedded content 176 | ========================================================================== */ 177 | 178 | /** 179 | * Remove border when inside `a` element in IE 8/9/10. 180 | */ 181 | 182 | img { 183 | border: 0; 184 | } 185 | 186 | /** 187 | * Correct overflow not hidden in IE 9/10/11. 188 | */ 189 | 190 | svg:not(:root) { 191 | overflow: hidden; 192 | } 193 | 194 | /* Grouping content 195 | ========================================================================== */ 196 | 197 | /** 198 | * Address margin not present in IE 8/9 and Safari. 199 | */ 200 | 201 | figure { 202 | margin: 1em 40px; 203 | } 204 | 205 | /** 206 | * Address differences between Firefox and other browsers. 207 | */ 208 | 209 | hr { 210 | -moz-box-sizing: content-box; 211 | box-sizing: content-box; 212 | height: 0; 213 | } 214 | 215 | /** 216 | * Contain overflow in all browsers. 217 | */ 218 | 219 | pre { 220 | overflow: auto; 221 | } 222 | 223 | /** 224 | * Address odd `em`-unit font size rendering in all browsers. 225 | */ 226 | 227 | code, 228 | kbd, 229 | pre, 230 | samp { 231 | font-family: monospace, monospace; 232 | font-size: 1em; 233 | } 234 | 235 | /* Forms 236 | ========================================================================== */ 237 | 238 | /** 239 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 240 | * styling of `select`, unless a `border` property is set. 241 | */ 242 | 243 | /** 244 | * 1. Correct color not being inherited. 245 | * Known issue: affects color of disabled elements. 246 | * 2. Correct font properties not being inherited. 247 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 248 | */ 249 | 250 | button, 251 | input, 252 | optgroup, 253 | select, 254 | textarea { 255 | color: inherit; /* 1 */ 256 | font: inherit; /* 2 */ 257 | margin: 0; /* 3 */ 258 | } 259 | 260 | /** 261 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 262 | */ 263 | 264 | button { 265 | overflow: visible; 266 | } 267 | 268 | /** 269 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 270 | * All other form control elements do not inherit `text-transform` values. 271 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 272 | * Correct `select` style inheritance in Firefox. 273 | */ 274 | 275 | button, 276 | select { 277 | text-transform: none; 278 | } 279 | 280 | /** 281 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 282 | * and `video` controls. 283 | * 2. Correct inability to style clickable `input` types in iOS. 284 | * 3. Improve usability and consistency of cursor style between image-type 285 | * `input` and others. 286 | */ 287 | 288 | button, 289 | html input[type="button"], /* 1 */ 290 | input[type="reset"], 291 | input[type="submit"] { 292 | -webkit-appearance: button; /* 2 */ 293 | cursor: pointer; /* 3 */ 294 | } 295 | 296 | /** 297 | * Re-set default cursor for disabled elements. 298 | */ 299 | 300 | button[disabled], 301 | html input[disabled] { 302 | cursor: default; 303 | } 304 | 305 | /** 306 | * Remove inner padding and border in Firefox 4+. 307 | */ 308 | 309 | button::-moz-focus-inner, 310 | input::-moz-focus-inner { 311 | border: 0; 312 | padding: 0; 313 | } 314 | 315 | /** 316 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 317 | * the UA stylesheet. 318 | */ 319 | 320 | input { 321 | line-height: normal; 322 | } 323 | 324 | /** 325 | * It's recommended that you don't attempt to style these elements. 326 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 327 | * 328 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 329 | * 2. Remove excess padding in IE 8/9/10. 330 | */ 331 | 332 | input[type="checkbox"], 333 | input[type="radio"] { 334 | box-sizing: border-box; /* 1 */ 335 | padding: 0; /* 2 */ 336 | } 337 | 338 | /** 339 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 340 | * `font-size` values of the `input`, it causes the cursor style of the 341 | * decrement button to change from `default` to `text`. 342 | */ 343 | 344 | input[type="number"]::-webkit-inner-spin-button, 345 | input[type="number"]::-webkit-outer-spin-button { 346 | height: auto; 347 | } 348 | 349 | /** 350 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 351 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 352 | * (include `-moz` to future-proof). 353 | */ 354 | 355 | input[type="search"] { 356 | -webkit-appearance: textfield; /* 1 */ 357 | -moz-box-sizing: content-box; 358 | -webkit-box-sizing: content-box; /* 2 */ 359 | box-sizing: content-box; 360 | } 361 | 362 | /** 363 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 364 | * Safari (but not Chrome) clips the cancel button when the search input has 365 | * padding (and `textfield` appearance). 366 | */ 367 | 368 | input[type="search"]::-webkit-search-cancel-button, 369 | input[type="search"]::-webkit-search-decoration { 370 | -webkit-appearance: none; 371 | } 372 | 373 | /** 374 | * Define consistent border, margin, and padding. 375 | */ 376 | 377 | fieldset { 378 | border: 1px solid #c0c0c0; 379 | margin: 0 2px; 380 | padding: 0.35em 0.625em 0.75em; 381 | } 382 | 383 | /** 384 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 385 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 386 | */ 387 | 388 | legend { 389 | border: 0; /* 1 */ 390 | padding: 0; /* 2 */ 391 | } 392 | 393 | /** 394 | * Remove default vertical scrollbar in IE 8/9/10/11. 395 | */ 396 | 397 | textarea { 398 | overflow: auto; 399 | } 400 | 401 | /** 402 | * Don't inherit the `font-weight` (applied by a rule above). 403 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 404 | */ 405 | 406 | optgroup { 407 | font-weight: bold; 408 | } 409 | 410 | /* Tables 411 | ========================================================================== */ 412 | 413 | /** 414 | * Remove most spacing between table cells. 415 | */ 416 | 417 | table { 418 | border-collapse: collapse; 419 | border-spacing: 0; 420 | } 421 | 422 | td, 423 | th { 424 | padding: 0; 425 | } 426 | -------------------------------------------------------------------------------- /test/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": true, 4 | "esnext": true, 5 | "bitwise": true, 6 | "camelcase": false, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": true, 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "false", 15 | "regexp": true, 16 | "undef": true, 17 | "unused": false, 18 | "strict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "white": true, 22 | "newcap": false, 23 | "globals": { 24 | "after": false, 25 | "afterEach": false, 26 | "react": false, 27 | "before": false, 28 | "beforeEach": false, 29 | "browser": false, 30 | "describe": false, 31 | "expect": false, 32 | "inject": false, 33 | "it": false, 34 | "spyOn": false, 35 | "jasmine": false, 36 | "spyOnConstructor": false, 37 | "React": true 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /test/helpers/phantomjs-shims.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 3 | var Ap = Array.prototype; 4 | var slice = Ap.slice; 5 | var Fp = Function.prototype; 6 | 7 | if (!Fp.bind) { 8 | // PhantomJS doesn't support Function.prototype.bind natively, so 9 | // polyfill it whenever this module is required. 10 | Fp.bind = function(context) { 11 | var func = this; 12 | var args = slice.call(arguments, 1); 13 | 14 | function bound() { 15 | var invokedAsConstructor = func.prototype && (this instanceof func); 16 | return func.apply( 17 | // Ignore the context parameter when invoking the bound function 18 | // as a constructor. Note that this includes not only constructor 19 | // invocations using the new keyword but also calls to base class 20 | // constructors such as BaseClass.call(this, ...) or super(...). 21 | !invokedAsConstructor && context || this, 22 | args.concat(slice.call(arguments)) 23 | ); 24 | } 25 | 26 | // The bound function must share the .prototype of the unbound 27 | // function so that any object created by one constructor will count 28 | // as an instance of both constructors. 29 | bound.prototype = func.prototype; 30 | 31 | return bound; 32 | }; 33 | } 34 | })(); 35 | -------------------------------------------------------------------------------- /test/spec/components/ReactInfiniteListApp.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('Main', function () { 4 | var React = require('react/addons'); 5 | var ReactInfiniteListApp, component; 6 | 7 | beforeEach(function () { 8 | var container = document.createElement('div'); 9 | container.id = 'content'; 10 | document.body.appendChild(container); 11 | 12 | ReactInfiniteListApp = require('components/ReactInfiniteListApp.js'); 13 | component = React.createElement(ReactInfiniteListApp); 14 | }); 15 | 16 | it('should create a new instance of ReactInfiniteListApp', function () { 17 | expect(component).toBeDefined(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Webpack development server configuration 3 | * 4 | * This file is set up for serving the webpack-dev-server, which will watch for changes and recompile as required if 5 | * the subfolder /webpack-dev-server/ is visited. Visiting the root will not automatically reload. 6 | */ 7 | 'use strict'; 8 | var webpack = require('webpack'); 9 | 10 | module.exports = { 11 | 12 | output: { 13 | filename: 'main.js', 14 | publicPath: '/assets/' 15 | }, 16 | 17 | cache: true, 18 | debug: true, 19 | devtool: false, 20 | entry: [ 21 | 'webpack/hot/only-dev-server', 22 | './src/scripts/examples/ReactInfiniteListAppExample.js' 23 | ], 24 | 25 | stats: { 26 | colors: true, 27 | reasons: true 28 | }, 29 | 30 | resolve: { 31 | extensions: ['', '.js'], 32 | alias: { 33 | 'styles': './src/styles', 34 | 'components': './src/scripts/components/' 35 | } 36 | }, 37 | module: { 38 | preLoaders: [{ 39 | test: /\.js$/, 40 | exclude: /node_modules/, 41 | loader: 'jsxhint' 42 | }], 43 | loaders: [{ 44 | test: /\.js$/, 45 | exclude: /node_modules/, 46 | loader: 'react-hot!jsx-loader?harmony' 47 | }, { 48 | test: /\.sass/, 49 | loader: 'style-loader!css-loader!sass-loader?outputStyle=expanded' 50 | }, { 51 | test: /\.css$/, 52 | loader: 'style-loader!css-loader' 53 | }, { 54 | test: /\.(png|jpg)$/, 55 | loader: 'url-loader?limit=8192' 56 | }, { 57 | test: /\.json/, 58 | loader: 'json-loader!json-loader' 59 | }] 60 | }, 61 | 62 | plugins: [ 63 | new webpack.HotModuleReplacementPlugin(), 64 | new webpack.NoErrorsPlugin() 65 | ] 66 | 67 | }; 68 | -------------------------------------------------------------------------------- /webpack.dist.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Webpack distribution configuration 3 | * 4 | * This file is set up for serving the distribution version. It will be compiled to dist/ by default 5 | */ 6 | 7 | 'use strict'; 8 | 9 | var webpack = require('webpack'); 10 | 11 | module.exports = { 12 | 13 | output: { 14 | publicPath: '/assets/', 15 | path: 'dist/assets/', 16 | filename: 'react-infinite-list.js' 17 | }, 18 | 19 | debug: false, 20 | devtool: false, 21 | entry: './src/scripts/react-infinite-list.js', 22 | 23 | stats: { 24 | colors: true, 25 | reasons: false 26 | }, 27 | 28 | plugins: [ 29 | new webpack.optimize.DedupePlugin(), 30 | new webpack.optimize.UglifyJsPlugin(), 31 | new webpack.optimize.OccurenceOrderPlugin(), 32 | new webpack.optimize.AggressiveMergingPlugin() 33 | ], 34 | 35 | resolve: { 36 | extensions: ['', '.js'], 37 | alias: { 38 | 'styles': './src/styles', 39 | 'components': './src/scripts/components/' 40 | } 41 | }, 42 | 43 | module: { 44 | preLoaders: [{ 45 | test: /\.js$/, 46 | exclude: /node_modules/, 47 | loader: 'jsxhint' 48 | }], 49 | 50 | loaders: [{ 51 | test: /\.js$/, 52 | exclude: /node_modules/, 53 | loader: 'jsx-loader?harmony' 54 | }, { 55 | test: /\.css$/, 56 | loader: 'style-loader!css-loader' 57 | }, { 58 | test: /\.sass/, 59 | loader: 'style-loader!css-loader!sass-loader?outputStyle=expanded' 60 | }, { 61 | test: /\.css$/, 62 | loader: 'style-loader!css-loader' 63 | }, { 64 | test: /\.json/, 65 | loader: 'json-loader!css-loader' 66 | }] 67 | } 68 | }; 69 | --------------------------------------------------------------------------------