/*!
* baguetteBox.js
* @author feimosi
* @version 1.8.2
* @url https://github.com/feimosi/baguetteBox.js
*/
/* global define, module */
(function (root, factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
define(factory);
} else if (typeof exports === 'object') {
module.exports = factory();
} else {
root.baguetteBox = factory();
}
}(this, function () {
'use strict';
// SVG shapes used on the buttons
var leftArrow = '<svg width="44" height="60">' +
'<polyline points="30 10 10 30 30 50" stroke="rgba(255,255,255,0.5)" stroke-width="4"' +
'stroke-linecap="butt" fill="none" stroke-linejoin="round"/>' +
'</svg>',
rightArrow = '<svg width="44" height="60">' +
'<polyline points="14 10 34 30 14 50" stroke="rgba(255,255,255,0.5)" stroke-width="4"' +
'stroke-linecap="butt" fill="none" stroke-linejoin="round"/>' +
'</svg>',
closeX = '<svg width="30" height="30">' +
'<g stroke="rgb(160,160,160)" stroke-width="4">' +
'<line x1="5" y1="5" x2="25" y2="25"/>' +
'<line x1="5" y1="25" x2="25" y2="5"/>' +
'</g></svg>';
// Global options and their defaults
var options = {},
defaults = {
captions: true,
fullScreen: false,
noScrollbars: false,
titleTag: false,
buttons: 'auto',
async: false,
preload: 2,
animation: 'slideIn',
afterShow: null,
afterHide: null,
// callback when image changes with `currentIndex` and `imagesElements.length` as parameters
onChange: null,
overlayBackgroundColor: 'rgba(0,0,0,.8)'
};
// Object containing information about features compatibility
var supports = {};
// DOM Elements references
var overlay, slider, previousButton, nextButton, closeButton;
// An array with all images in the current gallery
var currentGallery = [];
// Current image index inside the slider
var currentIndex = 0;
// Touch event start position (for slide gesture)
var touch = {};
// If set to true ignore touch events because animation was already fired
var touchFlag = false;
// Regex pattern to match image files
var regex = /.+\.(gif|jpe?g|png|webp)/i;
// Object of all used galleries
var data = {};
// Array containing temporary images DOM elements
var imagesElements = [];
// The last focused element before opening the overlay
var documentLastFocus = null;
var overlayClickHandler = function(event) {
// Close the overlay when user clicks directly on the background
if (event.target.id.indexOf('baguette-img') !== -1) {
hideOverlay();
}
};
var previousButtonClickHandler = function(event) {
event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true; // jshint ignore:line
showPreviousImage();
};
var nextButtonClickHandler = function(event) {
event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true; // jshint ignore:line
showNextImage();
};
var closeButtonClickHandler = function(event) {
event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true; // jshint ignore:line
hideOverlay();
};
var touchstartHandler = function(event) {
touch.count++;
if (touch.count > 1) {
touch.multitouch = true;
}
// Save x and y axis position
touch.startX = event.changedTouches[0].pageX;
touch.startY = event.changedTouches[0].pageY;
};
var touchmoveHandler = function(event) {
// If action was already triggered or multitouch return
if (touchFlag || touch.multitouch) {
return;
}
event.preventDefault ? event.preventDefault() : event.returnValue = false; // jshint ignore:line
var touchEvent = event.touches[0] || event.changedTouches[0];
// Move at least 40 pixels to trigger the action
if (touchEvent.pageX - touch.startX > 40) {
touchFlag = true;
showPreviousImage();
} else if (touchEvent.pageX - touch.startX < -40) {
touchFlag = true;
showNextImage();
// Move 100 pixels up to close the overlay
} else if (touch.startY - touchEvent.pageY > 100) {
hideOverlay();
}
};
var touchendHandler = function() {
touch.count--;
if (touch.count <= 0) {
touch.multitouch = false;
}
touchFlag = false;
};
var trapFocusInsideOverlay = function(event) {
if (overlay.style.display === 'block' && (overlay.contains && !overlay.contains(event.target))) {
event.stopPropagation();
initFocus();
}
};
// forEach polyfill for IE8
// http://stackoverflow.com/a/14827443/1077846
/* jshint ignore:start */
if (![].forEach) {
Array.prototype.forEach = function(callback, thisArg) {
for (var i = 0; i < this.length; i++) {
callback.call(thisArg, this[i], i, this);
}
};
}
// filter polyfill for IE8
// https://gist.github.com/eliperelman/1031656
if (![].filter) {
Array.prototype.filter = function(a, b, c, d, e) {
c = this;
d = [];
for (e = 0; e < c.length; e++)
a.call(b, c[e], e, c) && d.push(c[e]);
return d;
};
}
/* jshint ignore:end */
// Script entry point
function run(selector, userOptions) {
// Fill supports object
supports.transforms = testTransformsSupport();
supports.svg = testSVGSupport();
buildOverlay();
removeFromCache(selector);
bindImageClickListeners(selector, userOptions);
}
function bindImageClickListeners(selector, userOptions) {
// For each gallery bind a click event to every image inside it
var galleryNodeList = document.querySelectorAll(selector);
var selectorData = {
galleries: [],
nodeList: galleryNodeList
};
data[selector] = selectorData;
[].forEach.call(galleryNodeList, function(galleryElement) {
if (userOptions && userOptions.filter) {
regex = userOptions.filter;
}
// Get nodes from gallery elements or single-element galleries
var tagsNodeList = [];
if (galleryElement.tagName === 'A') {
tagsNodeList = [galleryElement];
} else {
tagsNodeList = galleryElement.getElementsByTagName('a');
}
// Filter 'a' elements from those not linking to images
tagsNodeList = [].filter.call(tagsNodeList, function(element) {
return regex.test(element.href);
});
if (tagsNodeList.length === 0) {
return;
}
var gallery = [];
[].forEach.call(tagsNodeList, function(imageElement, imageIndex) {
var imageElementClickHandler = function(event) {
event.preventDefault ? event.preventDefault() : event.returnValue = false; // jshint ignore:line
prepareOverlay(gallery, userOptions);
showOverlay(imageIndex);
};
var imageItem = {
eventHandler: imageElementClickHandler,
imageElement: imageElement
};
bind(imageElement, 'click', imageElementClickHandler);
gallery.push(imageItem);
});
selectorData.galleries.push(gallery);
});
}
function clearCachedData() {
for (var selector in data) {
if (data.hasOwnProperty(selector)) {
removeFromCache(selector);
}
}
}
function removeFromCache(selector) {
if (!data.hasOwnProperty(selector)) {
return;
}
var galleries = data[selector].galleries;
[].forEach.call(galleries, function(gallery) {
[].forEach.call(gallery, function(imageItem) {
unbind(imageItem.imageElement, 'click', imageItem.eventHandler);
});
if (currentGallery === gallery) {
currentGallery = [];
}
});
delete data[selector];
}
function buildOverlay() {
overlay = getByID('baguetteBox-overlay');
// Check if the overlay already exists
if (overlay) {
slider = getByID('baguetteBox-slider');
previousButton = getByID('previous-button');
nextButton = getByID('next-button');
closeButton = getByID('close-button');
return;
}
// Create overlay element
overlay = create('div');
overlay.setAttribute('role', 'dialog');
overlay.id = 'baguetteBox-overlay';
document.getElementsByTagName('body')[0].appendChild(overlay);
// Create gallery slider element
slider = create('div');
slider.id = 'baguetteBox-slider';
overlay.appendChild(slider);
// Create all necessary buttons
previousButton = create('button');
previousButton.setAttribute('type', 'button');
previousButton.id = 'previous-button';
previousButton.setAttribute('aria-label', 'Previous');
previousButton.innerHTML = supports.svg ? leftArrow : '<';
overlay.appendChild(previousButton);
nextButton = create('button');
nextButton.setAttribute('type', 'button');
nextButton.id = 'next-button';
nextButton.setAttribute('aria-label', 'Next');
nextButton.innerHTML = supports.svg ? rightArrow : '>';
overlay.appendChild(nextButton);
closeButton = create('button');
closeButton.setAttribute('type', 'button');
closeButton.id = 'close-button';
closeButton.setAttribute('aria-label', 'Close');
closeButton.innerHTML = supports.svg ? closeX : '×';
overlay.appendChild(closeButton);
previousButton.className = nextButton.className = closeButton.className = 'baguetteBox-button';
bindEvents();
}
function keyDownHandler(event) {
switch (event.keyCode) {
case 37: // Left arrow
showPreviousImage();
break;
case 39: // Right arrow
showNextImage();
break;
case 27: // Esc
hideOverlay();
break;
}
}
function bindEvents() {
bind(overlay, 'click', overlayClickHandler);
bind(previousButton, 'click', previousButtonClickHandler);
bind(nextButton, 'click', nextButtonClickHandler);
bind(closeButton, 'click', closeButtonClickHandler);
bind(overlay, 'touchstart', touchstartHandler);
bind(overlay, 'touchmove', touchmoveHandler);
bind(overlay, 'touchend', touchendHandler);
bind(document, 'focus', trapFocusInsideOverlay, true);
}
function unbindEvents() {
unbind(overlay, 'click', overlayClickHandler);
unbind(previousButton, 'click', previousButtonClickHandler);
unbind(nextButton, 'click', nextButtonClickHandler);
unbind(closeButton, 'click', closeButtonClickHandler);
unbind(overlay, 'touchstart', touchstartHandler);
unbind(overlay, 'touchmove', touchmoveHandler);
unbind(overlay, 'touchend', touchendHandler);
unbind(document, 'focus', trapFocusInsideOverlay, true);
}
function prepareOverlay(gallery, userOptions) {
// If the same gallery is being opened prevent from loading it once again
if (currentGallery === gallery) {
return;
}
currentGallery = gallery;
// Update gallery specific options
setOptions(userOptions);
// Empty slider of previous contents (more effective than .innerHTML = "")
while (slider.firstChild) {
slider.removeChild(slider.firstChild);
}
imagesElements.length = 0;
var imagesFiguresIds = [];
var imagesCaptionsIds = [];
// Prepare and append images containers and populate figure and captions IDs arrays
for (var i = 0, fullImage; i < gallery.length; i++) {
fullImage = create('div');
fullImage.className = 'full-image';
fullImage.id = 'baguette-img-' + i;
imagesElements.push(fullImage);
imagesFiguresIds.push('baguetteBox-figure-' + i);
imagesCaptionsIds.push('baguetteBox-figcaption-' + i);
slider.appendChild(imagesElements[i]);
}
overlay.setAttribute('aria-labelledby', imagesFiguresIds.join(' '));
overlay.setAttribute('aria-describedby', imagesCaptionsIds.join(' '));
}
function setOptions(newOptions) {
if (!newOptions) {
newOptions = {};
}
// Fill options object
for (var item in defaults) {
options[item] = defaults[item];
if (typeof newOptions[item] !== 'undefined') {
options[item] = newOptions[item];
}
}
/* Apply new options */
// Change transition for proper animation
slider.style.transition = slider.style.webkitTransition = (options.animation === 'fadeIn' ? 'opacity .4s ease' :
options.animation === 'slideIn' ? '' : 'none');
// Hide buttons if necessary
if (options.buttons === 'auto' && ('ontouchstart' in window || currentGallery.length === 1)) {
options.buttons = false;
}
// Set buttons style to hide or display them
previousButton.style.display = nextButton.style.display = (options.buttons ? '' : 'none');
// Set overlay color
try {
overlay.style.backgroundColor = options.overlayBackgroundColor;
} catch (e) {
// Silence the error and continue
}
}
function showOverlay(chosenImageIndex) {
if (options.noScrollbars) {
document.documentElement.style.overflowY = 'hidden';
document.body.style.overflowY = 'scroll';
}
if (overlay.style.display === 'block') {
return;
}
bind(document, 'keydown', keyDownHandler);
currentIndex = chosenImageIndex;
touch = {
count: 0,
startX: null,
startY: null
};
loadImage(currentIndex, function() {
preloadNext(currentIndex);
preloadPrev(currentIndex);
});
updateOffset();
overlay.style.display = 'block';
if (options.fullScreen) {
enterFullScreen();
}
// Fade in overlay
setTimeout(function() {
overlay.className = 'visible';
if (options.afterShow) {
options.afterShow();
}
}, 50);
if (options.onChange) {
options.onChange(currentIndex, imagesElements.length);
}
documentLastFocus = document.activeElement;
initFocus();
}
function initFocus() {
if (options.buttons) {
previousButton.focus();
} else {
closeButton.focus();
}
}
function enterFullScreen() {
if (overlay.requestFullscreen) {
overlay.requestFullscreen();
} else if (overlay.webkitRequestFullscreen) {
overlay.webkitRequestFullscreen();
} else if (overlay.mozRequestFullScreen) {
overlay.mozRequestFullScreen();
}
}
function exitFullscreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
function hideOverlay() {
if (options.noScrollbars) {
document.documentElement.style.overflowY = 'auto';
document.body.style.overflowY = 'auto';
}
if (overlay.style.display === 'none') {
return;
}
unbind(document, 'keydown', keyDownHandler);
// Fade out and hide the overlay
overlay.className = '';
setTimeout(function() {
overlay.style.display = 'none';
exitFullscreen();
if (options.afterHide) {
options.afterHide();
}
}, 500);
documentLastFocus.focus();
}
function loadImage(index, callback) {
var imageContainer = imagesElements[index];
var galleryItem = currentGallery[index];
// Return if the index exceeds prepared images in the overlay
// or if the current gallery has been changed / closed
if (imageContainer === undefined || galleryItem === undefined) {
return;
}
// If image is already loaded run callback and return
if (imageContainer.getElementsByTagName('img')[0]) {
if (callback) {
callback();
}
return;
}
// Get element reference, optional caption and source path
var imageElement = galleryItem.imageElement;
var thumbnailElement = imageElement.getElementsByTagName('img')[0];
var imageCaption = typeof options.captions === 'function' ?
options.captions.call(currentGallery, imageElement) :
imageElement.getAttribute('data-caption') || imageElement.title;
var imageSrc = getImageSrc(imageElement);
// Prepare figure element
var figure = create('figure');
figure.id = 'baguetteBox-figure-' + index;
figure.innerHTML = '<div class="baguetteBox-spinner">' +
'<div class="baguetteBox-double-bounce1"></div>' +
'<div class="baguetteBox-double-bounce2"></div>' +
'</div>';
// Insert caption if available
if (options.captions && imageCaption) {
var figcaption = create('figcaption');
figcaption.id = 'baguetteBox-figcaption-' + index;
figcaption.innerHTML = imageCaption;
figure.appendChild(figcaption);
}
imageContainer.appendChild(figure);
// Prepare gallery img element
var image = create('img');
image.onload = function() {
// Remove loader element
var spinner = document.querySelector('#baguette-img-' + index + ' .baguetteBox-spinner');
figure.removeChild(spinner);
if (!options.async && callback) {
callback();
}
};
image.setAttribute('src', imageSrc);
image.alt = thumbnailElement ? thumbnailElement.alt || '' : '';
if (options.titleTag && imageCaption) {
image.title = imageCaption;
}
figure.appendChild(image);
// Run callback
if (options.async && callback) {
callback();
}
}
// Get image source location, mostly used for responsive images
function getImageSrc(image) {
// Set default image path from href
var result = image.href;
// If dataset is supported find the most suitable image
if (image.dataset) {
var srcs = [];
// Get all possible image versions depending on the resolution
for (var item in image.dataset) {
if (item.substring(0, 3) === 'at-' && !isNaN(item.substring(3))) {
srcs[item.replace('at-', '')] = image.dataset[item];
}
}
// Sort resolutions ascending
var keys = Object.keys(srcs).sort(function(a, b) {
return parseInt(a, 10) < parseInt(b, 10) ? -1 : 1;
});
// Get real screen resolution
var width = window.innerWidth * window.devicePixelRatio;
// Find the first image bigger than or equal to the current width
var i = 0;
while (i < keys.length - 1 && keys[i] < width) {
i++;
}
result = srcs[keys[i]] || result;
}
return result;
}
// Return false at the right end of the gallery
function showNextImage() {
var returnValue;
// Check if next image exists
if (currentIndex <= imagesElements.length - 2) {
currentIndex++;
updateOffset();
preloadNext(currentIndex);
returnValue = true;
} else if (options.animation) {
slider.className = 'bounce-from-right';
setTimeout(function() {
slider.className = '';
}, 400);
returnValue = false;
}
if (options.onChange) {
options.onChange(currentIndex, imagesElements.length);
}
return returnValue;
}
// Return false at the left end of the gallery
function showPreviousImage() {
var returnValue;
// Check if previous image exists
if (currentIndex >= 1) {
currentIndex--;
updateOffset();
preloadPrev(currentIndex);
returnValue = true;
} else if (options.animation) {
slider.className = 'bounce-from-left';
setTimeout(function() {
slider.className = '';
}, 400);
returnValue = false;
}
if (options.onChange) {
options.onChange(currentIndex, imagesElements.length);
}
return returnValue;
}
function updateOffset() {
var offset = -currentIndex * 100 + '%';
if (options.animation === 'fadeIn') {
slider.style.opacity = 0;
setTimeout(function() {
/* jshint -W030 */
supports.transforms ?
slider.style.transform = slider.style.webkitTransform = 'translate3d(' + offset + ',0,0)'
: slider.style.left = offset;
slider.style.opacity = 1;
}, 400);
} else {
/* jshint -W030 */
supports.transforms ?
slider.style.transform = slider.style.webkitTransform = 'translate3d(' + offset + ',0,0)'
: slider.style.left = offset;
}
}
// CSS 3D Transforms test
function testTransformsSupport() {
var div = create('div');
return typeof div.style.perspective !== 'undefined' || typeof div.style.webkitPerspective !== 'undefined';
}
// Inline SVG test
function testSVGSupport() {
var div = create('div');
div.innerHTML = '<svg/>';
return (div.firstChild && div.firstChild.namespaceURI) === 'http://www.w3.org/2000/svg';
}
function preloadNext(index) {
if (index - currentIndex >= options.preload) {
return;
}
loadImage(index + 1, function() {
preloadNext(index + 1);
});
}
function preloadPrev(index) {
if (currentIndex - index >= options.preload) {
return;
}
loadImage(index - 1, function() {
preloadPrev(index - 1);
});
}
function bind(element, event, callback, useCapture) {
if (element.addEventListener) {
element.addEventListener(event, callback, useCapture);
} else {
// IE8 fallback
element.attachEvent('on' + event, function(event) {
// `event` and `event.target` are not provided in IE8
event = event || window.event;
event.target = event.target || event.srcElement;
callback(event);
});
}
}
function unbind(element, event, callback, useCapture) {
if (element.removeEventListener) {
element.removeEventListener(event, callback, useCapture);
} else {
// IE8 fallback
element.detachEvent('on' + event, callback);
}
}
function getByID(id) {
return document.getElementById(id);
}
function create(element) {
return document.createElement(element);
}
function destroyPlugin() {
unbindEvents();
clearCachedData();
unbind(document, 'keydown', keyDownHandler);
document.getElementsByTagName('body')[0].removeChild(document.getElementById('baguetteBox-overlay'));
data = {};
currentGallery = [];
currentIndex = 0;
}
return {
run: run,
destroy: destroyPlugin,
showNext: showNextImage,
showPrevious: showPreviousImage
};
}));