function createCommonjsModule(fn, module) {
  return (module = { exports: {} }), fn(module, module.exports), module.exports
}

var assignSymbols$1 = createCommonjsModule(function (module) {
  const toString = Object.prototype.toString
  const isEnumerable = Object.prototype.propertyIsEnumerable
  const getSymbols = Object.getOwnPropertySymbols

  module.exports = (target, ...args) => {
    if (!isObject(target)) {
      throw new TypeError('expected the first argument to be an object')
    }

    if (args.length === 0 || typeof Symbol !== 'function' || typeof getSymbols !== 'function') {
      return target
    }

    for (let arg of args) {
      let names = getSymbols(arg)

      for (let key of names) {
        if (isEnumerable.call(arg, key)) {
          target[key] = arg[key]
        }
      }
    }
    return target
  }

  function isObject(val) {
    return typeof val === 'function' || toString.call(val) === '[object Object]' || Array.isArray(val)
  }
})

var assignSymbols$2 = /*#__PURE__*/ Object.freeze({
  __proto__: null,
  default: assignSymbols$1,
  __moduleExports: assignSymbols$1
})

var assignSymbols = (assignSymbols$2 && assignSymbols$1) || assignSymbols$2

var assignDeep = createCommonjsModule(function (module) {
  const toString = Object.prototype.toString

  const isValidKey = (key) => {
    return key !== '__proto__' && key !== 'constructor' && key !== 'prototype'
  }

  const assign = (module.exports = (target, ...args) => {
    let i = 0
    if (isPrimitive(target)) target = args[i++]
    if (!target) target = {}
    for (; i < args.length; i++) {
      if (isObject(args[i])) {
        for (const key of Object.keys(args[i])) {
          if (isValidKey(key)) {
            if (isObject(target[key]) && isObject(args[i][key])) {
              assign(target[key], args[i][key])
            } else {
              target[key] = args[i][key]
            }
          }
        }
        assignSymbols(target, args[i])
      }
    }
    return target
  })

  function isObject(val) {
    return typeof val === 'function' || toString.call(val) === '[object Object]'
  }

  function isPrimitive(val) {
    return typeof val === 'object' ? val === null : typeof val !== 'function'
  }
})

const inBrowser = typeof window !== 'undefined' && window !== null

const hasIntersectionObserver = checkIntersectionObserver()

function checkIntersectionObserver() {
  if (
    inBrowser &&
    'IntersectionObserver' in window &&
    'IntersectionObserverEntry' in window &&
    'intersectionRatio' in window.IntersectionObserverEntry.prototype
  ) {
    // Minimal polyfill for Edge 15's lack of `isIntersecting`
    // See: https://github.com/w3c/IntersectionObserver/issues/211
    if (!('isIntersecting' in window.IntersectionObserverEntry.prototype)) {
      Object.defineProperty(window.IntersectionObserverEntry.prototype, 'isIntersecting', {
        get: function () {
          return this.intersectionRatio > 0
        }
      })
    }
    return true
  }
  return false
}

const modeType = {
  event: 'event',
  observer: 'observer'

  // CustomEvent polyfill for IE
}
const CustomEvent = (function () {
  if (!inBrowser) return
  // not IE
  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)
}

function some(arr, fn) {
  let has = false
  for (let i = 0, len = arr.length; i < len; i++) {
    if (fn(arr[i])) {
      has = true
      break
    }
  }
  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])
  })

  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

  for (let i = 0; i < result.length; i++) {
    tmpOption = result[i]
    bestSelectedSrc = tmpOption[1]
    const next = result[i + 1]
    if (next && next[0] < containerWidth) {
      bestSelectedSrc = tmpOption[1]
      break
    } else if (!next) {
      bestSelectedSrc = tmpOption[1]
      break
    }
  }

  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
    }
  }
  return item
}

const getDPR = (scale = 1) => (inBrowser ? window.devicePixelRatio || scale : scale)

function supportWebp() {
  if (!inBrowser) return false

  let support = true

  try {
    const elem = document.createElement('canvas')

    if (elem.getContext && elem.getContext('2d')) {
      support = elem.toDataURL('image/webp').indexOf('data:image/webp') === 0
    }
  } catch (err) {
    support = false
  }

  return support
}

function throttle(action, delay) {
  let timeout = null
  let movement = null
  let lastRun = 0
  let needRun = false
  return function () {
    needRun = true
    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)
    }
    if (needRun) {
      clearTimeout(movement)
      movement = setTimeout(runCallback, 2 * 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
}

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)
  }
}

