├── .babelrc ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples └── infinite-grid │ ├── grid.example.js │ ├── grid.js │ └── index.html ├── package.json ├── prepublish.js ├── src ├── AdaptiveGrid.js ├── Display.js ├── Grid.js ├── GridState.js ├── Item.js ├── Row.js ├── gridCalculations.js ├── gridStateFactory.js ├── index.js └── throttle.js ├── test ├── AdaptiveGrid.spec.js ├── Display.spec.js ├── Grid.spec.js ├── GridState.spec.js ├── Item.spec.js ├── Row.spec.js ├── gridCalculations.spec.js └── setup.js ├── webpack.config.examples.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ], 5 | "plugins": [ 6 | "transform-object-rest-spread", 7 | "transform-react-jsx" 8 | ], 9 | "sourceMaps": "true" 10 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "babo" 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | # IDE directory 36 | .idea 37 | 38 | # Buid 39 | lib 40 | dist -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5" 4 | script: 5 | - npm run lint 6 | - npm test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Babo-Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Adaptive Grid [![Build Status](https://travis-ci.org/babotech/react-adaptive-grid.svg?branch=master)](https://travis-ci.org/babotech/react-adaptive-grid) 2 | 3 | ## Installation 4 | 5 | ``` 6 | npm install --save react-adaptive-grid 7 | ``` 8 | 9 | ## Features 10 | 11 | * *windowing* - render only visible items 12 | * relative positioning - all items position relative each other 13 | * scale items in proportion 14 | 15 | ## Usage 16 | ```javascript 17 | import Grid from 'react-adaptive-grid' 18 | 19 | // Immutable.js List 20 | const items = List([ 21 | Map({id:1, foo: 'bar', width: 200, height: 300}), 22 | ... 23 | ]) 24 | 25 | // Your component must accept 'data' prop. 26 | const ItemComponent = ({data, width, height, additionalHeight}) => ( 27 | ... 28 | ) 29 | 30 | const props = { 31 | ItemComponent, 32 | items, 33 | minWidth: 200 34 | } 35 | 36 | ... 37 | 38 | ``` 39 | 40 | ## Infinite scroll 41 | 42 | ```javascript 43 | const props = { 44 | ItemComponent, 45 | items, 46 | minWidth, 47 | buffer, 48 | load: () => ( /* load more items */ ), 49 | more: Boolean, // has more 50 | loading: Boolean 51 | } 52 | 53 | 54 | ``` 55 | 56 | ## Example 57 | 58 | [Watch here](http://babotech.github.io/react-adaptive-grid/) 59 | 60 | ## License 61 | 62 | **MIT** 63 | -------------------------------------------------------------------------------- /examples/infinite-grid/grid.example.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | 3 | import AdaptiveGrid from '../../lib' 4 | import ReactDOM from 'react-dom' 5 | import {fromJS} from 'immutable' 6 | import rndoam from 'rndoam' 7 | 8 | let i = 0 9 | const colors = [ `039BE5`, `FF5722`, `673AB7`, `FFF176`, `FF3D00` ] 10 | const chunkSize = 100 11 | const max = 500 12 | const generateItems = () => 13 | fromJS(rndoam.collection({ 14 | id: () => i++, 15 | color: () => colors[ rndoam.number(0, 4) ], 16 | width: () => rndoam.number(100, 400), 17 | height: () => rndoam.number(100, 400) 18 | }, chunkSize)) 19 | 20 | const items = generateItems() 21 | 22 | const ItemComponent = ({data, width, height}) => { 23 | 24 | const backDropStyle = { 25 | display: `flex`, 26 | width, 27 | height, 28 | backgroundColor: `#${data.get(`color`)}` 29 | } 30 | const originalSquare = { 31 | display: `flex`, 32 | width: data.get(`width`), 33 | height: data.get(`height`), 34 | maxWidth: `100%`, 35 | maxHeight: `100%`, 36 | backgroundColor: `#FFFFFF`, 37 | margin: `auto`, 38 | opacity: .2, 39 | fontFamily: `Arial`, 40 | fontSize: `1em`, 41 | color: `#000000`, 42 | alignItems: `center`, 43 | justifyContent: `center` 44 | } 45 | 46 | return ( 47 |
48 |
49 | {`${data.get(`width`)}x${data.get(`height`)}`} 50 |
51 |
52 | ) 53 | } 54 | 55 | const defaultProps = { 56 | ItemComponent, 57 | minWidth: 200, 58 | padding: 100 59 | } 60 | 61 | class Container extends Component { 62 | 63 | constructor() { 64 | super() 65 | this.state = { 66 | loading: false, 67 | more: true, 68 | items 69 | } 70 | } 71 | 72 | render() { 73 | const load = () => { 74 | this.setState({ 75 | loading: true 76 | }) 77 | 78 | setTimeout(() => { 79 | const moreItems = this 80 | .state 81 | .items 82 | .concat(generateItems()) 83 | this.setState({ 84 | loading: false, 85 | items: moreItems, 86 | more: moreItems.size < max 87 | }) 88 | }, 1000) 89 | } 90 | const props = { 91 | ...defaultProps, 92 | ...this.state, 93 | buffer: 5, 94 | load 95 | } 96 | 97 | return 98 | } 99 | } 100 | 101 | const wrapperStyle = { 102 | height: `100vh`, 103 | width: `100vw` 104 | } 105 | 106 | document.addEventListener(`DOMContentLoaded`, () => { 107 | ReactDOM.render( 108 |
109 | 110 |
, 111 | document.getElementById(`app`) 112 | ) 113 | }) -------------------------------------------------------------------------------- /examples/infinite-grid/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React Adaptive Grid 6 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-adaptive-grid", 3 | "version": "0.4.0", 4 | "description": "React adaptive grid", 5 | "files": [ 6 | "dist", 7 | "lib", 8 | "src" 9 | ], 10 | "main": "./lib/index.js", 11 | "scripts": { 12 | "build:lib": "babel src --out-dir lib", 13 | "build:examples": "cross-env NODE_ENV=development webpack --config webpack.config.examples.js", 14 | "build:umd": "cross-env NODE_ENV=development webpack src/index.js dist/react-adaptive-grid.js", 15 | "build:umd:min": "cross-env NODE_ENV=production webpack src/index.js dist/react-adaptive-grid.min.js", 16 | "build": "npm run build:lib && npm run build:examples && npm run build:umd && npm run build:umd:min && node ./prepublish", 17 | "lint": "eslint src test", 18 | "lint:fix": "eslint --fix src test", 19 | "test": "BABEL_DISABLE_CACHE=1 mocha --compilers js:babel-register --recursive --require ./test/setup.js", 20 | "test:watch": "npm test -- --watch" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/babotech/react-adaptive-grid.git" 25 | }, 26 | "keywords": [ 27 | "react", 28 | "reactjs", 29 | "adaptive grid", 30 | "grid", 31 | "windowing" 32 | ], 33 | "author": "Galkin Rostislav (http://github.com/galkinrost)", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/babotech/react-adaptive-grid/issues" 37 | }, 38 | "homepage": "https://github.com/babotech/react-adaptive-grid#readme", 39 | "devDependencies": { 40 | "babel-cli": "^6.5.1", 41 | "babel-core": "^6.5.2", 42 | "babel-loader": "^6.2.2", 43 | "babel-plugin-transform-object-rest-spread": "^6.5.0", 44 | "babel-plugin-transform-react-jsx": "^6.5.0", 45 | "babel-preset-es2015": "^6.5.0", 46 | "cross-env": "^1.0.7", 47 | "es3ify": "^0.2.1", 48 | "eslint": "2.2.0", 49 | "eslint-config-babo": "^0.1.1", 50 | "expect": "^1.14.0", 51 | "expect-immutable": "0.0.2", 52 | "expect-jsx": "^2.3.0", 53 | "glob": "^7.0.0", 54 | "jsdom": "^8.0.2", 55 | "mocha": "^2.4.5", 56 | "mockery": "^1.4.0", 57 | "react": "^0.14.7", 58 | "react-addons-test-utils": "^0.14.7", 59 | "react-contextify": "^0.1.0", 60 | "react-dom": "^0.14.7", 61 | "rndoam": "^0.1.0", 62 | "webpack": "^1.12.13" 63 | }, 64 | "dependencies": { 65 | "immutable": "^3.7.6" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /prepublish.js: -------------------------------------------------------------------------------- 1 | var glob = require('glob'); 2 | var fs = require('fs'); 3 | var es3ify = require('es3ify'); 4 | 5 | glob('./@(lib|dist)/**/*.js', function (err, files) { 6 | if (err) { 7 | throw err 8 | } 9 | 10 | files.forEach(function (file) { 11 | fs.readFile(file, 'utf8', function (err, data) { 12 | if (err) { 13 | throw err 14 | } 15 | 16 | fs.writeFile(file, es3ify.transform(data), function (err) { 17 | if (err) { 18 | throw err 19 | } 20 | 21 | console.log('es3ified ' + file); 22 | }) 23 | }) 24 | }) 25 | }); -------------------------------------------------------------------------------- /src/AdaptiveGrid.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | import Display from './Display' 3 | import {List} from 'immutable' 4 | 5 | class AdaptiveGrid extends Component { 6 | 7 | getChildContext() { 8 | const {ItemComponent, additionalHeight, items, offsetLeft} = this.props 9 | 10 | return { 11 | ItemComponent, 12 | additionalHeight, 13 | items, 14 | offsetLeft 15 | } 16 | } 17 | 18 | render() { 19 | const {additionalHeight, buffer, items, minWidth, padding, offsetLeft, load, loading, more} = this.props 20 | 21 | const displayProps = { 22 | additionalHeight, 23 | buffer, 24 | items, 25 | minWidth, 26 | padding, 27 | offsetLeft, 28 | load, 29 | loading, 30 | more 31 | } 32 | 33 | return ( 34 | 35 | ) 36 | } 37 | } 38 | 39 | AdaptiveGrid.defaultProps = { 40 | buffer: 0, 41 | offsetLeft: 0, 42 | padding: 0, 43 | load: () => null, 44 | more: false, 45 | loading: false 46 | } 47 | 48 | AdaptiveGrid.childContextTypes = { 49 | ItemComponent: PropTypes.func, 50 | additionalHeight: PropTypes.number, 51 | items: PropTypes.instanceOf(List), 52 | offsetLeft: PropTypes.number 53 | } 54 | 55 | AdaptiveGrid.propTypes = { 56 | ItemComponent: PropTypes.func.isRequired, 57 | additionalHeight: PropTypes.number, 58 | buffer: PropTypes.number, 59 | minWidth: PropTypes.number.isRequired, 60 | items: PropTypes.instanceOf(List).isRequired, 61 | padding: PropTypes.number, 62 | offsetLeft: PropTypes.number, 63 | load: PropTypes.func 64 | } 65 | 66 | export default AdaptiveGrid -------------------------------------------------------------------------------- /src/Display.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | 3 | import Grid from './Grid' 4 | import debounce from './throttle' 5 | import gridStateFactory from './gridStateFactory' 6 | 7 | 8 | const resizeDelay = 500 9 | const displayStyle = { 10 | height: `100%`, 11 | overflowX: `hidden`, 12 | overflowY: `scroll`, 13 | position: `relative`, 14 | width: `100%` 15 | } 16 | 17 | const getDisplaySize = (inst) => { 18 | const {top: displayTop, width, height} = inst.getDisplayBoundingClientRect() 19 | const {top: contentTop} = inst.getContentBoundingClientRect() 20 | const scrollTop = displayTop - contentTop 21 | 22 | return { 23 | scrollTop, 24 | width, 25 | height 26 | } 27 | } 28 | 29 | const createScrollListener = inst => 30 | () => { 31 | const {scrollTop} = getDisplaySize(inst) 32 | inst.gridState.updateOffset(scrollTop) 33 | 34 | inst.setState(inst.gridState.getState()) 35 | } 36 | 37 | const createWindowResizeListener = inst => { 38 | inst.windowResizeListener = debounce(() => { 39 | const {scrollTop, width, height} = getDisplaySize(inst) 40 | const {items, more} = inst.props 41 | inst.gridState.updateGrid(items, width, height, scrollTop, more) 42 | inst.setState(inst.gridState.getState()) 43 | }, resizeDelay) 44 | 45 | return inst.windowResizeListener 46 | } 47 | 48 | 49 | class Display extends Component { 50 | 51 | constructor(props) { 52 | super() 53 | const {additionalHeight, buffer, minWidth, offsetLeft, padding, more} = props 54 | this.gridState = gridStateFactory({additionalHeight, buffer, minWidth, offsetLeft, padding, more}) 55 | 56 | this.state = this.gridState.getState() 57 | } 58 | 59 | componentDidMount() { 60 | const {items, more} = this.props 61 | const {scrollTop, width, height} = getDisplaySize(this) 62 | this.gridState.updateGrid(items, width, height, scrollTop, more) 63 | 64 | this.setState(this.gridState.getState()) 65 | this.display.addEventListener(`scroll`, createScrollListener(this)) 66 | window.addEventListener(`resize`, createWindowResizeListener(this)) 67 | } 68 | 69 | componentWillUnmount() { 70 | window.removeEventListener(`resize`, this.windowResizeListener) 71 | } 72 | 73 | componentWillReceiveProps(nextProps) { 74 | 75 | if (nextProps.items.size !== this.props.items.size) { 76 | 77 | const sizeDiff = nextProps.items.size - this.props.items.size 78 | 79 | this.gridState.insertItems(nextProps.items.takeLast(sizeDiff), nextProps.more) 80 | this.setState( 81 | this.gridState.getState() 82 | ) 83 | } 84 | } 85 | 86 | componentWillUpdate(nextProps, nextState) { 87 | const {load, loading, more} = nextProps 88 | const {loadMoreAllowed} = nextState 89 | 90 | if (more && !loading && loadMoreAllowed) { 91 | load() 92 | } 93 | } 94 | 95 | getDisplayBoundingClientRect() { 96 | return this.display.getBoundingClientRect() 97 | } 98 | 99 | getContentBoundingClientRect() { 100 | return this.content.getBoundingClientRect() 101 | } 102 | 103 | render() { 104 | 105 | const {offsetLeft} = this.props 106 | 107 | return ( 108 |
{ 109 | this.display = display 110 | }} 111 | style={displayStyle} 112 | > 113 |
{ 114 | this.content = content 115 | }} 116 | > 117 | 118 |
119 |
120 | ) 121 | } 122 | } 123 | 124 | 125 | export default Display -------------------------------------------------------------------------------- /src/Grid.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | 3 | import {List} from 'immutable' 4 | import Row from './Row' 5 | 6 | const defaultContentStyle = { 7 | position: `relative`, 8 | width: `100%`, 9 | height: 0 10 | } 11 | 12 | const defaultScrollHelperStyle = { 13 | display: `block`, 14 | position: `relative`, 15 | width: `100%`, 16 | height: 0 17 | } 18 | 19 | class Grid extends Component { 20 | 21 | render() { 22 | const { 23 | height, 24 | rows = List(), 25 | offset, 26 | padding 27 | } = this.props 28 | 29 | const contentStyle = { 30 | ...defaultContentStyle, 31 | height, 32 | paddingLeft: padding, 33 | paddingRight: padding 34 | } 35 | 36 | const scrollHelperStyle = { 37 | ...defaultScrollHelperStyle, 38 | height: offset 39 | } 40 | 41 | return ( 42 |
43 |
44 | {rows.map((row, index) => ( 45 | 46 | )).toArray()} 47 |
48 | ) 49 | } 50 | } 51 | 52 | export default Grid -------------------------------------------------------------------------------- /src/GridState.js: -------------------------------------------------------------------------------- 1 | import {List, Map} from 'immutable' 2 | import {calcGrid, calcGridExcludeLastRow, calcVisibleGrid, insertItems} from './gridCalculations' 3 | 4 | class GridState { 5 | 6 | constructor({ 7 | additionalHeight = 0, 8 | buffer = 0, 9 | containerWidth = 0, 10 | containerHeight = 0, 11 | minWidth = 0, 12 | more = false, 13 | offset = 0, 14 | offsetLeft = 0, 15 | padding = 0, 16 | grid = Map({ 17 | rows: List(), 18 | height: 0 19 | }) 20 | } = {}) { 21 | 22 | this.additionalHeight = additionalHeight 23 | this.buffer = buffer 24 | this.containerWidth = containerWidth 25 | this.containerHeight = containerHeight 26 | this.minWidth = minWidth 27 | this.more = more 28 | this.offset = offset 29 | this.offsetLeft = offsetLeft 30 | this.padding = padding 31 | this.grid = grid 32 | } 33 | 34 | getState() { 35 | const grid = this.more ? calcGridExcludeLastRow(this.grid) : this.grid 36 | 37 | const [ visibleGrid, lastVisibleRowIndex ] = calcVisibleGrid(grid, this.containerHeight, this.offset) 38 | 39 | const loadMoreAllowed = grid.get(`rows`).size - 1 - this.buffer <= lastVisibleRowIndex 40 | 41 | return { 42 | loadMoreAllowed, 43 | offset: visibleGrid.getIn([ `rows`, 0, `top` ]) || 0, 44 | padding: this.padding, 45 | ...visibleGrid.toObject() 46 | } 47 | } 48 | 49 | updateOffset(offset) { 50 | this.offset = offset 51 | } 52 | 53 | updateGrid(items, containerWidth, containerHeight, offset, more) { 54 | this.containerWidth = containerWidth 55 | this.containerHeight = containerHeight 56 | this.offset = offset 57 | this.more = more 58 | 59 | this.grid = calcGrid(items, this.additionalHeight, this.containerWidth, this.minWidth, this.offsetLeft, this.padding, this.padding) 60 | } 61 | 62 | insertItems(items, more) { 63 | this.more = more 64 | 65 | this.grid = insertItems(this.grid, items, this.additionalHeight, this.containerWidth, this.minWidth, this.offsetLeft, this.padding) 66 | } 67 | } 68 | 69 | export default GridState -------------------------------------------------------------------------------- /src/Item.js: -------------------------------------------------------------------------------- 1 | import {List, Map} from 'immutable' 2 | import React, {Component, PropTypes} from 'react' 3 | 4 | const defaultItemStyle = { 5 | display: `inline-block`, 6 | position: `relative`, 7 | marginLeft: 0 8 | } 9 | 10 | const getDataPridicate = item => i => item.get(`id`) === i.get(`id`) 11 | 12 | class Item extends Component { 13 | render() { 14 | const {item = Map()} = this.props 15 | const {ItemComponent, additionalHeight, items, offsetLeft} = this.context 16 | 17 | const itemStyle = { 18 | ...defaultItemStyle, 19 | marginLeft: offsetLeft 20 | } 21 | 22 | const data = items.find(getDataPridicate(item)) 23 | 24 | const props = { 25 | additionalHeight, 26 | data, 27 | height: item.get(`height`), 28 | width: item.get(`width`) 29 | } 30 | 31 | return ( 32 |
33 | 34 |
35 | ) 36 | } 37 | } 38 | 39 | Item.contextTypes = { 40 | ItemComponent: PropTypes.func, 41 | offsetLeft: PropTypes.number, 42 | items: PropTypes.instanceOf(List), 43 | additionalHeight: PropTypes.number 44 | } 45 | 46 | export default Item -------------------------------------------------------------------------------- /src/Row.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | 3 | import Item from './Item' 4 | 5 | const defaultRowStyle = { 6 | position: `relative`, 7 | width: `100%`, 8 | marginLeft: 0, 9 | display: `flex` 10 | } 11 | 12 | class Row extends Component { 13 | 14 | render() { 15 | const {row} = this.props 16 | const {offsetLeft} = this.context 17 | 18 | const rowStyle = { 19 | ...defaultRowStyle, 20 | height: row.get(`height`), 21 | marginLeft: -offsetLeft 22 | } 23 | 24 | return ( 25 |
26 | {row 27 | .get(`items`) 28 | .map((item, index) => ( 29 | 30 | )) 31 | .toArray()} 32 |
33 | ) 34 | } 35 | } 36 | 37 | Row.contextTypes = { 38 | offsetLeft: PropTypes.number 39 | } 40 | 41 | export default Row -------------------------------------------------------------------------------- /src/gridCalculations.js: -------------------------------------------------------------------------------- 1 | import {List, Map} from 'immutable' 2 | 3 | const scaleItemsToContainerWidth = (items, containerWidth, offsetLeft) => { 4 | const calculatedContainerWidth = containerWidth - (items.size - 1) * offsetLeft 5 | const sumWidth = items.reduce((acc, item) => acc + item.get(`width`), 0) 6 | const widthScaleFactor = calculatedContainerWidth / sumWidth 7 | return items.map(item => item.withMutations(i => 8 | i.update(`height`, h => h * widthScaleFactor).update(`width`, w => w * widthScaleFactor) 9 | )) 10 | } 11 | 12 | const scaleItemsToMinWidth = (items, minWidth) => { 13 | const minWidthItem = items.sortBy(item => item.get(`width`)).first() 14 | 15 | if (minWidthItem.get(`width`) < minWidth) { 16 | const k = minWidth / minWidthItem.get(`width`) 17 | 18 | items = items.map(item => 19 | item.withMutations(i => 20 | i.update(`width`, w => w * k) 21 | .update(`height`, h => h * k) 22 | ) 23 | ) 24 | } 25 | 26 | return items 27 | } 28 | 29 | export const calcGridRow = (top, items, containerWidth, additionalHeight, minWidth, offsetLeft = 0, fullWidth = true) => { 30 | let extraItems = List() 31 | const minHeight = items.minBy(item => item.get(`height`)).get(`height`) 32 | 33 | let calculatedItems = items.map(item => item.withMutations(i => { 34 | const k = minHeight / item.get(`height`) 35 | const origWidth = i.get(`width`) 36 | const origHeight = i.get(`height`) 37 | 38 | return i 39 | .set(`origHeight`, origHeight) 40 | .set(`origWidth`, origWidth) 41 | .set(`height`, minHeight) 42 | .update(`width`, w => k * w) 43 | })) 44 | 45 | if (fullWidth) { 46 | calculatedItems = scaleItemsToContainerWidth(calculatedItems, containerWidth, offsetLeft) 47 | 48 | while (calculatedItems.some(i => i.get(`width`) < minWidth)) { 49 | extraItems = extraItems.unshift(items.last()) 50 | items = items.pop() 51 | 52 | calculatedItems = calculatedItems.pop() 53 | 54 | calculatedItems = scaleItemsToContainerWidth(calculatedItems, containerWidth, offsetLeft) 55 | } 56 | } else { 57 | calculatedItems = scaleItemsToMinWidth(calculatedItems, minWidth) 58 | while (calculatedItems.reduce((acc, item) => acc + item.get(`width`), 0) > containerWidth) { 59 | extraItems = extraItems.unshift(items.last()) 60 | items = items.pop() 61 | 62 | calculatedItems = calculatedItems.pop() 63 | 64 | calculatedItems = scaleItemsToMinWidth(calculatedItems, minWidth) 65 | } 66 | } 67 | 68 | return Map({ 69 | row: Map({ 70 | items: calculatedItems, 71 | top, 72 | height: calculatedItems.first().get(`height`) + additionalHeight 73 | }), 74 | extraItems 75 | }) 76 | } 77 | 78 | export const calcGrid = (items, additionalHeight, containerWidth, minWidth, offsetLeft, padding = 0, initialTop = 0) => { 79 | const double = 2 80 | const actualContainerWidth = containerWidth - padding * double 81 | 82 | let width = 0 83 | let top = initialTop 84 | let itemsInRow = List() 85 | let rows = List() 86 | 87 | items.forEach(item => { 88 | width += item.get(`width`) 89 | 90 | if (width > actualContainerWidth && itemsInRow.size) { 91 | width = item.get(`width`) 92 | 93 | const calcGridRowResult = calcGridRow(top, itemsInRow, actualContainerWidth, additionalHeight, minWidth, offsetLeft) 94 | 95 | rows = rows.push(calcGridRowResult.get(`row`)) 96 | 97 | top += calcGridRowResult.getIn([ `row`, `height` ]) 98 | 99 | itemsInRow = calcGridRowResult.get(`extraItems`) 100 | } 101 | 102 | itemsInRow = itemsInRow.push(item) 103 | }) 104 | 105 | while (itemsInRow.size) { 106 | const calcGridRowResult = calcGridRow(top, itemsInRow, actualContainerWidth, additionalHeight, minWidth, offsetLeft) 107 | rows = rows.push(calcGridRowResult.get(`row`)) 108 | 109 | top += calcGridRowResult.getIn([ `row`, `height` ]) 110 | 111 | itemsInRow = calcGridRowResult.get(`extraItems`) 112 | } 113 | 114 | if (rows.size) { 115 | rows = rows.update(rows.size - 1, row => { 116 | top -= row.get(`height`) 117 | const origItems = row.get(`items`) 118 | .map(item => 119 | item.update(it => 120 | it.withMutations(i => 121 | i.set(`width`, i.get(`origWidth`)) 122 | .set(`height`, i.get(`origHeight`)) 123 | ) 124 | ) 125 | ) 126 | 127 | const updatedRow = calcGridRow(top, origItems, actualContainerWidth, additionalHeight, minWidth, offsetLeft, false).get(`row`) 128 | 129 | top += updatedRow.get(`height`) 130 | return updatedRow 131 | }) 132 | } 133 | return Map({ 134 | rows, 135 | height: rows.reduce((acc, item) => acc + item.get(`height`), 0) + initialTop 136 | }) 137 | } 138 | 139 | export const calcGridExcludeLastRow = (grid) => { 140 | return grid.get(`rows`).size ? grid.update(g => g 141 | .update(`height`, h => h - g.getIn([ `rows`, -1, `height` ])) 142 | .update(`rows`, r => r.skipLast(1)) 143 | ) : grid 144 | } 145 | 146 | export const calcVisibleGrid = (grid, visibleAreaHeight, offset) => { 147 | 148 | let lastVisibleRowIndex = 0 149 | return [ grid.update(`rows`, r => { 150 | let acc = List() 151 | r.some((it, i) => { 152 | const top = it.get(`top`) 153 | const height = it.get(`height`) 154 | 155 | if (top >= offset || top + height > offset) { 156 | if (top >= offset + visibleAreaHeight) { 157 | return true 158 | } 159 | 160 | lastVisibleRowIndex = i 161 | acc = acc.push(it) 162 | } 163 | 164 | return false 165 | }) 166 | return acc 167 | }), lastVisibleRowIndex ] 168 | } 169 | 170 | export const insertItems = (grid, items, additionalHeight, containerWidth, minWidth, offsetLeft, padding = 0) => { 171 | const lastRow = grid.getIn([ `rows`, -1 ]) 172 | 173 | const itemsToCalc = lastRow 174 | .get(`items`) 175 | .map(i => i 176 | .update(`width`, () => i.get(`origWidth`)) 177 | .update(`height`, () => i.get(`origHeight`))) 178 | .concat(items) 179 | 180 | return calcGrid(itemsToCalc, additionalHeight, containerWidth, minWidth, offsetLeft, padding, grid.get(`height`) - lastRow.get(`height`)) 181 | .update(g => 182 | g.update(`rows`, 183 | r => grid 184 | .get(`rows`) 185 | .skipLast(1) 186 | .concat(r))) 187 | } -------------------------------------------------------------------------------- /src/gridStateFactory.js: -------------------------------------------------------------------------------- 1 | import GridState from './GridState' 2 | 3 | const gridStateFactory = (...args) => new GridState(...args) 4 | 5 | export default gridStateFactory -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export {default} from './AdaptiveGrid' -------------------------------------------------------------------------------- /src/throttle.js: -------------------------------------------------------------------------------- 1 | const debounce = (func, wait) => { 2 | let timeout 3 | return (...args) => { 4 | const later = () => { 5 | timeout = null 6 | func(...args) 7 | } 8 | clearTimeout(timeout) 9 | timeout = setTimeout(later, wait) 10 | } 11 | } 12 | 13 | export default debounce -------------------------------------------------------------------------------- /test/AdaptiveGrid.spec.js: -------------------------------------------------------------------------------- 1 | import React, {Component, PropTypes} from 'react' 2 | 3 | import TestUtils from 'react-addons-test-utils' 4 | 5 | import expect from 'expect' 6 | import mockery from 'mockery' 7 | import rndoam from 'rndoam/lib/withImmutable' 8 | 9 | describe(`react-adaptive-grid`, () => { 10 | 11 | describe(`AdaptiveGrid`, () => { 12 | let AdaptiveGrid 13 | 14 | class DisplayMock extends Component { 15 | render() { 16 | return
17 | } 18 | } 19 | 20 | before(() => { 21 | mockery.enable({ 22 | warnOnUnregistered: false 23 | }) 24 | 25 | mockery.registerMock(`./Display`, DisplayMock); 26 | ({default: AdaptiveGrid} = require(`../src/AdaptiveGrid`)) 27 | }) 28 | 29 | after(() => { 30 | mockery.disable() 31 | mockery.deregisterAll() 32 | }) 33 | 34 | it(`should transfer props into the context`, () => { 35 | DisplayMock.contextTypes = { 36 | ItemComponent: PropTypes.func, 37 | additionalHeight: PropTypes.number, 38 | items: PropTypes.object, 39 | offsetLeft: PropTypes.number 40 | } 41 | 42 | try { 43 | const props = { 44 | ItemComponent: rndoam.noop(), 45 | additionalHeight: rndoam.number(), 46 | items: rndoam.list(), 47 | offsetLeft: rndoam.number() 48 | } 49 | 50 | const tree = TestUtils.renderIntoDocument( 51 | 52 | ) 53 | 54 | const display = TestUtils.findRenderedComponentWithType(tree, DisplayMock) 55 | 56 | expect(display.context).toEqual(props) 57 | 58 | } finally { 59 | DisplayMock.contextTypes = {} 60 | } 61 | }) 62 | 63 | it(`should transfer props into the Display component`, () => { 64 | const props = { 65 | ItemComponent: rndoam.noop(), 66 | additionalHeight: rndoam.number(), 67 | buffer: rndoam.number(), 68 | minWidth: rndoam.number(), 69 | padding: rndoam.number(), 70 | offsetLeft: rndoam.number(), 71 | items: rndoam.list(), 72 | load: rndoam.noop(), 73 | loading: false, 74 | more: true 75 | } 76 | 77 | const tree = TestUtils.renderIntoDocument( 78 | 79 | ) 80 | 81 | const display = TestUtils.findRenderedComponentWithType(tree, DisplayMock) 82 | 83 | const {ItemComponent, ...expectedProps} = props 84 | 85 | expect(display.props).toEqual(expectedProps) 86 | }) 87 | }) 88 | }) -------------------------------------------------------------------------------- /test/Display.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-magic-numbers */ 2 | 3 | import React, {Component} from 'react' 4 | 5 | import ReactDOM from 'react-dom' 6 | import TestUtils from 'react-addons-test-utils' 7 | 8 | import expect from 'expect' 9 | import mockery from 'mockery' 10 | import rndoam from 'rndoam/lib/withImmutable' 11 | 12 | class GridMock extends Component { 13 | render() { 14 | return
15 | } 16 | } 17 | 18 | const gridStateMock = {} 19 | 20 | const gridStateFactoryMock = expect.createSpy() 21 | .andReturn(gridStateMock) 22 | 23 | const createSetDisplayClientBoundingRect = (Display) => (displayClientBoundingRectMock) => { 24 | const spy = expect.spyOn(Display.prototype, `getDisplayBoundingClientRect`) 25 | spy.andReturn(displayClientBoundingRectMock) 26 | return spy.restore 27 | } 28 | 29 | const createSetContentClientBoundingRect = (Display) => (contentClientBoundingRectMock) => { 30 | const spy = expect.spyOn(Display.prototype, `getContentBoundingClientRect`) 31 | spy.andReturn(contentClientBoundingRectMock) 32 | return spy.restore 33 | } 34 | 35 | const simulateScroll = (node) => { 36 | const event = document.createEvent(`Event`) 37 | event.initEvent(`scroll`, true, true) 38 | node.dispatchEvent(event) 39 | return event 40 | } 41 | 42 | const simulateWindowResize = () => { 43 | const event = document.createEvent(`Event`) 44 | event.initEvent(`resize`, true, true) 45 | window.dispatchEvent(event) 46 | return event 47 | } 48 | 49 | describe(`react-adaptive-grid`, () => { 50 | 51 | describe(`Display`, () => { 52 | let Display, setDisplayClientBoundingRect, setContentClientBoundingRect, mountNode 53 | 54 | before(() => { 55 | mockery.enable({ 56 | warnOnUnregistered: false 57 | }) 58 | 59 | mockery.registerMock(`./Grid`, GridMock) 60 | mockery.registerMock(`./gridStateFactory`, gridStateFactoryMock); 61 | ({default: Display} = require(`../src/Display`)) 62 | 63 | setDisplayClientBoundingRect = createSetDisplayClientBoundingRect(Display) 64 | setContentClientBoundingRect = createSetContentClientBoundingRect(Display) 65 | 66 | }) 67 | 68 | after(() => { 69 | mockery.disable() 70 | mockery.deregisterAll() 71 | }) 72 | 73 | beforeEach(() => { 74 | mountNode = document.createElement(`div`) 75 | document.body.appendChild(mountNode) 76 | gridStateMock.getState = expect.createSpy() 77 | .andReturn({}) 78 | gridStateMock.updateGrid = expect.createSpy() 79 | gridStateMock.updateOffset = expect.createSpy() 80 | gridStateMock.insertItems = expect.createSpy() 81 | }) 82 | 83 | afterEach(() => { 84 | ReactDOM.unmountComponentAtNode(mountNode) 85 | expect.restoreSpies() 86 | }) 87 | 88 | it(`should set initial state`, () => { 89 | const props = { 90 | minWidth: rndoam.number(), 91 | additionalHeight: rndoam.number(), 92 | buffer: rndoam.number(), 93 | offsetLeft: rndoam.number(), 94 | padding: rndoam.number(), 95 | more: true 96 | } 97 | const state = rndoam.object() 98 | 99 | gridStateMock.getState.andReturn(state) 100 | 101 | const display = new Display(props) 102 | 103 | 104 | expect(gridStateFactoryMock.calls[ 0 ].arguments[ 0 ]) 105 | .toEqual(props) 106 | 107 | expect(display.state).toEqual(state) 108 | }) 109 | 110 | it(`should update state when component did mount`, () => { 111 | const props = { 112 | items: rndoam.array(), 113 | more: true 114 | } 115 | 116 | const containerWidth = 500 117 | const containerHeight = 200 118 | const offset = 200 119 | 120 | const restoreDisplay = setDisplayClientBoundingRect({ 121 | top: 0, 122 | width: containerWidth, 123 | height: containerHeight 124 | }) 125 | 126 | const restoreContent = setContentClientBoundingRect({ 127 | top: -1 * offset 128 | }) 129 | 130 | const state = rndoam.object() 131 | 132 | gridStateMock 133 | .getState 134 | .andReturn(state) 135 | 136 | TestUtils 137 | .renderIntoDocument( 138 | 139 | ) 140 | 141 | expect(gridStateMock.updateGrid.calls[ 0 ].arguments) 142 | .toEqual([ 143 | props.items, 144 | containerWidth, 145 | containerHeight, 146 | offset, 147 | true 148 | ]) 149 | 150 | expect(gridStateMock.getState.calls.length) 151 | .toEqual(2) 152 | 153 | restoreDisplay() 154 | restoreContent() 155 | }) 156 | 157 | it(`should transfer properties into the Grid component`, () => { 158 | 159 | const state = { 160 | offset: rndoam.number(), 161 | height: rndoam.number(), 162 | rows: rndoam.array(), 163 | padding: rndoam.number() 164 | } 165 | 166 | const offsetLeft = rndoam.number() 167 | 168 | gridStateMock 169 | .getState 170 | .andReturn(state) 171 | 172 | const display = TestUtils.renderIntoDocument( 173 | 174 | ) 175 | 176 | const grid = TestUtils.findRenderedComponentWithType(display, GridMock) 177 | 178 | 179 | expect(grid.props) 180 | .toEqual({ 181 | ...state, 182 | offsetLeft 183 | }) 184 | }) 185 | 186 | it(`should update state on scroll`, () => { 187 | 188 | const restoreDisplay = setDisplayClientBoundingRect({ 189 | top: 0, 190 | width: 100, 191 | height: 100 192 | }) 193 | 194 | const offset = 100 195 | 196 | const display = TestUtils.renderIntoDocument( 197 | 198 | ) 199 | 200 | const restoreContent = setContentClientBoundingRect({ 201 | top: -1 * offset 202 | }) 203 | 204 | const state = rndoam.object() 205 | 206 | gridStateMock 207 | .getState 208 | .andReturn(state) 209 | 210 | simulateScroll(display.display) 211 | 212 | expect(display.state) 213 | .toEqual(state) 214 | 215 | expect(gridStateMock.updateOffset.calls[ 0 ].arguments[ 0 ]) 216 | .toEqual(offset) 217 | 218 | restoreDisplay() 219 | restoreContent() 220 | }) 221 | 222 | it(`should update state on window resize`, (done) => { 223 | 224 | const items = rndoam.array() 225 | 226 | const display = TestUtils.renderIntoDocument( 227 | 228 | ) 229 | 230 | const containerWidth = rndoam.number() 231 | const containerHeight = rndoam.number() 232 | const offset = rndoam.number() 233 | 234 | const restoreDisplay = setDisplayClientBoundingRect({ 235 | top: 0, 236 | width: containerWidth, 237 | height: containerHeight 238 | }) 239 | 240 | const restoreContent = setContentClientBoundingRect({ 241 | top: -1 * offset 242 | }) 243 | const state = rndoam.object() 244 | 245 | gridStateMock 246 | .getState 247 | .andReturn(state) 248 | 249 | simulateWindowResize() 250 | 251 | setTimeout(() => { 252 | expect(gridStateMock.updateGrid.calls[ 1 ].arguments) 253 | .toEqual([ 254 | items, 255 | containerWidth, 256 | containerHeight, 257 | offset, 258 | true 259 | ]) 260 | 261 | expect(display.state) 262 | .toEqual(state) 263 | 264 | restoreDisplay() 265 | restoreContent() 266 | done() 267 | }, 500) 268 | }) 269 | 270 | it(`should remove listener from the window after component was unmount`, () => { 271 | 272 | const spyAddListener = expect 273 | .spyOn(window, `addEventListener`) 274 | .andCallThrough() 275 | 276 | ReactDOM.render( 277 | , 278 | mountNode 279 | ) 280 | 281 | expect(spyAddListener.calls.length).toEqual(1) 282 | 283 | const spyRemoveListener = expect 284 | .spyOn(window, `removeEventListener`) 285 | .andCallThrough() 286 | 287 | ReactDOM.unmountComponentAtNode(mountNode) 288 | 289 | expect(spyRemoveListener.calls.length).toEqual(1) 290 | 291 | expect(spyAddListener.calls[ 0 ].arguments[ 1 ]).toEqual(spyRemoveListener.calls[ 0 ].arguments[ 1 ]) 292 | }) 293 | 294 | 295 | it(`should not call the load method if it has already been called`, () => { 296 | 297 | const props = { 298 | items: rndoam.list(100), 299 | load: expect.createSpy(), 300 | loading: true, 301 | more: true 302 | } 303 | 304 | gridStateMock 305 | .getState 306 | .andReturn({ 307 | shouldLoad: true 308 | }) 309 | 310 | TestUtils.renderIntoDocument( 311 | 312 | ) 313 | 314 | expect(props.load.calls.length).toEqual(0) 315 | }) 316 | 317 | it(`should not call the load method if it's no more`, () => { 318 | 319 | const props = { 320 | items: rndoam.list(100), 321 | load: expect.createSpy(), 322 | loading: true, 323 | more: false 324 | } 325 | 326 | gridStateMock 327 | .getState 328 | .andReturn({ 329 | shouldLoad: true 330 | }) 331 | 332 | TestUtils.renderIntoDocument( 333 | 334 | ) 335 | 336 | expect(props.load.calls.length).toEqual(0) 337 | }) 338 | 339 | it(`should call the load`, () => { 340 | 341 | const props = { 342 | items: rndoam.list(100), 343 | load: expect.createSpy(), 344 | loading: false, 345 | more: true 346 | } 347 | 348 | gridStateMock 349 | .getState 350 | .andReturn({ 351 | loadMoreAllowed: true 352 | }) 353 | 354 | TestUtils.renderIntoDocument( 355 | 356 | ) 357 | 358 | expect(props.load.calls.length).toEqual(1) 359 | 360 | }) 361 | 362 | it(`should insert elements`, () => { 363 | const props = { 364 | items: rndoam.list(100), 365 | load: expect.createSpy(), 366 | loading: false, 367 | more: true 368 | } 369 | 370 | gridStateMock 371 | .getState 372 | .andReturn({ 373 | shouldLoad: true 374 | }) 375 | 376 | class Container extends Component { 377 | constructor() { 378 | super() 379 | this.state = props 380 | } 381 | 382 | componentDidMount() { 383 | this.setState({ 384 | items: rndoam.list(200) 385 | }) 386 | } 387 | 388 | render() { 389 | return 390 | } 391 | } 392 | 393 | TestUtils.renderIntoDocument( 394 | 395 | ) 396 | 397 | expect(gridStateMock.insertItems.calls.length).toEqual(1) 398 | 399 | const {arguments: args} = gridStateMock.insertItems.calls[0] 400 | 401 | expect(args[0].size).toEqual(100) 402 | }) 403 | 404 | }) 405 | }) -------------------------------------------------------------------------------- /test/Grid.spec.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | 3 | import TestUtils from 'react-addons-test-utils' 4 | 5 | import expect from 'expect' 6 | import mockery from 'mockery' 7 | 8 | class RowItem extends Component { 9 | render() { 10 | return
11 | } 12 | } 13 | 14 | 15 | describe(`react-adaptive-grid`, () => { 16 | 17 | describe(`Grid`, () => { 18 | let Grid 19 | 20 | before(() => { 21 | mockery.enable({ 22 | warnOnUnregistered: false 23 | }) 24 | 25 | mockery.registerMock(`./Row`, RowItem); 26 | ({default: Grid} = require(`../src/Grid`)) 27 | 28 | }) 29 | 30 | after(() => { 31 | mockery.disable() 32 | mockery.deregisterAll() 33 | }) 34 | 35 | it(`should set height for scroll helper tag`, () => { 36 | const props = { 37 | offset: 100 38 | } 39 | 40 | const grid = TestUtils 41 | .renderIntoDocument( 42 | 43 | ) 44 | 45 | const divs = TestUtils.scryRenderedDOMComponentsWithTag(grid, `div`) 46 | 47 | expect(divs[1].style.height).toEqual(`${props.offset}px`) 48 | }) 49 | 50 | 51 | it(`should set height of the content area`, () => { 52 | const props = { 53 | height: 100 54 | } 55 | 56 | const grid = TestUtils 57 | .renderIntoDocument( 58 | 59 | ) 60 | 61 | const divs = TestUtils.scryRenderedDOMComponentsWithTag(grid, `div`) 62 | 63 | expect(divs[0].style.height).toEqual(`${props.height}px`) 64 | }) 65 | 66 | it(`should set left padding of the content area`, () => { 67 | const props = { 68 | padding: 100 69 | } 70 | 71 | const grid = TestUtils 72 | .renderIntoDocument( 73 | 74 | ) 75 | 76 | const divs = TestUtils.scryRenderedDOMComponentsWithTag(grid, `div`) 77 | 78 | const {paddingLeft, paddingRight} = divs[0].style 79 | 80 | expect(paddingLeft).toEqual(`${props.padding}px`) 81 | expect(paddingRight).toEqual(`${props.padding}px`) 82 | }) 83 | }) 84 | }) -------------------------------------------------------------------------------- /test/GridState.spec.js: -------------------------------------------------------------------------------- 1 | import {List, Map} from 'immutable' 2 | 3 | import expect from 'expect' 4 | import expectImmutable from 'expect-immutable' 5 | import mockery from 'mockery' 6 | import rndoam from 'rndoam/lib/withImmutable' 7 | 8 | expect.extend(expectImmutable) 9 | 10 | describe(`react-adaptive-grid`, () => { 11 | 12 | describe(`GridState`, () => { 13 | let GridState 14 | 15 | const calcVisibleGridSpy = expect.createSpy() 16 | const calcGridExcludeLastRowSpy = expect.createSpy() 17 | const calcGridSpy = expect.createSpy() 18 | const insertItemsSpy = expect.createSpy() 19 | 20 | beforeEach(() => { 21 | mockery.enable({ 22 | warnOnUnregistered: false 23 | 24 | }) 25 | mockery.registerMock(`./gridCalculations`, { 26 | calcGrid: calcGridSpy, 27 | calcGridExcludeLastRow: calcGridExcludeLastRowSpy, 28 | calcVisibleGrid: calcVisibleGridSpy, 29 | insertItems: insertItemsSpy 30 | }); 31 | 32 | ({default: GridState} = require(`../src/GridState`)) 33 | }) 34 | 35 | afterEach(() => { 36 | mockery.deregisterAll() 37 | mockery.disable() 38 | 39 | calcVisibleGridSpy.reset() 40 | calcGridSpy.reset() 41 | insertItemsSpy.reset() 42 | }) 43 | 44 | 45 | it(`should initialize with default values`, () => { 46 | const gridState = new GridState() 47 | 48 | expect(gridState.additionalHeight).toEqual(0) 49 | expect(gridState.containerWidth).toEqual(0) 50 | expect(gridState.containerHeight).toEqual(0) 51 | expect(gridState.minWidth).toEqual(0) 52 | expect(gridState.more).toEqual(false) 53 | expect(gridState.offset).toEqual(0) 54 | expect(gridState.offsetLeft).toEqual(0) 55 | expect(gridState.padding).toEqual(0) 56 | expect(gridState.grid).toEqualImmutable(Map({ 57 | rows: List(), 58 | height: 0 59 | })) 60 | }) 61 | 62 | it(`should update offset`, () => { 63 | const gridState = new GridState() 64 | const offset = rndoam.number() 65 | 66 | gridState.updateOffset(offset) 67 | 68 | expect(gridState.offset).toEqual(offset) 69 | }) 70 | 71 | it(`should calculate grid`, () => { 72 | 73 | const additionalHeight = rndoam.number() 74 | const items = rndoam.list() 75 | const containerWidth = rndoam.number() 76 | const containerHeight = rndoam.number() 77 | const offset = rndoam.number() 78 | const offsetLeft = rndoam.number() 79 | const minWidth = rndoam.number() 80 | const padding = rndoam.number() 81 | 82 | const gridState = new GridState({additionalHeight, minWidth, offsetLeft, padding}) 83 | 84 | gridState.updateGrid(items, containerWidth, containerHeight, offset, true) 85 | 86 | expect(gridState.containerWidth).toEqual(containerWidth) 87 | expect(gridState.containerHeight).toEqual(containerHeight) 88 | expect(gridState.offset).toEqual(offset) 89 | expect(gridState.more).toEqual(true) 90 | 91 | expect(calcGridSpy.calls.length).toEqual(1) 92 | 93 | const {arguments: args} = calcGridSpy.calls[ 0 ] 94 | 95 | expect(args).toEqual([ 96 | items, additionalHeight, containerWidth, minWidth, offsetLeft, padding, padding 97 | ]) 98 | }) 99 | 100 | it(`should get initial state`, () => { 101 | const padding = rndoam.number() 102 | const grid = Map({ 103 | rows: List([ 104 | Map(), 105 | Map() 106 | ]) 107 | }) 108 | 109 | const gridState = new GridState({grid, padding}) 110 | const offset = rndoam.number() 111 | const height = rndoam.number() 112 | const rows = List([ 113 | Map({ 114 | top: offset 115 | }) 116 | ]) 117 | 118 | calcVisibleGridSpy.andReturn([ Map({ 119 | rows, 120 | height 121 | }), 0 ]) 122 | 123 | expect(gridState.getState()) 124 | .toEqual({ 125 | loadMoreAllowed: false, 126 | offset, 127 | rows, 128 | height, 129 | padding 130 | }) 131 | expect(calcVisibleGridSpy.calls.length).toEqual(1) 132 | }) 133 | 134 | it(`should pass into calcVisible function grid without last ro if loading awaiting`, () => { 135 | const grid = Map({ 136 | rows: List([ 137 | Map(), 138 | Map() 139 | ]) 140 | }) 141 | 142 | const gridState = new GridState({ 143 | grid, 144 | more: true 145 | }) 146 | 147 | const gridExcludeLastRow = Map({ 148 | rows: List([ 149 | Map() 150 | ]) 151 | }) 152 | 153 | calcGridExcludeLastRowSpy.andReturn(gridExcludeLastRow) 154 | 155 | gridState.getState() 156 | 157 | const {arguments: args} = calcVisibleGridSpy.calls[ 0 ] 158 | 159 | expect(args[ 0 ]) 160 | .toBe(gridExcludeLastRow) 161 | }) 162 | 163 | it(`should allow to load more items if last row is visible`, () => { 164 | const grid = Map({ 165 | rows: List([ 166 | Map(), 167 | Map() 168 | ]) 169 | }) 170 | 171 | const gridState = new GridState({ 172 | grid 173 | }) 174 | 175 | calcVisibleGridSpy.andReturn([ grid, 1 ]) 176 | 177 | expect(gridState.getState().loadMoreAllowed).toBeTruthy() 178 | }) 179 | 180 | it(`should allow to load more with buffer`, () => { 181 | const grid = Map({ 182 | rows: List([ 183 | Map(), 184 | Map(), 185 | Map(), 186 | Map() 187 | ]) 188 | }) 189 | 190 | const gridState = new GridState({ 191 | grid, 192 | buffer: 3 193 | }) 194 | 195 | calcVisibleGridSpy.andReturn([ grid, 1 ]) 196 | 197 | expect(gridState.getState().loadMoreAllowed).toBeTruthy() 198 | }) 199 | 200 | it(`should insert items and update more`, () => { 201 | const gridState = new GridState({ 202 | more: true 203 | }) 204 | const items = rndoam.list() 205 | 206 | gridState.insertItems(items, false) 207 | 208 | expect(gridState.more) 209 | .toEqual(false) 210 | 211 | expect(insertItemsSpy.calls.length).toEqual(1) 212 | }) 213 | }) 214 | }) -------------------------------------------------------------------------------- /test/Item.spec.js: -------------------------------------------------------------------------------- 1 | import {List, Map} from 'immutable' 2 | import React, {Component, PropTypes} from 'react' 3 | 4 | import Item from '../src/Item' 5 | import TestUtils from 'react-addons-test-utils' 6 | 7 | import contextify from 'react-contextify' 8 | import expect from 'expect' 9 | import rndoam from 'rndoam/lib/withImmutable' 10 | 11 | describe(`react-adaptive-grid`, () => { 12 | describe(`Item`, () => { 13 | class ItemComponentMock extends Component { 14 | render() { 15 | return
16 | } 17 | } 18 | 19 | const WithContext = contextify({ 20 | ItemComponent: PropTypes.func, 21 | additionalHeight: PropTypes.number, 22 | items: PropTypes.object, 23 | offsetLeft: PropTypes.number 24 | }, ({ItemComponent, additionalHeight, items, offsetLeft}) => ({ 25 | ItemComponent, 26 | additionalHeight, 27 | items, 28 | offsetLeft 29 | }))(Item) 30 | 31 | it(`should receive item props from the context`, () => { 32 | 33 | const props = { 34 | ItemComponent: ItemComponentMock, 35 | additionalHeight: rndoam.number(), 36 | items: List(), 37 | offsetLeft: rndoam.number() 38 | } 39 | 40 | const tree = TestUtils.renderIntoDocument( 41 | 42 | ) 43 | 44 | const item = TestUtils.findRenderedComponentWithType(tree, Item) 45 | 46 | expect(item.context).toEqual(props) 47 | }) 48 | 49 | it(`should transfer props into the ItemComponent`, () => { 50 | const width = rndoam.number() 51 | const height = rndoam.number() 52 | 53 | const itemFromGrid = Map({ 54 | id: 2, 55 | width, 56 | height 57 | }) 58 | 59 | const itemData = Map({ 60 | id: 2, 61 | foo: `bar` 62 | }) 63 | 64 | const items = List([ 65 | Map({ 66 | id: 1 67 | }), 68 | itemData, 69 | Map({ 70 | id: 3 71 | }) 72 | ]) 73 | 74 | const props = { 75 | ItemComponent: ItemComponentMock, 76 | additionalHeight: rndoam.additionalHeight, 77 | item: itemFromGrid, 78 | items 79 | } 80 | 81 | const tree = TestUtils.renderIntoDocument( 82 | 83 | ) 84 | 85 | const item = TestUtils.findRenderedComponentWithType(tree, ItemComponentMock) 86 | 87 | expect(item.props.data).toEqual(itemData) 88 | expect(item.props.additionalHeight).toEqual(props.additionalHeight) 89 | expect(item.props.height).toEqual(height) 90 | expect(item.props.width).toEqual(width) 91 | }) 92 | }) 93 | }) -------------------------------------------------------------------------------- /test/Row.spec.js: -------------------------------------------------------------------------------- 1 | import {List, Map} from 'immutable' 2 | import React, {Component, PropTypes} from 'react' 3 | 4 | import TestUtils from 'react-addons-test-utils' 5 | 6 | import contextify from 'react-contextify' 7 | import expect from 'expect' 8 | import mockery from 'mockery' 9 | 10 | class ItemMock extends Component { 11 | render() { 12 | return
13 | } 14 | } 15 | 16 | 17 | describe(`react-adaptive-grid`, () => { 18 | 19 | describe(`Row`, () => { 20 | let Row, WithContext 21 | 22 | before(() => { 23 | mockery.enable({ 24 | warnOnUnregistered: false 25 | }) 26 | 27 | mockery.registerMock(`./Item`, ItemMock); 28 | ({default: Row} = require(`../src/Row`)) 29 | 30 | WithContext = contextify({ 31 | offsetLeft: PropTypes.number 32 | }, ({offsetLeft}) => ({ 33 | offsetLeft 34 | }))(Row) 35 | }) 36 | 37 | after(() => { 38 | mockery.disable() 39 | mockery.deregisterAll() 40 | }) 41 | 42 | it(`should render items`, () => { 43 | const item = Map() 44 | const props = { 45 | row: Map({ 46 | items: List([ 47 | item 48 | ]) 49 | }) 50 | } 51 | 52 | const grid = TestUtils 53 | .renderIntoDocument( 54 | 55 | ) 56 | 57 | const items = TestUtils.scryRenderedComponentsWithType(grid, ItemMock) 58 | 59 | expect(items.length).toEqual(1) 60 | expect(items[ 0 ].props).toEqual({ 61 | item 62 | }) 63 | }) 64 | 65 | it(`should set height`, () => { 66 | const height = 100 67 | const offsetLeft = 100 68 | const props = { 69 | row: Map({ 70 | items: List(), 71 | height 72 | }), 73 | offsetLeft 74 | } 75 | 76 | const grid = TestUtils 77 | .renderIntoDocument( 78 | 79 | ) 80 | 81 | const divs = TestUtils.scryRenderedDOMComponentsWithTag(grid, `div`) 82 | const {style} = divs[ 0 ] 83 | 84 | expect(style.height).toEqual(`${height}px`) 85 | expect(style.marginLeft).toEqual(`${-offsetLeft}px`) 86 | }) 87 | }) 88 | }) -------------------------------------------------------------------------------- /test/gridCalculations.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-magic-numbers */ 2 | 3 | import {List, Map} from 'immutable' 4 | 5 | import {calcGrid, calcGridExcludeLastRow, calcGridRow, calcVisibleGrid, insertItems} from '../src/gridCalculations' 6 | 7 | import expect from 'expect' 8 | import expectImmutable from 'expect-immutable' 9 | 10 | expect.extend(expectImmutable) 11 | 12 | describe(`react-adaptive-grid`, () => { 13 | 14 | describe(`gridCalculations`, () => { 15 | 16 | describe(`calcGridRow->`, () => { 17 | 18 | it(`should return a list width exact width`, () => { 19 | const items = List([ 20 | Map({ 21 | height: 100, 22 | width: 250 23 | }), 24 | Map({ 25 | height: 150, 26 | width: 300 27 | }), 28 | Map({ 29 | height: 200, 30 | width: 200 31 | }) 32 | ]) 33 | 34 | const containerWidth = 1100 35 | const top = 100 36 | const additionalHeight = 130 37 | const minWidth = 200 38 | 39 | expect(calcGridRow(top, items, containerWidth, additionalHeight, minWidth)) 40 | .toEqualImmutable( 41 | Map({ 42 | extraItems: List(), 43 | row: Map({ 44 | items: List([ 45 | Map({ 46 | origHeight: 100, 47 | origWidth: 250, 48 | height: 200, 49 | width: 500 50 | }), 51 | Map({ 52 | origHeight: 150, 53 | origWidth: 300, 54 | height: 200, 55 | width: 400 56 | }), 57 | Map({ 58 | origHeight: 200, 59 | origWidth: 200, 60 | height: 200, 61 | width: 200 62 | }) 63 | ]), 64 | top, 65 | height: 330 66 | }) 67 | }) 68 | ) 69 | }) 70 | 71 | it(`should pop extra items`, () => { 72 | const items = List([ 73 | Map({ 74 | height: 100, 75 | width: 250 76 | }), 77 | Map({ 78 | height: 150, 79 | width: 300 80 | }), 81 | Map({ 82 | height: 200, 83 | width: 200 84 | }), 85 | Map({ 86 | height: 200, 87 | width: 150 88 | }) 89 | ]) 90 | 91 | const containerWidth = 900 92 | const top = 100 93 | const additionalHeight = 130 94 | const minWidth = 300 95 | 96 | expect(calcGridRow(top, items, containerWidth, additionalHeight, minWidth)) 97 | .toEqualImmutable( 98 | Map({ 99 | extraItems: List([ 100 | Map({ 101 | height: 200, 102 | width: 200 103 | }), 104 | Map({ 105 | height: 200, 106 | width: 150 107 | }) 108 | ]), 109 | row: Map({ 110 | items: List([ 111 | Map({ 112 | origHeight: 100, 113 | origWidth: 250, 114 | height: 200, 115 | width: 500 116 | }), 117 | Map({ 118 | origHeight: 150, 119 | origWidth: 300, 120 | height: 200, 121 | width: 400 122 | }) 123 | ]), 124 | top, 125 | height: 330 126 | }) 127 | }) 128 | ) 129 | }) 130 | 131 | it(`should calculate row taking in account offset left`, () => { 132 | const items = List([ 133 | Map({ 134 | height: 200, 135 | width: 250 136 | }), 137 | Map({ 138 | height: 200, 139 | width: 300 140 | }), 141 | Map({ 142 | height: 200, 143 | width: 200 144 | }) 145 | ]) 146 | 147 | const containerWidth = 850 148 | const top = 100 149 | const additionalHeight = 130 150 | const offsetLeft = 50 151 | const minWidth = 200 152 | 153 | expect(calcGridRow(top, items, containerWidth, additionalHeight, minWidth, offsetLeft)) 154 | .toEqualImmutable( 155 | Map({ 156 | extraItems: List(), 157 | row: Map({ 158 | items: List([ 159 | Map({ 160 | origHeight: 200, 161 | origWidth: 250, 162 | height: 200, 163 | width: 250 164 | }), 165 | Map({ 166 | origHeight: 200, 167 | origWidth: 300, 168 | height: 200, 169 | width: 300 170 | }), 171 | Map({ 172 | origHeight: 200, 173 | origWidth: 200, 174 | height: 200, 175 | width: 200 176 | }) 177 | ]), 178 | top, 179 | height: 330 180 | }) 181 | }) 182 | ) 183 | }) 184 | 185 | it(`should only lead items to the same height`, () => { 186 | const items = List([ 187 | Map({ 188 | height: 100, 189 | width: 250 190 | }), 191 | Map({ 192 | height: 200, 193 | width: 200 194 | }), 195 | Map({ 196 | height: 300, 197 | width: 300 198 | }) 199 | ]) 200 | 201 | const containerWidth = 900 202 | const top = 100 203 | const additionalHeight = 130 204 | const offsetLeft = 50 205 | const minWidth = 200 206 | 207 | expect(calcGridRow(top, items, containerWidth, additionalHeight, minWidth, offsetLeft, false)) 208 | .toEqualImmutable( 209 | Map({ 210 | extraItems: List(), 211 | row: Map({ 212 | items: List([ 213 | Map({ 214 | origHeight: 100, 215 | origWidth: 250, 216 | height: 200, 217 | width: 500 218 | }), 219 | Map({ 220 | origHeight: 200, 221 | origWidth: 200, 222 | height: 200, 223 | width: 200 224 | }), 225 | Map({ 226 | origHeight: 300, 227 | origWidth: 300, 228 | height: 200, 229 | width: 200 230 | }) 231 | ]), 232 | top, 233 | height: 330 234 | }) 235 | }) 236 | ) 237 | }) 238 | 239 | it(`should pop extra items while leading to min width`, () => { 240 | const items = List([ 241 | Map({ 242 | height: 100, 243 | width: 250 244 | }), 245 | Map({ 246 | height: 200, 247 | width: 200 248 | }), 249 | Map({ 250 | height: 300, 251 | width: 300 252 | }) 253 | ]) 254 | 255 | const containerWidth = 800 256 | const top = 100 257 | const additionalHeight = 130 258 | const offsetLeft = 50 259 | const minWidth = 200 260 | 261 | expect(calcGridRow(top, items, containerWidth, additionalHeight, minWidth, offsetLeft, false)) 262 | .toEqualImmutable( 263 | Map({ 264 | extraItems: List([ 265 | Map({ 266 | height: 300, 267 | width: 300 268 | }) 269 | ]), 270 | row: Map({ 271 | items: List([ 272 | Map({ 273 | origHeight: 100, 274 | origWidth: 250, 275 | height: 200, 276 | width: 500 277 | }), 278 | Map({ 279 | origHeight: 200, 280 | origWidth: 200, 281 | height: 200, 282 | width: 200 283 | }) 284 | ]), 285 | top, 286 | height: 330 287 | }) 288 | }) 289 | ) 290 | }) 291 | }) 292 | 293 | describe(`calcGrid->`, () => { 294 | it(`should fit items into 1100px`, () => { 295 | const items = List([ 296 | Map({ 297 | height: 100, 298 | width: 250 299 | }), 300 | Map({ 301 | height: 150, 302 | width: 300 303 | }), 304 | Map({ 305 | height: 200, 306 | width: 200 307 | }) 308 | ]) 309 | 310 | const containerWidth = 1100 311 | const additionalHeight = 130 312 | const minWidth = 200 313 | 314 | expect(calcGrid(items, additionalHeight, containerWidth, minWidth)) 315 | .toEqualImmutable(Map({ 316 | rows: List([ 317 | Map({ 318 | items: List([ 319 | Map({ 320 | origHeight: 100, 321 | origWidth: 250, 322 | height: 200, 323 | width: 500 324 | }), 325 | Map({ 326 | origHeight: 150, 327 | origWidth: 300, 328 | height: 200, 329 | width: 400 330 | }), 331 | Map({ 332 | origHeight: 200, 333 | origWidth: 200, 334 | height: 200, 335 | width: 200 336 | }) 337 | ]), 338 | top: 0, 339 | height: 330 340 | }) 341 | ]), 342 | height: 330 343 | })) 344 | }) 345 | 346 | 347 | it(`should fit two rows into 1100px with correct top values`, () => { 348 | const items = List([ 349 | Map({ 350 | height: 100, 351 | width: 250 352 | }), 353 | Map({ 354 | height: 150, 355 | width: 300 356 | }), 357 | Map({ 358 | height: 200, 359 | width: 200 360 | }), 361 | Map({ 362 | height: 100, 363 | width: 250 364 | }), 365 | Map({ 366 | height: 150, 367 | width: 300 368 | }), 369 | Map({ 370 | height: 200, 371 | width: 200 372 | }) 373 | ]) 374 | const containerWidth = 1100 375 | const additionalHeight = 130 376 | 377 | expect(calcGrid(items, additionalHeight, containerWidth).size) 378 | .toEqual(2) 379 | }) 380 | 381 | it(`should calculate right tops`, () => { 382 | const items = List([ 383 | Map({ 384 | height: 10, 385 | width: 10 386 | }), 387 | Map({ 388 | height: 10, 389 | width: 10 390 | }), 391 | Map({ 392 | height: 10, 393 | width: 10 394 | }) 395 | ]) 396 | const containerWidth = 10 397 | const additionalHeight = 130 398 | const minWidth = 10 399 | const grid = calcGrid(items, additionalHeight, containerWidth, minWidth) 400 | 401 | expect(grid.get(`rows`).reduce((acc, item) => [ ...acc, item.get(`top`) ], [])) 402 | .toEqual([ 0, 140, 280 ]) 403 | expect(grid.get(`height`)) 404 | .toEqual(420) 405 | }) 406 | 407 | it(`should fit items taking into account min. width limit`, () => { 408 | const items = List([ 409 | Map({ 410 | height: 200, 411 | width: 100 412 | }), 413 | Map({ 414 | height: 200, 415 | width: 200 416 | }), 417 | Map({ 418 | height: 200, 419 | width: 300 420 | }) 421 | ]) 422 | 423 | const containerWidth = 600 424 | const additionalHeight = 130 425 | const minWidth = 200 426 | 427 | expect(calcGrid(items, additionalHeight, containerWidth, minWidth)) 428 | .toEqualImmutable( 429 | Map({ 430 | rows: List([ 431 | Map({ 432 | items: List([ 433 | Map({ 434 | origHeight: 200, 435 | origWidth: 100, 436 | height: 400, 437 | width: 200 438 | }), 439 | Map({ 440 | origHeight: 200, 441 | origWidth: 200, 442 | height: 400, 443 | width: 400 444 | }) 445 | ]), 446 | top: 0, 447 | height: 530 448 | }), 449 | Map({ 450 | items: List([ 451 | Map({ 452 | origHeight: 200, 453 | origWidth: 300, 454 | height: 200, 455 | width: 300 456 | }) 457 | ]), 458 | top: 530, 459 | height: 330 460 | }) 461 | ]), 462 | height: 860 463 | }) 464 | ) 465 | }) 466 | 467 | it(`should calculate grid taking into account padding`, () => { 468 | const items = List([ 469 | Map({ 470 | height: 100, 471 | width: 100 472 | }), 473 | Map({ 474 | height: 100, 475 | width: 100 476 | }) 477 | ]) 478 | 479 | const additionalHeight = 0 480 | const containerWidth = 300 481 | const padding = 100 482 | const minWidth = 100 483 | const offsetLeft = 0 484 | 485 | expect(calcGrid(items, additionalHeight, containerWidth, minWidth, offsetLeft, padding, padding)) 486 | .toEqualImmutable( 487 | Map({ 488 | rows: List([ 489 | Map({ 490 | items: List([ 491 | Map({ 492 | origHeight: 100, 493 | origWidth: 100, 494 | height: 100, 495 | width: 100 496 | }) 497 | ]), 498 | top: 100, 499 | height: 100 500 | }), 501 | Map({ 502 | items: List([ 503 | Map({ 504 | origHeight: 100, 505 | origWidth: 100, 506 | height: 100, 507 | width: 100 508 | }) 509 | ]), 510 | top: 200, 511 | height: 100 512 | }) 513 | ]), 514 | height: 300 515 | }) 516 | ) 517 | }) 518 | }) 519 | 520 | describe(`calcGridExcludeLastRow`, () => { 521 | it(`should return grid without last row`, () => { 522 | const grid = Map({ 523 | rows: List([ 524 | Map({ 525 | items: List(), 526 | top: 0, 527 | height: 140 528 | }), 529 | Map({ 530 | items: List(), 531 | top: 140, 532 | height: 140 533 | }), 534 | Map({ 535 | items: List(), 536 | top: 280, 537 | height: 140 538 | }) 539 | ]), 540 | height: 420 541 | }) 542 | 543 | expect(calcGridExcludeLastRow(grid)) 544 | .toEqualImmutable(Map({ 545 | rows: List([ 546 | Map({ 547 | items: List(), 548 | top: 0, 549 | height: 140 550 | }), 551 | Map({ 552 | items: List(), 553 | top: 140, 554 | height: 140 555 | }) 556 | ]), 557 | height: 280 558 | })) 559 | }) 560 | 561 | it(`should not touch grid if it hasn't rows`, () => { 562 | const grid = Map({ 563 | rows: List(), 564 | height: 0 565 | }) 566 | 567 | expect(calcGridExcludeLastRow(grid)) 568 | .toBe(grid) 569 | }) 570 | }) 571 | 572 | describe(`calcVisibleGrid->`, () => { 573 | it(`should render only 1 row`, () => { 574 | const grid = Map({ 575 | rows: List([ 576 | Map({ 577 | items: List(), 578 | top: 0, 579 | height: 140 580 | }), 581 | Map({ 582 | items: List(), 583 | top: 140, 584 | height: 140 585 | }), 586 | Map({ 587 | items: List(), 588 | top: 280, 589 | height: 140 590 | }) 591 | ]), 592 | height: 420 593 | }) 594 | const visibleAreaHeight = 140 595 | const offset = 140 596 | 597 | expect(calcVisibleGrid(grid, visibleAreaHeight, offset)) 598 | .toEqual([ Map({ 599 | rows: List([ 600 | Map({ 601 | items: List(), 602 | top: 140, 603 | height: 140 604 | }) 605 | ]), 606 | height: 420 607 | }), 1 ]) 608 | }) 609 | 610 | it(`should render only 2 rows`, () => { 611 | const grid = Map({ 612 | rows: List([ 613 | Map({ 614 | items: List(), 615 | top: 0, 616 | height: 140 617 | }), 618 | Map({ 619 | items: List(), 620 | top: 140, 621 | height: 140 622 | }), 623 | Map({ 624 | items: List(), 625 | top: 280, 626 | height: 140 627 | }), 628 | Map({ 629 | items: List(), 630 | top: 420, 631 | height: 140 632 | }) 633 | ]), 634 | height: 560 635 | }) 636 | const visibleAreaHeight = 240 637 | const offset = 140 638 | 639 | expect(calcVisibleGrid(grid, visibleAreaHeight, offset)) 640 | .toEqual([ Map({ 641 | rows: List([ 642 | Map({ 643 | items: List(), 644 | top: 140, 645 | height: 140 646 | }), 647 | Map({ 648 | items: List(), 649 | top: 280, 650 | height: 140 651 | }) 652 | ]), 653 | height: 560 654 | }), 2 ]) 655 | }) 656 | 657 | it(`should render rows if partially visible`, () => { 658 | const grid = Map({ 659 | rows: List([ 660 | Map({ 661 | items: List(), 662 | top: 0, 663 | height: 140 664 | }), 665 | Map({ 666 | items: List(), 667 | top: 140, 668 | height: 140 669 | }), 670 | Map({ 671 | items: List(), 672 | top: 280, 673 | height: 140 674 | }), 675 | Map({ 676 | items: List(), 677 | top: 420, 678 | height: 140 679 | }) 680 | ]), 681 | height: 560 682 | }) 683 | const visibleAreaHeight = 240 684 | const offset = 130 685 | 686 | expect(calcVisibleGrid(grid, visibleAreaHeight, offset)) 687 | .toEqual([ Map({ 688 | rows: List([ 689 | Map({ 690 | items: List(), 691 | top: 0, 692 | height: 140 693 | }), 694 | Map({ 695 | items: List(), 696 | top: 140, 697 | height: 140 698 | }), 699 | Map({ 700 | items: List(), 701 | top: 280, 702 | height: 140 703 | }) 704 | ]), 705 | height: 560 706 | }), 2 ]) 707 | }) 708 | 709 | }) 710 | 711 | describe(`insertItems->`, () => { 712 | it(`should insert items`, () => { 713 | const initialGrid = Map({ 714 | rows: List([ 715 | Map({ 716 | items: List(), 717 | top: 0, 718 | height: 140 719 | }), 720 | Map({ 721 | items: List([ 722 | Map({ 723 | origWidth: 200, 724 | origHeight: 200 725 | }) 726 | ]), 727 | top: 140, 728 | height: 140 729 | }) 730 | ]), 731 | height: 280 732 | }) 733 | 734 | const additionalHeight = 0 735 | const containerWidth = 400 736 | 737 | const items = List([ 738 | Map({ 739 | width: 200, 740 | height: 200 741 | }), 742 | Map({ 743 | width: 200, 744 | height: 200 745 | }) 746 | ]) 747 | 748 | expect(insertItems(initialGrid, items, additionalHeight, containerWidth)) 749 | .toEqualImmutable(Map({ 750 | rows: List([ 751 | Map({ 752 | items: List(), 753 | top: 0, 754 | height: 140 755 | }), 756 | Map({ 757 | items: List([ 758 | Map({ 759 | width: 200, 760 | height: 200, 761 | origWidth: 200, 762 | origHeight: 200 763 | }), 764 | Map({ 765 | width: 200, 766 | height: 200, 767 | origWidth: 200, 768 | origHeight: 200 769 | }) 770 | ]), 771 | top: 140, 772 | height: 200 773 | }), 774 | Map({ 775 | items: List([ 776 | Map({ 777 | width: 200, 778 | height: 200, 779 | origWidth: 200, 780 | origHeight: 200 781 | }) 782 | ]), 783 | top: 340, 784 | height: 200 785 | }) 786 | ]), 787 | height: 540 788 | })) 789 | }) 790 | }) 791 | }) 792 | }) 793 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import { jsdom } from 'jsdom' 2 | 3 | global.document = jsdom(``) 4 | global.window = document.defaultView 5 | global.navigator = global.window.navigator -------------------------------------------------------------------------------- /webpack.config.examples.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | var path = require('path'); 3 | 4 | var glob = require('glob'); 5 | var webpack = require('webpack'); 6 | var env = process.env.NODE_ENV; 7 | 8 | function createConfig (filepath) { 9 | var filename = path.basename(filepath, '.example.js'); 10 | var outputPath = path.dirname(filepath); 11 | 12 | return { 13 | 14 | entry: path.resolve(__dirname, filepath), 15 | 16 | module: { 17 | loaders: [ 18 | { test: /\.js$/, loaders: ['babel-loader'], exclude: /node_modules/ } 19 | ] 20 | }, 21 | 22 | output: { 23 | filename: filename + '.js', 24 | path: path.resolve(__dirname, outputPath) 25 | }, 26 | 27 | plugins: [ 28 | { 29 | apply: function apply(compiler) { 30 | compiler.parser.plugin('expression global', function expressionGlobalPlugin() { 31 | this.state.module.addVariable('global', "(function() { return this; }()) || Function('return this')()") 32 | return false 33 | }) 34 | } 35 | }, 36 | new webpack.optimize.OccurenceOrderPlugin(), 37 | new webpack.DefinePlugin({ 38 | 'process.env.NODE_ENV': JSON.stringify(env) 39 | }) 40 | ] 41 | }; 42 | } 43 | 44 | module.exports = glob.sync('examples/**/*.example.js').map(createConfig); 45 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack'); 4 | var env = process.env.NODE_ENV; 5 | 6 | var reactExternal = { 7 | root: 'React', 8 | commonjs2: 'react', 9 | commonjs: 'react', 10 | amd: 'react' 11 | }; 12 | 13 | var config = { 14 | externals: { 15 | 'react': reactExternal 16 | }, 17 | module: { 18 | loaders: [ 19 | { test: /\.js$/, loaders: ['babel-loader'], exclude: /node_modules/ } 20 | ] 21 | }, 22 | output: { 23 | library: 'ReactAdaptiveGrid', 24 | libraryTarget: 'umd' 25 | }, 26 | plugins: [ 27 | { 28 | apply: function apply(compiler) { 29 | compiler.parser.plugin('expression global', function expressionGlobalPlugin() { 30 | this.state.module.addVariable('global', "(function() { return this; }()) || Function('return this')()") 31 | return false 32 | }) 33 | } 34 | }, 35 | new webpack.optimize.OccurenceOrderPlugin(), 36 | new webpack.DefinePlugin({ 37 | 'process.env.NODE_ENV': JSON.stringify(env) 38 | }) 39 | ] 40 | }; 41 | 42 | if (env === 'production') { 43 | config.plugins.push( 44 | new webpack.optimize.UglifyJsPlugin({ 45 | compressor: { 46 | pure_getters: true, 47 | unsafe: true, 48 | unsafe_comps: true, 49 | screw_ie8: true, 50 | warnings: false 51 | } 52 | }) 53 | ) 54 | }; 55 | 56 | module.exports = config; --------------------------------------------------------------------------------