/*! * Lightbox v2.11.4 with enhanced mobile swipe support * by Lokesh Dhakar (original) + swipe modifications * * More info: * http://lokeshdhakar.com/projects/lightbox2/ * * Copyright Lokesh Dhakar * Released under the MIT license * https://github.com/lokesh/lightbox2/blob/master/LICENSE * * @preserve */ (function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. define(['jquery'], factory); } else if (typeof exports === 'object') { // Node. Does not work with strict CommonJS, but // only CommonJS-like environments that support module.exports, // like Node. module.exports = factory(require('jquery')); } else { // Browser globals (root is window) root.lightbox = factory(root.jQuery); } }(this, function ($) { function Lightbox(options) { this.album = []; this.currentImageIndex = void 0; this.init(); // options this.options = $.extend({}, this.constructor.defaults); this.option(options); } // Descriptions of all options available on the demo site: // http://lokeshdhakar.com/projects/lightbox2/index.html#options Lightbox.defaults = { albumLabel: 'Image %1 of %2', alwaysShowNavOnTouchDevices: false, fadeDuration: 600, fitImagesInViewport: true, imageFadeDuration: 600, // maxWidth: 800, // maxHeight: 600, positionFromTop: 50, resizeDuration: 700, showImageNumberLabel: true, wrapAround: false, disableScrolling: false, /* Sanitize Title If the caption data is trusted, for example you are hardcoding it in, then leave this to false. This will free you to add html tags, such as links, in the caption. If the caption data is user submitted or from some other untrusted source, then set this to true to prevent xss and other injection attacks. */ sanitizeTitle: false }; Lightbox.prototype.option = function(options) { $.extend(this.options, options); }; Lightbox.prototype.imageCountLabel = function(currentImageNum, totalImages) { return this.options.albumLabel.replace(/%1/g, currentImageNum).replace(/%2/g, totalImages); }; Lightbox.prototype.init = function() { var self = this; // Both enable and build methods require the body tag to be in the DOM. $(document).ready(function() { self.enable(); self.build(); }); }; // Loop through anchors and areamaps looking for either data-lightbox attributes or rel attributes // that contain 'lightbox'. When these are clicked, start lightbox. Lightbox.prototype.enable = function() { var self = this; $('body').on('click', 'a[rel^=lightbox], area[rel^=lightbox], a[data-lightbox], area[data-lightbox]', function(event) { self.start($(event.currentTarget)); return false; }); }; // Build html for the lightbox and the overlay. // Attach event handlers to the new DOM elements. click click click Lightbox.prototype.build = function() { if ($('#lightbox').length > 0) { return; } var self = this; // The two root notes generated, #lightboxOverlay and #lightbox are given // tabindex attrs so they are focusable. We attach our keyboard event // listeners to these two elements, and not the document. Clicking anywhere // while Lightbox is opened will keep the focus on or inside one of these // two elements. $('
').appendTo($('body')); // Cache jQuery objects this.$lightbox = $('#lightbox'); this.$overlay = $('#lightboxOverlay'); this.$outerContainer = this.$lightbox.find('.lb-outerContainer'); this.$container = this.$lightbox.find('.lb-container'); this.$image = this.$lightbox.find('.lb-image'); this.$nav = this.$lightbox.find('.lb-nav'); // Store css values for future lookup this.containerPadding = { top: parseInt(this.$container.css('padding-top'), 10), right: parseInt(this.$container.css('padding-right'), 10), bottom: parseInt(this.$container.css('padding-bottom'), 10), left: parseInt(this.$container.css('padding-left'), 10) }; this.imageBorderWidth = { top: parseInt(this.$image.css('border-top-width'), 10), right: parseInt(this.$image.css('border-right-width'), 10), bottom: parseInt(this.$image.css('border-bottom-width'), 10), left: parseInt(this.$image.css('border-left-width'), 10) }; // Attach event handlers to the newly minted DOM elements this.$overlay.hide().on('click', function() { self.end(); return false; }); this.$lightbox.hide().on('click', function(event) { if ($(event.target).attr('id') === 'lightbox') { self.end(); } }); this.$outerContainer.on('click', function(event) { if ($(event.target).attr('id') === 'lightbox') { self.end(); } return false; }); this.$lightbox.find('.lb-prev').on('click', function() { if (self.currentImageIndex === 0) { self.changeImage(self.album.length - 1); } else { self.changeImage(self.currentImageIndex - 1); } return false; }); this.$lightbox.find('.lb-next').on('click', function() { if (self.currentImageIndex === self.album.length - 1) { self.changeImage(0); } else { self.changeImage(self.currentImageIndex + 1); } return false; }); this.$nav.on('mousedown', function(event) { if (event.which === 3) { self.$nav.css('pointer-events', 'none'); self.$lightbox.one('contextmenu', function() { setTimeout(function() { this.$nav.css('pointer-events', 'auto'); }.bind(self), 0); }); } }); this.$lightbox.find('.lb-loader, .lb-close').on('click keyup', function(e) { if (e.type === 'click' || (e.type === 'keyup' && (e.which === 13 || e.which === 32))) { self.end(); return false; } }); // Add enhanced swipe support for mobile this.setupEnhancedSwipeSupport(); }; /** * Enhanced swipe support for mobile devices * - Prevents conflict with navigation buttons * - Adds visual feedback during swipe * - Only activates on screens <576px */ Lightbox.prototype.setupEnhancedSwipeSupport = function() { var self = this; var startX = 0; var currentX = 0; var isSwiping = false; var swipeThreshold = 50; // Minimum distance to trigger swipe var maxSwipeOffset = 100; // Maximum image movement during swipe function handleTouchStart(e) { if (window.innerWidth >= 576) return; startX = e.originalEvent.touches[0].clientX; currentX = startX; isSwiping = true; // Temporarily disable nav buttons during swipe self.$lightbox.find('.lb-prev, .lb-next').css('pointer-events', 'none'); // Prepare image for movement self.$image.css('transition', 'none'); } function handleTouchMove(e) { if (!isSwiping || window.innerWidth >= 576) return; e.preventDefault(); currentX = e.originalEvent.touches[0].clientX; var diffX = currentX - startX; // Limit maximum movement if (Math.abs(diffX) > maxSwipeOffset) { diffX = diffX > 0 ? maxSwipeOffset : -maxSwipeOffset; } // Move image with finger self.$image.css('transform', 'translateX(' + diffX + 'px)'); } function handleTouchEnd() { if (!isSwiping || window.innerWidth >= 576) { resetSwipe(); return; } var diffX = currentX - startX; var absDiffX = Math.abs(diffX); // Reset image position with animation self.$image.css({ 'transition': 'transform 0.3s ease-out', 'transform': 'translateX(0)' }); // Re-enable nav buttons self.$lightbox.find('.lb-prev, .lb-next').css('pointer-events', 'auto'); // Check if swipe threshold was crossed if (absDiffX > swipeThreshold) { if (diffX < 0) { // Swipe left - next image navigateSwipe('next'); } else { // Swipe right - previous image navigateSwipe('prev'); } } isSwiping = false; // Remove transition after animation completes setTimeout(function() { self.$image.css('transition', 'none'); }, 300); } function resetSwipe() { self.$image.css({ 'transition': 'transform 0.3s ease-out', 'transform': 'translateX(0)' }); self.$lightbox.find('.lb-prev, .lb-next').css('pointer-events', 'auto'); isSwiping = false; setTimeout(function() { self.$image.css('transition', 'none'); }, 300); } function navigateSwipe(direction) { if (direction === 'next') { if (self.currentImageIndex !== self.album.length - 1) { self.changeImage(self.currentImageIndex + 1); } else if (self.options.wrapAround && self.album.length > 1) { self.changeImage(0); } } else if (direction === 'prev') { if (self.currentImageIndex !== 0) { self.changeImage(self.currentImageIndex - 1); } else if (self.options.wrapAround && self.album.length > 1) { self.changeImage(self.album.length - 1); } } } // Add event listeners this.$lightbox.on('touchstart', handleTouchStart); this.$lightbox.on('touchmove', handleTouchMove); this.$lightbox.on('touchend', handleTouchEnd); // Handle window resize $(window).on('resize.lightboxSwipe', function() { if (window.innerWidth < 576) { self.$lightbox.on('touchstart', handleTouchStart); self.$lightbox.on('touchmove', handleTouchMove); self.$lightbox.on('touchend', handleTouchEnd); } else { self.$lightbox.off('touchstart touchmove touchend'); } }); }; // Show overlay and lightbox. If the image is part of a set, add siblings to album array. Lightbox.prototype.start = function($link) { var self = this; var $window = $(window); $window.on('resize', $.proxy(this.sizeOverlay, this)); this.sizeOverlay(); this.album = []; var imageNumber = 0; function addToAlbum($link) { self.album.push({ alt: $link.attr('data-alt'), link: $link.attr('href'), title: $link.attr('data-title') || $link.attr('title') }); } // Support both data-lightbox attribute and rel attribute implementations var dataLightboxValue = $link.attr('data-lightbox'); var $links; if (dataLightboxValue) { $links = $($link.prop('tagName') + '[data-lightbox="' + dataLightboxValue + '"]'); for (var i = 0; i < $links.length; i = ++i) { addToAlbum($($links[i])); if ($links[i] === $link[0]) { imageNumber = i; } } } else { if ($link.attr('rel') === 'lightbox') { // If image is not part of a set addToAlbum($link); } else { // If image is part of a set $links = $($link.prop('tagName') + '[rel="' + $link.attr('rel') + '"]'); for (var j = 0; j < $links.length; j = ++j) { addToAlbum($($links[j])); if ($links[j] === $link[0]) { imageNumber = j; } } } } // Position Lightbox var top = $window.scrollTop() + this.options.positionFromTop; var left = $window.scrollLeft(); this.$lightbox.css({ top: top + 'px', left: left + 'px' }).fadeIn(this.options.fadeDuration); // Disable scrolling of the page while open if (this.options.disableScrolling) { $('body').addClass('lb-disable-scrolling'); } this.changeImage(imageNumber); }; // Hide most UI elements in preparation for the animated resizing of the lightbox. Lightbox.prototype.changeImage = function(imageNumber) { var self = this; var filename = this.album[imageNumber].link; var filetype = filename.split('.').slice(-1)[0]; var $image = this.$lightbox.find('.lb-image'); // Disable keyboard nav during transitions this.disableKeyboardNav(); // Show loading state this.$overlay.fadeIn(this.options.fadeDuration); $('.lb-loader').fadeIn('slow'); this.$lightbox.find('.lb-image, .lb-nav, .lb-prev, .lb-next, .lb-dataContainer, .lb-numbers, .lb-caption').hide(); this.$outerContainer.addClass('animating'); // Reset image position in case it was moved during swipe this.$image.css({ 'transform': 'translateX(0)', 'transition': 'none' }); // When image to show is preloaded, we send the width and height to sizeContainer() var preloader = new Image(); preloader.onload = function() { var $preloader; var imageHeight; var imageWidth; var maxImageHeight; var maxImageWidth; var windowHeight; var windowWidth; $image.attr({ 'alt': self.album[imageNumber].alt, 'src': filename }); $preloader = $(preloader); $image.width(preloader.width); $image.height(preloader.height); var aspectRatio = preloader.width / preloader.height; windowWidth = $(window).width(); windowHeight = $(window).height(); // Calculate the max image dimensions for the current viewport. // Take into account the border around the image and an additional 10px gutter on each side. maxImageWidth = windowWidth - self.containerPadding.left - self.containerPadding.right - self.imageBorderWidth.left - self.imageBorderWidth.right - 20; maxImageHeight = windowHeight - self.containerPadding.top - self.containerPadding.bottom - self.imageBorderWidth.top - self.imageBorderWidth.bottom - self.options.positionFromTop - 70; if (filetype === 'svg') { if (aspectRatio >= 1) { imageWidth = maxImageWidth; imageHeight = parseInt(maxImageWidth / aspectRatio, 10); } else { imageWidth = parseInt(maxImageHeight * aspectRatio, 10); imageHeight = maxImageHeight; } $image.width(imageWidth); $image.height(imageHeight); } else { // Fit image inside the viewport. if (self.options.fitImagesInViewport) { // Check if image size is larger then maxWidth|maxHeight in settings if (self.options.maxWidth && self.options.maxWidth < maxImageWidth) { maxImageWidth = self.options.maxWidth; } if (self.options.maxHeight && self.options.maxHeight < maxImageHeight) { maxImageHeight = self.options.maxHeight; } } else { maxImageWidth = self.options.maxWidth || preloader.width || maxImageWidth; maxImageHeight = self.options.maxHeight || preloader.height || maxImageHeight; } // Is the current image's width or height is greater than the maxImageWidth or maxImageHeight // option than we need to size down while maintaining the aspect ratio. if ((preloader.width > maxImageWidth) || (preloader.height > maxImageHeight)) { if ((preloader.width / maxImageWidth) > (preloader.height / maxImageHeight)) { imageWidth = maxImageWidth; imageHeight = parseInt(preloader.height / (preloader.width / imageWidth), 10); $image.width(imageWidth); $image.height(imageHeight); } else { imageHeight = maxImageHeight; imageWidth = parseInt(preloader.width / (preloader.height / imageHeight), 10); $image.width(imageWidth); $image.height(imageHeight); } } } self.sizeContainer($image.width(), $image.height()); }; // Preload image before showing preloader.src = this.album[imageNumber].link; this.currentImageIndex = imageNumber; }; // Stretch overlay to fit the viewport Lightbox.prototype.sizeOverlay = function() { var self = this; setTimeout(function() { self.$overlay .width($(document).width()) .height($(document).height()); }, 0); }; // Animate the size of the lightbox to fit the image we are showing Lightbox.prototype.sizeContainer = function(imageWidth, imageHeight) { var self = this; var oldWidth = this.$outerContainer.outerWidth(); var oldHeight = this.$outerContainer.outerHeight(); var newWidth = imageWidth + this.containerPadding.left + this.containerPadding.right + this.imageBorderWidth.left + this.imageBorderWidth.right; var newHeight = imageHeight + this.containerPadding.top + this.containerPadding.bottom + this.imageBorderWidth.top + this.imageBorderWidth.bottom; function postResize() { self.$lightbox.find('.lb-dataContainer').width(newWidth); self.$lightbox.find('.lb-prevLink').height(newHeight); self.$lightbox.find('.lb-nextLink').height(newHeight); // Set focus on one of the two root nodes so keyboard events are captured. self.$overlay.trigger('focus'); self.showImage(); } if (oldWidth !== newWidth || oldHeight !== newHeight) { this.$outerContainer.animate({ width: newWidth, height: newHeight }, this.options.resizeDuration, 'swing', function() { postResize(); }); } else { postResize(); } }; // Display the image and its details and begin preload neighboring images. Lightbox.prototype.showImage = function() { this.$lightbox.find('.lb-loader').stop(true).hide(); this.$lightbox.find('.lb-image').fadeIn(this.options.imageFadeDuration); this.updateNav(); this.updateDetails(); this.preloadNeighboringImages(); this.enableKeyboardNav(); }; // Display previous and next navigation if appropriate. Lightbox.prototype.updateNav = function() { // Check to see if the browser supports touch events. If so, we take the conservative approach // and assume that mouse hover events are not supported and always show prev/next navigation // arrows in image sets. var alwaysShowNav = false; try { document.createEvent('TouchEvent'); alwaysShowNav = (this.options.alwaysShowNavOnTouchDevices) ? true : false; } catch (e) {} this.$lightbox.find('.lb-nav').show(); if (this.album.length > 1) { if (this.options.wrapAround) { if (alwaysShowNav) { this.$lightbox.find('.lb-prev, .lb-next').css('opacity', '1'); } this.$lightbox.find('.lb-prev, .lb-next').show(); } else { if (this.currentImageIndex > 0) { this.$lightbox.find('.lb-prev').show(); if (alwaysShowNav) { this.$lightbox.find('.lb-prev').css('opacity', '1'); } } if (this.currentImageIndex < this.album.length - 1) { this.$lightbox.find('.lb-next').show(); if (alwaysShowNav) { this.$lightbox.find('.lb-next').css('opacity', '1'); } } } } }; // Display caption, image number, and closing button. Lightbox.prototype.updateDetails = function() { var self = this; // Enable anchor clicks in the injected caption html. if (typeof this.album[this.currentImageIndex].title !== 'undefined' && this.album[this.currentImageIndex].title !== '') { var $caption = this.$lightbox.find('.lb-caption'); if (this.options.sanitizeTitle) { $caption.text(this.album[this.currentImageIndex].title); } else { $caption.html(this.album[this.currentImageIndex].title); } $caption.fadeIn('fast'); } if (this.album.length > 1 && this.options.showImageNumberLabel) { var labelText = this.imageCountLabel(this.currentImageIndex + 1, this.album.length); this.$lightbox.find('.lb-number').text(labelText).fadeIn('fast'); } else { this.$lightbox.find('.lb-number').hide(); } this.$outerContainer.removeClass('animating'); this.$lightbox.find('.lb-dataContainer').fadeIn(this.options.resizeDuration, function() { return self.sizeOverlay(); }); }; // Preload previous and next images in set. Lightbox.prototype.preloadNeighboringImages = function() { if (this.album.length > this.currentImageIndex + 1) { var preloadNext = new Image(); preloadNext.src = this.album[this.currentImageIndex + 1].link; } if (this.currentImageIndex > 0) { var preloadPrev = new Image(); preloadPrev.src = this.album[this.currentImageIndex - 1].link; } }; Lightbox.prototype.enableKeyboardNav = function() { this.$lightbox.on('keyup.keyboard', $.proxy(this.keyboardAction, this)); this.$overlay.on('keyup.keyboard', $.proxy(this.keyboardAction, this)); }; Lightbox.prototype.disableKeyboardNav = function() { this.$lightbox.off('.keyboard'); this.$overlay.off('.keyboard'); }; Lightbox.prototype.keyboardAction = function(event) { var KEYCODE_ESC = 27; var KEYCODE_LEFTARROW = 37; var KEYCODE_RIGHTARROW = 39; var keycode = event.keyCode; if (keycode === KEYCODE_ESC) { // Prevent bubbling so as to not affect other components on the page. event.stopPropagation(); this.end(); } else if (keycode === KEYCODE_LEFTARROW) { if (this.currentImageIndex !== 0) { this.changeImage(this.currentImageIndex - 1); } else if (this.options.wrapAround && this.album.length > 1) { this.changeImage(this.album.length - 1); } } else if (keycode === KEYCODE_RIGHTARROW) { if (this.currentImageIndex !== this.album.length - 1) { this.changeImage(this.currentImageIndex + 1); } else if (this.options.wrapAround && this.album.length > 1) { this.changeImage(0); } } }; // Closing time. :-( Lightbox.prototype.end = function() { this.disableKeyboardNav(); $(window).off('resize', this.sizeOverlay); $(window).off('resize.lightboxSwipe'); this.$lightbox.off('touchstart touchmove touchend'); this.$lightbox.find('.lb-image').css({ 'transform': 'translateX(0)', 'transition': 'none' }); this.$lightbox.fadeOut(this.options.fadeDuration); this.$overlay.fadeOut(this.options.fadeDuration); if (this.options.disableScrolling) { $('body').removeClass('lb-disable-scrolling'); } }; return new Lightbox(); }));