Source: jquery.simplezoom.js

/**
 * @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));