From 65019a58b6cd26ff245def86313b3ee0846db96a Mon Sep 17 00:00:00 2001 From: awe Date: Sat, 10 Feb 2018 13:33:47 +0800 Subject: [PATCH] add: vue-lazy-container --- .babelrc | 12 +- .circleci/config.yml | 2 +- README.md | 22 +- build.js | 5 +- karma.conf.js | 50 +++ package.json | 32 +- src/index.js | 99 +++--- src/lazy-component.js | 88 +++--- src/lazy-container.js | 75 +++++ src/lazy.js | 535 ++++++++++++++++---------------- src/listener.js | 370 +++++++++++----------- src/util.js | 418 +++++++++++++------------ test/{tests.js => test-spec.js} | 29 +- vue-lazyload.js | 4 +- 14 files changed, 974 insertions(+), 767 deletions(-) create mode 100644 karma.conf.js create mode 100644 src/lazy-container.js rename test/{tests.js => test-spec.js} (55%) diff --git a/.babelrc b/.babelrc index 3ed94df..9bd07a7 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,13 @@ { "presets": [ - ["env", { "modules": false }] + [ + "env", + { + "modules": false + } + ] + ], + "plugins": [ + "external-helpers" ] -} +} \ No newline at end of file diff --git a/.circleci/config.yml b/.circleci/config.yml index ca093f1..8c39cd3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ jobs: build: docker: # specify the version you desire here - - image: circleci/node:7.10 + - image: circleci/node:8.9.4 # Specify service dependencies here if necessary # CircleCI maintains a library of pre-built images diff --git a/README.md b/README.md index 396a267..f03004c 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ CDN: [https://unpkg.com/vue-lazyload/vue-lazyload.js](https://unpkg.com/vue-lazy # Usage -main.js +main.js: ```javascript @@ -94,6 +94,26 @@ new Vue({ }) ``` +template: + +```html + +``` + +use `v-lazy-container` work with raw HTML + +```html +
+ + + +
+``` + ## Constructor Options |key|description|default|options| diff --git a/build.js b/build.js index e0ce76b..847c881 100644 --- a/build.js +++ b/build.js @@ -16,10 +16,7 @@ async function build() { const bundle = await rollup.rollup({ input: path.resolve(__dirname, 'src/index.js'), plugins: [ - babel({ - exclude: 'node_modules/**', - plugins: ['external-helpers'] - }), + babel({ runtimeHelpers: true }), uglify() ] }) diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 0000000..b1d671f --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,50 @@ +const DEBUG = !!process.env.DEBUG +if (!DEBUG) process.env.CHROME_BIN = require('puppeteer').executablePath() + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['mocha', 'chai'], + plugins: [ + require('karma-mocha'), + require('karma-chai'), + require('karma-chrome-launcher'), + require('karma-rollup-preprocessor'), + // require('karma-coverage') + ], + files: [ + { pattern: 'test/*-spec.js', watched: false } + ], + exclude: [], + preprocessors: { + 'test/*-spec.js': ['rollup'] + }, + // coverageReporter: { + // type: 'html', + // dir: 'coverage/' + // }, + rollupPreprocessor: { + plugins: [ + require('rollup-plugin-babel')(), + require('rollup-plugin-node-resolve')({ + jsnext: true, + browser: true + }), + require('rollup-plugin-replace')({ + 'process.env.NODE_ENV': JSON.stringify( 'production' ) + }) + ], + format: 'iife', // Helps prevent naming collisions. + name: 'lib', // Required for 'iife' format. + sourcemap: 'inline' // Sensible for testing. + }, + reporters: ['progress'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: DEBUG ? ['Chrome'] : ['ChromeHeadless'], + singleRun: !DEBUG, + concurrency: Infinity + }) +} diff --git a/package.json b/package.json index a24fd49..6e64f6e 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "vue-lazyload", - "version": "1.1.4", + "version": "1.2.0", "description": "Vue module for lazy-loading images in your vue.js applications.", "main": "vue-lazyload.js", "unpkg": "vue-lazyload.js", "scripts": { - "start": "node build", - "compile": "babel --presets env -d lib/ src/", - "test": "npm run compile && mocha --require babel-core/register" + "build": "node build", + "test": "karma start", + "test:debug": "cross-env DEBUG=true karma start" }, "dependencies": {}, "repository": { @@ -24,6 +24,11 @@ "bugs": { "url": "https://github.com/hilongjw/vue-lazyload/issues" }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not ie <= 8" + ], "license": "MIT", "devDependencies": { "babel-cli": "^6.26.0", @@ -34,10 +39,21 @@ "babel-preset-stage-0": "^6.24.1", "babel-register": "^6.26.0", "chai": "^4.1.2", + "karma": "^1.7.1", + "karma-chai": "^0.1.0", + "karma-chrome-launcher": "^2.2.0", + "karma-coverage": "^1.1.1", + "karma-expect": "^1.1.3", + "karma-mocha": "^1.3.0", + "karma-rollup-preprocessor": "^5.0.2", "mocha": "^4.0.1", - "rollup": "^0.52.1", - "rollup-plugin-babel": "^3.0.2", - "rollup-plugin-uglify": "^2.0.1", - "vue": "^2.5.9" + "puppeteer": "^0.13.0", + "rollup": "^0.51.1", + "rollup-plugin-babel": "^2.6.1", + "rollup-plugin-commonjs": "^8.2.6", + "rollup-plugin-node-resolve": "^3.0.2", + "rollup-plugin-replace": "^2.0.0", + "rollup-plugin-uglify": "^1.0.1", + "vue": "^2.5.13" } } diff --git a/src/index.js b/src/index.js index 168d917..05dba38 100644 --- a/src/index.js +++ b/src/index.js @@ -1,50 +1,73 @@ import Lazy from './lazy' import LazyComponent from './lazy-component' +import LazyContainer from './lazy-container' import { assign } from './util' export default { - /** - * install function - * @param {Vue} Vue - * @param {object} options lazyload options - */ - install (Vue, options = {}) { - const LazyClass = Lazy(Vue) - const lazy = new LazyClass(options) + /* + * install function + * @param {Vue} Vue + * @param {object} options lazyload options + */ + install (Vue, options = {}) { + const LazyClass = Lazy(Vue) + const lazy = new LazyClass(options) + const lazyContainer = new LazyContainer({ lazy }) - const isVueNext = Vue.version.split('.')[0] === '2' + const isVue2 = Vue.version.split('.')[0] === '2' - Vue.prototype.$Lazyload = lazy + Vue.prototype.$Lazyload = lazy - if (options.lazyComponent) { - Vue.component('lazy-component', LazyComponent(lazy)) + if (options.lazyComponent) { + Vue.component('lazy-component', LazyComponent(lazy)) + } + + if (isVue2) { + Vue.directive('lazy', { + bind: lazy.add.bind(lazy), + update: lazy.update.bind(lazy), + componentUpdated: lazy.lazyLoadHandler.bind(lazy), + unbind: lazy.remove.bind(lazy) + }) + Vue.directive('lazy-container', { + bind: lazyContainer.bind.bind(lazyContainer), + update: lazyContainer.update.bind(lazyContainer), + unbind: lazyContainer.unbind.bind(lazyContainer) + }) + } else { + Vue.directive('lazy', { + bind: lazy.lazyLoadHandler.bind(lazy), + update (newValue, oldValue) { + assign(this.vm.$refs, this.vm.$els) + lazy.add(this.el, { + modifiers: this.modifiers || {}, + arg: this.arg, + value: newValue, + oldValue: oldValue + }, { + context: this.vm + }) + }, + unbind () { + lazy.remove(this.el) } + }) - if (isVueNext) { - Vue.directive('lazy', { - bind: lazy.add.bind(lazy), - update: lazy.update.bind(lazy), - componentUpdated: lazy.lazyLoadHandler.bind(lazy), - unbind : lazy.remove.bind(lazy) - }) - } else { - Vue.directive('lazy', { - bind: lazy.lazyLoadHandler.bind(lazy), - update (newValue, oldValue) { - assign(this.vm.$refs, this.vm.$els) - lazy.add(this.el, { - modifiers: this.modifiers || {}, - arg: this.arg, - value: newValue, - oldValue: oldValue - }, { - context: this.vm - }) - }, - unbind () { - lazy.remove(this.el) - } - }) + Vue.directive('lazy-container', { + update (newValue, oldValue) { + lazyContainer.update(this.el, { + modifiers: this.modifiers || {}, + arg: this.arg, + value: newValue, + oldValue: oldValue + }, { + context: this.vm + }) + }, + unbind () { + lazyContainer.unbind(this.el) } + }) } -} \ No newline at end of file + } +} diff --git a/src/lazy-component.js b/src/lazy-component.js index 8643312..e503d8c 100644 --- a/src/lazy-component.js +++ b/src/lazy-component.js @@ -1,52 +1,52 @@ import { inBrowser } from './util' export default (lazy) => { - return { - props: { - tag: { - type: String, - default: 'div' - } + return { + props: { + tag: { + type: String, + default: 'div' + } + }, + render (h) { + if (this.show === false) { + return h(this.tag) + } + return h(this.tag, null, this.$slots.default) + }, + data () { + return { + el: null, + state: { + loaded: false }, - render (h) { - if (this.show === false) { - return h(this.tag) - } - return h(this.tag, null, this.$slots.default) - }, - data () { - return { - el: null, - state: { - loaded: false - }, - rect: {}, - show: false - } - }, - mounted () { - this.el = this.$el - lazy.addLazyBox(this) - lazy.lazyLoadHandler() - }, - beforeDestroy () { - lazy.removeComponent(this) - }, - methods: { - getRect () { - this.rect = this.$el.getBoundingClientRect() - }, - checkInView () { - this.getRect() - return inBrowser && + rect: {}, + show: false + } + }, + mounted () { + this.el = this.$el + lazy.addLazyBox(this) + lazy.lazyLoadHandler() + }, + beforeDestroy () { + lazy.removeComponent(this) + }, + methods: { + getRect () { + this.rect = this.$el.getBoundingClientRect() + }, + checkInView () { + this.getRect() + 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) - }, - load () { - this.show = true - this.state.loaded = true - this.$emit('show', this) - } - } + }, + load () { + this.show = true + this.state.loaded = true + this.$emit('show', this) + } } + } } diff --git a/src/lazy-container.js b/src/lazy-container.js new file mode 100644 index 0000000..80b01bb --- /dev/null +++ b/src/lazy-container.js @@ -0,0 +1,75 @@ +import { + assign, + find, + remove, + ArrayFrom +} from './util' + +export default class LazyContainerMananger { + constructor ({ lazy }) { + this.lazy = lazy + lazy.lazyContainerMananger = this + this._queue = [] + } + + bind (el, binding, vnode) { + const container = new LazyContainer({ el, binding, vnode, lazy: this.lazy }) + this._queue.push(container) + } + + update (el, binding, vnode) { + const container = find(this._queue, item => item.el === el) + if (!container) return + container.update({ el, binding, vnode }) + } + + unbind (el, binding, vnode) { + const container = find(this._queue, item => item.el === el) + if (!container) return + container.clear() + remove(this._queue, container) + } +} + +const defaultOptions = { + selector: 'img' +} + +class LazyContainer { + constructor ({ el, binding, vnode, lazy }) { + this.el = null + this.vnode = vnode + this.binding = binding + this.options = {} + this.lazy = lazy + + this._queue = [] + this.update({ el, binding }) + } + + update ({ el, binding }) { + this.el = el + this.options = assign({}, defaultOptions, binding.value) + const imgs = this.getImgs() + imgs.forEach(el => { + this.lazy.add(el, Object.assign({}, this.binding, { + value: { + src: el.getAttribute('data-src') + } + }), this.vnode) + }) + } + + getImgs () { + return ArrayFrom(this.el.querySelectorAll(this.options.selector)) + } + + clear () { + const imgs = this.getImgs() + imgs.forEach(el => this.lazy.remove(el)) + + this.vnode = null + this.binding = null + this.lazy = null + } +} diff --git a/src/lazy.js b/src/lazy.js index 7397be9..cb7d0f8 100644 --- a/src/lazy.js +++ b/src/lazy.js @@ -1,18 +1,19 @@ import { - inBrowser, - remove, - some, - find, - _, - throttle, - supportWebp, - getDPR, - scrollParent, - getBestSelectionFromSrcset, - assign, - isObject, - hasIntersectionObserver, - modeType + inBrowser, + CustomEvent, + remove, + some, + find, + _, + throttle, + supportWebp, + getDPR, + scrollParent, + getBestSelectionFromSrcset, + assign, + isObject, + hasIntersectionObserver, + modeType } from './util' import ReactiveListener from './listener' @@ -33,23 +34,23 @@ export default function (Vue) { this.TargetIndex = 0 this.TargetQueue = [] this.options = { - silent: silent, - dispatchEvent: !!dispatchEvent, - throttleWait: throttleWait || 200, - preLoad: preLoad || 1.3, - preLoadTop: preLoadTop || 0, - error: error || DEFAULT_URL, - loading: loading || DEFAULT_URL, - attempt: attempt || 3, - scale: scale || getDPR(scale), - ListenEvents: listenEvents || DEFAULT_EVENTS, - hasbind: false, - supportWebp: supportWebp(), - filter: filter || {}, - adapter: adapter || {}, - observer: !!observer, - observerOptions: observerOptions || DEFAULT_OBSERVER_OPTIONS - } + silent: silent, + dispatchEvent: !!dispatchEvent, + throttleWait: throttleWait || 200, + preLoad: preLoad || 1.3, + preLoadTop: preLoadTop || 0, + error: error || DEFAULT_URL, + loading: loading || DEFAULT_URL, + attempt: attempt || 3, + scale: scale || getDPR(scale), + ListenEvents: listenEvents || DEFAULT_EVENTS, + hasbind: false, + supportWebp: supportWebp(), + filter: filter || {}, + adapter: adapter || {}, + observer: !!observer, + observerOptions: observerOptions || DEFAULT_OBSERVER_OPTIONS + } this._initEvent() this.lazyLoadHandler = throttle(this._lazyLoadHandler.bind(this), this.options.throttleWait) @@ -57,106 +58,106 @@ export default function (Vue) { this.setMode(this.options.observer ? modeType.observer : modeType.event) } - /** - * update config - * @param {Object} config params - * @return - */ + /** + * update config + * @param {Object} config params + * @return + */ config (options = {}) { assign(this.options, options) } - /** - * output listener's load performance - * @return {Array} - */ + /** + * output listener's load performance + * @return {Array} + */ performance () { let list = [] this.ListenerQueue.map(item => { - list.push(item.performance()) - }) + list.push(item.performance()) + }) return list } - /** - * add lazy component to queue - * @param {Vue} vm lazy component instance - * @return - */ + /* + * add lazy component to queue + * @param {Vue} vm lazy component instance + * @return + */ addLazyBox (vm) { this.ListenerQueue.push(vm) if (inBrowser) { - this._addListenerTarget(window) - this._observer && this._observer.observe(vm.el) - if (vm.$el && vm.$el.parentNode) { - this._addListenerTarget(vm.$el.parentNode) - } + this._addListenerTarget(window) + this._observer && this._observer.observe(vm.el) + if (vm.$el && vm.$el.parentNode) { + this._addListenerTarget(vm.$el.parentNode) } + } } - /** - * add image listener to queue - * @param {DOM} el - * @param {object} binding vue directive binding - * @param {vnode} vnode vue directive vnode - * @return - */ + /* + * add image listener to queue + * @param {DOM} el + * @param {object} binding vue directive binding + * @param {vnode} vnode vue directive vnode + * @return + */ add (el, binding, vnode) { if (some(this.ListenerQueue, item => item.el === el)) { - this.update(el, binding) - return Vue.nextTick(this.lazyLoadHandler) - } + this.update(el, binding) + return Vue.nextTick(this.lazyLoadHandler) + } let { src, loading, error } = this._valueFormatter(binding.value) Vue.nextTick(() => { - src = getBestSelectionFromSrcset(el, this.options.scale) || src - this._observer && this._observer.observe(el) - - const container = Object.keys(binding.modifiers)[0] - let $parent + src = getBestSelectionFromSrcset(el, this.options.scale) || src + this._observer && this._observer.observe(el) - if (container) { - $parent = vnode.context.$refs[container] - // if there is container passed in, try ref first, then fallback to getElementById to support the original usage - $parent = $parent ? $parent.$el || $parent : document.getElementById(container) - } + const container = Object.keys(binding.modifiers)[0] + let $parent - if (!$parent) { - $parent = scrollParent(el) - } + if (container) { + $parent = vnode.context.$refs[container] + // if there is container passed in, try ref first, then fallback to getElementById to support the original usage + $parent = $parent ? $parent.$el || $parent : document.getElementById(container) + } - const newListener = new ReactiveListener({ - bindType: binding.arg, - $parent, - el, - loading, - error, - src, - elRenderer: this._elRenderer.bind(this), - options: this.options - }) - - this.ListenerQueue.push(newListener) - - if (inBrowser) { - this._addListenerTarget(window) - this._addListenerTarget($parent) - } + if (!$parent) { + $parent = scrollParent(el) + } - this.lazyLoadHandler() - Vue.nextTick(() => this.lazyLoadHandler()) + const newListener = new ReactiveListener({ + bindType: binding.arg, + $parent, + el, + loading, + error, + src, + elRenderer: this._elRenderer.bind(this), + options: this.options }) + + this.ListenerQueue.push(newListener) + + if (inBrowser) { + this._addListenerTarget(window) + this._addListenerTarget($parent) + } + + this.lazyLoadHandler() + Vue.nextTick(() => this.lazyLoadHandler()) + }) } - /** - * update image src - * @param {DOM} el - * @param {object} vue directive binding - * @return - */ + /** + * update image src + * @param {DOM} el + * @param {object} vue directive binding + * @return + */ update (el, binding) { let { src, loading, error } = this._valueFormatter(binding.value) src = getBestSelectionFromSrcset(el, this.options.scale) || src @@ -164,234 +165,238 @@ export default function (Vue) { const exist = find(this.ListenerQueue, item => item.el === el) exist && exist.update({ - src, - loading, - error - }) + src, + loading, + error + }) this._observer && this._observer.observe(el) this.lazyLoadHandler() Vue.nextTick(() => this.lazyLoadHandler()) } - /** - * remove listener form list - * @param {DOM} el - * @return - */ + /** + * remove listener form list + * @param {DOM} el + * @return + */ remove (el) { if (!el) return this._observer && this._observer.unobserve(el) const existItem = find(this.ListenerQueue, item => item.el === el) if (existItem) { - this._removeListenerTarget(existItem.$parent) - this._removeListenerTarget(window) - remove(this.ListenerQueue, existItem) && existItem.destroy() - } + this._removeListenerTarget(existItem.$parent) + this._removeListenerTarget(window) + remove(this.ListenerQueue, existItem) && existItem.destroy() + } } - /** - * remove lazy components form list - * @param {Vue} vm Vue instance - * @return - */ + /* + * remove lazy components form list + * @param {Vue} vm Vue instance + * @return + */ removeComponent (vm) { if (!vm) return remove(this.ListenerQueue, vm) this._observer && this._observer.unobserve(vm.el) if (vm.$parent && vm.$el.parentNode) { - this._removeListenerTarget(vm.$el.parentNode) - } + this._removeListenerTarget(vm.$el.parentNode) + } this._removeListenerTarget(window) } setMode (mode) { if (!hasIntersectionObserver && mode === modeType.observer) { - mode = modeType.event - } + mode = modeType.event + } this.mode = mode // event or observer if (mode === modeType.event) { - if (this._observer) { - this.ListenerQueue.forEach(listener => { - this._observer.unobserve(listener.el) - }) - this._observer = null - } - - this.TargetQueue.forEach(target => { - this._initListen(target.el, true) - }) - } else { - this.TargetQueue.forEach(target => { - this._initListen(target.el, false) - }) - this._initIntersectionObserver() + if (this._observer) { + this.ListenerQueue.forEach(listener => { + this._observer.unobserve(listener.el) + }) + this._observer = null } + + this.TargetQueue.forEach(target => { + this._initListen(target.el, true) + }) + } else { + this.TargetQueue.forEach(target => { + this._initListen(target.el, false) + }) + this._initIntersectionObserver() + } } - /** ** Private functions ****/ + /**** Private functions ****/ - /** - * add listener target - * @param {DOM} el listener target - * @return - */ + /* + * add listener target + * @param {DOM} el listener target + * @return + */ _addListenerTarget (el) { if (!el) return let target = find(this.TargetQueue, target => target.el === el) if (!target) { - target = { - el: el, - id: ++this.TargetIndex, - childrenCount: 1, - listened: true - } - this.mode === modeType.event && this._initListen(target.el, true) - this.TargetQueue.push(target) - } else { - target.childrenCount++ + target = { + el: el, + id: ++this.TargetIndex, + childrenCount: 1, + listened: true } + this.mode === modeType.event && this._initListen(target.el, true) + this.TargetQueue.push(target) + } else { + target.childrenCount++ + } return this.TargetIndex } - /** - * remove listener target or reduce target childrenCount - * @param {DOM} el or window - * @return - */ + /* + * remove listener target or reduce target childrenCount + * @param {DOM} el or window + * @return + */ _removeListenerTarget (el) { this.TargetQueue.forEach((target, index) => { - if (target.el === el) { - target.childrenCount-- - if (!target.childrenCount) { - this._initListen(target.el, false) - this.TargetQueue.splice(index, 1) - target = null - } - } - }) + if (target.el === el) { + target.childrenCount-- + if (!target.childrenCount) { + this._initListen(target.el, false) + this.TargetQueue.splice(index, 1) + target = null + } + } + }) } - /** - * add or remove eventlistener - * @param {DOM} el DOM or Window - * @param {boolean} start flag - * @return - */ + /* + * add or remove eventlistener + * @param {DOM} el DOM or Window + * @param {boolean} start flag + * @return + */ _initListen (el, start) { this.options.ListenEvents.forEach((evt) => _[start ? 'on' : 'off'](el, evt, this.lazyLoadHandler)) } _initEvent () { this.Event = { - listeners: { - loading: [], - loaded: [], - error: [] - } + listeners: { + loading: [], + loaded: [], + error: [] } + } this.$on = (event, func) => { - this.Event.listeners[event].push(func) - } + this.Event.listeners[event].push(func) + } this.$once = (event, func) => { - const vm = this - function on () { - vm.$off(event, on) - func.apply(vm, arguments) - } - this.$on(event, on) + const vm = this + function on () { + vm.$off(event, on) + func.apply(vm, arguments) } + this.$on(event, on) + } this.$off = (event, func) => { - if (!func) { - this.Event.listeners[event] = [] - return - } - remove(this.Event.listeners[event], func) + if (!func) { + this.Event.listeners[event] = [] + return } + remove(this.Event.listeners[event], func) + } this.$emit = (event, context, inCache) => { - this.Event.listeners[event].forEach(func => func(context, inCache)) - } + this.Event.listeners[event].forEach(func => func(context, inCache)) + } } - /** - * find nodes which in viewport and trigger load - * @return - */ + /** + * find nodes which in viewport and trigger load + * @return + */ _lazyLoadHandler () { - console.log(this) let catIn = false - this.ListenerQueue.forEach((listener, index)=> { - if (listener.state.loaded) return - catIn = listener.checkInView() - catIn && (listener.load(() => this.ListenerQueue.splice(index, 1))) + this.ListenerQueue.forEach((listener, index) => { + if (listener.state.loaded) return + catIn = listener.checkInView() + if (!catIn) return + listener.load(() => { + if (!listener.error && listener.loaded) { + this.ListenerQueue.splice(index, 1) + } }) + }) } - /** - * init IntersectionObserver - * set mode to observer - * @return - */ + /** + * init IntersectionObserver + * set mode to observer + * @return + */ _initIntersectionObserver () { if (!hasIntersectionObserver) return this._observer = new IntersectionObserver(this._observerHandler.bind(this), this.options.observerOptions) if (this.ListenerQueue.length) { - this.ListenerQueue.forEach(listener => { - this._observer.observe(listener.el) - }) - } + this.ListenerQueue.forEach(listener => { + this._observer.observe(listener.el) + }) + } } - /** - * init IntersectionObserver - * @return - */ + /** + * init IntersectionObserver + * @return + */ _observerHandler (entries, observer) { entries.forEach(entry => { - if (entry.isIntersecting) { - this.ListenerQueue.forEach(listener => { - if (listener.el === entry.target) { - if (listener.state.loaded) return this._observer.unobserve(listener.el) - listener.load() - } - }) + if (entry.isIntersecting) { + this.ListenerQueue.forEach(listener => { + if (listener.el === entry.target) { + if (listener.state.loaded) return this._observer.unobserve(listener.el) + listener.load() } - }) + }) + } + }) } - /** - * set element attribute with image'url and state - * @param {object} lazyload listener object - * @param {string} state will be rendered - * @param {bool} inCache is rendered from cache - * @return - */ + /** + * set element attribute with image'url and state + * @param {object} lazyload listener object + * @param {string} state will be rendered + * @param {bool} inCache is rendered from cache + * @return + */ _elRenderer (listener, state, cache) { if (!listener.el) return const { el, bindType } = listener let src switch (state) { - case 'loading': - src = listener.loading - break - case 'error': - src = listener.error - break - default: - src = listener.src - break - } + case 'loading': + src = listener.loading + break + case 'error': + src = listener.error + break + default: + src = listener.src + break + } if (bindType) { - el.style[bindType] = 'url(' + src + ')' - } else if (el.getAttribute('src') !== src) { - el.setAttribute('src', src) - } + el.style[bindType] = 'url(' + src + ')' + } else if (el.getAttribute('src') !== src) { + el.setAttribute('src', src) + } el.setAttribute('lazy', state) @@ -399,35 +404,35 @@ export default function (Vue) { this.options.adapter[state] && this.options.adapter[state](listener, this.options) if (this.options.dispatchEvent) { - const event = new CustomEvent(state, { - detail: listener - }) - el.dispatchEvent(event) - } + const event = new CustomEvent(state, { + detail: listener + }) + el.dispatchEvent(event) + } } - /** - * generate loading loaded error image url - * @param {string} image's src - * @return {object} image's loading, loaded, error url - */ + /** + * generate loading loaded error image url + * @param {string} image's src + * @return {object} image's loading, loaded, error url + */ _valueFormatter (value) { let src = value let loading = this.options.loading let error = this.options.error - // value is object + // value is object if (isObject(value)) { - if (!value.src && !this.options.silent) console.error('Vue Lazyload warning: miss src with ' + value) - src = value.src - loading = value.loading || this.options.loading - error = value.error || this.options.error - } + if (!value.src && !this.options.silent) console.error('Vue Lazyload warning: miss src with ' + value) + src = value.src + loading = value.loading || this.options.loading + error = value.error || this.options.error + } return { - src, - loading, - error - } - } + src, + loading, + error + } } + } } diff --git a/src/listener.js b/src/listener.js index c5b5553..f0cb0f0 100644 --- a/src/listener.js +++ b/src/listener.js @@ -2,204 +2,212 @@ import { loadImageAsync, ObjectKeys } from './util' let imageCache = {} -export default class ReactiveListener { - constructor ({ el, src, error, loading, bindType, $parent, options, elRenderer }) { - this.el = el - this.src = src - this.error = error - this.loading = loading - this.bindType = bindType - this.attempt = 0 - - this.naturalHeight = 0 - this.naturalWidth = 0 - - this.options = options - - this.filter() +// el: { +// state, +// src, +// error, +// loading +// } - this.initState() +export default class ReactiveListener { + constructor ({ el, src, error, loading, bindType, $parent, options, elRenderer }) { + this.el = el + this.src = src + this.error = error + this.loading = loading + this.bindType = bindType + this.attempt = 0 - this.performanceData = { - init: Date.now(), - loadStart: null, - loadEnd: null - } + this.naturalHeight = 0 + this.naturalWidth = 0 - this.rect = el.getBoundingClientRect() + this.options = options - this.$parent = $parent - this.elRenderer = elRenderer - this.render('loading', false) - } + this.rect = null - /** - * init listener state - * @return - */ - initState () { - this.state = { - error: false, - loaded: false, - rendered: false - } - } + this.$parent = $parent + this.elRenderer = elRenderer - /** - * record performance - * @return - */ - record (event) { - this.performanceData[event] = Date.now() + this.performanceData = { + init: Date.now(), + loadStart: 0, + loadEnd: 0 } - /** - * update image listener data - * @param {String} image uri - * @param {String} loading image uri - * @param {String} error image uri - * @return - */ - update ({ src, loading, error }) { - const oldSrc = this.src - this.src = src - this.loading = loading - this.error = error - this.filter() - if (oldSrc !== this.src) { - this.attempt = 0 - this.initState() - } + this.filter() + this.initState() + this.render('loading', false) + } + + /* + * init listener state + * @return + */ + initState () { + this.el.dataset.src = this.src + this.state = { + error: false, + loaded: false, + rendered: false } - - /** - * get el node rect - * @return - */ - getRect () { - this.rect = this.el.getBoundingClientRect() + } + + /* + * record performance + * @return + */ + record (event) { + this.performanceData[event] = Date.now() + } + + /* + * update image listener data + * @param {String} image uri + * @param {String} loading image uri + * @param {String} error image uri + * @return + */ + update ({ src, loading, error }) { + const oldSrc = this.src + this.src = src + this.loading = loading + this.error = error + this.filter() + if (oldSrc !== this.src) { + this.attempt = 0 + this.initState() } - - /** - * check el is in view - * @return {Boolean} el is in view - */ - checkInView () { - this.getRect() - return (this.rect.top < window.innerHeight * this.options.preLoad && this.rect.bottom > this.options.preLoadTop) && + } + + /* + * get el node rect + * @return + */ + getRect () { + this.rect = this.el.getBoundingClientRect() + } + + /* + * check el is in view + * @return {Boolean} el is in view + */ + checkInView () { + this.getRect() + 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) + } + + /* + * listener filter + */ + filter () { + ObjectKeys(this.options.filter).map(key => { + this.options.filter[key](this, this.options) + }) + } + + /* + * render loading first + * @params cb:Function + * @return + */ + renderLoading (cb) { + loadImageAsync({ + src: this.loading + }, data => { + this.render('loading', false) + cb() + }, () => { + // handler `loading image` load failed + cb() + if (!this.options.silent) console.warn(`VueLazyload log: load failed with loading image(${this.loading})`) + }) + } + + /* + * try load image and render it + * @return + */ + load (onFinish) { + if ((this.attempt > this.options.attempt - 1) && this.state.error) { + if (!this.options.silent) console.log(`VueLazyload log: ${this.src} tried too more than ${this.options.attempt} times`) + onFinish() + return } - /** - * listener filter - */ - filter () { - ObjectKeys(this.options.filter).map(key => { - this.options.filter[key](this, this.options) - }) - } - - /** - * render loading first - * @params cb:Function - * @return - */ - renderLoading (cb) { - loadImageAsync({ - src: this.loading - }, data => { - this.render('loading', false) - cb() - }, err => { - // handler `loading image` load failed - cb() - if (!this.options.silent) console.warn(`VueLazyload log: load failed with loading image(${this.loading})`) - }) + if (this.state.loaded || imageCache[this.src]) { + this.state.loaded = true + onFinish() + return this.render('loaded', true) } - /** - * try load image and render it - * @return - */ - load (onFinish) { - if ((this.attempt > this.options.attempt - 1) && this.state.error) { - if (!this.options.silent) console.log(`VueLazyload log: ${this.src} tried too more than ${this.options.attempt} times`) - onFinish() - return - } - - if (this.state.loaded || imageCache[this.src]) { - this.state.loaded = true - onFinish() - return this.render('loaded', true) - } - - this.renderLoading(() => { - this.attempt++ - - this.record('loadStart') - - loadImageAsync({ - src: this.src - }, data => { - this.naturalHeight = data.naturalHeight - this.naturalWidth = data.naturalWidth - this.state.loaded = true - this.state.error = false - this.record('loadEnd') - this.render('loaded', false) - imageCache[this.src] = 1 - onFinish() - }, err => { - this.state.error = true - this.state.loaded = false - this.render('error', false) - }) - }) + this.renderLoading(() => { + this.attempt++ + + this.record('loadStart') + + loadImageAsync({ + src: this.src + }, data => { + this.naturalHeight = data.naturalHeight + this.naturalWidth = data.naturalWidth + this.state.loaded = true + this.state.error = false + this.record('loadEnd') + this.render('loaded', false) + imageCache[this.src] = 1 + onFinish() + }, err => { + console.error(err) + this.state.error = true + this.state.loaded = false + this.render('error', false) + }) + }) + } + + /* + * render image + * @param {String} state to render // ['loading', 'src', 'error'] + * @param {String} is form cache + * @return + */ + render (state, cache) { + this.elRenderer(this, state, cache) + } + + /* + * output performance data + * @return {Object} performance data + */ + performance () { + let state = 'loading' + let time = 0 + + if (this.state.loaded) { + state = 'loaded' + time = (this.performanceData.loadEnd - this.performanceData.loadStart) / 1000 } - /** - * render image - * @param {String} state to render // ['loading', 'src', 'error'] - * @param {String} is form cache - * @return - */ - render (state, cache) { - this.elRenderer(this, state, cache) - } - - /** - * output performance data - * @return {Object} performance data - */ - performance () { - let state = 'loading' - let time = 0 - - if (this.state.loaded) { - state = 'loaded' - time = (this.performanceData.loadEnd - this.performanceData.loadStart) / 1000 - } - - if (this.state.error) state = 'error' - - return { - src: this.src, - state, - time - } - } + if (this.state.error) state = 'error' - /** - * destroy - * @return - */ - destroy () { - this.el = null - this.src = null - this.error = null - this.loading = null - this.bindType = null - this.attempt = 0 + return { + src: this.src, + state, + time } + } + + /* + * destroy + * @return + */ + destroy () { + this.el = null + this.src = null + this.error = null + this.loading = null + this.bindType = null + this.attempt = 0 + } } diff --git a/src/util.js b/src/util.js index 73a3439..088e0d3 100644 --- a/src/util.js +++ b/src/util.js @@ -2,238 +2,251 @@ const inBrowser = typeof window !== 'undefined' export const hasIntersectionObserver = inBrowser && 'IntersectionObserver' in window export const modeType = { - event: 'event', - observer: 'observer' + event: 'event', + observer: 'observer' } +// CustomEvent polyfill +const CustomEvent = (function () { + if (!inBrowser) return + if (typeof window.CustomEvent === 'function') return window.CustomEvent + function CustomEvent (event, params) { + params = params || { bubbles: false, cancelable: false, detail: undefined } + var evt = document.createEvent('CustomEvent') + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail) + return evt + } + CustomEvent.prototype = window.Event.prototype + return CustomEvent +})() + function remove (arr, item) { - if (!arr.length) return - const index = arr.indexOf(item) - if (index > -1) return arr.splice(index, 1) + if (!arr.length) return + const index = arr.indexOf(item) + if (index > -1) return arr.splice(index, 1) } function assign (target, source) { - if (!target || !source) return target || {} - if (target instanceof Object) { - for (let key in source) { - target[key] = source[key] - } + if (!target || !source) return target || {} + if (target instanceof Object) { + for (let key in source) { + target[key] = source[key] } - return target + } + return target } function some (arr, fn) { - let has = false - for (let i = 0, len = arr.length; i < len; i++) { - if (fn(arr[i])) { - has = true - break - } + let has = false + for (let i = 0, len = arr.length; i < len; i++) { + if (fn(arr[i])) { + has = true + break } - return has + } + return has } function getBestSelectionFromSrcset (el, scale) { - if (el.tagName !== 'IMG' || !el.getAttribute('data-srcset')) return - - let options = el.getAttribute('data-srcset') - const result = [] - const container = el.parentNode - const containerWidth = container.offsetWidth * scale - - let spaceIndex - let tmpSrc - let tmpWidth - - options = options.trim().split(',') - - options.map(item => { - item = item.trim() - spaceIndex = item.lastIndexOf(' ') - if (spaceIndex === -1) { - tmpSrc = item - tmpWidth = 999998 - } else { - tmpSrc = item.substr(0, spaceIndex) - tmpWidth = parseInt(item.substr(spaceIndex + 1, item.length - spaceIndex - 2), 10) - } - result.push([tmpWidth, tmpSrc]) - }) + if (el.tagName !== 'IMG' || !el.getAttribute('data-srcset')) return - result.sort(function (a, b) { - if (a[0] < b[0]) { - return -1 - } - if (a[0] > b[0]) { - return 1 - } - if (a[0] === b[0]) { - if (b[1].indexOf('.webp', b[1].length - 5) !== -1) { - return 1 - } - if (a[1].indexOf('.webp', a[1].length - 5) !== -1) { - return -1 - } - } - return 0 - }) - let bestSelectedSrc = '' - let tmpOption - const resultCount = result.length - - for (let i = 0; i < resultCount; i++) { - tmpOption = result[i] - if (tmpOption[0] >= containerWidth) { - bestSelectedSrc = tmpOption[1] - break - } + let options = el.getAttribute('data-srcset') + const result = [] + const container = el.parentNode + const containerWidth = container.offsetWidth * scale + + let spaceIndex + let tmpSrc + let tmpWidth + + options = options.trim().split(',') + + options.map(item => { + item = item.trim() + spaceIndex = item.lastIndexOf(' ') + if (spaceIndex === -1) { + tmpSrc = item + tmpWidth = 999998 + } else { + tmpSrc = item.substr(0, spaceIndex) + tmpWidth = parseInt(item.substr(spaceIndex + 1, item.length - spaceIndex - 2), 10) + } + result.push([tmpWidth, tmpSrc]) + }) + + result.sort(function (a, b) { + if (a[0] < b[0]) { + return -1 + } + if (a[0] > b[0]) { + return 1 + } + if (a[0] === b[0]) { + if (b[1].indexOf('.webp', b[1].length - 5) !== -1) { + return 1 + } + if (a[1].indexOf('.webp', a[1].length - 5) !== -1) { + return -1 + } + } + return 0 + }) + let bestSelectedSrc = '' + let tmpOption + const resultCount = result.length + + for (let i = 0; i < resultCount; i++) { + tmpOption = result[i] + if (tmpOption[0] >= containerWidth) { + bestSelectedSrc = tmpOption[1] + break } + } - return bestSelectedSrc + return bestSelectedSrc } function find (arr, fn) { - let item - for (let i = 0, len = arr.length; i < len; i++) { - if (fn(arr[i])) { - item = arr[i] - break - } + let item + for (let i = 0, len = arr.length; i < len; i++) { + if (fn(arr[i])) { + item = arr[i] + break } - return item + } + return item } const getDPR = (scale = 1) => inBrowser && window.devicePixelRatio || scale function supportWebp () { - if (!inBrowser) return false - - let support = true - const d = document - - try { - let el = d.createElement('object') - el.type = 'image/webp' - el.style.visibility = 'hidden' - el.innerHTML = '!' - d.body.appendChild(el) - support = !el.offsetWidth - d.body.removeChild(el) - } catch (err) { - support = false - } - - return support + if (!inBrowser) return false + + let support = true + const d = document + + try { + let el = d.createElement('object') + el.type = 'image/webp' + el.style.visibility = 'hidden' + el.innerHTML = '!' + d.body.appendChild(el) + support = !el.offsetWidth + d.body.removeChild(el) + } catch (err) { + support = false + } + + return support } function throttle (action, delay) { - let timeout = null - let lastRun = 0 - return function () { - if (timeout) { - return - } - let elapsed = Date.now() - lastRun - let context = this - let args = arguments - let runCallback = function () { - lastRun = Date.now() - timeout = false - action.apply(context, args) - } - if (elapsed >= delay) { - runCallback() - } - else { - timeout = setTimeout(runCallback, delay) - } + let timeout = null + let lastRun = 0 + return function () { + if (timeout) { + return + } + let elapsed = Date.now() - lastRun + let context = this + let args = arguments + let runCallback = function () { + lastRun = Date.now() + timeout = false + action.apply(context, args) + } + if (elapsed >= delay) { + runCallback() + } else { + timeout = setTimeout(runCallback, delay) } + } } function testSupportsPassive () { - if (!inBrowser) return - let support = false - try { - let opts = Object.defineProperty({}, 'passive', { - get: function() { - support = true - } - }) - window.addEventListener("test", null, opts) - } catch (e) {} - return support + if (!inBrowser) return + let support = false + try { + let opts = Object.defineProperty({}, 'passive', { + get: function () { + support = true + } + }) + window.addEventListener('test', null, opts) + } catch (e) {} + return support } const supportsPassive = testSupportsPassive() const _ = { - on (el, type, func, capture = false) { - if (supportsPassive) { - el.addEventListener(type, func, { - capture: capture, - passive: true - }) - } else { - el.addEventListener(type, func, capture) - } - }, - off (el, type, func, capture = false) { - el.removeEventListener(type, func, capture) + on (el, type, func, capture = false) { + if (supportsPassive) { + el.addEventListener(type, func, { + capture: capture, + passive: true + }) + } else { + el.addEventListener(type, func, capture) } + }, + off (el, type, func, capture = false) { + el.removeEventListener(type, func, capture) + } } const loadImageAsync = (item, resolve, reject) => { - let image = new Image() - image.src = item.src - - image.onload = function () { - resolve({ - naturalHeight: image.naturalHeight, - naturalWidth: image.naturalWidth, - src: image.src - }) - } + let image = new Image() + image.src = item.src + + image.onload = function () { + resolve({ + naturalHeight: image.naturalHeight, + naturalWidth: image.naturalWidth, + src: image.src + }) + } - image.onerror = function (e) { - reject(e) - } + image.onerror = function (e) { + reject(e) + } } const style = (el, prop) => { - return typeof getComputedStyle !== 'undefined' + return typeof getComputedStyle !== 'undefined' ? getComputedStyle(el, null).getPropertyValue(prop) : el.style[prop] } const overflow = (el) => { - return style(el, 'overflow') + style(el, 'overflow-y') + style(el, 'overflow-x') + return style(el, 'overflow') + style(el, 'overflow-y') + style(el, 'overflow-x') } const scrollParent = (el) => { - if (!inBrowser) return - if (!(el instanceof HTMLElement)) { - return window - } - - let parent = el + if (!inBrowser) return + if (!(el instanceof HTMLElement)) { + return window + } - while (parent) { - if (parent === document.body || parent === document.documentElement) { - break - } + let parent = el - if (!parent.parentNode) { - break - } + while (parent) { + if (parent === document.body || parent === document.documentElement) { + break + } - if (/(scroll|auto)/.test(overflow(parent))) { - return parent - } + if (!parent.parentNode) { + break + } - parent = parent.parentNode + if (/(scroll|auto)/.test(overflow(parent))) { + return parent } - return window + parent = parent.parentNode + } + + return window } function isObject (obj) { @@ -241,33 +254,44 @@ function isObject (obj) { } function ObjectKeys (obj) { - if (!(obj instanceof Object)) return [] - if (Object.keys) { - return Object.keys(obj) - } else { - let keys = [] - for (let key in obj) { - if (obj.hasOwnProperty(key)) { - keys.push(key) - } - } - return keys + if (!(obj instanceof Object)) return [] + if (Object.keys) { + return Object.keys(obj) + } else { + let keys = [] + for (let key in obj) { + if (obj.hasOwnProperty(key)) { + keys.push(key) + } } + return keys + } +} + +function ArrayFrom (arrLike) { + let len = arrLike.length + const list = [] + for (let i = 0; i < len; i++) { + list.push(arrLike[i]) + } + return list } export { - inBrowser, - remove, - some, - find, - assign, - _, - isObject, - throttle, - supportWebp, - getDPR, - scrollParent, - loadImageAsync, - getBestSelectionFromSrcset, - ObjectKeys + inBrowser, + CustomEvent, + remove, + some, + find, + assign, + ArrayFrom, + _, + isObject, + throttle, + supportWebp, + getDPR, + scrollParent, + loadImageAsync, + getBestSelectionFromSrcset, + ObjectKeys } diff --git a/test/tests.js b/test/test-spec.js similarity index 55% rename from test/tests.js rename to test/test-spec.js index a0a05d8..8496651 100644 --- a/test/tests.js +++ b/test/test-spec.js @@ -1,19 +1,15 @@ -'use strict' -const it = require('mocha').it -const chai = require('chai') -const expect = require('chai').expect -const lazyload = require('../lib') -const Vue = require('vue') +import Vue from 'vue' +import VueLazyload from '../src' +import genLazyCore from '../src/lazy' describe('VueLazyload.js Test Suite', function () { it('install', function () { - Vue.use(lazyload) + Vue.use(VueLazyload) const vm = new Vue() - expect(vm.$Lazyload, 'has $Lazyload') + assert(vm.$Lazyload, 'has $Lazyload') }) it('_valueFormatter', function () { - const genLazyCore = require('../lib/lazy').default const LazyCore = genLazyCore(Vue) const lazyload = new LazyCore({ @@ -43,19 +39,4 @@ describe('VueLazyload.js Test Suite', function () { loading: 'loading', }).loading).to.equal('loading') }) - - // it('add and remove TargetListener', function () { - // Vue.use(lazyload) - // const vm = new Vue() - - // const list = Array.from({ length: 10 }).map((v, i) => { - // return { i, addEventListener () {}, removeEventListener () {} } - // }) - - // list.map(el => vm.$Lazyload._addListenerTarget(el)) - - // list.map(el => vm.$Lazyload._removeListenerTarget(el)) - - // expect(vm.$Lazyload.TargetQueue.length).to.equal(0) - // }) }) diff --git a/vue-lazyload.js b/vue-lazyload.js index 9d76411..9888769 100644 --- a/vue-lazyload.js +++ b/vue-lazyload.js @@ -1,6 +1,6 @@ /*! - * Vue-Lazyload.js v1.1.4 + * Vue-Lazyload.js v1.2.0 * (c) 2018 Awe * Released under the MIT License. */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.VueLazyload=t()}(this,function(){"use strict";var e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},t=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")},n=function(){function e(e,t){for(var n=0;n-1?e.splice(n,1):void 0}}function a(e,t){if(!e||!t)return e||{};if(e instanceof Object)for(var n in t)e[n]=t[n];return e}function u(e,t){if("IMG"===e.tagName&&e.getAttribute("data-srcset")){var n=e.getAttribute("data-srcset"),i=[],r=e.parentNode.offsetWidth*t,o=void 0,s=void 0,a=void 0;(n=n.trim().split(",")).map(function(e){e=e.trim(),-1===(o=e.lastIndexOf(" "))?(s=e,a=999998):(s=e.substr(0,o),a=parseInt(e.substr(o+1,e.length-o-2),10)),i.push([a,s])}),i.sort(function(e,t){if(e[0]t[0])return 1;if(e[0]===t[0]){if(-1!==t[1].indexOf(".webp",t[1].length-5))return 1;if(-1!==e[1].indexOf(".webp",e[1].length-5))return-1}return 0});for(var u="",l=void 0,d=i.length,h=0;h=r){u=l[1];break}return u}}function l(e,t){for(var n=void 0,i=0,r=e.length;i0&&void 0!==arguments[0]?arguments[0]:1;return i&&window.devicePixelRatio||e};var h=function(){if(i){var e=!1;try{var t=Object.defineProperty({},"passive",{get:function(){e=!0}});window.addEventListener("test",null,t)}catch(e){}return e}}(),c={on:function(e,t,n){var i=arguments.length>3&&void 0!==arguments[3]&&arguments[3];h?e.addEventListener(t,n,{capture:i,passive:!0}):e.addEventListener(t,n,i)},off:function(e,t,n){var i=arguments.length>3&&void 0!==arguments[3]&&arguments[3];e.removeEventListener(t,n,i)}},f=function(e,t,n){var i=new Image;i.src=e.src,i.onload=function(){t({naturalHeight:i.naturalHeight,naturalWidth:i.naturalWidth,src:i.src})},i.onerror=function(e){n(e)}},v=function(e,t){return"undefined"!=typeof getComputedStyle?getComputedStyle(e,null).getPropertyValue(t):e.style[t]},p=function(e){if(i){if(!(e instanceof HTMLElement))return window;for(var t,n=e;n&&n!==document.body&&n!==document.documentElement&&n.parentNode;){if(/(scroll|auto)/.test(v(t=n,"overflow")+v(t,"overflow-y")+v(t,"overflow-x")))return n;n=n.parentNode}return window}};var g={},y=function(){function e(n){var i=n.el,r=n.src,o=n.error,s=n.loading,a=n.bindType,u=n.$parent,l=n.options,d=n.elRenderer;t(this,e),this.el=i,this.src=r,this.error=o,this.loading=s,this.bindType=a,this.attempt=0,this.naturalHeight=0,this.naturalWidth=0,this.options=l,this.filter(),this.initState(),this.performanceData={init:Date.now(),loadStart:null,loadEnd:null},this.rect=i.getBoundingClientRect(),this.$parent=u,this.elRenderer=d,this.render("loading",!1)}return n(e,[{key:"initState",value:function(){this.state={error:!1,loaded:!1,rendered:!1}}},{key:"record",value:function(e){this.performanceData[e]=Date.now()}},{key:"update",value:function(e){var t=e.src,n=e.loading,i=e.error,r=this.src;this.src=t,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 e=this;(function(e){if(!(e instanceof Object))return[];if(Object.keys)return Object.keys(e);var t=[];for(var n in e)e.hasOwnProperty(n)&&t.push(n);return t})(this.options.filter).map(function(t){e.options.filter[t](e,e.options)})}},{key:"renderLoading",value:function(e){var t=this;f({src:this.loading},function(n){t.render("loading",!1),e()},function(n){e(),t.options.silent||console.warn("VueLazyload log: load failed with loading image("+t.loading+")")})}},{key:"load",value:function(e){var t=this;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.loaded||g[this.src]?(this.state.loaded=!0,e(),this.render("loaded",!0)):void this.renderLoading(function(){t.attempt++,t.record("loadStart"),f({src:t.src},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),g[t.src]=1,e()},function(e){t.state.error=!0,t.state.loaded=!1,t.render("error",!1)})})}},{key:"render",value:function(e,t){this.elRenderer(this,e,t)}},{key:"performance",value:function(){var e="loading",t=0;return this.state.loaded&&(e="loaded",t=(this.performanceData.loadEnd-this.performanceData.loadStart)/1e3),this.state.error&&(e="error"),{src:this.src,state:e,time:t}}},{key:"destroy",value:function(){this.el=null,this.src=null,this.error=null,this.loading=null,this.bindType=null,this.attempt=0}}]),e}(),b="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",m=["scroll","wheel","mousewheel","resize","animationend","transitionend","touchmove"],L={rootMargin:"0px",threshold:0};function w(h){return function(){function f(e){var n,r,s,a,u=e.preLoad,l=e.error,h=e.throttleWait,c=e.preLoadTop,v=e.dispatchEvent,p=e.loading,g=e.attempt,y=e.silent,w=void 0===y||y,_=e.scale,k=e.listenEvents,E=(e.hasbind,e.filter),A=e.adapter,T=e.observer,$=e.observerOptions;t(this,f),this.version="1.1.4",this.mode=o.event,this.ListenerQueue=[],this.TargetIndex=0,this.TargetQueue=[],this.options={silent:w,dispatchEvent:!!v,throttleWait:h||200,preLoad:u||1.3,preLoadTop:c||0,error:l||b,loading:p||b,attempt:g||3,scale:_||d(_),ListenEvents:k||m,hasbind:!1,supportWebp:function(){if(!i)return!1;var e=!0,t=document;try{var n=t.createElement("object");n.type="image/webp",n.style.visibility="hidden",n.innerHTML="!",t.body.appendChild(n),e=!n.offsetWidth,t.body.removeChild(n)}catch(t){e=!1}return e}(),filter:E||{},adapter:A||{},observer:!!T,observerOptions:$||L},this._initEvent(),this.lazyLoadHandler=(n=this._lazyLoadHandler.bind(this),r=this.options.throttleWait,s=null,a=0,function(){if(!s){var e=this,t=arguments,i=function(){a=Date.now(),s=!1,n.apply(e,t)};Date.now()-a>=r?i():s=setTimeout(i,r)}}),this.setMode(this.options.observer?o.observer:o.event)}return n(f,[{key:"config",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};a(this.options,e)}},{key:"performance",value:function(){var e=[];return this.ListenerQueue.map(function(t){e.push(t.performance())}),e}},{key:"addLazyBox",value:function(e){this.ListenerQueue.push(e),i&&(this._addListenerTarget(window),this._observer&&this._observer.observe(e.el),e.$el&&e.$el.parentNode&&this._addListenerTarget(e.$el.parentNode))}},{key:"add",value:function(e,t,n){var r=this;if(function(e,t){for(var n=!1,i=0,r=e.length;i1&&void 0!==arguments[1]?arguments[1]:{},r=new(w(e))(n),o="2"===e.version.split(".")[0];e.prototype.$Lazyload=r,n.lazyComponent&&e.component("lazy-component",(t=r,{props:{tag:{type:String,default:"div"}},render:function(e){return!1===this.show?e(this.tag):e(this.tag,null,this.$slots.default)},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(),i&&this.rect.top0&&this.rect.left0},load:function(){this.show=!0,this.state.loaded=!0,this.$emit("show",this)}}})),o?e.directive("lazy",{bind:r.add.bind(r),update:r.update.bind(r),componentUpdated:r.lazyLoadHandler.bind(r),unbind:r.remove.bind(r)}):e.directive("lazy",{bind:r.lazyLoadHandler.bind(r),update:function(e,t){a(this.vm.$refs,this.vm.$els),r.add(this.el,{modifiers:this.modifiers||{},arg:this.arg,value:e,oldValue:t},{context:this.vm})},unbind:function(){r.remove(this.el)}})}}}); +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.VueLazyload=t()}(this,function(){"use strict";function e(e,t){if(e.length){var n=e.indexOf(t);return n>-1?e.splice(n,1):void 0}}function t(e,t){if(!e||!t)return e||{};if(e instanceof Object)for(var n in t)e[n]=t[n];return e}function n(e,t){for(var n=!1,i=0,r=e.length;it[0])return 1;if(e[0]===t[0]){if(-1!==t[1].indexOf(".webp",t[1].length-5))return 1;if(-1!==e[1].indexOf(".webp",e[1].length-5))return-1}return 0});for(var l="",d=void 0,h=i.length,c=0;c=o){l=d[1];break}return l}}function r(e,t){for(var n=void 0,i=0,r=e.length;i=t?a():n=setTimeout(a,t)}}}function a(e){return null!==e&&"object"===(void 0===e?"undefined":d(e))}function u(e){if(!(e instanceof Object))return[];if(Object.keys)return Object.keys(e);var t=[];for(var n in e)e.hasOwnProperty(n)&&t.push(n);return t}function l(e){for(var t=e.length,n=[],i=0;i0&&void 0!==arguments[0]?arguments[0]:1;return f&&window.devicePixelRatio||e},g=function(){if(f){var e=!1;try{var t=Object.defineProperty({},"passive",{get:function(){e=!0}});window.addEventListener("test",null,t)}catch(e){}return e}}(),m={on:function(e,t,n){var i=arguments.length>3&&void 0!==arguments[3]&&arguments[3];g?e.addEventListener(t,n,{capture:i,passive:!0}):e.addEventListener(t,n,i)},off:function(e,t,n){var i=arguments.length>3&&void 0!==arguments[3]&&arguments[3];e.removeEventListener(t,n,i)}},L=function(e,t,n){var i=new Image;i.src=e.src,i.onload=function(){t({naturalHeight:i.naturalHeight,naturalWidth:i.naturalWidth,src:i.src})},i.onerror=function(e){n(e)}},w=function(e,t){return"undefined"!=typeof getComputedStyle?getComputedStyle(e,null).getPropertyValue(t):e.style[t]},_=function(e){return w(e,"overflow")+w(e,"overflow-y")+w(e,"overflow-x")},k=function(e){if(f){if(!(e instanceof HTMLElement))return window;for(var t=e;t&&t!==document.body&&t!==document.documentElement&&t.parentNode;){if(/(scroll|auto)/.test(_(t)))return t;t=t.parentNode}return window}},E={},z=function(){function e(t){var n=t.el,i=t.src,r=t.error,o=t.loading,s=t.bindType,a=t.$parent,u=t.options,l=t.elRenderer;h(this,e),this.el=n,this.src=i,this.error=r,this.loading=o,this.bindType=s,this.attempt=0,this.naturalHeight=0,this.naturalWidth=0,this.options=u,this.rect=null,this.$parent=a,this.elRenderer=l,this.performanceData={init:Date.now(),loadStart:0,loadEnd:0},this.filter(),this.initState(),this.render("loading",!1)}return c(e,[{key:"initState",value:function(){this.el.dataset.src=this.src,this.state={error:!1,loaded:!1,rendered:!1}}},{key:"record",value:function(e){this.performanceData[e]=Date.now()}},{key:"update",value:function(e){var t=e.src,n=e.loading,i=e.error,r=this.src;this.src=t,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 e=this;u(this.options.filter).map(function(t){e.options.filter[t](e,e.options)})}},{key:"renderLoading",value:function(e){var t=this;L({src:this.loading},function(n){t.render("loading",!1),e()},function(){e(),t.options.silent||console.warn("VueLazyload log: load failed with loading image("+t.loading+")")})}},{key:"load",value:function(e){var t=this;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.loaded||E[this.src]?(this.state.loaded=!0,e(),this.render("loaded",!0)):void this.renderLoading(function(){t.attempt++,t.record("loadStart"),L({src:t.src},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),E[t.src]=1,e()},function(e){console.error(e),t.state.error=!0,t.state.loaded=!1,t.render("error",!1)})})}},{key:"render",value:function(e,t){this.elRenderer(this,e,t)}},{key:"performance",value:function(){var e="loading",t=0;return this.state.loaded&&(e="loaded",t=(this.performanceData.loadEnd-this.performanceData.loadStart)/1e3),this.state.error&&(e="error"),{src:this.src,state:e,time:t}}},{key:"destroy",value:function(){this.el=null,this.src=null,this.error=null,this.loading=null,this.bindType=null,this.attempt=0}}]),e}(),A="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",T=["scroll","wheel","mousewheel","resize","animationend","transitionend","touchmove"],$={rootMargin:"0px",threshold:0},H=function(u){return function(){function l(e){var t=e.preLoad,n=e.error,i=e.throttleWait,r=e.preLoadTop,a=e.dispatchEvent,u=e.loading,d=e.attempt,c=e.silent,f=void 0===c||c,v=e.scale,b=e.listenEvents,g=(e.hasbind,e.filter),m=e.adapter,L=e.observer,w=e.observerOptions;h(this,l),this.version="1.2.0",this.mode=p.event,this.ListenerQueue=[],this.TargetIndex=0,this.TargetQueue=[],this.options={silent:f,dispatchEvent:!!a,throttleWait:i||200,preLoad:t||1.3,preLoadTop:r||0,error:n||A,loading:u||A,attempt:d||3,scale:v||y(v),ListenEvents:b||T,hasbind:!1,supportWebp:o(),filter:g||{},adapter:m||{},observer:!!L,observerOptions:w||$},this._initEvent(),this.lazyLoadHandler=s(this._lazyLoadHandler.bind(this),this.options.throttleWait),this.setMode(this.options.observer?p.observer:p.event)}return c(l,[{key:"config",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};t(this.options,e)}},{key:"performance",value:function(){var e=[];return this.ListenerQueue.map(function(t){e.push(t.performance())}),e}},{key:"addLazyBox",value:function(e){this.ListenerQueue.push(e),f&&(this._addListenerTarget(window),this._observer&&this._observer.observe(e.el),e.$el&&e.$el.parentNode&&this._addListenerTarget(e.$el.parentNode))}},{key:"add",value:function(e,t,r){var o=this;if(n(this.ListenerQueue,function(t){return t.el===e}))return this.update(e,t),u.nextTick(this.lazyLoadHandler);var s=this._valueFormatter(t.value),a=s.src,l=s.loading,d=s.error;u.nextTick(function(){a=i(e,o.options.scale)||a,o._observer&&o._observer.observe(e);var n=Object.keys(t.modifiers)[0],s=void 0;n&&(s=r.context.$refs[n],s=s?s.$el||s:document.getElementById(n)),s||(s=k(e));var h=new z({bindType:t.arg,$parent:s,el:e,loading:l,error:d,src:a,elRenderer:o._elRenderer.bind(o),options:o.options});o.ListenerQueue.push(h),f&&(o._addListenerTarget(window),o._addListenerTarget(s)),o.lazyLoadHandler(),u.nextTick(function(){return o.lazyLoadHandler()})})}},{key:"update",value:function(e,t){var n=this,o=this._valueFormatter(t.value),s=o.src,a=o.loading,l=o.error;s=i(e,this.options.scale)||s;var d=r(this.ListenerQueue,function(t){return t.el===e});d&&d.update({src:s,loading:a,error:l}),this._observer&&this._observer.observe(e),this.lazyLoadHandler(),u.nextTick(function(){return n.lazyLoadHandler()})}},{key:"remove",value:function(t){if(t){this._observer&&this._observer.unobserve(t);var n=r(this.ListenerQueue,function(e){return e.el===t});n&&(this._removeListenerTarget(n.$parent),this._removeListenerTarget(window),e(this.ListenerQueue,n)&&n.destroy())}}},{key:"removeComponent",value:function(t){t&&(e(this.ListenerQueue,t),this._observer&&this._observer.unobserve(t.el),t.$parent&&t.$el.parentNode&&this._removeListenerTarget(t.$el.parentNode),this._removeListenerTarget(window))}},{key:"setMode",value:function(e){var t=this;v||e!==p.observer||(e=p.event),this.mode=e,e===p.event?(this._observer&&(this.ListenerQueue.forEach(function(e){t._observer.unobserve(e.el)}),this._observer=null),this.TargetQueue.forEach(function(e){t._initListen(e.el,!0)})):(this.TargetQueue.forEach(function(e){t._initListen(e.el,!1)}),this._initIntersectionObserver())}},{key:"_addListenerTarget",value:function(e){if(e){var t=r(this.TargetQueue,function(t){return t.el===e});return t?t.childrenCount++:(t={el:e,id:++this.TargetIndex,childrenCount:1,listened:!0},this.mode===p.event&&this._initListen(t.el,!0),this.TargetQueue.push(t)),this.TargetIndex}}},{key:"_removeListenerTarget",value:function(e){var t=this;this.TargetQueue.forEach(function(n,i){n.el===e&&(--n.childrenCount||(t._initListen(n.el,!1),t.TargetQueue.splice(i,1),n=null))})}},{key:"_initListen",value:function(e,t){var n=this;this.options.ListenEvents.forEach(function(i){return m[t?"on":"off"](e,i,n.lazyLoadHandler)})}},{key:"_initEvent",value:function(){var t=this;this.Event={listeners:{loading:[],loaded:[],error:[]}},this.$on=function(e,n){t.Event.listeners[e].push(n)},this.$once=function(e,n){function i(){r.$off(e,i),n.apply(r,arguments)}var r=t;t.$on(e,i)},this.$off=function(n,i){if(!i)return void(t.Event.listeners[n]=[]);e(t.Event.listeners[n],i)},this.$emit=function(e,n,i){t.Event.listeners[e].forEach(function(e){return e(n,i)})}}},{key:"_lazyLoadHandler",value:function(){var e=this,t=!1;this.ListenerQueue.forEach(function(n,i){n.state.loaded||(t=n.checkInView())&&n.load(function(){!n.error&&n.loaded&&e.ListenerQueue.splice(i,1)})})}},{key:"_initIntersectionObserver",value:function(){var e=this;v&&(this._observer=new IntersectionObserver(this._observerHandler.bind(this),this.options.observerOptions),this.ListenerQueue.length&&this.ListenerQueue.forEach(function(t){e._observer.observe(t.el)}))}},{key:"_observerHandler",value:function(e,t){var n=this;e.forEach(function(e){e.isIntersecting&&n.ListenerQueue.forEach(function(t){if(t.el===e.target){if(t.state.loaded)return n._observer.unobserve(t.el);t.load()}})})}},{key:"_elRenderer",value:function(e,t,n){if(e.el){var i=e.el,r=e.bindType,o=void 0;switch(t){case"loading":o=e.loading;break;case"error":o=e.error;break;default:o=e.src}if(r?i.style[r]="url("+o+")":i.getAttribute("src")!==o&&i.setAttribute("src",o),i.setAttribute("lazy",t),this.$emit(t,e,n),this.options.adapter[t]&&this.options.adapter[t](e,this.options),this.options.dispatchEvent){var s=new b(t,{detail:e});i.dispatchEvent(s)}}}},{key:"_valueFormatter",value:function(e){var t=e,n=this.options.loading,i=this.options.error;return a(e)&&(e.src||this.options.silent||console.error("Vue Lazyload warning: miss src with "+e),t=e.src,n=e.loading||this.options.loading,i=e.error||this.options.error),{src:t,loading:n,error:i}}}]),l}()},O=function(e){return{props:{tag:{type:String,default:"div"}},render:function(e){return!1===this.show?e(this.tag):e(this.tag,null,this.$slots.default)},data:function(){return{el:null,state:{loaded:!1},rect:{},show:!1}},mounted:function(){this.el=this.$el,e.addLazyBox(this),e.lazyLoadHandler()},beforeDestroy:function(){e.removeComponent(this)},methods:{getRect:function(){this.rect=this.$el.getBoundingClientRect()},checkInView:function(){return this.getRect(),f&&this.rect.top0&&this.rect.left0},load:function(){this.show=!0,this.state.loaded=!0,this.$emit("show",this)}}}},Q=function(){function t(e){var n=e.lazy;h(this,t),this.lazy=n,n.lazyContainerMananger=this,this._queue=[]}return c(t,[{key:"bind",value:function(e,t,n){var i=new x({el:e,binding:t,vnode:n,lazy:this.lazy});this._queue.push(i)}},{key:"update",value:function(e,t,n){var i=r(this._queue,function(t){return t.el===e});i&&i.update({el:e,binding:t,vnode:n})}},{key:"unbind",value:function(t,n,i){var o=r(this._queue,function(e){return e.el===t});o&&(o.clear(),e(this._queue,o))}}]),t}(),I={selector:"img"},x=function(){function e(t){var n=t.el,i=t.binding,r=t.vnode,o=t.lazy;h(this,e),this.el=null,this.vnode=r,this.binding=i,this.options={},this.lazy=o,this._queue=[],this.update({el:n,binding:i})}return c(e,[{key:"update",value:function(e){var n=this,i=e.el,r=e.binding;this.el=i,this.options=t({},I,r.value),this.getImgs().forEach(function(e){n.lazy.add(e,Object.assign({},n.binding,{value:{src:e.getAttribute("data-src")}}),n.vnode)})}},{key:"getImgs",value:function(){return l(this.el.querySelectorAll(this.options.selector))}},{key:"clear",value:function(){var e=this;this.getImgs().forEach(function(t){return e.lazy.remove(t)}),this.vnode=null,this.binding=null,this.lazy=null}}]),e}();return{install:function(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=H(e),r=new i(n),o=new Q({lazy:r}),s="2"===e.version.split(".")[0];e.prototype.$Lazyload=r,n.lazyComponent&&e.component("lazy-component",O(r)),s?(e.directive("lazy",{bind:r.add.bind(r),update:r.update.bind(r),componentUpdated:r.lazyLoadHandler.bind(r),unbind:r.remove.bind(r)}),e.directive("lazy-container",{bind:o.bind.bind(o),update:o.update.bind(o),unbind:o.unbind.bind(o)})):(e.directive("lazy",{bind:r.lazyLoadHandler.bind(r),update:function(e,n){t(this.vm.$refs,this.vm.$els),r.add(this.el,{modifiers:this.modifiers||{},arg:this.arg,value:e,oldValue:n},{context:this.vm})},unbind:function(){r.remove(this.el)}}),e.directive("lazy-container",{update:function(e,t){o.update(this.el,{modifiers:this.modifiers||{},arg:this.arg,value:e,oldValue:t},{context:this.vm})},unbind:function(){o.unbind(this.el)}}))}}});