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
JavaScript
/*
* 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