/**
* @preserve simpleZoom 1.0.0 - jQuery plugin
* Copyright (c) 2014 Kraig Halfpap (http://halfpapstudios.com)
* Licensed under the MIT (MIT-LICENSE.txt) license.
*/
/**
* The {@link http://api.jquery.com/Types/#jQuery jQuery} object.
* @class jQuery
* @global
*/
/**
* The {@link http://docs.jquery.com/Plugins/Authoring jQuery plugin} namespace.
* @namespace fn
* @memberOf jQuery
*/
(function($) {
'use strict';
var uuid = function() {
var d = $.now();
var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = d / 16 | 0;
return (c === 'x' ? r : (r & 0x7 | 0x8)).toString(16);
});
return uuid;
};
var _window = window;
var NAMESPACE = 'simpleZoom';
var SIMPLEZOOM_ID = uuid();
var VIEWPORT_ACTIVE_IMAGE = uuid();
var VIEWPORT_OCCUPANTS = uuid();
var NULL = uuid();
var FUNCTION = 'function';
var FIXED = 'fixed';
var MARGIN_LEFT = 'margin-left';
var MARGIN_TOP = 'margin-top';
var BORDER_LEFT_WIDTH = 'border-left-width';
var BORDER_TOP_WIDTH = 'border-top-width';
var PADDING_LEFT = 'padding-left';
var PADDING_TOP = 'padding-top';
var POSITION = 'position';
var _setTimeout = _window.setTimeout;
var _defer = function(fn, elapsed, context) {
var timeout;
var throttled = function(args) {
timeout = null;
fn.apply(context || this, args);
};
return function() {
var args = _toArray(arguments);
if (timeout) {
_window.clearTimeout(timeout);
}
timeout = _setTimeout(function() {
throttled(args);
}, elapsed);
};
};
var _clamp = function(x, min, max) {
return min > max ? x : Math.max(min, Math.min(x, max));
};
var _px = function(number) {
return String(number.toFixed(2)) + 'px';
};
var _call = function(fn, context) {
if (_is(fn, FUNCTION)) {
return fn.call(context);
}
};
var _is = function(value, type) {
return $.type(value) === type;
};
var _getPercentage = function(str) {
var result = /^(\d*\.*\d*)%$/.exec(str);
var percent;
if (result && result[1]) {
percent = parseFloat(result[1]);
}
if ($.isNumeric(percent)) {
return percent / 100;
} else {
return false;
}
};
var _isUndefined = function(value) {
return _is(value, 'undefined');
};
var _isString = function(value) {
return _is(value, 'string');
};
var _requestAnimFrame = _window.requestAnimationFrame ||
_window.webkitRequestAnimationFrame ||
_window.mozRequestAnimationFrame ||
_window.oRequestAnimationFrame ||
_window.msRequestAnimationFrame ||
function(callback) {
_setTimeout(callback, 1000 / 60);
};
var _getCssInteger = function($T, cssProperty) {
return parseInt($T.css(cssProperty), 10) || 0;
};
var _sumCssProperties = function(element, cssProperties) {
var value = 0;
$.each(cssProperties, function(index, property) {
value += _getCssInteger(element, property);
});
return value;
};
var _getSpacing = function($T) {
return {
left: _sumCssProperties($T, [MARGIN_LEFT, PADDING_LEFT, BORDER_LEFT_WIDTH]),
top: _sumCssProperties($T, [MARGIN_TOP, PADDING_TOP, BORDER_TOP_WIDTH])
};
};
var _getContentPosition = function($T) {
var position = $T.position(),
space = _getSpacing($T);
return {
left: position.left + space.left,
top: position.top + space.top
};
};
var _getContentOffset = function($T) {
var offset = $T.offset();
return {
left: offset.left + _sumCssProperties($T, [PADDING_LEFT, BORDER_LEFT_WIDTH]),
top: offset.top + _sumCssProperties($T, [PADDING_TOP, BORDER_TOP_WIDTH])
};
};
var _getContentLocalFromOffset = function($T, offset) {
var newOrigin = _getContentOffset($T);
return {
left: offset.left - newOrigin.left,
top: offset.top - newOrigin.top
};
};
var _toArray = function(args, index) {
return Array.prototype.slice.call(args, index);
};
var _create = function(type) {
return $(document.createElement(type));
};
var _eventTypesMap = {};
var _eventTypes = [
'afterhide',
'aftershow',
'beforedestroy',
'beforehide',
'beforeshow',
'create',
'destroy',
'load',
'movestop',
'movestart',
'refresh'
];
$.each(_eventTypes, function(index, type) {
_eventTypesMap[type] = NAMESPACE.toLowerCase() + type;
});
var ViewportImage = function(src) {
var self = this;
var target = self.$T = _create('img').attr('src', src);
self.src = src;
// scaled width and height
self.width = 0;
self.height = 0;
// actual image dimensions
self.H = 0;
self.W = 0;
self.load(function() {
self.H = target[0].height;
self.W = target[0].width;
});
};
var ViewportImageProto = ViewportImage.prototype;
ViewportImageProto.load = function(callback) {
var target = this.$T;
if (target.prop('complete')) {
callback();
}
else {
target.one('load', callback);
}
};
ViewportImageProto.resize = function(targetWidth, targetHeight, zoom) {
var self = this;
if (_is(zoom, 'number')) {
zoom = zoom + 1;
self.width = targetWidth * zoom;
self.height = targetHeight * zoom;
}
else if (targetHeight > targetWidth) {
self.width = self.W;
self.height = self.width * (targetHeight / targetWidth);
} else {
self.height = self.H;
self.width = self.height * (targetWidth / targetHeight);
}
};
var Overlay = function(target, className) {
var self = this;
self.$T = _create('div')
.addClass(className)
.insertAfter(target);
self.target = target;
self.width = 0;
self.height = 0;
self.left = 0;
self.top = 0;
self.position = '';
self.events = [];
};
var OverlayProto = Overlay.prototype;
OverlayProto.registerEvent = function(event, fn) {
this.events.push({
event: event,
fn: fn
});
};
OverlayProto.enable = function() {
var self = this;
$.each(self.events, function(index, event) {
self.$T.on(event.event, event.fn);
});
};
OverlayProto.disable = function() {
this.$T.off();
};
OverlayProto.layout = function() {
var self = this;
var target = self.target;
var contentPosition = _getContentPosition(target);
self.position = target.css(POSITION) === FIXED ? FIXED : 'absolute';
self.width = target.width();
self.height = target.height();
self.left = contentPosition.left;
self.top = contentPosition.top;
self.$T.css(
{
position: self.position,
width: self.width,
height: self.height,
left: self.left,
top: self.top
}
);
};
OverlayProto.destroy = function() {
this.disable();
this.$T.remove();
};
var Viewfinder = function(container, options) {
var self = this;
self.$T = _create('div')
.insertBefore(container.$T)
.hide()
.addClass(options.className.viewfinder);
self.container = container;
self.visible = false;
self.showing = false;
self.width = 0;
self.height = 0;
self.radius = {x: 0, y: 0};
self.range = {minX: 0, minY: 0, supX: 0, supY: 0};
};
var ViewfinderProto = Viewfinder.prototype;
ViewfinderProto.canShow = function() {
var self = this;
return !self.visible && !self.showing;
};
ViewfinderProto.show = function(duration, beforeShow, afterShow) {
var self = this;
self.showing = true;
self.$T.stop(false, true).fadeIn({
start: beforeShow,
duration: duration,
done: function() {
self.showing = false;
self.visible = true;
afterShow();
}
});
};
ViewfinderProto.hide = function(duration, beforeHide, afterHide) {
var self = this;
self.$T.stop(false, true).fadeOut({
duration: duration,
start: beforeHide,
done: function() {
self.visible = false;
afterHide();
}
});
};
ViewfinderProto.resize = function(width, height, confine) {
var self = this;
var viewfinder = self.$T;
var container = self.container;
var marginLeft = _getCssInteger(viewfinder, MARGIN_LEFT);
var marginTop = _getCssInteger(viewfinder, MARGIN_TOP);
var offsetLeft = marginLeft + _getCssInteger(viewfinder, BORDER_LEFT_WIDTH);
var offsetTop = marginTop + _getCssInteger(viewfinder, BORDER_TOP_WIDTH);
var radius = self.radius;
var range = self.range;
radius.x = width / 2 + offsetLeft;
radius.y = height / 2 + offsetTop;
self.$T.css(
{
position: container.$T.css(POSITION),
width: width,
height: height
}
);
if (confine) {
range.minX = radius.x - marginLeft;
range.minY = radius.y - marginTop;
range.supX = container.width - radius.x + marginLeft;
range.supY = container.height - radius.y + marginTop;
}
if (!confine || range.supX < range.minX) {
range.minX = 0;
range.supX = container.width;
}
if (!confine || range.supY < range.minY) {
range.minY = 0;
range.supY = container.height;
}
};
// container space coordinates
ViewfinderProto.draw = function(x, y) {
var self = this;
var container = self.container;
var left = container.left + x - self.radius.x;
var top = container.top + y - self.radius.y;
self.$T.css({
top: _px(top),
left: _px(left)
});
};
ViewfinderProto.destroy = function() {
this.$T.remove();
};
var ImageViewport = function(target, options, image) {
var self = this;
self.$T = target
.addClass(options.className.viewport);
var css = {
overflow: 'hidden'
};
if (target.css(POSITION) === 'static') {
css.position = 'relative';
}
self.image = image
.css(POSITION, 'absolute');
target.css(css);
self.width = 0;
self.height = 0;
self.radius = {
x: 0,
y: 0
};
self.removeOnDestroy = !options.viewport;
var occupants = self.$T.data(VIEWPORT_OCCUPANTS) || 0;
target.data(VIEWPORT_OCCUPANTS, occupants + 1);
};
var ImageViewportProto = ImageViewport.prototype;
/**
* Multiple bindings to a single viewport is supported
*/
ImageViewportProto.setAsActive = function() {
var self = this;
var target = self.$T;
var activeViewportImage = target.data(VIEWPORT_ACTIVE_IMAGE);
if (!activeViewportImage) {
target.append(self.image);
} else if (!activeViewportImage.is(self.image)) {
activeViewportImage.detach();
target.append(self.image);
}
target.data(VIEWPORT_ACTIVE_IMAGE, self.image);
};
ImageViewportProto.resize = function(width, height) {
var self = this;
var target = self.$T;
self.image.css({
width: width,
height: height
});
self.height = target.innerHeight();
self.width = target.innerWidth();
self.radius.x = self.width / 2;
self.radius.y = self.height / 2;
};
// called after resize. see SimpleZoom#_layout for render order
ImageViewportProto.draw = function(x, y) {
this.image.css({
left: _px(x),
top: _px(y)
});
};
ImageViewportProto.destroy = function() {
var self = this;
var target = self.$T;
// plugin built viewport
if (self.removeOnDestroy) {
target.remove();
return;
}
var occupants = target.data(VIEWPORT_OCCUPANTS);
target.data(VIEWPORT_OCCUPANTS, occupants - 1);
self.image.remove();
};
/**
* @class The class underlying the {@link jQuery.fn.simpleZoom} plugin.
* @name SimpleZoom
* @global
*/
var SimpleZoom = function(target, options) {
var self = this;
self.$T = target;
self.options = $.extend(
true,
{},
$.fn[NAMESPACE].defaults,
options
);
self.overlay = null;
self.viewfinder = null;
self.viewport = null;
self.animate = false;
self.scrollPrev = {x: 0, y: 0};
self.local = {x: 0, y: 0};
self.coordinates = {left: 0, top: 0};
self.travel = {x: 0, y: 0};
self.parametric = {x0: 0, y0: 0, x1: 0, y1: 0};
self.fixed = false;
self.scrollEnabled = false;
// true when user is in control of the viewfinder
self.userMode = false;
if (!self.options.src) {
self.options.src = target.data('src') || target.attr('src');
}
self.initialized = false;
// public method refresh
self._deferredLayout = _defer(self._layout, 100, self);
// event handlers
var onload = function() {
self.image = new ViewportImage(self.options.src);
self.image.load(function() {
_call(self.options.onLoad, target);
target.trigger(_eventTypesMap.load);
self._init();
});
};
if (target.is('img') && !target.prop('complete')) {
target.one('load', onload);
}
else {
onload();
}
};
var SimpleZoomProto = SimpleZoom.prototype;
SimpleZoomProto._drawViewport = function() {
var self = this;
var radius = self.viewport.radius;
var travel = self.travel;
var x = travel.x / self.overlay.width;
var y = travel.y / self.overlay.height;
var x0 = radius.x;
var y0 = radius.y;
var x1 = -self.image.width + radius.x;
var y1 = -self.image.height + radius.y;
this.viewport.draw(
(1 - x) * x0 + x * x1,
(1 - y) * y0 + y * y1
);
};
SimpleZoomProto._resizeViewfinder = function() {
var self = this;
var overlay = self.overlay;
var options = self.options;
var width;
var height;
if (self.merged) {
var viewportWidth = options.viewfinder.width;
var viewportHeight = options.viewfinder.height;
var percent = _getPercentage(viewportWidth);
width = (percent !== false) ? overlay.width * percent : viewportWidth;
percent = _getPercentage(viewportHeight);
height = (percent !== false) ? overlay.height * percent : viewportHeight;
}
else {
width = self.viewport.width * overlay.width / self.image.width;
height = self.viewport.height * overlay.height / self.image.height;
}
self.viewfinder.resize(
width,
height,
options.confine
);
};
SimpleZoomProto._drawViewfinder = function() {
var self = this;
self.viewfinder.draw(self.travel.x, self.travel.y);
};
SimpleZoomProto._init = function() {
var self = this;
var options = self.options;
var target = self.$T;
var handlers = self.handlers = {
scroll: function(event) {
self._scrollHandler(event);
},
show: function() {
var showEnabled = options.viewfinder.show.enabled;
if (showEnabled && self.viewfinder.canShow()) {
self._showViewfinder();
}
},
move: function(event) {
if (self.userMode) {
self._updateCoordinates(event);
}
},
moveStart: function(event) {
if (!self.userMode) {
target.trigger(_eventTypesMap.movestart);
_call(options.onMoveStart, target);
var jumpToUser = options.viewfinder.moveStart.jumpTo;
self.userMode = true;
self._enableScroll();
self._updateCoordinates(event, jumpToUser);
self.viewport.setAsActive();
self._layout(jumpToUser);
}
},
moveStop: function() {
target.trigger(_eventTypesMap.movestop);
_call(options.onMoveStop, target);
self._end();
},
resize: function() {
self._deferredLayout(true);
},
hide: function() {
if (options.viewfinder.hide.enabled) {
self._hideViewfinder();
}
}
};
var overlay = self.overlay = new Overlay(
target,
options.className.overlay
);
$.each(['show', 'hide', 'move', 'moveStart', 'moveStop'], function(index, value) {
overlay.registerEvent(options.viewfinder[value].event, handlers[value]);
});
self.viewfinder = new Viewfinder(
self.overlay,
options
);
var viewportElement = $(options.viewport).first();
if (!viewportElement.length) {
self.merged = true;
viewportElement = self.viewfinder.$T;
}
self.viewport = new ImageViewport(
viewportElement,
options,
self.image.$T
);
self.initialized = true;
if (options.autoEnable) {
self.enable();
}
$(_window).on('resize', handlers.resize);
_call(options.onCreate, target);
target.trigger(_eventTypesMap.create);
self._layout(true);
self.viewport.setAsActive();
};
SimpleZoomProto._hideViewfinder = function() {
var self = this;
var target = self.$T;
var options = self.options;
self.viewfinder.hide(options.viewfinder.hide.duration,
function() {
_call(options.beforeHide, target);
self.$T.trigger(_eventTypesMap.beforehide);
}, function() {
_call(options.afterHide, target);
target.trigger(_eventTypesMap.afterhide);
});
};
SimpleZoomProto._end = function() {
var self = this;
if (self.userMode) {
self.userMode = false;
self._disableScroll();
}
};
// first layout call must have noAnimation === true in order for
SimpleZoomProto._layout = function(noAnimation) {
var self = this;
var target = self.$T;
var options = self.options;
var image = self.image;
var overlay = self.overlay;
self.fixed = target.css(POSITION) === FIXED ||
target.parents().filter(function() {
return $(this).css(POSITION) === FIXED;
}).length;
overlay.layout();
image.resize(overlay.width, overlay.height, options.zoom);
if (self.merged) {
self._resizeViewfinder();
self.viewport.resize(image.width, image.height, options.src);
}
else {
self.viewport.resize(image.width, image.height, options.src);
// draws here are necessary to correct for zoom however unless options.confine is toggled
// applyCoordinates method will short circuit because the coordinates are the same
self._resizeViewfinder();
}
self._drawViewport();
self._drawViewfinder();
// respond to change in options.confine
self._setCoordinates(self.coordinates.left, self.coordinates.top);
// animate for dimension update/ confine update
self._applyCoordinates(noAnimation);
target.trigger(_eventTypesMap.refresh);
_call(options.onRefresh, target);
};
SimpleZoomProto._setCoordinates = function(left, top) {
// user absolute coordinates
var self = this;
var coordinates = self.coordinates;
var range;
var local = self.local;
coordinates.left = left;
coordinates.top = top;
if (self.options.confine) {
// clamped coordinates
range = self.viewfinder.range;
local.x = _clamp(coordinates.left, range.minX, range.supX);
local.y = _clamp(coordinates.top, range.minY, range.supY);
} else {
local.x = coordinates.left;
local.y = coordinates.top;
}
};
SimpleZoomProto._applyCoordinates = function(noAnimation) {
var self = this;
if (noAnimation) {
self.travel.x = self.local.x;
self.travel.y = self.local.y;
self._draw();
}
else if (!self.animate) {
self.animate = true;
self._animate();
}
};
SimpleZoomProto._updateCoordinates = function(event, noAnimation) {
var coordinates = _getContentLocalFromOffset(this.overlay.$T, { 'left': event.pageX, 'top': event.pageY});
this.setPosition(coordinates.left, coordinates.top, noAnimation);
};
SimpleZoomProto._scrollHandler = function() {
var self = this;
var scrollTop = $(_window).scrollTop();
var scrollLeft = $(_window).scrollLeft();
self.setPosition(self.coordinates.left + (scrollLeft - self.scrollPrev.x),
self.coordinates.top + (scrollTop - self.scrollPrev.y));
self.scrollPrev.x = scrollLeft;
self.scrollPrev.y = scrollTop;
};
SimpleZoomProto._enableScroll = function() {
var self = this;
if (self.options.enableScroll && !self.fixed && !self.mobile && !self.scrollEnabled) {
self.scrollEnabled = true;
self.scrollPrev.x = $(_window).scrollLeft();
self.scrollPrev.y = $(_window).scrollTop();
$(_window).on({
'scroll': self.handlers.scroll
});
}
};
SimpleZoomProto._disableScroll = function() {
var self = this;
if (self.scrollEnabled) {
self.scrollEnabled = false;
$(_window).off({
'scroll': self.handlers.scroll
});
}
};
SimpleZoomProto._animate = function() {
var self = this;
if (this.animate) {
_requestAnimFrame(function() {
self._redraw();
});
}
};
SimpleZoomProto._redraw = function() { //viewfinder.animate / viewport.animate
var self = this;
var travel = self.travel;
var options = self.options;
var dx = self.local.x - travel.x;
var dy = self.local.y - travel.y;
if (Math.sqrt(dx * dx + dy * dy) > options.tolerance) {
travel.x += dx * options.smoothing;
travel.y += dy * options.smoothing;
this._draw();
} else {
self.animate = false;
}
self._animate();
};
SimpleZoomProto._draw = function() {
// boundary check for scroll
// only hide if in user mode
var self = this;
var travel = self.travel;
if (self.scrollEnabled &&
(travel.y < 0 ||
travel.y > self.overlay.height ||
travel.x < 0 ||
travel.x > self.overlay.width)) {
self.animate = false;
self._end();
// return false;
} else {
self._drawViewport();
self._drawViewfinder();
}
};
var _invalidOption = function(optionName) {
$.error('"' + optionName + '" is an invalid option.');
};
SimpleZoomProto._getOption = function(optionName) {
if ($.fn[NAMESPACE].defaults.hasOwnProperty(optionName)) {
var option = this.options[optionName];
if (_is(option, 'object')) {
return $.extend(true, {}, option);
}
else {
return option;
}
}
else {
_invalidOption(optionName);
}
};
SimpleZoomProto._setOption = function(optionName, value) {
var defaults = $.fn[NAMESPACE].defaults;
var self = this;
if (defaults.hasOwnProperty(optionName)) {
if (_is((defaults[optionName]), 'object') && !_is(value, 'object')) {
$.error('"' + optionName + ' must be an Object.');
return false;
}
var extension = {};
extension[optionName] = value;
self.options = $.extend(true, self.options, extension);
return true;
}
else {
_invalidOption(optionName);
return false;
}
};
// PUBLIC METHODS
/**
* Get an object exposing all public methods of the plugin.
* @function SimpleZoom#getSimpleZoom
* @returns {SimpleZoom}
*/
SimpleZoomProto.getSimpleZoom = function() {
var clone = {};
var self = this;
var iterator = function(method) {
return function() {
var result = SimpleZoomProto[method].apply(self, _toArray(arguments));
return NULL === result ? clone : result;
};
};
for (var method in SimpleZoomProto) {
if (method.indexOf('_') !== 0) {
clone[method] = iterator(method);
}
}
return clone;
};
var _methodError = function(methodName) {
$.error('Cannot invoke the "' + methodName + '" method on uninitialized plugin.');
};
/**
* Destroys the plugin instance.
* @function SimpleZoom#destroy
* @param {Boolean} [removeEvents] If true then any plugin specified events bound to the target will be removed.
* @returns {(jQuery|SimpleZoom)}
*/
SimpleZoomProto.destroy = function(removeEvents) {
var self = this;
var target = self.$T;
var options = self.options;
if (self.initialized) {
target.trigger(_eventTypesMap.beforedestroy);
_call(options.beforeDestroy, target);
$(_window).off('resize', self.handlers.resize);
self.overlay.destroy();
self.viewport.destroy();
self.viewfinder.destroy();
target.removeData(SIMPLEZOOM_ID);
self.initialized = false;
// plugin is no longer accessible via target; headless API reference may remain
target.trigger(_eventTypesMap.destroy);
if (removeEvents) {
$.each(_eventTypes, function(index, event) {
target.off(event);
});
}
_call(options.onDestroy, target);
}
else {
_methodError('destroy');
}
return NULL;
};
/**
* Disables user interaction.
* @function SimpleZoom#disable
* @returns {(jQuery|SimpleZoom)}
*/
SimpleZoomProto.disable = function() {
var self = this;
if (self.initialized) {
if (self.enabled) {
self._end();
self.enabled = false;
self.overlay.disable();
}
} else {
_methodError('disable');
}
return NULL;
};
/**
* Enable user interaction.
* @function SimpleZoom#enable
* @returns {(jQuery|SimpleZoom)}
*/
SimpleZoomProto.enable = function() {
var self = this;
if (self.initialized) {
if (!self.enabled) {
self.enabled = true;
self.overlay.enable();
}
} else {
_methodError('enable');
}
return NULL;
};
/**
* Get the element which is overlaid upon the plugin target in order to capture user events.
* @function SimpleZoom#getOverlay
* @returns {jQuery}
*/
SimpleZoomProto.getOverlay = function() {
if (this.initialized) {
return this.overlay.$T;
}
else {
_methodError('getOverlay');
}
return NULL;
};
/**
* @typedef {Object} SimpleZoom~position
* @property {number} top Distance from the center of the viewfinder to the top of the targets content region.
* @property {number} left Distance from the center of the viewfinder to the left side of the targets content region.
* @property {number} minLeft The minimum value the <code>left</code> property.
* @property {number} maxLeft The maximum value of the <code>left</code> property.
* @property {number} minTop The minimum value of the <code>top</code> property.
* @property {number} maxTop The maximum value of the <code>top</code> property.
*/
/**
* Get an object detailing the current position and positional constraints of the viewfinder.
* @function SimpleZoom#getPosition
* @returns {SimpleZoom~position}
*/
SimpleZoomProto.getPosition = function() {
var self = this;
var range;
if (self.initialized) {
range = self.viewfinder.range;
return {
top: self.travel.y,
left: self.travel.x,
minLeft: range.minX,
maxLeft: range.supX,
minTop: range.minY,
maxTop: range.supY
};
}
else {
_methodError('getPosition');
}
return NULL;
};
/**
* Hides the plugin viewfinder.
* @function SimpleZoom#hide
* @returns {(jQuery|SimpleZoom)}
*/
SimpleZoomProto.hide = function() {
var self = this;
if (self.initialized) {
self._hideViewfinder();
}
else {
_methodError('hide');
}
return NULL;
};
/**
* Determine whether or not the plugin is enabled for user interaction.
* @function SimpleZoom#isEnabled
* @returns {boolean}
*/
SimpleZoomProto.isEnabled = function() {
return this.enabled;
};
/**
* Get the initialization state of the plugin. The plugin is not
* considered initialized until after the <code>create</code> event has occured.
* @function SimpleZoom#isInitialized
* @returns {boolean}
*/
SimpleZoomProto.isInitialized = function() {
return this.initialized;
};
/**
* Get the visibility state of the viewfinder.
* @function SimpleZoom#isVisible
* @returns {boolean}
*/
SimpleZoomProto.isVisible = function() {
return this.viewfinder && this.viewfinder.visible;
};
/**
* Translate a string of plugin events into their global equivalent.
* @param {string} eventType
* @returns {string}
* @private
*/
var _translateEvents = function(eventType) {
var eventTypes = eventType.split(/\s+/);
// $.map The function can return null or undefined, to remove the item
return $.map(eventTypes,function(type) {
type = type.toLowerCase();
if (type.indexOf(NAMESPACE.toLowerCase()) === 0) {
return type;
}
if (_eventTypesMap.hasOwnProperty(type)) {
return _eventTypesMap[type];
}
}).join(' ');
};
/**
* Unbind a handler to the specified event. See {@link SimpleZoom#on} for more information.
* @function SimpleZoom#off
* @param {string} eventType
* @param {function} handler
* @returns {(jQuery|SimpleZoom)}
*/
SimpleZoomProto.off = function(eventType, handler) {
if (_isString(eventType)) {
var translatedEvents = _translateEvents(eventType);
this.$T.off(translatedEvents, handler);
}
return NULL;
};
/**
* Bind a handler to the specified <code>event</code>.
* @function SimpleZoom#on
* @param {string} event A single event or a whitespace separated list.
* @param {function} handler
* @returns {(jQuery|SimpleZoom)}
*/
SimpleZoomProto.on = function(event, handler) {
if (_isString(event)) {
var translatedEvents = _translateEvents(event);
this.$T.on(translatedEvents, handler);
}
return NULL;
};
/**
* This method is used to set or get plugin options.
* <ul><li>If <code>optionNameOrObject</code> is not specified then a complete copy of the plugins options is returned.</li><li>
* If <code>optionNameOrObject</code> is a string and no <code>value</code> is specified then a copy of the specified option
* is returned.</li><li>
* If <code>optionNameOrObject</code> and <code>value</code> are both defined then the corresponding option is updated with the
* specified <code>value</code>.</li><li>
* If <code>optionNameOrObject</code> refers to an object property of {@link SimpleZoom~options} then that object
* is extended by the provided <code>value</code>.</li></ul>
* @function SimpleZoom#option
* @param {(Object|string)} [optionNameOrObject]
* @param {(Object|string)} [value]
* @returns {*}
*/
SimpleZoomProto.option = function(optionNameOrObject, value) {
var self = this;
var refresh = false;
if (_isUndefined(optionNameOrObject)) {
return $.extend(true, {}, self.options);
}
// setter $('.selector').simpleZoom('option', {zoom: 2});
else if ($.type(optionNameOrObject) === 'object') {
$.each(optionNameOrObject, function(optionName, value) {
refresh = self._setOption(optionName, value) || refresh;
});
}
else if (_isString(optionNameOrObject)) {
if (_isUndefined(value)) {
// getter $('.selector').simpleZoom('option', 'zoom');
return self._getOption(optionNameOrObject);
}
else {
// setter $('.selector').simpleZoom('option', 'zoom', 2);
refresh = self._setOption(optionNameOrObject, value);
}
}
if (self.isInitialized() && refresh) {
self._layout();
}
return NULL;
};
/**
* Updates the layout. This method should be invoked whenever the target changes position or is resized.
* By default a throttled version of this method is invoked whenever the browser window is resized.
* @function SimpleZoom#refresh
* @returns {(jQuery|SimpleZoom)}
*/
SimpleZoomProto.refresh = function() {
if (this.initialized) {
this._layout();
}
else {
_methodError('refresh');
}
return NULL;
};
/**
* <p>Set the position of the viewfinder relative to the content box of the plugin target. The viewfinder
* is positioned so that its center coincides with this point. If the <code>confine</code> {@link SimpleZoom~options option} is
* <code>true</code> then the viewfinder's position will be clamped to ensure that it remains within the
* content box of the target.</p>
* @param {number} left The distance in CSS pixels from the left side of the target's content box to the viewfinder's
* center.
* @param {number} top The distance in CSS pixels from the top of the target's content box to the viewfinder's
* center.
* @param {boolean} [noAnimation] Determines whether or not the invocation should trigger and animation
* or jump to the final specified coordinates.
*/
SimpleZoomProto.setPosition = function(left, top, noAnimation) {
var self = this;
if (self.initialized) {
self._setCoordinates(left, top);
self._applyCoordinates(noAnimation);
}
else {
_methodError('setPosition');
}
return NULL;
};
SimpleZoomProto._showViewfinder = function() {
var self = this;
var target = self.$T;
var options = self.options;
self.viewfinder.show(
options.viewfinder.show.duration,
function() {
_call(options.beforeShow, target);
target.trigger(_eventTypesMap.beforeshow);
}, function() {
_call(options.afterShow, target);
target.trigger(_eventTypesMap.aftershow);
});
};
/**
* Show the viewfinder.
* @function SimpleZoom#show
* @returns {jQuery|SimpleZoom}
*/
SimpleZoomProto.show = function() {
var self = this;
if (self.initialized) {
self.viewport.setAsActive();
self._layout();
self._showViewfinder();
}
else {
_methodError('show');
}
return NULL;
};
/**
* Plugin options.
* @typedef {Object} SimpleZoom~options
* @property {Function} [afterHide=null] A callback to be invoked after the viewfinder is hidden. Note that all
* options specified callbacks will be invoked with <code>this</code> set to be the plugin's target.
* @property {Function} [afterShow=null] A callback to be invoked after the viewfinder is shown.
* @property {boolean} [autoEnable=true] Determine whether or not {@link SimpleZoom#enable} is invoked upon initialization.
* @property {Function} [beforeDestroy=null] A callback to be invoked before the plugin is destroyed.
* @property {Function} [beforeHide=null] A callback to be invoked when the viewfinder hide animation begins.
* @property {Function} [beforeShow=null] A callback to be invoked when the viewfinder show animation begins.
* @property {Object} [className]
* @property {string} [className.overlay="simplezoom-overlay"] The CSS class to be assigned to the element which overlays the plugin target.
* @property {string} [className.viewport="simplezoom-viewport"] The CSS class to be assigned to the designated viewport.
* @property {string} [className.viewfinder="simplezoom-viewfinder'] The CSS class to be assigned to the viewfinder.
* @property {boolean} [confine=true] Determines if the viewfinder should be confined to the boundary of the target.
* @property {boolean} [enableScroll=false] When enabled the plugin will track scroll events on the window and
* animate the viewfinder accordingly. This option will be automatically disabled if <code>mobile</code> is <code>true</code> or the
* target lies within a scrollable region which is not the window.
* @property {boolean} [mobile=false] Enable mobile support. Currently all functionality is supported
* except for <code>nableScroll</code>
* @property {Function} [onCreate=null] A callback to be invoked once the plugin has completed initialization.
* Note that most plugin methods will throw an exception prior to this event occurring.
* @property {Function} [onDestroy=null] A callback to be invoked once the plugin has been {@link SimpleZoom#destroy destroyed}.
* @property {Function} [onLoad=null] A callback to be invoked once the image resources associated with the plugin
* have been loaded.
* @property {Function} [onMoveStart=null] A callback to be invoked when the user takes control of the viewfinder.
* @property {Function} [onMoveStop=null] A callback to be invoked when the user cedes control of the viewfinder.
* @property {Function} [onRefresh=null] A callback to be invoked every time the plugin's layout is recalculated.
* @property {number} [smoothing=0.35] A value on the range 0 to 1, inclusive, which determines how smooth the
* viewfinder animation is. A value of 0 disables animation while 1 causes the viewfinder to remain stationary.
* @property {string} [src=null] The URL of an image resource to be displayed within the plugins viewport.
* @property {Object} [viewfinder]
* * @property {Object} [viewfinder.hide]
* @property {(string|number)} [viewfinder.height] Determines the height of the viewfinder when it is presented as
* a magnifying lens, e.g., no <code>viewport</code> has been specified.
* @property {string} [viewfinder.hide.event='mouseout'] The trigger event for the viewfinder's hide animation.
* @property {number} [viewfinder.hide.duration=100] The duration of the viewfinder's hide animation in milliseconds.
* @property {Object} [viewfinder.show]
* @property {string} [viewfinder.show.event='mouseover'] The event upon which the viewfinder will be shown.
* @property {number} [viewfinder.show.duration=250] The duration of the viewfinder's show animation in milliseconds.
* @property {string} [viewfinder.move.event="mousemove"] The event used to determine the position of the viewfinder.
* The event type must produce an <code>event</code> object with <code>pageX</code> and <code>pageY</code> properties.
* @property {boolean} [viewfinder.moveStart.jumpTo=true] 'If <code>true</code> the viewfinder will jump to the users
* position when the <code>viewfinder.moveStart.event</code> event is triggered, else the viewfinder is animated to
* the users position from it's previous position.
* @property {string} [viewfinder.moveStart.event="mouseover mousemove"] The event which grants control of the viewfinder
* to the user. Once triggered the event will not be triggered until after a <code>viewfinder.moveStop</code> event
* has occurred.
* @property {string} [viewfinder.moveStop.event="mouseout"] The event upon which the user cedes control of the viewfinder.
* @property {(string|number)} [viewfinder.width='33%'] Analogous to <code>viewfinder.height</code>.
* @property {(string|jQuery)} [viewport] Designates an element as the plugins active viewport. If a collection
* is specified then the first element is selected.
* @property {number} [zoom=null] A decimal representing the percent magnification level.
*/
/**
* <p><i>A jQuery plugin for magnifying images.</i></p>
* <h2>Description</h2>
* <p>The SimpleZoom jQuery plugin creates a user controlled viewfinder element which is used to designate
* a region of the target image to be enlarged and then displayed within the plugins designated viewport element. By default
* the viewport and viewfinder are the same element creating the impression of a magnifying lens. Alternatively,
* a separate element may be specified as the active viewport.</p>
* <h3>Usage</h3>
* <h4>Initializing the plugin</h4>
* <pre><code>$("#selector").simpleZoom();</code></pre>
* <h4>Initializing the plugin with options</h4>
* <p>The plugin may be invoked with an {@link SimpleZoom~options} object containing properties to override the defaults.
* The defaults may be accessed at {@link jQuery.fn.simpleZoom.defaults}.</p>
* <pre><code>$("#selector").simpleZoom({
* zoom: 0.25 //set a 25% magnification
* });</code></pre>
* <h3>Plugin Methods</h3>
* <p>The plugin exposes a collection of API methods for controlling it's behavior. There are two general ways to access these methods:</p>
* <p>Once instantiated, a number of plugin methods become available to control the behavior of the plugin.
* For example, the {@link SimpleZoom#option} method may be used to set the plugins scale factor as follows:</p>
* <pre><code>$("#selector").simpleZoom(
* "option", // plugin method name
* "zoom", // first method argument
* 1.25 // second method argument
* );</code></pre>
* <h4>Direct invocation</h4>
* <p>A reference to the plugin's underlying {@link SimpleZoom} instance is returned by the
{@link SimpleZoom#getSimpleZoom}} method allowing for direct invocation of plugin methods.</p>
* <pre><code>$("#selector").simpleZoom("getSimpleZoom")
* .option("zoom", 0.25) //setter methods can be chained
* .setPosition(0, 0);</code></pre>
* <h3>Notes</h3>
* <ul><li>If the plugin target is a collection of elements then the plugin is only applied to the first element.</li>
* <li>In order to function properly the plugin requires additional CSS included in the download package.</li></ul>
* @function jQuery.fn.simpleZoom
* @param {(string|SimpleZoom~options)} [methodOrOptions] A method name or plugin options.
*/
$.fn[NAMESPACE] = function(methodOrOptions) {
var self = this;
if (!self.length) {
return self;
}
var target = self.first();
var args = _toArray(arguments, 1);
var instance = target.data(SIMPLEZOOM_ID);
// CASE: action method (public method on PLUGIN class)
if (instance &&
_isString(methodOrOptions) &&
_is(instance[methodOrOptions], FUNCTION) &&
methodOrOptions.indexOf('_') !== 0) {
var result = instance[methodOrOptions].apply(instance, args);
if (result !== NULL) {
return result;
}
// CASE: argument is options object or empty = initialise
} else if (_is(methodOrOptions, 'object') || !methodOrOptions) {
if (instance) {
instance.destroy();
}
instance = new SimpleZoom(target, methodOrOptions);
target.data(SIMPLEZOOM_ID, instance);
// CASE: method called before init
} else if (!instance) {
$.error('Plugin must be initialised before calling: ' + methodOrOptions);
// CASE: invalid method
} else {
$.error('Method ' + methodOrOptions + ' does not exist.');
}
return self;
};
/**
* Plugin default options.
* @type {SimpleZoom~options}
* @name defaults
* @memberOf jQuery.fn.simpleZoom
*/
$.fn[NAMESPACE].defaults = {
afterHide: null,
afterShow: null,
autoEnable: true,
beforeDestroy: null,
beforeHide: null,
beforeShow: null,
className: {
overlay: 'simplezoom-overlay',
viewfinder: 'simplezoom-viewfinder',
viewport: 'simplezoom-viewport'
},
confine: true,
enableScroll: false,
mobile: false,
onCreate: null,
onDestroy: null,
onLoad: null,
onMoveStop: null,
onMoveStart: null,
onRefresh: null,
smoothing: 0.40,
src: '',
tolerance: 0.0625,
viewfinder: {
height: '33%',
hide: {
enabled: true,
event: 'mouseout',
duration: 100
},
show: {
enabled: true,
event: 'mouseover mousemove',
duration: 150
},
move: {
event: 'mousemove'
},
moveStart: {
jumpTo: true,
event: 'mouseover mousemove'
},
moveStop: {
event: 'mouseout'
},
width: '33%'
},
viewport: null,
zoom: null
};
$.fn[NAMESPACE].getEvents = function() {
return $.extend(true, {}, _eventTypesMap);
};
$.fn[NAMESPACE].extend = function(methods) {
$.extend(true, SimpleZoomProto, methods);
};
}(jQuery));