const loadImageAsync = (item, resolve, reject) => {
  const attributes = item.el.attributes
  let image = new Image()
  if (!item || !item.src) {
    const err = new Error('image src is required')
    return reject(err)
  }
  // 将图片预配置的attributes赋值给image实例
  for (let i = 0; i < attributes.length; i++) {
    if (!['id', 'class', 'src'].includes(attributes[i].name)) {
      image.setAttribute(attributes[i].name, attributes[i].value)
    }
  }
  image.src = item.src
  if (item.cors) {
    image.crossOrigin = item.cors
  }

  image.onload = function () {
    resolve({
      naturalHeight: image.naturalHeight,
      naturalWidth: image.naturalWidth,
      src: image.src
    })
  }

  image.onerror = function (e) {
    reject(e)
  }
}

const style = (el, prop) => {
  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')
}

const scrollParent = (el) => {
  if (!inBrowser) return
  if (!(el instanceof HTMLElement)) {
    return window
  }

  let parent = el

  while (parent) {
    if (parent === document.body || parent === document.documentElement) {
      break
    }

    if (!parent.parentNode) {
      break
    }

    if (/(scroll|auto)/.test(overflow(parent))) {
      return parent
    }

    parent = parent.parentNode
  }

  return window
}

function isObject(obj) {
  return obj !== null && typeof obj === 'object'
}

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
  }
}

function ArrayFrom(arrLike) {
  let len = arrLike.length
  const list = []
  for (let i = 0; i < len; i++) {
    list.push(arrLike[i])
  }
  return list
}

function noop() {}

class ImageCache {
  constructor({ max }) {
    this.options = {
      max: max || 100
    }
    this._caches = []
  }

  has(key) {
    return this._caches.indexOf(key) > -1
  }

  add(key) {
    if (this.has(key)) return
    this._caches.push(key)
    if (this._caches.length > this.options.max) {
      this.free()
    }
  }

  free() {
    this._caches.shift()
  }
}

// el: {
//     state,
//     src,
//     error,
//     loading
// }

class ReactiveListener {
  constructor({ el, src, error, loading, bindType, $parent, options, cors, elRenderer, imageCache }) {
    this.el = el
    this.src = src
    this.error = error
    this.loading = loading
    this.bindType = bindType
    this.attempt = 0
    this.cors = cors

    this.naturalHeight = 0
    this.naturalWidth = 0

    this.options = options

    this.rect = null

    this.$parent = $parent
    this.elRenderer = elRenderer
    this._imageCache = imageCache
    this.performanceData = {
      init: Date.now(),
      loadStart: 0,
      loadEnd: 0
    }

    this.filter()
    this.initState()
    this.render('loading', false)
  }

  /*
   * init listener state
   * @return
   */
  initState() {
    if ('dataset' in this.el) {
      this.el.dataset.src = this.src
    } else {
      this.el.setAttribute('data-src', this.src)
    }

    this.state = {
      loading: false,
      error: false,
      loaded: false,
      rendered: false
    }
  }

  /*
   * 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()
    }
  }

  /*
   * 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) {
    this.state.loading = true
    loadImageAsync(
      {
        src: this.loading,
        cors: this.cors,
        el: this.el
      },
      (data) => {
        this.render('loading', false)
        this.state.loading = false
        cb()
      },
      () => {
        // handler `loading image` load failed
        cb()
        this.state.loading = false
        if (!this.options.silent) {
          console.warn(`VueLazyload log: load failed with loading image(${this.loading})`)
        }
      }
    )
  }

  /*
   * try load image and  render it
   * @return
   */
  load(onFinish = noop) {
    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.rendered && this.state.loaded) return
    if (this._imageCache.has(this.src)) {
      this.state.loaded = true
      this.render('loaded', true)
      this.state.rendered = true
      return onFinish()
    }

