UNPKG

blueimp-gallery

Version:

blueimp Gallery is a touch-enabled, responsive and customizable image and video gallery, carousel and lightbox, optimized for both mobile and desktop web browsers. It features swipe, mouse and keyboard navigation, transition effects, slideshow functionali

1,539 lines (1,463 loc) 50.3 kB
/* * blueimp Gallery JS * 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: * https://opensource.org/licenses/MIT */ /* global define, DocumentTouch */ /* eslint-disable no-param-reassign */ ;(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' /** * Gallery constructor * * @class * @param {Array|NodeList} list Gallery content * @param {object} [options] Gallery options * @returns {object} Gallery object */ 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 to add when the browser supports SVG as img (or background): svgasimgClass: 'blueimp-gallery-svgasimg', // The class to add when the browser supports SMIL (animated SVGs): smilClass: 'blueimp-gallery-smil', // The class for all slides: slideClass: 'slide', // The slide class for the active (current index) slide: slideActiveClass: 'slide-active', // The slide class for the previous (before current index) slide: slidePrevClass: 'slide-prev', // The slide class for the next (after current index) slide: slideNextClass: 'slide-next', // 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 alt text: altTextProperty: 'alt', // The list object property (or data attribute) with the object URL: urlProperty: 'href', // The list object property (or data attribute) with the object srcset: srcsetProperty: 'srcset', // The list object property (or data attribute) with the object sizes: sizesProperty: 'sizes', // The list object property (or data attribute) with the object sources: sourcesProperty: 'sources', // 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, // Toggle the controls on pressing the Enter key: toggleControlsOnEnter: true, // Toggle the controls on slide click: toggleControlsOnSlideClick: true, // Toggle the automatic slideshow interval on pressing the Space key: toggleSlideshowOnSpace: true, // Navigate the gallery by pressing the ArrowLeft and ArrowRight keys: enableKeyboardNavigation: true, // Close the gallery on pressing the Escape 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, // Close the gallery when the URL hash changes: closeOnHashChange: 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 direction the slides are moving: ltr=LeftToRight or rtl=RightToLeft slideshowDirection: 'ltr', // 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 duration between slide changes in milliseconds: transitionDuration: 300, // The transition duration for automatic slide changes, set to an integer // greater 0 to override the default transition duration: slideshowTransitionDuration: 500, // 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, toggleControlsOnEnter: false, toggleSlideshowOnSpace: false, enableKeyboardNavigation: false, closeOnEscape: false, closeOnSlideClick: false, closeOnSwipeUpOrDown: false, closeOnHashChange: 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 = { source: !!window.HTMLSourceElement, picture: !!window.HTMLPictureElement, svgasimg: document.implementation.hasFeature( 'http://www.w3.org/TR/SVG11/feature#Image', '1.1' ), smil: !!document.createElementNS && /SVGAnimate/.test( document .createElementNS('http://www.w3.org/2000/svg', 'animate') .toString() ), touch: window.ontouchstart !== undefined || (window.DocumentTouch && document instanceof DocumentTouch) } var transitions = { webkitTransition: { end: 'webkitTransitionEnd', prefix: '-webkit-' }, MozTransition: { end: 'transitionend', prefix: '-moz-' }, OTransition: { end: 'otransitionend', prefix: '-o-' }, transition: { end: 'transitionend', prefix: '' } } var prop for (prop in transitions) { if ( Object.prototype.hasOwnProperty.call(transitions, prop) && element.style[prop] !== undefined ) { support.transition = transitions[prop] support.transition.name = prop break } } /** * Tests browser support */ function elementTests() { var transition = support.transition var prop var 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' } } } document.body.removeChild(element) } 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, cancelAnimationFrame: window.cancelAnimationFrame || window.webkitCancelRequestAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame, 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, duration) { window.clearTimeout(this.timeout) var index = this.index var direction var naturalDirection var diff if (index === to || this.num === 1) { return } if (!duration) { duration = this.options.transitionDuration } if (this.support.transform) { 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, duration) this.move(to, 0, duration) 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, duration) } 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 var nextIndex = this.index + (this.options.slideshowDirection === 'rtl' ? -1 : 1) 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, duration) { that.animationFrameId = that.requestAnimationFrame.call( window, function () { that.slide(to, duration) } ) }, [nextIndex, this.options.slideshowTransitionDuration], this.interval ) } this.container.addClass(this.options.playingClass) this.slidesContainer[0].setAttribute('aria-live', 'off') if (this.playPauseElement.length) { this.playPauseElement[0].setAttribute('aria-pressed', 'true') } }, pause: function () { window.clearTimeout(this.timeout) this.interval = null if (this.cancelAnimationFrame) { this.cancelAnimationFrame.call(window, this.animationFrameId) this.animationFrameId = null } this.container.removeClass(this.options.playingClass) this.slidesContainer[0].setAttribute('aria-live', 'polite') if (this.playPauseElement.length) { this.playPauseElement[0].setAttribute('aria-pressed', 'false') } }, 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.unloadAllSlides() 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 /** * Close handler * * @param {event} event Close event */ function closeHandler(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, duration) { this.translateX(index, dist, duration) this.positions[index] = dist }, translate: function (index, x, y, duration) { if (!this.slides[index]) return var style = this.slides[index].style var transition = this.support.transition var transform = this.support.transform style[transition.name + 'Duration'] = duration + 'ms' style[transform.name] = 'translate(' + x + 'px, ' + y + 'px)' + (transform.translateZ ? ' translateZ(0)' : '') }, translateX: function (index, x, duration) { this.translate(index, x, 0, duration) }, translateY: function (index, y, duration) { this.translate(index, 0, y, duration) }, animate: function (from, to, duration) { if (!duration) { this.slidesContainer[0].style.left = to + 'px' return } var that = this var start = new Date().getTime() var timer = window.setInterval(function () { var timeElap = new Date().getTime() - start if (timeElap > duration) { that.slidesContainer[0].style.left = to + 'px' that.ontransitionend() window.clearInterval(timer) return } that.slidesContainer[0].style.left = (to - from) * (Math.floor((timeElap / duration) * 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) }, onhashchange: function () { if (this.options.closeOnHashChange) { this.close() } }, onmousedown: function (event) { // Trigger on clicks of the left mouse button only // and exclude video & audio elements: if ( event.which && event.which === 1 && event.target.nodeName !== 'VIDEO' && event.target.nodeName !== 'AUDIO' ) { // 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 var 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 touch = (event.originalEvent || event).touches[0] this.touchStart = { // Remember the initial touch coordinates: x: touch.pageX, y: touch.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 var touch = touches[0] var scale = (event.originalEvent || event).scale var index = this.index var touchDeltaX var 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: touch.pageX - this.touchStart.x, y: touch.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.carousel) { this.translateY(index, this.touchDelta.y + this.positions[index], 0) } }, ontouchend: function (event) { if (this.options.stopTouchEventsPropagation) { this.stopPropagation(event) } var index = this.index var absTouchDeltaX = Math.abs(this.touchDelta.x) var slideWidth = this.slideWidth var duration = Math.ceil( (this.options.transitionDuration * (1 - absTouchDeltaX / slideWidth)) / 2 ) // Determine if slide attempt triggers next/prev slide: var isValidSlide = absTouchDeltaX > 20 // Determine if slide attempt is past start or end: var isPastBounds = (!index && this.touchDelta.x > 0) || (index === this.num - 1 && this.touchDelta.x < 0) var isValidClose = !isValidSlide && this.options.closeOnSwipeUpOrDown && Math.abs(this.touchDelta.y) > 20 var direction var indexForward var indexBackward var distanceForward var 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, duration) this.move( this.circle(indexBackward), this.positions[this.circle(indexBackward)] + distanceForward, duration ) index = this.circle(indexBackward) this.onslide(index) } else { // Move back into position if (this.options.continuous) { this.move(this.circle(index - 1), -slideWidth, duration) this.move(index, 0, duration) this.move(this.circle(index + 1), slideWidth, duration) } else { if (index) { this.move(index - 1, -slideWidth, duration) } this.move(index, 0, duration) if (index < this.num - 1) { this.move(index + 1, slideWidth, duration) } } } } else { if (isValidClose) { this.close() } else { // Move back into position this.translateY(index, 0, duration) } } }, 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 var parent = target && target.parentNode var 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: // Enter if (this.options.toggleControlsOnEnter) { this.preventDefault(event) this.toggleControls() } break case 27: // Escape if (this.options.closeOnEscape) { this.close() // prevent Escape from closing other things event.stopImmediatePropagation() } break case 32: // Space if (this.options.toggleSlideshowOnSpace) { this.preventDefault(event) this.toggleSlideshow() } break case 37: // ArrowLeft if (this.options.enableKeyboardNavigation) { this.preventDefault(event) this.prev() } break case 39: // ArrowRight if (this.options.enableKeyboardNavigation) { this.preventDefault(event) this.next() } break } }, handleClick: function (event) { var options = this.options var target = event.target || event.srcElement var parent = target.parentNode /** * Checks if the target from the close has the given class * * @param {string} className Class name * @returns {boolean} Returns true if the target has the class name */ function isTarget(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 if (options.closeOnSlideClick) { this.preventDefault(event) this.close() } else if (options.toggleControlsOnSlideClick) { this.preventDefault(event) this.toggleControls() } } else if ( parent.parentNode && parent.parentNode === this.slidesContainer[0] ) { // Click on displayed element if (options.toggleControlsOnSlideClick) { 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) } }, updateActiveSlide: function (oldIndex, newIndex) { var slides = this.slides var options = this.options var list = [ { index: newIndex, method: 'addClass', hidden: false }, { index: oldIndex, method: 'removeClass', hidden: true } ] var item, index while (list.length) { item = list.pop() $(slides[item.index])[item.method](options.slideActiveClass) index = this.circle(item.index - 1) if (options.continuous || index < item.index) { $(slides[index])[item.method](options.slidePrevClass) } index = this.circle(item.index + 1) if (options.continuous || index > item.index) { $(slides[index])[item.method](options.slideNextClass) } } this.slides[oldIndex].setAttribute('aria-hidden', 'true') this.slides[newIndex].removeAttribute('aria-hidden') }, handleSlide: function (oldIndex, newIndex) { if (!this.options.continuous) { this.updateEdgeClasses(newIndex) } this.updateActiveSlide(oldIndex, newIndex) this.loadElements(newIndex) if (this.options.unloadElements) { this.unloadElements(oldIndex, newIndex) } this.setTitle(newIndex) }, onslide: function (index) { this.handleSlide(this.index, index) this.index = index this.setTimeout(this.options.onslide, [index, this.slides[index]]) }, setTitle: function (index) { var firstChild = this.slides[index].firstChild var text = firstChild.title || firstChild.alt var 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 options = this.options var that = this var url = obj var img = this.imagePrototype.cloneNode(false) var picture var called var sources var srcset var sizes var title var altText var i /** * Wraps the callback function for the load/error event * * @param {event} event load/error event * @returns {number} timeout ID */ function callbackWrapper(event) { if (!called) { event = { type: event.type, target: picture || img } if (!event.target.parentNode) { // Fix for browsers (e.g. 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) callback(event) } } if (typeof url !== 'string') { url = this.getItemProperty(obj, options.urlProperty) sources = this.support.picture && this.support.source && this.getItemProperty(obj, options.sourcesProperty) srcset = this.getItemProperty(obj, options.srcsetProperty) sizes = this.getItemProperty(obj, options.sizesProperty) title = this.getItemProperty(obj, options.titleProperty) altText = this.getItemProperty(obj, options.altTextProperty) || title } img.draggable = false if (title) { img.title = title } if (altText) { img.alt = altText } $(img).on('load error', callbackWrapper) if (sources && sources.length) { picture = this.picturePrototype.cloneNode(false) for (i = 0; i < sources.length; i += 1) { picture.appendChild( $.extend(this.sourcePrototype.cloneNode(false), sources[i]) ) } picture.appendChild(img) $(picture).addClass(options.toggleClass) } if (srcset) { if (sizes) { img.sizes = sizes } img.srcset = srcset } img.src = url if (picture) return picture return img }, createElement: function (obj, callback) { var type = obj && this.getItemProperty(obj, this.options.typeProperty) var factory = (type && this[type.split('/')[0] + 'Factory']) || this.imageFactory var 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 }, iteratePreloadRange: function (index, func) { var num = this.num var options = this.options var limit = Math.min(num, options.preloadRange * 2 + 1) var j = index var i for (i = 0; i < limit; i += 1) { // First iterate to the current index (0), // then the next one (+1), // then the previous one (-1), // then the next after next (+2), // then the one before the previous one (-2), etc.: j += i * (i % 2 === 0 ? -1 : 1) if (j < 0 || j >= num) { if (!options.continuous) continue // Connect the ends of the list to load slide elements for // continuous iteration: j = this.circle(j) } func.call(this, j) } }, 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) { this.iteratePreloadRange(index, this.loadElement) }, unloadElements: function (oldIndex, newIndex) { var preloadRange = this.options.preloadRange this.iteratePreloadRange(oldIndex, function (i) { var diff = Math.abs(i - newIndex) if (diff > preloadRange && diff + preloadRange < this.num) { this.unloadSlide(i) delete this.elements[i] } }) }, addSlide: function (index) { var slide = this.slidePrototype.cloneNode(false) slide.setAttribute('data-index', index) slide.setAttribute('aria-hidden', 'true') 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.transform) { 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.picturePrototype = this.support.picture && document.createElement('picture') this.sourcePrototype = this.support.source && document.createElement('source') this.imagePrototype = document.createElement('img') this.elementPrototype = document.createElement('div') this.slidePrototype = this.elementPrototype.cloneNode(false) $(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].clientWidth this.slideHeight = this.container[0].clientHeight 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.transform) { this.move(this.circle(this.index - 1), -this.slideWidth, 0) this.move(this.circle(this.index + 1), this.slideWidth, 0) } if (!this.support.transform) { this.slidesContainer[0].style.left = this.index * -this.slideWidth + 'px' } }, unloadSlide: function (index) { var slide, firstChild slide = this.slides[index] firstChild = slide.firstChild if (firstChild !== null) { slide.removeChild(firstChild) } }, unloadAllSlides: function () { var i, len for (i = 0, len = this.slides.length; i < len; i++) { this.unloadSlide(i) } }, 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]' // eslint-disable-next-line no-useless-escape /\[(?:'([^']+)'|"([^"]+)"|(\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) { var key var prop if (obj.dataset) { key = property.replace(/-([a-z])/g, function (_, b) { return b.toUpperCase() }) prop = obj.dataset[key] } else if (obj.getAttribute) { prop = obj.getAttribute( 'data-' + property.replace(/([A-Z])/g, '-$1').toLowerCase() ) } if (typeof prop === 'string') { // eslint-disable-next-line no-useless-escape if ( /^(true|false|null|-?\d+(\.\d+)?|\{[\s\S]*\}|\[[\s\S]*\])$/.test(prop) ) { try { return $.parseJSON(prop) } catch (ignore) { // ignore JSON parsing errors } } return prop } }, getItemProperty: function (obj, property) { var prop = this.getDataProperty(obj, property) if (prop === undefined) { prop = obj[property] } if (prop === undefined) { prop = this.getNestedProperty(obj, property) } return prop }, initStartIndex: function () { var index = this.options.index var urlProperty = this.options.urlProperty var 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 var slidesContainer = this.slidesContainer /** * Proxy listener * * @param {event} event original event */ function proxyListener(event) { var type = that.support.transition && that.support.transition.end === event.type ? 'transitionend' : event.type that['on' + type](event) } $(window).on('resize', proxyListener) $(window).on('hashchange', 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 var 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 /** * Open handler * * @param {event} event Gallery open event */ function openHandler(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() this.playPauseElement = this.container .find('.' + this.options.playPauseClass) .first() if (this.num === 1) { this.container.addClass(this.options.singleClass) } if (this.support.svgasimg) { this.container.addClass(this.options.svgasimgClass) } if (this.support.smil) { this.container.addClass(this.options.smilClass) } 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.c