// ----------------------------------------------------------------------------------- // // Lightbox v2.04 // by Lokesh Dhakar - http://www.lokeshdhakar.com // Last Modification: 2/9/08 // // For more information, visit: // http://lokeshdhakar.com/projects/lightbox2/ // // Modification by Fabian Lange - blog.hma-info.de // - Integration of automatic resize from Michael R. Bagnall - elusivemind.net & Sebastien Grosjean - ZenCocoon.com // - do not display caption of previous image if new image has none // - moved opera hack to resolve disortion in FireFox // - readded window sizes to getPageSize() // // Modifications by Chris Hecker - chrishecker.com // - fixed to work on latest wordpress // - added ability to use anchors (hash links) to link directly to lightboxed images on a page // - added this link, and raw links to images on the details // - ported to jquery, scroll and touch handling, tons of other changes and fixes // // Licensed under the Creative Commons Attribution 2.5 License - http://creativecommons.org/licenses/by/2.5/ // - Free for use in both personal and commercial projects // - Attribution requires leaving author name, author link, and the license info intact. // // Thanks: Scott Upton(uptonic.com), Peter-Paul Koch(quirksmode.com), and Thomas Fuchs(mir.aculo.us) for ideas, libs, and snippets. // Artemy Tregubenko (arty.name) for cleanup and help in updating to latest ver of proto-aculous. // // ----------------------------------------------------------------------------------- var LightboxOptions = { fileLoadingImage: lb_path + 'images/loading.gif', fileBottomNavCloseImage: lb_path + 'images/closelabel.gif', overlayOpacity: lb_opacity, // controls transparency of shadow overlay animate: true, // toggles resizing animations resizeSpeed: lb_resize, // controls the speed of the image resizing animations (1=slowest and 10=fastest) maxBorderSize: 10, // this is imageContainer padding breathingSize: 50, // control the minimum space around the image box closeOriginalWidth: 66, // 66px gif nextBackground: { 'background-image': 'url(' + lb_path + 'images/nextarrow.png)', 'background-repeat': 'no-repeat', 'background-position': '95% 50%' }, prevBackground: { 'background-image': 'url(' + lb_path + 'images/prevarrow.png)', 'background-repeat': 'no-repeat', 'background-position': '5% 50%' }, // When grouping images this is used to write: Image # of #. // Change it for non-english localization labelImage: "Image", labelOf: "of", labelLink: "Link", labelRaw: "File" }; // this seems to be the most reliable way to get the visible viewport without the scroll bars function getVisibleViewportWidth() { if(window.innerWidth) { return Math.min(jQuery(window).width(),window.innerWidth); } else { return jQuery(window).width(); } } function getVisibleViewportHeight() { if(window.innerHeight) { return Math.min(jQuery(window).height(),window.innerHeight); } else { return jQuery(window).height(); } } var Lightbox_debug = false; // console.log("lightbox"); // debugger; function Lightbox_update_debug() { if(Lightbox_debug) { var $ = jQuery; var documentWidth = $(document).width(); var documentHeight = $(document).height(); var windowWidth = $(window).width(); var windowHeight = $(window).height(); var innerWidth = window.innerWidth; var innerHeight = window.innerHeight; var scrollLeft = $(window).scrollLeft(); var scrollTop = $(window).scrollTop(); var viewportWidth = getVisibleViewportWidth(); var viewportHeight = getVisibleViewportHeight(); var lbdebug = $('#lbdebug'); if(lbdebug.length == 0) { lbdebug = $('#overlay').after('
'); } lbdebug.html("d(" + documentWidth + ", " + documentHeight + "), w(" + windowWidth + ", " + windowHeight + "), i(" + innerWidth + ", " + innerHeight + "), v(" + viewportWidth + ", " + viewportHeight + "), s(" + scrollLeft + ", " + scrollTop + ")"); } } function Lightbox($) { var lb = this; lb.imageArray = []; lb.activeImage = undefined; LightboxOptions.resizeSpeed = Math.max(Math.min(LightboxOptions.resizeSpeed,10),1); lb.resizeDuration = 1000 * (LightboxOptions.animate ? ((11 - LightboxOptions.resizeSpeed) * 0.15) : 0); lb.overlayDuration = 1000 * (LightboxOptions.animate ? 0.2 : 0); // shadow fade in/out duration $('body').append('' + ' '); $('#overlay, #lightbox, #outerImageContainer, #imageContainer, #lightboxImage, ' + '#hoverNav, #prevLink, #nextLink, #loading, #loadingLink, ' + '#imageDataContainer, #imageData, #imageDetails, #caption, ' + '#numberDisplay, #bottomNav, #bottomNavClose').each(function() { // this adds all the ids as variables to the lb object, so like lb.imageContainer works var th = $(this); lb[th.attr('id')] = th; }); lb.overlay.click(function(event) { lb.end(); }); lb.lightbox.click(function(event) { if($(event.target).attr('id') == 'lightbox') { // @todo why is this necessary, some kind of inherit? lb.end(); } }); lb.prevLink.click(function(event) { event.preventDefault(); lb.changeImage(lb.activeImage - 1); }); lb.nextLink.click(function(event) { event.preventDefault(); lb.changeImage(lb.activeImage + 1); }); lb.loadingLink.click(function(event) { event.preventDefault(); lb.end(); }); lb.bottomNavClose.click(function(event) { event.preventDefault(); lb.end(); }); lb.touch_startX = lb.touch_startY = lb.touch_startTime = 0; lb.touch_dragging = false; [lb.imageContainer,lb.overlay].forEach(obj => obj.on('touchstart', function (e) { event.preventDefault(); lb.touch_start(e); })); [lb.imageContainer,lb.overlay].forEach(obj => obj.on('touchmove', function (e) { event.preventDefault(); lb.touch_move(e); })); [lb.imageContainer,lb.overlay].forEach(obj => obj.on('touchend', function (e) { event.preventDefault(); lb.touch_end(e); })); lb.lightboxVisible = false; $(document).click(function(event) { if(event.which == 1) { // left click, go jquery var target = $(event.target).closest('a[rel^=lightbox]') || $(event.target).closest('area[rel^=lightbox]'); // @todo why do we search for a or area and then filter on a here? if(target.length && (target[0].localName == 'a') && !target.hasClass('nolightbox')) { event.preventDefault(); lb.start(target[0]); } } }); lb.windowWidth = $(window).width(); lb.viewportWidth = getVisibleViewportWidth(); lb.viewportHeight = getVisibleViewportHeight(); $(window).resize(function() { lb.viewportWidth = getVisibleViewportWidth(); lb.viewportHeight = getVisibleViewportHeight(); // @todo for some reason this is getting called on android chrome on scroll and zoom?! // also, the explicit width lightbox is keeping the document size large var windowWidth = $(window).width(); if(lb.windowWidth != windowWidth) { lb.windowWidth = windowWidth; if(lb.lightboxVisible) { lb.overlay.css({ width: $(document).width() + 'px', height: $(document).height() + 'px' }); lb.adjustImageSize(true); } } Lightbox_update_debug(); }); lb.handleScrollSetup = function() { Lightbox_handleScroll(lb); }; if(Lightbox_debug) { $(document).scroll(Lightbox_update_debug); } Lightbox_update_debug(); } // Display overlay and lightbox. If image is part of a set, add siblings to imageArray. Lightbox.prototype.start = function(imageLink) { var $ = jQuery; var lb = this; var scrollTop = $(window).scrollTop(); lb.overlay.css({ width: $(document).width() + 'px', height: $(document).height() + 'px', opacity: LightboxOptions.overlayOpacity }).show(); lb.imageArray = []; var imageNum = 0; function find_title( el ) { var whitespace = /^\s*$/; var title = el.title; if(!title || whitespace.test(title)) { title = $(el).find('img').title; } if(!title || whitespace.test(title)) { var pp = $(el).next("p.wp-caption-text"); if(pp.length == 1) { title = pp.html(); } } return title; } if(imageLink.rel == 'lightbox') { // if image is NOT part of a set, add single image to imageArray imageLink.horzOnly = $(imageLink).hasClass('lightboxhorzonly'); lb.imageArray.push([imageLink.href, find_title(imageLink), imageLink]); } else { // if image is part of a set... var links = $(imageLink.localName + '[href][rel="' + imageLink.rel + '"]').each(function( idx, el ) { var title = find_title(el); el.horzOnly = $(el).hasClass('lightboxhorzonly'); lb.imageArray.push([el.href, title, el]); }); // @todo sigh, jquery has no Array.uniq(); unlike prototype if(lb.imageArray.length > 0) { while (lb.imageArray[imageNum][0] != imageLink.href) { imageNum++; } } else { return; } } // calculate top and left offset for the lightbox var lightboxTop = scrollTop + ($(window).height() / 10); // hide these before showing lightbox because they might have old widths in them and might overflow lb.outerImageContainer.css({ width: '250px', height: '250px' }); lb.imageDataContainer.hide(); lb.lightboxImage.hide(); lb.lightbox.css({ top: lightboxTop + 'px' }).show(); lb.lightboxVisible = true; lb.changeImage(imageNum); lb.registerHandlers(); } // touch swipe // http://www.javascriptkit.com/javatutors/touchevents2.shtml Lightbox.prototype.touch_start = function(e) { var lb = this; if(e.touches.length == 1) { var touchobj = e.touches[0]; lb.touch_startX = touchobj.pageX; lb.touch_startY = touchobj.pageY; lb.touch_startTime = new Date().getTime(); lb.touch_dragging = true; } } Lightbox.prototype.touch_move = function(e) { var lb = this; if(lb.touch_dragging && (e.touches.length == 1)) { } else { lb.touch_dragging = false; } } Lightbox.prototype.touch_end = function(e) { var lb = this; if(lb.touch_dragging && (e.changedTouches.length == 1)) { lb.touch_dragging = false; var touchobj = e.changedTouches[0]; var dx = touchobj.pageX - lb.touch_startX; var elapsedTime = new Date().getTime() - lb.touch_startTime; var xthreshold = 75; var allowedTime = 500; if((elapsedTime <= allowedTime) && (Math.abs(dx) >= xthreshold) && (Math.abs(touchobj.pageY - lb.touch_startY) <= 100)) { if(dx > 0) { // right lb.changeImage(lb.activeImage - 1); } else if(dx < 0) { // left lb.changeImage(lb.activeImage + 1); } } else { touchobj.target.click(); } } lb.touch_dragging = false; } // Hide most elements and preload image in preparation for resizing image container. Lightbox.prototype.changeImage = function(imageNum) { var $ = jQuery; var lb = this; if((imageNum < 0) || (imageNum > lb.imageArray.length - 1)) { return; } lb.activeImage = imageNum; // update global var // hide elements during transition lb.loading.show(); lb.lightboxImage.hide(); lb.hoverNav.hide(); lb.prevLink.hide(); lb.nextLink.hide(); lb.imageDataContainer.hide(); lb.numberDisplay.hide(); var imgPreloader = new Image(); // once image is preloaded, resize image container imgPreloader.onload = function() { lb.lightboxImage[0].src = lb.imageArray[lb.activeImage][0]; lb.imageArray[lb.activeImage][3] = imgPreloader.width; lb.imageArray[lb.activeImage][4] = imgPreloader.height; lb.adjustImageSize(false); }; imgPreloader.src = lb.imageArray[lb.activeImage][0]; } Lightbox.prototype.adjustImageSize = function( recall ) { var $ = jQuery; var lb = this; var imgWidth = 1, imgHeight = 1; var horzOnly = false; if(lb.activeImage != undefined) { imgWidth = lb.imageArray[lb.activeImage][3]; imgHeight = lb.imageArray[lb.activeImage][4]; horzOnly = lb.imageArray[lb.activeImage][2].horzOnly; } // nb. this really should only be called on resize or create, not on scroll or mobile zoom! var viewportTop = window.pageYOffset ? window.pageYOffset : document.body.scrollTop; var lightboxTop = parseFloat(lb.lightbox[0].style.top); var topOffset = lightboxTop - viewportTop; var windowWidth = $(window).width(); var windowHeight = $(window).height(); var visibleWidth = getVisibleViewportWidth(); var visibleHeight = getVisibleViewportHeight(); var borderSize = LightboxOptions.maxBorderSize; var closeWidth = LightboxOptions.closeOriginalWidth; var nextPrevAlways = false; var maxBreathingSize = LightboxOptions.breathingSize; var minBreathingSize = 3; var maxBreathingSizeWidth = 1024; var minBreathingSizeWidth = 640; var breathingSize = maxBreathingSize; if((windowWidth < minBreathingSizeWidth) || (windowHeight < 480)) { // we're reall small, so don't waste so much room on borders borderSize = 3; closeWidth = 44; nextPrevAlways = true; breathingSize = minBreathingSize; } else if(windowWidth < maxBreathingSizeWidth) { var d = windowWidth - minBreathingSizeWidth; var t = (1.0*d) / (maxBreathingSizeWidth - minBreathingSizeWidth); breathingSize = minBreathingSize + t * (maxBreathingSize-minBreathingSize); } var dataHeightEstimate = 40; var imageAspect = imgWidth / imgHeight; var maxImageWidth = windowWidth - borderSize*2 - breathingSize*2; // @todo this is slightly busted because the breathingSize will decrease as the width decrease so a tall image will grow! // @todo what to do here about the height? if unzoomed, use the window height? // @todo chrome bug where at large zooms these sometimes differ by 1?! var height = (windowWidth <= visibleWidth+1) ? visibleHeight : (imgWidth/imageAspect); var maxImageHeight = height - borderSize*2 - breathingSize - dataHeightEstimate - topOffset; if(!horzOnly && (imgWidth > maxImageWidth) && (imgHeight > maxImageHeight)) { // overflowing both, figure out which to maximize var twh = maxImageWidth / imageAspect; var thw = maxImageHeight * imageAspect; if(twh >= maxImageHeight) { // shrink height imgHeight = maxImageHeight; imgWidth = imgHeight * imageAspect; } else if(thw > maxImageWidth) { // shrink width imgWidth = maxImageWidth; imgHeight = imgWidth / imageAspect; } } else if(imgWidth > maxImageWidth) { // just width imgWidth = maxImageWidth; imgHeight = imgWidth / imageAspect; } else if(!horzOnly && (imgHeight > maxImageHeight)) { // just height imgHeight = maxImageHeight; imgWidth = imgHeight * imageAspect; } lb.overlay.css({ width: $(document).width() + 'px', height: $(document).height() + 'px' }); lb.lightboxImage.css({ height: imgHeight + 'px', width: imgWidth + 'px'}); lb.imageContainer.css({ padding: borderSize + 'px' }); lb.bottomNavClose.css({ width: closeWidth + 'px' }); if(nextPrevAlways) { lb.nextLink.css(LightboxOptions.nextBackground); lb.prevLink.css(LightboxOptions.prevBackground); } else { // must use "" to delete with jquery var clearbg = {'background-image': "", 'background-repeat': "", 'background-position': "", 'background-size': "", 'background-size': "" }; lb.nextLink.css(clearbg); lb.prevLink.css(clearbg); } if (recall == true) { lb.outerImageContainer.css({height: (imgHeight + (borderSize * 2)) + 'px', width: (imgWidth + (borderSize * 2)) + 'px'}); lb.imageDataContainer.css({ width: (imgWidth + (borderSize * 2)) + 'px' }); lb.lightboxTotalHeight = lb.outerImageContainer.height() + lb.imageDataContainer.height(); } else { lb.resizeImageContainer(imgWidth, imgHeight); } } Lightbox.prototype.resizeImageContainer = function(imgWidth, imgHeight) { var $ = jQuery; var lb = this; // get current width and height var widthCurrent = lb.outerImageContainer.width(); var heightCurrent = lb.outerImageContainer.height(); // get new width and height var borderSize = parseInt(lb.imageContainer.css('padding-right')); var widthNew = (imgWidth + borderSize * 2); var heightNew = (imgHeight + borderSize * 2); if(LightboxOptions.animate) { lb.outerImageContainer.animate({ width: widthNew, height: heightNew },lb.resizeDuration,function () { lb.lightboxTotalHeight = lb.outerImageContainer.height() + lb.imageDataContainer.height(); }); // if new and old image are same size and no scaling transition is necessary, // do a quick pause to prevent image flicker. var timeout = 0; if((widthCurrent == widthNew) && (heightCurrent = heightNew)) { timeout = 100; } window.setTimeout(function () { lb.imageDataContainer.css({ width: widthNew + 'px' }); lb.showImage(); },timeout); } else { lb.outerImageContainer.css({ width: widthNew + 'px', height: heightNew + 'px' }); lb.imageDataContainer.css({ width: widthNew + 'px' }); lb.lightboxTotalHeight = lb.outerImageContainer.height() + lb.imageDataContainer.height(); lb.showImage(); } } // Display image and begin preloading neighbors. Lightbox.prototype.showImage = function() { var $ = jQuery; var lb = this; lb.loading.hide(); lb.lightboxImage.fadeIn(lb.resizeDuration, function () { lb.updateDetails(); }); lb.preloadNeighborImages(); } // Display caption, image number, and bottom nav. Lightbox.prototype.updateDetails = function() { var $ = jQuery; var lb = this; // if image is part of set display 'Image x of x' and the links // @todo this is probably not very robust var el = lb.imageArray[lb.activeImage][2]; var postlink; try { postlink = el.closest('.post').find('.posttitle').find('a'); } catch(e) { } var posthref; if(postlink) { posthref = postlink.href; } else { // could be on a regular page, not a blog post posthref = location.protocol + '//' + location.hostname + location.pathname; } var splits = lb.imageArray[lb.activeImage][0].split('/'); var hashlink = posthref + '#lightbox=' + splits[splits.length-1]; var links = '(' + LightboxOptions.labelLink + ', ' + LightboxOptions.labelRaw + ')'; // update the location bar location.hash = '#lightbox=' + splits[splits.length-1]; lb.caption.html(lb.imageArray[lb.activeImage][1] ? lb.imageArray[lb.activeImage][1] : "").show(); if(lb.imageArray.length > 1){ lb.numberDisplay.html(LightboxOptions.labelImage + ' ' + (lb.activeImage + 1) + ' ' + LightboxOptions.labelOf + ' ' + lb.imageArray.length + ' ' + links).show(); } else { lb.numberDisplay.html(links).show(); } lb.imageDataContainer.slideDown(lb.resizeDuration,function () { // update overlay size and update nav lb.overlay.css({ width: $(document).width() + 'px', height: $(document).height() + 'px' }); lb.lightboxTotalHeight = lb.outerImageContainer.height() + lb.imageDataContainer.height(); lb.updateNav(); }); } // Display appropriate previous and next hover navigation. Lightbox.prototype.updateNav = function() { var $ = jQuery; var lb = this; lb.hoverNav.show(); // if not first image in set, display prev image button if(lb.activeImage > 0) { lb.prevLink.show(); } // if not last image in set, display next image button if(lb.activeImage < (lb.imageArray.length - 1)) { lb.nextLink.show(); } } Lightbox.prototype.registerHandlers = function() { var $ = jQuery; var lb = this; $(document).bind('keydown',lb,lb.keyboardAction); $(document).bind('mousewheel wheel',lb,lb.wheelAction); $(window).bind('DOMMouseScroll mousewheel wheel',lb,lb.wheelAction); lb.bodyOverflow = $(document.body).css('overflow'); $(document.body).css({overflow:"hidden"}); } Lightbox.prototype.unregisterHandlers = function() { var $ = jQuery; var lb = this; $(document).unbind('keydown',lb.keyboardAction); $(document).unbind('mousewheel wheel',lb.wheelAction); $(window).unbind('DOMMouseScroll mousewheel wheel',lb.wheelAction); $(document.body).css({overflow:lb.bodyOverflow}); } function Lightbox_handleScroll( lb ) { // this is actually going to be the scroll handler, so make it as snappy as possible! // no jQuery at all, it makes a huge performance difference! // @todo I just can't get this to not suck on mobile if(lb.lightboxVisible) { var lightboxTop = parseInt(lb.lightbox[0].style.top); var lightboxHeight = lb.lightboxTotalHeight; var viewportTop = window.pageYOffset ? window.pageYOffset : document.body.scrollTop; var viewportHeight = lb.viewportHeight; if(viewportHeight < lightboxHeight) { // lightbox is bigger than viewport, we're zoomed in, keep edges on screen if(lightboxTop > viewportTop) { lb.lightbox[0].style.top = viewportTop + 'px'; } else if((lightboxTop + lightboxHeight) < (viewportTop + viewportHeight)) { lb.lightbox[0].style.top = (viewportTop + viewportHeight - lightboxHeight) + 'px'; } } else { // lightbox is smaller than viewport, so don't let it go past edge if(lightboxTop < viewportTop) { lb.lightbox[0].style.top = viewportTop + 'px'; } else if((lightboxTop + lightboxHeight) > (viewportTop + viewportHeight)) { lb.lightbox[0].style.top = (viewportTop + viewportHeight - lightboxHeight) + 'px'; } } } Lightbox_update_debug(); } Lightbox.prototype.wheelAction = function(event) { var $ = jQuery; var lb = event.data; var horzOnly = false; if(lb.activeImage != undefined) { horzOnly = lb.imageArray[lb.activeImage][2].horzOnly; } if(!horzOnly) { if(lb.wheelTimestamp != event.timeStamp) { lb.wheelTimestamp = event.timeStamp; // we get multiple calls here, so need to filter them var delta = -event.originalEvent.deltaY || event.wheelDelta || -event.detail || event.originalEvent.wheelDelta || -event.originalEvent.detail; if(delta > 0) { lb.changeImage(lb.activeImage - 1); } else if(delta < 0) { lb.changeImage(lb.activeImage + 1); } } event.preventDefault(); } } Lightbox.prototype.keyboardAction = function(event) { var $ = jQuery; var lb = event.data; var keycode = event.which; var key = String.fromCharCode(keycode).toLowerCase(); var horzOnly = false; if(lb.activeImage != undefined) { horzOnly = lb.imageArray[lb.activeImage][2].horzOnly; } if(key.match(/x|c/) || (keycode == 27 /* escape */)) { // close lightbox lb.end(); event.preventDefault(); } else if((key == 'p') || (keycode == 37 /* left */) || (!horzOnly && ((keycode == 33 /* pageup */) || (keycode == 38 /* up */)))) { // display previous image lb.changeImage(lb.activeImage - 1); event.preventDefault(); } else if((key == 'n') || (keycode == 39 /* right */) || (keycode == 32 /* space */) || (!horzOnly && ((keycode == 34 /* pagedown */) || (keycode == 40 /* down */)))) { // display next image lb.changeImage(lb.activeImage + 1); event.preventDefault(); } else { // ignore var keys = [35 /* end */, 36 /* home */]; for (var i = keys.length; i--;) { if (keycode == keys[i]) { event.preventDefault(); return; } } } } Lightbox.prototype.preloadNeighborImages = function() { var $ = jQuery; var lb = this; if(lb.imageArray.length > lb.activeImage + 1){ var preloadNextImage = new Image(); preloadNextImage.src = lb.imageArray[lb.activeImage + 1][0]; } if(lb.activeImage > 0){ var preloadPrevImage = new Image(); preloadPrevImage.src = lb.imageArray[lb.activeImage - 1][0]; } } Lightbox.prototype.end = function() { var $ = jQuery; var lb = this; lb.unregisterHandlers(); lb.lightbox.hide(); lb.lightboxVisible = false; lb.overlay.hide(); location.hash = '#n'; // @todo sadly, we can't remove the hash on end because IE sucks } jQuery(document).ready(function($) { var lb = new Lightbox($); // looking for #lightbox=p1040055 var hash = location.hash; var pattern = /lightbox=(.*)$/i; var matches = hash.match(pattern); if(matches && (matches.length == 2)) { var img = matches[1]; var target = $('a[rel^=lightbox][href*="'+img+'"]'); if(target.length == 1) { window.setTimeout(function () { lb.start(target[0]); },500); } } });