├── .babelrc
├── .circleci
├── config.yml
└── setup_puppeteer.sh
├── .editorconfig
├── .eslintrc
├── .github
└── workflows
│ └── codeql-analysis.yml
├── .gitignore
├── LICENSE
├── README.md
├── build.js
├── package.json
├── src
├── index.js
├── lazy-component.js
├── lazy-container.js
├── lazy-image.js
├── lazy.js
├── listener.js
└── util.js
├── test
└── test.spec.js
├── types
├── index.d.ts
├── lazyload.d.ts
├── test
│ ├── index.ts
│ └── tsconfig.json
└── vue.d.ts
├── vue-lazyload.esm.js
└── vue-lazyload.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | ["env", {
4 | "modules": false
5 | }]
6 | ],
7 | "plugins": [
8 | "syntax-dynamic-import",
9 | "external-helpers"
10 | ],
11 | "ignore": [
12 | "dist/*.js"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/.circleci/config.yml:
--------------------------------------------------------------------------------
1 | # Javascript Node CircleCI 2.0 configuration file
2 | #
3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details
4 | #
5 | version: 2
6 | jobs:
7 | build:
8 | docker:
9 | # specify the version you desire here
10 | - image: circleci/node:10.23.3
11 |
12 | # Specify service dependencies here if necessary
13 | # CircleCI maintains a library of pre-built images
14 | # documented at https://circleci.com/docs/2.0/circleci-images/
15 | # - image: circleci/mongo:3.4.4
16 |
17 | working_directory: ~/repo
18 |
19 | steps:
20 | - checkout
21 |
22 | # Download and cache dependencies
23 | - restore_cache:
24 | keys:
25 | - v1-dependencies-{{ checksum "package.json" }}
26 | # fallback to using the latest cache if no exact match is found
27 | - v1-dependencies-
28 |
29 | - run:
30 | name: Workaround for GoogleChrome/puppeteer#290
31 | command: 'sh .circleci/setup_puppeteer.sh'
32 |
33 | - run: yarn install
34 |
35 | - save_cache:
36 | paths:
37 | - node_modules
38 | key: v1-dependencies-{{ checksum "package.json" }}
39 |
40 | # run tests!
41 | - run: yarn lint
42 | - run: yarn test
43 |
--------------------------------------------------------------------------------
/.circleci/setup_puppeteer.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | sudo apt-get update
4 | sudo apt-get install -yq gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
5 | libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
6 | libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \
7 | libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \
8 | ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 4
6 | tab_width = 4
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "standard",
3 | "env": {
4 | "browser": true,
5 | "es6": true,
6 | "jest": true,
7 | "mocha": true
8 | }
9 | }
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ master ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ master ]
20 | schedule:
21 | - cron: '40 0 * * 4'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 |
28 | strategy:
29 | fail-fast: false
30 | matrix:
31 | language: [ 'javascript' ]
32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
33 | # Learn more:
34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
35 |
36 | steps:
37 | - name: Checkout repository
38 | uses: actions/checkout@v2
39 |
40 | # Initializes the CodeQL tools for scanning.
41 | - name: Initialize CodeQL
42 | uses: github/codeql-action/init@v1
43 | with:
44 | languages: ${{ matrix.language }}
45 | # If you wish to specify custom queries, you can do so here or in a config file.
46 | # By default, queries listed here will override any specified in a config file.
47 | # Prefix the list here with "+" to use these queries and those in the config file.
48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
49 |
50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
51 | # If this step fails, then you should remove it and run the build manually (see below)
52 | - name: Autobuild
53 | uses: github/codeql-action/autobuild@v1
54 |
55 | # ℹ️ Command-line programs to run using the OS shell.
56 | # 📚 https://git.io/JvXDl
57 |
58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
59 | # and modify them (or add more) to build your code if your project
60 | # uses a compiled language
61 |
62 | #- run: |
63 | # make bootstrap
64 | # make release
65 |
66 | - name: Perform CodeQL Analysis
67 | uses: github/codeql-action/analyze@v1
68 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | dist
4 | lib
5 | npm-debug.log
6 | package-lock.json
7 | .idea
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Awe
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vue-Lazyload
2 |
3 | [](https://circleci.com/gh/hilongjw/vue-lazyload)
4 | [](http://badge.fury.io/js/vue-lazyload)
5 | [](http://badge.fury.io/js/vue-lazyload)
6 | [](http://badge.fury.io/js/vue-lazyload)
7 | [](http://makeapullrequest.com)
8 | [](https://cdnjs.com/libraries/vue-lazyload)
9 |
10 | Vue module for lazyloading images in your applications. Some of goals of this project worth noting include:
11 |
12 | * Be lightweight, powerful and easy to use
13 | * Work on any image type
14 | * Add loading class while image is loading
15 | * Supports both of Vue 1.0 and Vue 2.0
16 |
17 | # For Vue 3
18 | Please use vue-lazyload@3.x, see [here](https://github.com/hilongjw/vue-lazyload/tree/next)
19 |
20 | # Table of Contents
21 |
22 | * [___Demo___](#demo)
23 | * [___Requirements___](#requirements)
24 | * [___Installation___](#installation)
25 | * [___Usage___](#usage)
26 | * [___Constructor Options___](#constructor-options)
27 | * [___Implementation___](#implementation)
28 | * [___Basic___](#basic)
29 | * [___Css state___](#css-state)
30 | * [___Methods___](#methods)
31 | * [__Event hook__](#event-hook)
32 | * [__LazyLoadHandler__](#lazyloadhandler)
33 | * [__Performance__](#performance)
34 | * [___Authors && Contributors___](#authors-&&-Contributors)
35 | * [___License___](#license)
36 |
37 |
38 | # Demo
39 |
40 | [___Demo___](http://hilongjw.github.io/vue-lazyload/)
41 |
42 | # Requirements
43 |
44 | - [Vue.js](https://github.com/vuejs/vue) `1.x` or `2.x`
45 |
46 |
47 | # Installation
48 |
49 | ## npm
50 |
51 | ```bash
52 |
53 | $ npm i vue-lazyload -S
54 |
55 | ```
56 |
57 | ## yarn
58 |
59 | ```bash
60 |
61 | $ yarn add vue-lazyload
62 |
63 | ```
64 |
65 | ## CDN
66 |
67 | CDN: [https://unpkg.com/vue-lazyload/vue-lazyload.js](https://unpkg.com/vue-lazyload/vue-lazyload.js)
68 |
69 | ```html
70 |
71 |
75 |
76 | ```
77 |
78 | # Usage
79 |
80 | main.js:
81 |
82 | ```javascript
83 |
84 | import Vue from 'vue'
85 | import App from './App.vue'
86 | import VueLazyload from 'vue-lazyload'
87 |
88 | Vue.use(VueLazyload)
89 |
90 | // or with options
91 | const loadimage = require('./assets/loading.gif')
92 | const errorimage = require('./assets/error.gif')
93 |
94 | Vue.use(VueLazyload, {
95 | preLoad: 1.3,
96 | error: errorimage,
97 | loading: loadimage,
98 | attempt: 1
99 | })
100 |
101 | new Vue({
102 | el: 'body',
103 | components: {
104 | App
105 | }
106 | })
107 | ```
108 |
109 | template:
110 |
111 | ```html
112 |
113 | -
114 |
115 |
116 |
117 | ```
118 |
119 | use `v-lazy-container` work with raw HTML
120 |
121 | ```html
122 |
127 | ```
128 |
129 | custom `error` and `loading` placeholder image
130 |
131 | ```html
132 |
137 | ```
138 |
139 | ```html
140 |
145 | ```
146 |
147 | ## Constructor Options
148 |
149 | |key|description|default|options|
150 | |:---|---|---|---|
151 | | `preLoad`|proportion of pre-loading height|`1.3`|`Number`|
152 | |`error`|src of the image upon load fail|`'data-src'`|`String`
153 | |`loading`|src of the image while loading|`'data-src'`|`String`|
154 | |`attempt`|attempts count|`3`|`Number`|
155 | |`listenEvents`|events that you want vue listen for|`['scroll', 'wheel', 'mousewheel', 'resize', 'animationend', 'transitionend', 'touchmove']`| [Desired Listen Events](#desired-listen-events) |
156 | |`adapter`| dynamically modify the attribute of element |`{ }`| [Element Adapter](#element-adapter) |
157 | |`filter`| the image's listener filter |`{ }`| [Image listener filter](#image-listener-filter) |
158 | |`lazyComponent`| lazyload component | `false` | [Lazy Component](#lazy-component)
159 | | `dispatchEvent`|trigger the dom event|`false`|`Boolean`|
160 | | `throttleWait`|throttle wait|`200`|`Number`|
161 | | `observer`|use IntersectionObserver|`false`|`Boolean`|
162 | | `observerOptions`|IntersectionObserver options|{ rootMargin: '0px', threshold: 0.1 }|[IntersectionObserver](#intersectionobserver)|
163 | | `silent`|do not print debug info|`true`|`Boolean`|
164 |
165 | ### Desired Listen Events
166 |
167 | You can configure which events you want vue-lazyload by passing in an array
168 | of listener names.
169 |
170 | ```javascript
171 | Vue.use(VueLazyload, {
172 | preLoad: 1.3,
173 | error: 'dist/error.png',
174 | loading: 'dist/loading.gif',
175 | attempt: 1,
176 | // the default is ['scroll', 'wheel', 'mousewheel', 'resize', 'animationend', 'transitionend']
177 | listenEvents: [ 'scroll' ]
178 | })
179 | ```
180 |
181 | This is useful if you are having trouble with this plugin resetting itself to loading
182 | when you have certain animations and transitions taking place
183 |
184 |
185 | ### Image listener filter
186 |
187 | dynamically modify the src of image
188 |
189 | ```javascript
190 | Vue.use(vueLazy, {
191 | filter: {
192 | progressive (listener, options) {
193 | const isCDN = /qiniudn.com/
194 | if (isCDN.test(listener.src)) {
195 | listener.el.setAttribute('lazy-progressive', 'true')
196 | listener.loading = listener.src + '?imageView2/1/w/10/h/10'
197 | }
198 | },
199 | webp (listener, options) {
200 | if (!options.supportWebp) return
201 | const isCDN = /qiniudn.com/
202 | if (isCDN.test(listener.src)) {
203 | listener.src += '?imageView2/2/format/webp'
204 | }
205 | }
206 | }
207 | })
208 | ```
209 |
210 |
211 | ### Element Adapter
212 |
213 | ```javascript
214 | Vue.use(vueLazy, {
215 | adapter: {
216 | loaded ({ bindType, el, naturalHeight, naturalWidth, $parent, src, loading, error, Init }) {
217 | // do something here
218 | // example for call LoadedHandler
219 | LoadedHandler(el)
220 | },
221 | loading (listender, Init) {
222 | console.log('loading')
223 | },
224 | error (listender, Init) {
225 | console.log('error')
226 | }
227 | }
228 | })
229 | ```
230 |
231 | ### IntersectionObserver
232 |
233 | use [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) to to improve performance of a large number of nodes.
234 |
235 | ```javascript
236 | Vue.use(vueLazy, {
237 | // set observer to true
238 | observer: true,
239 |
240 | // optional
241 | observerOptions: {
242 | rootMargin: '0px',
243 | threshold: 0.1
244 | }
245 | })
246 | ```
247 |
248 |
249 | ### Lazy Component
250 | ```javascript
251 | Vue.use(VueLazyload, {
252 | lazyComponent: true
253 | });
254 | ```
255 |
256 | ```html
257 |
258 |
259 |
260 |
261 |
272 | ```
273 | Use in list
274 | ```html
275 |
276 |
277 |
278 | ```
279 |
280 |
281 | ## Implementation
282 |
283 | ### Basic
284 |
285 | vue-lazyload will set this img element's `src` with `imgUrl` string
286 |
287 | ```html
288 |
302 |
303 |
304 |
305 |
![]()
306 |
307 |
308 |
309 |
![]()
310 |
311 |
312 |
313 |
![]()
314 |
315 |
316 |
317 |
![]()
318 |
![]()
319 |
320 |
321 | ```
322 |
323 | ### CSS state
324 |
325 | There are three states while img loading
326 |
327 | `loading` `loaded` `error`
328 |
329 | ```html
330 |
331 |
332 |
333 | ```
334 |
335 | ```html
336 |
359 | ```
360 |
361 | ## Methods
362 |
363 | ### Event Hook
364 |
365 | `vm.$Lazyload.$on(event, callback)`
366 | `vm.$Lazyload.$off(event, callback)`
367 | `vm.$Lazyload.$once(event, callback)`
368 |
369 | - `$on` Listen for a custom events `loading`, `loaded`, `error`
370 | - `$once` Listen for a custom event, but only once. The listener will be removed once it triggers for the first time.
371 | - `$off` Remove event listener(s).
372 |
373 | #### `vm.$Lazyload.$on`
374 |
375 | #### Arguments:
376 |
377 | * `{string} event`
378 | * `{Function} callback`
379 |
380 | #### Example
381 |
382 | ```javascript
383 | vm.$Lazyload.$on('loaded', function ({ bindType, el, naturalHeight, naturalWidth, $parent, src, loading, error }, formCache) {
384 | console.log(el, src)
385 | })
386 | ```
387 |
388 | #### `vm.$Lazyload.$once`
389 |
390 | #### Arguments:
391 |
392 | * `{string} event`
393 | * `{Function} callback`
394 |
395 | #### Example
396 |
397 | ```javascript
398 | vm.$Lazyload.$once('loaded', function ({ el, src }) {
399 | console.log(el, src)
400 | })
401 | ```
402 |
403 | #### `vm.$Lazyload.$off`
404 |
405 | If only the event is provided, remove all listeners for that event
406 |
407 | #### Arguments:
408 |
409 | * `{string} event`
410 | * `{Function} callback`
411 |
412 | #### Example
413 |
414 | ```javascript
415 | function handler ({ el, src }, formCache) {
416 | console.log(el, src)
417 | }
418 | vm.$Lazyload.$on('loaded', handler)
419 | vm.$Lazyload.$off('loaded', handler)
420 | vm.$Lazyload.$off('loaded')
421 | ```
422 |
423 | ### LazyLoadHandler
424 |
425 | `vm.$Lazyload.lazyLoadHandler`
426 |
427 | Manually trigger lazy loading position calculation
428 |
429 | #### Example
430 |
431 | ```javascript
432 |
433 | this.$Lazyload.lazyLoadHandler()
434 |
435 | ```
436 |
437 | ### Performance
438 |
439 | ```javascript
440 | this.$Lazyload.$on('loaded', function (listener) {
441 | console.table(this.$Lazyload.performance())
442 | })
443 | ```
444 |
445 | 
446 |
447 | ### Dynamic switching pictures
448 |
449 | ```vue
450 |
451 | ```
452 |
453 |
454 | # Authors && Contributors
455 |
456 | - [hilongjw](https://github.com/hilongjw)
457 | - [imcvampire](https://github.com/imcvampire)
458 | - [darrynten](https://github.com/darrynten)
459 | - [biluochun](https://github.com/biluochun)
460 | - [whwnow](https://github.com/whwnow)
461 | - [Leopoldthecoder](https://github.com/Leopoldthecoder)
462 | - [michalbcz](https://github.com/michalbcz)
463 | - [blue0728](https://github.com/blue0728)
464 | - [JounQin](https://github.com/JounQin)
465 | - [llissery](https://github.com/llissery)
466 | - [mega667](https://github.com/mega667)
467 | - [RobinCK](https://github.com/RobinCK)
468 | - [GallenHu](https://github.com/GallenHu)
469 |
470 | # License
471 |
472 | [The MIT License](http://opensource.org/licenses/MIT)
473 |
--------------------------------------------------------------------------------
/build.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const rollup = require('rollup')
3 | const babel = require('rollup-plugin-babel')
4 | const replace = require('@rollup/plugin-replace')
5 | const { terser } = require('rollup-plugin-terser')
6 | const resolve = require('rollup-plugin-node-resolve')
7 | const commonjs = require('rollup-plugin-commonjs')
8 | const version = process.env.VERSION || require('./package.json').version
9 |
10 | const banner =
11 | '/*!\n' +
12 | ' * Vue-Lazyload.js v' + version + '\n' +
13 | ' * (c) ' + new Date().getFullYear() + ' Awe \n' +
14 | ' * Released under the MIT License.\n' +
15 | ' */\n'
16 |
17 | async function build (options, _outputOptions) {
18 | try {
19 | const bundle = await rollup.rollup(options)
20 | const outputOptions = {
21 | format: _outputOptions.format,
22 | exports: 'named',
23 | banner: banner,
24 | file: path.resolve(__dirname, _outputOptions.filename),
25 | name: 'VueLazyload'
26 | }
27 | const { output } = await bundle.generate(outputOptions)
28 | await bundle.write(outputOptions)
29 | const code = output[0].code
30 | console.log(blue(outputOptions.file) + ' ' + getSize(code))
31 | } catch (e) {
32 | console.error(e)
33 | }
34 | }
35 |
36 | function getSize (code) {
37 | return (Buffer.byteLength(code, 'utf8') / 1024).toFixed(2) + 'kb'
38 | }
39 |
40 | function blue (str) {
41 | return '\x1b[1m\x1b[34m' + str + '\x1b[39m\x1b[22m'
42 | }
43 |
44 | build({
45 | input: path.resolve(__dirname, 'src/index.js'),
46 | plugins: [
47 | resolve(),
48 | commonjs(),
49 | babel({ runtimeHelpers: true }),
50 | replace({
51 | '__VUE_LAZYLOAD_VERSION__': JSON.stringify(version)
52 | }),
53 | terser()
54 | ]
55 | }, {
56 | format: 'umd',
57 | filename: 'vue-lazyload.js'
58 | })
59 |
60 | build({
61 | input: path.resolve(__dirname, 'src/index.js'),
62 | plugins: [
63 | resolve(),
64 | commonjs(),
65 | replace({
66 | '__VUE_LAZYLOAD_VERSION__': JSON.stringify(version)
67 | }),
68 | babel({ runtimeHelpers: true })
69 | ]
70 | }, {
71 | format: 'esm',
72 | filename: 'vue-lazyload.esm.js'
73 | })
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-lazyload",
3 | "version": "1.3.5",
4 | "description": "Vue module for lazy-loading images in your vue.js applications.",
5 | "main": "vue-lazyload.js",
6 | "module": "vue-lazyload.esm.js",
7 | "unpkg": "vue-lazyload.js",
8 | "scripts": {
9 | "build": "node build",
10 | "lint": "eslint ./src",
11 | "test": "jest"
12 | },
13 | "dependencies": {},
14 | "repository": {
15 | "type": "git",
16 | "url": "https://github.com/hilongjw/vue-lazyload.git"
17 | },
18 | "typings": "types/index.d.ts",
19 | "keywords": [
20 | "vue-lazyload",
21 | "vue",
22 | "lazyload",
23 | "vue-directive"
24 | ],
25 | "author": "Awe ",
26 | "bugs": {
27 | "url": "https://github.com/hilongjw/vue-lazyload/issues"
28 | },
29 | "browserslist": [
30 | "> 1%",
31 | "last 2 versions",
32 | "not ie <= 8"
33 | ],
34 | "license": "MIT",
35 | "jest": {
36 | "setupFiles": [
37 | "jest-canvas-mock"
38 | ]
39 | },
40 | "devDependencies": {
41 | "@rollup/plugin-replace": "^2.3.4",
42 | "assign-deep": "^1.0.1",
43 | "babel-cli": "^6.26.0",
44 | "babel-core": "^6.26.3",
45 | "babel-plugin-external-helpers": "^6.22.0",
46 | "babel-plugin-syntax-dynamic-import": "^6.18.0",
47 | "babel-polyfill": "^6.26.0",
48 | "babel-preset-env": "^1.7.0",
49 | "babel-preset-stage-0": "^6.24.1",
50 | "babel-register": "^6.26.0",
51 | "chai": "^4.3.0",
52 | "eslint": "^4.19.1",
53 | "eslint-config-standard": "^11.0.0",
54 | "eslint-plugin-import": "^2.22.1",
55 | "eslint-plugin-node": "^5.2.1",
56 | "eslint-plugin-promise": "^3.8.0",
57 | "eslint-plugin-standard": "^3.1.0",
58 | "jest": "^26.6.3",
59 | "jest-canvas-mock": "^2.3.1",
60 | "mocha": "^4.0.1",
61 | "rollup": "^2.39.0",
62 | "rollup-plugin-babel": "^2.6.1",
63 | "rollup-plugin-commonjs": "^8.4.1",
64 | "rollup-plugin-node-resolve": "^3.4.0",
65 | "rollup-plugin-replace": "^2.2.0",
66 | "rollup-plugin-terser": "^7.0.2",
67 | "rollup-plugin-uglify": "^1.0.1",
68 | "vue": "^2.6.12"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Lazy from './lazy'
2 | import LazyComponent from './lazy-component'
3 | import LazyContainer from './lazy-container'
4 | import LazyImage from './lazy-image'
5 | import { assign } from './util'
6 |
7 | export default {
8 | /*
9 | * install function
10 | * @param {Vue} Vue
11 | * @param {object} options lazyload options
12 | */
13 | install (Vue, options = {}) {
14 | const LazyClass = Lazy(Vue)
15 | const lazy = new LazyClass(options)
16 | const lazyContainer = new LazyContainer({ lazy })
17 |
18 | const isVue2 = Vue.version.split('.')[0] === '2'
19 |
20 | Vue.prototype.$Lazyload = lazy
21 |
22 | if (options.lazyComponent) {
23 | Vue.component('lazy-component', LazyComponent(lazy))
24 | }
25 |
26 | if (options.lazyImage) {
27 | Vue.component('lazy-image', LazyImage(lazy))
28 | }
29 |
30 | if (isVue2) {
31 | Vue.directive('lazy', {
32 | bind: lazy.add.bind(lazy),
33 | update: lazy.update.bind(lazy),
34 | componentUpdated: lazy.lazyLoadHandler.bind(lazy),
35 | unbind: lazy.remove.bind(lazy)
36 | })
37 | Vue.directive('lazy-container', {
38 | bind: lazyContainer.bind.bind(lazyContainer),
39 | componentUpdated: lazyContainer.update.bind(lazyContainer),
40 | unbind: lazyContainer.unbind.bind(lazyContainer)
41 | })
42 | } else {
43 | Vue.directive('lazy', {
44 | bind: lazy.lazyLoadHandler.bind(lazy),
45 | update (newValue, oldValue) {
46 | assign(this.vm.$refs, this.vm.$els)
47 | lazy.add(this.el, {
48 | modifiers: this.modifiers || {},
49 | arg: this.arg,
50 | value: newValue,
51 | oldValue: oldValue
52 | }, {
53 | context: this.vm
54 | })
55 | },
56 | unbind () {
57 | lazy.remove(this.el)
58 | }
59 | })
60 |
61 | Vue.directive('lazy-container', {
62 | update (newValue, oldValue) {
63 | lazyContainer.update(this.el, {
64 | modifiers: this.modifiers || {},
65 | arg: this.arg,
66 | value: newValue,
67 | oldValue: oldValue
68 | }, {
69 | context: this.vm
70 | })
71 | },
72 | unbind () {
73 | lazyContainer.unbind(this.el)
74 | }
75 | })
76 | }
77 | }
78 | }
79 |
80 | export {
81 | Lazy,
82 | LazyComponent,
83 | LazyImage,
84 | LazyContainer
85 | }
86 |
--------------------------------------------------------------------------------
/src/lazy-component.js:
--------------------------------------------------------------------------------
1 | import { inBrowser } from './util'
2 | import Lazy from './lazy'
3 |
4 | const LazyComponent = (lazy) => {
5 | return {
6 | props: {
7 | tag: {
8 | type: String,
9 | default: 'div'
10 | }
11 | },
12 | render (h) {
13 | return h(this.tag, null, this.show ? this.$slots.default : null)
14 | },
15 | data () {
16 | return {
17 | el: null,
18 | state: {
19 | loaded: false
20 | },
21 | rect: {},
22 | show: false
23 | }
24 | },
25 | mounted () {
26 | this.el = this.$el
27 | lazy.addLazyBox(this)
28 | lazy.lazyLoadHandler()
29 | },
30 | beforeDestroy () {
31 | lazy.removeComponent(this)
32 | },
33 | methods: {
34 | getRect () {
35 | this.rect = this.$el.getBoundingClientRect()
36 | },
37 | checkInView () {
38 | this.getRect()
39 | return inBrowser &&
40 | (this.rect.top < window.innerHeight * lazy.options.preLoad && this.rect.bottom > 0) &&
41 | (this.rect.left < window.innerWidth * lazy.options.preLoad && this.rect.right > 0)
42 | },
43 | load () {
44 | this.show = true
45 | this.state.loaded = true
46 | this.$emit('show', this)
47 | },
48 | destroy () {
49 | return this.$destroy
50 | }
51 | }
52 | }
53 | }
54 |
55 | LazyComponent.install = function (Vue, options = {}) {
56 | const LazyClass = Lazy(Vue)
57 | const lazy = new LazyClass(options)
58 | Vue.component('lazy-component', LazyComponent(lazy))
59 | }
60 |
61 | export default LazyComponent
62 |
--------------------------------------------------------------------------------
/src/lazy-container.js:
--------------------------------------------------------------------------------
1 | import {
2 | find,
3 | remove,
4 | assign,
5 | ArrayFrom
6 | } from './util'
7 | import Lazy from './lazy'
8 |
9 | export default class LazyContainerMananger {
10 | constructor ({ lazy }) {
11 | this.lazy = lazy
12 | lazy.lazyContainerMananger = this
13 | this._queue = []
14 | }
15 |
16 | bind (el, binding, vnode) {
17 | const container = new LazyContainer({ el, binding, vnode, lazy: this.lazy })
18 | this._queue.push(container)
19 | }
20 |
21 | update (el, binding, vnode) {
22 | const container = find(this._queue, item => item.el === el)
23 | if (!container) return
24 | container.update({ el, binding, vnode })
25 | }
26 |
27 | unbind (el, binding, vnode) {
28 | const container = find(this._queue, item => item.el === el)
29 | if (!container) return
30 | container.clear()
31 | remove(this._queue, container)
32 | }
33 | }
34 |
35 | const defaultOptions = {
36 | selector: 'img'
37 | }
38 |
39 | class LazyContainer {
40 | constructor ({ el, binding, vnode, lazy }) {
41 | this.el = null
42 | this.vnode = vnode
43 | this.binding = binding
44 | this.options = {}
45 | this.lazy = lazy
46 |
47 | this._queue = []
48 | this.update({ el, binding })
49 | }
50 |
51 | update ({ el, binding }) {
52 | this.el = el
53 | this.options = assign({}, defaultOptions, binding.value)
54 |
55 | const imgs = this.getImgs()
56 | imgs.forEach(el => {
57 | this.lazy.add(el, assign({}, this.binding, {
58 | value: {
59 | src: 'dataset' in el ? el.dataset.src : el.getAttribute('data-src'),
60 | error: ('dataset' in el ? el.dataset.error : el.getAttribute('data-error')) || this.options.error,
61 | loading: ('dataset' in el ? el.dataset.loading : el.getAttribute('data-loading')) || this.options.loading
62 | }
63 | }), this.vnode)
64 | })
65 | }
66 |
67 | getImgs () {
68 | return ArrayFrom(this.el.querySelectorAll(this.options.selector))
69 | }
70 |
71 | clear () {
72 | const imgs = this.getImgs()
73 | imgs.forEach(el => this.lazy.remove(el))
74 |
75 | this.vnode = null
76 | this.binding = null
77 | this.lazy = null
78 | }
79 | }
80 |
81 | LazyContainer.install = (Vue, options = {}) => {
82 | const LazyClass = Lazy(Vue)
83 | const lazy = new LazyClass(options)
84 | const lazyContainer = new LazyContainer({ lazy })
85 |
86 | const isVue2 = Vue.version.split('.')[0] === '2'
87 | if (isVue2) {
88 | Vue.directive('lazy-container', {
89 | bind: lazyContainer.bind.bind(lazyContainer),
90 | componentUpdated: lazyContainer.update.bind(lazyContainer),
91 | unbind: lazyContainer.unbind.bind(lazyContainer)
92 | })
93 | } else {
94 | Vue.directive('lazy-container', {
95 | update (newValue, oldValue) {
96 | lazyContainer.update(this.el, {
97 | modifiers: this.modifiers || {},
98 | arg: this.arg,
99 | value: newValue,
100 | oldValue: oldValue
101 | }, {
102 | context: this.vm
103 | })
104 | },
105 | unbind () {
106 | lazyContainer.unbind(this.el)
107 | }
108 | })
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/src/lazy-image.js:
--------------------------------------------------------------------------------
1 | import {
2 | inBrowser,
3 | loadImageAsync,
4 | noop
5 | } from './util'
6 | import Lazy from './lazy'
7 |
8 | const LazyImage = (lazyManager) => {
9 | return {
10 | props: {
11 | src: [String, Object],
12 | tag: {
13 | type: String,
14 | default: 'img'
15 | }
16 | },
17 | render (h) {
18 | return h(this.tag, {
19 | attrs: {
20 | src: this.renderSrc
21 | }
22 | }, this.$slots.default)
23 | },
24 | data () {
25 | return {
26 | el: null,
27 | options: {
28 | src: '',
29 | error: '',
30 | loading: '',
31 | attempt: lazyManager.options.attempt
32 | },
33 | state: {
34 | loaded: false,
35 | error: false,
36 | attempt: 0
37 | },
38 | rect: {},
39 | renderSrc: ''
40 | }
41 | },
42 | watch: {
43 | src () {
44 | this.init()
45 | lazyManager.addLazyBox(this)
46 | lazyManager.lazyLoadHandler()
47 | }
48 | },
49 | created () {
50 | this.init()
51 | this.renderSrc = this.options.loading
52 | },
53 | mounted () {
54 | this.el = this.$el
55 | lazyManager.addLazyBox(this)
56 | lazyManager.lazyLoadHandler()
57 | },
58 | beforeDestroy () {
59 | lazyManager.removeComponent(this)
60 | },
61 | methods: {
62 | init () {
63 | const { src, loading, error } = lazyManager._valueFormatter(this.src)
64 | this.state.loaded = false
65 | this.options.src = src
66 | this.options.error = error
67 | this.options.loading = loading
68 | this.renderSrc = this.options.loading
69 | },
70 | getRect () {
71 | this.rect = this.$el.getBoundingClientRect()
72 | },
73 | checkInView () {
74 | this.getRect()
75 | return inBrowser &&
76 | (this.rect.top < window.innerHeight * lazyManager.options.preLoad && this.rect.bottom > 0) &&
77 | (this.rect.left < window.innerWidth * lazyManager.options.preLoad && this.rect.right > 0)
78 | },
79 | load (onFinish = noop) {
80 | if ((this.state.attempt > this.options.attempt - 1) && this.state.error) {
81 | if (!lazyManager.options.silent) console.log(`VueLazyload log: ${this.options.src} tried too more than ${this.options.attempt} times`)
82 | onFinish()
83 | return
84 | }
85 | const src = this.options.src
86 | loadImageAsync({ src }, ({ src }) => {
87 | this.renderSrc = src
88 | this.state.loaded = true
89 | }, e => {
90 | this.state.attempt++
91 | this.renderSrc = this.options.error
92 | this.state.error = true
93 | })
94 | }
95 | }
96 | }
97 | }
98 |
99 | LazyImage.install = (Vue, options = {}) => {
100 | const LazyClass = Lazy(Vue)
101 | const lazy = new LazyClass(options)
102 | Vue.component('lazy-image', LazyImage(lazy))
103 | }
104 |
105 | export default LazyImage
106 |
--------------------------------------------------------------------------------
/src/lazy.js:
--------------------------------------------------------------------------------
1 | import {
2 | inBrowser,
3 | CustomEvent,
4 | remove,
5 | some,
6 | find,
7 | _,
8 | throttle,
9 | supportWebp,
10 | getDPR,
11 | scrollParent,
12 | getBestSelectionFromSrcset,
13 | assign,
14 | isObject,
15 | hasIntersectionObserver,
16 | modeType,
17 | ImageCache
18 | } from './util'
19 |
20 | import ReactiveListener from './listener'
21 |
22 | const DEFAULT_URL = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
23 | const DEFAULT_EVENTS = ['scroll', 'wheel', 'mousewheel', 'resize', 'animationend', 'transitionend', 'touchmove']
24 | const DEFAULT_OBSERVER_OPTIONS = {
25 | rootMargin: '0px',
26 | threshold: 0
27 | }
28 |
29 | export default function Lazy (Vue) {
30 | return class Lazy {
31 | constructor ({ preLoad, error, throttleWait, preLoadTop, dispatchEvent, loading, attempt, silent = true, scale, listenEvents, hasbind, filter, adapter, observer, observerOptions }) {
32 | this.version = '__VUE_LAZYLOAD_VERSION__'
33 | this.mode = modeType.event
34 | this.ListenerQueue = []
35 | this.TargetIndex = 0
36 | this.TargetQueue = []
37 | this.options = {
38 | silent: silent,
39 | dispatchEvent: !!dispatchEvent,
40 | throttleWait: throttleWait || 200,
41 | preLoad: preLoad || 1.3,
42 | preLoadTop: preLoadTop || 0,
43 | error: error || DEFAULT_URL,
44 | loading: loading || DEFAULT_URL,
45 | attempt: attempt || 3,
46 | scale: scale || getDPR(scale),
47 | ListenEvents: listenEvents || DEFAULT_EVENTS,
48 | hasbind: false,
49 | supportWebp: supportWebp(),
50 | filter: filter || {},
51 | adapter: adapter || {},
52 | observer: !!observer,
53 | observerOptions: observerOptions || DEFAULT_OBSERVER_OPTIONS
54 | }
55 | this._initEvent()
56 | this._imageCache = new ImageCache({ max: 200 })
57 | this.lazyLoadHandler = throttle(this._lazyLoadHandler.bind(this), this.options.throttleWait)
58 |
59 | this.setMode(this.options.observer ? modeType.observer : modeType.event)
60 | }
61 |
62 | /**
63 | * update config
64 | * @param {Object} config params
65 | * @return
66 | */
67 | config (options = {}) {
68 | assign(this.options, options)
69 | }
70 |
71 | /**
72 | * output listener's load performance
73 | * @return {Array}
74 | */
75 | performance () {
76 | let list = []
77 |
78 | this.ListenerQueue.map(item => {
79 | list.push(item.performance())
80 | })
81 |
82 | return list
83 | }
84 |
85 | /*
86 | * add lazy component to queue
87 | * @param {Vue} vm lazy component instance
88 | * @return
89 | */
90 | addLazyBox (vm) {
91 | this.ListenerQueue.push(vm)
92 | if (inBrowser) {
93 | this._addListenerTarget(window)
94 | this._observer && this._observer.observe(vm.el)
95 | if (vm.$el && vm.$el.parentNode) {
96 | this._addListenerTarget(vm.$el.parentNode)
97 | }
98 | }
99 | }
100 |
101 | /*
102 | * add image listener to queue
103 | * @param {DOM} el
104 | * @param {object} binding vue directive binding
105 | * @param {vnode} vnode vue directive vnode
106 | * @return
107 | */
108 | add (el, binding, vnode) {
109 | if (some(this.ListenerQueue, item => item.el === el)) {
110 | this.update(el, binding)
111 | return Vue.nextTick(this.lazyLoadHandler)
112 | }
113 |
114 | let { src, loading, error, cors } = this._valueFormatter(binding.value)
115 |
116 | Vue.nextTick(() => {
117 | src = getBestSelectionFromSrcset(el, this.options.scale) || src
118 | this._observer && this._observer.observe(el)
119 |
120 | const container = Object.keys(binding.modifiers)[0]
121 | let $parent
122 |
123 | if (container) {
124 | $parent = vnode.context.$refs[container]
125 | // if there is container passed in, try ref first, then fallback to getElementById to support the original usage
126 | $parent = $parent ? $parent.$el || $parent : document.getElementById(container)
127 | }
128 |
129 | if (!$parent) {
130 | $parent = scrollParent(el)
131 | }
132 |
133 | const newListener = new ReactiveListener({
134 | bindType: binding.arg,
135 | $parent,
136 | el,
137 | loading,
138 | error,
139 | src,
140 | cors,
141 | elRenderer: this._elRenderer.bind(this),
142 | options: this.options,
143 | imageCache: this._imageCache
144 | })
145 |
146 | this.ListenerQueue.push(newListener)
147 |
148 | if (inBrowser) {
149 | this._addListenerTarget(window)
150 | this._addListenerTarget($parent)
151 | }
152 |
153 | this.lazyLoadHandler()
154 | Vue.nextTick(() => this.lazyLoadHandler())
155 | })
156 | }
157 |
158 | /**
159 | * update image src
160 | * @param {DOM} el
161 | * @param {object} vue directive binding
162 | * @return
163 | */
164 | update (el, binding, vnode) {
165 | let { src, loading, error } = this._valueFormatter(binding.value)
166 | src = getBestSelectionFromSrcset(el, this.options.scale) || src
167 |
168 | const exist = find(this.ListenerQueue, item => item.el === el)
169 | if (!exist) {
170 | this.add(el, binding, vnode)
171 | } else {
172 | exist.update({
173 | src,
174 | loading,
175 | error
176 | })
177 | }
178 | if (this._observer) {
179 | this._observer.unobserve(el)
180 | this._observer.observe(el)
181 | }
182 | this.lazyLoadHandler()
183 | Vue.nextTick(() => this.lazyLoadHandler())
184 | }
185 |
186 | /**
187 | * remove listener form list
188 | * @param {DOM} el
189 | * @return
190 | */
191 | remove (el) {
192 | if (!el) return
193 | this._observer && this._observer.unobserve(el)
194 | const existItem = find(this.ListenerQueue, item => item.el === el)
195 | if (existItem) {
196 | this._removeListenerTarget(existItem.$parent)
197 | this._removeListenerTarget(window)
198 | remove(this.ListenerQueue, existItem)
199 | existItem.$destroy()
200 | }
201 | }
202 |
203 | /*
204 | * remove lazy components form list
205 | * @param {Vue} vm Vue instance
206 | * @return
207 | */
208 | removeComponent (vm) {
209 | if (!vm) return
210 | remove(this.ListenerQueue, vm)
211 | this._observer && this._observer.unobserve(vm.el)
212 | if (vm.$parent && vm.$el.parentNode) {
213 | this._removeListenerTarget(vm.$el.parentNode)
214 | }
215 | this._removeListenerTarget(window)
216 | }
217 |
218 | setMode (mode) {
219 | if (!hasIntersectionObserver && mode === modeType.observer) {
220 | mode = modeType.event
221 | }
222 |
223 | this.mode = mode // event or observer
224 |
225 | if (mode === modeType.event) {
226 | if (this._observer) {
227 | this.ListenerQueue.forEach(listener => {
228 | this._observer.unobserve(listener.el)
229 | })
230 | this._observer = null
231 | }
232 |
233 | this.TargetQueue.forEach(target => {
234 | this._initListen(target.el, true)
235 | })
236 | } else {
237 | this.TargetQueue.forEach(target => {
238 | this._initListen(target.el, false)
239 | })
240 | this._initIntersectionObserver()
241 | }
242 | }
243 |
244 | /*
245 | *** Private functions ***
246 | */
247 |
248 | /*
249 | * add listener target
250 | * @param {DOM} el listener target
251 | * @return
252 | */
253 | _addListenerTarget (el) {
254 | if (!el) return
255 | let target = find(this.TargetQueue, target => target.el === el)
256 | if (!target) {
257 | target = {
258 | el: el,
259 | id: ++this.TargetIndex,
260 | childrenCount: 1,
261 | listened: true
262 | }
263 | this.mode === modeType.event && this._initListen(target.el, true)
264 | this.TargetQueue.push(target)
265 | } else {
266 | target.childrenCount++
267 | }
268 | return this.TargetIndex
269 | }
270 |
271 | /*
272 | * remove listener target or reduce target childrenCount
273 | * @param {DOM} el or window
274 | * @return
275 | */
276 | _removeListenerTarget (el) {
277 | this.TargetQueue.forEach((target, index) => {
278 | if (target.el === el) {
279 | target.childrenCount--
280 | if (!target.childrenCount) {
281 | this._initListen(target.el, false)
282 | this.TargetQueue.splice(index, 1)
283 | target = null
284 | }
285 | }
286 | })
287 | }
288 |
289 | /*
290 | * add or remove eventlistener
291 | * @param {DOM} el DOM or Window
292 | * @param {boolean} start flag
293 | * @return
294 | */
295 | _initListen (el, start) {
296 | this.options.ListenEvents.forEach((evt) => _[start ? 'on' : 'off'](el, evt, this.lazyLoadHandler))
297 | }
298 |
299 | _initEvent () {
300 | this.Event = {
301 | listeners: {
302 | loading: [],
303 | loaded: [],
304 | error: []
305 | }
306 | }
307 |
308 | this.$on = (event, func) => {
309 | if (!this.Event.listeners[event]) this.Event.listeners[event] = []
310 | this.Event.listeners[event].push(func)
311 | }
312 |
313 | this.$once = (event, func) => {
314 | const vm = this
315 | function on () {
316 | vm.$off(event, on)
317 | func.apply(vm, arguments)
318 | }
319 | this.$on(event, on)
320 | }
321 |
322 | this.$off = (event, func) => {
323 | if (!func) {
324 | if (!this.Event.listeners[event]) return
325 | this.Event.listeners[event].length = 0
326 | return
327 | }
328 | remove(this.Event.listeners[event], func)
329 | }
330 |
331 | this.$emit = (event, context, inCache) => {
332 | if (!this.Event.listeners[event]) return
333 | this.Event.listeners[event].forEach(func => func(context, inCache))
334 | }
335 | }
336 |
337 | /**
338 | * find nodes which in viewport and trigger load
339 | * @return
340 | */
341 | _lazyLoadHandler () {
342 | const freeList = []
343 | this.ListenerQueue.forEach((listener, index) => {
344 | if (!listener.el || !listener.el.parentNode) {
345 | freeList.push(listener)
346 | }
347 | const catIn = listener.checkInView()
348 | if (!catIn) return
349 | listener.load()
350 | })
351 | freeList.forEach(item => {
352 | remove(this.ListenerQueue, item)
353 | item.$destroy()
354 | })
355 | }
356 | /**
357 | * init IntersectionObserver
358 | * set mode to observer
359 | * @return
360 | */
361 | _initIntersectionObserver () {
362 | if (!hasIntersectionObserver) return
363 | this._observer = new IntersectionObserver(this._observerHandler.bind(this), this.options.observerOptions)
364 | if (this.ListenerQueue.length) {
365 | this.ListenerQueue.forEach(listener => {
366 | this._observer.observe(listener.el)
367 | })
368 | }
369 | }
370 |
371 | /**
372 | * init IntersectionObserver
373 | * @return
374 | */
375 | _observerHandler (entries, observer) {
376 | entries.forEach(entry => {
377 | if (entry.isIntersecting) {
378 | this.ListenerQueue.forEach(listener => {
379 | if (listener.el === entry.target) {
380 | if (listener.state.loaded) return this._observer.unobserve(listener.el)
381 | listener.load()
382 | }
383 | })
384 | }
385 | })
386 | }
387 |
388 | /**
389 | * set element attribute with image'url and state
390 | * @param {object} lazyload listener object
391 | * @param {string} state will be rendered
392 | * @param {bool} inCache is rendered from cache
393 | * @return
394 | */
395 | _elRenderer (listener, state, cache) {
396 | if (!listener.el) return
397 | const { el, bindType } = listener
398 |
399 | let src
400 | switch (state) {
401 | case 'loading':
402 | src = listener.loading
403 | break
404 | case 'error':
405 | src = listener.error
406 | break
407 | default:
408 | src = listener.src
409 | break
410 | }
411 |
412 | if (bindType) {
413 | el.style[bindType] = 'url("' + src + '")'
414 | } else if (el.getAttribute('src') !== src) {
415 | el.setAttribute('src', src)
416 | }
417 |
418 | el.setAttribute('lazy', state)
419 |
420 | this.$emit(state, listener, cache)
421 | this.options.adapter[state] && this.options.adapter[state](listener, this.options)
422 |
423 | if (this.options.dispatchEvent) {
424 | const event = new CustomEvent(state, {
425 | detail: listener
426 | })
427 | el.dispatchEvent(event)
428 | }
429 | }
430 |
431 | /**
432 | * generate loading loaded error image url
433 | * @param {string} image's src
434 | * @return {object} image's loading, loaded, error url
435 | */
436 | _valueFormatter (value) {
437 | let src = value
438 | let loading = this.options.loading
439 | let error = this.options.error
440 |
441 | // value is object
442 | if (isObject(value)) {
443 | if (!value.src && !this.options.silent) console.error('Vue Lazyload warning: miss src with ' + value)
444 | src = value.src
445 | loading = value.loading || this.options.loading
446 | error = value.error || this.options.error
447 | }
448 | return {
449 | src,
450 | loading,
451 | error
452 | }
453 | }
454 | }
455 | }
456 |
457 | Lazy.install = (Vue, options = {}) => {
458 | const LazyClass = Lazy(Vue)
459 | const lazy = new LazyClass(options)
460 |
461 | const isVue2 = Vue.version.split('.')[0] === '2'
462 | if (isVue2) {
463 | Vue.directive('lazy', {
464 | bind: lazy.add.bind(lazy),
465 | update: lazy.update.bind(lazy),
466 | componentUpdated: lazy.lazyLoadHandler.bind(lazy),
467 | unbind: lazy.remove.bind(lazy)
468 | })
469 | } else {
470 | Vue.directive('lazy', {
471 | bind: lazy.lazyLoadHandler.bind(lazy),
472 | update (newValue, oldValue) {
473 | assign(this.vm.$refs, this.vm.$els)
474 | lazy.add(this.el, {
475 | modifiers: this.modifiers || {},
476 | arg: this.arg,
477 | value: newValue,
478 | oldValue: oldValue
479 | }, {
480 | context: this.vm
481 | })
482 | },
483 | unbind () {
484 | lazy.remove(this.el)
485 | }
486 | })
487 | }
488 | }
489 |
--------------------------------------------------------------------------------
/src/listener.js:
--------------------------------------------------------------------------------
1 | import {
2 | loadImageAsync,
3 | ObjectKeys,
4 | noop
5 | } from './util'
6 |
7 | // el: {
8 | // state,
9 | // src,
10 | // error,
11 | // loading
12 | // }
13 |
14 | export default class ReactiveListener {
15 | constructor ({ el, src, error, loading, bindType, $parent, options, cors, elRenderer, imageCache }) {
16 | this.el = el
17 | this.src = src
18 | this.error = error
19 | this.loading = loading
20 | this.bindType = bindType
21 | this.attempt = 0
22 | this.cors = cors
23 |
24 | this.naturalHeight = 0
25 | this.naturalWidth = 0
26 |
27 | this.options = options
28 |
29 | this.rect = null
30 |
31 | this.$parent = $parent
32 | this.elRenderer = elRenderer
33 | this._imageCache = imageCache
34 | this.performanceData = {
35 | init: Date.now(),
36 | loadStart: 0,
37 | loadEnd: 0
38 | }
39 |
40 | this.filter()
41 | this.initState()
42 | this.render('loading', false)
43 | }
44 |
45 | /*
46 | * init listener state
47 | * @return
48 | */
49 | initState () {
50 | if ('dataset' in this.el) {
51 | this.el.dataset.src = this.src
52 | } else {
53 | this.el.setAttribute('data-src', this.src)
54 | }
55 |
56 | this.state = {
57 | loading: false,
58 | error: false,
59 | loaded: false,
60 | rendered: false
61 | }
62 | }
63 |
64 | /*
65 | * record performance
66 | * @return
67 | */
68 | record (event) {
69 | this.performanceData[event] = Date.now()
70 | }
71 |
72 | /*
73 | * update image listener data
74 | * @param {String} image uri
75 | * @param {String} loading image uri
76 | * @param {String} error image uri
77 | * @return
78 | */
79 | update ({ src, loading, error }) {
80 | const oldSrc = this.src
81 | this.src = src
82 | this.loading = loading
83 | this.error = error
84 | this.filter()
85 | if (oldSrc !== this.src) {
86 | this.attempt = 0
87 | this.initState()
88 | }
89 | }
90 |
91 | /*
92 | * get el node rect
93 | * @return
94 | */
95 | getRect () {
96 | this.rect = this.el.getBoundingClientRect()
97 | }
98 |
99 | /*
100 | * check el is in view
101 | * @return {Boolean} el is in view
102 | */
103 | checkInView () {
104 | this.getRect()
105 | return (this.rect.top < window.innerHeight * this.options.preLoad && this.rect.bottom > this.options.preLoadTop) &&
106 | (this.rect.left < window.innerWidth * this.options.preLoad && this.rect.right > 0)
107 | }
108 |
109 | /*
110 | * listener filter
111 | */
112 | filter () {
113 | ObjectKeys(this.options.filter).map(key => {
114 | this.options.filter[key](this, this.options)
115 | })
116 | }
117 |
118 | /*
119 | * render loading first
120 | * @params cb:Function
121 | * @return
122 | */
123 | renderLoading (cb) {
124 | this.state.loading = true
125 | loadImageAsync({
126 | src: this.loading,
127 | cors: this.cors
128 | }, data => {
129 | this.render('loading', false)
130 | this.state.loading = false
131 | cb()
132 | }, () => {
133 | // handler `loading image` load failed
134 | cb()
135 | this.state.loading = false
136 | if (!this.options.silent) console.warn(`VueLazyload log: load failed with loading image(${this.loading})`)
137 | })
138 | }
139 |
140 | /*
141 | * try load image and render it
142 | * @return
143 | */
144 | load (onFinish = noop) {
145 | if ((this.attempt > this.options.attempt - 1) && this.state.error) {
146 | if (!this.options.silent) console.log(`VueLazyload log: ${this.src} tried too more than ${this.options.attempt} times`)
147 | onFinish()
148 | return
149 | }
150 | if (this.state.rendered && this.state.loaded) return
151 | if (this._imageCache.has(this.src)) {
152 | this.state.loaded = true
153 | this.render('loaded', true)
154 | this.state.rendered = true
155 | return onFinish()
156 | }
157 |
158 | this.renderLoading(() => {
159 | this.attempt++
160 |
161 | this.options.adapter['beforeLoad'] && this.options.adapter['beforeLoad'](this, this.options)
162 | this.record('loadStart')
163 |
164 | loadImageAsync({
165 | src: this.src,
166 | cors: this.cors
167 | }, data => {
168 | this.naturalHeight = data.naturalHeight
169 | this.naturalWidth = data.naturalWidth
170 | this.state.loaded = true
171 | this.state.error = false
172 | this.record('loadEnd')
173 | this.render('loaded', false)
174 | this.state.rendered = true
175 | this._imageCache.add(this.src)
176 | onFinish()
177 | }, err => {
178 | !this.options.silent && console.error(err)
179 | this.state.error = true
180 | this.state.loaded = false
181 | this.render('error', false)
182 | })
183 | })
184 | }
185 |
186 | /*
187 | * render image
188 | * @param {String} state to render // ['loading', 'src', 'error']
189 | * @param {String} is form cache
190 | * @return
191 | */
192 | render (state, cache) {
193 | this.elRenderer(this, state, cache)
194 | }
195 |
196 | /*
197 | * output performance data
198 | * @return {Object} performance data
199 | */
200 | performance () {
201 | let state = 'loading'
202 | let time = 0
203 |
204 | if (this.state.loaded) {
205 | state = 'loaded'
206 | time = (this.performanceData.loadEnd - this.performanceData.loadStart) / 1000
207 | }
208 |
209 | if (this.state.error) state = 'error'
210 |
211 | return {
212 | src: this.src,
213 | state,
214 | time
215 | }
216 | }
217 |
218 | /*
219 | * $destroy
220 | * @return
221 | */
222 | $destroy () {
223 | this.el = null
224 | this.src = null
225 | this.error = null
226 | this.loading = null
227 | this.bindType = null
228 | this.attempt = 0
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/src/util.js:
--------------------------------------------------------------------------------
1 | import assign from 'assign-deep'
2 |
3 | const inBrowser = typeof window !== 'undefined' && window !== null
4 |
5 | export const hasIntersectionObserver = checkIntersectionObserver()
6 |
7 | function checkIntersectionObserver () {
8 | if (inBrowser &&
9 | 'IntersectionObserver' in window &&
10 | 'IntersectionObserverEntry' in window &&
11 | 'intersectionRatio' in window.IntersectionObserverEntry.prototype) {
12 | // Minimal polyfill for Edge 15's lack of `isIntersecting`
13 | // See: https://github.com/w3c/IntersectionObserver/issues/211
14 | if (!('isIntersecting' in window.IntersectionObserverEntry.prototype)) {
15 | Object.defineProperty(window.IntersectionObserverEntry.prototype,
16 | 'isIntersecting', {
17 | get: function () {
18 | return this.intersectionRatio > 0
19 | }
20 | })
21 | }
22 | return true
23 | }
24 | return false
25 | }
26 |
27 | export const modeType = {
28 | event: 'event',
29 | observer: 'observer'
30 | }
31 |
32 | // CustomEvent polyfill for IE
33 | const CustomEvent = (function () {
34 | if (!inBrowser) return
35 | // not IE
36 | if (typeof window.CustomEvent === 'function') return window.CustomEvent
37 | function CustomEvent (event, params) {
38 | params = params || { bubbles: false, cancelable: false, detail: undefined }
39 | var evt = document.createEvent('CustomEvent')
40 | evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail)
41 | return evt
42 | }
43 | CustomEvent.prototype = window.Event.prototype
44 | return CustomEvent
45 | })()
46 |
47 | function remove (arr, item) {
48 | if (!arr.length) return
49 | const index = arr.indexOf(item)
50 | if (index > -1) return arr.splice(index, 1)
51 | }
52 |
53 | function some (arr, fn) {
54 | let has = false
55 | for (let i = 0, len = arr.length; i < len; i++) {
56 | if (fn(arr[i])) {
57 | has = true
58 | break
59 | }
60 | }
61 | return has
62 | }
63 |
64 | function getBestSelectionFromSrcset (el, scale) {
65 | if (el.tagName !== 'IMG' || !el.getAttribute('data-srcset')) return
66 |
67 | let options = el.getAttribute('data-srcset')
68 | const result = []
69 | const container = el.parentNode
70 | const containerWidth = container.offsetWidth * scale
71 |
72 | let spaceIndex
73 | let tmpSrc
74 | let tmpWidth
75 |
76 | options = options.trim().split(',')
77 |
78 | options.map(item => {
79 | item = item.trim()
80 | spaceIndex = item.lastIndexOf(' ')
81 | if (spaceIndex === -1) {
82 | tmpSrc = item
83 | tmpWidth = 999998
84 | } else {
85 | tmpSrc = item.substr(0, spaceIndex)
86 | tmpWidth = parseInt(item.substr(spaceIndex + 1, item.length - spaceIndex - 2), 10)
87 | }
88 | result.push([tmpWidth, tmpSrc])
89 | })
90 |
91 | result.sort(function (a, b) {
92 | if (a[0] < b[0]) {
93 | return 1
94 | }
95 | if (a[0] > b[0]) {
96 | return -1
97 | }
98 | if (a[0] === b[0]) {
99 | if (b[1].indexOf('.webp', b[1].length - 5) !== -1) {
100 | return 1
101 | }
102 | if (a[1].indexOf('.webp', a[1].length - 5) !== -1) {
103 | return -1
104 | }
105 | }
106 | return 0
107 | })
108 | let bestSelectedSrc = ''
109 | let tmpOption
110 |
111 | for (let i = 0; i < result.length; i++) {
112 | tmpOption = result[i]
113 | bestSelectedSrc = tmpOption[1]
114 | const next = result[i + 1]
115 | if (next && next[0] < containerWidth) {
116 | bestSelectedSrc = tmpOption[1]
117 | break
118 | } else if (!next) {
119 | bestSelectedSrc = tmpOption[1]
120 | break
121 | }
122 | }
123 |
124 | return bestSelectedSrc
125 | }
126 |
127 | function find (arr, fn) {
128 | let item
129 | for (let i = 0, len = arr.length; i < len; i++) {
130 | if (fn(arr[i])) {
131 | item = arr[i]
132 | break
133 | }
134 | }
135 | return item
136 | }
137 |
138 | const getDPR = (scale = 1) => inBrowser ? (window.devicePixelRatio || scale) : scale
139 |
140 | function supportWebp () {
141 | if (!inBrowser) return false
142 |
143 | let support = true
144 |
145 | try {
146 | const elem = document.createElement('canvas')
147 |
148 | if (elem.getContext && elem.getContext('2d')) {
149 | support = elem.toDataURL('image/webp').indexOf('data:image/webp') === 0
150 | }
151 | } catch (err) {
152 | support = false
153 | }
154 |
155 | return support
156 | }
157 |
158 | function throttle (action, delay) {
159 | let timeout = null
160 | let movement = null
161 | let lastRun = 0
162 | let needRun = false
163 | return function () {
164 | needRun = true
165 | if (timeout) {
166 | return
167 | }
168 | let elapsed = Date.now() - lastRun
169 | let context = this
170 | let args = arguments
171 | let runCallback = function () {
172 | lastRun = Date.now()
173 | timeout = false
174 | action.apply(context, args)
175 | }
176 | if (elapsed >= delay) {
177 | runCallback()
178 | } else {
179 | timeout = setTimeout(runCallback, delay)
180 | }
181 | if (needRun) {
182 | clearTimeout(movement)
183 | movement = setTimeout(runCallback, 2 * delay)
184 | }
185 | }
186 | }
187 |
188 | function testSupportsPassive () {
189 | if (!inBrowser) return
190 | let support = false
191 | try {
192 | let opts = Object.defineProperty({}, 'passive', {
193 | get: function () {
194 | support = true
195 | }
196 | })
197 | window.addEventListener('test', null, opts)
198 | } catch (e) {}
199 | return support
200 | }
201 |
202 | const supportsPassive = testSupportsPassive()
203 |
204 | const _ = {
205 | on (el, type, func, capture = false) {
206 | if (supportsPassive) {
207 | el.addEventListener(type, func, {
208 | capture: capture,
209 | passive: true
210 | })
211 | } else {
212 | el.addEventListener(type, func, capture)
213 | }
214 | },
215 | off (el, type, func, capture = false) {
216 | el.removeEventListener(type, func, capture)
217 | }
218 | }
219 |
220 | const loadImageAsync = (item, resolve, reject) => {
221 | let image = new Image()
222 | if (!item || !item.src) {
223 | const err = new Error('image src is required')
224 | return reject(err)
225 | }
226 |
227 | image.src = item.src
228 | if (item.cors) {
229 | image.crossOrigin = item.cors
230 | }
231 |
232 | image.onload = function () {
233 | resolve({
234 | naturalHeight: image.naturalHeight,
235 | naturalWidth: image.naturalWidth,
236 | src: image.src
237 | })
238 | }
239 |
240 | image.onerror = function (e) {
241 | reject(e)
242 | }
243 | }
244 |
245 | const style = (el, prop) => {
246 | return typeof getComputedStyle !== 'undefined'
247 | ? getComputedStyle(el, null).getPropertyValue(prop)
248 | : el.style[prop]
249 | }
250 |
251 | const overflow = (el) => {
252 | return style(el, 'overflow') + style(el, 'overflow-y') + style(el, 'overflow-x')
253 | }
254 |
255 | const scrollParent = (el) => {
256 | if (!inBrowser) return
257 | if (!(el instanceof HTMLElement)) {
258 | return window
259 | }
260 |
261 | let parent = el
262 |
263 | while (parent) {
264 | if (parent === document.body || parent === document.documentElement) {
265 | break
266 | }
267 |
268 | if (!parent.parentNode) {
269 | break
270 | }
271 |
272 | if (/(scroll|auto)/.test(overflow(parent))) {
273 | return parent
274 | }
275 |
276 | parent = parent.parentNode
277 | }
278 |
279 | return window
280 | }
281 |
282 | function isObject (obj) {
283 | return obj !== null && typeof obj === 'object'
284 | }
285 |
286 | function ObjectKeys (obj) {
287 | if (!(obj instanceof Object)) return []
288 | if (Object.keys) {
289 | return Object.keys(obj)
290 | } else {
291 | let keys = []
292 | for (let key in obj) {
293 | if (obj.hasOwnProperty(key)) {
294 | keys.push(key)
295 | }
296 | }
297 | return keys
298 | }
299 | }
300 |
301 | function ArrayFrom (arrLike) {
302 | let len = arrLike.length
303 | const list = []
304 | for (let i = 0; i < len; i++) {
305 | list.push(arrLike[i])
306 | }
307 | return list
308 | }
309 |
310 | function noop () {}
311 |
312 | class ImageCache {
313 | constructor ({ max }) {
314 | this.options = {
315 | max: max || 100
316 | }
317 | this._caches = []
318 | }
319 |
320 | has (key) {
321 | return this._caches.indexOf(key) > -1
322 | }
323 |
324 | add (key) {
325 | if (this.has(key)) return
326 | this._caches.push(key)
327 | if (this._caches.length > this.options.max) {
328 | this.free()
329 | }
330 | }
331 |
332 | free () {
333 | this._caches.shift()
334 | }
335 | }
336 |
337 | export {
338 | ImageCache,
339 | inBrowser,
340 | CustomEvent,
341 | remove,
342 | some,
343 | find,
344 | assign,
345 | noop,
346 | ArrayFrom,
347 | _,
348 | isObject,
349 | throttle,
350 | supportWebp,
351 | getDPR,
352 | scrollParent,
353 | loadImageAsync,
354 | getBestSelectionFromSrcset,
355 | ObjectKeys
356 | }
357 |
--------------------------------------------------------------------------------
/test/test.spec.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import VueLazyload from '../src'
3 | import genLazyCore from '../src/lazy'
4 | import assert from 'assert'
5 |
6 | describe('VueLazyload.js Test Suite', function () {
7 | it('install', function () {
8 | Vue.use(VueLazyload)
9 | const vm = new Vue()
10 | assert(vm.$Lazyload, 'has $Lazyload')
11 | })
12 |
13 | it('_valueFormatter', function () {
14 | const LazyCore = genLazyCore(Vue)
15 |
16 | const lazyload = new LazyCore({
17 | error: 'error',
18 | loading: 'loading'
19 | })
20 |
21 | expect(lazyload._valueFormatter('src').src).toBe('src')
22 | expect(lazyload._valueFormatter('src').error).toBe('error')
23 | expect(lazyload._valueFormatter('src').loading).toBe('loading')
24 |
25 | expect(lazyload._valueFormatter({
26 | src: 'src',
27 | error: 'error',
28 | loading: 'loading'
29 | }).src).toBe('src')
30 |
31 | expect(lazyload._valueFormatter({
32 | src: 'src',
33 | error: 'error',
34 | loading: 'loading'
35 | }).error).toBe('error')
36 |
37 | expect(lazyload._valueFormatter({
38 | src: 'src',
39 | error: 'error',
40 | loading: 'loading'
41 | }).loading).toBe('loading')
42 | })
43 | })
44 |
--------------------------------------------------------------------------------
/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import "./vue";
2 | import { VueLazyloadPluginObject } from "./lazyload";
3 |
4 | declare var VueLazyload: VueLazyloadPluginObject;
5 | export default VueLazyload;
6 |
7 | export {
8 | VueLazyloadImage,
9 | VueLazyloadOptions,
10 | VueLazyloadHandler,
11 | VueReactiveListener
12 | } from "./lazyload";
13 |
--------------------------------------------------------------------------------
/types/lazyload.d.ts:
--------------------------------------------------------------------------------
1 | import { PluginObject } from "vue";
2 |
3 | interface IntersectionObserverInit {
4 | root?: Element | null;
5 | rootMargin?: string;
6 | threshold?: number | number[];
7 | }
8 |
9 | export interface VueLazyloadImage {
10 | src: string;
11 | error?: string;
12 | loading?: string;
13 | }
14 |
15 | export interface VueLazyloadOptions {
16 | lazyComponent?: boolean;
17 | preLoad?: number;
18 | error?: string;
19 | loading?: string;
20 | attempt?: number;
21 | listenEvents?: string[];
22 | adapter?: any;
23 | filter?: any;
24 | dispatchEvent?: boolean;
25 | throttleWait?: number;
26 | observer?: boolean;
27 | observerOptions?: IntersectionObserverInit;
28 | silent?: boolean;
29 | preLoadTop?: number;
30 | scale?: number;
31 | hasbind?: boolean;
32 | }
33 |
34 | export interface VueReactiveListener {
35 | el: Element;
36 | src: string;
37 | error: string;
38 | loading: string;
39 | bindType: string;
40 | attempt: number;
41 | naturalHeight: number;
42 | naturalWidth: number;
43 | options: VueLazyloadOptions;
44 | rect: DOMRect;
45 | $parent: Element
46 | elRenderer: Function;
47 | performanceData: {
48 | init: number,
49 | loadStart: number,
50 | loadEnd: number
51 | };
52 | }
53 |
54 | export interface VueLazyloadListenEvent {
55 | (listener: VueReactiveListener, cache: boolean) : void;
56 | }
57 |
58 | export interface VueLazyloadHandler {
59 | $on (event: string, callback: VueLazyloadListenEvent): void;
60 | $once (event: string, callback: VueLazyloadListenEvent): void;
61 | $off (event: string, callback?: VueLazyloadListenEvent): void;
62 | lazyLoadHandler (): void;
63 | }
64 |
65 | export interface VueLazyloadPluginObject extends PluginObject {}
66 |
--------------------------------------------------------------------------------
/types/test/index.ts:
--------------------------------------------------------------------------------
1 | import Vue from "vue";
2 | import VueLazyload, { VueLazyloadOptions } from "../index";
3 |
4 | Vue.use(VueLazyload);
5 |
6 | Vue.use(VueLazyload, {
7 | preLoad: 0,
8 | });
9 |
10 | const vm = new Vue({});
11 |
12 | vm.$Lazyload.lazyLoadHandler();
13 | vm.$Lazyload.$on('loading', function (state, cache) {
14 | const err: string = state.error;
15 | const el: Element = state.el;
16 | const bol: boolean = cache;
17 | });
--------------------------------------------------------------------------------
/types/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "es2015",
5 | "moduleResolution": "node",
6 | "lib": [
7 | "es5",
8 | "dom",
9 | "es2015.promise",
10 | "es2015.core"
11 | ],
12 | "strict": true,
13 | "noEmit": true
14 | },
15 | "include": [
16 | "*.ts",
17 | "../*.d.ts"
18 | ]
19 | }
--------------------------------------------------------------------------------
/types/vue.d.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Augment the typings of Vue.js
3 | */
4 |
5 | import Vue from "vue";
6 | import { VueLazyloadHandler } from "./index";
7 |
8 | declare module "vue/types/vue" {
9 | interface Vue {
10 | $Lazyload: VueLazyloadHandler;
11 | }
12 | }
13 |
14 |
--------------------------------------------------------------------------------
/vue-lazyload.esm.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Vue-Lazyload.js v1.3.5
3 | * (c) 2023 Awe
4 | * Released under the MIT License.
5 | */
6 |
7 | function createCommonjsModule(fn, module) {
8 | return module = { exports: {} }, fn(module, module.exports), module.exports;
9 | }
10 |
11 | var assignSymbols$1 = createCommonjsModule(function (module) {
12 |
13 | var toString = Object.prototype.toString;
14 | var isEnumerable = Object.prototype.propertyIsEnumerable;
15 | var getSymbols = Object.getOwnPropertySymbols;
16 |
17 | module.exports = function (target) {
18 | for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
19 | args[_key - 1] = arguments[_key];
20 | }
21 |
22 | if (!isObject(target)) {
23 | throw new TypeError('expected the first argument to be an object');
24 | }
25 |
26 | if (args.length === 0 || typeof Symbol !== 'function' || typeof getSymbols !== 'function') {
27 | return target;
28 | }
29 |
30 | var _iteratorNormalCompletion = true;
31 | var _didIteratorError = false;
32 | var _iteratorError = undefined;
33 |
34 | try {
35 | for (var _iterator = args[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
36 | var arg = _step.value;
37 |
38 | var names = getSymbols(arg);
39 |
40 | var _iteratorNormalCompletion2 = true;
41 | var _didIteratorError2 = false;
42 | var _iteratorError2 = undefined;
43 |
44 | try {
45 | for (var _iterator2 = names[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
46 | var key = _step2.value;
47 |
48 | if (isEnumerable.call(arg, key)) {
49 | target[key] = arg[key];
50 | }
51 | }
52 | } catch (err) {
53 | _didIteratorError2 = true;
54 | _iteratorError2 = err;
55 | } finally {
56 | try {
57 | if (!_iteratorNormalCompletion2 && _iterator2.return) {
58 | _iterator2.return();
59 | }
60 | } finally {
61 | if (_didIteratorError2) {
62 | throw _iteratorError2;
63 | }
64 | }
65 | }
66 | }
67 | } catch (err) {
68 | _didIteratorError = true;
69 | _iteratorError = err;
70 | } finally {
71 | try {
72 | if (!_iteratorNormalCompletion && _iterator.return) {
73 | _iterator.return();
74 | }
75 | } finally {
76 | if (_didIteratorError) {
77 | throw _iteratorError;
78 | }
79 | }
80 | }
81 |
82 | return target;
83 | };
84 |
85 | function isObject(val) {
86 | return typeof val === 'function' || toString.call(val) === '[object Object]' || Array.isArray(val);
87 | }
88 | });
89 |
90 | var assignSymbols$2 = /*#__PURE__*/Object.freeze({
91 | __proto__: null,
92 | 'default': assignSymbols$1,
93 | __moduleExports: assignSymbols$1
94 | });
95 |
96 | var assignSymbols = ( assignSymbols$2 && assignSymbols$1 ) || assignSymbols$2;
97 |
98 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
99 | return typeof obj;
100 | } : function (obj) {
101 | return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
102 | };
103 |
104 | var classCallCheck = function (instance, Constructor) {
105 | if (!(instance instanceof Constructor)) {
106 | throw new TypeError("Cannot call a class as a function");
107 | }
108 | };
109 |
110 | var createClass = function () {
111 | function defineProperties(target, props) {
112 | for (var i = 0; i < props.length; i++) {
113 | var descriptor = props[i];
114 | descriptor.enumerable = descriptor.enumerable || false;
115 | descriptor.configurable = true;
116 | if ("value" in descriptor) descriptor.writable = true;
117 | Object.defineProperty(target, descriptor.key, descriptor);
118 | }
119 | }
120 |
121 | return function (Constructor, protoProps, staticProps) {
122 | if (protoProps) defineProperties(Constructor.prototype, protoProps);
123 | if (staticProps) defineProperties(Constructor, staticProps);
124 | return Constructor;
125 | };
126 | }();
127 |
128 | var assignDeep = createCommonjsModule(function (module) {
129 |
130 | var toString = Object.prototype.toString;
131 |
132 | var isValidKey = function isValidKey(key) {
133 | return key !== '__proto__' && key !== 'constructor' && key !== 'prototype';
134 | };
135 |
136 | var assign = module.exports = function (target) {
137 | for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
138 | args[_key - 1] = arguments[_key];
139 | }
140 |
141 | var i = 0;
142 | if (isPrimitive(target)) target = args[i++];
143 | if (!target) target = {};
144 | for (; i < args.length; i++) {
145 | if (isObject(args[i])) {
146 | var _iteratorNormalCompletion = true;
147 | var _didIteratorError = false;
148 | var _iteratorError = undefined;
149 |
150 | try {
151 | for (var _iterator = Object.keys(args[i])[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
152 | var key = _step.value;
153 |
154 | if (isValidKey(key)) {
155 | if (isObject(target[key]) && isObject(args[i][key])) {
156 | assign(target[key], args[i][key]);
157 | } else {
158 | target[key] = args[i][key];
159 | }
160 | }
161 | }
162 | } catch (err) {
163 | _didIteratorError = true;
164 | _iteratorError = err;
165 | } finally {
166 | try {
167 | if (!_iteratorNormalCompletion && _iterator.return) {
168 | _iterator.return();
169 | }
170 | } finally {
171 | if (_didIteratorError) {
172 | throw _iteratorError;
173 | }
174 | }
175 | }
176 |
177 | assignSymbols(target, args[i]);
178 | }
179 | }
180 | return target;
181 | };
182 |
183 | function isObject(val) {
184 | return typeof val === 'function' || toString.call(val) === '[object Object]';
185 | }
186 |
187 | function isPrimitive(val) {
188 | return (typeof val === 'undefined' ? 'undefined' : _typeof(val)) === 'object' ? val === null : typeof val !== 'function';
189 | }
190 | });
191 |
192 | var inBrowser = typeof window !== 'undefined' && window !== null;
193 |
194 | var hasIntersectionObserver = checkIntersectionObserver();
195 |
196 | function checkIntersectionObserver() {
197 | if (inBrowser && 'IntersectionObserver' in window && 'IntersectionObserverEntry' in window && 'intersectionRatio' in window.IntersectionObserverEntry.prototype) {
198 | // Minimal polyfill for Edge 15's lack of `isIntersecting`
199 | // See: https://github.com/w3c/IntersectionObserver/issues/211
200 | if (!('isIntersecting' in window.IntersectionObserverEntry.prototype)) {
201 | Object.defineProperty(window.IntersectionObserverEntry.prototype, 'isIntersecting', {
202 | get: function get() {
203 | return this.intersectionRatio > 0;
204 | }
205 | });
206 | }
207 | return true;
208 | }
209 | return false;
210 | }
211 |
212 | var modeType = {
213 | event: 'event',
214 | observer: 'observer'
215 |
216 | // CustomEvent polyfill for IE
217 | };var CustomEvent = function () {
218 | if (!inBrowser) return;
219 | // not IE
220 | if (typeof window.CustomEvent === 'function') return window.CustomEvent;
221 | function CustomEvent(event, params) {
222 | params = params || { bubbles: false, cancelable: false, detail: undefined };
223 | var evt = document.createEvent('CustomEvent');
224 | evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
225 | return evt;
226 | }
227 | CustomEvent.prototype = window.Event.prototype;
228 | return CustomEvent;
229 | }();
230 |
231 | function remove(arr, item) {
232 | if (!arr.length) return;
233 | var index = arr.indexOf(item);
234 | if (index > -1) return arr.splice(index, 1);
235 | }
236 |
237 | function some(arr, fn) {
238 | var has = false;
239 | for (var i = 0, len = arr.length; i < len; i++) {
240 | if (fn(arr[i])) {
241 | has = true;
242 | break;
243 | }
244 | }
245 | return has;
246 | }
247 |
248 | function getBestSelectionFromSrcset(el, scale) {
249 | if (el.tagName !== 'IMG' || !el.getAttribute('data-srcset')) return;
250 |
251 | var options = el.getAttribute('data-srcset');
252 | var result = [];
253 | var container = el.parentNode;
254 | var containerWidth = container.offsetWidth * scale;
255 |
256 | var spaceIndex = void 0;
257 | var tmpSrc = void 0;
258 | var tmpWidth = void 0;
259 |
260 | options = options.trim().split(',');
261 |
262 | options.map(function (item) {
263 | item = item.trim();
264 | spaceIndex = item.lastIndexOf(' ');
265 | if (spaceIndex === -1) {
266 | tmpSrc = item;
267 | tmpWidth = 999998;
268 | } else {
269 | tmpSrc = item.substr(0, spaceIndex);
270 | tmpWidth = parseInt(item.substr(spaceIndex + 1, item.length - spaceIndex - 2), 10);
271 | }
272 | result.push([tmpWidth, tmpSrc]);
273 | });
274 |
275 | result.sort(function (a, b) {
276 | if (a[0] < b[0]) {
277 | return 1;
278 | }
279 | if (a[0] > b[0]) {
280 | return -1;
281 | }
282 | if (a[0] === b[0]) {
283 | if (b[1].indexOf('.webp', b[1].length - 5) !== -1) {
284 | return 1;
285 | }
286 | if (a[1].indexOf('.webp', a[1].length - 5) !== -1) {
287 | return -1;
288 | }
289 | }
290 | return 0;
291 | });
292 | var bestSelectedSrc = '';
293 | var tmpOption = void 0;
294 |
295 | for (var i = 0; i < result.length; i++) {
296 | tmpOption = result[i];
297 | bestSelectedSrc = tmpOption[1];
298 | var next = result[i + 1];
299 | if (next && next[0] < containerWidth) {
300 | bestSelectedSrc = tmpOption[1];
301 | break;
302 | } else if (!next) {
303 | bestSelectedSrc = tmpOption[1];
304 | break;
305 | }
306 | }
307 |
308 | return bestSelectedSrc;
309 | }
310 |
311 | function find(arr, fn) {
312 | var item = void 0;
313 | for (var i = 0, len = arr.length; i < len; i++) {
314 | if (fn(arr[i])) {
315 | item = arr[i];
316 | break;
317 | }
318 | }
319 | return item;
320 | }
321 |
322 | var getDPR = function getDPR() {
323 | var scale = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
324 | return inBrowser ? window.devicePixelRatio || scale : scale;
325 | };
326 |
327 | function supportWebp() {
328 | if (!inBrowser) return false;
329 |
330 | var support = true;
331 |
332 | try {
333 | var elem = document.createElement('canvas');
334 |
335 | if (elem.getContext && elem.getContext('2d')) {
336 | support = elem.toDataURL('image/webp').indexOf('data:image/webp') === 0;
337 | }
338 | } catch (err) {
339 | support = false;
340 | }
341 |
342 | return support;
343 | }
344 |
345 | function throttle(action, delay) {
346 | var timeout = null;
347 | var movement = null;
348 | var lastRun = 0;
349 | var needRun = false;
350 | return function () {
351 | needRun = true;
352 | if (timeout) {
353 | return;
354 | }
355 | var elapsed = Date.now() - lastRun;
356 | var context = this;
357 | var args = arguments;
358 | var runCallback = function runCallback() {
359 | lastRun = Date.now();
360 | timeout = false;
361 | action.apply(context, args);
362 | };
363 | if (elapsed >= delay) {
364 | runCallback();
365 | } else {
366 | timeout = setTimeout(runCallback, delay);
367 | }
368 | if (needRun) {
369 | clearTimeout(movement);
370 | movement = setTimeout(runCallback, 2 * delay);
371 | }
372 | };
373 | }
374 |
375 | function testSupportsPassive() {
376 | if (!inBrowser) return;
377 | var support = false;
378 | try {
379 | var opts = Object.defineProperty({}, 'passive', {
380 | get: function get() {
381 | support = true;
382 | }
383 | });
384 | window.addEventListener('test', null, opts);
385 | } catch (e) {}
386 | return support;
387 | }
388 |
389 | var supportsPassive = testSupportsPassive();
390 |
391 | var _ = {
392 | on: function on(el, type, func) {
393 | var capture = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
394 |
395 | if (supportsPassive) {
396 | el.addEventListener(type, func, {
397 | capture: capture,
398 | passive: true
399 | });
400 | } else {
401 | el.addEventListener(type, func, capture);
402 | }
403 | },
404 | off: function off(el, type, func) {
405 | var capture = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
406 |
407 | el.removeEventListener(type, func, capture);
408 | }
409 | };
410 |
411 | var loadImageAsync = function loadImageAsync(item, resolve, reject) {
412 | var image = new Image();
413 | if (!item || !item.src) {
414 | var err = new Error('image src is required');
415 | return reject(err);
416 | }
417 |
418 | image.src = item.src;
419 | if (item.cors) {
420 | image.crossOrigin = item.cors;
421 | }
422 |
423 | image.onload = function () {
424 | resolve({
425 | naturalHeight: image.naturalHeight,
426 | naturalWidth: image.naturalWidth,
427 | src: image.src
428 | });
429 | };
430 |
431 | image.onerror = function (e) {
432 | reject(e);
433 | };
434 | };
435 |
436 | var style = function style(el, prop) {
437 | return typeof getComputedStyle !== 'undefined' ? getComputedStyle(el, null).getPropertyValue(prop) : el.style[prop];
438 | };
439 |
440 | var overflow = function overflow(el) {
441 | return style(el, 'overflow') + style(el, 'overflow-y') + style(el, 'overflow-x');
442 | };
443 |
444 | var scrollParent = function scrollParent(el) {
445 | if (!inBrowser) return;
446 | if (!(el instanceof HTMLElement)) {
447 | return window;
448 | }
449 |
450 | var parent = el;
451 |
452 | while (parent) {
453 | if (parent === document.body || parent === document.documentElement) {
454 | break;
455 | }
456 |
457 | if (!parent.parentNode) {
458 | break;
459 | }
460 |
461 | if (/(scroll|auto)/.test(overflow(parent))) {
462 | return parent;
463 | }
464 |
465 | parent = parent.parentNode;
466 | }
467 |
468 | return window;
469 | };
470 |
471 | function isObject(obj) {
472 | return obj !== null && (typeof obj === 'undefined' ? 'undefined' : _typeof(obj)) === 'object';
473 | }
474 |
475 | function ObjectKeys(obj) {
476 | if (!(obj instanceof Object)) return [];
477 | if (Object.keys) {
478 | return Object.keys(obj);
479 | } else {
480 | var keys = [];
481 | for (var key in obj) {
482 | if (obj.hasOwnProperty(key)) {
483 | keys.push(key);
484 | }
485 | }
486 | return keys;
487 | }
488 | }
489 |
490 | function ArrayFrom(arrLike) {
491 | var len = arrLike.length;
492 | var list = [];
493 | for (var i = 0; i < len; i++) {
494 | list.push(arrLike[i]);
495 | }
496 | return list;
497 | }
498 |
499 | function noop() {}
500 |
501 | var ImageCache = function () {
502 | function ImageCache(_ref) {
503 | var max = _ref.max;
504 | classCallCheck(this, ImageCache);
505 |
506 | this.options = {
507 | max: max || 100
508 | };
509 | this._caches = [];
510 | }
511 |
512 | createClass(ImageCache, [{
513 | key: 'has',
514 | value: function has(key) {
515 | return this._caches.indexOf(key) > -1;
516 | }
517 | }, {
518 | key: 'add',
519 | value: function add(key) {
520 | if (this.has(key)) return;
521 | this._caches.push(key);
522 | if (this._caches.length > this.options.max) {
523 | this.free();
524 | }
525 | }
526 | }, {
527 | key: 'free',
528 | value: function free() {
529 | this._caches.shift();
530 | }
531 | }]);
532 | return ImageCache;
533 | }();
534 |
535 | // el: {
536 | // state,
537 | // src,
538 | // error,
539 | // loading
540 | // }
541 |
542 | var ReactiveListener = function () {
543 | function ReactiveListener(_ref) {
544 | var el = _ref.el,
545 | src = _ref.src,
546 | error = _ref.error,
547 | loading = _ref.loading,
548 | bindType = _ref.bindType,
549 | $parent = _ref.$parent,
550 | options = _ref.options,
551 | cors = _ref.cors,
552 | elRenderer = _ref.elRenderer,
553 | imageCache = _ref.imageCache;
554 | classCallCheck(this, ReactiveListener);
555 |
556 | this.el = el;
557 | this.src = src;
558 | this.error = error;
559 | this.loading = loading;
560 | this.bindType = bindType;
561 | this.attempt = 0;
562 | this.cors = cors;
563 |
564 | this.naturalHeight = 0;
565 | this.naturalWidth = 0;
566 |
567 | this.options = options;
568 |
569 | this.rect = null;
570 |
571 | this.$parent = $parent;
572 | this.elRenderer = elRenderer;
573 | this._imageCache = imageCache;
574 | this.performanceData = {
575 | init: Date.now(),
576 | loadStart: 0,
577 | loadEnd: 0
578 | };
579 |
580 | this.filter();
581 | this.initState();
582 | this.render('loading', false);
583 | }
584 |
585 | /*
586 | * init listener state
587 | * @return
588 | */
589 |
590 |
591 | createClass(ReactiveListener, [{
592 | key: 'initState',
593 | value: function initState() {
594 | if ('dataset' in this.el) {
595 | this.el.dataset.src = this.src;
596 | } else {
597 | this.el.setAttribute('data-src', this.src);
598 | }
599 |
600 | this.state = {
601 | loading: false,
602 | error: false,
603 | loaded: false,
604 | rendered: false
605 | };
606 | }
607 |
608 | /*
609 | * record performance
610 | * @return
611 | */
612 |
613 | }, {
614 | key: 'record',
615 | value: function record(event) {
616 | this.performanceData[event] = Date.now();
617 | }
618 |
619 | /*
620 | * update image listener data
621 | * @param {String} image uri
622 | * @param {String} loading image uri
623 | * @param {String} error image uri
624 | * @return
625 | */
626 |
627 | }, {
628 | key: 'update',
629 | value: function update(_ref2) {
630 | var src = _ref2.src,
631 | loading = _ref2.loading,
632 | error = _ref2.error;
633 |
634 | var oldSrc = this.src;
635 | this.src = src;
636 | this.loading = loading;
637 | this.error = error;
638 | this.filter();
639 | if (oldSrc !== this.src) {
640 | this.attempt = 0;
641 | this.initState();
642 | }
643 | }
644 |
645 | /*
646 | * get el node rect
647 | * @return
648 | */
649 |
650 | }, {
651 | key: 'getRect',
652 | value: function getRect() {
653 | this.rect = this.el.getBoundingClientRect();
654 | }
655 |
656 | /*
657 | * check el is in view
658 | * @return {Boolean} el is in view
659 | */
660 |
661 | }, {
662 | key: 'checkInView',
663 | value: function checkInView() {
664 | this.getRect();
665 | return this.rect.top < window.innerHeight * this.options.preLoad && this.rect.bottom > this.options.preLoadTop && this.rect.left < window.innerWidth * this.options.preLoad && this.rect.right > 0;
666 | }
667 |
668 | /*
669 | * listener filter
670 | */
671 |
672 | }, {
673 | key: 'filter',
674 | value: function filter() {
675 | var _this = this;
676 |
677 | ObjectKeys(this.options.filter).map(function (key) {
678 | _this.options.filter[key](_this, _this.options);
679 | });
680 | }
681 |
682 | /*
683 | * render loading first
684 | * @params cb:Function
685 | * @return
686 | */
687 |
688 | }, {
689 | key: 'renderLoading',
690 | value: function renderLoading(cb) {
691 | var _this2 = this;
692 |
693 | this.state.loading = true;
694 | loadImageAsync({
695 | src: this.loading,
696 | cors: this.cors
697 | }, function (data) {
698 | _this2.render('loading', false);
699 | _this2.state.loading = false;
700 | cb();
701 | }, function () {
702 | // handler `loading image` load failed
703 | cb();
704 | _this2.state.loading = false;
705 | if (!_this2.options.silent) console.warn('VueLazyload log: load failed with loading image(' + _this2.loading + ')');
706 | });
707 | }
708 |
709 | /*
710 | * try load image and render it
711 | * @return
712 | */
713 |
714 | }, {
715 | key: 'load',
716 | value: function load() {
717 | var _this3 = this;
718 |
719 | var onFinish = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : noop;
720 |
721 | if (this.attempt > this.options.attempt - 1 && this.state.error) {
722 | if (!this.options.silent) console.log('VueLazyload log: ' + this.src + ' tried too more than ' + this.options.attempt + ' times');
723 | onFinish();
724 | return;
725 | }
726 | if (this.state.rendered && this.state.loaded) return;
727 | if (this._imageCache.has(this.src)) {
728 | this.state.loaded = true;
729 | this.render('loaded', true);
730 | this.state.rendered = true;
731 | return onFinish();
732 | }
733 |
734 | this.renderLoading(function () {
735 | _this3.attempt++;
736 |
737 | _this3.options.adapter['beforeLoad'] && _this3.options.adapter['beforeLoad'](_this3, _this3.options);
738 | _this3.record('loadStart');
739 |
740 | loadImageAsync({
741 | src: _this3.src,
742 | cors: _this3.cors
743 | }, function (data) {
744 | _this3.naturalHeight = data.naturalHeight;
745 | _this3.naturalWidth = data.naturalWidth;
746 | _this3.state.loaded = true;
747 | _this3.state.error = false;
748 | _this3.record('loadEnd');
749 | _this3.render('loaded', false);
750 | _this3.state.rendered = true;
751 | _this3._imageCache.add(_this3.src);
752 | onFinish();
753 | }, function (err) {
754 | !_this3.options.silent && console.error(err);
755 | _this3.state.error = true;
756 | _this3.state.loaded = false;
757 | _this3.render('error', false);
758 | });
759 | });
760 | }
761 |
762 | /*
763 | * render image
764 | * @param {String} state to render // ['loading', 'src', 'error']
765 | * @param {String} is form cache
766 | * @return
767 | */
768 |
769 | }, {
770 | key: 'render',
771 | value: function render(state, cache) {
772 | this.elRenderer(this, state, cache);
773 | }
774 |
775 | /*
776 | * output performance data
777 | * @return {Object} performance data
778 | */
779 |
780 | }, {
781 | key: 'performance',
782 | value: function performance() {
783 | var state = 'loading';
784 | var time = 0;
785 |
786 | if (this.state.loaded) {
787 | state = 'loaded';
788 | time = (this.performanceData.loadEnd - this.performanceData.loadStart) / 1000;
789 | }
790 |
791 | if (this.state.error) state = 'error';
792 |
793 | return {
794 | src: this.src,
795 | state: state,
796 | time: time
797 | };
798 | }
799 |
800 | /*
801 | * $destroy
802 | * @return
803 | */
804 |
805 | }, {
806 | key: '$destroy',
807 | value: function $destroy() {
808 | this.el = null;
809 | this.src = null;
810 | this.error = null;
811 | this.loading = null;
812 | this.bindType = null;
813 | this.attempt = 0;
814 | }
815 | }]);
816 | return ReactiveListener;
817 | }();
818 |
819 | var DEFAULT_URL = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
820 | var DEFAULT_EVENTS = ['scroll', 'wheel', 'mousewheel', 'resize', 'animationend', 'transitionend', 'touchmove'];
821 | var DEFAULT_OBSERVER_OPTIONS = {
822 | rootMargin: '0px',
823 | threshold: 0
824 | };
825 |
826 | function Lazy(Vue) {
827 | return function () {
828 | function Lazy(_ref) {
829 | var preLoad = _ref.preLoad,
830 | error = _ref.error,
831 | throttleWait = _ref.throttleWait,
832 | preLoadTop = _ref.preLoadTop,
833 | dispatchEvent = _ref.dispatchEvent,
834 | loading = _ref.loading,
835 | attempt = _ref.attempt,
836 | _ref$silent = _ref.silent,
837 | silent = _ref$silent === undefined ? true : _ref$silent,
838 | scale = _ref.scale,
839 | listenEvents = _ref.listenEvents;
840 | _ref.hasbind;
841 | var filter = _ref.filter,
842 | adapter = _ref.adapter,
843 | observer = _ref.observer,
844 | observerOptions = _ref.observerOptions;
845 | classCallCheck(this, Lazy);
846 |
847 | this.version = '"1.3.5"';
848 | this.mode = modeType.event;
849 | this.ListenerQueue = [];
850 | this.TargetIndex = 0;
851 | this.TargetQueue = [];
852 | this.options = {
853 | silent: silent,
854 | dispatchEvent: !!dispatchEvent,
855 | throttleWait: throttleWait || 200,
856 | preLoad: preLoad || 1.3,
857 | preLoadTop: preLoadTop || 0,
858 | error: error || DEFAULT_URL,
859 | loading: loading || DEFAULT_URL,
860 | attempt: attempt || 3,
861 | scale: scale || getDPR(scale),
862 | ListenEvents: listenEvents || DEFAULT_EVENTS,
863 | hasbind: false,
864 | supportWebp: supportWebp(),
865 | filter: filter || {},
866 | adapter: adapter || {},
867 | observer: !!observer,
868 | observerOptions: observerOptions || DEFAULT_OBSERVER_OPTIONS
869 | };
870 | this._initEvent();
871 | this._imageCache = new ImageCache({ max: 200 });
872 | this.lazyLoadHandler = throttle(this._lazyLoadHandler.bind(this), this.options.throttleWait);
873 |
874 | this.setMode(this.options.observer ? modeType.observer : modeType.event);
875 | }
876 |
877 | /**
878 | * update config
879 | * @param {Object} config params
880 | * @return
881 | */
882 |
883 |
884 | createClass(Lazy, [{
885 | key: 'config',
886 | value: function config() {
887 | var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
888 |
889 | assignDeep(this.options, options);
890 | }
891 |
892 | /**
893 | * output listener's load performance
894 | * @return {Array}
895 | */
896 |
897 | }, {
898 | key: 'performance',
899 | value: function performance() {
900 | var list = [];
901 |
902 | this.ListenerQueue.map(function (item) {
903 | list.push(item.performance());
904 | });
905 |
906 | return list;
907 | }
908 |
909 | /*
910 | * add lazy component to queue
911 | * @param {Vue} vm lazy component instance
912 | * @return
913 | */
914 |
915 | }, {
916 | key: 'addLazyBox',
917 | value: function addLazyBox(vm) {
918 | this.ListenerQueue.push(vm);
919 | if (inBrowser) {
920 | this._addListenerTarget(window);
921 | this._observer && this._observer.observe(vm.el);
922 | if (vm.$el && vm.$el.parentNode) {
923 | this._addListenerTarget(vm.$el.parentNode);
924 | }
925 | }
926 | }
927 |
928 | /*
929 | * add image listener to queue
930 | * @param {DOM} el
931 | * @param {object} binding vue directive binding
932 | * @param {vnode} vnode vue directive vnode
933 | * @return
934 | */
935 |
936 | }, {
937 | key: 'add',
938 | value: function add(el, binding, vnode) {
939 | var _this = this;
940 |
941 | if (some(this.ListenerQueue, function (item) {
942 | return item.el === el;
943 | })) {
944 | this.update(el, binding);
945 | return Vue.nextTick(this.lazyLoadHandler);
946 | }
947 |
948 | var _valueFormatter2 = this._valueFormatter(binding.value),
949 | src = _valueFormatter2.src,
950 | loading = _valueFormatter2.loading,
951 | error = _valueFormatter2.error,
952 | cors = _valueFormatter2.cors;
953 |
954 | Vue.nextTick(function () {
955 | src = getBestSelectionFromSrcset(el, _this.options.scale) || src;
956 | _this._observer && _this._observer.observe(el);
957 |
958 | var container = Object.keys(binding.modifiers)[0];
959 | var $parent = void 0;
960 |
961 | if (container) {
962 | $parent = vnode.context.$refs[container];
963 | // if there is container passed in, try ref first, then fallback to getElementById to support the original usage
964 | $parent = $parent ? $parent.$el || $parent : document.getElementById(container);
965 | }
966 |
967 | if (!$parent) {
968 | $parent = scrollParent(el);
969 | }
970 |
971 | var newListener = new ReactiveListener({
972 | bindType: binding.arg,
973 | $parent: $parent,
974 | el: el,
975 | loading: loading,
976 | error: error,
977 | src: src,
978 | cors: cors,
979 | elRenderer: _this._elRenderer.bind(_this),
980 | options: _this.options,
981 | imageCache: _this._imageCache
982 | });
983 |
984 | _this.ListenerQueue.push(newListener);
985 |
986 | if (inBrowser) {
987 | _this._addListenerTarget(window);
988 | _this._addListenerTarget($parent);
989 | }
990 |
991 | _this.lazyLoadHandler();
992 | Vue.nextTick(function () {
993 | return _this.lazyLoadHandler();
994 | });
995 | });
996 | }
997 |
998 | /**
999 | * update image src
1000 | * @param {DOM} el
1001 | * @param {object} vue directive binding
1002 | * @return
1003 | */
1004 |
1005 | }, {
1006 | key: 'update',
1007 | value: function update(el, binding, vnode) {
1008 | var _this2 = this;
1009 |
1010 | var _valueFormatter3 = this._valueFormatter(binding.value),
1011 | src = _valueFormatter3.src,
1012 | loading = _valueFormatter3.loading,
1013 | error = _valueFormatter3.error;
1014 |
1015 | src = getBestSelectionFromSrcset(el, this.options.scale) || src;
1016 |
1017 | var exist = find(this.ListenerQueue, function (item) {
1018 | return item.el === el;
1019 | });
1020 | if (!exist) {
1021 | this.add(el, binding, vnode);
1022 | } else {
1023 | exist.update({
1024 | src: src,
1025 | loading: loading,
1026 | error: error
1027 | });
1028 | }
1029 | if (this._observer) {
1030 | this._observer.unobserve(el);
1031 | this._observer.observe(el);
1032 | }
1033 | this.lazyLoadHandler();
1034 | Vue.nextTick(function () {
1035 | return _this2.lazyLoadHandler();
1036 | });
1037 | }
1038 |
1039 | /**
1040 | * remove listener form list
1041 | * @param {DOM} el
1042 | * @return
1043 | */
1044 |
1045 | }, {
1046 | key: 'remove',
1047 | value: function remove$1(el) {
1048 | if (!el) return;
1049 | this._observer && this._observer.unobserve(el);
1050 | var existItem = find(this.ListenerQueue, function (item) {
1051 | return item.el === el;
1052 | });
1053 | if (existItem) {
1054 | this._removeListenerTarget(existItem.$parent);
1055 | this._removeListenerTarget(window);
1056 | remove(this.ListenerQueue, existItem);
1057 | existItem.$destroy();
1058 | }
1059 | }
1060 |
1061 | /*
1062 | * remove lazy components form list
1063 | * @param {Vue} vm Vue instance
1064 | * @return
1065 | */
1066 |
1067 | }, {
1068 | key: 'removeComponent',
1069 | value: function removeComponent(vm) {
1070 | if (!vm) return;
1071 | remove(this.ListenerQueue, vm);
1072 | this._observer && this._observer.unobserve(vm.el);
1073 | if (vm.$parent && vm.$el.parentNode) {
1074 | this._removeListenerTarget(vm.$el.parentNode);
1075 | }
1076 | this._removeListenerTarget(window);
1077 | }
1078 | }, {
1079 | key: 'setMode',
1080 | value: function setMode(mode) {
1081 | var _this3 = this;
1082 |
1083 | if (!hasIntersectionObserver && mode === modeType.observer) {
1084 | mode = modeType.event;
1085 | }
1086 |
1087 | this.mode = mode; // event or observer
1088 |
1089 | if (mode === modeType.event) {
1090 | if (this._observer) {
1091 | this.ListenerQueue.forEach(function (listener) {
1092 | _this3._observer.unobserve(listener.el);
1093 | });
1094 | this._observer = null;
1095 | }
1096 |
1097 | this.TargetQueue.forEach(function (target) {
1098 | _this3._initListen(target.el, true);
1099 | });
1100 | } else {
1101 | this.TargetQueue.forEach(function (target) {
1102 | _this3._initListen(target.el, false);
1103 | });
1104 | this._initIntersectionObserver();
1105 | }
1106 | }
1107 |
1108 | /*
1109 | *** Private functions ***
1110 | */
1111 |
1112 | /*
1113 | * add listener target
1114 | * @param {DOM} el listener target
1115 | * @return
1116 | */
1117 |
1118 | }, {
1119 | key: '_addListenerTarget',
1120 | value: function _addListenerTarget(el) {
1121 | if (!el) return;
1122 | var target = find(this.TargetQueue, function (target) {
1123 | return target.el === el;
1124 | });
1125 | if (!target) {
1126 | target = {
1127 | el: el,
1128 | id: ++this.TargetIndex,
1129 | childrenCount: 1,
1130 | listened: true
1131 | };
1132 | this.mode === modeType.event && this._initListen(target.el, true);
1133 | this.TargetQueue.push(target);
1134 | } else {
1135 | target.childrenCount++;
1136 | }
1137 | return this.TargetIndex;
1138 | }
1139 |
1140 | /*
1141 | * remove listener target or reduce target childrenCount
1142 | * @param {DOM} el or window
1143 | * @return
1144 | */
1145 |
1146 | }, {
1147 | key: '_removeListenerTarget',
1148 | value: function _removeListenerTarget(el) {
1149 | var _this4 = this;
1150 |
1151 | this.TargetQueue.forEach(function (target, index) {
1152 | if (target.el === el) {
1153 | target.childrenCount--;
1154 | if (!target.childrenCount) {
1155 | _this4._initListen(target.el, false);
1156 | _this4.TargetQueue.splice(index, 1);
1157 | target = null;
1158 | }
1159 | }
1160 | });
1161 | }
1162 |
1163 | /*
1164 | * add or remove eventlistener
1165 | * @param {DOM} el DOM or Window
1166 | * @param {boolean} start flag
1167 | * @return
1168 | */
1169 |
1170 | }, {
1171 | key: '_initListen',
1172 | value: function _initListen(el, start) {
1173 | var _this5 = this;
1174 |
1175 | this.options.ListenEvents.forEach(function (evt) {
1176 | return _[start ? 'on' : 'off'](el, evt, _this5.lazyLoadHandler);
1177 | });
1178 | }
1179 | }, {
1180 | key: '_initEvent',
1181 | value: function _initEvent() {
1182 | var _this6 = this;
1183 |
1184 | this.Event = {
1185 | listeners: {
1186 | loading: [],
1187 | loaded: [],
1188 | error: []
1189 | }
1190 | };
1191 |
1192 | this.$on = function (event, func) {
1193 | if (!_this6.Event.listeners[event]) _this6.Event.listeners[event] = [];
1194 | _this6.Event.listeners[event].push(func);
1195 | };
1196 |
1197 | this.$once = function (event, func) {
1198 | var vm = _this6;
1199 | function on() {
1200 | vm.$off(event, on);
1201 | func.apply(vm, arguments);
1202 | }
1203 | _this6.$on(event, on);
1204 | };
1205 |
1206 | this.$off = function (event, func) {
1207 | if (!func) {
1208 | if (!_this6.Event.listeners[event]) return;
1209 | _this6.Event.listeners[event].length = 0;
1210 | return;
1211 | }
1212 | remove(_this6.Event.listeners[event], func);
1213 | };
1214 |
1215 | this.$emit = function (event, context, inCache) {
1216 | if (!_this6.Event.listeners[event]) return;
1217 | _this6.Event.listeners[event].forEach(function (func) {
1218 | return func(context, inCache);
1219 | });
1220 | };
1221 | }
1222 |
1223 | /**
1224 | * find nodes which in viewport and trigger load
1225 | * @return
1226 | */
1227 |
1228 | }, {
1229 | key: '_lazyLoadHandler',
1230 | value: function _lazyLoadHandler() {
1231 | var _this7 = this;
1232 |
1233 | var freeList = [];
1234 | this.ListenerQueue.forEach(function (listener, index) {
1235 | if (!listener.el || !listener.el.parentNode) {
1236 | freeList.push(listener);
1237 | }
1238 | var catIn = listener.checkInView();
1239 | if (!catIn) return;
1240 | listener.load();
1241 | });
1242 | freeList.forEach(function (item) {
1243 | remove(_this7.ListenerQueue, item);
1244 | item.$destroy();
1245 | });
1246 | }
1247 | /**
1248 | * init IntersectionObserver
1249 | * set mode to observer
1250 | * @return
1251 | */
1252 |
1253 | }, {
1254 | key: '_initIntersectionObserver',
1255 | value: function _initIntersectionObserver() {
1256 | var _this8 = this;
1257 |
1258 | if (!hasIntersectionObserver) return;
1259 | this._observer = new IntersectionObserver(this._observerHandler.bind(this), this.options.observerOptions);
1260 | if (this.ListenerQueue.length) {
1261 | this.ListenerQueue.forEach(function (listener) {
1262 | _this8._observer.observe(listener.el);
1263 | });
1264 | }
1265 | }
1266 |
1267 | /**
1268 | * init IntersectionObserver
1269 | * @return
1270 | */
1271 |
1272 | }, {
1273 | key: '_observerHandler',
1274 | value: function _observerHandler(entries, observer) {
1275 | var _this9 = this;
1276 |
1277 | entries.forEach(function (entry) {
1278 | if (entry.isIntersecting) {
1279 | _this9.ListenerQueue.forEach(function (listener) {
1280 | if (listener.el === entry.target) {
1281 | if (listener.state.loaded) return _this9._observer.unobserve(listener.el);
1282 | listener.load();
1283 | }
1284 | });
1285 | }
1286 | });
1287 | }
1288 |
1289 | /**
1290 | * set element attribute with image'url and state
1291 | * @param {object} lazyload listener object
1292 | * @param {string} state will be rendered
1293 | * @param {bool} inCache is rendered from cache
1294 | * @return
1295 | */
1296 |
1297 | }, {
1298 | key: '_elRenderer',
1299 | value: function _elRenderer(listener, state, cache) {
1300 | if (!listener.el) return;
1301 | var el = listener.el,
1302 | bindType = listener.bindType;
1303 |
1304 |
1305 | var src = void 0;
1306 | switch (state) {
1307 | case 'loading':
1308 | src = listener.loading;
1309 | break;
1310 | case 'error':
1311 | src = listener.error;
1312 | break;
1313 | default:
1314 | src = listener.src;
1315 | break;
1316 | }
1317 |
1318 | if (bindType) {
1319 | el.style[bindType] = 'url("' + src + '")';
1320 | } else if (el.getAttribute('src') !== src) {
1321 | el.setAttribute('src', src);
1322 | }
1323 |
1324 | el.setAttribute('lazy', state);
1325 |
1326 | this.$emit(state, listener, cache);
1327 | this.options.adapter[state] && this.options.adapter[state](listener, this.options);
1328 |
1329 | if (this.options.dispatchEvent) {
1330 | var event = new CustomEvent(state, {
1331 | detail: listener
1332 | });
1333 | el.dispatchEvent(event);
1334 | }
1335 | }
1336 |
1337 | /**
1338 | * generate loading loaded error image url
1339 | * @param {string} image's src
1340 | * @return {object} image's loading, loaded, error url
1341 | */
1342 |
1343 | }, {
1344 | key: '_valueFormatter',
1345 | value: function _valueFormatter(value) {
1346 | var src = value;
1347 | var loading = this.options.loading;
1348 | var error = this.options.error;
1349 |
1350 | // value is object
1351 | if (isObject(value)) {
1352 | if (!value.src && !this.options.silent) console.error('Vue Lazyload warning: miss src with ' + value);
1353 | src = value.src;
1354 | loading = value.loading || this.options.loading;
1355 | error = value.error || this.options.error;
1356 | }
1357 | return {
1358 | src: src,
1359 | loading: loading,
1360 | error: error
1361 | };
1362 | }
1363 | }]);
1364 | return Lazy;
1365 | }();
1366 | }
1367 |
1368 | Lazy.install = function (Vue) {
1369 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1370 |
1371 | var LazyClass = Lazy(Vue);
1372 | var lazy = new LazyClass(options);
1373 |
1374 | var isVue2 = Vue.version.split('.')[0] === '2';
1375 | if (isVue2) {
1376 | Vue.directive('lazy', {
1377 | bind: lazy.add.bind(lazy),
1378 | update: lazy.update.bind(lazy),
1379 | componentUpdated: lazy.lazyLoadHandler.bind(lazy),
1380 | unbind: lazy.remove.bind(lazy)
1381 | });
1382 | } else {
1383 | Vue.directive('lazy', {
1384 | bind: lazy.lazyLoadHandler.bind(lazy),
1385 | update: function update(newValue, oldValue) {
1386 | assignDeep(this.vm.$refs, this.vm.$els);
1387 | lazy.add(this.el, {
1388 | modifiers: this.modifiers || {},
1389 | arg: this.arg,
1390 | value: newValue,
1391 | oldValue: oldValue
1392 | }, {
1393 | context: this.vm
1394 | });
1395 | },
1396 | unbind: function unbind() {
1397 | lazy.remove(this.el);
1398 | }
1399 | });
1400 | }
1401 | };
1402 |
1403 | var LazyComponent = function LazyComponent(lazy) {
1404 | return {
1405 | props: {
1406 | tag: {
1407 | type: String,
1408 | default: 'div'
1409 | }
1410 | },
1411 | render: function render(h) {
1412 | return h(this.tag, null, this.show ? this.$slots.default : null);
1413 | },
1414 | data: function data() {
1415 | return {
1416 | el: null,
1417 | state: {
1418 | loaded: false
1419 | },
1420 | rect: {},
1421 | show: false
1422 | };
1423 | },
1424 | mounted: function mounted() {
1425 | this.el = this.$el;
1426 | lazy.addLazyBox(this);
1427 | lazy.lazyLoadHandler();
1428 | },
1429 | beforeDestroy: function beforeDestroy() {
1430 | lazy.removeComponent(this);
1431 | },
1432 |
1433 | methods: {
1434 | getRect: function getRect() {
1435 | this.rect = this.$el.getBoundingClientRect();
1436 | },
1437 | checkInView: function checkInView() {
1438 | this.getRect();
1439 | return inBrowser && this.rect.top < window.innerHeight * lazy.options.preLoad && this.rect.bottom > 0 && this.rect.left < window.innerWidth * lazy.options.preLoad && this.rect.right > 0;
1440 | },
1441 | load: function load() {
1442 | this.show = true;
1443 | this.state.loaded = true;
1444 | this.$emit('show', this);
1445 | },
1446 | destroy: function destroy() {
1447 | return this.$destroy;
1448 | }
1449 | }
1450 | };
1451 | };
1452 |
1453 | LazyComponent.install = function (Vue) {
1454 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1455 |
1456 | var LazyClass = Lazy(Vue);
1457 | var lazy = new LazyClass(options);
1458 | Vue.component('lazy-component', LazyComponent(lazy));
1459 | };
1460 |
1461 | var LazyContainerMananger = function () {
1462 | function LazyContainerMananger(_ref) {
1463 | var lazy = _ref.lazy;
1464 | classCallCheck(this, LazyContainerMananger);
1465 |
1466 | this.lazy = lazy;
1467 | lazy.lazyContainerMananger = this;
1468 | this._queue = [];
1469 | }
1470 |
1471 | createClass(LazyContainerMananger, [{
1472 | key: 'bind',
1473 | value: function bind(el, binding, vnode) {
1474 | var container = new LazyContainer({ el: el, binding: binding, vnode: vnode, lazy: this.lazy });
1475 | this._queue.push(container);
1476 | }
1477 | }, {
1478 | key: 'update',
1479 | value: function update(el, binding, vnode) {
1480 | var container = find(this._queue, function (item) {
1481 | return item.el === el;
1482 | });
1483 | if (!container) return;
1484 | container.update({ el: el, binding: binding, vnode: vnode });
1485 | }
1486 | }, {
1487 | key: 'unbind',
1488 | value: function unbind(el, binding, vnode) {
1489 | var container = find(this._queue, function (item) {
1490 | return item.el === el;
1491 | });
1492 | if (!container) return;
1493 | container.clear();
1494 | remove(this._queue, container);
1495 | }
1496 | }]);
1497 | return LazyContainerMananger;
1498 | }();
1499 |
1500 |
1501 | var defaultOptions = {
1502 | selector: 'img'
1503 | };
1504 |
1505 | var LazyContainer = function () {
1506 | function LazyContainer(_ref2) {
1507 | var el = _ref2.el,
1508 | binding = _ref2.binding,
1509 | vnode = _ref2.vnode,
1510 | lazy = _ref2.lazy;
1511 | classCallCheck(this, LazyContainer);
1512 |
1513 | this.el = null;
1514 | this.vnode = vnode;
1515 | this.binding = binding;
1516 | this.options = {};
1517 | this.lazy = lazy;
1518 |
1519 | this._queue = [];
1520 | this.update({ el: el, binding: binding });
1521 | }
1522 |
1523 | createClass(LazyContainer, [{
1524 | key: 'update',
1525 | value: function update(_ref3) {
1526 | var _this = this;
1527 |
1528 | var el = _ref3.el,
1529 | binding = _ref3.binding;
1530 |
1531 | this.el = el;
1532 | this.options = assignDeep({}, defaultOptions, binding.value);
1533 |
1534 | var imgs = this.getImgs();
1535 | imgs.forEach(function (el) {
1536 | _this.lazy.add(el, assignDeep({}, _this.binding, {
1537 | value: {
1538 | src: 'dataset' in el ? el.dataset.src : el.getAttribute('data-src'),
1539 | error: ('dataset' in el ? el.dataset.error : el.getAttribute('data-error')) || _this.options.error,
1540 | loading: ('dataset' in el ? el.dataset.loading : el.getAttribute('data-loading')) || _this.options.loading
1541 | }
1542 | }), _this.vnode);
1543 | });
1544 | }
1545 | }, {
1546 | key: 'getImgs',
1547 | value: function getImgs() {
1548 | return ArrayFrom(this.el.querySelectorAll(this.options.selector));
1549 | }
1550 | }, {
1551 | key: 'clear',
1552 | value: function clear() {
1553 | var _this2 = this;
1554 |
1555 | var imgs = this.getImgs();
1556 | imgs.forEach(function (el) {
1557 | return _this2.lazy.remove(el);
1558 | });
1559 |
1560 | this.vnode = null;
1561 | this.binding = null;
1562 | this.lazy = null;
1563 | }
1564 | }]);
1565 | return LazyContainer;
1566 | }();
1567 |
1568 | LazyContainer.install = function (Vue) {
1569 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1570 |
1571 | var LazyClass = Lazy(Vue);
1572 | var lazy = new LazyClass(options);
1573 | var lazyContainer = new LazyContainer({ lazy: lazy });
1574 |
1575 | var isVue2 = Vue.version.split('.')[0] === '2';
1576 | if (isVue2) {
1577 | Vue.directive('lazy-container', {
1578 | bind: lazyContainer.bind.bind(lazyContainer),
1579 | componentUpdated: lazyContainer.update.bind(lazyContainer),
1580 | unbind: lazyContainer.unbind.bind(lazyContainer)
1581 | });
1582 | } else {
1583 | Vue.directive('lazy-container', {
1584 | update: function update(newValue, oldValue) {
1585 | lazyContainer.update(this.el, {
1586 | modifiers: this.modifiers || {},
1587 | arg: this.arg,
1588 | value: newValue,
1589 | oldValue: oldValue
1590 | }, {
1591 | context: this.vm
1592 | });
1593 | },
1594 | unbind: function unbind() {
1595 | lazyContainer.unbind(this.el);
1596 | }
1597 | });
1598 | }
1599 | };
1600 |
1601 | var LazyImage = function LazyImage(lazyManager) {
1602 | return {
1603 | props: {
1604 | src: [String, Object],
1605 | tag: {
1606 | type: String,
1607 | default: 'img'
1608 | }
1609 | },
1610 | render: function render(h) {
1611 | return h(this.tag, {
1612 | attrs: {
1613 | src: this.renderSrc
1614 | }
1615 | }, this.$slots.default);
1616 | },
1617 | data: function data() {
1618 | return {
1619 | el: null,
1620 | options: {
1621 | src: '',
1622 | error: '',
1623 | loading: '',
1624 | attempt: lazyManager.options.attempt
1625 | },
1626 | state: {
1627 | loaded: false,
1628 | error: false,
1629 | attempt: 0
1630 | },
1631 | rect: {},
1632 | renderSrc: ''
1633 | };
1634 | },
1635 |
1636 | watch: {
1637 | src: function src() {
1638 | this.init();
1639 | lazyManager.addLazyBox(this);
1640 | lazyManager.lazyLoadHandler();
1641 | }
1642 | },
1643 | created: function created() {
1644 | this.init();
1645 | this.renderSrc = this.options.loading;
1646 | },
1647 | mounted: function mounted() {
1648 | this.el = this.$el;
1649 | lazyManager.addLazyBox(this);
1650 | lazyManager.lazyLoadHandler();
1651 | },
1652 | beforeDestroy: function beforeDestroy() {
1653 | lazyManager.removeComponent(this);
1654 | },
1655 |
1656 | methods: {
1657 | init: function init() {
1658 | var _lazyManager$_valueFo = lazyManager._valueFormatter(this.src),
1659 | src = _lazyManager$_valueFo.src,
1660 | loading = _lazyManager$_valueFo.loading,
1661 | error = _lazyManager$_valueFo.error;
1662 |
1663 | this.state.loaded = false;
1664 | this.options.src = src;
1665 | this.options.error = error;
1666 | this.options.loading = loading;
1667 | this.renderSrc = this.options.loading;
1668 | },
1669 | getRect: function getRect() {
1670 | this.rect = this.$el.getBoundingClientRect();
1671 | },
1672 | checkInView: function checkInView() {
1673 | this.getRect();
1674 | return inBrowser && this.rect.top < window.innerHeight * lazyManager.options.preLoad && this.rect.bottom > 0 && this.rect.left < window.innerWidth * lazyManager.options.preLoad && this.rect.right > 0;
1675 | },
1676 | load: function load() {
1677 | var _this = this;
1678 |
1679 | var onFinish = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : noop;
1680 |
1681 | if (this.state.attempt > this.options.attempt - 1 && this.state.error) {
1682 | if (!lazyManager.options.silent) console.log('VueLazyload log: ' + this.options.src + ' tried too more than ' + this.options.attempt + ' times');
1683 | onFinish();
1684 | return;
1685 | }
1686 | var src = this.options.src;
1687 | loadImageAsync({ src: src }, function (_ref) {
1688 | var src = _ref.src;
1689 |
1690 | _this.renderSrc = src;
1691 | _this.state.loaded = true;
1692 | }, function (e) {
1693 | _this.state.attempt++;
1694 | _this.renderSrc = _this.options.error;
1695 | _this.state.error = true;
1696 | });
1697 | }
1698 | }
1699 | };
1700 | };
1701 |
1702 | LazyImage.install = function (Vue) {
1703 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1704 |
1705 | var LazyClass = Lazy(Vue);
1706 | var lazy = new LazyClass(options);
1707 | Vue.component('lazy-image', LazyImage(lazy));
1708 | };
1709 |
1710 | var index = {
1711 | /*
1712 | * install function
1713 | * @param {Vue} Vue
1714 | * @param {object} options lazyload options
1715 | */
1716 | install: function install(Vue) {
1717 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1718 |
1719 | var LazyClass = Lazy(Vue);
1720 | var lazy = new LazyClass(options);
1721 | var lazyContainer = new LazyContainerMananger({ lazy: lazy });
1722 |
1723 | var isVue2 = Vue.version.split('.')[0] === '2';
1724 |
1725 | Vue.prototype.$Lazyload = lazy;
1726 |
1727 | if (options.lazyComponent) {
1728 | Vue.component('lazy-component', LazyComponent(lazy));
1729 | }
1730 |
1731 | if (options.lazyImage) {
1732 | Vue.component('lazy-image', LazyImage(lazy));
1733 | }
1734 |
1735 | if (isVue2) {
1736 | Vue.directive('lazy', {
1737 | bind: lazy.add.bind(lazy),
1738 | update: lazy.update.bind(lazy),
1739 | componentUpdated: lazy.lazyLoadHandler.bind(lazy),
1740 | unbind: lazy.remove.bind(lazy)
1741 | });
1742 | Vue.directive('lazy-container', {
1743 | bind: lazyContainer.bind.bind(lazyContainer),
1744 | componentUpdated: lazyContainer.update.bind(lazyContainer),
1745 | unbind: lazyContainer.unbind.bind(lazyContainer)
1746 | });
1747 | } else {
1748 | Vue.directive('lazy', {
1749 | bind: lazy.lazyLoadHandler.bind(lazy),
1750 | update: function update(newValue, oldValue) {
1751 | assignDeep(this.vm.$refs, this.vm.$els);
1752 | lazy.add(this.el, {
1753 | modifiers: this.modifiers || {},
1754 | arg: this.arg,
1755 | value: newValue,
1756 | oldValue: oldValue
1757 | }, {
1758 | context: this.vm
1759 | });
1760 | },
1761 | unbind: function unbind() {
1762 | lazy.remove(this.el);
1763 | }
1764 | });
1765 |
1766 | Vue.directive('lazy-container', {
1767 | update: function update(newValue, oldValue) {
1768 | lazyContainer.update(this.el, {
1769 | modifiers: this.modifiers || {},
1770 | arg: this.arg,
1771 | value: newValue,
1772 | oldValue: oldValue
1773 | }, {
1774 | context: this.vm
1775 | });
1776 | },
1777 | unbind: function unbind() {
1778 | lazyContainer.unbind(this.el);
1779 | }
1780 | });
1781 | }
1782 | }
1783 | };
1784 |
1785 | export { Lazy, LazyComponent, LazyContainerMananger as LazyContainer, LazyImage, index as default };
1786 |
--------------------------------------------------------------------------------
/vue-lazyload.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Vue-Lazyload.js v1.3.5
3 | * (c) 2023 Awe
4 | * Released under the MIT License.
5 | */
6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).VueLazyload={})}(this,(function(t){"use strict";function e(t,e){return t(e={exports:{}},e.exports),e.exports}var n=e((function(t){var e=Object.prototype.toString,n=Object.prototype.propertyIsEnumerable,i=Object.getOwnPropertySymbols;t.exports=function(t){for(var r=arguments.length,o=Array(r>1?r-1:0),s=1;s1?e-1:0),u=1;u0}}),!0;return!1}();var c="event",h="observer",f=function(){if(l)return"function"==typeof window.CustomEvent?window.CustomEvent:(t.prototype=window.Event.prototype,t);function t(t,e){e=e||{bubbles:!1,cancelable:!1,detail:void 0};var n=document.createEvent("CustomEvent");return n.initCustomEvent(t,e.bubbles,e.cancelable,e.detail),n}}();function v(t,e){if(t.length){var n=t.indexOf(e);return n>-1?t.splice(n,1):void 0}}function p(t,e){if("IMG"===t.tagName&&t.getAttribute("data-srcset")){var n=t.getAttribute("data-srcset"),i=[],r=t.parentNode.offsetWidth*e,o=void 0,s=void 0,a=void 0;(n=n.trim().split(",")).map((function(t){t=t.trim(),-1===(o=t.lastIndexOf(" "))?(s=t,a=999998):(s=t.substr(0,o),a=parseInt(t.substr(o+1,t.length-o-2),10)),i.push([a,s])})),i.sort((function(t,e){if(t[0]e[0])return-1;if(t[0]===e[0]){if(-1!==e[1].indexOf(".webp",e[1].length-5))return 1;if(-1!==t[1].indexOf(".webp",t[1].length-5))return-1}return 0}));for(var u="",l=void 0,d=0;d0&&void 0!==arguments[0]?arguments[0]:1;return l&&window.devicePixelRatio||t};function b(){if(!l)return!1;var t=!0;try{var e=document.createElement("canvas");e.getContext&&e.getContext("2d")&&(t=0===e.toDataURL("image/webp").indexOf("data:image/webp"))}catch(e){t=!1}return t}var m=function(){if(l){var t=!1;try{var e=Object.defineProperty({},"passive",{get:function(){t=!0}});window.addEventListener("test",null,e)}catch(t){}return t}}(),w={on:function(t,e,n){var i=arguments.length>3&&void 0!==arguments[3]&&arguments[3];m?t.addEventListener(e,n,{capture:i,passive:!0}):t.addEventListener(e,n,i)},off:function(t,e,n){var i=arguments.length>3&&void 0!==arguments[3]&&arguments[3];t.removeEventListener(e,n,i)}},L=function(t,e,n){var i=new Image;if(!t||!t.src){var r=new Error("image src is required");return n(r)}i.src=t.src,t.cors&&(i.crossOrigin=t.cors),i.onload=function(){e({naturalHeight:i.naturalHeight,naturalWidth:i.naturalWidth,src:i.src})},i.onerror=function(t){n(t)}},_=function(t,e){return"undefined"!=typeof getComputedStyle?getComputedStyle(t,null).getPropertyValue(e):t.style[e]},z=function(t){return _(t,"overflow")+_(t,"overflow-y")+_(t,"overflow-x")};function E(){}var k=function(){function t(e){var n=e.max;s(this,t),this.options={max:n||100},this._caches=[]}return a(t,[{key:"has",value:function(t){return this._caches.indexOf(t)>-1}},{key:"add",value:function(t){this.has(t)||(this._caches.push(t),this._caches.length>this.options.max&&this.free())}},{key:"free",value:function(){this._caches.shift()}}]),t}(),x=function(){function t(e){var n=e.el,i=e.src,r=e.error,o=e.loading,a=e.bindType,u=e.$parent,l=e.options,d=e.cors,c=e.elRenderer,h=e.imageCache;s(this,t),this.el=n,this.src=i,this.error=r,this.loading=o,this.bindType=a,this.attempt=0,this.cors=d,this.naturalHeight=0,this.naturalWidth=0,this.options=l,this.rect=null,this.$parent=u,this.elRenderer=c,this._imageCache=h,this.performanceData={init:Date.now(),loadStart:0,loadEnd:0},this.filter(),this.initState(),this.render("loading",!1)}return a(t,[{key:"initState",value:function(){"dataset"in this.el?this.el.dataset.src=this.src:this.el.setAttribute("data-src",this.src),this.state={loading:!1,error:!1,loaded:!1,rendered:!1}}},{key:"record",value:function(t){this.performanceData[t]=Date.now()}},{key:"update",value:function(t){var e=t.src,n=t.loading,i=t.error,r=this.src;this.src=e,this.loading=n,this.error=i,this.filter(),r!==this.src&&(this.attempt=0,this.initState())}},{key:"getRect",value:function(){this.rect=this.el.getBoundingClientRect()}},{key:"checkInView",value:function(){return this.getRect(),this.rect.topthis.options.preLoadTop&&this.rect.left0}},{key:"filter",value:function(){var t=this;(function(t){if(!(t instanceof Object))return[];if(Object.keys)return Object.keys(t);var e=[];for(var n in t)t.hasOwnProperty(n)&&e.push(n);return e})(this.options.filter).map((function(e){t.options.filter[e](t,t.options)}))}},{key:"renderLoading",value:function(t){var e=this;this.state.loading=!0,L({src:this.loading,cors:this.cors},(function(n){e.render("loading",!1),e.state.loading=!1,t()}),(function(){t(),e.state.loading=!1,e.options.silent||console.warn("VueLazyload log: load failed with loading image("+e.loading+")")}))}},{key:"load",value:function(){var t=this,e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:E;return this.attempt>this.options.attempt-1&&this.state.error?(this.options.silent||console.log("VueLazyload log: "+this.src+" tried too more than "+this.options.attempt+" times"),void e()):this.state.rendered&&this.state.loaded?void 0:this._imageCache.has(this.src)?(this.state.loaded=!0,this.render("loaded",!0),this.state.rendered=!0,e()):void this.renderLoading((function(){t.attempt++,t.options.adapter.beforeLoad&&t.options.adapter.beforeLoad(t,t.options),t.record("loadStart"),L({src:t.src,cors:t.cors},(function(n){t.naturalHeight=n.naturalHeight,t.naturalWidth=n.naturalWidth,t.state.loaded=!0,t.state.error=!1,t.record("loadEnd"),t.render("loaded",!1),t.state.rendered=!0,t._imageCache.add(t.src),e()}),(function(e){!t.options.silent&&console.error(e),t.state.error=!0,t.state.loaded=!1,t.render("error",!1)}))}))}},{key:"render",value:function(t,e){this.elRenderer(this,t,e)}},{key:"performance",value:function(){var t="loading",e=0;return this.state.loaded&&(t="loaded",e=(this.performanceData.loadEnd-this.performanceData.loadStart)/1e3),this.state.error&&(t="error"),{src:this.src,state:t,time:e}}},{key:"$destroy",value:function(){this.el=null,this.src=null,this.error=null,this.loading=null,this.bindType=null,this.attempt=0}}]),t}(),A="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",T=["scroll","wheel","mousewheel","resize","animationend","transitionend","touchmove"],O={rootMargin:"0px",threshold:0};function $(t){return function(){function e(t){var n=t.preLoad,i=t.error,r=t.throttleWait,o=t.preLoadTop,a=t.dispatchEvent,u=t.loading,l=t.attempt,d=t.silent,f=void 0===d||d,v=t.scale,p=t.listenEvents;t.hasbind;var y,m,w,L,_,z,E=t.filter,x=t.adapter,$=t.observer,I=t.observerOptions;s(this,e),this.version='"1.3.5"',this.mode=c,this.ListenerQueue=[],this.TargetIndex=0,this.TargetQueue=[],this.options={silent:f,dispatchEvent:!!a,throttleWait:r||200,preLoad:n||1.3,preLoadTop:o||0,error:i||A,loading:u||A,attempt:l||3,scale:v||g(v),ListenEvents:p||T,hasbind:!1,supportWebp:b(),filter:E||{},adapter:x||{},observer:!!$,observerOptions:I||O},this._initEvent(),this._imageCache=new k({max:200}),this.lazyLoadHandler=(y=this._lazyLoadHandler.bind(this),m=this.options.throttleWait,w=null,L=null,_=0,z=!1,function(){if(z=!0,!w){var t=Date.now()-_,e=this,n=arguments,i=function(){_=Date.now(),w=!1,y.apply(e,n)};t>=m?i():w=setTimeout(i,m),z&&(clearTimeout(L),L=setTimeout(i,2*m))}}),this.setMode(this.options.observer?h:c)}return a(e,[{key:"config",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};u(this.options,t)}},{key:"performance",value:function(){var t=[];return this.ListenerQueue.map((function(e){t.push(e.performance())})),t}},{key:"addLazyBox",value:function(t){this.ListenerQueue.push(t),l&&(this._addListenerTarget(window),this._observer&&this._observer.observe(t.el),t.$el&&t.$el.parentNode&&this._addListenerTarget(t.$el.parentNode))}},{key:"add",value:function(e,n,i){var r=this;if(function(t,e){for(var n=!1,i=0,r=t.length;i1&&void 0!==arguments[1]?arguments[1]:{},n=new($(t))(e);"2"===t.version.split(".")[0]?t.directive("lazy",{bind:n.add.bind(n),update:n.update.bind(n),componentUpdated:n.lazyLoadHandler.bind(n),unbind:n.remove.bind(n)}):t.directive("lazy",{bind:n.lazyLoadHandler.bind(n),update:function(t,e){u(this.vm.$refs,this.vm.$els),n.add(this.el,{modifiers:this.modifiers||{},arg:this.arg,value:t,oldValue:e},{context:this.vm})},unbind:function(){n.remove(this.el)}})};var I=function(t){return{props:{tag:{type:String,default:"div"}},render:function(t){return t(this.tag,null,this.show?this.$slots.default:null)},data:function(){return{el:null,state:{loaded:!1},rect:{},show:!1}},mounted:function(){this.el=this.$el,t.addLazyBox(this),t.lazyLoadHandler()},beforeDestroy:function(){t.removeComponent(this)},methods:{getRect:function(){this.rect=this.$el.getBoundingClientRect()},checkInView:function(){return this.getRect(),l&&this.rect.top0&&this.rect.left0},load:function(){this.show=!0,this.state.loaded=!0,this.$emit("show",this)},destroy:function(){return this.$destroy}}}};I.install=function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=new($(t))(e);t.component("lazy-component",I(n))};var C=function(){function t(e){var n=e.lazy;s(this,t),this.lazy=n,n.lazyContainerMananger=this,this._queue=[]}return a(t,[{key:"bind",value:function(t,e,n){var i=new H({el:t,binding:e,vnode:n,lazy:this.lazy});this._queue.push(i)}},{key:"update",value:function(t,e,n){var i=y(this._queue,(function(e){return e.el===t}));i&&i.update({el:t,binding:e,vnode:n})}},{key:"unbind",value:function(t,e,n){var i=y(this._queue,(function(e){return e.el===t}));i&&(i.clear(),v(this._queue,i))}}]),t}(),S={selector:"img"},H=function(){function t(e){var n=e.el,i=e.binding,r=e.vnode,o=e.lazy;s(this,t),this.el=null,this.vnode=r,this.binding=i,this.options={},this.lazy=o,this._queue=[],this.update({el:n,binding:i})}return a(t,[{key:"update",value:function(t){var e=this,n=t.el,i=t.binding;this.el=n,this.options=u({},S,i.value),this.getImgs().forEach((function(t){e.lazy.add(t,u({},e.binding,{value:{src:"dataset"in t?t.dataset.src:t.getAttribute("data-src"),error:("dataset"in t?t.dataset.error:t.getAttribute("data-error"))||e.options.error,loading:("dataset"in t?t.dataset.loading:t.getAttribute("data-loading"))||e.options.loading}}),e.vnode)}))}},{key:"getImgs",value:function(){return function(t){for(var e=t.length,n=[],i=0;i1&&void 0!==arguments[1]?arguments[1]:{},n=new($(t))(e),i=new H({lazy:n});"2"===t.version.split(".")[0]?t.directive("lazy-container",{bind:i.bind.bind(i),componentUpdated:i.update.bind(i),unbind:i.unbind.bind(i)}):t.directive("lazy-container",{update:function(t,e){i.update(this.el,{modifiers:this.modifiers||{},arg:this.arg,value:t,oldValue:e},{context:this.vm})},unbind:function(){i.unbind(this.el)}})};var j=function(t){return{props:{src:[String,Object],tag:{type:String,default:"img"}},render:function(t){return t(this.tag,{attrs:{src:this.renderSrc}},this.$slots.default)},data:function(){return{el:null,options:{src:"",error:"",loading:"",attempt:t.options.attempt},state:{loaded:!1,error:!1,attempt:0},rect:{},renderSrc:""}},watch:{src:function(){this.init(),t.addLazyBox(this),t.lazyLoadHandler()}},created:function(){this.init(),this.renderSrc=this.options.loading},mounted:function(){this.el=this.$el,t.addLazyBox(this),t.lazyLoadHandler()},beforeDestroy:function(){t.removeComponent(this)},methods:{init:function(){var e=t._valueFormatter(this.src),n=e.src,i=e.loading,r=e.error;this.state.loaded=!1,this.options.src=n,this.options.error=r,this.options.loading=i,this.renderSrc=this.options.loading},getRect:function(){this.rect=this.$el.getBoundingClientRect()},checkInView:function(){return this.getRect(),l&&this.rect.top0&&this.rect.left0},load:function(){var e=this,n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:E;if(this.state.attempt>this.options.attempt-1&&this.state.error)return t.options.silent||console.log("VueLazyload log: "+this.options.src+" tried too more than "+this.options.attempt+" times"),void n();var i=this.options.src;L({src:i},(function(t){var n=t.src;e.renderSrc=n,e.state.loaded=!0}),(function(t){e.state.attempt++,e.renderSrc=e.options.error,e.state.error=!0}))}}}};j.install=function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=new($(t))(e);t.component("lazy-image",j(n))};var Q={install:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=new($(t))(e),i=new C({lazy:n}),r="2"===t.version.split(".")[0];t.prototype.$Lazyload=n,e.lazyComponent&&t.component("lazy-component",I(n)),e.lazyImage&&t.component("lazy-image",j(n)),r?(t.directive("lazy",{bind:n.add.bind(n),update:n.update.bind(n),componentUpdated:n.lazyLoadHandler.bind(n),unbind:n.remove.bind(n)}),t.directive("lazy-container",{bind:i.bind.bind(i),componentUpdated:i.update.bind(i),unbind:i.unbind.bind(i)})):(t.directive("lazy",{bind:n.lazyLoadHandler.bind(n),update:function(t,e){u(this.vm.$refs,this.vm.$els),n.add(this.el,{modifiers:this.modifiers||{},arg:this.arg,value:t,oldValue:e},{context:this.vm})},unbind:function(){n.remove(this.el)}}),t.directive("lazy-container",{update:function(t,e){i.update(this.el,{modifiers:this.modifiers||{},arg:this.arg,value:t,oldValue:e},{context:this.vm})},unbind:function(){i.unbind(this.el)}}))}};t.Lazy=$,t.LazyComponent=I,t.LazyContainer=C,t.LazyImage=j,t.default=Q,Object.defineProperty(t,"__esModule",{value:!0})}));
7 |
--------------------------------------------------------------------------------