lightbox.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. /*!
  2. * Lightbox v2.11.0
  3. * by Lokesh Dhakar
  4. *
  5. * More info:
  6. * http://lokeshdhakar.com/projects/lightbox2/
  7. *
  8. * Copyright Lokesh Dhakar
  9. * Released under the MIT license
  10. * https://github.com/lokesh/lightbox2/blob/master/LICENSE
  11. *
  12. * @preserve
  13. */
  14. // Uses Node, AMD or browser globals to create a module.
  15. (function (root, factory) {
  16. if (typeof define === 'function' && define.amd) {
  17. // AMD. Register as an anonymous module.
  18. define(['jquery'], factory);
  19. } else if (typeof exports === 'object') {
  20. // Node. Does not work with strict CommonJS, but
  21. // only CommonJS-like environments that support module.exports,
  22. // like Node.
  23. module.exports = factory(require('jquery'));
  24. } else {
  25. // Browser globals (root is window)
  26. root.lightbox = factory(root.jQuery);
  27. }
  28. }(this, function ($) {
  29. function Lightbox(options) {
  30. this.album = [];
  31. this.currentImageIndex = void 0;
  32. this.init();
  33. // options
  34. this.options = $.extend({}, this.constructor.defaults);
  35. this.option(options);
  36. }
  37. // Descriptions of all options available on the demo site:
  38. // http://lokeshdhakar.com/projects/lightbox2/index.html#options
  39. Lightbox.defaults = {
  40. albumLabel: 'Image %1 of %2',
  41. alwaysShowNavOnTouchDevices: false,
  42. fadeDuration: 600,
  43. fitImagesInViewport: true,
  44. imageFadeDuration: 600,
  45. // maxWidth: 800,
  46. // maxHeight: 600,
  47. positionFromTop: 50,
  48. resizeDuration: 700,
  49. showImageNumberLabel: true,
  50. wrapAround: false,
  51. disableScrolling: false,
  52. /*
  53. Sanitize Title
  54. If the caption data is trusted, for example you are hardcoding it in, then leave this to false.
  55. This will free you to add html tags, such as links, in the caption.
  56. If the caption data is user submitted or from some other untrusted source, then set this to true
  57. to prevent xss and other injection attacks.
  58. */
  59. sanitizeTitle: false
  60. };
  61. Lightbox.prototype.option = function(options) {
  62. $.extend(this.options, options);
  63. };
  64. Lightbox.prototype.imageCountLabel = function(currentImageNum, totalImages) {
  65. return this.options.albumLabel.replace(/%1/g, currentImageNum).replace(/%2/g, totalImages);
  66. };
  67. Lightbox.prototype.init = function() {
  68. var self = this;
  69. // Both enable and build methods require the body tag to be in the DOM.
  70. $(document).ready(function() {
  71. self.enable();
  72. self.build();
  73. });
  74. };
  75. // Loop through anchors and areamaps looking for either data-lightbox attributes or rel attributes
  76. // that contain 'lightbox'. When these are clicked, start lightbox.
  77. Lightbox.prototype.enable = function() {
  78. var self = this;
  79. $('body').on('click', 'a[rel^=lightbox], area[rel^=lightbox], a[data-lightbox], area[data-lightbox]', function(event) {
  80. self.start($(event.currentTarget));
  81. return false;
  82. });
  83. };
  84. // Build html for the lightbox and the overlay.
  85. // Attach event handlers to the new DOM elements. click click click
  86. Lightbox.prototype.build = function() {
  87. if ($('#lightbox').length > 0) {
  88. return;
  89. }
  90. var self = this;
  91. $('<div id="lightboxOverlay" class="lightboxOverlay"></div><div id="lightbox" class="lightbox"><div class="lb-outerContainer"><div class="lb-container"><img class="lb-image" src="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" alt=""/><div class="lb-nav"><a class="lb-prev" aria-label="Previous image" href="" ></a><a class="lb-next" aria-label="Next image" href="" ></a></div><div class="lb-loader"><a class="lb-cancel"></a></div></div></div><div class="lb-dataContainer"><div class="lb-data"><div class="lb-details"><span class="lb-caption"></span><span class="lb-number"></span></div><div class="lb-closeContainer"><a class="lb-close"></a></div></div></div></div>').appendTo($('body'));
  92. // Cache jQuery objects
  93. this.$lightbox = $('#lightbox');
  94. this.$overlay = $('#lightboxOverlay');
  95. this.$outerContainer = this.$lightbox.find('.lb-outerContainer');
  96. this.$container = this.$lightbox.find('.lb-container');
  97. this.$image = this.$lightbox.find('.lb-image');
  98. this.$nav = this.$lightbox.find('.lb-nav');
  99. // Store css values for future lookup
  100. this.containerPadding = {
  101. top: parseInt(this.$container.css('padding-top'), 10),
  102. right: parseInt(this.$container.css('padding-right'), 10),
  103. bottom: parseInt(this.$container.css('padding-bottom'), 10),
  104. left: parseInt(this.$container.css('padding-left'), 10)
  105. };
  106. this.imageBorderWidth = {
  107. top: parseInt(this.$image.css('border-top-width'), 10),
  108. right: parseInt(this.$image.css('border-right-width'), 10),
  109. bottom: parseInt(this.$image.css('border-bottom-width'), 10),
  110. left: parseInt(this.$image.css('border-left-width'), 10)
  111. };
  112. // Attach event handlers to the newly minted DOM elements
  113. this.$overlay.hide().on('click', function() {
  114. self.end();
  115. return false;
  116. });
  117. this.$lightbox.hide().on('click', function(event) {
  118. if ($(event.target).attr('id') === 'lightbox') {
  119. self.end();
  120. }
  121. });
  122. this.$outerContainer.on('click', function(event) {
  123. if ($(event.target).attr('id') === 'lightbox') {
  124. self.end();
  125. }
  126. return false;
  127. });
  128. this.$lightbox.find('.lb-prev').on('click', function() {
  129. if (self.currentImageIndex === 0) {
  130. self.changeImage(self.album.length - 1);
  131. } else {
  132. self.changeImage(self.currentImageIndex - 1);
  133. }
  134. return false;
  135. });
  136. this.$lightbox.find('.lb-next').on('click', function() {
  137. if (self.currentImageIndex === self.album.length - 1) {
  138. self.changeImage(0);
  139. } else {
  140. self.changeImage(self.currentImageIndex + 1);
  141. }
  142. return false;
  143. });
  144. /*
  145. Show context menu for image on right-click
  146. There is a div containing the navigation that spans the entire image and lives above of it. If
  147. you right-click, you are right clicking this div and not the image. This prevents users from
  148. saving the image or using other context menu actions with the image.
  149. To fix this, when we detect the right mouse button is pressed down, but not yet clicked, we
  150. set pointer-events to none on the nav div. This is so that the upcoming right-click event on
  151. the next mouseup will bubble down to the image. Once the right-click/contextmenu event occurs
  152. we set the pointer events back to auto for the nav div so it can capture hover and left-click
  153. events as usual.
  154. */
  155. this.$nav.on('mousedown', function(event) {
  156. if (event.which === 3) {
  157. self.$nav.css('pointer-events', 'none');
  158. self.$lightbox.one('contextmenu', function() {
  159. setTimeout(function() {
  160. this.$nav.css('pointer-events', 'auto');
  161. }.bind(self), 0);
  162. });
  163. }
  164. });
  165. this.$lightbox.find('.lb-loader, .lb-close').on('click', function() {
  166. self.end();
  167. return false;
  168. });
  169. };
  170. // Show overlay and lightbox. If the image is part of a set, add siblings to album array.
  171. Lightbox.prototype.start = function($link) {
  172. var self = this;
  173. var $window = $(window);
  174. $window.on('resize', $.proxy(this.sizeOverlay, this));
  175. this.sizeOverlay();
  176. this.album = [];
  177. var imageNumber = 0;
  178. function addToAlbum($link) {
  179. self.album.push({
  180. alt: $link.attr('data-alt'),
  181. link: $link.attr('href'),
  182. title: $link.attr('data-title') || $link.attr('title')
  183. });
  184. }
  185. // Support both data-lightbox attribute and rel attribute implementations
  186. var dataLightboxValue = $link.attr('data-lightbox');
  187. var $links;
  188. if (dataLightboxValue) {
  189. $links = $($link.prop('tagName') + '[data-lightbox="' + dataLightboxValue + '"]');
  190. for (var i = 0; i < $links.length; i = ++i) {
  191. addToAlbum($($links[i]));
  192. if ($links[i] === $link[0]) {
  193. imageNumber = i;
  194. }
  195. }
  196. } else {
  197. if ($link.attr('rel') === 'lightbox') {
  198. // If image is not part of a set
  199. addToAlbum($link);
  200. } else {
  201. // If image is part of a set
  202. $links = $($link.prop('tagName') + '[rel="' + $link.attr('rel') + '"]');
  203. for (var j = 0; j < $links.length; j = ++j) {
  204. addToAlbum($($links[j]));
  205. if ($links[j] === $link[0]) {
  206. imageNumber = j;
  207. }
  208. }
  209. }
  210. }
  211. // Position Lightbox
  212. var top = $window.scrollTop() + this.options.positionFromTop;
  213. var left = $window.scrollLeft();
  214. this.$lightbox.css({
  215. top: top + 'px',
  216. left: left + 'px'
  217. }).fadeIn(this.options.fadeDuration);
  218. // Disable scrolling of the page while open
  219. if (this.options.disableScrolling) {
  220. $('body').addClass('lb-disable-scrolling');
  221. }
  222. this.changeImage(imageNumber);
  223. };
  224. // Hide most UI elements in preparation for the animated resizing of the lightbox.
  225. Lightbox.prototype.changeImage = function(imageNumber) {
  226. var self = this;
  227. var filename = this.album[imageNumber].link;
  228. var filetype = filename.split('.').slice(-1)[0];
  229. var $image = this.$lightbox.find('.lb-image');
  230. // Disable keyboard nav during transitions
  231. this.disableKeyboardNav();
  232. // Show loading state
  233. this.$overlay.fadeIn(this.options.fadeDuration);
  234. $('.lb-loader').fadeIn('slow');
  235. this.$lightbox.find('.lb-image, .lb-nav, .lb-prev, .lb-next, .lb-dataContainer, .lb-numbers, .lb-caption').hide();
  236. this.$outerContainer.addClass('animating');
  237. // When image to show is preloaded, we send the width and height to sizeContainer()
  238. var preloader = new Image();
  239. preloader.onload = function() {
  240. var $preloader;
  241. var imageHeight;
  242. var imageWidth;
  243. var maxImageHeight;
  244. var maxImageWidth;
  245. var windowHeight;
  246. var windowWidth;
  247. $image.attr({
  248. 'alt': self.album[imageNumber].alt,
  249. 'src': filename
  250. });
  251. $preloader = $(preloader);
  252. $image.width(preloader.width);
  253. $image.height(preloader.height);
  254. windowWidth = $(window).width();
  255. windowHeight = $(window).height();
  256. // Calculate the max image dimensions for the current viewport.
  257. // Take into account the border around the image and an additional 10px gutter on each side.
  258. maxImageWidth = windowWidth - self.containerPadding.left - self.containerPadding.right - self.imageBorderWidth.left - self.imageBorderWidth.right - 20;
  259. maxImageHeight = windowHeight - self.containerPadding.top - self.containerPadding.bottom - self.imageBorderWidth.top - self.imageBorderWidth.bottom - self.options.positionFromTop - 70;
  260. /*
  261. SVGs that don't have width and height attributes specified are reporting width and height
  262. values of 0 in Firefox 47 and IE11 on Windows. To fix, we set the width and height to the max
  263. dimensions for the viewport rather than 0 x 0.
  264. https://github.com/lokesh/lightbox2/issues/552
  265. */
  266. if (filetype === 'svg') {
  267. if ((preloader.width === 0) || preloader.height === 0) {
  268. $image.width(maxImageWidth);
  269. $image.height(maxImageHeight);
  270. }
  271. }
  272. // Fit image inside the viewport.
  273. if (self.options.fitImagesInViewport) {
  274. // Check if image size is larger then maxWidth|maxHeight in settings
  275. if (self.options.maxWidth && self.options.maxWidth < maxImageWidth) {
  276. maxImageWidth = self.options.maxWidth;
  277. }
  278. if (self.options.maxHeight && self.options.maxHeight < maxImageWidth) {
  279. maxImageHeight = self.options.maxHeight;
  280. }
  281. // Is the current image's width or height is greater than the maxImageWidth or maxImageHeight
  282. // option than we need to size down while maintaining the aspect ratio.
  283. if ((preloader.width > maxImageWidth) || (preloader.height > maxImageHeight)) {
  284. if ((preloader.width / maxImageWidth) > (preloader.height / maxImageHeight)) {
  285. imageWidth = maxImageWidth;
  286. imageHeight = parseInt(preloader.height / (preloader.width / imageWidth), 10);
  287. $image.width(imageWidth);
  288. $image.height(imageHeight);
  289. } else {
  290. imageHeight = maxImageHeight;
  291. imageWidth = parseInt(preloader.width / (preloader.height / imageHeight), 10);
  292. $image.width(imageWidth);
  293. $image.height(imageHeight);
  294. }
  295. }
  296. }
  297. self.sizeContainer($image.width(), $image.height());
  298. };
  299. // Preload image before showing
  300. preloader.src = this.album[imageNumber].link;
  301. this.currentImageIndex = imageNumber;
  302. };
  303. // Stretch overlay to fit the viewport
  304. Lightbox.prototype.sizeOverlay = function() {
  305. var self = this;
  306. /*
  307. We use a setTimeout 0 to pause JS execution and let the rendering catch-up.
  308. Why do this? If the `disableScrolling` option is set to true, a class is added to the body
  309. tag that disables scrolling and hides the scrollbar. We want to make sure the scrollbar is
  310. hidden before we measure the document width, as the presence of the scrollbar will affect the
  311. number.
  312. */
  313. setTimeout(function() {
  314. self.$overlay
  315. .width($(document).width())
  316. .height($(document).height());
  317. }, 0);
  318. };
  319. // Animate the size of the lightbox to fit the image we are showing
  320. // This method also shows the the image.
  321. Lightbox.prototype.sizeContainer = function(imageWidth, imageHeight) {
  322. var self = this;
  323. var oldWidth = this.$outerContainer.outerWidth();
  324. var oldHeight = this.$outerContainer.outerHeight();
  325. var newWidth = imageWidth + this.containerPadding.left + this.containerPadding.right + this.imageBorderWidth.left + this.imageBorderWidth.right;
  326. var newHeight = imageHeight + this.containerPadding.top + this.containerPadding.bottom + this.imageBorderWidth.top + this.imageBorderWidth.bottom;
  327. function postResize() {
  328. self.$lightbox.find('.lb-dataContainer').width(newWidth);
  329. self.$lightbox.find('.lb-prevLink').height(newHeight);
  330. self.$lightbox.find('.lb-nextLink').height(newHeight);
  331. self.showImage();
  332. }
  333. if (oldWidth !== newWidth || oldHeight !== newHeight) {
  334. this.$outerContainer.animate({
  335. width: newWidth,
  336. height: newHeight
  337. }, this.options.resizeDuration, 'swing', function() {
  338. postResize();
  339. });
  340. } else {
  341. postResize();
  342. }
  343. };
  344. // Display the image and its details and begin preload neighboring images.
  345. Lightbox.prototype.showImage = function() {
  346. this.$lightbox.find('.lb-loader').stop(true).hide();
  347. this.$lightbox.find('.lb-image').fadeIn(this.options.imageFadeDuration);
  348. this.updateNav();
  349. this.updateDetails();
  350. this.preloadNeighboringImages();
  351. this.enableKeyboardNav();
  352. };
  353. // Display previous and next navigation if appropriate.
  354. Lightbox.prototype.updateNav = function() {
  355. // Check to see if the browser supports touch events. If so, we take the conservative approach
  356. // and assume that mouse hover events are not supported and always show prev/next navigation
  357. // arrows in image sets.
  358. var alwaysShowNav = false;
  359. try {
  360. document.createEvent('TouchEvent');
  361. alwaysShowNav = (this.options.alwaysShowNavOnTouchDevices) ? true : false;
  362. } catch (e) {}
  363. this.$lightbox.find('.lb-nav').show();
  364. if (this.album.length > 1) {
  365. if (this.options.wrapAround) {
  366. if (alwaysShowNav) {
  367. this.$lightbox.find('.lb-prev, .lb-next').css('opacity', '1');
  368. }
  369. this.$lightbox.find('.lb-prev, .lb-next').show();
  370. } else {
  371. if (this.currentImageIndex > 0) {
  372. this.$lightbox.find('.lb-prev').show();
  373. if (alwaysShowNav) {
  374. this.$lightbox.find('.lb-prev').css('opacity', '1');
  375. }
  376. }
  377. if (this.currentImageIndex < this.album.length - 1) {
  378. this.$lightbox.find('.lb-next').show();
  379. if (alwaysShowNav) {
  380. this.$lightbox.find('.lb-next').css('opacity', '1');
  381. }
  382. }
  383. }
  384. }
  385. };
  386. // Display caption, image number, and closing button.
  387. Lightbox.prototype.updateDetails = function() {
  388. var self = this;
  389. // Enable anchor clicks in the injected caption html.
  390. // Thanks Nate Wright for the fix. @https://github.com/NateWr
  391. if (typeof this.album[this.currentImageIndex].title !== 'undefined' &&
  392. this.album[this.currentImageIndex].title !== '') {
  393. var $caption = this.$lightbox.find('.lb-caption');
  394. if (this.options.sanitizeTitle) {
  395. $caption.text(this.album[this.currentImageIndex].title);
  396. } else {
  397. $caption.html(this.album[this.currentImageIndex].title);
  398. }
  399. $caption.fadeIn('fast');
  400. }
  401. if (this.album.length > 1 && this.options.showImageNumberLabel) {
  402. var labelText = this.imageCountLabel(this.currentImageIndex + 1, this.album.length);
  403. this.$lightbox.find('.lb-number').text(labelText).fadeIn('fast');
  404. } else {
  405. this.$lightbox.find('.lb-number').hide();
  406. }
  407. this.$outerContainer.removeClass('animating');
  408. this.$lightbox.find('.lb-dataContainer').fadeIn(this.options.resizeDuration, function() {
  409. return self.sizeOverlay();
  410. });
  411. };
  412. // Preload previous and next images in set.
  413. Lightbox.prototype.preloadNeighboringImages = function() {
  414. if (this.album.length > this.currentImageIndex + 1) {
  415. var preloadNext = new Image();
  416. preloadNext.src = this.album[this.currentImageIndex + 1].link;
  417. }
  418. if (this.currentImageIndex > 0) {
  419. var preloadPrev = new Image();
  420. preloadPrev.src = this.album[this.currentImageIndex - 1].link;
  421. }
  422. };
  423. Lightbox.prototype.enableKeyboardNav = function() {
  424. $(document).on('keyup.keyboard', $.proxy(this.keyboardAction, this));
  425. };
  426. Lightbox.prototype.disableKeyboardNav = function() {
  427. $(document).off('.keyboard');
  428. };
  429. Lightbox.prototype.keyboardAction = function(event) {
  430. var KEYCODE_ESC = 27;
  431. var KEYCODE_LEFTARROW = 37;
  432. var KEYCODE_RIGHTARROW = 39;
  433. var keycode = event.keyCode;
  434. if (keycode === KEYCODE_ESC) {
  435. this.end();
  436. } else if (keycode === KEYCODE_LEFTARROW) {
  437. if (this.currentImageIndex !== 0) {
  438. this.changeImage(this.currentImageIndex - 1);
  439. } else if (this.options.wrapAround && this.album.length > 1) {
  440. this.changeImage(this.album.length - 1);
  441. }
  442. } else if (keycode === KEYCODE_RIGHTARROW) {
  443. if (this.currentImageIndex !== this.album.length - 1) {
  444. this.changeImage(this.currentImageIndex + 1);
  445. } else if (this.options.wrapAround && this.album.length > 1) {
  446. this.changeImage(0);
  447. }
  448. }
  449. };
  450. // Closing time. :-(
  451. Lightbox.prototype.end = function() {
  452. this.disableKeyboardNav();
  453. $(window).off('resize', this.sizeOverlay);
  454. this.$lightbox.fadeOut(this.options.fadeDuration);
  455. this.$overlay.fadeOut(this.options.fadeDuration);
  456. if (this.options.disableScrolling) {
  457. $('body').removeClass('lb-disable-scrolling');
  458. }
  459. };
  460. return new Lightbox();
  461. }));