vendor: 47,602 bytes, lines 1-1562
1/* 2 * Backstretch 3 * http://srobbin.com/jquery-plugins/backstretch/ 4 * 5 * Copyright (c) 2013 Scott Robbin 6 * Licensed under the MIT license. 7 */ 8 9;(function ($, window, undefined) { 10 'use strict'; 11 12 /** @const */ 13 var YOUTUBE_REGEXP = /^.*(youtu\.be\/|youtube\.com\/v\/|youtube\.com\/embed\/|youtube\.com\/watch\?v=|youtube\.com\/watch\?.*\&v=)([^#\&\?]*).*/i; 14 15 /* PLUGIN DEFINITION 16 * ========================= */ 17 18 $.fn.backstretch = function (images, options) { 19 var args = arguments; 20 21 /* 22 * Scroll the page one pixel to get the right window height on iOS 23 * Pretty harmless for everyone else 24 */ 25 if ($(window).scrollTop() === 0 ) { 26 window.scrollTo(0, 0); 27 } 28 29 var returnValues; 30 31 this.each(function (eachIndex) { 32 var $this = $(this) 33 , obj = $this.data('backstretch'); 34 35 // Do we already have an instance attached to this element? 36 if (obj) { 37 38 // Is this a method they're trying to execute? 39 if (typeof args[0] === 'string' && 40 typeof obj[args[0]] === 'function') { 41 42 // Call the method 43 var returnValue = obj[args[0]].apply(obj, Array.prototype.slice.call(args, 1)); 44 if (returnValue === obj) { // If a method is chaining 45 returnValue = undefined; 46 } 47 if (returnValue !== undefined) { 48 returnValues = returnValues || []; 49 returnValues[eachIndex] = returnValue; 50 } 51 52 return; // Nothing further to do 53 } 54 55 // Merge the old options with the new 56 options = $.extend(obj.options, options); 57 58 // Remove the old instance 59 if ( obj.hasOwnProperty('destroy') ) { 60 obj.destroy(true); 61 } 62 } 63 64 // We need at least one image 65 if (!images || (images && images.length === 0)) { 66 var cssBackgroundImage = $this.css('background-image'); 67 if (cssBackgroundImage && cssBackgroundImage !== 'none') { 68 images = [ { url: $this.css('backgroundImage').replace(/url\(|\)|"|'/g,"") } ]; 69 } else { 70 $.error('No images were supplied for Backstretch, or element must have a CSS-defined background image.'); 71 } 72 } 73 74 obj = new Backstretch(this, images, options || {}); 75 $this.data('backstretch', obj); 76 }); 77 78 return returnValues ? returnValues.length === 1 ? returnValues[0] : returnValues : this; 79 }; 80 81 // If no element is supplied, we'll attach to body 82 $.backstretch = function (images, options) { 83 // Return the instance 84 return $('body') 85 .backstretch(images, options) 86 .data('backstretch'); 87 }; 88 89 // Custom selector 90 $.expr[':'].backstretch = function(elem) { 91 return $(elem).data('backstretch') !== undefined; 92 }; 93 94 /* DEFAULTS 95 * ========================= */ 96 97 $.fn.backstretch.defaults = { 98 duration: 5000 // Amount of time in between slides (if slideshow) 99 , transition: 'fade' // Type of transition between slides 100 , transitionDuration: 0 // Duration of transition between slides 101 , animateFirst: true // Animate the transition of first image of slideshow in? 102 , alignX: 0.5 // The x-alignment for the image, can be 'left'|'center'|'right' or any number between 0.0 and 1.0 103 , alignY: 0.5 // The y-alignment for the image, can be 'top'|'center'|'bottom' or any number between 0.0 and 1.0 104 , paused: false // Whether the images should slide after given duration 105 , start: 0 // Index of the first image to show 106 , preload: 2 // How many images preload at a time? 107 , preloadSize: 1 // How many images can we preload in parallel? 108 , resolutionRefreshRate: 2500 // How long to wait before switching resolution? 109 , resolutionChangeRatioThreshold: 0.1 // How much a change should it be before switching resolution? 110 }; 111 112 /* STYLES 113 * 114 * Baked-in styles that we'll apply to our elements. 115 * In an effort to keep the plugin simple, these are not exposed as options. 116 * That said, anyone can override these in their own stylesheet. 117 * ========================= */ 118 var styles = { 119 wrap: { 120 left: 0 121 , top: 0 122 , overflow: 'hidden' 123 , margin: 0 124 , padding: 0 125 , height: '100%' 126 , width: '100%' 127 , zIndex: -999999 128 } 129 , itemWrapper: { 130 position: 'absolute' 131 , display: 'none' 132 , margin: 0 133 , padding: 0 134 , border: 'none' 135 , width: '100%' 136 , height: '100%' 137 , zIndex: -999999 138 } 139 , item: { 140 position: 'absolute' 141 , margin: 0 142 , padding: 0 143 , border: 'none' 144 , width: '100%' 145 , height: '100%' 146 , maxWidth: 'none' 147 } 148 }; 149 150 /* Given an array of different options for an image, 151 * choose the optimal image for the container size. 152 * 153 * Given an image template (a string with {{ width }} and/or 154 * {{height}} inside) and a container object, returns the 155 * image url with the exact values for the size of that 156 * container. 157 * 158 * Returns an array of urls optimized for the specified resolution. 159 * 160 */ 161 var optimalSizeImages = (function () { 162 163 /* Sorts the array of image sizes based on width */ 164 var widthInsertSort = function (arr) { 165 for (var i = 1; i < arr.length; i++) { 166 var tmp = arr[i], 167 j = i; 168 while (arr[j - 1] && parseInt(arr[j - 1].width, 10) > parseInt(tmp.width, 10)) { 169 arr[j] = arr[j - 1]; 170 --j; 171 } 172 arr[j] = tmp; 173 } 174 175 return arr; 176 }; 177 178 /* Given an array of various sizes of the same image and a container width, 179 * return the best image. 180 */ 181 var selectBest = function (containerWidth, containerHeight, imageSizes) { 182 183 var devicePixelRatio = window.devicePixelRatio || 1; 184 var deviceOrientation = getDeviceOrientation(); 185 var windowOrientation = getWindowOrientation(); 186 var wrapperOrientation = (containerHeight > containerWidth) ? 187 'portrait' : 188 (containerWidth > containerHeight ? 'landscape' : 'square'); 189 190 var lastAllowedImage = 0; 191 var testWidth; 192 193 for (var j = 0, image; j < imageSizes.length; j++) { 194 195 image = imageSizes[j]; 196 197 // In case a new image was pushed in, process it: 198 if (typeof image === 'string') { 199 image = imageSizes[j] = { url: image }; 200 } 201 202 if (image.pixelRatio && image.pixelRatio !== 'auto' && parseFloat(image.pixelRatio) !== devicePixelRatio) { 203 // We disallowed choosing this image for current device pixel ratio, 204 // So skip this one. 205 continue; 206 } 207 208 if (image.deviceOrientation && image.deviceOrientation !== deviceOrientation) { 209 // We disallowed choosing this image for current device orientation, 210 // So skip this one. 211 continue; 212 } 213 214 if (image.windowOrientation && image.windowOrientation !== deviceOrientation) { 215 // We disallowed choosing this image for current window orientation, 216 // So skip this one. 217 continue; 218 } 219 220 if (image.orientation && image.orientation !== wrapperOrientation) { 221 // We disallowed choosing this image for current element's orientation, 222 // So skip this one. 223 continue; 224 } 225 226 // Mark this one as the last one we investigated 227 // which does not violate device pixel ratio rules. 228 // We may choose this one later if there's no match. 229 lastAllowedImage = j; 230 231 // For most images, we match the specified width against element width, 232 // And enforcing a limit depending on the "pixelRatio" property if specified. 233 // But if a pixelRatio="auto", then we consider the width as the physical width of the image, 234 // And match it while considering the device's pixel ratio. 235 testWidth = containerWidth; 236 if (image.pixelRatio === 'auto') { 237 containerWidth *= devicePixelRatio; 238 } 239 240 // Stop when the width of the image is larger or equal to the container width 241 if (image.width >= testWidth) { 242 break; 243 } 244 } 245 246 // Use the image located at where we stopped 247 return imageSizes[Math.min(j, lastAllowedImage)]; 248 }; 249 250 var replaceTagsInUrl = function (url, templateReplacer) { 251 252 if (typeof url === 'string') { 253 url = url.replace(/{{(width|height)}}/g, templateReplacer); 254 } else if (url instanceof Array) { 255 for (var i = 0; i < url.length; i++) { 256 if (url[i].src) { 257 url[i].src = replaceTagsInUrl(url[i].src, templateReplacer); 258 } else { 259 url[i] = replaceTagsInUrl(url[i], templateReplacer); 260 } 261 } 262 } 263 264 return url; 265 }; 266 267 return function ($container, images) { 268 var containerWidth = $container.width(), 269 containerHeight = $container.height(); 270 271 var chosenImages = []; 272 273 var templateReplacer = function (match, key) { 274 if (key === 'width') { 275 return containerWidth; 276 } 277 if (key === 'height') { 278 return containerHeight; 279 } 280 return match; 281 }; 282 283 for (var i = 0; i < images.length; i++) { 284 if ($.isArray(images[i])) { 285 images[i] = widthInsertSort(images[i]); 286 var chosen = selectBest(containerWidth, containerHeight, images[i]); 287 chosenImages.push(chosen); 288 } else { 289 // In case a new image was pushed in, process it: 290 if (typeof images[i] === 'string') { 291 images[i] = { url: images[i] }; 292 } 293 294 var item = $.extend({}, images[i]); 295 item.url = replaceTagsInUrl(item.url, templateReplacer); 296 chosenImages.push(item); 297 } 298 } 299 return chosenImages; 300 }; 301 302 })(); 303 304 var isVideoSource = function (source) { 305 return YOUTUBE_REGEXP.test(source.url) || source.isVideo; 306 }; 307 308 /* Preload images */ 309 var preload = (function (sources, startAt, count, batchSize, callback) { 310 // Plugin cache 311 var cache = []; 312 313 // Wrapper for cache 314 var caching = function(image){ 315 for (var i = 0; i < cache.length; i++) { 316 if (cache[i].src === image.src) { 317 return cache[i]; 318 } 319 } 320 cache.push(image); 321 return image; 322 }; 323 324 // Execute callback 325 var exec = function(sources, callback, last){ 326 if (typeof callback === 'function') { 327 callback.call(sources, last); 328 } 329 }; 330 331 // Closure to hide cache 332 return function preload (sources, startAt, count, batchSize, callback){ 333 // Check input data 334 if (typeof sources === 'undefined') { 335 return; 336 } 337 if (!$.isArray(sources)) { 338 sources = [sources]; 339 } 340 341 if (arguments.length < 5 && typeof arguments[arguments.length - 1] === 'function') { 342 callback = arguments[arguments.length - 1]; 343 } 344 345 startAt = (typeof startAt === 'function' || !startAt) ? 0 : startAt; 346 count = (typeof count === 'function' || !count || count < 0) ? sources.length : Math.min(count, sources.length); 347 batchSize = (typeof batchSize === 'function' || !batchSize) ? 1 : batchSize; 348 349 if (startAt >= sources.length) { 350 startAt = 0; 351 count = 0; 352 } 353 if (batchSize < 0) { 354 batchSize = count; 355 } 356 batchSize = Math.min(batchSize, count); 357 358 var next = sources.slice(startAt + batchSize, count - batchSize); 359 sources = sources.slice(startAt, batchSize); 360 count = sources.length; 361 362 // If sources array is empty 363 if (!count) { 364 exec(sources, callback, true); 365 return; 366 } 367 368 // Image loading callback 369 var countLoaded = 0; 370 371 var loaded = function() { 372 countLoaded++; 373 if (countLoaded !== count) { 374 return; 375 } 376 377 exec(sources, callback, !next); 378 preload(next, 0, 0, batchSize, callback); 379 }; 380 381 // Loop sources to preload 382 var image; 383 384 for (var i = 0; i < sources.length; i++) { 385 386 if (isVideoSource(sources[i])) { 387 388 // Do not preload videos. There are issues with that. 389 // First - we need to keep an instance of the preloaded and use that exactly, not a copy. 390 // Second - there are memory issues. 391 // If there will be a requirement from users - I'll try to implement this. 392 393 continue; 394 395 } else { 396 397 image = new Image(); 398 image.src = sources[i].url; 399 400 image = caching(image); 401 402 if (image.complete) { 403 loaded(); 404 } else { 405 $(image).on('load error', loaded); 406 } 407 408 } 409 410 } 411 }; 412 })(); 413 414 /* Process images array */ 415 var processImagesArray = function (images) { 416 var processed = []; 417 for (var i = 0; i < images.length; i++) { 418 if (typeof images[i] === 'string') { 419 processed.push({ url: images[i] }); 420 } 421 else if ($.isArray(images[i])) { 422 processed.push(processImagesArray(images[i])); 423 } 424 else { 425 processed.push(processOptions(images[i])); 426 } 427 } 428 return processed; 429 }; 430 431 /* Process options */ 432 var processOptions = function (options, required) { 433 434 // Convert old options 435 436 // centeredX/centeredY are deprecated 437 if (options.centeredX || options.centeredY) { 438 if (window.console && window.console.log) { 439 window.console.log('jquery.backstretch: `centeredX`/`centeredY` is deprecated, please use `alignX`/`alignY`'); 440 } 441 if (options.centeredX) { 442 options.alignX = 0.5; 443 } 444 if (options.centeredY) { 445 options.alignY = 0.5; 446 } 447 } 448 449 // Deprecated spec 450 if (options.speed !== undefined) { 451 452 if (window.console && window.console.log) { 453 window.console.log('jquery.backstretch: `speed` is deprecated, please use `transitionDuration`'); 454 } 455 456 options.transitionDuration = options.speed; 457 options.transition = 'fade'; 458 } 459 460 // Typo 461 if (options.resolutionChangeRatioTreshold !== undefined) { 462 window.console.log('jquery.backstretch: `treshold` is a typo!'); 463 options.resolutionChangeRatioThreshold = options.resolutionChangeRatioTreshold; 464 } 465 466 // Current spec that needs processing 467 468 if (options.fadeFirst !== undefined) { 469 options.animateFirst = options.fadeFirst; 470 } 471 472 if (options.fade !== undefined) { 473 options.transitionDuration = options.fade; 474 options.transition = 'fade'; 475 } 476 477 if (options.scale) { 478 options.scale = validScale(options.scale); 479 } 480 481 return processAlignOptions(options); 482 }; 483 484 /* Process align options */ 485 var processAlignOptions = function (options, required) { 486 if (options.alignX === 'left') { 487 options.alignX = 0.0; 488 } 489 else if (options.alignX === 'center') { 490 options.alignX = 0.5; 491 } 492 else if (options.alignX === 'right') { 493 options.alignX = 1.0; 494 } 495 else { 496 if (options.alignX !== undefined || required) { 497 options.alignX = parseFloat(options.alignX); 498 if (isNaN(options.alignX)) { 499 options.alignX = 0.5; 500 } 501 } 502 } 503 504 if (options.alignY === 'top') { 505 options.alignY = 0.0; 506 } 507 else if (options.alignY === 'center') { 508 options.alignY = 0.5; 509 } 510 else if (options.alignY === 'bottom') { 511 options.alignY = 1.0; 512 } 513 else { 514 if (options.alignX !== undefined || required) { 515 options.alignY = parseFloat(options.alignY); 516 if (isNaN(options.alignY)) { 517 options.alignY = 0.5; 518 } 519 } 520 } 521 522 return options; 523 }; 524 525 var SUPPORTED_SCALE_OPTIONS = { 526 'cover': 'cover', 527 'fit': 'fit', 528 'fit-smaller': 'fit-smaller', 529 'fill': 'fill' 530 }; 531 532 function validScale(scale) { 533 if (!SUPPORTED_SCALE_OPTIONS.hasOwnProperty(scale)) { 534 return 'cover'; 535 } 536 return scale; 537 } 538 539 /* CLASS DEFINITION 540 * ========================= */ 541 var Backstretch = function (container, images, options) { 542 this.options = $.extend({}, $.fn.backstretch.defaults, options || {}); 543 544 this.firstShow = true; 545 546 // Process options 547 processOptions(this.options, true); 548 549 /* In its simplest form, we allow Backstretch to be called on an image path. 550 * e.g. $.backstretch('/path/to/image.jpg') 551 * So, we need to turn this back into an array. 552 */ 553 this.images = processImagesArray($.isArray(images) ? images : [images]); 554 555 /** 556 * Paused-Option 557 */ 558 if (this.options.paused) { 559 this.paused = true; 560 } 561 562 /** 563 * Start-Option (Index) 564 */ 565 if (this.options.start >= this.images.length) 566 { 567 this.options.start = this.images.length - 1; 568 } 569 if (this.options.start < 0) 570 { 571 this.options.start = 0; 572 } 573 574 // Convenience reference to know if the container is body. 575 this.isBody = container === document.body; 576 577 /* We're keeping track of a few different elements 578 * 579 * Container: the element that Backstretch was called on. 580 * Wrap: a DIV that we place the image into, so we can hide the overflow. 581 * Root: Convenience reference to help calculate the correct height. 582 */ 583 var $window = $(window); 584 this.$container = $(container); 585 this.$root = this.isBody ? supportsFixedPosition ? $window : $(document) : this.$container; 586 587 this.originalImages = this.images; 588 this.images = optimalSizeImages( 589 this.options.alwaysTestWindowResolution ? $window : this.$root, 590 this.originalImages); 591 592 /** 593 * Pre-Loading. 594 * This is the first image, so we will preload a minimum of 1 images. 595 */ 596 preload(this.images, this.options.start || 0, this.options.preload || 1); 597 598 // Don't create a new wrap if one already exists (from a previous instance of Backstretch) 599 var $existing = this.$container.children(".backstretch").first(); 600 this.$wrap = $existing.length ? $existing : 601 $('<div class="backstretch"></div>') 602 .css(this.options.bypassCss ? {} : styles.wrap) 603 .appendTo(this.$container); 604 605 if (!this.options.bypassCss) { 606 607 // Non-body elements need some style adjustments 608 if (!this.isBody) { 609 // If the container is statically positioned, we need to make it relative, 610 // and if no zIndex is defined, we should set it to zero. 611 var position = this.$container.css('position') 612 , zIndex = this.$container.css('zIndex'); 613 614 this.$container.css({ 615 position: position === 'static' ? 'relative' : position 616 , zIndex: zIndex === 'auto' ? 0 : zIndex 617 }); 618 619 // Needs a higher z-index 620 this.$wrap.css({zIndex: -999998}); 621 } 622 623 // Fixed or absolute positioning? 624 this.$wrap.css({ 625 position: this.isBody && supportsFixedPosition ? 'fixed' : 'absolute' 626 }); 627 628 } 629 630 // Set the first image 631 this.index = this.options.start; 632 this.show(this.index); 633 634 // Listen for resize 635 $window.on('resize.backstretch', $.proxy(this.resize, this)) 636 .on('orientationchange.backstretch', $.proxy(function () { 637 // Need to do this in order to get the right window height 638 if (this.isBody && window.pageYOffset === 0) { 639 window.scrollTo(0, 1); 640 this.resize(); 641 } 642 }, this)); 643 }; 644 645 var performTransition = function (options) { 646 647 var transition = options.transition || 'fade'; 648 649 // Look for multiple options 650 if (typeof transition === 'string' && transition.indexOf('|') > -1) { 651 transition = transition.split('|'); 652 } 653 654 if (transition instanceof Array) { 655 transition = transition[Math.round(Math.random() * (transition.length - 1))]; 656 } 657 658 var $new = options['new']; 659 var $old = options['old'] ? options['old'] : $([]); 660 661 switch (transition.toString().toLowerCase()) { 662 663 default: 664 case 'fade': 665 $new.fadeIn({ 666 duration: options.duration, 667 complete: options.complete, 668 easing: options.easing || undefined 669 }); 670 break; 671 672 case 'fadeinout': 673 case 'fade_in_out': 674 675 var fadeInNew = function () { 676 $new.fadeIn({ 677 duration: options.duration / 2, 678 complete: options.complete, 679 easing: options.easing || undefined 680 }); 681 }; 682 683 if ($old.length) { 684 $old.fadeOut({ 685 duration: options.duration / 2, 686 complete: fadeInNew, 687 easing: options.easing || undefined 688 }); 689 } else { 690 fadeInNew(); 691 } 692 693 break; 694 695 case 'pushleft': 696 case 'push_left': 697 case 'pushright': 698 case 'push_right': 699 case 'pushup': 700 case 'push_up': 701 case 'pushdown': 702 case 'push_down': 703 case 'coverleft': 704 case 'cover_left': 705 case 'coverright': 706 case 'cover_right': 707 case 'coverup': 708 case 'cover_up': 709 case 'coverdown': 710 case 'cover_down': 711 712 var transitionParts = transition.match(/^(cover|push)_?(.*)$/); 713 714 var animProp = transitionParts[2] === 'left' ? 'right' : 715 transitionParts[2] === 'right' ? 'left' : 716 transitionParts[2] === 'down' ? 'top' : 717 transitionParts[2] === 'up' ? 'bottom' : 718 'right'; 719 720 var newCssStart = { 721 'display': '' 722 }, newCssAnim = {}; 723 newCssStart[animProp] = '-100%'; 724 newCssAnim[animProp] = 0; 725 726 $new 727 .css(newCssStart) 728 .animate(newCssAnim, { 729 duration: options.duration, 730 complete: function () { 731 $new.css(animProp, ''); 732 options.complete.apply(this, arguments); 733 }, 734 easing: options.easing || undefined 735 }); 736 737 if (transitionParts[1] === 'push' && $old.length) { 738 var oldCssAnim = {}; 739 oldCssAnim[animProp] = '100%'; 740 741 $old 742 .animate(oldCssAnim, { 743 duration: options.duration, 744 complete: function () { 745 $old.css('display', 'none'); 746 }, 747 easing: options.easing || undefined 748 }); 749 } 750 751 break; 752 } 753 754 }; 755 756 /* PUBLIC METHODS 757 * ========================= */ 758 Backstretch.prototype = { 759 760 resize: function () { 761 try { 762 763 // Check for a better suited image after the resize 764 var $resTest = this.options.alwaysTestWindowResolution ? $(window) : this.$root; 765 var newContainerWidth = $resTest.width(); 766 var newContainerHeight = $resTest.height(); 767 var changeRatioW = newContainerWidth / (this._lastResizeContainerWidth || 0); 768 var changeRatioH = newContainerHeight / (this._lastResizeContainerHeight || 0); 769 var resolutionChangeRatioThreshold = this.options.resolutionChangeRatioThreshold || 0.0; 770 771 // check for big changes in container size 772 if ((newContainerWidth !== this._lastResizeContainerWidth || 773 newContainerHeight !== this._lastResizeContainerHeight) && 774 ((Math.abs(changeRatioW - 1) >= resolutionChangeRatioThreshold || isNaN(changeRatioW)) || 775 (Math.abs(changeRatioH - 1) >= resolutionChangeRatioThreshold || isNaN(changeRatioH)))) { 776 777 this._lastResizeContainerWidth = newContainerWidth; 778 this._lastResizeContainerHeight = newContainerHeight; 779 780 // Big change: rebuild the entire images array 781 this.images = optimalSizeImages($resTest, this.originalImages); 782 783 // Preload them (they will be automatically inserted on the next cycle) 784 if (this.options.preload) { 785 preload(this.images, (this.index + 1) % this.images.length, this.options.preload); 786 } 787 788 // In case there is no cycle and the new source is different than the current 789 if (this.images.length === 1 && 790 this._currentImage.url !== this.images[0].url) { 791 792 // Wait a little an update the image being showed 793 var that = this; 794 clearTimeout(that._selectAnotherResolutionTimeout); 795 that._selectAnotherResolutionTimeout = setTimeout(function () { 796 that.show(0); 797 }, this.options.resolutionRefreshRate); 798 } 799 } 800 801 var bgCSS = {left: 0, top: 0, right: 'auto', bottom: 'auto'} 802 803 , boxWidth = this.isBody ? this.$root.width() : this.$root.innerWidth() 804 , boxHeight = this.isBody ? ( window.innerHeight ? window.innerHeight : this.$root.height() ) : this.$root.innerHeight() 805 806 , naturalWidth = this.$itemWrapper.data('width') 807 , naturalHeight = this.$itemWrapper.data('height') 808 809 , ratio = (naturalWidth / naturalHeight) || 1 810 811 , alignX = this._currentImage.alignX === undefined ? this.options.alignX : this._currentImage.alignX 812 , alignY = this._currentImage.alignY === undefined ? this.options.alignY : this._currentImage.alignY 813 , scale = validScale(this._currentImage.scale || this.options.scale); 814 815 var width, height; 816 817 if (scale === 'fit' || scale === 'fit-smaller') { 818 width = naturalWidth; 819 height = naturalHeight; 820 821 if (width > boxWidth || 822 height > boxHeight || 823 scale === 'fit-smaller') { 824 var boxRatio = boxWidth / boxHeight; 825 if (boxRatio > ratio) { 826 width = Math.floor(boxHeight * ratio); 827 height = boxHeight; 828 } else if (boxRatio < ratio) { 829 width = boxWidth; 830 height = Math.floor(boxWidth / ratio); 831 } else { 832 width = boxWidth; 833 height = boxHeight; 834 } 835 } 836 } else if (scale === 'fill') { 837 width = boxWidth; 838 height = boxHeight; 839 } else { // 'cover' 840 width = Math.max(boxHeight * ratio, boxWidth); 841 height = Math.max(width / ratio, boxHeight); 842 } 843 844 // Make adjustments based on image ratio 845 bgCSS.top = -(height - boxHeight) * alignY; 846 bgCSS.left = -(width - boxWidth) * alignX; 847 bgCSS.width = width; 848 bgCSS.height = height; 849 850 if (!this.options.bypassCss) { 851 852 this.$wrap 853 .css({width: boxWidth, height: boxHeight}) 854 .find('>.backstretch-item').not('.deleteable') 855 .each(function () { 856 var $wrapper = $(this); 857 $wrapper.find('img,video,iframe') 858 .css(bgCSS); 859 }); 860 } 861 862 var evt = $.Event('backstretch.resize', { 863 relatedTarget: this.$container[0] 864 }); 865 this.$container.trigger(evt, this); 866 867 } catch(err) { 868 // IE7 seems to trigger resize before the image is loaded. 869 // This try/catch block is a hack to let it fail gracefully. 870 } 871 872 return this; 873 } 874 875 // Show the slide at a certain position 876 , show: function (newIndex, overrideOptions) { 877 878 // Validate index 879 if (Math.abs(newIndex) > this.images.length - 1) { 880 return; 881 } 882 883 // Vars 884 var that = this 885 , $oldItemWrapper = that.$wrap.find('>.backstretch-item').addClass('deleteable') 886 , oldVideoWrapper = that.videoWrapper 887 , evtOptions = { relatedTarget: that.$container[0] }; 888 889 // Trigger the "before" event 890 that.$container.trigger($.Event('backstretch.before', evtOptions), [that, newIndex]); 891 892 // Set the new frame index 893 this.index = newIndex; 894 var selectedImage = that.images[newIndex]; 895 896 // Pause the slideshow 897 clearTimeout(that._cycleTimeout); 898 899 // New image 900 901 delete that.videoWrapper; // Current item may not be a video 902 903 var isVideo = isVideoSource(selectedImage); 904 if (isVideo) { 905 that.videoWrapper = new VideoWrapper(selectedImage); 906 that.$item = that.videoWrapper.$video.css('pointer-events', 'none'); 907 } else { 908 that.$item = $('<img />'); 909 } 910 911 that.$itemWrapper = $('<div class="backstretch-item">') 912 .append(that.$item); 913 914 if (this.options.bypassCss) { 915 that.$itemWrapper.css({ 916 'display': 'none' 917 }); 918 } else { 919 that.$itemWrapper.css(styles.itemWrapper); 920 that.$item.css(styles.item); 921 } 922 923 that.$item.bind(isVideo ? 'canplay' : 'load', function (e) { 924 var $this = $(this) 925 , $wrapper = $this.parent() 926 , options = $wrapper.data('options'); 927 928 if (overrideOptions) { 929 options = $.extend({}, options, overrideOptions); 930 } 931 932 var imgWidth = this.naturalWidth || this.videoWidth || this.width 933 , imgHeight = this.naturalHeight || this.videoHeight || this.height; 934 935 // Save the natural dimensions 936 $wrapper 937 .data('width', imgWidth) 938 .data('height', imgHeight); 939 940 var getOption = function (opt) { 941 return options[opt] !== undefined ? 942 options[opt] : 943 that.options[opt]; 944 }; 945 946 var transition = getOption('transition'); 947 var transitionEasing = getOption('transitionEasing'); 948 var transitionDuration = getOption('transitionDuration'); 949 950 // Show the image, then delete the old one 951 var bringInNextImage = function () { 952 953 if (oldVideoWrapper) { 954 oldVideoWrapper.stop(); 955 oldVideoWrapper.destroy(); 956 } 957 958 $oldItemWrapper.remove(); 959 960 // Resume the slideshow 961 if (!that.paused && that.images.length > 1) { 962 that.cycle(); 963 } 964 965 // Now we can clear the background on the element, to spare memory 966 if (!that.options.bypassCss && !that.isBody) { 967 that.$container.css('background-image', 'none'); 968 } 969 970 // Trigger the "after" and "show" events 971 // "show" is being deprecated 972 $(['after', 'show']).each(function () { 973 that.$container.trigger($.Event('backstretch.' + this, evtOptions), [that, newIndex]); 974 }); 975 976 if (isVideo) { 977 that.videoWrapper.play(); 978 } 979 }; 980 981 if ((that.firstShow && !that.options.animateFirst) || !transitionDuration || !transition) { 982 // Avoid transition on first show or if there's no transitionDuration value 983 $wrapper.show(); 984 bringInNextImage(); 985 } else { 986 987 performTransition({ 988 'new': $wrapper, 989 old: $oldItemWrapper, 990 transition: transition, 991 duration: transitionDuration, 992 easing: transitionEasing, 993 complete: bringInNextImage 994 }); 995 996 } 997 998 that.firstShow = false; 999 1000 // Resize 1001 that.resize(); 1002 }); 1003 1004 that.$itemWrapper.appendTo(that.$wrap); 1005 1006 that.$item.attr('alt', selectedImage.alt || ''); 1007 that.$itemWrapper.data('options', selectedImage); 1008 1009 if (!isVideo) { 1010 that.$item.attr('src', selectedImage.url); 1011 } 1012 1013 that._currentImage = selectedImage; 1014 1015 return that; 1016 } 1017 1018 , current: function () { 1019 return this.index; 1020 } 1021 1022 , next: function () { 1023 var args = Array.prototype.slice.call(arguments, 0); 1024 args.unshift(this.index < this.images.length - 1 ? this.index + 1 : 0); 1025 return this.show.apply(this, args); 1026 } 1027 1028 , prev: function () { 1029 var args = Array.prototype.slice.call(arguments, 0); 1030 args.unshift(this.index === 0 ? this.images.length - 1 : this.index - 1); 1031 return this.show.apply(this, args); 1032 } 1033 1034 , pause: function () { 1035 // Pause the slideshow 1036 this.paused = true; 1037 1038 if (this.videoWrapper) { 1039 this.videoWrapper.pause(); 1040 } 1041 1042 return this; 1043 } 1044 1045 , resume: function () { 1046 // Resume the slideshow 1047 this.paused = false; 1048 1049 if (this.videoWrapper) { 1050 this.videoWrapper.play(); 1051 } 1052 1053 this.cycle(); 1054 return this; 1055 } 1056 1057 , cycle: function () { 1058 // Start/resume the slideshow 1059 if(this.images.length > 1) { 1060 // Clear the timeout, just in case 1061 clearTimeout(this._cycleTimeout); 1062 1063 var duration = (this._currentImage && this._currentImage.duration) || this.options.duration; 1064 var isVideo = isVideoSource(this._currentImage); 1065 1066 var callNext = function () { 1067 this.$item.off('.cycle'); 1068 1069 // Check for paused slideshow 1070 if (!this.paused) { 1071 this.next(); 1072 } 1073 }; 1074 1075 // Special video handling 1076 if (isVideo) { 1077 1078 // Leave video at last frame 1079 if (!this._currentImage.loop) { 1080 var lastFrameTimeout = 0; 1081 1082 this.$item 1083 .on('playing.cycle', function () { 1084 var player = $(this).data('player'); 1085 1086 clearTimeout(lastFrameTimeout); 1087 lastFrameTimeout = setTimeout(function () { 1088 player.pause(); 1089 player.$video.trigger('ended'); 1090 }, (player.getDuration() - player.getCurrentTime()) * 1000); 1091 }) 1092 .on('ended.cycle', function () { 1093 clearTimeout(lastFrameTimeout); 1094 }); 1095 } 1096 1097 // On error go to next 1098 this.$item.on('error.cycle initerror.cycle', $.proxy(callNext, this)); 1099 } 1100 1101 if (isVideo && !this._currentImage.duration) { 1102 // It's a video - playing until end 1103 this.$item.on('ended.cycle', $.proxy(callNext, this)); 1104 1105 } else { 1106 // Cycling according to specified duration 1107 this._cycleTimeout = setTimeout($.proxy(callNext, this), duration); 1108 } 1109 1110 } 1111 return this; 1112 } 1113 1114 , destroy: function (preserveBackground) { 1115 // Stop the resize events 1116 $(window).off('resize.backstretch orientationchange.backstretch'); 1117 1118 // Stop any videos 1119 if (this.videoWrapper) { 1120 this.videoWrapper.destroy(); 1121 } 1122 1123 // Clear the timeout 1124 clearTimeout(this._cycleTimeout); 1125 1126 // Remove Backstretch 1127 if(!preserveBackground) { 1128 this.$wrap.remove(); 1129 } 1130 this.$container.removeData('backstretch'); 1131 } 1132 }; 1133 1134 /** 1135 * Video Abstraction Layer 1136 * 1137 * Static methods: 1138 * > VideoWrapper.loadYoutubeAPI() -> Call in order to load the Youtube API. 1139 * An 'youtube_api_load' event will be triggered on $(window) when the API is loaded. 1140 * 1141 * Generic: 1142 * > player.type -> type of the video 1143 * > player.video / player.$video -> contains the element holding the video 1144 * > player.play() -> plays the video 1145 * > player.pause() -> pauses the video 1146 * > player.setCurrentTime(position) -> seeks to a position by seconds 1147 * 1148 * Youtube: 1149 * > player.ytId will contain the youtube ID if the source is a youtube url 1150 * > player.ytReady is a flag telling whether the youtube source is ready for playback 1151 * */ 1152 1153 var VideoWrapper = function () { this.init.apply(this, arguments); }; 1154 1155 /** 1156 * @param {Object} options 1157 * @param {String|Array<String>|Array<{{src: String, type: String?}}>} options.url 1158 * @param {Boolean} options.loop=false 1159 * @param {Boolean?} options.mute=true 1160 * @param {String?} options.poster 1161 * loop, mute, poster 1162 */ 1163 VideoWrapper.prototype.init = function (options) { 1164 1165 var that = this; 1166 1167 var $video; 1168 1169 var setVideoElement = function () { 1170 that.$video = $video; 1171 that.video = $video[0]; 1172 }; 1173 1174 // Determine video type 1175 1176 var videoType = 'video'; 1177 1178 if (!(options.url instanceof Array) && 1179 YOUTUBE_REGEXP.test(options.url)) { 1180 videoType = 'youtube'; 1181 } 1182 1183 that.type = videoType; 1184 1185 if (videoType === 'youtube') { 1186 1187 // Try to load the API in the meantime 1188 VideoWrapper.loadYoutubeAPI(); 1189 1190 that.ytId = options.url.match(YOUTUBE_REGEXP)[2]; 1191 var src = 'https://www.youtube.com/embed/' + that.ytId + 1192 '?rel=0&autoplay=0&showinfo=0&controls=0&modestbranding=1' + 1193 '&cc_load_policy=0&disablekb=1&iv_load_policy=3&loop=0' + 1194 '&enablejsapi=1&origin=' + encodeURIComponent(window.location.origin); 1195 1196 that.__ytStartMuted = !!options.mute || options.mute === undefined; 1197 1198 $video = $('<iframe />') 1199 .attr({ 'src_to_load': src }) 1200 .css({ 'border': 0, 'margin': 0, 'padding': 0 }) 1201 .data('player', that); 1202 1203 if (options.loop) { 1204 $video.on('ended.loop', function () { 1205 if (!that.__manuallyStopped) { 1206 that.play(); 1207 } 1208 }); 1209 } 1210 1211 that.ytReady = false; 1212 1213 setVideoElement(); 1214 1215 if (window['YT']) { 1216 that._initYoutube(); 1217 $video.trigger('initsuccess'); 1218 } else { 1219 $(window).one('youtube_api_load', function () { 1220 that._initYoutube(); 1221 $video.trigger('initsuccess'); 1222 }); 1223 } 1224 1225 } 1226 else { 1227 // Traditional <video> tag with multiple sources 1228 1229 $video = $('<video>') 1230 .prop('autoplay', false) 1231 .prop('controls', false) 1232 .prop('loop', !!options.loop) 1233 .prop('muted', !!options.mute || options.mute === undefined) 1234 1235 // Let the first frames be available before playback, as we do transitions 1236 .prop('preload', 'auto') 1237 .prop('poster', options.poster || ''); 1238 1239 var sources = (options.url instanceof Array) ? options.url : [options.url]; 1240 1241 for (var i = 0; i < sources.length; i++) { 1242 var sourceItem = sources[i]; 1243 if (typeof(sourceItem) === 'string') { 1244 sourceItem = { src: sourceItem }; 1245 } 1246 $('<source>') 1247 .attr('src', sourceItem.src) 1248 // Make sure to not specify type if unknown - 1249 // so the browser will try to autodetect. 1250 .attr('type', sourceItem.type || null) 1251 .appendTo($video); 1252 } 1253 1254 if (!$video[0].canPlayType || !sources.length) { 1255 $video.trigger('initerror'); 1256 } else { 1257 $video.trigger('initsuccess'); 1258 } 1259 1260 setVideoElement(); 1261 } 1262 1263 }; 1264 1265 VideoWrapper.prototype._initYoutube = function () { 1266 var that = this; 1267 1268 var YT = window['YT']; 1269 1270 that.$video 1271 .attr('src', that.$video.attr('src_to_load')) 1272 .removeAttr('src_to_load'); 1273 1274 // It won't init if it's not in the DOM, so we emulate that 1275 var hasParent = !!that.$video[0].parentNode; 1276 if (!hasParent) { 1277 var $tmpParent = $('<div>').css('display', 'none !important').appendTo(document.body); 1278 that.$video.appendTo($tmpParent); 1279 } 1280 1281 var player = new YT.Player(that.video, { 1282 events: { 1283 'onReady': function () { 1284 1285 if (that.__ytStartMuted) { 1286 player.mute(); 1287 } 1288 1289 if (!hasParent) { 1290 // Restore parent to old state - without interrupting any changes 1291 if (that.$video[0].parentNode === $tmpParent[0]) { 1292 that.$video.detach(); 1293 } 1294 $tmpParent.remove(); 1295 } 1296 1297 that.ytReady = true; 1298 that._updateYoutubeSize(); 1299 that.$video.trigger('canplay'); 1300 }, 1301 'onStateChange': function (event) { 1302 switch (event.data) { 1303 case YT.PlayerState.PLAYING: 1304 that.$video.trigger('playing'); 1305 break; 1306 case YT.PlayerState.ENDED: 1307 that.$video.trigger('ended'); 1308 break; 1309 case YT.PlayerState.PAUSED: 1310 that.$video.trigger('pause'); 1311 break; 1312 case YT.PlayerState.BUFFERING: 1313 that.$video.trigger('waiting'); 1314 break; 1315 case YT.PlayerState.CUED: 1316 that.$video.trigger('canplay'); 1317 break; 1318 } 1319 }, 1320 'onPlaybackQualityChange': function () { 1321 that._updateYoutubeSize(); 1322 that.$video.trigger('resize'); 1323 }, 1324 'onError': function (err) { 1325 that.hasError = true; 1326 that.$video.trigger({ 'type': 'error', 'error': err }); 1327 } 1328 } 1329 }); 1330 1331 that.ytPlayer = player; 1332 1333 return that; 1334 }; 1335 1336 VideoWrapper.prototype._updateYoutubeSize = function () { 1337 var that = this; 1338 1339 switch (that.ytPlayer.getPlaybackQuality() || 'medium') { 1340 case 'small': 1341 that.video.videoWidth = 426; 1342 that.video.videoHeight = 240; 1343 break; 1344 case 'medium': 1345 that.video.videoWidth = 640; 1346 that.video.videoHeight = 360; 1347 break; 1348 default: 1349 case 'large': 1350 that.video.videoWidth = 854; 1351 that.video.videoHeight = 480; 1352 break; 1353 case 'hd720': 1354 that.video.videoWidth = 1280; 1355 that.video.videoHeight = 720; 1356 break; 1357 case 'hd1080': 1358 that.video.videoWidth = 1920; 1359 that.video.videoHeight = 1080; 1360 break; 1361 case 'highres': 1362 that.video.videoWidth = 2560; 1363 that.video.videoHeight = 1440; 1364 break; 1365 } 1366 1367 return that; 1368 }; 1369 1370 VideoWrapper.prototype.play = function () { 1371 var that = this; 1372 1373 that.__manuallyStopped = false; 1374 1375 if (that.type === 'youtube') { 1376 if (that.ytReady) { 1377 that.$video.trigger('play'); 1378 that.ytPlayer.playVideo(); 1379 } 1380 } else { 1381 that.video.play(); 1382 } 1383 1384 return that; 1385 }; 1386 1387 VideoWrapper.prototype.pause = function () { 1388 var that = this; 1389 1390 that.__manuallyStopped = false; 1391 1392 if (that.type === 'youtube') { 1393 if (that.ytReady) { 1394 that.ytPlayer.pauseVideo(); 1395 } 1396 } else { 1397 that.video.pause(); 1398 } 1399 1400 return that; 1401 }; 1402 1403 VideoWrapper.prototype.stop = function () { 1404 var that = this; 1405 1406 that.__manuallyStopped = true; 1407 1408 if (that.type === 'youtube') { 1409 if (that.ytReady) { 1410 that.ytPlayer.pauseVideo(); 1411 that.ytPlayer.seekTo(0); 1412 } 1413 } else { 1414 that.video.pause(); 1415 that.video.currentTime = 0; 1416 } 1417 1418 return that; 1419 }; 1420 1421 VideoWrapper.prototype.destroy = function () { 1422 var that = this; 1423 1424 if (that.ytPlayer) { 1425 that.ytPlayer.destroy(); 1426 } 1427 1428 that.$video.remove(); 1429 1430 return that; 1431 }; 1432 1433 VideoWrapper.prototype.getCurrentTime = function (seconds) { 1434 var that = this; 1435 1436 if (that.type === 'youtube') { 1437 if (that.ytReady) { 1438 return that.ytPlayer.getCurrentTime(); 1439 } 1440 } else { 1441 return that.video.currentTime; 1442 } 1443 1444 return 0; 1445 }; 1446 1447 VideoWrapper.prototype.setCurrentTime = function (seconds) { 1448 var that = this; 1449 1450 if (that.type === 'youtube') { 1451 if (that.ytReady) { 1452 that.ytPlayer.seekTo(seconds, true); 1453 } 1454 } else { 1455 that.video.currentTime = seconds; 1456 } 1457 1458 return that; 1459 }; 1460 1461 VideoWrapper.prototype.getDuration = function () { 1462 var that = this; 1463 1464 if (that.type === 'youtube') { 1465 if (that.ytReady) { 1466 return that.ytPlayer.getDuration(); 1467 } 1468 } else { 1469 return that.video.duration; 1470 } 1471 1472 return 0; 1473 }; 1474 1475 /** 1476 * This will load the youtube API (if not loaded yet) 1477 * Use $(window).one('youtube_api_load', ...) to listen for API loaded event 1478 */ 1479 VideoWrapper.loadYoutubeAPI = function () { 1480 if (window['YT']) { 1481 return; 1482 } 1483 if (!$('script[src*=www\\.youtube\\.com\\/iframe_api]').length) { 1484 $('<script type="text/javascript" src="https://www.youtube.com/iframe_api">').appendTo('body'); 1485 } 1486 var ytAPILoadInt = setInterval(function () { 1487 if (window['YT'] && window['YT'].loaded) { 1488 $(window).trigger('youtube_api_load'); 1489 clearTimeout(ytAPILoadInt); 1490 } 1491 }, 50); 1492 }; 1493 1494 var getDeviceOrientation = function () { 1495 1496 if ('matchMedia' in window) { 1497 if (window.matchMedia("(orientation: portrait)").matches) { 1498 return 'portrait'; 1499 } else if (window.matchMedia("(orientation: landscape)").matches) { 1500 return 'landscape'; 1501 } 1502 } 1503 1504 if (screen.height > screen.width) { 1505 return 'portrait'; 1506 } 1507 1508 // Even square devices have orientation, 1509 // but a desktop browser may be too old for `matchMedia`. 1510 // Defaulting to `landscape` for the VERY rare case of a square desktop screen is good enough. 1511 return 'landscape'; 1512 }; 1513 1514 var getWindowOrientation = function () { 1515 if (window.innerHeight > window.innerWidth) { 1516 return 'portrait'; 1517 } 1518 if (window.innerWidth > window.innerHeight) { 1519 return 'landscape'; 1520 } 1521 1522 return 'square'; 1523 }; 1524 1525 /* SUPPORTS FIXED POSITION? 1526 * 1527 * Based on code from jQuery Mobile 1.1.0 1528 * http://jquerymobile.com/ 1529 * 1530 * In a nutshell, we need to figure out if fixed positioning is supported. 1531 * Unfortunately, this is very difficult to do on iOS, and usually involves 1532 * injecting content, scrolling the page, etc.. It's ugly. 1533 * jQuery Mobile uses this workaround. It's not ideal, but works. 1534 * 1535 * Modified to detect IE6 1536 * ========================= */ 1537 1538 var supportsFixedPosition = (function () { 1539 var ua = navigator.userAgent 1540 , platform = navigator.platform 1541 // Rendering engine is Webkit, and capture major version 1542 , wkmatch = ua.match( /AppleWebKit\/([0-9]+)/ ) 1543 , wkversion = !!wkmatch && wkmatch[ 1 ] 1544 , ffmatch = ua.match( /Fennec\/([0-9]+)/ ) 1545 , ffversion = !!ffmatch && ffmatch[ 1 ] 1546 , operammobilematch = ua.match( /Opera Mobi\/([0-9]+)/ ) 1547 , omversion = !!operammobilematch && operammobilematch[ 1 ] 1548 , iematch = ua.match( /MSIE ([0-9]+)/ ) 1549 , ieversion = !!iematch && iematch[ 1 ]; 1550 1551 return !( 1552 // iOS 4.3 and older : Platform is iPhone/Pad/Touch and Webkit version is less than 534 (ios5) 1553 ((platform.indexOf( "iPhone" ) > -1 || platform.indexOf( "iPad" ) > -1 || platform.indexOf( "iPod" ) > -1 ) && wkversion && wkversion < 534) || 1554 1555 // Opera Mini 1556 (window.operamini && ({}).toString.call( window.operamini ) === "[object OperaMini]") || 1557 (operammobilematch && omversion < 7458) || 1558 1559 //Android lte 2.1: Platform is Android and Webkit version is less than 533 (Android 2.2) 1560 (ua.indexOf( "Android" ) > -1 && wkversion && wkversion < 533) || 1561 1562 // Firefox Mobile before 6.0 -
1563 (ffversion && ffversion < 6) || 1564 1565 // WebOS less than 3 1566 ("palmGetResource" in window && wkversion && wkversion < 534) || 1567 1568 // MeeGo 1569 (ua.indexOf( "MeeGo" ) > -1 && ua.indexOf( "NokiaBrowser/8.5.0" ) > -1) || 1570 1571 // IE6 1572 (ieversion && ieversion <= 6) 1573 ); 1574 }()); 1575 1576}(jQuery, window));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.