├── .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 | [![Build Status](https://img.shields.io/circleci/project/hilongjw/vue-lazyload/master.svg?style=flat-square)](https://circleci.com/gh/hilongjw/vue-lazyload) 4 | [![npm version](https://img.shields.io/npm/v/vue-lazyload.svg?style=flat-square)](http://badge.fury.io/js/vue-lazyload) 5 | [![npm downloads](https://img.shields.io/npm/dm/vue-lazyload.svg?style=flat-square)](http://badge.fury.io/js/vue-lazyload) 6 | [![npm license](https://img.shields.io/npm/l/vue-lazyload.svg?style=flat-square)](http://badge.fury.io/js/vue-lazyload) 7 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) 8 | [![CDNJS version](https://img.shields.io/cdnjs/v/vue-lazyload.svg)](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 | 117 | ``` 118 | 119 | use `v-lazy-container` work with raw HTML 120 | 121 | ```html 122 |
123 | 124 | 125 | 126 |
127 | ``` 128 | 129 | custom `error` and `loading` placeholder image 130 | 131 | ```html 132 |
133 | 134 | 135 | 136 |
137 | ``` 138 | 139 | ```html 140 |
141 | 142 | 143 | 144 |
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 | 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 | ![performance-demo](http://ww1.sinaimg.cn/large/69402bf8gw1fbo62ocvlaj213k09w78w.jpg) 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 | --------------------------------------------------------------------------------