baguetteBox.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. /*!
  2. * baguetteBox.js
  3. * @author feimosi
  4. * @version 0.7.0
  5. * @url https://github.com/feimosi/baguetteBox.js
  6. */
  7. var baguetteBox = (function() {
  8. // SVG shapes used in buttons
  9. var leftArrow = '<svg width="40" height="60" xmlns="http://www.w3.org/2000/svg" version="1.1">' +
  10. '<polyline points="30 10 10 30 30 50" stroke="rgba(255,255,255,0.5)" stroke-width="4"' +
  11. 'stroke-linecap="butt" fill="none" stroke-linejoin="round">&lt;</polyline>' +
  12. '</svg>',
  13. rightArrow = '<svg width="40" height="60" xmlns="http://www.w3.org/2000/svg" version="1.1">' +
  14. '<polyline points="10 10 30 30 10 50" stroke="rgba(255,255,255,0.5)" stroke-width="4"' +
  15. 'stroke-linecap="butt" fill="none" stroke-linejoin="round">&gt;</polyline>' +
  16. '</svg>',
  17. closeX = '<svg width="30" height="30" xmlns="http://www.w3.org/2000/svg" version="1.1">' +
  18. '<g stroke="rgb(160, 160, 160)" stroke-width="4">' +
  19. '<line x1="5" y1="5" x2="25" y2="25"/>' +
  20. '<line x1="5" y1="25" x2="25" y2="5"/>' +
  21. 'X</g></svg>';
  22. // Main ID names
  23. var overlayID = 'baguetteBox-overlay';
  24. var sliderID = 'baguetteBox-slider';
  25. // Global options and their defaults
  26. var options = {}, defaults = {
  27. captions: true,
  28. buttons: 'auto',
  29. async: false,
  30. preload: 2,
  31. animation: 'slideIn'
  32. };
  33. // DOM Elements references
  34. var overlay, slider, previousButton, nextButton, closeButton;
  35. // Current image index inside the slider and displayed gallery index
  36. var currentIndex = 0, currentGallery = -1;
  37. // Touch event start position (for slide gesture)
  38. var touchStartX;
  39. // If set to true ignore touch events because animation was already fired
  40. var touchFlag = false;
  41. // Array of all used galleries (DOM elements)
  42. var galleries = [];
  43. // 2D array of galleries and images inside them
  44. var imagesMap = [];
  45. // Array containing temporary images DOM elements
  46. var imagesArray = [];
  47. // forEach polyfill for IE8
  48. if(!Array.prototype.forEach) {
  49. Array.prototype.forEach = function(callback, thisArg) {
  50. var len = this.length;
  51. for(var i = 0; i < len; i++) {
  52. callback.call(thisArg, this[i], i, this);
  53. }
  54. };
  55. }
  56. // Script entry point
  57. function run(selector, userOptions) {
  58. buildOverlay();
  59. // For each gallery bind a click event to every image inside it
  60. galleries = document.querySelectorAll(selector);
  61. [].forEach.call(
  62. galleries,
  63. function (galleryElement, galleryIndex) {
  64. var galleryID = imagesMap.length;
  65. imagesMap.push(galleryElement.getElementsByTagName('a'));
  66. imagesMap[galleryID].options = userOptions;
  67. [].forEach.call(
  68. imagesMap[galleryID],
  69. function (imageElement, imageIndex) {
  70. bind(imageElement, 'click', function(event) {
  71. /*jshint -W030 */
  72. event.preventDefault ? event.preventDefault() : event.returnValue = false;
  73. prepareOverlay(galleryID);
  74. showOverlay(imageIndex);
  75. });
  76. }
  77. );
  78. }
  79. );
  80. defaults.transforms = testTransformsSupport();
  81. }
  82. function buildOverlay() {
  83. overlay = document.getElementById(overlayID);
  84. // Check if the overlay already exists
  85. if(overlay) {
  86. slider = document.getElementById(sliderID);
  87. previousButton = document.getElementById('previous-button');
  88. nextButton = document.getElementById('next-button');
  89. closeButton = document.getElementById('close-button');
  90. return;
  91. }
  92. // Create overlay element
  93. overlay = document.createElement('div');
  94. overlay.id = overlayID;
  95. document.getElementsByTagName('body')[0].appendChild(overlay);
  96. // Create gallery slider element
  97. slider = document.createElement('div');
  98. slider.id = sliderID;
  99. overlay.appendChild(slider);
  100. // Create all necessary buttons
  101. previousButton = document.createElement('button');
  102. previousButton.id = 'previous-button';
  103. previousButton.innerHTML = leftArrow;
  104. overlay.appendChild(previousButton);
  105. nextButton = document.createElement('button');
  106. nextButton.id = 'next-button';
  107. nextButton.innerHTML = rightArrow;
  108. overlay.appendChild(nextButton);
  109. closeButton = document.createElement('button');
  110. closeButton.id = 'close-button';
  111. closeButton.innerHTML = closeX;
  112. overlay.appendChild(closeButton);
  113. previousButton.className = nextButton.className = closeButton.className = 'baguetteBox-button';
  114. bindEvents();
  115. }
  116. function bindEvents() {
  117. // When clicked on the overlay (outside displayed image) close it
  118. bind(overlay, 'click', function(event) {
  119. if(event.target && event.target.nodeName !== "IMG")
  120. hideOverlay();
  121. });
  122. // Add event listeners for buttons
  123. bind(document.getElementById('previous-button'), 'click', function(event) {
  124. /*jshint -W030 */
  125. event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true;
  126. showPreviousImage();
  127. });
  128. bind(document.getElementById('next-button'), 'click', function(event) {
  129. /*jshint -W030 */
  130. event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true;
  131. showNextImage();
  132. });
  133. bind(document.getElementById('close-button'), 'click', function(event) {
  134. /*jshint -W030 */
  135. event.stopPropagation ? event.stopPropagation() : event.cancelBubble = true;
  136. hideOverlay();
  137. });
  138. // Add touch events
  139. bind(overlay, 'touchstart', function(event) {
  140. // Save x axis position
  141. touchStartX = event.changedTouches[0].pageX;
  142. });
  143. bind(overlay, 'touchmove', function(event) {
  144. if(touchFlag)
  145. return;
  146. /*jshint -W030 */
  147. event.preventDefault ? event.preventDefault() : event.returnValue = false;
  148. touch = event.touches[0] || event.changedTouches[0];
  149. if(touch.pageX - touchStartX > 40) {
  150. touchFlag = true;
  151. showPreviousImage();
  152. } else if (touch.pageX - touchStartX < -40) {
  153. touchFlag = true;
  154. showNextImage();
  155. }
  156. });
  157. bind(overlay, 'touchend', function(event) {
  158. touchFlag = false;
  159. });
  160. // Activate keyboard shortcuts
  161. bind(document, 'keydown', function(event) {
  162. switch(event.keyCode) {
  163. case 37: // Left arrow
  164. showPreviousImage();
  165. break;
  166. case 39: // Right arrow
  167. showNextImage();
  168. break;
  169. case 27: // Esc
  170. hideOverlay();
  171. break;
  172. }
  173. });
  174. }
  175. function prepareOverlay(galleryIndex) {
  176. // If the same gallery is being opened prevent from loading it once again
  177. if(currentGallery === galleryIndex)
  178. return;
  179. currentGallery = galleryIndex;
  180. // Update gallery specific options
  181. setOptions(imagesMap[galleryIndex].options);
  182. // Empty slider of previous contents (more effective than .innerHTML = "")
  183. while(slider.firstChild)
  184. slider.removeChild(slider.firstChild);
  185. imagesArray.length = 0;
  186. // Prepare and append images containers
  187. for(var i = 0; i < imagesMap[galleryIndex].length; i++) {
  188. imagesArray.push(returnImageContainer());
  189. slider.appendChild(imagesArray[i]);
  190. }
  191. }
  192. function setOptions(newOptions) {
  193. if(!newOptions)
  194. newOptions = {};
  195. for(var item in defaults) {
  196. options[item] = defaults[item];
  197. if(typeof newOptions[item] !== 'undefined')
  198. options[item] = newOptions[item];
  199. }
  200. /* Apply new options */
  201. // Change transition for proper animation
  202. slider.style.transition = slider.style.webkitTransition = options.animation === 'fadeIn' ? 'opacity .4s ease' : '';
  203. // Hide buttons if necessary
  204. if(options.buttons === 'auto' && ('ontouchstart' in window || imagesMap[currentGallery].length === 1))
  205. options.buttons = false;
  206. // Set buttons style to hide or display them
  207. previousButton.style.display = nextButton.style.display = options.buttons ? '' : 'none';
  208. }
  209. // Return DOM element for image container <div class="full-image">...</div>
  210. function returnImageContainer() {
  211. var fullImage = document.createElement('div');
  212. fullImage.className = 'full-image';
  213. return fullImage;
  214. }
  215. function showOverlay(index) {
  216. // Return if overlay is already visible
  217. if(overlay.style.display === 'block')
  218. return;
  219. // Set current index to a new value and show proper image
  220. currentIndex = index;
  221. loadImage(currentIndex, function() {
  222. preloadNext(currentIndex);
  223. preloadPrev(currentIndex);
  224. });
  225. updateOffset();
  226. overlay.style.display = 'block';
  227. // Fade in overlay
  228. setTimeout(function() {
  229. overlay.className = 'visible';
  230. }, 50);
  231. }
  232. function hideOverlay() {
  233. // Return if overlay is already hidden
  234. if(overlay.style.display === 'none')
  235. return;
  236. // Fade out and hide the overlay
  237. overlay.className = '';
  238. setTimeout(function() {
  239. overlay.style.display = 'none';
  240. }, 500);
  241. }
  242. function loadImage(index, callback) {
  243. var imageContainer = imagesArray[index];
  244. // If index is invalid return
  245. if(typeof imageContainer === 'undefined')
  246. return;
  247. // If image is already loaded run callback and return
  248. if(imageContainer.getElementsByTagName('img')[0]) {
  249. if(callback)
  250. callback();
  251. return;
  252. }
  253. // Get element reference, optional caption and source path
  254. imageElement = imagesMap[currentGallery][index];
  255. imageCaption = imageElement.getAttribute('data-caption') || imageElement.title;
  256. imageSrc = getImageSrc(imageElement);
  257. // Prepare image container elements
  258. var figure = document.createElement('figure');
  259. var image = document.createElement('img');
  260. var figcaption = document.createElement('figcaption');
  261. imageContainer.appendChild(figure);
  262. // Add loader element
  263. figure.innerHTML = '<div class="spinner">' +
  264. '<div class="double-bounce1"></div>' +
  265. '<div class="double-bounce2"></div>' +
  266. '</div>';
  267. // Set callback function when image loads
  268. image.onload = function() {
  269. // Remove loader element
  270. var spinner = this.parentNode.querySelector('.spinner');
  271. this.parentNode.removeChild(spinner);
  272. if(!options.async && callback)
  273. callback();
  274. };
  275. image.setAttribute('src', imageSrc);
  276. figure.appendChild(image);
  277. // Insert caption if available
  278. if(options.captions && imageCaption) {
  279. figcaption.innerHTML = imageCaption;
  280. figure.appendChild(figcaption);
  281. }
  282. // Run callback
  283. if(options.async && callback)
  284. callback();
  285. }
  286. function getImageSrc(image) {
  287. // Set dafult image path from href
  288. var result = imageElement.getAttribute('href');
  289. // If dataset is supported find the most suitable image
  290. if(image.dataset) {
  291. var srcs = [];
  292. // Get all possible image versions depending on the resolution
  293. for(var item in image.dataset) {
  294. if(item.substring(0, 3) === 'at-' && !isNaN(item.substring(3)))
  295. srcs[item.replace('at-', '')] = image.dataset[item];
  296. }
  297. // Sort resolutions ascending
  298. keys = Object.keys(srcs).sort(function(a, b) {
  299. return parseInt(a) < parseInt(b) ? -1 : 1;
  300. });
  301. // Get real screen resolution
  302. var width = window.innerWidth * window.devicePixelRatio;
  303. // Find first image bigger than or equal to the current width
  304. for(var i = 0; i < keys.length; i++) {
  305. if(keys[i] >= width) {
  306. result = srcs[keys[i]];
  307. break;
  308. }
  309. result = srcs[keys[i]];
  310. }
  311. }
  312. return result;
  313. }
  314. function showNextImage() {
  315. if(currentIndex <= imagesArray.length - 2) {
  316. currentIndex++;
  317. updateOffset();
  318. preloadNext(currentIndex);
  319. } else {
  320. slider.className = 'bounce-from-right';
  321. setTimeout(function() {
  322. slider.className = '';
  323. }, 400);
  324. }
  325. }
  326. function showPreviousImage() {
  327. if(currentIndex >= 1) {
  328. currentIndex--;
  329. updateOffset();
  330. preloadPrev(currentIndex);
  331. } else {
  332. slider.className = 'bounce-from-left';
  333. setTimeout(function() {
  334. slider.className = '';
  335. }, 400);
  336. }
  337. }
  338. function updateOffset() {
  339. var offset = -currentIndex * 100 + '%';
  340. if(options.animation === 'fadeIn') {
  341. slider.style.opacity = 0;
  342. setTimeout(function() {
  343. /*jshint -W030 */
  344. options.transforms ?
  345. slider.style.transform = slider.style.webkitTransform = 'translate3d(' + offset + ',0,0)'
  346. : slider.style.left = offset;
  347. slider.style.opacity = 1;
  348. }, 400);
  349. } else {
  350. /*jshint -W030 */
  351. options.transforms ?
  352. slider.style.transform = slider.style.webkitTransform = 'translate3d(' + offset + ',0,0)'
  353. : slider.style.left = offset;
  354. }
  355. }
  356. function testTransformsSupport() {
  357. var div = document.createElement('div'),
  358. support = false;
  359. support = typeof div.style.perspective !== 'undefined' || typeof div.style.webkitPerspective !== 'undefined';
  360. return support;
  361. }
  362. function preloadNext(index) {
  363. if(index - currentIndex >= options.preload)
  364. return;
  365. loadImage(index + 1, function() { preloadNext(index + 1); });
  366. }
  367. function preloadPrev(index) {
  368. if(currentIndex - index >= options.preload)
  369. return;
  370. loadImage(index - 1, function() { preloadPrev(index - 1); });
  371. }
  372. function bind(element, event, callback) {
  373. if(element.addEventListener)
  374. element.addEventListener(event, callback, false);
  375. else
  376. element.attachEvent('on' + event, callback);
  377. }
  378. return {
  379. run: run
  380. };
  381. })();