├── .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 [](https://travis-ci.org/twobin/react-lazyload) [](http://badge.fury.io/js/react-lazyload) [](https://coveralls.io/github/jasonslyvia/react-lazyload?branch=master) [](https://www.npmjs.com/package/react-lazyload)
9 |
10 | Lazyload your Components, Images or anything matters the performance.
11 |
12 | [](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 |
17 | normal
18 | using with <img>
19 | using with decorator
20 | using with scrollTo
21 | using inside overflow container
22 | using debounce
23 | custom placeholder
24 | cool fadeIn
effect
25 | using forceVisible
26 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------