├── .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 |
--------------------------------------------------------------------------------