/*

* blueimp Gallery JS 2.14.0
* https://github.com/blueimp/Gallery
*
* Copyright 2013, Sebastian Tschan
* https://blueimp.net
*
* Swipe implementation based on
* https://github.com/bradbirdsall/Swipe
*
* Licensed under the MIT license:
* http://www.opensource.org/licenses/MIT
*/

/* global define, window, document, DocumentTouch */

(function (factory) {

'use strict';
if (typeof define === 'function' && define.amd) {
    // Register as an anonymous AMD module:
    define(['./blueimp-helper'], factory);
} else {
    // Browser globals:
    window.blueimp = window.blueimp || {};
    window.blueimp.Gallery = factory(
        window.blueimp.helper || window.jQuery
    );
}

}(function ($) {

'use strict';

function Gallery(list, options) {
    if (document.body.style.maxHeight === undefined) {
        // document.body.style.maxHeight is undefined on IE6 and lower
        return null;
    }
    if (!this || this.options !== Gallery.prototype.options) {
        // Called as function instead of as constructor,
        // so we simply return a new instance:
        return new Gallery(list, options);
    }
    if (!list || !list.length) {
        this.console.log(
            'blueimp Gallery: No or empty list provided as first argument.',
            list
        );
        return;
    }
    this.list = list;
    this.num = list.length;
    this.initOptions(options);
    this.initialize();
}

$.extend(Gallery.prototype, {

    options: {
        // The Id, element or querySelector of the gallery widget:
        container: '#blueimp-gallery',
        // The tag name, Id, element or querySelector of the slides container:
        slidesContainer: 'div',
        // The tag name, Id, element or querySelector of the title element:
        titleElement: 'h3',
        // The class to add when the gallery is visible:
        displayClass: 'blueimp-gallery-display',
        // The class to add when the gallery controls are visible:
        controlsClass: 'blueimp-gallery-controls',
        // The class to add when the gallery only displays one element:
        singleClass: 'blueimp-gallery-single',
        // The class to add when the left edge has been reached:
        leftEdgeClass: 'blueimp-gallery-left',
        // The class to add when the right edge has been reached:
        rightEdgeClass: 'blueimp-gallery-right',
        // The class to add when the automatic slideshow is active:
        playingClass: 'blueimp-gallery-playing',
        // The class for all slides:
        slideClass: 'slide',
        // The slide class for loading elements:
        slideLoadingClass: 'slide-loading',
        // The slide class for elements that failed to load:
        slideErrorClass: 'slide-error',
        // The class for the content element loaded into each slide:
        slideContentClass: 'slide-content',
        // The class for the "toggle" control:
        toggleClass: 'toggle',
        // The class for the "prev" control:
        prevClass: 'prev',
        // The class for the "next" control:
        nextClass: 'next',
        // The class for the "close" control:
        closeClass: 'close',
        // The class for the "play-pause" toggle control:
        playPauseClass: 'play-pause',
        // The list object property (or data attribute) with the object type:
        typeProperty: 'type',
        // The list object property (or data attribute) with the object title:
        titleProperty: 'title',
        // The list object property (or data attribute) with the object URL:
        urlProperty: 'href',
        // The gallery listens for transitionend events before triggering the
        // opened and closed events, unless the following option is set to false:
        displayTransition: true,
        // Defines if the gallery slides are cleared from the gallery modal,
        // or reused for the next gallery initialization:
        clearSlides: true,
        // Defines if images should be stretched to fill the available space,
        // while maintaining their aspect ratio (will only be enabled for browsers
        // supporting background-size="contain", which excludes IE < 9).
        // Set to "cover", to make images cover all available space (requires
        // support for background-size="cover", which excludes IE < 9):
        stretchImages: false,
        // Toggle the controls on pressing the Return key:
        toggleControlsOnReturn: true,
        // Toggle the automatic slideshow interval on pressing the Space key:
        toggleSlideshowOnSpace: true,
        // Navigate the gallery by pressing left and right on the keyboard:
        enableKeyboardNavigation: true,
        // Close the gallery on pressing the Esc key:
        closeOnEscape: true,
        // Close the gallery when clicking on an empty slide area:
        closeOnSlideClick: true,
        // Close the gallery by swiping up or down:
        closeOnSwipeUpOrDown: true,
        // Emulate touch events on mouse-pointer devices such as desktop browsers:
        emulateTouchEvents: true,
        // Stop touch events from bubbling up to ancestor elements of the Gallery:
        stopTouchEventsPropagation: false,
        // Hide the page scrollbars: 
        hidePageScrollbars: true,
        // Stops any touches on the container from scrolling the page:
        disableScroll: true,
        // Carousel mode (shortcut for carousel specific options):
        carousel: false,
        // Allow continuous navigation, moving from last to first
        // and from first to last slide:
        continuous: true,
        // Remove elements outside of the preload range from the DOM:
        unloadElements: true,
        // Start with the automatic slideshow:
        startSlideshow: false,
        // Delay in milliseconds between slides for the automatic slideshow:
        slideshowInterval: 5000,
        // The starting index as integer.
        // Can also be an object of the given list,
        // or an equal object with the same url property:
        index: 0,
        // The number of elements to load around the current index:
        preloadRange: 2,
        // The transition speed between slide changes in milliseconds:
        transitionSpeed: 400,
        // The transition speed for automatic slide changes, set to an integer
        // greater 0 to override the default transition speed:
        slideshowTransitionSpeed: undefined,
        // The event object for which the default action will be canceled
        // on Gallery initialization (e.g. the click event to open the Gallery):
        event: undefined,
        // Callback function executed when the Gallery is initialized.
        // Is called with the gallery instance as "this" object:
        onopen: undefined,
        // Callback function executed when the Gallery has been initialized
        // and the initialization transition has been completed.
        // Is called with the gallery instance as "this" object:
        onopened: undefined,
        // Callback function executed on slide change.
        // Is called with the gallery instance as "this" object and the
        // current index and slide as arguments:
        onslide: undefined,
        // Callback function executed after the slide change transition.
        // Is called with the gallery instance as "this" object and the
        // current index and slide as arguments:
        onslideend: undefined,
        // Callback function executed on slide content load.
        // Is called with the gallery instance as "this" object and the
        // slide index and slide element as arguments:
        onslidecomplete: undefined,
        // Callback function executed when the Gallery is about to be closed.
        // Is called with the gallery instance as "this" object:
        onclose: undefined,
        // Callback function executed when the Gallery has been closed
        // and the closing transition has been completed.
        // Is called with the gallery instance as "this" object:
        onclosed: undefined
    },

    carouselOptions: {
        hidePageScrollbars: false,
        toggleControlsOnReturn: false,
        toggleSlideshowOnSpace: false,
        enableKeyboardNavigation: false,
        closeOnEscape: false,
        closeOnSlideClick: false,
        closeOnSwipeUpOrDown: false,
        disableScroll: false,
        startSlideshow: true
    },

    console: window.console && typeof window.console.log === 'function' ?
        window.console :
        {log: function () {}},

    // Detect touch, transition, transform and background-size support:
    support: (function (element) {
        var support = {
                touch: window.ontouchstart !== undefined ||
                    (window.DocumentTouch && document instanceof DocumentTouch)
            },
            transitions = {
                webkitTransition: {
                    end: 'webkitTransitionEnd',
                    prefix: '-webkit-'
                },
                MozTransition: {
                    end: 'transitionend',
                    prefix: '-moz-'
                },
                OTransition: {
                    end: 'otransitionend',
                    prefix: '-o-'
                },
                transition: {
                    end: 'transitionend',
                    prefix: ''
                }
            },
            elementTests = function () {
                var transition = support.transition,
                    prop,
                    translateZ;
                document.body.appendChild(element);
                if (transition) {
                    prop = transition.name.slice(0, -9) + 'ransform';
                    if (element.style[prop] !== undefined) {
                        element.style[prop] = 'translateZ(0)';
                        translateZ = window.getComputedStyle(element)
                            .getPropertyValue(transition.prefix + 'transform');
                        support.transform = {
                            prefix: transition.prefix,
                            name: prop,
                            translate: true,
                            translateZ: !!translateZ && translateZ !== 'none'
                        };
                    }
                }
                if (element.style.backgroundSize !== undefined) {
                    support.backgroundSize = {};
                    element.style.backgroundSize = 'contain';
                    support.backgroundSize.contain = window
                            .getComputedStyle(element)
                            .getPropertyValue('background-size') === 'contain';
                    element.style.backgroundSize = 'cover';
                    support.backgroundSize.cover = window
                            .getComputedStyle(element)
                            .getPropertyValue('background-size') === 'cover';
                }
                document.body.removeChild(element);
            };
        (function (support, transitions) {
            var prop;
            for (prop in transitions) {
                if (transitions.hasOwnProperty(prop) &&
                        element.style[prop] !== undefined) {
                    support.transition = transitions[prop];
                    support.transition.name = prop;
                    break;
                }
            }
        }(support, transitions));
        if (document.body) {
            elementTests();
        } else {
            $(document).on('DOMContentLoaded', elementTests);
        }
        return support;
        // Test element, has to be standard HTML and must not be hidden
        // for the CSS3 tests using window.getComputedStyle to be applicable:
    }(document.createElement('div'))),

    requestAnimationFrame: window.requestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        window.mozRequestAnimationFrame,

    initialize: function () {
        this.initStartIndex();
        if (this.initWidget() === false) {
            return false;
        }
        this.initEventListeners();
        // Load the slide at the given index:
        this.onslide(this.index);
        // Manually trigger the slideend event for the initial slide:
        this.ontransitionend();
        // Start the automatic slideshow if applicable:
        if (this.options.startSlideshow) {
            this.play();
        }
    },

    slide: function (to, speed) {
        window.clearTimeout(this.timeout);
        var index = this.index,
            direction,
            naturalDirection,
            diff;
        if (index === to || this.num === 1) {
            return;
        }
        if (!speed) {
            speed = this.options.transitionSpeed;
        }
        if (this.support.transition) {
            if (!this.options.continuous) {
                to = this.circle(to);
            }
            // 1: backward, -1: forward:
            direction = Math.abs(index - to) / (index - to);
            // Get the actual position of the slide:
            if (this.options.continuous) {
                naturalDirection = direction;
                direction = -this.positions[this.circle(to)] / this.slideWidth;
                // If going forward but to < index, use to = slides.length + to
                // If going backward but to > index, use to = -slides.length + to
                if (direction !== naturalDirection) {
                    to = -direction * this.num + to;
                }
            }
            diff = Math.abs(index - to) - 1;
            // Move all the slides between index and to in the right direction:
            while (diff) {
                diff -= 1;
                this.move(
                    this.circle((to > index ? to : index) - diff - 1),
                    this.slideWidth * direction,
                    0
                );
            }
            to = this.circle(to);
            this.move(index, this.slideWidth * direction, speed);
            this.move(to, 0, speed);
            if (this.options.continuous) {
                this.move(
                    this.circle(to - direction),
                    -(this.slideWidth * direction),
                    0
                );
            }
        } else {
            to = this.circle(to);
            this.animate(index * -this.slideWidth, to * -this.slideWidth, speed);
        }
        this.onslide(to);
    },

    getIndex: function () {
        return this.index;
    },

    getNumber: function () {
        return this.num;
    },

    prev: function () {
        if (this.options.continuous || this.index) {
            this.slide(this.index - 1);
        }
    },

    next: function () {
        if (this.options.continuous || this.index < this.num - 1) {
            this.slide(this.index + 1);
        }
    },

    play: function (time) {
        var that = this;
        window.clearTimeout(this.timeout);
        this.interval = time || this.options.slideshowInterval;
        if (this.elements[this.index] > 1) {
            this.timeout = this.setTimeout(
                (!this.requestAnimationFrame && this.slide) || function (to, speed) {
                    that.animationFrameId = that.requestAnimationFrame.call(
                        window,
                        function () {
                            that.slide(to, speed);
                        }
                    );
                },
                [this.index + 1, this.options.slideshowTransitionSpeed],
                this.interval
            );
        }
        this.container.addClass(this.options.playingClass);
    },

    pause: function () {
        window.clearTimeout(this.timeout);
        this.interval = null;
        this.container.removeClass(this.options.playingClass);
    },

    add: function (list) {
        var i;
        if (!list.concat) {
            // Make a real array out of the list to add:
            list = Array.prototype.slice.call(list);
        }
        if (!this.list.concat) {
            // Make a real array out of the Gallery list:
            this.list = Array.prototype.slice.call(this.list);
        }
        this.list = this.list.concat(list);
        this.num = this.list.length;
        if (this.num > 2 && this.options.continuous === null) {
            this.options.continuous = true;
            this.container.removeClass(this.options.leftEdgeClass);
        }
        this.container
            .removeClass(this.options.rightEdgeClass)
            .removeClass(this.options.singleClass);
        for (i = this.num - list.length; i < this.num; i += 1) {
            this.addSlide(i);
            this.positionSlide(i);
        }
        this.positions.length = this.num;
        this.initSlides(true);
    },

    resetSlides: function () {
        this.slidesContainer.empty();
        this.slides = [];
    },

    handleClose: function () {
        var options = this.options;
        this.destroyEventListeners();
        // Cancel the slideshow:
        this.pause();
        this.container[0].style.display = 'none';
        this.container
            .removeClass(options.displayClass)
            .removeClass(options.singleClass)
            .removeClass(options.leftEdgeClass)
            .removeClass(options.rightEdgeClass);
        if (options.hidePageScrollbars) {
            document.body.style.overflow = this.bodyOverflowStyle;
        }
        if (this.options.clearSlides) {
            this.resetSlides();
        }
        if (this.options.onclosed) {
            this.options.onclosed.call(this);
        }
    },

    close: function () {
        var that = this,
            closeHandler = function (event) {
                if (event.target === that.container[0]) {
                    that.container.off(
                        that.support.transition.end,
                        closeHandler
                    );
                    that.handleClose();
                }
            };
        if (this.options.onclose) {
            this.options.onclose.call(this);
        }
        if (this.support.transition && this.options.displayTransition) {
            this.container.on(
                this.support.transition.end,
                closeHandler
            );
            this.container.removeClass(this.options.displayClass);
        } else {
            this.handleClose();
        }
    },

    circle: function (index) {
        // Always return a number inside of the slides index range:
        return (this.num + (index % this.num)) % this.num;
    },

    move: function (index, dist, speed) {
        this.translateX(index, dist, speed);
        this.positions[index] = dist;
    },

    translate: function (index, x, y, speed) {
        var style = this.slides[index].style,
            transition = this.support.transition,
            transform = this.support.transform;
        style[transition.name + 'Duration'] = speed + 'ms';
        style[transform.name] = 'translate(' + x + 'px, ' + y + 'px)' +
            (transform.translateZ ? ' translateZ(0)' : '');
    },

    translateX: function (index, x, speed) {
        this.translate(index, x, 0, speed);
    },

    translateY: function (index, y, speed) {
        this.translate(index, 0, y, speed);
    },

    animate: function (from, to, speed) {
        if (!speed) {
            this.slidesContainer[0].style.left = to + 'px';
            return;
        }
        var that = this,
            start = new Date().getTime(),
            timer = window.setInterval(function () {
                var timeElap = new Date().getTime() - start;
                if (timeElap > speed) {
                    that.slidesContainer[0].style.left = to + 'px';
                    that.ontransitionend();
                    window.clearInterval(timer);
                    return;
                }
                that.slidesContainer[0].style.left = (((to - from) *
                    (Math.floor((timeElap / speed) * 100) / 100)) +
                        from) + 'px';
            }, 4);
    },

    preventDefault: function (event) {
        if (event.preventDefault) {
            event.preventDefault();
        } else {
            event.returnValue = false;
        }
    },

    stopPropagation: function (event) {
        if (event.stopPropagation) {
            event.stopPropagation();
        } else {
            event.cancelBubble = true;
        }
    },

    onresize: function () {
        this.initSlides(true);
    },

    onmousedown: function (event) {
        // Trigger on clicks of the left mouse button only
        // and exclude video elements:
        if (event.which && event.which === 1 &&
                event.target.nodeName !== 'VIDEO') {
            // Preventing the default mousedown action is required
            // to make touch emulation work with Firefox:
            event.preventDefault();
            (event.originalEvent || event).touches = [{
                pageX: event.pageX,
                pageY: event.pageY
            }];
            this.ontouchstart(event);
        }
    },

    onmousemove: function (event) {
        if (this.touchStart) {
            (event.originalEvent || event).touches = [{
                pageX: event.pageX,
                pageY: event.pageY
            }];
            this.ontouchmove(event);
        }
    },

    onmouseup: function (event) {
        if (this.touchStart) {
            this.ontouchend(event);
            delete this.touchStart;
        }
    },

    onmouseout: function (event) {
        if (this.touchStart) {
            var target = event.target,
                related = event.relatedTarget;
            if (!related || (related !== target &&
                    !$.contains(target, related))) {
                this.onmouseup(event);
            }
        }
    },

    ontouchstart: function (event) {
        if (this.options.stopTouchEventsPropagation) {
            this.stopPropagation(event);
        }
        // jQuery doesn't copy touch event properties by default,
        // so we have to access the originalEvent object:
        var touches = (event.originalEvent || event).touches[0];
        this.touchStart = {
            // Remember the initial touch coordinates:
            x: touches.pageX,
            y: touches.pageY,
            // Store the time to determine touch duration:
            time: Date.now()
        };
        // Helper variable to detect scroll movement:
        this.isScrolling = undefined;
        // Reset delta values:
        this.touchDelta = {};
    },

    ontouchmove: function (event) {
        if (this.options.stopTouchEventsPropagation) {
            this.stopPropagation(event);
        }
        // jQuery doesn't copy touch event properties by default,
        // so we have to access the originalEvent object:
        var touches = (event.originalEvent || event).touches[0],
            scale = (event.originalEvent || event).scale,
            index = this.index,
            touchDeltaX,
            indices;
        // Ensure this is a one touch swipe and not, e.g. a pinch:
        if (touches.length > 1 || (scale && scale !== 1)) {
            return;
        }
        if (this.options.disableScroll) {
            event.preventDefault();
        }
        // Measure change in x and y coordinates:
        this.touchDelta = {
            x: touches.pageX - this.touchStart.x,
            y: touches.pageY - this.touchStart.y
        };
        touchDeltaX = this.touchDelta.x;
        // Detect if this is a vertical scroll movement (run only once per touch):
        if (this.isScrolling === undefined) {
            this.isScrolling = this.isScrolling ||
                Math.abs(touchDeltaX) < Math.abs(this.touchDelta.y);
        }
        if (!this.isScrolling) {
            // Always prevent horizontal scroll:
            event.preventDefault();
            // Stop the slideshow:
            window.clearTimeout(this.timeout);
            if (this.options.continuous) {
                indices = [
                    this.circle(index + 1),
                    index,
                    this.circle(index - 1)
                ];
            } else {
                // Increase resistance if first slide and sliding left
                // or last slide and sliding right:
                this.touchDelta.x = touchDeltaX =
                    touchDeltaX /
                    (((!index && touchDeltaX > 0) ||
                        (index === this.num - 1 && touchDeltaX < 0)) ?
                            (Math.abs(touchDeltaX) / this.slideWidth + 1) : 1);
                indices = [index];
                if (index) {
                    indices.push(index - 1);
                }
                if (index < this.num - 1) {
                    indices.unshift(index + 1);
                }
            }
            while (indices.length) {
                index = indices.pop();
                this.translateX(index, touchDeltaX + this.positions[index], 0);
            }
        } else if (this.options.closeOnSwipeUpOrDown) {
            this.translateY(index, this.touchDelta.y + this.positions[index], 0);
        }
    },

    ontouchend: function (event) {
        if (this.options.stopTouchEventsPropagation) {
            this.stopPropagation(event);
        }
        var index = this.index,
            speed = this.options.transitionSpeed,
            slideWidth = this.slideWidth,
            isShortDuration = Number(Date.now() - this.touchStart.time) < 250,
            // Determine if slide attempt triggers next/prev slide:
            isValidSlide = (isShortDuration && Math.abs(this.touchDelta.x) > 20) ||
                Math.abs(this.touchDelta.x) > slideWidth / 2,
            // Determine if slide attempt is past start or end:
            isPastBounds = (!index && this.touchDelta.x > 0) ||
                    (index === this.num - 1 && this.touchDelta.x < 0),
            isValidClose = !isValidSlide && this.options.closeOnSwipeUpOrDown &&
                ((isShortDuration && Math.abs(this.touchDelta.y) > 20) ||
                    Math.abs(this.touchDelta.y) > this.slideHeight / 2),
            direction,
            indexForward,
            indexBackward,
            distanceForward,
            distanceBackward;
        if (this.options.continuous) {
            isPastBounds = false;
        }
        // Determine direction of swipe (true: right, false: left):
        direction = this.touchDelta.x < 0 ? -1 : 1;
        if (!this.isScrolling) {
            if (isValidSlide && !isPastBounds) {
                indexForward = index + direction;
                indexBackward = index - direction;
                distanceForward = slideWidth * direction;
                distanceBackward = -slideWidth * direction;
                if (this.options.continuous) {
                    this.move(this.circle(indexForward), distanceForward, 0);
                    this.move(this.circle(index - 2 * direction), distanceBackward, 0);
                } else if (indexForward >= 0 &&
                        indexForward < this.num) {
                    this.move(indexForward, distanceForward, 0);
                }
                this.move(index, this.positions[index] + distanceForward, speed);
                this.move(
                    this.circle(indexBackward),
                    this.positions[this.circle(indexBackward)] + distanceForward,
                    speed
                );
                index = this.circle(indexBackward);
                this.onslide(index);
            } else {
                // Move back into position
                if (this.options.continuous) {
                    this.move(this.circle(index - 1), -slideWidth, speed);
                    this.move(index, 0, speed);
                    this.move(this.circle(index + 1), slideWidth, speed);
                } else {
                    if (index) {
                        this.move(index - 1, -slideWidth, speed);
                    }
                    this.move(index, 0, speed);
                    if (index < this.num - 1) {
                        this.move(index + 1, slideWidth, speed);
                    }
                }
            }
        } else {
            if (isValidClose) {
                this.close();
            } else {
                // Move back into position
                this.translateY(index, 0, speed);
            }
        }
    },

    ontouchcancel: function (event) {
        if (this.touchStart) {
            this.ontouchend(event);
            delete this.touchStart;
        }
    },

    ontransitionend: function (event) {
        var slide = this.slides[this.index];
        if (!event || slide === event.target) {
            if (this.interval) {
                this.play();
            }
            this.setTimeout(
                this.options.onslideend,
                [this.index, slide]
            );
        }
    },

    oncomplete: function (event) {
        var target = event.target || event.srcElement,
            parent = target && target.parentNode,
            index;
        if (!target || !parent) {
            return;
        }
        index = this.getNodeIndex(parent);
        $(parent).removeClass(this.options.slideLoadingClass);
        if (event.type === 'error') {
            $(parent).addClass(this.options.slideErrorClass);
            this.elements[index] = 3; // Fail
        } else {
            this.elements[index] = 2; // Done
        }
        // Fix for IE7's lack of support for percentage max-height:
        if (target.clientHeight > this.container[0].clientHeight) {
            target.style.maxHeight = this.container[0].clientHeight;
        }
        if (this.interval && this.slides[this.index] === parent) {
            this.play();
        }
        this.setTimeout(
            this.options.onslidecomplete,
            [index, parent]
        );
    },

    onload: function (event) {
        this.oncomplete(event);
    },

    onerror: function (event) {
        this.oncomplete(event);
    },

    onkeydown: function (event) {
        switch (event.which || event.keyCode) {
        case 13: // Return
            if (this.options.toggleControlsOnReturn) {
                this.preventDefault(event);
                this.toggleControls();
            }
            break;
        case 27: // Esc
            if (this.options.closeOnEscape) {
                this.close();
            }
            break;
        case 32: // Space
            if (this.options.toggleSlideshowOnSpace) {
                this.preventDefault(event);
                this.toggleSlideshow();
            }
            break;
        case 37: // Left
            if (this.options.enableKeyboardNavigation) {
                this.preventDefault(event);
                this.prev();
            }
            break;
        case 39: // Right
            if (this.options.enableKeyboardNavigation) {
                this.preventDefault(event);
                this.next();
            }
            break;
        }
    },

    handleClick: function (event) {
        var options = this.options,
            target = event.target || event.srcElement,
            parent = target.parentNode,
            isTarget = function (className) {
                return $(target).hasClass(className) ||
                    $(parent).hasClass(className);
            };
        if (isTarget(options.toggleClass)) {
            // Click on "toggle" control
            this.preventDefault(event);
            this.toggleControls();
        } else if (isTarget(options.prevClass)) {
            // Click on "prev" control
            this.preventDefault(event);
            this.prev();
        } else if (isTarget(options.nextClass)) {
            // Click on "next" control
            this.preventDefault(event);
            this.next();
        } else if (isTarget(options.closeClass)) {
            // Click on "close" control
            this.preventDefault(event);
            this.close();
        } else if (isTarget(options.playPauseClass)) {
            // Click on "play-pause" control
            this.preventDefault(event);
            this.toggleSlideshow();
        } else if (parent === this.slidesContainer[0]) {
            // Click on slide background
            this.preventDefault(event);
            if (options.closeOnSlideClick) {
                this.close();
            } else {
                this.toggleControls();
            }
        } else if (parent.parentNode &&
                parent.parentNode === this.slidesContainer[0]) {
            // Click on displayed element
            this.preventDefault(event);
            this.toggleControls();
        }
    },

    onclick: function (event) {
        if (this.options.emulateTouchEvents &&
                this.touchDelta && (Math.abs(this.touchDelta.x) > 20 ||
                    Math.abs(this.touchDelta.y) > 20)) {
            delete this.touchDelta;
            return;
        }
        return this.handleClick(event);
    },

    updateEdgeClasses: function (index) {
        if (!index) {
            this.container.addClass(this.options.leftEdgeClass);
        } else {
            this.container.removeClass(this.options.leftEdgeClass);
        }
        if (index === this.num - 1) {
            this.container.addClass(this.options.rightEdgeClass);
        } else {
            this.container.removeClass(this.options.rightEdgeClass);
        }
    },

    handleSlide: function (index) {
        if (!this.options.continuous) {
            this.updateEdgeClasses(index);
        }
        this.loadElements(index);
        if (this.options.unloadElements) {
            this.unloadElements(index);
        }
        this.setTitle(index);
    },

    onslide: function (index) {
        this.index = index;
        this.handleSlide(index);
        this.setTimeout(this.options.onslide, [index, this.slides[index]]);
    },

    setTitle: function (index) {
        var text = this.slides[index].firstChild.title,
            titleElement = this.titleElement;
        if (titleElement.length) {
            this.titleElement.empty();
            if (text) {
                titleElement[0].appendChild(document.createTextNode(text));
            }
        }
    },

    setTimeout: function (func, args, wait) {
        var that = this;
        return func && window.setTimeout(function () {
            func.apply(that, args || []);
        }, wait || 0);
    },

    imageFactory: function (obj, callback) {
        var that = this,
            img = this.imagePrototype.cloneNode(false),
            url = obj,
            backgroundSize = this.options.stretchImages,
            called,
            element,
            callbackWrapper = function (event) {
                if (!called) {
                    event = {
                        type: event.type,
                        target: element
                    };
                    if (!element.parentNode) {
                        // Fix for IE7 firing the load event for
                        // cached images before the element could
                        // be added to the DOM:
                        return that.setTimeout(callbackWrapper, [event]);
                    }
                    called = true;
                    $(img).off('load error', callbackWrapper);
                    if (backgroundSize) {
                        if (event.type === 'load') {
                            element.style.background = 'url("' + url +
                                '") center no-repeat';
                            element.style.backgroundSize = backgroundSize;
                        }
                    }
                    callback(event);
                }
            },
            title;
        if (typeof url !== 'string') {
            url = this.getItemProperty(obj, this.options.urlProperty);
            title = this.getItemProperty(obj, this.options.titleProperty);
        }
        if (backgroundSize === true) {
            backgroundSize = 'contain';
        }
        backgroundSize = this.support.backgroundSize &&
            this.support.backgroundSize[backgroundSize] && backgroundSize;
        if (backgroundSize) {
            element = this.elementPrototype.cloneNode(false);
        } else {
            element = img;
            img.draggable = false;
        }
        if (title) {
            element.title = title;
        }
        $(img).on('load error', callbackWrapper);
        img.src = url;
        return element;
    },

    createElement: function (obj, callback) {
        var type = obj && this.getItemProperty(obj, this.options.typeProperty),
            factory = (type && this[type.split('/')[0] + 'Factory']) ||
                this.imageFactory,
            element = obj && factory.call(this, obj, callback);
        if (!element) {
            element = this.elementPrototype.cloneNode(false);
            this.setTimeout(callback, [{
                type: 'error',
                target: element
            }]);
        }
        $(element).addClass(this.options.slideContentClass);
        return element;
    },

    loadElement: function (index) {
        if (!this.elements[index]) {
            if (this.slides[index].firstChild) {
                this.elements[index] = $(this.slides[index])
                    .hasClass(this.options.slideErrorClass) ? 3 : 2;
            } else {
                this.elements[index] = 1; // Loading
                $(this.slides[index]).addClass(this.options.slideLoadingClass);
                this.slides[index].appendChild(this.createElement(
                    this.list[index],
                    this.proxyListener
                ));
            }
        }
    },

    loadElements: function (index) {
        var limit = Math.min(this.num, this.options.preloadRange * 2 + 1),
            j = index,
            i;
        for (i = 0; i < limit; i += 1) {
            // First load the current slide element (0),
            // then the next one (+1),
            // then the previous one (-2),
            // then the next after next (+2), etc.:
            j += i * (i % 2 === 0 ? -1 : 1);
            // Connect the ends of the list to load slide elements for
            // continuous navigation:
            j = this.circle(j);
            this.loadElement(j);
        }
    },

    unloadElements: function (index) {
        var i,
            slide,
            diff;
        for (i in this.elements) {
            if (this.elements.hasOwnProperty(i)) {
                diff = Math.abs(index - i);
                if (diff > this.options.preloadRange &&
                        diff + this.options.preloadRange < this.num) {
                    slide = this.slides[i];
                    slide.removeChild(slide.firstChild);
                    delete this.elements[i];
                }
            }
        }
    },

    addSlide: function (index) {
        var slide = this.slidePrototype.cloneNode(false);
        slide.setAttribute('data-index', index);
        this.slidesContainer[0].appendChild(slide);
        this.slides.push(slide);
    },

    positionSlide: function (index) {
        var slide = this.slides[index];
        slide.style.width = this.slideWidth + 'px';
        if (this.support.transition) {
            slide.style.left = (index * -this.slideWidth) + 'px';
            this.move(index, this.index > index ? -this.slideWidth :
                    (this.index < index ? this.slideWidth : 0), 0);
        }
    },

    initSlides: function (reload) {
        var clearSlides,
            i;
        if (!reload) {
            this.positions = [];
            this.positions.length = this.num;
            this.elements = {};
            this.imagePrototype = document.createElement('img');
            this.elementPrototype = document.createElement('div');
            this.slidePrototype = document.createElement('div');
            $(this.slidePrototype).addClass(this.options.slideClass);
            this.slides = this.slidesContainer[0].children;
            clearSlides = this.options.clearSlides ||
                this.slides.length !== this.num;
        }
        this.slideWidth = this.container[0].offsetWidth;
        this.slideHeight = this.container[0].offsetHeight;
        this.slidesContainer[0].style.width =
            (this.num * this.slideWidth) + 'px';
        if (clearSlides) {
            this.resetSlides();
        }
        for (i = 0; i < this.num; i += 1) {
            if (clearSlides) {
                this.addSlide(i);
            }
            this.positionSlide(i);
        }
        // Reposition the slides before and after the given index:
        if (this.options.continuous && this.support.transition) {
            this.move(this.circle(this.index - 1), -this.slideWidth, 0);
            this.move(this.circle(this.index + 1), this.slideWidth, 0);
        }
        if (!this.support.transition) {
            this.slidesContainer[0].style.left =
                (this.index * -this.slideWidth) + 'px';
        }
    },

    toggleControls: function () {
        var controlsClass = this.options.controlsClass;
        if (this.container.hasClass(controlsClass)) {
            this.container.removeClass(controlsClass);
        } else {
            this.container.addClass(controlsClass);
        }
    },

    toggleSlideshow: function () {
        if (!this.interval) {
            this.play();
        } else {
            this.pause();
        }
    },

    getNodeIndex: function (element) {
        return parseInt(element.getAttribute('data-index'), 10);
    },

    getNestedProperty: function (obj, property) {
        property.replace(
            // Matches native JavaScript notation in a String,
            // e.g. '["doubleQuoteProp"].dotProp[2]'
            /\[(?:'([^']+)'|"([^"]+)"|(\d+))\]|(?:(?:^|\.)([^\.\[]+))/g,
            function (str, singleQuoteProp, doubleQuoteProp, arrayIndex, dotProp) {
                var prop = dotProp || singleQuoteProp || doubleQuoteProp ||
                        (arrayIndex && parseInt(arrayIndex, 10));
                if (str && obj) {
                    obj = obj[prop];
                }
            }
        );
        return obj;
    },

    getDataProperty: function (obj, property) {
        if (obj.getAttribute) {
            var prop = obj.getAttribute('data-' +
                    property.replace(/([A-Z])/g, '-$1').toLowerCase());
            if (typeof prop === 'string') {
                if (/^(true|false|null|-?\d+(\.\d+)?|\{[\s\S]*\}|\[[\s\S]*\])$/
                        .test(prop)) {
                    try {
                        return $.parseJSON(prop);
                    } catch (ignore) {}
                }
                return prop;
            }
        }
    },

    getItemProperty: function (obj, property) {
        var prop = obj[property];
        if (prop === undefined) {
            prop = this.getDataProperty(obj, property);
            if (prop === undefined) {
                prop = this.getNestedProperty(obj, property);
            }
        }
        return prop;
    },

    initStartIndex: function () {
        var index = this.options.index,
            urlProperty = this.options.urlProperty,
            i;
        // Check if the index is given as a list object:
        if (index && typeof index !== 'number') {
            for (i = 0; i < this.num; i += 1) {
                if (this.list[i] === index ||
                        this.getItemProperty(this.list[i], urlProperty) ===
                            this.getItemProperty(index, urlProperty)) {
                    index  = i;
                    break;
                }
            }
        }
        // Make sure the index is in the list range:
        this.index = this.circle(parseInt(index, 10) || 0);
    },

    initEventListeners: function () {
        var that = this,
            slidesContainer = this.slidesContainer,
            proxyListener = function (event) {
                var type = that.support.transition &&
                        that.support.transition.end === event.type ?
                                'transitionend' : event.type;
                that['on' + type](event);
            };
        $(window).on('resize', proxyListener);
        $(document.body).on('keydown', proxyListener);
        this.container.on('click', proxyListener);
        if (this.support.touch) {
            slidesContainer
                .on('touchstart touchmove touchend touchcancel', proxyListener);
        } else if (this.options.emulateTouchEvents &&
                this.support.transition) {
            slidesContainer
                .on('mousedown mousemove mouseup mouseout', proxyListener);
        }
        if (this.support.transition) {
            slidesContainer.on(
                this.support.transition.end,
                proxyListener
            );
        }
        this.proxyListener = proxyListener;
    },

    destroyEventListeners: function () {
        var slidesContainer = this.slidesContainer,
            proxyListener = this.proxyListener;
        $(window).off('resize', proxyListener);
        $(document.body).off('keydown', proxyListener);
        this.container.off('click', proxyListener);
        if (this.support.touch) {
            slidesContainer
                .off('touchstart touchmove touchend touchcancel', proxyListener);
        } else if (this.options.emulateTouchEvents &&
                this.support.transition) {
            slidesContainer
                .off('mousedown mousemove mouseup mouseout', proxyListener);
        }
        if (this.support.transition) {
            slidesContainer.off(
                this.support.transition.end,
                proxyListener
            );
        }
    },

    handleOpen: function () {
        if (this.options.onopened) {
            this.options.onopened.call(this);
        }
    },

    initWidget: function () {
        var that = this,
            openHandler = function (event) {
                if (event.target === that.container[0]) {
                    that.container.off(
                        that.support.transition.end,
                        openHandler
                    );
                    that.handleOpen();
                }
            };
        this.container = $(this.options.container);
        if (!this.container.length) {
            this.console.log(
                'blueimp Gallery: Widget container not found.',
                this.options.container
            );
            return false;
        }
        this.slidesContainer = this.container.find(
            this.options.slidesContainer
        ).first();
        if (!this.slidesContainer.length) {
            this.console.log(
                'blueimp Gallery: Slides container not found.',
                this.options.slidesContainer
            );
            return false;
        }
        this.titleElement = this.container.find(
            this.options.titleElement
        ).first();
        if (this.num === 1) {
            this.container.addClass(this.options.singleClass);
        }
        if (this.options.onopen) {
            this.options.onopen.call(this);
        }
        if (this.support.transition && this.options.displayTransition) {
            this.container.on(
                this.support.transition.end,
                openHandler
            );
        } else {
            this.handleOpen();
        }
        if (this.options.hidePageScrollbars) {
            // Hide the page scrollbars:
            this.bodyOverflowStyle = document.body.style.overflow;
            document.body.style.overflow = 'hidden';
        }
        this.container[0].style.display = 'block';
        this.initSlides();
        this.container.addClass(this.options.displayClass);
    },

    initOptions: function (options) {
        // Create a copy of the prototype options:
        this.options = $.extend({}, this.options);
        // Check if carousel mode is enabled:
        if ((options && options.carousel) ||
                (this.options.carousel && (!options || options.carousel !== false))) {
            $.extend(this.options, this.carouselOptions);
        }
        // Override any given options:
        $.extend(this.options, options);
        if (this.num < 3) {
            // 1 or 2 slides cannot be displayed continuous,
            // remember the original option by setting to null instead of false:
            this.options.continuous = this.options.continuous ? null : false;
        }
        if (!this.support.transition) {
            this.options.emulateTouchEvents = false;
        }
        if (this.options.event) {
            this.preventDefault(this.options.event);
        }
    }

});

return Gallery;

}));