Source: mag-jquery.js

/**
 * mag-jquery
 */

/**
 * @external jQuery
 * @see {@link https://api.jquery.com/jQuery/}
 */

/**
 * @external HTMLElement
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement}
 */

(function (root, factory) {
  'use strict'; // eslint-disable-line semi

  var name = 'Magnificent'
  if (typeof define === 'function' && define.amd) {
    define(['./mag', './mag-analytics', 'jquery', 'hammerjs', 'prevent-ghost-click', 'jquery-bridget'],
      function (mag, MagnificentAnalytics, jQuery, Hammer) {
        return (root[name] = factory(mag, MagnificentAnalytics, jQuery, Hammer, root.PreventGhostClick))
      }
    )
  } else if (typeof exports === 'object') {
    module.exports = factory(require('./mag'), require('./mag-analytics'),
      require('jquery'), require('hammerjs'), require('prevent-ghost-click'),
      require('jquery-bridget')
    )
  } else {
    root[name] = factory(root.Mag, root.MagnificentAnalytics,
      root.jQuery, root.Hammer, root.PreventGhostClick
    )
  }
}(this, function (Mag, MagnificentAnalytics, $, Hammer, PreventGhostClick) {
  'use strict'; // eslint-disable-line semi

  $(':root').addClass('mag-js')

  var normalizeOffsets = function (e, $target) {
    $target = $target || $(e.target)
    var offset = $target.offset()
    return {
      x: e.pageX - offset.left,
      y: e.pageY - offset.top
    }
  }

  var ratioOffsets = function (e, $target) {
    $target = $target || $(e.target)
    var normOff = normalizeOffsets(e, $target)
    return {
      x: normOff.x / $target.width(),
      y: normOff.y / $target.height()
    }
  }

  var ratioOffsetsFor = function ($target, x, y) {
    return {
      x: x / $target.width(),
      y: y / $target.height()
    }
  }

  var cssPerc = function (frac) {
    return (frac * 100) + '%'
  }

  var toCSS = function (pt, mode, id) {
    if (mode === '3d') {
      return toCSSTransform3d(pt, id)
    }

    if (mode === '2d') {
      return toCSSTransform2d(pt, id)
    }

    // mode === 'position'
    return toCSSPosition(pt, id)
  }

  var toCSSPosition = function (pt, id) {
    var css = {}

    if (pt.x !== undefined) css.left = cssPerc(pt.x)
    if (pt.y !== undefined) css.top = cssPerc(pt.y)
    if (pt.w !== undefined) css.width = cssPerc(pt.w)
    if (pt.h !== undefined) css.height = cssPerc(pt.h)

    return css
  }

  var toCSSTransform2d = function (pt, id) {
    var css = {}
    var left
    var top
    var width
    var height

    var x = pt.x
    var y = pt.y
    var w = pt.w
    var h = pt.h

    x += (w - 1) * (0.5 - x) / w
    y += (h - 1) * (0.5 - y) / h

    if (x !== undefined) left = cssPerc(x)
    if (y !== undefined) top = cssPerc(y)
    if (w !== undefined) width = w
    if (h !== undefined) height = h

    var transform = ''

    if (width) transform += ' scaleX(' + width + ')'
    if (height) transform += ' scaleY(' + height + ')'
    if (left) transform += ' translateX(' + left + ')'
    if (top) transform += ' translateY(' + top + ')'

    css['-webkit-transform'] = transform
    css['-moz-transform'] = transform
    css['-ms-transform'] = transform
    css['-o-transform'] = transform
    css.transform = transform

    return css
  }

  var toCSSTransform3d = function (pt, id) {
    var css = {}
    var left
    var top
    var width
    var height

    var x = pt.x
    var y = pt.y
    var w = pt.w
    var h = pt.h

    x += (w - 1) * (0.5 - x) / w
    y += (h - 1) * (0.5 - y) / h

    if (x !== undefined) left = cssPerc(x)
    if (y !== undefined) top = cssPerc(y)
    if (w !== undefined) width = w
    if (h !== undefined) height = h

    var transform = ''
    transform += ' scale3d(' +
      (width !== undefined ? width : 0) + ',' +
      (height !== undefined ? height : 0) +
      ',1)'
    transform += ' translate3d(' +
      (left !== undefined ? left : 0) + ',' +
      (top !== undefined ? top : 0) +
      ',0)'

    css['-webkit-transform'] = transform
    css['-moz-transform'] = transform
    css['-ms-transform'] = transform
    css['-o-transform'] = transform
    css.transform = transform

    css.width = '100%'
    css.height = '100%'
    css.position = 'absolute'
    css.top = '0'
    css.left = '0'

    return css
  }

  /**
   * Magnificent constructor.
   *
   * @alias module:mag-jquery
   *
   * @class
   * @param {external:HTMLElement|external:jQuery} element - DOM element to embellish.
   * @param {MagnificentOptions} options - Options to override defaults.
   */
  var Magnificent = function (element, options) {
    this.element = $(element)
    this.options = $.extend(true, {}, this.options, options)
    this._init()
  }

  /**
   * Default options.
   *
   * @typedef MagnificentOptions
   *
   *  Mode:<br>
   * @property {string} mode
   *  <dl>
   *    <dt>"inner"</dt><dd><i>(default)</i> Zoom region embedded in thumbnail.</dd>
   *    <dt>"outer"</dt><dd>Zoom region independent of thumbnail.</dd>
   *  </dl>
   * @property {string|boolean} position - What interaction(s) position zoomed region.
   *  <dl>
   *    <dt>"mirror"</dt><dd><i>(default)</i> Zoomed region follows mouse/pointer.</dd>
   *    <dt>"drag"</dt><dd>Drag to move.</dd>
   *    <dt>"joystick"</dt><dd>Weird joystick interaction to move.</dd>
   *    <dt>false</dt><dd>No mouse/touch.</dd>
   *  </dl>
   * @property {string} positionEvent - Controls what event(s) cause positioning.
   *  <dl>
   *    <dt>"move"</dt><dd><i>(default)</i> On move (e.g. mouseover).</dd>
   *    <dt>"hold"</dt><dd>On hold (e.g. while mousedown).</dd>
   *  </dl>
   * @property {string} theme - Themes apply a style to the widgets.
   *  <dl>
   *    <dt>"default"</dt><dd><i>(default)</i> Default theme.</dd>
   *  </dl>
   * @property {string} initialShow
   *  <dl>
   *    <dt>"thumb"</dt><dd><i>(default)</i> Whether to show thumbnail or zoomed first,
   *      e.g. in "inner" mode.</dd>
   *  </dl>
   * @property {number} zoomRate - Rate at which to adjust zoom, from (0,∞). Default = 0.2.
   * @property {number} zoomMin - Minimum zoom level allowed, from (0,∞). Default = 2.
   * @property {number} zoomMax - Maximum zoom level allowed, from (0,∞). Default = 10.
   * @property {number} dragRate - Rate at which to drag, from (0,∞). Default = 0.2.
   * @property {number} ratio - Ratio of outer (w/h) to inner (w/h) container ratios. Default = 1.
   * @property {boolean} constrainLens - Whether lens position is constrained. Default = true.
   * @property {boolean} constrainZoomed - Whether zoomed position is constrained. Default = false.
   * @property {boolean} toggle - Whether toggle display of zoomed vs. thumbnail upon interaction. Default = true.
   * @property {boolean} smooth - Whether the zoomed region should gradually approach target, rather than immediately. Default = true.
   * @property {string} cssMode - CSS mode to use for scaling and translating. Either '3d', '2d', or 'position'. Default = '3d'.
   * @property {number} renderIntervalTime - Milliseconds for render loop interval. Adjust for performance vs. frame rate. Default = 20.
   * @property {MagModel} initial - Initial settings for model - focus, lens, zoom, etc.
   */
  Magnificent.prototype.options = {
    mode: 'inner',
    position: 'mirror',
    positionEvent: 'move',
    theme: 'default',
    initialShow: 'thumb',
    constrainLens: true,
    constrainZoomed: false,
    zoomMin: 1,
    zoomMax: 10,
    zoomRate: 0.2,
    dragRate: 0.2,
    ratio: 1,
    toggle: true,
    smooth: true,
    renderIntervalTime: 20,
    cssMode: '3d',
    eventNamespace: 'magnificent',
    dataNamespace: 'magnificent'
  }

  /**
   * Default toggle implementation.
   *
   * @param {boolean} enter - Whether entering, rather leaving.
   */
  Magnificent.prototype.toggle = function (enter) {
    if (enter) {
      this.$zoomedContainer.fadeIn()
      if (this.$lens) {
        this.$lens.fadeIn()
      }
    } else {
      this.$zoomedContainer.fadeOut()
      if (this.$lens) {
        this.$lens.fadeOut()
      }
    }
  }

  Magnificent.prototype.compute = function () {
    var that = this
    that.mag.compute()
    that.$el.trigger('compute', that)
  }

  Magnificent.prototype.render = function () {
    var that = this
    var lens, zoomed
    var $lens = this.$lens
    var $zoomed = this.$zoomed
    if ($lens) {
      lens = this.modelLazy.lens
      var lensCSS = toCSS(lens, that.options.cssMode, that.id)
      $lens.css(lensCSS)
    }
    zoomed = this.modelLazy.zoomed
    var zoomedCSS = toCSS(zoomed, that.options.cssMode, that.id)
    $zoomed.css(zoomedCSS)

    this.$el.trigger('render', that)
  }

  Magnificent.prototype.eventName = function (name) {
    name = name || ''
    var namespace = this.options.eventNamespace
    return name + (namespace ? ('.' + namespace) : '')
  }

  Magnificent.prototype.dataName = function (name) {
    name = name || ''
    var namespace = this.options.dataNamespace
    return (namespace ? (namespace + '.') : '') + name
  }

  Magnificent.prototype._init = function () {
    var that = this

    that.intervals = {}

    var $el = this.$el = this.element

    this.$originalEl = $el.clone()

    var options = this.options

    var id = $el.attr('mag-thumb') || $el.attr('data-mag-thumb')
    this.id = id

    if ($.isFunction(options.toggle)) {
      this.toggle = options.toggle
    }

    var $lens = this.$lens

    var ratio = options.ratio

    var initial = options.initial || {}
    var zoom = typeof initial.zoom !== 'undefined' ? initial.zoom : 2
    var focus = typeof initial.focus !== 'undefined' ? initial.focus : {
      x: 0.5,
      y: 0.5
    }
    var lens = typeof initial.lens !== 'undefined' ? initial.lens : {
      w: 0,
      h: 0
    }

    var model = this.model = {
      focus: focus,
      zoom: zoom,
      lens: lens,
      ratio: ratio
    }

    var mag = this.mag = new Mag({
      zoomMin: options.zoomMin,
      zoomMax: options.zoomMax,
      constrainLens: options.constrainLens,
      constrainZoomed: options.constrainZoomed,
      model: model
    })

    var modelLazy = this.modelLazy = {
      focus: {
        x: model.focus.x,
        y: model.focus.y
      },
      zoom: model.zoom,
      lens: {
        w: model.lens.w,
        h: model.lens.h
      },
      ratio: ratio
    }

    var magLazy = this.magLazy = new Mag({
      zoomMin: options.zoomMin,
      zoomMax: options.zoomMax,
      constrainLens: options.constrainLens,
      constrainZoomed: options.constrainZoomed,
      model: modelLazy
    })

    mag.compute()
    magLazy.compute()

    var $zoomedChildren
    var $thumbChildren
    var $zoomed
    var $zoomedContainer

    $thumbChildren = $el.children()

    $el.empty()
    $el.addClass('mag-host')

    if (!options.zoomedContainer) {
      options.zoomedContainer = $('[mag-zoom="' + that.id + '"], [data-mag-zoom="' + that.id + '"]')
    }

    if (options.zoomedContainer) {
      $zoomedContainer = $(options.zoomedContainer)

      that.$originalZoomedContainer = $zoomedContainer.clone()

      $zoomedChildren = $zoomedContainer.children()
      $zoomedContainer.empty()

      if (options.mode === 'inner') {
        $zoomedContainer.remove()
      }
    }

    if (options.mode === 'outer' && typeof options.showLens === 'undefined') {
      options.showLens = true
    }

    if (!$zoomedChildren || !$zoomedChildren.length) {
      $zoomedChildren = $thumbChildren.clone()
    }

    if (options.mode) {
      $el.attr('mag-mode', options.mode)
      $el.attr('data-mag-mode', options.mode)
    }

    if (options.theme) {
      $el.attr('mag-theme', 'default')
      $el.attr('data-mag-theme', 'default')
    }

    if (options.position) {
      $el.attr('mag-position', options.position)
      $el.attr('data-mag-position', options.position)
    } else if (options.position === false) {
      options.positionEvent = false
    }

    if (options.positionEvent) {
      $el.attr('mag-position-event', options.positionEvent)
      $el.attr('data-mag-position-event', options.positionEvent)
    }

    $el.attr('mag-toggle', options.toggle)
    $el.attr('data-mag-toggle', options.toggle)

    if (options.showLens) {
      $lens = this.$lens = $('<div class="mag-lens"></div>')
      $el.append($lens)
    }

    var $noflow = $('<div class="mag-noflow" mag-theme="' + options.theme + '"></div>')
    $el.append($noflow)

    if (options.mode === 'inner') {
      $zoomedContainer = $noflow
    } else if (options.mode === 'outer') {
      if (!options.zoomedContainer) {
        throw new Error("Required 'zoomedContainer' option.")
      }
      $zoomedContainer = $(options.zoomedContainer)
    } else {
      throw new Error("Invalid 'mode' option.")
    }

    $zoomedContainer.attr('mag-theme', options.theme)
    $zoomedContainer.attr('data-mag-theme', options.theme)
    $zoomedContainer.addClass('mag-zoomed-container')
    $zoomedContainer.addClass('mag-zoomed-bg')

    var $thumb = $('<div class="mag-thumb"></div>')
    $thumb.html($thumbChildren)
    $el.append($thumb)

    $zoomed = this.$zoomed = $('<div class="mag-zoomed"></div>')
    $zoomed.html($zoomedChildren)
    $zoomedContainer.append($zoomed)

    $zoomedContainer.attr('mag-toggle', options.toggle)
    $zoomedContainer.attr('data-mag-toggle', options.toggle)

    var $zone = $('<div class="mag-zone"></div>')
    var zone = $zone.get(0)
    $el.append($zone)

    this.$el = $el
    this.$zone = $zone
    this.$noflow = $noflow
    this.$thumb = $thumb
    this.$zoomed = $zoomed
    this.$zoomedContainer = $zoomedContainer

    that.proxyToZone($zoomedContainer)
    if (options.mode === 'outer') {
      that.proxyToZone($thumb)
    }

    if (options.toggle) {
      if (options.initialShow === 'thumb') {
        $zoomedContainer.hide()
        if ($lens) {
          $lens.hide()
        }
      } else if (options.initialShow === 'zoomed') {
        //
      } else {
        throw new Error("Invalid 'initialShow' option.")
      }

      $el.on(that.eventName('mouseenter'), function () {
        that.toggle(true)
      })

      $el.on(that.eventName('mouseleave'), function () {
        that.toggle(false)
      })
    }

    that.render()

    var lazyRate = 0.25
    var renderIntervalTime = options.renderIntervalTime
    var dragRate = options.dragRate
    var zoomRate = options.zoomRate

    var approach = function (enabled, thresh, rate, dest, src, props, srcProps) {
      srcProps = srcProps || props
      if (!$.isArray(props)) {
        props = [props]
        srcProps = [srcProps]
      }
      for (var i = 0, m = props.length; i < m; ++i) {
        var prop = props[i]
        var srcProp = srcProps[i]
        var diff = src[srcProp] - dest[prop]
        if (enabled && Math.abs(diff) > thresh) {
          dest[prop] += diff * rate
        } else {
          dest[prop] += diff
        }
      }
    }

    var renderLoop = function () {
      approach(options.smooth, 0.01, lazyRate, modelLazy.focus, model.focus, 'x')
      approach(options.smooth, 0.01, lazyRate, modelLazy.focus, model.focus, 'y')
      approach(options.smooth, 0.05, lazyRate, modelLazy, model, 'zoom')

      that.magLazy.compute()

      that.render()
    }

    var adjustForMirror = function (focus) {
      model.focus.x = focus.x
      model.focus.y = focus.y
      that.compute()
    }

    if (options.position === 'mirror') {
      if (options.positionEvent === 'move') {
        lazyRate = 0.2

        $zone.on(that.eventName('mousemove'), function (e, e2) {
          e = typeof e2 === 'object' ? e2 : e
          var ratios = ratioOffsets(e, $zone)
          adjustForMirror(ratios)
        })
      } else if (options.positionEvent === 'hold') {
        lazyRate = 0.2

        $zone.on(that.eventName('dragstart'), function (e, dd, e2) {
          e = typeof e2 === 'object' ? e2 : e
          dragging = true
          $el.addClass('mag--dragging')
        })

        $zone.on(that.eventName('dragend'), function (e, dd, e2) {
          e = typeof e2 === 'object' ? e2 : e
          dragging = false
          $el.removeClass('mag--dragging')
        })

        $zone.on(that.eventName('drag'), function (e, dd, e2) {
          // console.log('drag', arguments, JSON.stringify(dd))
          e = typeof e2 === 'object' ? e2 : e
          var offset = $zone.offset()
          var ratios = ratioOffsetsFor($zone, e.pageX - offset.left, e.pageY - offset.top)
          adjustForMirror(ratios)
        })
      } else {
        throw new Error("Invalid 'positionEvent' option.")
      }
    } else if (options.position === 'drag') {
      var startFocus

      if (options.mode === 'inner') {
        $zone.on(that.eventName('dragstart'), function (e, dd, e2) {
          e = typeof e2 === 'object' ? e2 : e
          e.preventDefault()
          dragging = true
          $el.addClass('mag--dragging')
          startFocus = {
            x: model.focus.x,
            y: model.focus.y
          }
        })

        $zone.on(that.eventName('dragend'), function (e, dd, e2) {
          e = typeof e2 === 'object' ? e2 : e
          dragging = false
          $el.removeClass('mag--dragging')
          startFocus = undefined
        })

        $zone.on(that.eventName('drag'), function (e, dd, e2) {
          // console.log('drag', arguments, JSON.stringify(dd))
          e = typeof e2 === 'object' ? e2 : e

          // Modified plugin to improve touch functionality
          if (e.originalEvent) {
            if (e.originalEvent.scale !== 1) {
              return
            }
          }
          // End of modification

          ratios = ratioOffsetsFor($zone, dd.originalX - dd.offsetX, dd.originalY - dd.offsetY)

          ratios = {
            x: ratios.x / model.zoom,
            y: ratios.y / model.zoom
          }

          var focus = model.focus

          focus.x = startFocus.x + ratios.x
          focus.y = startFocus.y + ratios.y

          that.compute()
        })
      } else {
        $zone.on(that.eventName('dragstart'), function (e, dd, e2) {
          e = typeof e2 === 'object' ? e2 : e
          dragging = true
          $el.addClass('mag--dragging')
          startFocus = {
            x: model.focus.x,
            y: model.focus.y
          }
        })

        $zone.on(that.eventName('dragend'), function (e, dd, e2) {
          e = typeof e2 === 'object' ? e2 : e
          dragging = false
          $el.removeClass('mag--dragging')
          startFocus = undefined
        })

        $zone.on(that.eventName('drag'), function (e, dd, e2) {
          // console.log('drag', arguments, JSON.stringify(dd))
          var offset = $zone.offset()
          ratios = ratioOffsetsFor($zone, e.pageX - offset.left, e.pageY - offset.top)

          var focus = model.focus

          focus.x = ratios.x
          focus.y = ratios.y

          that.compute()
        })

        $zone.on(that.eventName('click'), function (e) {
          var offset = $zone.offset()
          ratios = ratioOffsetsFor($zone, e.pageX - offset.left, e.pageY - offset.top)

          var focus = model.focus

          focus.x = ratios.x
          focus.y = ratios.y

          that.compute()
        })
      }
    } else if (options.position === 'joystick') {
      var joystickIntervalTime = 50

      var dragging = false

      var ratios = {
        x: model.focus.x,
        y: model.focus.y
      }

      if (options.positionEvent === 'move') {
        dragging = true
        lazyRate = 0.5

        $zone.on(that.eventName('mousemove'), function (e) {
          ratios = ratioOffsets(e, $zone)
        })
      } else if (options.positionEvent === 'hold') {
        lazyRate = 0.5

        $zone.drag('start', function () {
          dragging = true
          $el.addClass('mag--dragging')
        })

        $zone.drag('end', function () {
          dragging = false
          $el.removeClass('mag--dragging')
        })

        $zone.drag(function (e, dd) {
          var offset = $zone.offset()
          ratios = ratioOffsetsFor($zone, e.pageX - offset.left, e.pageY - offset.top)
        })
      } else {
        throw new Error("Invalid 'positionEvent' option.")
      }

      that.intervals.joystick = setInterval(function () {
        if (!dragging) return

        var focus = model.focus

        var adjustedDragRate = dragRate
        focus.x += (ratios.x - 0.5) * adjustedDragRate
        focus.y += (ratios.y - 0.5) * adjustedDragRate
        that.compute()
      }, joystickIntervalTime)
    } else if (options.position === false) {
      // assume manual programmatic positioning
    } else {
      throw new Error("Invalid 'position' option.")
    }

    if (options.position) {
      $zone.on(that.eventName('mousewheel'), function (e, e2) {
        e = typeof e2 === 'object' ? e2 : e
        // console.log('mousewheel', {
        //   deltaX: e.deltaX,
        //   deltaY: e.deltaY,
        //   deltaFactor: e.deltaFactor
        // })
        e.preventDefault()

        var rate = zoomRate
        var zoom = model.zoom
        var delta = (e.deltaY + e.deltaX) / 2
        // if (e.deltaFactor) {
        //   delta *= e.deltaFactor
        // }
        delta *= rate
        delta += 1
        zoom *= delta
        model.zoom = zoom
        that.compute()
      })

      if (PreventGhostClick) {
        PreventGhostClick(zone)
      }

      if (Hammer) {
        var hammerEl = zone
        var $hammerEl = $zone
        var hammerOptions = {}
        var hammertime = new Hammer(hammerEl, hammerOptions)

        // Register custom destroy event listener to queue Hammer destroy.
        that.$el.on(that.eventName('destroy'), function () {
          hammertime.destroy()
        })

        $hammerEl.data(that.dataName('hammer'))

        hammertime.get('pinch').set({ enable: true })

        hammertime.on('pinch', function (e) {
          e.preventDefault()

          that.toggle(true)

          var zoom = model.zoom
          var scale = e.scale || (e.originalEvent && e.originalEvent.scale)
          zoom *= scale
          model.zoom = zoom
          that.compute()
        })

        // if (options.position === 'mirror') {
        if (options.mode === 'inner') {
          var pinch = hammertime.get('pinch')
          var pan = hammertime.get('pan')

          pinch.recognizeWith(pan)

          hammertime.on('pan', function (e) {
            e.preventDefault()
            // console.log('pan', e)

            that.toggle(true)

            var rate = -0.0005

            model.focus.x += rate * e.deltaX
            model.focus.y += rate * e.deltaY
          })
        }
      }
    }

    that.intervals.renderLoop = setInterval(renderLoop, renderIntervalTime)
  }

  Magnificent.prototype.proxyToZone = function ($el) {
    var that = this
    var $zone = that.$zone
    /*
      Proxy events from container to zone for weird IE 9-10 behavior despite z-index.
     */
    var proxyEvents = [
      'mousemove',
      // 'mouseenter',
      // 'mouseleave',
      // 'mouseover',
      // 'mouseout',
      'click',
      'touchstart',
      'touchend',
      'touchmove',
      'touchcancel',
      'mousewheel',
      'draginit',
      'dragstart',
      'drag',
      'dragend'
    ]
    var nsProxyEvents = $.map(proxyEvents, function (name) {
      return that.eventName(name)
    })
    $el.on(nsProxyEvents.join(' '), function (e) {
      var args = Array.prototype.slice.call(arguments)
      // console.log(['a', args[0], args[1], args[2], args[3], args[4], args[5]])
      e.triggered = true
      args.push(e)
      args.unshift(that.eventName(e.type))
      // console.log(['b', args[0], args[1], args[2], args[3], args[4], args[5]])
      $zone.trigger.apply($zone, args)
    })
  }

  Magnificent.prototype.destroy = function () {
    var that = this
    // Trigger custom destroy event for any listeners.
    that.$el.trigger(that.eventName('destroy'))

    $.each(that.intervals, function (key, interval) {
      clearInterval(interval)
    })

    // Unbind and replace elements with originals.

    that.off()

    if (that.$originalZoomedContainer && that.$zoomedContainer) {
      // Replace
      that.$zoomedContainer.after(that.$originalZoomedContainer)
      that.$zoomedContainer.remove()
    }

    // Replace
    that.$el.after(that.$originalEl)
    that.$el.remove()
  }

  Magnificent.prototype.off = function () {
    var that = this

    if (that.$originalZoomedContainer && that.$zoomedContainer) {
      // Turn off all events.
      that.$zoomedContainer.off(that.eventName())
    }

    // Turn off all events.
    that.$el.off(that.eventName())

    return this
  }

  Magnificent.prototype.zoomBy = function (factor) {
    this.model.zoom *= 1 + factor
    this.compute()
  }

  Magnificent.prototype.zoomTo = function (zoom) {
    this.model.zoom = zoom
    this.compute()
  }

  Magnificent.prototype.moveBy = function (shift) {
    if (typeof shift.x !== 'undefined') {
      if (!shift.absolute) {
        shift.x /= this.model.zoom
      }
      this.model.focus.x += shift.x
    }
    if (typeof shift.y !== 'undefined') {
      if (!shift.absolute) {
        shift.y /= this.model.zoom
      }
      this.model.focus.y += shift.y
    }
    this.compute()
  }

  Magnificent.prototype.moveTo = function (coords) {
    if (typeof coords.x !== 'undefined') {
      this.model.focus.x = coords.x
    }
    if (typeof coords.y !== 'undefined') {
      this.model.focus.y = coords.y
    }
    this.compute()
  }

  $.bridget('mag', Magnificent)

  if (MagnificentAnalytics) {
    MagnificentAnalytics.track('mag-jquery.js')
  }

  return Magnificent
}))