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