├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples ├── app.js ├── components │ ├── Operation.js │ ├── Placeholder.js │ └── Widget.js ├── index.html ├── js │ ├── bundle.min.js │ ├── bundle.min.js.map │ ├── vendors.js │ └── vendors.js.map ├── pages │ ├── debounce.js │ ├── decorator.js │ ├── fadein.js │ ├── forcevisible.js │ ├── image.js │ ├── normal.js │ ├── overflow.js │ ├── placeholder.js │ └── scroll.js └── utils │ └── index.js ├── lib ├── decorator.js ├── index.js └── utils │ ├── debounce.js │ ├── event.js │ ├── scrollParent.js │ └── throttle.js ├── package-lock.json ├── package.json ├── src ├── index.jsx └── utils │ ├── debounce.js │ ├── event.js │ ├── scrollParent.js │ └── throttle.js ├── test ├── Test.component.js ├── karma.conf.js └── specs │ ├── events.spec.js │ ├── lazyload.debounce.spec.js │ ├── lazyload.spec.js │ └── lazyload.throttle.spec.js ├── webpack.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["stage-0", "es2015", "react"], 3 | "plugins": ["transform-decorators-legacy"] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "airbnb", 4 | "env": { 5 | "mocha": true, 6 | "browser": true 7 | }, 8 | "globals": { 9 | "chai": true 10 | }, 11 | "rules": { 12 | "comma-dangle": 0, 13 | "no-console": 0, 14 | "react/prefer-stateless-function": 1, 15 | "react/jsx-no-bind": 0, 16 | "arrow-body-style": 1, 17 | "no-nested-ternary": 0, 18 | "no-param-reassign": 0, 19 | "prefer-rest-params": 0, 20 | "max-len": 0, 21 | "no-continue": 0 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | coverage/ 3 | .DS_Store 4 | .idea 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples/ 2 | .babelrc 3 | .eslintrc 4 | webpack.config.js 5 | src/ 6 | test/ 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "10" 5 | - "12" 6 | services: 7 | - xvfb 8 | before_script: 9 | - export CHROME_BIN=chromium-browser 10 | - export DISPLAY=:99.0 11 | - sleep 3 12 | addons: 13 | chrome: stable 14 | script: "./node_modules/karma/bin/karma start test/karma.conf.js --browsers Chrome_travis_ci --single-run --no-auto-watch --capture-timeout 300000" 15 | cache: yarn 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Sen Yang 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Note 2 | 3 | This project is now currently maintained by 4 | [@ameerthehacker](https://github.com/ameerthehacker), please reach out to him on any issues or help. 5 | 6 | ---- 7 | 8 | # react-lazyload [![Build Status](https://travis-ci.org/twobin/react-lazyload.svg)](https://travis-ci.org/twobin/react-lazyload) [![npm version](https://badge.fury.io/js/react-lazyload.svg)](http://badge.fury.io/js/react-lazyload) [![Coverage Status](https://coveralls.io/repos/github/jasonslyvia/react-lazyload/badge.svg?branch=master)](https://coveralls.io/github/jasonslyvia/react-lazyload?branch=master) [![npm downloads](https://img.shields.io/npm/dm/react-lazyload.svg)](https://www.npmjs.com/package/react-lazyload) 9 | 10 | Lazyload your Components, Images or anything matters the performance. 11 | 12 | [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/react-lazyload?tab=posts) 13 | 14 | [Demo](//twobin.github.io/react-lazyload/examples/) 15 | 16 | ## Why it's better 17 | 18 | - Take performance in mind, only 2 event listeners for all lazy-loaded components 19 | - Support both `one-time lazy load` and `continuous lazy load` mode 20 | - `scroll` / `resize` event handler is throttled so you won't suffer frequent update, you can switch to debounce mode too 21 | - Decorator supported 22 | - Server Side Rendering friendly 23 | - Thoroughly tested 24 | 25 | ## Installation 26 | 27 | > 2.0.0 is finally out, read [Upgrade Guide](https://github.com/twobin/react-lazyload/wiki/Upgrade-Guide), it's almost painless to upgrade! 28 | > 3.0.0 fixes the findDomNode warning through usage of React ref, and the following are the changes you need to be aware of 29 | 30 | * Now we have an extra div wrapping the lazy loaded component for the React ref to work 31 | * We can understand that it is an extra DOM node, and we are working to optimize that if possible 32 | * It might break your UI or snapshot tests based on your usage 33 | * To customize the styling to the extra div please refer [here](#classNamePrefix) 34 | * Found any other problem, please feel free to leave a comment over [here](https://github.com/twobin/react-lazyload/issues/310) 35 | 36 | ``` 37 | $ npm install --save react-lazyload 38 | ``` 39 | 40 | ## Usage 41 | 42 | ```javascript 43 | import React from 'react'; 44 | import ReactDOM from 'react-dom'; 45 | import LazyLoad from 'react-lazyload'; 46 | import MyComponent from './MyComponent'; 47 | 48 | const App = () => { 49 | return ( 50 |
51 | 52 | /* 53 | Lazy loading images is supported out of box, 54 | no extra config needed, set `height` for better 55 | experience 56 | */ 57 | 58 | 59 | /* Once this component is loaded, LazyLoad will 60 | not care about it anymore, set this to `true` 61 | if you're concerned about improving performance */ 62 | 63 | 64 | 65 | /* This component will be loaded when it's top 66 | edge is 100px from viewport. It's useful to 67 | make user ignorant about lazy load effect. */ 68 | 69 | 70 | 71 | 72 | 73 |
74 | ); 75 | }; 76 | 77 | ReactDOM.render(, document.body); 78 | ``` 79 | 80 | If you want to have your component lazyloaded by default, try this handy decorator: 81 | 82 | ```javascript 83 | import { lazyload } from 'react-lazyload'; 84 | 85 | @lazyload({ 86 | height: 200, 87 | once: true, 88 | offset: 100 89 | }) 90 | class MyComponent extends React.Component { 91 | render() { 92 | return
this component is lazyloaded by default!
; 93 | } 94 | } 95 | ``` 96 | 97 | ## Special Tips 98 | 99 | You should be aware that your component will only be mounted when it's visible in viewport, before that a placeholder will be rendered. 100 | 101 | So you can safely send request in your component's `componentDidMount` without worrying about performance loss or add some pretty entering effects, see this [demo](https://twobin.github.io/react-lazyload/examples/#/fadein) for more detail. 102 | 103 | ## Props 104 | 105 | ### children 106 | 107 | Type: Node Default: undefined 108 | 109 | **NOTICE** 110 | Only one child is allowed to be passed. 111 | 112 | ### scrollContainer 113 | 114 | Type: String/DOM node Default: undefined 115 | 116 | Pass a query selector string or DOM node. LazyLoad will attach to the window object's scroll events if no container is passed. 117 | 118 | ### height 119 | 120 | Type: Number/String Default: undefined 121 | 122 | In the first round of render, LazyLoad will render a placeholder for your component if no placeholder is provided and measure if this component is visible. Set `height` properly will make LazyLoad calculate more precisely. The value can be number or string like `'100%'`. You can also use css to set the height of the placeholder instead of using `height`. 123 | 124 | ### once 125 | 126 | Type: Bool Default: false 127 | 128 | Once the lazy loaded component is loaded, do not detect scroll/resize event anymore. Useful for images or simple components. 129 | 130 | ### offset 131 | 132 | Type: Number/Array(Number) Default: 0 133 | 134 | Say if you want to preload a component even if it's 100px below the viewport (user have to scroll 100px more to see this component), you can set `offset` props to `100`. On the other hand, if you want to delay loading a component even if it's top edge has already appeared at viewport, set `offset` to negative number. 135 | 136 | Library supports horizontal lazy load out of the box. So when you provide this prop with number like `100` it will automatically set left edge offset to `100` and top edge to `100`; 137 | 138 | If you provide this prop with array like `[100, 200]`, it will set left edge offset to `100` and top offset to `200`. 139 | 140 | ### scroll 141 | 142 | Type: Bool Default: true 143 | 144 | Listen and react to scroll event. 145 | 146 | ### resize 147 | 148 | Type: Bool Default: false 149 | 150 | Respond to `resize` event, set it to `true` if you do need LazyLoad listen resize event. 151 | 152 | **NOTICE** If you tend to support legacy IE, set this props carefully, refer to [this question](http://stackoverflow.com/questions/1852751/window-resize-event-firing-in-internet-explorer) for further reading. 153 | 154 | ### overflow 155 | 156 | Type: Bool Default: false 157 | 158 | If lazy loading components inside a overflow container, set this to `true`. Also make sure a `position` property other than `static` has been set to your overflow container. 159 | 160 | [demo](https://twobin.github.io/react-lazyload/examples/#/overflow) 161 | 162 | ### placeholder 163 | 164 | Type: Any Default: undefined 165 | 166 | Specify a placeholder for your lazy loaded component. 167 | 168 | [demo](https://twobin.github.io/react-lazyload/examples/#/placeholder) 169 | 170 | **If you provide your own placeholder, do remember add appropriate `height` or `minHeight` to your placeholder element for better lazyload performance.** 171 | 172 | ### unmountIfInvisible 173 | 174 | Type: Bool Default: false 175 | 176 | The lazy loaded component is unmounted and replaced by the placeholder when it is no longer visible in the viewport. 177 | 178 | 179 | ### debounce/throttle 180 | 181 | Type: Bool / Number Default: undefined 182 | 183 | Lazyload will try to use [passive event](https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md) by default to improve scroll/resize event handler's performance. If you prefer control this behaviour by yourself, you can set `debounce` or `throttle` to enable built in delay feature. 184 | 185 | If you provide a number, that will be how many `ms` to wait; if you provide `true`, the wait time defaults to `300ms`. 186 | 187 | **NOTICE** Set `debounce` / `throttle` to all lazy loaded components unanimously, if you don't, the first occurrence is respected. 188 | 189 | [demo](https://twobin.github.io/react-lazyload/examples/#/debounce) 190 | 191 | ### classNamePrefix 192 | 193 | Type: String Default: `lazyload` 194 | 195 | While rendering, Lazyload will add some elements to the component tree in addition to the wrapped component children. 196 | 197 | The `classNamePrefix` prop allows the user to supply their own custom class prefix to help: 198 | # Avoid class conflicts on an implementing app 199 | # Allow easier custom styling 200 | 201 | These being: 202 | # A wrapper div, which is present at all times (default ) 203 | 204 | ### style 205 | 206 | Type: Object Default: undefined 207 | 208 | Similar to [classNamePrefix](#classNamePrefix), the `style` prop allows users to pass custom CSS styles to wrapper div. 209 | 210 | ### wheel 211 | 212 | **DEPRECATED NOTICE** 213 | This props is not supported anymore, try set `overflow` for lazy loading in overflow containers. 214 | 215 | ## Utility 216 | 217 | ### forceCheck 218 | 219 | It is available to manually trigger checking for elements in viewport. Helpful when LazyLoad components enter the viewport without resize or scroll events, e.g. when the components' container was hidden then become visible. 220 | 221 | Import `forceCheck`: 222 | 223 | ```javascript 224 | import { forceCheck } from 'react-lazyload'; 225 | ``` 226 | 227 | Then call the function: 228 | 229 | ```javascript 230 | forceCheck(); 231 | ``` 232 | 233 | ### forceVisible 234 | 235 | Forces the component to display regardless of whether the element is visible in the viewport. 236 | 237 | ```javascript 238 | import { forceVisible } from 'react-lazyload'; 239 | ``` 240 | 241 | Then call the function: 242 | 243 | ```javascript 244 | forceVisible(); 245 | ``` 246 | 247 | ## Scripts 248 | 249 | ``` 250 | $ npm run demo:watch 251 | $ npm run build 252 | ``` 253 | 254 | ## Who should use it 255 | 256 | Let's say there is a `fixed` date picker on the page, when user picks a different date, all components displaying data should send ajax requests with new date parameter to retreive updated data, even many of them aren't visible in viewport. This makes server load furious when there are too many requests in one time. 257 | 258 | Using `LazyLoad` component will help ease this situation by only updating components visible in viewport. 259 | 260 | ## Contributors 261 | 262 | 1. [lancehub](https://github.com/lancehub) 263 | 2. [doug-wade](https://github.com/doug-wade) 264 | 3. [ameerthehacker](https://github.com/ameerthehacker) 265 | 266 | 267 | ## License 268 | 269 | MIT 270 | -------------------------------------------------------------------------------- /examples/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { Router, Route, hashHistory, Link } from 'react-router'; 4 | 5 | import Decorator from './pages/decorator'; 6 | import Normal from './pages/normal'; 7 | import Scroll from './pages/scroll'; 8 | import Overflow from './pages/overflow'; 9 | import Image from './pages/image'; 10 | import Debounce from './pages/debounce'; 11 | import Placeholder from './pages/placeholder'; 12 | import FadeIn from './pages/fadein'; 13 | import ForceVisible from './pages/forcevisible'; 14 | 15 | const Home = () => ( 16 | 27 | ); 28 | 29 | const routes = ( 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | 44 | ReactDOM.render(routes, document.getElementById('app')); 45 | -------------------------------------------------------------------------------- /examples/components/Operation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Link} from 'react-router'; 3 | 4 | export default ({ type, onClickUpdate, noExtra }) => ( 5 |
6 |
7 | 10 | source 11 | 12 | back 13 |
14 | {!noExtra && ( 15 |
16 | Update 17 |

18 | Clicking this button will make all Widgets in visible area 19 | reload data from server. 20 |

21 |

22 | Pay attention to props from parent block in Widget 23 | to identify how LazyLoad works. 24 |

25 |
26 | )} 27 |
28 | ); 29 | -------------------------------------------------------------------------------- /examples/components/Placeholder.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function Placeholder() { 4 | return ( 5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /examples/components/Widget.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | 3 | class Widget extends Component { 4 | constructor(props) { 5 | super(props); 6 | 7 | this.state = { 8 | isReady: true, 9 | count: 1 10 | }; 11 | } 12 | 13 | componentWillReceiveProps(nextProps) { 14 | if (nextProps.id !== this.props.id && this.props.id) { 15 | this.setState({ 16 | isReady: false 17 | }); 18 | 19 | setTimeout(() => { 20 | this.setState({ 21 | isReady: true, 22 | count: this.state.count + 1 23 | }); 24 | }, 500); 25 | } else { 26 | this.setState({ 27 | isReady: true 28 | }); 29 | } 30 | } 31 | 32 | render() { 33 | return this.state.isReady ? ( 34 |
35 | {this.props.count} 36 | {this.props.once ? ( 37 |
38 | 39 | <LazyLoad once>
40 |   <Widget />
41 | </LazyLoad> 42 |
43 |
44 | ) : ( 45 |
46 | 47 | <LazyLoad>
48 |   <Widget />
49 | </LazyLoad> 50 |
51 |
52 | )} 53 |

render times: {this.state.count}

54 |

props from parent: {this.props.id}

55 |
56 | ) : ( 57 |
58 | loading... 59 |
60 | ); 61 | } 62 | } 63 | 64 | export default Widget; 65 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | react-lazyload demo 5 | 14 | 180 | 181 | 182 | Fork me on GitHub 183 |
184 | 185 | 186 | 187 | -------------------------------------------------------------------------------- /examples/js/vendors.js: -------------------------------------------------------------------------------- 1 | !function(e){function n(t){if(r[t])return r[t].exports;var a=r[t]={exports:{},id:t,loaded:!1};return e[t].call(a.exports,a,a.exports,n),a.loaded=!0,a.exports}var t=window.webpackJsonp;window.webpackJsonp=function(o,l){for(var p,s,c=0,i=[];c { 14 | return { 15 | uniqueId: id, 16 | once: [6, 7].indexOf(index) > -1 17 | }; 18 | }) 19 | }; 20 | } 21 | 22 | handleClick() { 23 | const id = uniqueId(); 24 | 25 | this.setState({ 26 | arr: this.state.arr.map(el => { 27 | return { 28 | ...el, 29 | uniqueId: id 30 | }; 31 | }) 32 | }); 33 | } 34 | 35 | render() { 36 | return ( 37 |
38 | 39 |
40 | {this.state.arr.map((el, index) => { 41 | return ( 42 | 43 | 44 | 45 | ); 46 | })} 47 |
48 |
49 | ); 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /examples/pages/decorator.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { lazyload } from '../../src/'; 3 | import Widget from '../components/Widget'; 4 | import Operation from '../components/Operation'; 5 | import { uniqueId } from '../utils'; 6 | 7 | @lazyload({ 8 | height: 200, 9 | throttle: 100 10 | }) 11 | class MyWidget extends Component { 12 | render() { 13 | return ; 14 | } 15 | } 16 | 17 | export default class Decorator extends Component { 18 | constructor() { 19 | super(); 20 | 21 | const id = uniqueId(); 22 | this.state = { 23 | arr: Array.apply(null, Array(20)).map((a, index) => ({ 24 | uniqueId: id, 25 | once: [6, 7].indexOf(index) > -1 26 | })) 27 | }; 28 | } 29 | 30 | handleClick() { 31 | const id = uniqueId(); 32 | 33 | this.setState({ 34 | arr: this.state.arr.map(el => ({ 35 | ...el, 36 | uniqueId: id 37 | })) 38 | }); 39 | } 40 | 41 | render() { 42 | return ( 43 |
44 | 45 |
46 | {this.state.arr.map((el, index) => { 47 | return ( 48 | 49 | ); 50 | })} 51 |
52 |
53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/pages/fadein.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { CSSTransitionGroup } from 'react-transition-group'; 3 | 4 | import Lazyload from '../../src/'; 5 | import Operation from '../components/Operation'; 6 | 7 | export default class FadeIn extends Component { 8 | render() { 9 | return ( 10 |
11 | 12 |
13 | 14 | 20 | 21 | 22 | 23 | 24 | 30 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 50 | 51 | 52 | 53 | 54 | 60 | 61 | 62 | 63 | 64 | 70 | 71 | 72 | 73 | 74 | 80 | 81 | 82 | 83 | 84 | 90 | 91 | 92 | 93 |
94 |
95 | ); 96 | } 97 | } 98 | 99 | -------------------------------------------------------------------------------- /examples/pages/forcevisible.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { forceVisible } from '../../src/'; 3 | import Normal from './normal'; 4 | 5 | export default class ForceVisible extends Component { 6 | componentDidMount() { 7 | forceVisible(); 8 | } 9 | render() { 10 | return ( 11 | 12 | ); 13 | } 14 | } 15 | 16 | -------------------------------------------------------------------------------- /examples/pages/image.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Lazyload from '../../src/'; 3 | import Operation from '../components/Operation'; 4 | 5 | export default class Image extends Component { 6 | render() { 7 | return ( 8 |
9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |
36 |
37 | ); 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /examples/pages/normal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import LazyLoad from '../../src/'; 3 | import Widget from '../components/Widget'; 4 | import Operation from '../components/Operation'; 5 | import { uniqueId } from '../utils'; 6 | 7 | export default class Normal extends Component { 8 | constructor() { 9 | super(); 10 | 11 | const id = uniqueId(); 12 | this.state = { 13 | arr: Array.apply(null, Array(20)).map((a, index) => { 14 | return { 15 | uniqueId: id, 16 | once: [6, 7].indexOf(index) > -1 17 | }; 18 | }) 19 | }; 20 | } 21 | 22 | handleClick() { 23 | const id = uniqueId(); 24 | 25 | this.setState({ 26 | arr: this.state.arr.map(el => { 27 | return { 28 | ...el, 29 | uniqueId: id 30 | }; 31 | }) 32 | }); 33 | } 34 | 35 | render() { 36 | return ( 37 |
38 | 39 |
40 | {this.state.arr.map((el, index) => { 41 | return ( 42 | 43 | 44 | 45 | ); 46 | })} 47 |
48 |
49 | ); 50 | } 51 | } 52 | 53 | -------------------------------------------------------------------------------- /examples/pages/overflow.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import LazyLoad from '../../src/'; 3 | import Widget from '../components/Widget'; 4 | import Operation from '../components/Operation'; 5 | import {uniqueId} from '../utils'; 6 | 7 | export default class Overflow extends Component { 8 | constructor() { 9 | super(); 10 | 11 | const id = uniqueId(); 12 | this.state = { 13 | arr: Array.apply(null, Array(20)).map((a, index) => { 14 | return { 15 | uniqueId: id, 16 | once: [6, 7].indexOf(index) > -1 17 | }; 18 | }) 19 | }; 20 | } 21 | 22 | handleClick() { 23 | const id = uniqueId(); 24 | 25 | this.setState({ 26 | arr: this.state.arr.map(el => { 27 | return { 28 | ...el, 29 | uniqueId: id 30 | }; 31 | }) 32 | }); 33 | } 34 | 35 | render() { 36 | return ( 37 |
38 | 39 |

LazyLoad in Overflow Container

40 |
41 | {this.state.arr.map((el, index) => { 42 | return ( 43 | 44 | 45 | 46 | ); 47 | })} 48 |
49 |
50 | ); 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /examples/pages/placeholder.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import LazyLoad from '../../src/'; 3 | import Widget from '../components/Widget'; 4 | import Operation from '../components/Operation'; 5 | import { uniqueId } from '../utils'; 6 | import PlaceholderComponent from '../components/Placeholder'; 7 | 8 | export default class Placeholder extends Component { 9 | constructor() { 10 | super(); 11 | 12 | const id = uniqueId(); 13 | this.state = { 14 | arr: Array.apply(null, Array(20)).map((a, index) => { 15 | return { 16 | uniqueId: id, 17 | once: [6, 7].indexOf(index) > -1 18 | }; 19 | }) 20 | }; 21 | } 22 | 23 | handleClick() { 24 | const id = uniqueId(); 25 | 26 | this.setState({ 27 | arr: this.state.arr.map(el => { 28 | return { 29 | ...el, 30 | uniqueId: id 31 | }; 32 | }) 33 | }); 34 | } 35 | 36 | render() { 37 | return ( 38 |
39 | 40 |
41 | {this.state.arr.map((el, index) => { 42 | return ( 43 | } debounce={500}> 45 | 46 | 47 | ); 48 | })} 49 |
50 |
51 | ); 52 | } 53 | } 54 | 55 | -------------------------------------------------------------------------------- /examples/pages/scroll.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import LazyLoad from '../../src/'; 3 | import Widget from '../components/Widget'; 4 | import Operation from '../components/Operation'; 5 | import { uniqueId } from '../utils'; 6 | 7 | export default class Scroll extends Component { 8 | constructor() { 9 | super(); 10 | 11 | const id = uniqueId(); 12 | this.state = { 13 | arr: Array.apply(null, Array(20)).map((a, index) => ({ 14 | uniqueId: id, 15 | once: [6, 7].indexOf(index) > -1 16 | })) 17 | }; 18 | } 19 | 20 | handleClick() { 21 | const id = uniqueId(); 22 | 23 | this.setState({ 24 | arr: this.state.arr.map(el => ({ ...el, uniqueId: id })) 25 | }); 26 | } 27 | 28 | handleQuickJump(index, e) { 29 | if (e) { 30 | e.preventDefault(); 31 | } 32 | 33 | const nodeList = document.querySelectorAll('.widget-list .widget-wrapper'); 34 | if (nodeList[index]) { 35 | window.scrollTo(0, nodeList[index].getBoundingClientRect().top + window.pageYOffset); 36 | } 37 | } 38 | 39 | render() { 40 | return ( 41 |
42 | 43 |
44 |

Quick jump to:

45 | {this.state.arr.map((el, index) => ( 46 | {index + 1} 47 | ))} 48 |
49 |
50 | {this.state.arr.map((el, index) => ( 51 |
52 | 53 | 54 | 55 |
56 | ))} 57 |
58 |
59 | ); 60 | } 61 | } 62 | 63 | -------------------------------------------------------------------------------- /examples/utils/index.js: -------------------------------------------------------------------------------- 1 | export function uniqueId() { 2 | return (Math.random().toString(36) + '00000000000000000').slice(2, 10); 3 | } 4 | -------------------------------------------------------------------------------- /lib/decorator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _react = require('react'); 10 | 11 | var _react2 = _interopRequireDefault(_react); 12 | 13 | var _index = require('./index'); 14 | 15 | var _index2 = _interopRequireDefault(_index); 16 | 17 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 18 | 19 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 20 | 21 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 22 | 23 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 24 | 25 | var getDisplayName = function getDisplayName(WrappedComponent) { 26 | return WrappedComponent.displayName || WrappedComponent.name || 'Component'; 27 | }; 28 | 29 | exports.default = function () { 30 | var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 31 | return function lazyload(WrappedComponent) { 32 | return function (_Component) { 33 | _inherits(LazyLoadDecorated, _Component); 34 | 35 | function LazyLoadDecorated() { 36 | _classCallCheck(this, LazyLoadDecorated); 37 | 38 | var _this = _possibleConstructorReturn(this, (LazyLoadDecorated.__proto__ || Object.getPrototypeOf(LazyLoadDecorated)).call(this)); 39 | 40 | _this.displayName = 'LazyLoad' + getDisplayName(WrappedComponent); 41 | return _this; 42 | } 43 | 44 | _createClass(LazyLoadDecorated, [{ 45 | key: 'render', 46 | value: function render() { 47 | return _react2.default.createElement( 48 | _index2.default, 49 | options, 50 | _react2.default.createElement(WrappedComponent, this.props) 51 | ); 52 | } 53 | }]); 54 | 55 | return LazyLoadDecorated; 56 | }(_react.Component); 57 | }; 58 | }; -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.forceVisible = exports.forceCheck = exports.lazyload = undefined; 7 | 8 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 9 | 10 | var _react = require('react'); 11 | 12 | var _react2 = _interopRequireDefault(_react); 13 | 14 | var _propTypes = require('prop-types'); 15 | 16 | var _propTypes2 = _interopRequireDefault(_propTypes); 17 | 18 | var _event = require('./utils/event'); 19 | 20 | var _scrollParent = require('./utils/scrollParent'); 21 | 22 | var _scrollParent2 = _interopRequireDefault(_scrollParent); 23 | 24 | var _debounce = require('./utils/debounce'); 25 | 26 | var _debounce2 = _interopRequireDefault(_debounce); 27 | 28 | var _throttle = require('./utils/throttle'); 29 | 30 | var _throttle2 = _interopRequireDefault(_throttle); 31 | 32 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 33 | 34 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 35 | 36 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 37 | 38 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /** 39 | * react-lazyload 40 | */ 41 | 42 | 43 | var defaultBoundingClientRect = { 44 | top: 0, 45 | right: 0, 46 | bottom: 0, 47 | left: 0, 48 | width: 0, 49 | height: 0 50 | }; 51 | var LISTEN_FLAG = 'data-lazyload-listened'; 52 | var listeners = []; 53 | var pending = []; 54 | 55 | // try to handle passive events 56 | var passiveEventSupported = false; 57 | try { 58 | var opts = Object.defineProperty({}, 'passive', { 59 | get: function get() { 60 | passiveEventSupported = true; 61 | } 62 | }); 63 | window.addEventListener('test', null, opts); 64 | } catch (e) {} 65 | // if they are supported, setup the optional params 66 | // IMPORTANT: FALSE doubles as the default CAPTURE value! 67 | var passiveEvent = passiveEventSupported ? { capture: false, passive: true } : false; 68 | 69 | /** 70 | * Check if `component` is visible in overflow container `parent` 71 | * @param {node} component React component 72 | * @param {node} parent component's scroll parent 73 | * @return {bool} 74 | */ 75 | var checkOverflowVisible = function checkOverflowVisible(component, parent) { 76 | var node = component.ref; 77 | 78 | var parentTop = void 0; 79 | var parentLeft = void 0; 80 | var parentHeight = void 0; 81 | var parentWidth = void 0; 82 | 83 | try { 84 | var _parent$getBoundingCl = parent.getBoundingClientRect(); 85 | 86 | parentTop = _parent$getBoundingCl.top; 87 | parentLeft = _parent$getBoundingCl.left; 88 | parentHeight = _parent$getBoundingCl.height; 89 | parentWidth = _parent$getBoundingCl.width; 90 | } catch (e) { 91 | parentTop = defaultBoundingClientRect.top; 92 | parentLeft = defaultBoundingClientRect.left; 93 | parentHeight = defaultBoundingClientRect.height; 94 | parentWidth = defaultBoundingClientRect.width; 95 | } 96 | 97 | var windowInnerHeight = window.innerHeight || document.documentElement.clientHeight; 98 | var windowInnerWidth = window.innerWidth || document.documentElement.clientWidth; 99 | 100 | // calculate top and height of the intersection of the element's scrollParent and viewport 101 | var intersectionTop = Math.max(parentTop, 0); // intersection's top relative to viewport 102 | var intersectionLeft = Math.max(parentLeft, 0); // intersection's left relative to viewport 103 | var intersectionHeight = Math.min(windowInnerHeight, parentTop + parentHeight) - intersectionTop; // height 104 | var intersectionWidth = Math.min(windowInnerWidth, parentLeft + parentWidth) - intersectionLeft; // width 105 | 106 | // check whether the element is visible in the intersection 107 | var top = void 0; 108 | var left = void 0; 109 | var height = void 0; 110 | var width = void 0; 111 | 112 | try { 113 | var _node$getBoundingClie = node.getBoundingClientRect(); 114 | 115 | top = _node$getBoundingClie.top; 116 | left = _node$getBoundingClie.left; 117 | height = _node$getBoundingClie.height; 118 | width = _node$getBoundingClie.width; 119 | } catch (e) { 120 | top = defaultBoundingClientRect.top; 121 | left = defaultBoundingClientRect.left; 122 | height = defaultBoundingClientRect.height; 123 | width = defaultBoundingClientRect.width; 124 | } 125 | 126 | var offsetTop = top - intersectionTop; // element's top relative to intersection 127 | var offsetLeft = left - intersectionLeft; // element's left relative to intersection 128 | 129 | var offsets = Array.isArray(component.props.offset) ? component.props.offset : [component.props.offset, component.props.offset]; // Be compatible with previous API 130 | 131 | return offsetTop - offsets[0] <= intersectionHeight && offsetTop + height + offsets[1] >= 0 && offsetLeft - offsets[0] <= intersectionWidth && offsetLeft + width + offsets[1] >= 0; 132 | }; 133 | 134 | /** 135 | * Check if `component` is visible in document 136 | * @param {node} component React component 137 | * @return {bool} 138 | */ 139 | var checkNormalVisible = function checkNormalVisible(component) { 140 | var node = component.ref; 141 | 142 | // If this element is hidden by css rules somehow, it's definitely invisible 143 | if (!(node.offsetWidth || node.offsetHeight || node.getClientRects().length)) return false; 144 | 145 | var top = void 0; 146 | var elementHeight = void 0; 147 | 148 | try { 149 | var _node$getBoundingClie2 = node.getBoundingClientRect(); 150 | 151 | top = _node$getBoundingClie2.top; 152 | elementHeight = _node$getBoundingClie2.height; 153 | } catch (e) { 154 | top = defaultBoundingClientRect.top; 155 | elementHeight = defaultBoundingClientRect.height; 156 | } 157 | 158 | var windowInnerHeight = window.innerHeight || document.documentElement.clientHeight; 159 | 160 | var offsets = Array.isArray(component.props.offset) ? component.props.offset : [component.props.offset, component.props.offset]; // Be compatible with previous API 161 | 162 | return top - offsets[0] <= windowInnerHeight && top + elementHeight + offsets[1] >= 0; 163 | }; 164 | 165 | /** 166 | * Detect if element is visible in viewport, if so, set `visible` state to true. 167 | * If `once` prop is provided true, remove component as listener after checkVisible 168 | * 169 | * @param {React} component React component that respond to scroll and resize 170 | */ 171 | var checkVisible = function checkVisible(component) { 172 | var node = component.ref; 173 | if (!(node instanceof HTMLElement)) { 174 | return; 175 | } 176 | 177 | var parent = (0, _scrollParent2.default)(node); 178 | var isOverflow = component.props.overflow && parent !== node.ownerDocument && parent !== document && parent !== document.documentElement; 179 | var visible = isOverflow ? checkOverflowVisible(component, parent) : checkNormalVisible(component); 180 | if (visible) { 181 | // Avoid extra render if previously is visible 182 | if (!component.visible) { 183 | if (component.props.once) { 184 | pending.push(component); 185 | } 186 | 187 | component.visible = true; 188 | component.forceUpdate(); 189 | } 190 | } else if (!(component.props.once && component.visible)) { 191 | component.visible = false; 192 | if (component.props.unmountIfInvisible) { 193 | component.forceUpdate(); 194 | } 195 | } 196 | }; 197 | 198 | var purgePending = function purgePending() { 199 | pending.forEach(function (component) { 200 | var index = listeners.indexOf(component); 201 | if (index !== -1) { 202 | listeners.splice(index, 1); 203 | } 204 | }); 205 | 206 | pending = []; 207 | }; 208 | 209 | var lazyLoadHandler = function lazyLoadHandler() { 210 | for (var i = 0; i < listeners.length; ++i) { 211 | var listener = listeners[i]; 212 | checkVisible(listener); 213 | } 214 | // Remove `once` component in listeners 215 | purgePending(); 216 | }; 217 | 218 | /** 219 | * Forces the component to display regardless of whether the element is visible in the viewport. 220 | */ 221 | var forceVisible = function forceVisible() { 222 | for (var i = 0; i < listeners.length; ++i) { 223 | var listener = listeners[i]; 224 | listener.visible = true; 225 | listener.forceUpdate(); 226 | } 227 | // Remove `once` component in listeners 228 | purgePending(); 229 | }; 230 | 231 | // Depending on component's props 232 | var delayType = void 0; 233 | var finalLazyLoadHandler = null; 234 | 235 | var isString = function isString(string) { 236 | return typeof string === 'string'; 237 | }; 238 | 239 | var LazyLoad = function (_Component) { 240 | _inherits(LazyLoad, _Component); 241 | 242 | function LazyLoad(props) { 243 | _classCallCheck(this, LazyLoad); 244 | 245 | var _this = _possibleConstructorReturn(this, (LazyLoad.__proto__ || Object.getPrototypeOf(LazyLoad)).call(this, props)); 246 | 247 | _this.visible = false; 248 | _this.setRef = _this.setRef.bind(_this); 249 | return _this; 250 | } 251 | 252 | _createClass(LazyLoad, [{ 253 | key: 'componentDidMount', 254 | value: function componentDidMount() { 255 | // It's unlikely to change delay type on the fly, this is mainly 256 | // designed for tests 257 | var scrollport = window; 258 | var scrollContainer = this.props.scrollContainer; 259 | 260 | if (scrollContainer) { 261 | if (isString(scrollContainer)) { 262 | scrollport = scrollport.document.querySelector(scrollContainer); 263 | } 264 | } 265 | var needResetFinalLazyLoadHandler = this.props.debounce !== undefined && delayType === 'throttle' || delayType === 'debounce' && this.props.debounce === undefined; 266 | 267 | if (needResetFinalLazyLoadHandler) { 268 | (0, _event.off)(scrollport, 'scroll', finalLazyLoadHandler, passiveEvent); 269 | (0, _event.off)(window, 'resize', finalLazyLoadHandler, passiveEvent); 270 | finalLazyLoadHandler = null; 271 | } 272 | 273 | if (!finalLazyLoadHandler) { 274 | if (this.props.debounce !== undefined) { 275 | finalLazyLoadHandler = (0, _debounce2.default)(lazyLoadHandler, typeof this.props.debounce === 'number' ? this.props.debounce : 300); 276 | delayType = 'debounce'; 277 | } else if (this.props.throttle !== undefined) { 278 | finalLazyLoadHandler = (0, _throttle2.default)(lazyLoadHandler, typeof this.props.throttle === 'number' ? this.props.throttle : 300); 279 | delayType = 'throttle'; 280 | } else { 281 | finalLazyLoadHandler = lazyLoadHandler; 282 | } 283 | } 284 | 285 | if (this.props.overflow) { 286 | var parent = (0, _scrollParent2.default)(this.ref); 287 | if (parent && typeof parent.getAttribute === 'function') { 288 | var listenerCount = 1 + +parent.getAttribute(LISTEN_FLAG); 289 | if (listenerCount === 1) { 290 | parent.addEventListener('scroll', finalLazyLoadHandler, passiveEvent); 291 | } 292 | parent.setAttribute(LISTEN_FLAG, listenerCount); 293 | } 294 | } else if (listeners.length === 0 || needResetFinalLazyLoadHandler) { 295 | var _props = this.props, 296 | scroll = _props.scroll, 297 | resize = _props.resize; 298 | 299 | 300 | if (scroll) { 301 | (0, _event.on)(scrollport, 'scroll', finalLazyLoadHandler, passiveEvent); 302 | } 303 | 304 | if (resize) { 305 | (0, _event.on)(window, 'resize', finalLazyLoadHandler, passiveEvent); 306 | } 307 | } 308 | 309 | listeners.push(this); 310 | checkVisible(this); 311 | } 312 | }, { 313 | key: 'shouldComponentUpdate', 314 | value: function shouldComponentUpdate() { 315 | return this.visible; 316 | } 317 | }, { 318 | key: 'componentWillUnmount', 319 | value: function componentWillUnmount() { 320 | if (this.props.overflow) { 321 | var parent = (0, _scrollParent2.default)(this.ref); 322 | if (parent && typeof parent.getAttribute === 'function') { 323 | var listenerCount = +parent.getAttribute(LISTEN_FLAG) - 1; 324 | if (listenerCount === 0) { 325 | parent.removeEventListener('scroll', finalLazyLoadHandler, passiveEvent); 326 | parent.removeAttribute(LISTEN_FLAG); 327 | } else { 328 | parent.setAttribute(LISTEN_FLAG, listenerCount); 329 | } 330 | } 331 | } 332 | 333 | var index = listeners.indexOf(this); 334 | if (index !== -1) { 335 | listeners.splice(index, 1); 336 | } 337 | 338 | if (listeners.length === 0 && typeof window !== 'undefined') { 339 | (0, _event.off)(window, 'resize', finalLazyLoadHandler, passiveEvent); 340 | (0, _event.off)(window, 'scroll', finalLazyLoadHandler, passiveEvent); 341 | } 342 | } 343 | }, { 344 | key: 'setRef', 345 | value: function setRef(element) { 346 | if (element) { 347 | this.ref = element; 348 | } 349 | } 350 | }, { 351 | key: 'render', 352 | value: function render() { 353 | var _props2 = this.props, 354 | height = _props2.height, 355 | children = _props2.children, 356 | placeholder = _props2.placeholder, 357 | className = _props2.className, 358 | classNamePrefix = _props2.classNamePrefix, 359 | style = _props2.style; 360 | 361 | 362 | return _react2.default.createElement( 363 | 'div', 364 | { className: classNamePrefix + '-wrapper ' + className, ref: this.setRef, style: style }, 365 | this.visible ? children : placeholder ? placeholder : _react2.default.createElement('div', { 366 | style: { height: height }, 367 | className: classNamePrefix + '-placeholder' 368 | }) 369 | ); 370 | } 371 | }]); 372 | 373 | return LazyLoad; 374 | }(_react.Component); 375 | 376 | LazyLoad.propTypes = { 377 | className: _propTypes2.default.string, 378 | classNamePrefix: _propTypes2.default.string, 379 | once: _propTypes2.default.bool, 380 | height: _propTypes2.default.oneOfType([_propTypes2.default.number, _propTypes2.default.string]), 381 | offset: _propTypes2.default.oneOfType([_propTypes2.default.number, _propTypes2.default.arrayOf(_propTypes2.default.number)]), 382 | overflow: _propTypes2.default.bool, 383 | resize: _propTypes2.default.bool, 384 | scroll: _propTypes2.default.bool, 385 | children: _propTypes2.default.node, 386 | throttle: _propTypes2.default.oneOfType([_propTypes2.default.number, _propTypes2.default.bool]), 387 | debounce: _propTypes2.default.oneOfType([_propTypes2.default.number, _propTypes2.default.bool]), 388 | placeholder: _propTypes2.default.node, 389 | scrollContainer: _propTypes2.default.oneOfType([_propTypes2.default.string, _propTypes2.default.object]), 390 | unmountIfInvisible: _propTypes2.default.bool, 391 | style: _propTypes2.default.object 392 | }; 393 | 394 | LazyLoad.defaultProps = { 395 | className: '', 396 | classNamePrefix: 'lazyload', 397 | once: false, 398 | offset: 0, 399 | overflow: false, 400 | resize: false, 401 | scroll: true, 402 | unmountIfInvisible: false 403 | }; 404 | 405 | var getDisplayName = function getDisplayName(WrappedComponent) { 406 | return WrappedComponent.displayName || WrappedComponent.name || 'Component'; 407 | }; 408 | 409 | var decorator = function decorator() { 410 | var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 411 | return function lazyload(WrappedComponent) { 412 | return function (_Component2) { 413 | _inherits(LazyLoadDecorated, _Component2); 414 | 415 | function LazyLoadDecorated() { 416 | _classCallCheck(this, LazyLoadDecorated); 417 | 418 | var _this2 = _possibleConstructorReturn(this, (LazyLoadDecorated.__proto__ || Object.getPrototypeOf(LazyLoadDecorated)).call(this)); 419 | 420 | _this2.displayName = 'LazyLoad' + getDisplayName(WrappedComponent); 421 | return _this2; 422 | } 423 | 424 | _createClass(LazyLoadDecorated, [{ 425 | key: 'render', 426 | value: function render() { 427 | return _react2.default.createElement( 428 | LazyLoad, 429 | options, 430 | _react2.default.createElement(WrappedComponent, this.props) 431 | ); 432 | } 433 | }]); 434 | 435 | return LazyLoadDecorated; 436 | }(_react.Component); 437 | }; 438 | }; 439 | 440 | exports.lazyload = decorator; 441 | exports.default = LazyLoad; 442 | exports.forceCheck = lazyLoadHandler; 443 | exports.forceVisible = forceVisible; -------------------------------------------------------------------------------- /lib/utils/debounce.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = debounce; 7 | function debounce(func, wait, immediate) { 8 | var timeout = void 0; 9 | var args = void 0; 10 | var context = void 0; 11 | var timestamp = void 0; 12 | var result = void 0; 13 | 14 | var later = function later() { 15 | var last = +new Date() - timestamp; 16 | 17 | if (last < wait && last >= 0) { 18 | timeout = setTimeout(later, wait - last); 19 | } else { 20 | timeout = null; 21 | if (!immediate) { 22 | result = func.apply(context, args); 23 | if (!timeout) { 24 | context = null; 25 | args = null; 26 | } 27 | } 28 | } 29 | }; 30 | 31 | return function debounced() { 32 | context = this; 33 | args = arguments; 34 | timestamp = +new Date(); 35 | 36 | var callNow = immediate && !timeout; 37 | if (!timeout) { 38 | timeout = setTimeout(later, wait); 39 | } 40 | 41 | if (callNow) { 42 | result = func.apply(context, args); 43 | context = null; 44 | args = null; 45 | } 46 | 47 | return result; 48 | }; 49 | } -------------------------------------------------------------------------------- /lib/utils/event.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.on = on; 7 | exports.off = off; 8 | function on(el, eventName, callback, opts) { 9 | opts = opts || false; 10 | if (el.addEventListener) { 11 | el.addEventListener(eventName, callback, opts); 12 | } else if (el.attachEvent) { 13 | el.attachEvent("on" + eventName, function (e) { 14 | callback.call(el, e || window.event); 15 | }); 16 | } 17 | } 18 | 19 | function off(el, eventName, callback, opts) { 20 | opts = opts || false; 21 | if (el.removeEventListener) { 22 | el.removeEventListener(eventName, callback, opts); 23 | } else if (el.detachEvent) { 24 | el.detachEvent("on" + eventName, callback); 25 | } 26 | } -------------------------------------------------------------------------------- /lib/utils/scrollParent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | /** 8 | * @fileOverview Find scroll parent 9 | */ 10 | 11 | exports.default = function (node) { 12 | if (!(node instanceof HTMLElement)) { 13 | return document.documentElement; 14 | } 15 | 16 | var excludeStaticParent = node.style.position === 'absolute'; 17 | var overflowRegex = /(scroll|auto)/; 18 | var parent = node; 19 | 20 | while (parent) { 21 | if (!parent.parentNode) { 22 | return node.ownerDocument || document.documentElement; 23 | } 24 | 25 | var style = window.getComputedStyle(parent); 26 | var position = style.position; 27 | var overflow = style.overflow; 28 | var overflowX = style['overflow-x']; 29 | var overflowY = style['overflow-y']; 30 | 31 | if (position === 'static' && excludeStaticParent) { 32 | parent = parent.parentNode; 33 | continue; 34 | } 35 | 36 | if (overflowRegex.test(overflow) && overflowRegex.test(overflowX) && overflowRegex.test(overflowY)) { 37 | return parent; 38 | } 39 | 40 | parent = parent.parentNode; 41 | } 42 | 43 | return node.ownerDocument || node.documentElement || document.documentElement; 44 | }; -------------------------------------------------------------------------------- /lib/utils/throttle.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = throttle; 7 | /*eslint-disable */ 8 | function throttle(fn, threshhold, scope) { 9 | threshhold || (threshhold = 250); 10 | var last, deferTimer; 11 | return function () { 12 | var context = scope || this; 13 | 14 | var now = +new Date(), 15 | args = arguments; 16 | if (last && now < last + threshhold) { 17 | // hold on to it 18 | clearTimeout(deferTimer); 19 | deferTimer = setTimeout(function () { 20 | last = now; 21 | fn.apply(context, args); 22 | }, threshhold); 23 | } else { 24 | last = now; 25 | fn.apply(context, args); 26 | } 27 | }; 28 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-lazyload", 3 | "version": "3.2.1", 4 | "description": "Lazyload your components, images or anything where performance matters.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "karma start test/karma.conf.js", 8 | "demo:watch": "webpack-dev-server --inline --config webpack.config.js --content-base examples --port 8721", 9 | "demo:build": "NODE_ENV=production webpack", 10 | "build": "babel src/ --out-dir lib/", 11 | "lint": "eslint -c .eslintrc src/" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/jasonslyvia/react-lazyload.git" 16 | }, 17 | "keywords": [ 18 | "react-component", 19 | "react", 20 | "lazyload" 21 | ], 22 | "author": "jasonslyvia (http://undefinedblog.com/)", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/jasonslyvia/react-lazyload/issues" 26 | }, 27 | "homepage": "https://github.com/jasonslyvia/react-lazyload", 28 | "devDependencies": { 29 | "babel-cli": "^6.24.0", 30 | "babel-core": "^6.24.0", 31 | "babel-eslint": "^7.1.1", 32 | "babel-loader": "~6.4.1", 33 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 34 | "babel-preset-es2015": "^6.24.0", 35 | "babel-preset-react": "^6.23.0", 36 | "babel-preset-stage-0": "^6.22.0", 37 | "chai": "^3.5.0", 38 | "chai-spies": "^0.7.1", 39 | "istanbul": "~0.4.5", 40 | "istanbul-instrumenter-loader": "^0.2.0", 41 | "karma": "^0.13.22", 42 | "karma-chai": "^0.1.0", 43 | "karma-chrome-launcher": "^2.2.0", 44 | "karma-coverage": "^0.5.5", 45 | "karma-coveralls": "^1.1.2", 46 | "karma-firefox-launcher": "^1.0.1", 47 | "karma-mocha": "^0.2.2", 48 | "karma-sourcemap-loader": "^0.3.7", 49 | "karma-webpack": "^1.7.0", 50 | "mocha": "^2.2.5", 51 | "prop-types": "^15.5.6", 52 | "puppeteer": "^2.1.1", 53 | "react": "^16", 54 | "react-dom": "^16", 55 | "react-hot-loader": "~1.3.1", 56 | "react-router": "^3", 57 | "react-transition-group": "1.x", 58 | "webpack": "~1.11.0", 59 | "webpack-dev-server": "~1.10.1" 60 | }, 61 | "peerDependencies": { 62 | "react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", 63 | "react-dom": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | * react-lazyload 3 | */ 4 | import React, { Component } from 'react'; 5 | import PropTypes from 'prop-types'; 6 | import { on, off } from './utils/event'; 7 | import scrollParent from './utils/scrollParent'; 8 | import debounce from './utils/debounce'; 9 | import throttle from './utils/throttle'; 10 | 11 | const defaultBoundingClientRect = { 12 | top: 0, 13 | right: 0, 14 | bottom: 0, 15 | left: 0, 16 | width: 0, 17 | height: 0 18 | }; 19 | const LISTEN_FLAG = 'data-lazyload-listened'; 20 | const listeners = []; 21 | let pending = []; 22 | 23 | // try to handle passive events 24 | let passiveEventSupported = false; 25 | try { 26 | const opts = Object.defineProperty({}, 'passive', { 27 | get() { 28 | passiveEventSupported = true; 29 | } 30 | }); 31 | window.addEventListener('test', null, opts); 32 | } catch (e) {} 33 | // if they are supported, setup the optional params 34 | // IMPORTANT: FALSE doubles as the default CAPTURE value! 35 | const passiveEvent = passiveEventSupported 36 | ? { capture: false, passive: true } 37 | : false; 38 | 39 | /** 40 | * Check if `component` is visible in overflow container `parent` 41 | * @param {node} component React component 42 | * @param {node} parent component's scroll parent 43 | * @return {bool} 44 | */ 45 | const checkOverflowVisible = function checkOverflowVisible(component, parent) { 46 | const node = component.ref; 47 | 48 | let parentTop; 49 | let parentLeft; 50 | let parentHeight; 51 | let parentWidth; 52 | 53 | try { 54 | ({ 55 | top: parentTop, 56 | left: parentLeft, 57 | height: parentHeight, 58 | width: parentWidth 59 | } = parent.getBoundingClientRect()); 60 | } catch (e) { 61 | ({ 62 | top: parentTop, 63 | left: parentLeft, 64 | height: parentHeight, 65 | width: parentWidth 66 | } = defaultBoundingClientRect); 67 | } 68 | 69 | const windowInnerHeight = 70 | window.innerHeight || document.documentElement.clientHeight; 71 | const windowInnerWidth = 72 | window.innerWidth || document.documentElement.clientWidth; 73 | 74 | // calculate top and height of the intersection of the element's scrollParent and viewport 75 | const intersectionTop = Math.max(parentTop, 0); // intersection's top relative to viewport 76 | const intersectionLeft = Math.max(parentLeft, 0); // intersection's left relative to viewport 77 | const intersectionHeight = 78 | Math.min(windowInnerHeight, parentTop + parentHeight) - intersectionTop; // height 79 | const intersectionWidth = 80 | Math.min(windowInnerWidth, parentLeft + parentWidth) - intersectionLeft; // width 81 | 82 | // check whether the element is visible in the intersection 83 | let top; 84 | let left; 85 | let height; 86 | let width; 87 | 88 | try { 89 | ({ top, left, height, width } = node.getBoundingClientRect()); 90 | } catch (e) { 91 | ({ top, left, height, width } = defaultBoundingClientRect); 92 | } 93 | 94 | const offsetTop = top - intersectionTop; // element's top relative to intersection 95 | const offsetLeft = left - intersectionLeft; // element's left relative to intersection 96 | 97 | const offsets = Array.isArray(component.props.offset) 98 | ? component.props.offset 99 | : [component.props.offset, component.props.offset]; // Be compatible with previous API 100 | 101 | return ( 102 | offsetTop - offsets[0] <= intersectionHeight && 103 | offsetTop + height + offsets[1] >= 0 && 104 | offsetLeft - offsets[0] <= intersectionWidth && 105 | offsetLeft + width + offsets[1] >= 0 106 | ); 107 | }; 108 | 109 | /** 110 | * Check if `component` is visible in document 111 | * @param {node} component React component 112 | * @return {bool} 113 | */ 114 | const checkNormalVisible = function checkNormalVisible(component) { 115 | const node = component.ref; 116 | 117 | // If this element is hidden by css rules somehow, it's definitely invisible 118 | if (!(node.offsetWidth || node.offsetHeight || node.getClientRects().length)) 119 | return false; 120 | 121 | let top; 122 | let elementHeight; 123 | 124 | try { 125 | ({ top, height: elementHeight } = node.getBoundingClientRect()); 126 | } catch (e) { 127 | ({ top, height: elementHeight } = defaultBoundingClientRect); 128 | } 129 | 130 | const windowInnerHeight = 131 | window.innerHeight || document.documentElement.clientHeight; 132 | 133 | const offsets = Array.isArray(component.props.offset) 134 | ? component.props.offset 135 | : [component.props.offset, component.props.offset]; // Be compatible with previous API 136 | 137 | return ( 138 | top - offsets[0] <= windowInnerHeight && 139 | top + elementHeight + offsets[1] >= 0 140 | ); 141 | }; 142 | 143 | /** 144 | * Detect if element is visible in viewport, if so, set `visible` state to true. 145 | * If `once` prop is provided true, remove component as listener after checkVisible 146 | * 147 | * @param {React} component React component that respond to scroll and resize 148 | */ 149 | const checkVisible = function checkVisible(component) { 150 | const node = component.ref; 151 | if (!(node instanceof HTMLElement)) { 152 | return; 153 | } 154 | 155 | const parent = scrollParent(node); 156 | const isOverflow = 157 | component.props.overflow && 158 | parent !== node.ownerDocument && 159 | parent !== document && 160 | parent !== document.documentElement; 161 | const visible = isOverflow 162 | ? checkOverflowVisible(component, parent) 163 | : checkNormalVisible(component); 164 | if (visible) { 165 | // Avoid extra render if previously is visible 166 | if (!component.visible) { 167 | if (component.props.once) { 168 | pending.push(component); 169 | } 170 | 171 | component.visible = true; 172 | component.forceUpdate(); 173 | } 174 | } else if (!(component.props.once && component.visible)) { 175 | component.visible = false; 176 | if (component.props.unmountIfInvisible) { 177 | component.forceUpdate(); 178 | } 179 | } 180 | }; 181 | 182 | const purgePending = function purgePending() { 183 | pending.forEach(component => { 184 | const index = listeners.indexOf(component); 185 | if (index !== -1) { 186 | listeners.splice(index, 1); 187 | } 188 | }); 189 | 190 | pending = []; 191 | }; 192 | 193 | const lazyLoadHandler = () => { 194 | for (let i = 0; i < listeners.length; ++i) { 195 | const listener = listeners[i]; 196 | checkVisible(listener); 197 | } 198 | // Remove `once` component in listeners 199 | purgePending(); 200 | }; 201 | 202 | /** 203 | * Forces the component to display regardless of whether the element is visible in the viewport. 204 | */ 205 | const forceVisible = () => { 206 | for (let i = 0; i < listeners.length; ++i) { 207 | const listener = listeners[i]; 208 | listener.visible = true; 209 | listener.forceUpdate(); 210 | } 211 | // Remove `once` component in listeners 212 | purgePending(); 213 | }; 214 | 215 | // Depending on component's props 216 | let delayType; 217 | let finalLazyLoadHandler = null; 218 | 219 | const isString = string => typeof string === 'string'; 220 | 221 | class LazyLoad extends Component { 222 | constructor(props) { 223 | super(props); 224 | 225 | this.visible = false; 226 | this.setRef = this.setRef.bind(this); 227 | } 228 | 229 | componentDidMount() { 230 | // It's unlikely to change delay type on the fly, this is mainly 231 | // designed for tests 232 | let scrollport = window; 233 | const { scrollContainer } = this.props; 234 | if (scrollContainer) { 235 | if (isString(scrollContainer)) { 236 | scrollport = scrollport.document.querySelector(scrollContainer); 237 | } 238 | } 239 | const needResetFinalLazyLoadHandler = 240 | (this.props.debounce !== undefined && delayType === 'throttle') || 241 | (delayType === 'debounce' && this.props.debounce === undefined); 242 | 243 | if (needResetFinalLazyLoadHandler) { 244 | off(scrollport, 'scroll', finalLazyLoadHandler, passiveEvent); 245 | off(window, 'resize', finalLazyLoadHandler, passiveEvent); 246 | finalLazyLoadHandler = null; 247 | } 248 | 249 | if (!finalLazyLoadHandler) { 250 | if (this.props.debounce !== undefined) { 251 | finalLazyLoadHandler = debounce( 252 | lazyLoadHandler, 253 | typeof this.props.debounce === 'number' ? this.props.debounce : 300 254 | ); 255 | delayType = 'debounce'; 256 | } else if (this.props.throttle !== undefined) { 257 | finalLazyLoadHandler = throttle( 258 | lazyLoadHandler, 259 | typeof this.props.throttle === 'number' ? this.props.throttle : 300 260 | ); 261 | delayType = 'throttle'; 262 | } else { 263 | finalLazyLoadHandler = lazyLoadHandler; 264 | } 265 | } 266 | 267 | if (this.props.overflow) { 268 | const parent = scrollParent(this.ref); 269 | if (parent && typeof parent.getAttribute === 'function') { 270 | const listenerCount = 1 + +parent.getAttribute(LISTEN_FLAG); 271 | if (listenerCount === 1) { 272 | parent.addEventListener('scroll', finalLazyLoadHandler, passiveEvent); 273 | } 274 | parent.setAttribute(LISTEN_FLAG, listenerCount); 275 | } 276 | } else if (listeners.length === 0 || needResetFinalLazyLoadHandler) { 277 | const { scroll, resize } = this.props; 278 | 279 | if (scroll) { 280 | on(scrollport, 'scroll', finalLazyLoadHandler, passiveEvent); 281 | } 282 | 283 | if (resize) { 284 | on(window, 'resize', finalLazyLoadHandler, passiveEvent); 285 | } 286 | } 287 | 288 | listeners.push(this); 289 | checkVisible(this); 290 | } 291 | 292 | shouldComponentUpdate() { 293 | return this.visible; 294 | } 295 | 296 | componentWillUnmount() { 297 | if (this.props.overflow) { 298 | const parent = scrollParent(this.ref); 299 | if (parent && typeof parent.getAttribute === 'function') { 300 | const listenerCount = +parent.getAttribute(LISTEN_FLAG) - 1; 301 | if (listenerCount === 0) { 302 | parent.removeEventListener( 303 | 'scroll', 304 | finalLazyLoadHandler, 305 | passiveEvent 306 | ); 307 | parent.removeAttribute(LISTEN_FLAG); 308 | } else { 309 | parent.setAttribute(LISTEN_FLAG, listenerCount); 310 | } 311 | } 312 | } 313 | 314 | const index = listeners.indexOf(this); 315 | if (index !== -1) { 316 | listeners.splice(index, 1); 317 | } 318 | 319 | if (listeners.length === 0 && typeof window !== 'undefined') { 320 | off(window, 'resize', finalLazyLoadHandler, passiveEvent); 321 | off(window, 'scroll', finalLazyLoadHandler, passiveEvent); 322 | } 323 | } 324 | 325 | setRef(element) { 326 | if (element) { 327 | this.ref = element; 328 | } 329 | } 330 | 331 | render() { 332 | const { 333 | height, 334 | children, 335 | placeholder, 336 | className, 337 | classNamePrefix, 338 | style 339 | } = this.props; 340 | 341 | return ( 342 |
343 | {this.visible ? ( 344 | children 345 | ) : placeholder ? ( 346 | placeholder 347 | ) : ( 348 |
352 | )} 353 |
354 | ); 355 | } 356 | } 357 | 358 | LazyLoad.propTypes = { 359 | className: PropTypes.string, 360 | classNamePrefix: PropTypes.string, 361 | once: PropTypes.bool, 362 | height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), 363 | offset: PropTypes.oneOfType([ 364 | PropTypes.number, 365 | PropTypes.arrayOf(PropTypes.number) 366 | ]), 367 | overflow: PropTypes.bool, 368 | resize: PropTypes.bool, 369 | scroll: PropTypes.bool, 370 | children: PropTypes.node, 371 | throttle: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]), 372 | debounce: PropTypes.oneOfType([PropTypes.number, PropTypes.bool]), 373 | placeholder: PropTypes.node, 374 | scrollContainer: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), 375 | unmountIfInvisible: PropTypes.bool, 376 | style: PropTypes.object 377 | }; 378 | 379 | LazyLoad.defaultProps = { 380 | className: '', 381 | classNamePrefix: 'lazyload', 382 | once: false, 383 | offset: 0, 384 | overflow: false, 385 | resize: false, 386 | scroll: true, 387 | unmountIfInvisible: false 388 | }; 389 | 390 | const getDisplayName = WrappedComponent => 391 | WrappedComponent.displayName || WrappedComponent.name || 'Component'; 392 | 393 | const decorator = (options = {}) => 394 | function lazyload(WrappedComponent) { 395 | return class LazyLoadDecorated extends Component { 396 | constructor() { 397 | super(); 398 | this.displayName = `LazyLoad${getDisplayName(WrappedComponent)}`; 399 | } 400 | 401 | render() { 402 | return ( 403 | 404 | 405 | 406 | ); 407 | } 408 | }; 409 | }; 410 | 411 | export { decorator as lazyload }; 412 | export default LazyLoad; 413 | export { lazyLoadHandler as forceCheck }; 414 | export { forceVisible }; 415 | -------------------------------------------------------------------------------- /src/utils/debounce.js: -------------------------------------------------------------------------------- 1 | export default function debounce(func, wait, immediate) { 2 | let timeout; 3 | let args; 4 | let context; 5 | let timestamp; 6 | let result; 7 | 8 | const later = function later() { 9 | const last = +(new Date()) - timestamp; 10 | 11 | if (last < wait && last >= 0) { 12 | timeout = setTimeout(later, wait - last); 13 | } else { 14 | timeout = null; 15 | if (!immediate) { 16 | result = func.apply(context, args); 17 | if (!timeout) { 18 | context = null; 19 | args = null; 20 | } 21 | } 22 | } 23 | }; 24 | 25 | return function debounced() { 26 | context = this; 27 | args = arguments; 28 | timestamp = +(new Date()); 29 | 30 | const callNow = immediate && !timeout; 31 | if (!timeout) { 32 | timeout = setTimeout(later, wait); 33 | } 34 | 35 | if (callNow) { 36 | result = func.apply(context, args); 37 | context = null; 38 | args = null; 39 | } 40 | 41 | return result; 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/event.js: -------------------------------------------------------------------------------- 1 | export function on(el, eventName, callback, opts) { 2 | opts = opts || false; 3 | if (el.addEventListener) { 4 | el.addEventListener(eventName, callback, opts); 5 | } else if (el.attachEvent) { 6 | el.attachEvent(`on${eventName}`, (e) => { 7 | callback.call(el, e || window.event); 8 | }); 9 | } 10 | } 11 | 12 | export function off(el, eventName, callback, opts) { 13 | opts = opts || false; 14 | if (el.removeEventListener) { 15 | el.removeEventListener(eventName, callback, opts); 16 | } else if (el.detachEvent) { 17 | el.detachEvent(`on${eventName}`, callback); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/scrollParent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview Find scroll parent 3 | */ 4 | 5 | export default (node) => { 6 | if (!(node instanceof HTMLElement)) { 7 | return document.documentElement; 8 | } 9 | 10 | const excludeStaticParent = node.style.position === 'absolute'; 11 | const overflowRegex = /(scroll|auto)/; 12 | let parent = node; 13 | 14 | while (parent) { 15 | if (!parent.parentNode) { 16 | return node.ownerDocument || document.documentElement; 17 | } 18 | 19 | const style = window.getComputedStyle(parent); 20 | const position = style.position; 21 | const overflow = style.overflow; 22 | const overflowX = style['overflow-x']; 23 | const overflowY = style['overflow-y']; 24 | 25 | if (position === 'static' && excludeStaticParent) { 26 | parent = parent.parentNode; 27 | continue; 28 | } 29 | 30 | if (overflowRegex.test(overflow) && overflowRegex.test(overflowX) && overflowRegex.test(overflowY)) { 31 | return parent; 32 | } 33 | 34 | parent = parent.parentNode; 35 | } 36 | 37 | return node.ownerDocument || node.documentElement || document.documentElement; 38 | }; 39 | -------------------------------------------------------------------------------- /src/utils/throttle.js: -------------------------------------------------------------------------------- 1 | /*eslint-disable */ 2 | export default function throttle(fn, threshhold, scope) { 3 | threshhold || (threshhold = 250); 4 | var last, 5 | deferTimer; 6 | return function () { 7 | var context = scope || this; 8 | 9 | var now = +new Date, 10 | args = arguments; 11 | if (last && now < last + threshhold) { 12 | // hold on to it 13 | clearTimeout(deferTimer); 14 | deferTimer = setTimeout(function () { 15 | last = now; 16 | fn.apply(context, args); 17 | }, threshhold); 18 | } else { 19 | last = now; 20 | fn.apply(context, args); 21 | } 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /test/Test.component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class Test extends React.Component { 4 | constructor() { 5 | super(); 6 | this.state = { 7 | times: 1 8 | }; 9 | } 10 | 11 | componentWillReceiveProps() { 12 | this.setState({ 13 | times: this.state.times + 1 14 | }); 15 | } 16 | 17 | render() { 18 | return ( 19 |
20 | {this.state.times} 21 | {this.props.children} 22 |
23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Wed Mar 18 2015 11:41:18 GMT+0800 (CST) 3 | 'use strict'; 4 | 5 | const process = require('process'); 6 | process.env.CHROME_BIN = require('puppeteer').executablePath(); 7 | 8 | module.exports = function (config) { 9 | config.set({ 10 | 11 | // base path that will be used to resolve all patterns (eg. files, exclude) 12 | basePath: '../', 13 | 14 | 15 | // frameworks to use 16 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 17 | frameworks: ['mocha', 'chai'], 18 | 19 | // list of files / patterns to load in the browser 20 | files: [ 21 | { pattern: 'test/specs/*.js', included: true, watched: false }, 22 | ], 23 | 24 | 25 | // list of files to exclude 26 | exclude: [ 27 | 'test/coverage/**', 28 | 'lib/**', 29 | 'node_modules/' 30 | ], 31 | 32 | 33 | // preprocess matching files before serving them to the browser 34 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 35 | preprocessors: { 36 | 'test/**/*.js': ['webpack', 'sourcemap'], 37 | }, 38 | 39 | webpack: { 40 | devtool: 'inline-source-map', 41 | module: { 42 | loaders: [{ 43 | test: /\.jsx?$/, 44 | include: /src|test|demo/, 45 | query: { 46 | presets: ['stage-0', 'es2015', 'react'], 47 | plugins: ['transform-decorators-legacy'] 48 | }, 49 | loader: 'babel' 50 | }], 51 | postLoaders: [{ 52 | test: /\.js$/, 53 | include: /src/, 54 | loader: 'istanbul-instrumenter' 55 | }] 56 | }, 57 | resolve: { 58 | extensions: ['', '.js', '.jsx'] 59 | } 60 | }, 61 | 62 | 63 | plugins: [ 64 | 'karma-webpack', 65 | 'karma-mocha', 66 | 'karma-coverage', 67 | 'karma-chai', 68 | 'karma-sourcemap-loader', 69 | 'karma-chrome-launcher', 70 | 'istanbul-instrumenter-loader', 71 | 'karma-coveralls' 72 | ], 73 | 74 | 75 | // test results reporter to use 76 | // possible values: 'dots', 'progress' 77 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 78 | reporters: ['progress', 'coverage', 'coveralls'], 79 | 80 | coverageReporter: { 81 | dir: 'test', 82 | reporters: [{ 83 | type: 'html', 84 | subdir: 'coverage' 85 | }, { 86 | type: 'text', 87 | }, { 88 | type: 'lcov', 89 | subdir: 'coverage' 90 | }] 91 | }, 92 | 93 | // web server port 94 | port: 9876, 95 | 96 | 97 | // enable / disable colors in the output (reporters and logs) 98 | colors: true, 99 | 100 | 101 | // level of logging 102 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 103 | logLevel: config.LOG_DEBUG, 104 | 105 | 106 | // enable / disable watching file and executing tests whenever any file changes 107 | autoWatch: true, 108 | 109 | customLaunchers: { 110 | Chrome_travis_ci: { 111 | base: 'ChromeHeadless', 112 | flags: ['--no-sandbox'] 113 | } 114 | }, 115 | 116 | // start these browsers 117 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 118 | browsers: ['Chrome', 'ChromeHeadless'], 119 | 120 | browserNoActivityTimeout: 300000, 121 | browserDisconnectTimeout: 300000, 122 | 123 | // Continuous Integration mode 124 | // if true, Karma captures browsers, runs the tests and exits 125 | singleRun: false, 126 | processKillTimeout: 300000, 127 | }); 128 | }; 129 | -------------------------------------------------------------------------------- /test/specs/events.spec.js: -------------------------------------------------------------------------------- 1 | import spies from 'chai-spies'; 2 | import * as event from '../../src/utils/event'; 3 | chai.use(spies); 4 | 5 | describe('Event', () => { 6 | const fakeCallBack = chai.spy(); 7 | 8 | it('should call attachEvent when addEventListener does not exist', () => { 9 | document.addEventListener = null; 10 | var fakeAttachEvent = chai.spy(); 11 | document.attachEvent = fakeAttachEvent; 12 | 13 | 14 | event.on(document, 'click', fakeCallBack); 15 | expect(fakeAttachEvent).to.be.called.once; 16 | }); 17 | 18 | 19 | it('should call detachEvent when removeEventListener does not exist', () => { 20 | document.removeEventListener = null; 21 | var fakeDetachEvent = chai.spy(); 22 | document.detachEvent = fakeDetachEvent; 23 | 24 | event.off(document, 'click', fakeCallBack); 25 | expect(fakeDetachEvent).to.be.called.once; 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/specs/lazyload.debounce.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-expressions: 0 */ 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import LazyLoad from '../../src/index'; 5 | import spies from 'chai-spies'; 6 | import Test from '../Test.component'; 7 | 8 | chai.use(spies); 9 | const expect = chai.expect; 10 | 11 | 12 | before(() => { 13 | document.body.style.margin = 0; 14 | document.body.style.padding = 0; 15 | }); 16 | 17 | let div; 18 | 19 | beforeEach(() => { 20 | div = document.createElement('div'); 21 | document.body.appendChild(div); 22 | }); 23 | 24 | afterEach(() => { 25 | ReactDOM.unmountComponentAtNode(div); 26 | div.parentNode.removeChild(div); 27 | window.scrollTo(0, 0); 28 | }); 29 | 30 | describe('Throttle', () => { 31 | it('should throttle scroll event by default', (done) => { 32 | const windowHeight = window.innerHeight + 1; 33 | ReactDOM.render( 34 |
35 | 36 | 37 | 38 |
39 | , div); 40 | 41 | window.scrollTo(0, 10); 42 | 43 | setTimeout(() => { 44 | window.scrollTo(0, 9999); 45 | }, 50); 46 | 47 | setTimeout(() => { 48 | window.scrollTo(0, 10); 49 | }, 50); 50 | 51 | // let `scroll` event handler done their job first 52 | setTimeout(() => { 53 | expect(document.querySelectorAll('.test').length).to.equal(2); 54 | done(); 55 | }, 500); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /test/specs/lazyload.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-expressions: 0 */ 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import LazyLoad, { lazyload } from '../../src/index'; 5 | import spies from 'chai-spies'; 6 | import Test from '../Test.component'; 7 | import * as event from '../../src/utils/event'; 8 | 9 | chai.use(spies); 10 | const expect = chai.expect; 11 | 12 | 13 | before(() => { 14 | document.body.style.margin = 0; 15 | document.body.style.padding = 0; 16 | }); 17 | 18 | let div; 19 | 20 | beforeEach(() => { 21 | div = document.createElement('div'); 22 | document.body.appendChild(div); 23 | }); 24 | 25 | afterEach(() => { 26 | ReactDOM.unmountComponentAtNode(div); 27 | div.parentNode.removeChild(div); 28 | window.scrollTo(0, 0); 29 | }); 30 | 31 | describe('LazyLoad', () => { 32 | describe('Basic setup', () => { 33 | it('should render passed children', () => { 34 | ReactDOM.render(, div); 35 | expect(document.querySelector('.test')).to.exist; 36 | }); 37 | 38 | it('should render `lazyload-placeholder` when children not visible', () => { 39 | ReactDOM.render( 40 |
41 | 123 42 | 123 43 |
44 | , div); 45 | 46 | expect(document.querySelector('.something')).to.exist; 47 | expect(document.querySelector('.lazyload-placeholder')).to.exist; 48 | expect(document.querySelector('.treasure')).to.not.exist; 49 | }); 50 | 51 | it('should NOT update when invisble', (done) => { 52 | ReactDOM.render( 53 |
54 | 55 | 56 |
57 | , div); 58 | 59 | expect(document.querySelector('.test1 .times').textContent).to.equal('1'); 60 | expect(document.querySelector('.test2')).to.not.exist; 61 | 62 | ReactDOM.render( 63 |
64 | 65 | 66 |
67 | , div); 68 | 69 | expect(document.querySelector('.test1 .times').textContent).to.equal('2'); 70 | expect(document.querySelector('.test2')).to.not.exist; 71 | 72 | window.scrollTo(0, 99999); 73 | 74 | setTimeout(() => { 75 | expect(document.querySelector('.test2 .times').textContent).to.equal('1'); 76 | 77 | ReactDOM.render( 78 |
79 | 80 | 81 |
82 | , div); 83 | 84 | expect(document.querySelector('.test1 .times').textContent).to.equal('2'); 85 | expect(document.querySelector('.test2 .times').textContent).to.equal('2'); 86 | done(); 87 | }, 500); 88 | }); 89 | 90 | it('should NOT controlled by LazyLoad when `once` and visible', (done) => { 91 | ReactDOM.render( 92 |
93 | 94 | 95 |
96 | , div); 97 | 98 | ReactDOM.render( 99 |
100 | 101 | 102 |
103 | , div); 104 | 105 | window.scrollTo(0, 99999); 106 | 107 | setTimeout(() => { 108 | ReactDOM.render( 109 |
110 | 111 | 112 |
113 | , div); 114 | 115 | // Differnce between the test above 116 | expect(document.querySelector('.test1 .times').textContent).to.equal('3'); 117 | done(); 118 | }, 500); 119 | }); 120 | 121 | it('should render `placeholder` if provided', () => { 122 | ReactDOM.render( 123 |
124 |
}> 125 | 126 | 127 |
}> 128 | 129 | 130 | , div); 131 | 132 | expect(document.querySelector('.my-placeholder')).to.exist; 133 | }); 134 | it('should render `placeholder` and `wrapper` elements with custom `classNamePrefix` when provided', () => { 135 | ReactDOM.render( 136 |
137 | 138 | 123 139 | 140 | 141 | 123 142 | 143 |
, div); 144 | console.log(div.innerHTML); 145 | expect(document.querySelector('.custom-lazyload-wrapper')).to.exist; 146 | expect(document.querySelector('.custom-lazyload-placeholder')).to.exist; 147 | }); 148 | it('should render wrapper with `style` when provided', () => { 149 | ReactDOM.render( 150 |
151 | 152 | 123 153 | 154 |
, div); 155 | console.log(div.innerHTML); 156 | expect(document.querySelector('.custom-lazyload-wrapper').getBoundingClientRect().height).to.equal(200); 157 | }) 158 | }); 159 | 160 | describe('Checking visibility', () => { 161 | it('should consider visible when top edge is visible', () => { 162 | const windowHeight = window.innerHeight; 163 | ReactDOM.render( 164 |
165 | 123 166 | 123 167 |
168 | , div); 169 | 170 | expect(document.querySelector('.something')).to.exist; 171 | expect(document.querySelector('.lazyload-placeholder')).to.not.exist; 172 | expect(document.querySelector('.treasure')).to.exist; 173 | }); 174 | 175 | it('should render children when scrolled visible', (done) => { 176 | const windowHeight = window.innerHeight + 20; 177 | ReactDOM.render( 178 |
179 | 123 180 | 123 181 |
182 | , div); 183 | 184 | expect(document.querySelector('.lazyload-placeholder')).to.exist; 185 | window.scrollTo(0, 50); 186 | 187 | setTimeout(() => { 188 | expect(document.querySelector('.lazyload-placeholder')).to.not.exist; 189 | expect(document.querySelector('.treasure')).to.exist; 190 | done(); 191 | }, 1000); 192 | }); 193 | 194 | it('should work inside overflow container', (done) => { 195 | ReactDOM.render( 196 |
197 |
123
198 |
123
199 |
123
200 |
201 | , div); 202 | 203 | const container = document.querySelector('.container'); 204 | expect(container.querySelector('.something')).to.exist; 205 | // tests run well locally, but not on travisci, need to dig it 206 | // @FIXME 207 | // expect(container.querySelector('.lazyload-placeholder')).to.exist; 208 | // expect(container.querySelector('.treasure')).to.not.exist; 209 | 210 | container.scrollTop = 200; 211 | // since scroll event is throttled, has to wait for a delay to make assertion 212 | setTimeout(() => { 213 | expect(container.querySelector('.lazyload-placeholder')).to.not.exist; 214 | expect(container.querySelector('.treasure')).to.exist; 215 | done(); 216 | }, 500); 217 | }); 218 | }); 219 | 220 | describe('Decorator', () => { 221 | it('should work properly', () => { 222 | @lazyload({ 223 | height: 9999 224 | }) 225 | class Test extends React.Component { 226 | render() { 227 | return ( 228 |
{this.props.children}
229 | ); 230 | } 231 | } 232 | 233 | ReactDOM.render( 234 |
235 | 1 236 | 2 237 | 3 238 |
239 | , div); 240 | 241 | expect(document.querySelectorAll('.test').length).to.equal(1); 242 | expect(document.querySelector('.lazyload-placeholder')).to.exist; 243 | }); 244 | }); 245 | 246 | describe('Overflow', () => { 247 | // https://github.com/jasonslyvia/react-lazyload/issues/71 248 | // http://stackoverflow.com/a/6433475/761124 249 | it('should not detect a overflow container when only one of the scroll property is auto\/scroll', () => { 250 | ReactDOM.render( 251 |
255 |
260 |
123
261 |
123
262 |
123
263 |
264 |
265 | , div); 266 | 267 | const container = document.querySelector('.container'); 268 | expect(container.querySelector('.something')).to.exist; 269 | expect(container.querySelector('.lazyload-placeholder')).not.to.exist; 270 | expect(container.querySelector('.treasure')).to.exist; 271 | }); 272 | }); 273 | 274 | describe('scrollContainer', () => { 275 | it('should focus on a different scroll container', (done) => { 276 | const $body = document.body; 277 | const $html = $body.parentNode; 278 | const $els = [$body, $html, div]; 279 | setStyle('100%'); 280 | ReactDOM.render( 281 |
284 |
287 |
290 | 293 |
294 |
295 |
296 |
297 | , div); 298 | const $container = document.querySelector('#scroll-container'); 299 | event.on($container, 'scroll', scroller); 300 | expect($container.querySelector('.lazyload-placeholder')).to.exist; 301 | expect($container.querySelector('#content')).not.to.exist; 302 | $container.scrollTop = $container.scrollHeight - window.innerHeight; 303 | 304 | function setStyle(value) { 305 | $els.forEach($el => { 306 | $el.style.height = value; 307 | $el.style.width = value; 308 | }); 309 | } 310 | 311 | function scroller() { 312 | expect($container.querySelector('.lazyload-placeholder')).not.to.exist; 313 | expect($container.querySelector('#content')).to.exist; 314 | setStyle(''); 315 | event.off($container, 'scroll', scroller); 316 | done(); 317 | } 318 | }); 319 | }); 320 | }); 321 | -------------------------------------------------------------------------------- /test/specs/lazyload.throttle.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint no-unused-expressions: 0 */ 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | import LazyLoad from '../../src/index'; 5 | import spies from 'chai-spies'; 6 | import Test from '../Test.component'; 7 | 8 | chai.use(spies); 9 | const expect = chai.expect; 10 | 11 | 12 | before(() => { 13 | document.body.style.margin = 0; 14 | document.body.style.padding = 0; 15 | }); 16 | 17 | let div; 18 | 19 | beforeEach(() => { 20 | div = document.createElement('div'); 21 | document.body.appendChild(div); 22 | }); 23 | 24 | afterEach(() => { 25 | ReactDOM.unmountComponentAtNode(div); 26 | div.parentNode.removeChild(div); 27 | window.scrollTo(0, 0); 28 | }); 29 | 30 | describe('Debounce', () => { 31 | it('should debounce when `debounce` is set', (done) => { 32 | const windowHeight = window.innerHeight + 20; 33 | ReactDOM.render( 34 |
35 | 36 | 37 | 38 |
39 | , div); 40 | 41 | window.scrollTo(0, 9999); 42 | 43 | setTimeout(() => { 44 | window.scrollTo(0, 9999); 45 | }, 30); 46 | 47 | setTimeout(() => { 48 | window.scrollTo(0, 0); 49 | }, 60); 50 | 51 | setTimeout(() => { 52 | window.scrollTo(0, 9999); 53 | }, 90); 54 | 55 | setTimeout(() => { 56 | window.scrollTo(0, 0); 57 | }, 120); 58 | 59 | // let `scroll` event handler done their job first 60 | setTimeout(() => { 61 | expect(document.querySelectorAll('.test').length).to.equal(1); 62 | expect(document.querySelector('.lazyload-placeholder')).to.exist; 63 | done(); 64 | }, 500); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 'use strict'; 3 | 4 | var webpack = require('webpack'); 5 | var path = require('path'); 6 | 7 | var plugins = [ 8 | new webpack.optimize.OccurenceOrderPlugin(), 9 | new webpack.DefinePlugin({ 10 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 11 | }) 12 | ]; 13 | 14 | var DEV_MODE = process.env.NODE_ENV !== 'production'; 15 | 16 | if (!DEV_MODE) { 17 | plugins.push( 18 | new webpack.optimize.UglifyJsPlugin({ 19 | compressor: { 20 | warnings: false 21 | } 22 | }) 23 | ); 24 | } 25 | 26 | module.exports = { 27 | module: { 28 | loaders: [{ 29 | test: /\.jsx?$/, 30 | loader: 'babel', 31 | exclude: /node_modules/ 32 | }] 33 | }, 34 | 35 | entry: { 36 | app: './examples/app.js', 37 | }, 38 | 39 | watch: DEV_MODE, 40 | devtool: DEV_MODE ? 'inline-source-map' : 'source-map', 41 | 42 | output: { 43 | path: path.join(__dirname, 'examples/js/'), 44 | filename: 'bundle.min.js', 45 | publicPath: '/js/' 46 | }, 47 | 48 | plugins: plugins, 49 | resolve: { 50 | extensions: ['', '.js', '.jsx'] 51 | } 52 | }; 53 | --------------------------------------------------------------------------------