    this.renderLoading(() => {
      this.attempt++

      this.options.adapter['beforeLoad'] && this.options.adapter['beforeLoad'](this, this.options)
      this.record('loadStart')

      loadImageAsync(
        {
          src: this.src,
          cors: this.cors,
          el: this.el
        },
        (data) => {
          this.naturalHeight = data.naturalHeight
          this.naturalWidth = data.naturalWidth
          this.state.loaded = true
          this.state.error = false
          this.record('loadEnd')
          this.render('loaded', false)
          this.state.rendered = true
          this._imageCache.add(this.src)
          onFinish()
        },
        (err) => {
          !this.options.silent && 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
    }

    if (this.state.error) state = 'error'

    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
  }
}

const DEFAULT_URL = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
const DEFAULT_EVENTS = ['scroll', 'wheel', 'mousewheel', 'resize', 'animationend', 'transitionend', 'touchmove']
const DEFAULT_OBSERVER_OPTIONS = {
  rootMargin: '0px',
  threshold: 0
}

function Lazy(Vue) {
  return class Lazy {
    constructor({
      preLoad,
      error,
      throttleWait,
      preLoadTop,
      dispatchEvent,
      loading,
      attempt,
      silent = true,
      scale,
      listenEvents,
      hasbind,
      filter,
      adapter,
      observer,
      observerOptions
    }) {
      this.version = '"1.3.4"'
      this.mode = modeType.event
      this.ListenerQueue = []
      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
      }
      this._initEvent()
      this._imageCache = new ImageCache({ max: 200 })
      this.lazyLoadHandler = throttle(this._lazyLoadHandler.bind(this), this.options.throttleWait)

      this.setMode(this.options.observer ? modeType.observer : modeType.event)
    }

    /**
     * update config
     * @param  {Object} config params
     * @return
     */
    config(options = {}) {
      assignDeep(this.options, options)
    }

    /**
     * output listener's load performance
     * @return {Array}
     */
    performance() {
      let list = []

      this.ListenerQueue.map((item) => {
        list.push(item.performance())
      })

      return list
    }

    /*
     * 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)
        }
      }
    }

    /*
     * 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)
      }

      let { src, loading, error, cors } = 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

        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)
        }

        if (!$parent) {
          $parent = scrollParent(el)
        }

        const newListener = new ReactiveListener({
          bindType: binding.arg,
          $parent,
          el,
          loading,
          error,
          src,
          cors,
          elRenderer: this._elRenderer.bind(this),
          options: this.options,
          imageCache: this._imageCache
        })

        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(el, binding, vnode) {
      let { src, loading, error } = this._valueFormatter(binding.value)
      src = getBestSelectionFromSrcset(el, this.options.scale) || src

      const exist = find(this.ListenerQueue, (item) => item.el === el)
      if (!exist) {
        this.add(el, binding, vnode)
      } else {
        exist.update({
          src,
          loading,
          error
        })
      }
      if (this._observer) {
        this._observer.unobserve(el)
        this._observer.observe(el)
      }
      this.lazyLoadHandler()
      Vue.nextTick(() => this.lazyLoadHandler())
    }

    /**
     * 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()
      }
    }

    /*
     * 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(window)
    }

    setMode(mode) {
      if (!hasIntersectionObserver && mode === modeType.observer) {
        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()
      }
    }

    /*
     *** Private functions ***
     */

    /*
     * 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++
      }
      return this.TargetIndex
    }

    /*
     * 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
          }
        }
      })
    }

    /*
     * 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: []
        }
      }

      this.$on = (event, func) => {
        if (!this.Event.listeners[event]) this.Event.listeners[event] = []
        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)
      }

      this.$off = (event, func) => {
        if (!func) {
          if (!this.Event.listeners[event]) return
          this.Event.listeners[event].length = 0
          return
        }
        remove(this.Event.listeners[event], func)
      }

      this.$emit = (event, context, inCache) => {
        if (!this.Event.listeners[event]) return
        this.Event.listeners[event].forEach((func) => func(context, inCache))
      }
    }

    /**
     * find nodes which in viewport and trigger load
     * @return
     */
    _lazyLoadHandler() {
      const freeList = []
      this.ListenerQueue.forEach((listener, index) => {
        if (!listener.el || !listener.el.parentNode) {
          freeList.push(listener)
        }
        const catIn = listener.checkInView()
        if (!catIn) return
        listener.load()
      })
      freeList.forEach((item) => {
        remove(this.ListenerQueue, item)
        item.$destroy()
      })
    }
    /**
     * 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)
        })
      }
    }

    /**
     * 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()
            }
          })
        }
      })
    }

    /**
     * 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
      }

      if (bindType) {
        el.style[bindType] = 'url("' + src + '")'
      } else if (el.getAttribute('src') !== src) {
        el.setAttribute('src', src)
      }

      el.setAttribute('lazy', state)

      this.$emit(state, listener, cache)
      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)
      }
    }

    /**
     * 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
      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
      }
      return {
        src,
        loading,
        error
      }
    }
  }
}

Lazy.install = (Vue, options = {}) => {
  const LazyClass = Lazy(Vue)
  const lazy = new LazyClass(options)

  const isVue2 = Vue.version.split('.')[0] === '2'
  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)
    })
  } else {
    Vue.directive('lazy', {
      bind: lazy.lazyLoadHandler.bind(lazy),
      update(newValue, oldValue) {
        assignDeep(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)
      }
    })
  }
}

const LazyComponent = (lazy) => {
  return {
    props: {
      tag: {
        type: String,
        default: 'div'
      }
    },
    render(h) {
      return h(this.tag, null, this.show ? this.$slots.default : null)
    },
    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 &&
          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)
      },
      destroy() {
        return this.$destroy
      }
    }
  }
}

LazyComponent.install = function (Vue, options = {}) {
  const LazyClass = Lazy(Vue)
  const lazy = new LazyClass(options)
  Vue.component('lazy-component', LazyComponent(lazy))
}

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 = assignDeep({}, defaultOptions, binding.value)

    const imgs = this.getImgs()
    imgs.forEach((el) => {
      this.lazy.add(
        el,
        assignDeep({}, this.binding, {
          value: {
            src: 'dataset' in el ? el.dataset.src : el.getAttribute('data-src'),
            error: ('dataset' in el ? el.dataset.error : el.getAttribute('data-error')) || this.options.error,
            loading: ('dataset' in el ? el.dataset.loading : el.getAttribute('data-loading')) || this.options.loading
          }
        }),
        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
  }
}

LazyContainer.install = (Vue, options = {}) => {
  const LazyClass = Lazy(Vue)
  const lazy = new LazyClass(options)
  const lazyContainer = new LazyContainer({ lazy })

  const isVue2 = Vue.version.split('.')[0] === '2'
  if (isVue2) {
    Vue.directive('lazy-container', {
      bind: lazyContainer.bind.bind(lazyContainer),
      componentUpdated: lazyContainer.update.bind(lazyContainer),
      unbind: lazyContainer.unbind.bind(lazyContainer)
    })
  } else {
    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)
      }
    })
  }
}

const LazyImage = (lazyManager) => {
  return {
    props: {
      src: [String, Object],
      tag: {
        type: String,
        default: 'img'
      }
    },
    render(h) {
      return h(
        this.tag,
        {
          attrs: {
            src: this.renderSrc
          }
        },
        this.$slots.default
      )
    },
    data() {
      return {
        el: null,
        options: {
          src: '',
          error: '',
          loading: '',
          attempt: lazyManager.options.attempt
        },
        state: {
          loaded: false,
          error: false,
          attempt: 0
        },
        rect: {},
        renderSrc: ''
      }
    },
    watch: {
      src() {
        this.init()
        lazyManager.addLazyBox(this)
        lazyManager.lazyLoadHandler()
      }
    },
    created() {
      this.init()
      this.renderSrc = this.options.loading
    },
    mounted() {
      this.el = this.$el
      lazyManager.addLazyBox(this)
      lazyManager.lazyLoadHandler()
    },
    beforeDestroy() {
      lazyManager.removeComponent(this)
    },
    methods: {
      init() {
        const { src, loading, error } = lazyManager._valueFormatter(this.src)
        this.state.loaded = false
        this.options.src = src
        this.options.error = error
        this.options.loading = loading
        this.renderSrc = this.options.loading
      },
      getRect() {
        this.rect = this.$el.getBoundingClientRect()
      },
      checkInView() {
        this.getRect()
        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
        )
      },
      load(onFinish = noop) {
        if (this.state.attempt > this.options.attempt - 1 && this.state.error) {
          if (!lazyManager.options.silent)
            console.log(`VueLazyload log: ${this.options.src} tried too more than ${this.options.attempt} times`)
          onFinish()
          return
        }
        const src = this.options.src
        loadImageAsync(
          { src, el: this.$el },
          ({ src }) => {
            this.renderSrc = src
            this.state.loaded = true
          },
          (e) => {
            this.state.attempt++
            this.renderSrc = this.options.error
            this.state.error = true
          }
        )
      }
    }
  }
}

LazyImage.install = (Vue, options = {}) => {
  const LazyClass = Lazy(Vue)
  const lazy = new LazyClass(options)
  Vue.component('lazy-image', LazyImage(lazy))
}

var index = {
  /*
   * 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 LazyContainerMananger({ lazy })

    const isVue2 = Vue.version.split('.')[0] === '2'

    Vue.prototype.$Lazyload = lazy

    if (options.lazyComponent) {
      Vue.component('lazy-component', LazyComponent(lazy))
    }

    if (options.lazyImage) {
      Vue.component('lazy-image', LazyImage(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),
        componentUpdated: lazyContainer.update.bind(lazyContainer),
        unbind: lazyContainer.unbind.bind(lazyContainer)
      })
    } else {
      Vue.directive('lazy', {
        bind: lazy.lazyLoadHandler.bind(lazy),
        update(newValue, oldValue) {
          assignDeep(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)
        }
      })
    }
  }
}

export { Lazy, LazyComponent, LazyContainerMananger as LazyContainer, LazyImage, index as default }
