4 http://www.jacklmoore.com/colorbox
6 (function ($, document, window) {
8 // Default settings object.
9 // See http://jacklmoore.com/colorbox for details.
17 // behavior and appearance
18 transition: "elastic",
51 slideshowStart: "start slideshow",
52 slideshowStop: "stop slideshow",
53 photoRegex: /\.(gif|png|jp(e|g|eg)|bmp|ico|webp|jxr|svg)((#|\?).*)?$/i,
55 // alternate image paths for high-res displays
58 retinaSuffix: '@2x.$1',
60 // internationalization
61 current: "image {current} of {total}",
65 xhrError: "This content failed to load.",
66 imgError: "This image failed to load.",
83 // using this.href would give the absolute url, when the href may have been inteded as a selector (e.g. '#container')
84 return $(this).attr('href');
89 createImg: function() {
90 var img = new Image();
91 var attrs = $(this).data('cbox-img-attrs');
93 if (typeof attrs === 'object') {
94 $.each(attrs, function(key, val){
101 createIframe: function() {
102 var iframe = document.createElement('iframe');
103 var attrs = $(this).data('cbox-iframe-attrs');
105 if (typeof attrs === 'object') {
106 $.each(attrs, function(key, val){
111 if ('frameBorder' in iframe) {
112 iframe.frameBorder = 0;
114 if ('allowTransparency' in iframe) {
115 iframe.allowTransparency = "true";
117 iframe.name = (new Date()).getTime(); // give the iframe a unique name to prevent caching
118 iframe.allowFullscreen = true;
124 // Abstracting the HTML and event identifiers for easy rebranding
125 colorbox = 'colorbox',
127 boxElement = prefix + 'Element',
130 event_open = prefix + '_open',
131 event_load = prefix + '_load',
132 event_complete = prefix + '_complete',
133 event_cleanup = prefix + '_cleanup',
134 event_closed = prefix + '_closed',
135 event_purge = prefix + '_purge',
137 // Cached jQuery Object Variables
158 $events = $('<a/>'), // $({}) would be prefered, but there is an issue with jQuery 1.4.2
160 // Variables for cached values or use across multiple functions
182 // Convenience function for creating new jQuery objects
183 function $tag(tag, id, css) {
184 var element = document.createElement(tag);
187 element.id = prefix + id;
191 element.style.cssText = css;
197 // Get the window height using innerHeight when available to avoid an issue with iOS
198 // http://bugs.jquery.com/ticket/6724
199 function winheight() {
200 return window.innerHeight ? window.innerHeight : $(window).height();
203 function Settings(element, options) {
204 if (options !== Object(options)) {
211 this.value = function(key) {
214 if (this.cache[key] === undefined) {
215 dataAttr = $(this.el).attr('data-cbox-'+key);
217 if (dataAttr !== undefined) {
218 this.cache[key] = dataAttr;
219 } else if (options[key] !== undefined) {
220 this.cache[key] = options[key];
221 } else if (defaults[key] !== undefined) {
222 this.cache[key] = defaults[key];
226 return this.cache[key];
229 this.get = function(key) {
230 var value = this.value(key);
231 return $.isFunction(value) ? value.call(this.el, this) : value;
235 // Determine the next and previous members in a group.
236 function getIndex(increment) {
238 max = $related.length,
239 newIndex = (index + increment) % max;
241 return (newIndex < 0) ? max + newIndex : newIndex;
244 // Convert '%' and 'px' values to integers
245 function setSize(size, dimension) {
246 return Math.round((/%/.test(size) ? ((dimension === 'x' ? $window.width() : winheight()) / 100) : 1) * parseInt(size, 10));
249 // Checks an href to see if it is a photo.
250 // There is a force photo option (photo: true) for hrefs that cannot be matched by the regex.
251 function isImage(settings, url) {
252 return settings.get('photo') || settings.get('photoRegex').test(url);
255 function retinaUrl(settings, url) {
256 return settings.get('retinaUrl') && window.devicePixelRatio > 1 ? url.replace(settings.get('photoRegex'), settings.get('retinaSuffix')) : url;
259 function trapFocus(e) {
260 if ('contains' in $box[0] && !$box[0].contains(e.target) && e.target !== $overlay[0]) {
266 function setClass(str) {
267 if (setClass.str !== str) {
268 $box.add($overlay).removeClass(setClass.str).addClass(str);
273 function getRelated(rel) {
276 if (rel && rel !== false && rel !== 'nofollow') {
277 $related = $('.' + boxElement).filter(function () {
278 var options = $.data(this, colorbox);
279 var settings = new Settings(this, options);
280 return (settings.get('rel') === rel);
282 index = $related.index(settings.el);
284 // Check direct calls to Colorbox.
286 $related = $related.add(settings.el);
287 index = $related.length - 1;
290 $related = $(settings.el);
294 function trigger(event) {
296 $(document).trigger(event);
298 $events.triggerHandler(event);
301 var slideshow = (function(){
303 className = prefix + "Slideshow_",
304 click = "click." + prefix,
308 clearTimeout(timeOut);
312 if (settings.get('loop') || $related[index + 1]) {
314 timeOut = setTimeout(publicMethod.next, settings.get('slideshowSpeed'));
320 .html(settings.get('slideshowStop'))
325 .bind(event_complete, set)
326 .bind(event_load, clear);
328 $box.removeClass(className + "off").addClass(className + "on");
335 .unbind(event_complete, set)
336 .unbind(event_load, clear);
339 .html(settings.get('slideshowStart'))
341 .one(click, function () {
346 $box.removeClass(className + "on").addClass(className + "off");
354 .unbind(event_complete, set)
355 .unbind(event_load, clear);
356 $box.removeClass(className + "off " + className + "on");
361 if (!settings.get('slideshow')) {
362 $events.unbind(event_cleanup, reset);
366 if (settings.get('slideshow') && $related[1]) {
368 $events.one(event_cleanup, reset);
369 if (settings.get('slideshowAuto')) {
382 function launch(element) {
387 options = $(element).data(colorbox);
389 settings = new Settings(element, options);
391 getRelated(settings.get('rel'));
394 open = active = true; // Prevents the page-change action from queuing up if the visitor holds down the left or right keys.
396 setClass(settings.get('className'));
398 // Show colorbox so the sizes can be calculated in older versions of jQuery
399 $box.css({visibility:'hidden', display:'block', opacity:''});
401 $loaded = $tag(div, 'LoadedContent', 'width:0; height:0; overflow:hidden; visibility:hidden');
402 $content.css({width:'', height:''}).append($loaded);
404 // Cache values needed for size calculations
405 interfaceHeight = $topBorder.height() + $bottomBorder.height() + $content.outerHeight(true) - $content.height();
406 interfaceWidth = $leftBorder.width() + $rightBorder.width() + $content.outerWidth(true) - $content.width();
407 loadedHeight = $loaded.outerHeight(true);
408 loadedWidth = $loaded.outerWidth(true);
410 // Opens inital empty Colorbox prior to content being loaded.
411 var initialWidth = setSize(settings.get('initialWidth'), 'x');
412 var initialHeight = setSize(settings.get('initialHeight'), 'y');
413 var maxWidth = settings.get('maxWidth');
414 var maxHeight = settings.get('maxHeight');
416 settings.w = Math.max((maxWidth !== false ? Math.min(initialWidth, setSize(maxWidth, 'x')) : initialWidth) - loadedWidth - interfaceWidth, 0);
417 settings.h = Math.max((maxHeight !== false ? Math.min(initialHeight, setSize(maxHeight, 'y')) : initialHeight) - loadedHeight - interfaceHeight, 0);
419 $loaded.css({width:'', height:settings.h});
420 publicMethod.position();
423 settings.get('onOpen');
425 $groupControls.add($title).hide();
429 if (settings.get('trapFocus')) {
430 // Confine focus to the modal
431 // Uses event capturing that is not supported in IE8-
432 if (document.addEventListener) {
434 document.addEventListener('focus', trapFocus, true);
436 $events.one(event_closed, function () {
437 document.removeEventListener('focus', trapFocus, true);
442 // Return focus on closing
443 if (settings.get('returnFocus')) {
444 $events.one(event_closed, function () {
445 $(settings.el).focus();
450 var opacity = parseFloat(settings.get('opacity'));
452 opacity: opacity === opacity ? opacity : '',
453 cursor: settings.get('overlayClose') ? 'pointer' : '',
454 visibility: 'visible'
457 if (settings.get('closeButton')) {
458 $close.html(settings.get('close')).appendTo($content);
460 $close.appendTo('<div/>'); // replace with .detach() when dropping jQuery < 1.4
467 // Colorbox's markup needs to be added to the DOM prior to being called
468 // so that the browser will go ahead and load the CSS background images.
469 function appendHTML() {
473 $box = $tag(div).attr({
475 'class': $.support.opacity === false ? prefix + 'IE' : '', // class for optional IE8 & lower targeted CSS.
479 $overlay = $tag(div, "Overlay").hide();
480 $loadingOverlay = $([$tag(div, "LoadingOverlay")[0],$tag(div, "LoadingGraphic")[0]]);
481 $wrap = $tag(div, "Wrapper");
482 $content = $tag(div, "Content").append(
483 $title = $tag(div, "Title"),
484 $current = $tag(div, "Current"),
485 $prev = $('<button type="button"/>').attr({id:prefix+'Previous'}),
486 $next = $('<button type="button"/>').attr({id:prefix+'Next'}),
487 $slideshow = $tag('button', "Slideshow"),
491 $close = $('<button type="button"/>').attr({id:prefix+'Close'});
493 $wrap.append( // The 3x3 Grid that makes up Colorbox
495 $tag(div, "TopLeft"),
496 $topBorder = $tag(div, "TopCenter"),
497 $tag(div, "TopRight")
499 $tag(div, false, 'clear:left').append(
500 $leftBorder = $tag(div, "MiddleLeft"),
502 $rightBorder = $tag(div, "MiddleRight")
504 $tag(div, false, 'clear:left').append(
505 $tag(div, "BottomLeft"),
506 $bottomBorder = $tag(div, "BottomCenter"),
507 $tag(div, "BottomRight")
509 ).find('div div').css({'float': 'left'});
511 $loadingBay = $tag(div, false, 'position:absolute; width:9999px; visibility:hidden; display:none; max-width:none;');
513 $groupControls = $next.add($prev).add($current).add($slideshow);
515 if (document.body && !$box.parent().length) {
516 $(document.body).append($overlay, $box.append($wrap, $loadingBay));
520 // Add Colorbox's event bindings
521 function addBindings() {
522 function clickHandler(e) {
523 // ignore non-left-mouse-clicks and clicks modified with ctrl / command, shift, or alt.
524 // See: http://jacklmoore.com/notes/click-events/
525 if (!(e.which > 1 || e.shiftKey || e.altKey || e.metaKey || e.ctrlKey)) {
535 // Anonymous functions here keep the public method from being cached, thereby allowing them to be redefined on the fly.
536 $next.click(function () {
539 $prev.click(function () {
542 $close.click(function () {
543 publicMethod.close();
545 $overlay.click(function () {
546 if (settings.get('overlayClose')) {
547 publicMethod.close();
552 $(document).bind('keydown.' + prefix, function (e) {
554 if (open && settings.get('escKey') && key === 27) {
556 publicMethod.close();
558 if (open && settings.get('arrowKey') && $related[1] && !e.altKey) {
562 } else if (key === 39) {
569 if ($.isFunction($.fn.on)) {
571 $(document).on('click.'+prefix, '.'+boxElement, clickHandler);
573 // For jQuery 1.3.x -> 1.6.x
574 // This code is never reached in jQuery 1.9, so do not contact me about 'live' being removed.
575 // This is not here for jQuery 1.9, it's here for legacy users.
576 $('.'+boxElement).live('click.'+prefix, clickHandler);
584 // Don't do anything if Colorbox already exists.
589 // Append the HTML when the DOM loads
595 // Usage format: $.colorbox.close();
596 // Usage from within an iframe: parent.jQuery.colorbox.close();
599 publicMethod = $.fn[colorbox] = $[colorbox] = function (options, callback) {
603 options = options || {};
605 if ($.isFunction($obj)) { // assume a call to $.colorbox
610 if (!$obj[0]) { // colorbox being applied to empty collection
619 options.onComplete = callback;
622 $obj.each(function () {
623 var old = $.data(this, colorbox) || {};
624 $.data(this, colorbox, $.extend(old, options));
625 }).addClass(boxElement);
627 settings = new Settings($obj[0], options);
629 if (settings.get('open')) {
637 publicMethod.position = function (speed, loadedCallback) {
642 offset = $box.offset(),
646 $window.unbind('resize.' + prefix);
648 // remove the modal so that it doesn't influence the document width/height
649 $box.css({top: -9e4, left: -9e4});
651 scrollTop = $window.scrollTop();
652 scrollLeft = $window.scrollLeft();
654 if (settings.get('fixed')) {
655 offset.top -= scrollTop;
656 offset.left -= scrollLeft;
657 $box.css({position: 'fixed'});
661 $box.css({position: 'absolute'});
664 // keeps the top and left positions within the browser's viewport.
665 if (settings.get('right') !== false) {
666 left += Math.max($window.width() - settings.w - loadedWidth - interfaceWidth - setSize(settings.get('right'), 'x'), 0);
667 } else if (settings.get('left') !== false) {
668 left += setSize(settings.get('left'), 'x');
670 left += Math.round(Math.max($window.width() - settings.w - loadedWidth - interfaceWidth, 0) / 2);
673 if (settings.get('bottom') !== false) {
674 top += Math.max(winheight() - settings.h - loadedHeight - interfaceHeight - setSize(settings.get('bottom'), 'y'), 0);
675 } else if (settings.get('top') !== false) {
676 top += setSize(settings.get('top'), 'y');
678 top += Math.round(Math.max(winheight() - settings.h - loadedHeight - interfaceHeight, 0) / 2);
681 $box.css({top: offset.top, left: offset.left, visibility:'visible'});
683 // this gives the wrapper plenty of breathing room so it's floated contents can move around smoothly,
684 // but it has to be shrank down around the size of div#colorbox when it's done. If not,
685 // it can invoke an obscure IE bug when using iframes.
686 $wrap[0].style.width = $wrap[0].style.height = "9999px";
688 function modalDimensions() {
689 $topBorder[0].style.width = $bottomBorder[0].style.width = $content[0].style.width = (parseInt($box[0].style.width,10) - interfaceWidth)+'px';
690 $content[0].style.height = $leftBorder[0].style.height = $rightBorder[0].style.height = (parseInt($box[0].style.height,10) - interfaceHeight)+'px';
693 css = {width: settings.w + loadedWidth + interfaceWidth, height: settings.h + loadedHeight + interfaceHeight, top: top, left: left};
695 // setting the speed to 0 if the content hasn't changed size or position
698 $.each(css, function(i){
699 if (css[i] !== previousCSS[i]) {
713 $box.dequeue().animate(css, {
714 duration: speed || 0,
715 complete: function () {
720 // shrink the wrapper down to exactly the size of colorbox to avoid a bug in IE's iframe implementation.
721 $wrap[0].style.width = (settings.w + loadedWidth + interfaceWidth) + "px";
722 $wrap[0].style.height = (settings.h + loadedHeight + interfaceHeight) + "px";
724 if (settings.get('reposition')) {
725 setTimeout(function () { // small delay before binding onresize due to an IE8 bug.
726 $window.bind('resize.' + prefix, publicMethod.position);
730 if ($.isFunction(loadedCallback)) {
734 step: modalDimensions
738 publicMethod.resize = function (options) {
742 options = options || {};
745 settings.w = setSize(options.width, 'x') - loadedWidth - interfaceWidth;
748 if (options.innerWidth) {
749 settings.w = setSize(options.innerWidth, 'x');
752 $loaded.css({width: settings.w});
754 if (options.height) {
755 settings.h = setSize(options.height, 'y') - loadedHeight - interfaceHeight;
758 if (options.innerHeight) {
759 settings.h = setSize(options.innerHeight, 'y');
762 if (!options.innerHeight && !options.height) {
763 scrolltop = $loaded.scrollTop();
764 $loaded.css({height: "auto"});
765 settings.h = $loaded.height();
768 $loaded.css({height: settings.h});
771 $loaded.scrollTop(scrolltop);
774 publicMethod.position(settings.get('transition') === "none" ? 0 : settings.get('speed'));
778 publicMethod.prep = function (object) {
783 var callback, speed = settings.get('transition') === "none" ? 0 : settings.get('speed');
787 $loaded = $tag(div, 'LoadedContent').append(object);
789 function getWidth() {
790 settings.w = settings.w || $loaded.width();
791 settings.w = settings.mw && settings.mw < settings.w ? settings.mw : settings.w;
794 function getHeight() {
795 settings.h = settings.h || $loaded.height();
796 settings.h = settings.mh && settings.mh < settings.h ? settings.mh : settings.h;
801 .appendTo($loadingBay.show())// content has to be appended to the DOM for accurate size calculations.
802 .css({width: getWidth(), overflow: settings.get('scrolling') ? 'auto' : 'hidden'})
803 .css({height: getHeight()})// sets the height independently from the width in case the new width influences the value of height.
804 .prependTo($content);
808 // floating the IMG removes the bottom line-height and fixed a problem where IE miscalculates the width of the parent element as 100% of the document width.
810 $(photo).css({'float': 'none'});
812 setClass(settings.get('className'));
814 callback = function () {
815 var total = $related.length,
823 function removeFilter() { // Needed for IE8 in versions of jQuery prior to 1.7.2
824 if ($.support.opacity === false) {
825 $box[0].style.removeAttribute('filter');
829 complete = function () {
830 clearTimeout(loadingTimer);
831 $loadingOverlay.hide();
832 trigger(event_complete);
833 settings.get('onComplete');
837 $title.html(settings.get('title')).show();
840 if (total > 1) { // handle grouping
841 if (typeof settings.get('current') === "string") {
842 $current.html(settings.get('current').replace('{current}', index + 1).replace('{total}', total)).show();
845 $next[(settings.get('loop') || index < total - 1) ? "show" : "hide"]().html(settings.get('next'));
846 $prev[(settings.get('loop') || index) ? "show" : "hide"]().html(settings.get('previous'));
850 // Preloads images within a rel group
851 if (settings.get('preloading')) {
852 $.each([getIndex(-1), getIndex(1)], function(){
855 settings = new Settings(i, $.data(i, colorbox)),
856 src = settings.get('href');
858 if (src && isImage(settings, src)) {
859 src = retinaUrl(settings, src);
860 img = document.createElement('img');
866 $groupControls.hide();
869 if (settings.get('iframe')) {
871 iframe = settings.get('createIframe');
873 if (!settings.get('scrolling')) {
874 iframe.scrolling = "no";
879 src: settings.get('href'),
880 'class': prefix + 'Iframe'
882 .one('load', complete)
885 $events.one(event_purge, function () {
886 iframe.src = "//about:blank";
889 if (settings.get('fastIframe')) {
890 $(iframe).trigger('load');
896 if (settings.get('transition') === 'fade') {
897 $box.fadeTo(speed, 1, removeFilter);
903 if (settings.get('transition') === 'fade') {
904 $box.fadeTo(speed, 0, function () {
905 publicMethod.position(0, callback);
908 publicMethod.position(speed, callback);
913 var href, setResize, prep = publicMethod.prep, $inline, request = ++requests;
919 trigger(event_purge);
921 settings.get('onLoad');
923 settings.h = settings.get('height') ?
924 setSize(settings.get('height'), 'y') - loadedHeight - interfaceHeight :
925 settings.get('innerHeight') && setSize(settings.get('innerHeight'), 'y');
927 settings.w = settings.get('width') ?
928 setSize(settings.get('width'), 'x') - loadedWidth - interfaceWidth :
929 settings.get('innerWidth') && setSize(settings.get('innerWidth'), 'x');
931 // Sets the minimum dimensions for use in image scaling
932 settings.mw = settings.w;
933 settings.mh = settings.h;
935 // Re-evaluate the minimum width and height based on maxWidth and maxHeight values.
936 // If the width or height exceed the maxWidth or maxHeight, use the maximum values instead.
937 if (settings.get('maxWidth')) {
938 settings.mw = setSize(settings.get('maxWidth'), 'x') - loadedWidth - interfaceWidth;
939 settings.mw = settings.w && settings.w < settings.mw ? settings.w : settings.mw;
941 if (settings.get('maxHeight')) {
942 settings.mh = setSize(settings.get('maxHeight'), 'y') - loadedHeight - interfaceHeight;
943 settings.mh = settings.h && settings.h < settings.mh ? settings.h : settings.mh;
946 href = settings.get('href');
948 loadingTimer = setTimeout(function () {
949 $loadingOverlay.show();
952 if (settings.get('inline')) {
953 var $target = $(href);
954 // Inserts an empty placeholder where inline content is being pulled from.
955 // An event is bound to put inline content back when Colorbox closes or loads new content.
956 $inline = $('<div>').hide().insertBefore($target);
958 $events.one(event_purge, function () {
959 $inline.replaceWith($target);
963 } else if (settings.get('iframe')) {
964 // IFrame element won't be added to the DOM until it is ready to be displayed,
965 // to avoid problems with DOM-ready JS that might be trying to run in that iframe.
967 } else if (settings.get('html')) {
968 prep(settings.get('html'));
969 } else if (isImage(settings, href)) {
971 href = retinaUrl(settings, href);
973 photo = settings.get('createImg');
976 .addClass(prefix + 'Photo')
977 .bind('error.'+prefix,function () {
978 prep($tag(div, 'Error').html(settings.get('imgError')));
980 .one('load', function () {
981 if (request !== requests) {
985 // A small pause because some browsers will occassionaly report a
986 // img.width and img.height of zero immediately after the img.onload fires
987 setTimeout(function(){
990 if (settings.get('retinaImage') && window.devicePixelRatio > 1) {
991 photo.height = photo.height / window.devicePixelRatio;
992 photo.width = photo.width / window.devicePixelRatio;
995 if (settings.get('scalePhotos')) {
996 setResize = function () {
997 photo.height -= photo.height * percent;
998 photo.width -= photo.width * percent;
1000 if (settings.mw && photo.width > settings.mw) {
1001 percent = (photo.width - settings.mw) / photo.width;
1004 if (settings.mh && photo.height > settings.mh) {
1005 percent = (photo.height - settings.mh) / photo.height;
1011 photo.style.marginTop = Math.max(settings.mh - photo.height, 0) / 2 + 'px';
1014 if ($related[1] && (settings.get('loop') || $related[index + 1])) {
1015 photo.style.cursor = 'pointer';
1017 $(photo).bind('click.'+prefix, function () {
1018 publicMethod.next();
1022 photo.style.width = photo.width + 'px';
1023 photo.style.height = photo.height + 'px';
1031 $loadingBay.load(href, settings.get('data'), function (data, status) {
1032 if (request === requests) {
1033 prep(status === 'error' ? $tag(div, 'Error').html(settings.get('xhrError')) : $(this).contents());
1039 // Navigates to the next page/image in a set.
1040 publicMethod.next = function () {
1041 if (!active && $related[1] && (settings.get('loop') || $related[index + 1])) {
1042 index = getIndex(1);
1043 launch($related[index]);
1047 publicMethod.prev = function () {
1048 if (!active && $related[1] && (settings.get('loop') || index)) {
1049 index = getIndex(-1);
1050 launch($related[index]);
1054 // Note: to use this within an iframe use the following format: parent.jQuery.colorbox.close();
1055 publicMethod.close = function () {
1056 if (open && !closing) {
1060 trigger(event_cleanup);
1061 settings.get('onCleanup');
1062 $window.unbind('.' + prefix);
1063 $overlay.fadeTo(settings.get('fadeOut') || 0, 0);
1065 $box.stop().fadeTo(settings.get('fadeOut') || 0, 0, function () {
1068 trigger(event_purge);
1071 setTimeout(function () {
1073 trigger(event_closed);
1074 settings.get('onClosed');
1080 // Removes changes Colorbox made to the document, but does not remove the plugin.
1081 publicMethod.remove = function () {
1082 if (!$box) { return; }
1085 $[colorbox].close();
1086 $box.stop(false, true).remove();
1091 .removeData(colorbox)
1092 .removeClass(boxElement);
1094 $(document).unbind('click.'+prefix).unbind('keydown.'+prefix);
1097 // A method for fetching the current element Colorbox is referencing.
1098 // returns a jQuery object.
1099 publicMethod.element = function () {
1100 return $(settings.el);
1103 publicMethod.settings = defaults;
1105 }(jQuery, document, window));