jquery.backstretch.js (48070B)
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 return processAlignOptions(options); 478 }; 479 480 /* Process align options */ 481 var processAlignOptions = function (options, required) { 482 if (options.alignX === 'left') { 483 options.alignX = 0.0; 484 } 485 else if (options.alignX === 'center') { 486 options.alignX = 0.5; 487 } 488 else if (options.alignX === 'right') { 489 options.alignX = 1.0; 490 } 491 else { 492 if (options.alignX !== undefined || required) { 493 options.alignX = parseFloat(options.alignX); 494 if (isNaN(options.alignX)) { 495 options.alignX = 0.5; 496 } 497 } 498 } 499 500 if (options.alignY === 'top') { 501 options.alignY = 0.0; 502 } 503 else if (options.alignY === 'center') { 504 options.alignY = 0.5; 505 } 506 else if (options.alignY === 'bottom') { 507 options.alignY = 1.0; 508 } 509 else { 510 if (options.alignX !== undefined || required) { 511 options.alignY = parseFloat(options.alignY); 512 if (isNaN(options.alignY)) { 513 options.alignY = 0.5; 514 } 515 } 516 } 517 518 return options; 519 }; 520 521 /* CLASS DEFINITION 522 * ========================= */ 523 var Backstretch = function (container, images, options) { 524 this.options = $.extend({}, $.fn.backstretch.defaults, options || {}); 525 526 this.firstShow = true; 527 528 // Process options 529 processOptions(this.options, true); 530 531 /* In its simplest form, we allow Backstretch to be called on an image path. 532 * e.g. $.backstretch('/path/to/image.jpg') 533 * So, we need to turn this back into an array. 534 */ 535 this.images = processImagesArray($.isArray(images) ? images : [images]); 536 537 /** 538 * Paused-Option 539 */ 540 if (this.options.paused) { 541 this.paused = true; 542 } 543 544 /** 545 * Start-Option (Index) 546 */ 547 if (this.options.start >= this.images.length) 548 { 549 this.options.start = this.images.length - 1; 550 } 551 if (this.options.start < 0) 552 { 553 this.options.start = 0; 554 } 555 556 // Convenience reference to know if the container is body. 557 this.isBody = container === document.body; 558 559 /* We're keeping track of a few different elements 560 * 561 * Container: the element that Backstretch was called on. 562 * Wrap: a DIV that we place the image into, so we can hide the overflow. 563 * Root: Convenience reference to help calculate the correct height. 564 */ 565 var $window = $(window); 566 this.$container = $(container); 567 this.$root = this.isBody ? supportsFixedPosition ? $window : $(document) : this.$container; 568 569 this.originalImages = this.images; 570 this.images = optimalSizeImages( 571 this.options.alwaysTestWindowResolution ? $window : this.$root, 572 this.originalImages); 573 574 /** 575 * Pre-Loading. 576 * This is the first image, so we will preload a minimum of 1 images. 577 */ 578 preload(this.images, this.options.start || 0, this.options.preload || 1); 579 580 // Don't create a new wrap if one already exists (from a previous instance of Backstretch) 581 var $existing = this.$container.children(".backstretch").first(); 582 this.$wrap = $existing.length ? $existing : 583 $('<div class="backstretch"></div>') 584 .css(this.options.bypassCss ? {} : styles.wrap) 585 .appendTo(this.$container); 586 587 if (!this.options.bypassCss) { 588 589 // Non-body elements need some style adjustments 590 if (!this.isBody) { 591 // If the container is statically positioned, we need to make it relative, 592 // and if no zIndex is defined, we should set it to zero. 593 var position = this.$container.css('position') 594 , zIndex = this.$container.css('zIndex'); 595 596 this.$container.css({ 597 position: position === 'static' ? 'relative' : position 598 , zIndex: zIndex === 'auto' ? 0 : zIndex 599 }); 600 601 // Needs a higher z-index 602 this.$wrap.css({zIndex: -999998}); 603 } 604 605 // Fixed or absolute positioning? 606 this.$wrap.css({ 607 position: this.isBody && supportsFixedPosition ? 'fixed' : 'absolute' 608 }); 609 610 } 611 612 // Set the first image 613 this.index = this.options.start; 614 this.show(this.index); 615 616 // Listen for resize 617 $window.on('resize.backstretch', $.proxy(this.resize, this)) 618 .on('orientationchange.backstretch', $.proxy(function () { 619 // Need to do this in order to get the right window height 620 if (this.isBody && window.pageYOffset === 0) { 621 window.scrollTo(0, 1); 622 this.resize(); 623 } 624 }, this)); 625 }; 626 627 var performTransition = function (options) { 628 629 var transition = options.transition || 'fade'; 630 631 // Look for multiple options 632 if (typeof transition === 'string' && transition.indexOf('|') > -1) { 633 transition = transition.split('|'); 634 } 635 636 if (transition instanceof Array) { 637 transition = transition[Math.round(Math.random() * (transition.length - 1))]; 638 } 639 640 var $new = options['new']; 641 var $old = options['old'] ? options['old'] : $([]); 642 643 switch (transition.toString().toLowerCase()) { 644 645 default: 646 case 'fade': 647 $new.fadeIn({ 648 duration: options.duration, 649 complete: options.complete, 650 easing: options.easing || undefined 651 }); 652 break; 653 654 case 'fadeinout': 655 case 'fade_in_out': 656 657 var fadeInNew = function () { 658 $new.fadeIn({ 659 duration: options.duration / 2, 660 complete: options.complete, 661 easing: options.easing || undefined 662 }); 663 }; 664 665 if ($old.length) { 666 $old.fadeOut({ 667 duration: options.duration / 2, 668 complete: fadeInNew, 669 easing: options.easing || undefined 670 }); 671 } else { 672 fadeInNew(); 673 } 674 675 break; 676 677 case 'pushleft': 678 case 'push_left': 679 case 'pushright': 680 case 'push_right': 681 case 'pushup': 682 case 'push_up': 683 case 'pushdown': 684 case 'push_down': 685 case 'coverleft': 686 case 'cover_left': 687 case 'coverright': 688 case 'cover_right': 689 case 'coverup': 690 case 'cover_up': 691 case 'coverdown': 692 case 'cover_down': 693 694 var transitionParts = transition.match(/^(cover|push)_?(.*)$/); 695 696 var animProp = transitionParts[2] === 'left' ? 'right' : 697 transitionParts[2] === 'right' ? 'left' : 698 transitionParts[2] === 'down' ? 'top' : 699 transitionParts[2] === 'up' ? 'bottom' : 700 'right'; 701 702 var newCssStart = { 703 'display': '' 704 }, newCssAnim = {}; 705 newCssStart[animProp] = '-100%'; 706 newCssAnim[animProp] = 0; 707 708 $new 709 .css(newCssStart) 710 .animate(newCssAnim, { 711 duration: options.duration, 712 complete: function () { 713 $new.css(animProp, ''); 714 options.complete.apply(this, arguments); 715 }, 716 easing: options.easing || undefined 717 }); 718 719 if (transitionParts[1] === 'push' && $old.length) { 720 var oldCssAnim = {}; 721 oldCssAnim[animProp] = '100%'; 722 723 $old 724 .animate(oldCssAnim, { 725 duration: options.duration, 726 complete: function () { 727 $old.css('display', 'none'); 728 }, 729 easing: options.easing || undefined 730 }); 731 } 732 733 break; 734 } 735 736 }; 737 738 /* PUBLIC METHODS 739 * ========================= */ 740 Backstretch.prototype = { 741 742 resize: function () { 743 try { 744 745 // Check for a better suited image after the resize 746 var $resTest = this.options.alwaysTestWindowResolution ? $(window) : this.$root; 747 var newContainerWidth = $resTest.width(); 748 var newContainerHeight = $resTest.height(); 749 var changeRatioW = newContainerWidth / (this._lastResizeContainerWidth || 0); 750 var changeRatioH = newContainerHeight / (this._lastResizeContainerHeight || 0); 751 var resolutionChangeRatioThreshold = this.options.resolutionChangeRatioThreshold || 0.0; 752 753 // check for big changes in container size 754 if ((newContainerWidth !== this._lastResizeContainerWidth || 755 newContainerHeight !== this._lastResizeContainerHeight) && 756 ((Math.abs(changeRatioW - 1) >= resolutionChangeRatioThreshold || isNaN(changeRatioW)) || 757 (Math.abs(changeRatioH - 1) >= resolutionChangeRatioThreshold || isNaN(changeRatioH)))) { 758 759 this._lastResizeContainerWidth = newContainerWidth; 760 this._lastResizeContainerHeight = newContainerHeight; 761 762 // Big change: rebuild the entire images array 763 this.images = optimalSizeImages($resTest, this.originalImages); 764 765 // Preload them (they will be automatically inserted on the next cycle) 766 if (this.options.preload) { 767 preload(this.images, (this.index + 1) % this.images.length, this.options.preload); 768 } 769 770 // In case there is no cycle and the new source is different than the current 771 if (this.images.length === 1 && 772 this._currentImage.url !== this.images[0].url) { 773 774 // Wait a little an update the image being showed 775 var that = this; 776 clearTimeout(that._selectAnotherResolutionTimeout); 777 that._selectAnotherResolutionTimeout = setTimeout(function () { 778 that.show(0); 779 }, this.options.resolutionRefreshRate); 780 } 781 } 782 783 var bgCSS = {left: 0, top: 0, right: 'auto', bottom: 'auto'} 784 , rootWidth = this.isBody ? this.$root.width() : this.$root.innerWidth() 785 , rootHeight = this.isBody ? ( window.innerHeight ? window.innerHeight : this.$root.height() ) : this.$root.innerHeight() 786 , bgWidth = rootWidth 787 , bgHeight = bgWidth / this.$itemWrapper.data('ratio') 788 , evt = $.Event('backstretch.resize', { 789 relatedTarget: this.$container[0] 790 }) 791 , bgOffset 792 , alignX = this._currentImage.alignX === undefined ? this.options.alignX : this._currentImage.alignX 793 , alignY = this._currentImage.alignY === undefined ? this.options.alignY : this._currentImage.alignY; 794 795 // Make adjustments based on image ratio 796 if (bgHeight >= rootHeight) { 797 bgCSS.top = -(bgHeight - rootHeight) * alignY; 798 } else { 799 bgHeight = rootHeight; 800 bgWidth = bgHeight * this.$itemWrapper.data('ratio'); 801 bgOffset = (bgWidth - rootWidth) / 2; 802 bgCSS.left = -(bgWidth - rootWidth) * alignX; 803 } 804 805 if (!this.options.bypassCss) { 806 807 this.$wrap 808 .css({width: rootWidth, height: rootHeight}) 809 .find('>.backstretch-item').not('.deleteable') 810 .each(function () { 811 var $wrapper = $(this); 812 $wrapper.find('img,video,iframe') 813 .css({width: bgWidth, height: bgHeight}) 814 .css(bgCSS); 815 }); 816 } 817 818 this.$container.trigger(evt, this); 819 } catch(err) { 820 // IE7 seems to trigger resize before the image is loaded. 821 // This try/catch block is a hack to let it fail gracefully. 822 } 823 824 return this; 825 } 826 827 // Show the slide at a certain position 828 , show: function (newIndex, overrideOptions) { 829 830 // Validate index 831 if (Math.abs(newIndex) > this.images.length - 1) { 832 return; 833 } 834 835 // Vars 836 var that = this 837 , $oldItemWrapper = that.$wrap.find('>.backstretch-item').addClass('deleteable') 838 , oldVideoWrapper = that.videoWrapper 839 , evtOptions = { relatedTarget: that.$container[0] }; 840 841 // Trigger the "before" event 842 that.$container.trigger($.Event('backstretch.before', evtOptions), [that, newIndex]); 843 844 // Set the new frame index 845 this.index = newIndex; 846 var selectedImage = that.images[newIndex]; 847 848 // Pause the slideshow 849 clearTimeout(that._cycleTimeout); 850 851 // New image 852 853 delete that.videoWrapper; // Current item may not be a video 854 855 var isVideo = isVideoSource(selectedImage); 856 if (isVideo) { 857 that.videoWrapper = new VideoWrapper(selectedImage); 858 that.$item = that.videoWrapper.$video.css('pointer-events', 'none'); 859 } else { 860 that.$item = $('<img />'); 861 } 862 863 that.$itemWrapper = $('<div class="backstretch-item">') 864 .append(that.$item); 865 866 if (this.options.bypassCss) { 867 that.$itemWrapper.css({ 868 'display': 'none' 869 }); 870 } else { 871 that.$itemWrapper.css(styles.itemWrapper); 872 that.$item.css(styles.item); 873 } 874 875 that.$item.bind(isVideo ? 'canplay' : 'load', function (e) { 876 var $this = $(this) 877 , $wrapper = $this.parent() 878 , options = $wrapper.data('options'); 879 880 if (overrideOptions) { 881 options = $.extend({}, options, overrideOptions); 882 } 883 884 var imgWidth = this.naturalWidth || this.videoWidth || this.width 885 , imgHeight = this.naturalHeight || this.videoHeight || this.height; 886 887 // Save the ratio 888 $wrapper.data('ratio', imgWidth / imgHeight); 889 890 var getOption = function (opt) { 891 return options[opt] !== undefined ? 892 options[opt] : 893 that.options[opt]; 894 }; 895 896 var transition = getOption('transition'); 897 var transitionEasing = getOption('transitionEasing'); 898 var transitionDuration = getOption('transitionDuration'); 899 900 // Show the image, then delete the old one 901 var bringInNextImage = function () { 902 903 if (oldVideoWrapper) { 904 oldVideoWrapper.stop(); 905 oldVideoWrapper.destroy(); 906 } 907 908 $oldItemWrapper.remove(); 909 910 // Resume the slideshow 911 if (!that.paused && that.images.length > 1) { 912 that.cycle(); 913 } 914 915 // Now we can clear the background on the element, to spare memory 916 if (!that.options.bypassCss && !that.isBody) { 917 that.$container.css('background-image', 'none'); 918 } 919 920 // Trigger the "after" and "show" events 921 // "show" is being deprecated 922 $(['after', 'show']).each(function () { 923 that.$container.trigger($.Event('backstretch.' + this, evtOptions), [that, newIndex]); 924 }); 925 926 if (isVideo) { 927 that.videoWrapper.play(); 928 } 929 }; 930 931 if ((that.firstShow && !that.options.animateFirst) || !transitionDuration || !transition) { 932 // Avoid transition on first show or if there's no transitionDuration value 933 $wrapper.show(); 934 bringInNextImage(); 935 } else { 936 937 performTransition({ 938 'new': $wrapper, 939 old: $oldItemWrapper, 940 transition: transition, 941 duration: transitionDuration, 942 easing: transitionEasing, 943 complete: bringInNextImage 944 }); 945 946 } 947 948 that.firstShow = false; 949 950 // Resize 951 that.resize(); 952 }); 953 954 that.$itemWrapper.appendTo(that.$wrap); 955 956 that.$item.attr('alt', selectedImage.alt || ''); 957 that.$itemWrapper.data('options', selectedImage); 958 959 if (!isVideo) { 960 that.$item.attr('src', selectedImage.url); 961 } 962 963 that._currentImage = selectedImage; 964 965 return that; 966 } 967 968 , current: function () { 969 return this.index; 970 } 971 972 , next: function () { 973 var args = Array.prototype.slice.call(arguments, 0); 974 args.unshift(this.index < this.images.length - 1 ? this.index + 1 : 0); 975 return this.show.apply(this, args); 976 } 977 978 , prev: function () { 979 var args = Array.prototype.slice.call(arguments, 0); 980 args.unshift(this.index === 0 ? this.images.length - 1 : this.index - 1); 981 return this.show.apply(this, args); 982 } 983 984 , pause: function () { 985 // Pause the slideshow 986 this.paused = true; 987 988 if (this.videoWrapper) { 989 this.videoWrapper.pause(); 990 } 991 992 return this; 993 } 994 995 , resume: function () { 996 // Resume the slideshow 997 this.paused = false; 998 999 if (this.videoWrapper) { 1000 this.videoWrapper.play(); 1001 } 1002 1003 this.cycle(); 1004 return this; 1005 } 1006 1007 , cycle: function () { 1008 // Start/resume the slideshow 1009 if(this.images.length > 1) { 1010 // Clear the timeout, just in case 1011 clearTimeout(this._cycleTimeout); 1012 1013 var duration = (this._currentImage && this._currentImage.duration) || this.options.duration; 1014 var isVideo = isVideoSource(this._currentImage); 1015 1016 var callNext = function () { 1017 this.$item.off('.cycle'); 1018 1019 // Check for paused slideshow 1020 if (!this.paused) { 1021 this.next(); 1022 } 1023 }; 1024 1025 // Special video handling 1026 if (isVideo) { 1027 1028 // Leave video at last frame 1029 if (!this._currentImage.loop) { 1030 var lastFrameTimeout = 0; 1031 1032 this.$item 1033 .on('playing.cycle', function () { 1034 var player = $(this).data('player'); 1035 1036 clearTimeout(lastFrameTimeout); 1037 lastFrameTimeout = setTimeout(function () { 1038 player.pause(); 1039 player.$video.trigger('ended'); 1040 }, (player.getDuration() - player.getCurrentTime()) * 1000); 1041 }) 1042 .on('ended.cycle', function () { 1043 clearTimeout(lastFrameTimeout); 1044 }); 1045 } 1046 1047 // On error go to next 1048 this.$item.on('error.cycle initerror.cycle', $.proxy(callNext, this)); 1049 } 1050 1051 if (isVideo && !this._currentImage.duration) { 1052 // It's a video - playing until end 1053 this.$item.on('ended.cycle', $.proxy(callNext, this)); 1054 1055 } else { 1056 // Cycling according to specified duration 1057 this._cycleTimeout = setTimeout($.proxy(callNext, this), duration); 1058 } 1059 1060 } 1061 return this; 1062 } 1063 1064 , destroy: function (preserveBackground) { 1065 // Stop the resize events 1066 $(window).off('resize.backstretch orientationchange.backstretch'); 1067 1068 // Stop any videos 1069 if (this.videoWrapper) { 1070 this.videoWrapper.destroy(); 1071 } 1072 1073 // Clear the timeout 1074 clearTimeout(this._cycleTimeout); 1075 1076 // Remove Backstretch 1077 if(!preserveBackground) { 1078 this.$wrap.remove(); 1079 } 1080 this.$container.removeData('backstretch'); 1081 } 1082 }; 1083 1084 /** 1085 * Video Abstraction Layer 1086 * 1087 * Static methods: 1088 * > VideoWrapper.loadYoutubeAPI() -> Call in order to load the Youtube API. 1089 * An 'youtube_api_load' event will be triggered on $(window) when the API is loaded. 1090 * 1091 * Generic: 1092 * > player.type -> type of the video 1093 * > player.video / player.$video -> contains the element holding the video 1094 * > player.play() -> plays the video 1095 * > player.pause() -> pauses the video 1096 * > player.setCurrentTime(position) -> seeks to a position by seconds 1097 * 1098 * Youtube: 1099 * > player.ytId will contain the youtube ID if the source is a youtube url 1100 * > player.ytReady is a flag telling whether the youtube source is ready for playback 1101 * */ 1102 1103 var VideoWrapper = function () { this.init.apply(this, arguments); }; 1104 1105 /** 1106 * @param {Object} options 1107 * @param {String|Array<String>|Array<{{src: String, type: String?}}>} options.url 1108 * @param {Boolean} options.loop=false 1109 * @param {Boolean?} options.mute=true 1110 * @param {String?} options.poster 1111 * loop, mute, poster 1112 */ 1113 VideoWrapper.prototype.init = function (options) { 1114 1115 var that = this; 1116 1117 var $video; 1118 1119 var setVideoElement = function () { 1120 that.$video = $video; 1121 that.video = $video[0]; 1122 }; 1123 1124 // Determine video type 1125 1126 var videoType = 'video'; 1127 1128 if (!(options.url instanceof Array) && 1129 YOUTUBE_REGEXP.test(options.url)) { 1130 videoType = 'youtube'; 1131 } 1132 1133 that.type = videoType; 1134 1135 if (videoType === 'youtube') { 1136 1137 // Try to load the API in the meantime 1138 VideoWrapper.loadYoutubeAPI(); 1139 1140 that.ytId = options.url.match(YOUTUBE_REGEXP)[2]; 1141 var src = 'https://www.youtube.com/embed/' + that.ytId + 1142 '?rel=0&autoplay=0&showinfo=0&controls=0&modestbranding=1' + 1143 '&cc_load_policy=0&disablekb=1&iv_load_policy=3&loop=0' + 1144 '&enablejsapi=1&origin=' + encodeURIComponent(window.location.origin); 1145 1146 that.__ytStartMuted = !!options.mute || options.mute === undefined; 1147 1148 $video = $('<iframe />') 1149 .attr({ 'src_to_load': src }) 1150 .css({ 'border': 0, 'margin': 0, 'padding': 0 }) 1151 .data('player', that); 1152 1153 if (options.loop) { 1154 $video.on('ended.loop', function () { 1155 if (!that.__manuallyStopped) { 1156 that.play(); 1157 } 1158 }); 1159 } 1160 1161 that.ytReady = false; 1162 1163 setVideoElement(); 1164 1165 if (window['YT']) { 1166 that._initYoutube(); 1167 $video.trigger('initsuccess'); 1168 } else { 1169 $(window).one('youtube_api_load', function () { 1170 that._initYoutube(); 1171 $video.trigger('initsuccess'); 1172 }); 1173 } 1174 1175 } 1176 else { 1177 // Traditional <video> tag with multiple sources 1178 1179 $video = $('<video>') 1180 .prop('autoplay', false) 1181 .prop('controls', false) 1182 .prop('loop', !!options.loop) 1183 .prop('muted', !!options.mute || options.mute === undefined) 1184 1185 // Let the first frames be available before playback, as we do transitions 1186 .prop('preload', 'auto') 1187 .prop('poster', options.poster || ''); 1188 1189 var sources = (options.url instanceof Array) ? options.url : [options.url]; 1190 1191 for (var i = 0; i < sources.length; i++) { 1192 var sourceItem = sources[i]; 1193 if (typeof(sourceItem) === 'string') { 1194 sourceItem = { src: sourceItem }; 1195 } 1196 $('<source>') 1197 .attr('src', sourceItem.src) 1198 // Make sure to not specify type if unknown - 1199 // so the browser will try to autodetect. 1200 .attr('type', sourceItem.type || null) 1201 .appendTo($video); 1202 } 1203 1204 if (!$video[0].canPlayType || !sources.length) { 1205 $video.trigger('initerror'); 1206 } else { 1207 $video.trigger('initsuccess'); 1208 } 1209 1210 setVideoElement(); 1211 } 1212 1213 }; 1214 1215 VideoWrapper.prototype._initYoutube = function () { 1216 var that = this; 1217 1218 var YT = window['YT']; 1219 1220 that.$video 1221 .attr('src', that.$video.attr('src_to_load')) 1222 .removeAttr('src_to_load'); 1223 1224 // It won't init if it's not in the DOM, so we emulate that 1225 var hasParent = !!that.$video[0].parentNode; 1226 if (!hasParent) { 1227 var $tmpParent = $('<div>').css('display', 'none !important').appendTo(document.body); 1228 that.$video.appendTo($tmpParent); 1229 } 1230 1231 var player = new YT.Player(that.video, { 1232 events: { 1233 'onReady': function () { 1234 1235 if (that.__ytStartMuted) { 1236 player.mute(); 1237 } 1238 1239 if (!hasParent) { 1240 // Restore parent to old state - without interrupting any changes 1241 if (that.$video[0].parentNode === $tmpParent[0]) { 1242 that.$video.detach(); 1243 } 1244 $tmpParent.remove(); 1245 } 1246 1247 that.ytReady = true; 1248 that._updateYoutubeSize(); 1249 that.$video.trigger('canplay'); 1250 }, 1251 'onStateChange': function (event) { 1252 switch (event.data) { 1253 case YT.PlayerState.PLAYING: 1254 that.$video.trigger('playing'); 1255 break; 1256 case YT.PlayerState.ENDED: 1257 that.$video.trigger('ended'); 1258 break; 1259 case YT.PlayerState.PAUSED: 1260 that.$video.trigger('pause'); 1261 break; 1262 case YT.PlayerState.BUFFERING: 1263 that.$video.trigger('waiting'); 1264 break; 1265 case YT.PlayerState.CUED: 1266 that.$video.trigger('canplay'); 1267 break; 1268 } 1269 }, 1270 'onPlaybackQualityChange': function () { 1271 that._updateYoutubeSize(); 1272 that.$video.trigger('resize'); 1273 }, 1274 'onError': function (err) { 1275 that.hasError = true; 1276 that.$video.trigger({ 'type': 'error', 'error': err }); 1277 } 1278 } 1279 }); 1280 1281 that.ytPlayer = player; 1282 1283 return that; 1284 }; 1285 1286 VideoWrapper.prototype._updateYoutubeSize = function () { 1287 var that = this; 1288 1289 switch (that.ytPlayer.getPlaybackQuality() || 'medium') { 1290 case 'small': 1291 that.video.videoWidth = 426; 1292 that.video.videoHeight = 240; 1293 break; 1294 case 'medium': 1295 that.video.videoWidth = 640; 1296 that.video.videoHeight = 360; 1297 break; 1298 default: 1299 case 'large': 1300 that.video.videoWidth = 854; 1301 that.video.videoHeight = 480; 1302 break; 1303 case 'hd720': 1304 that.video.videoWidth = 1280; 1305 that.video.videoHeight = 720; 1306 break; 1307 case 'hd1080': 1308 that.video.videoWidth = 1920; 1309 that.video.videoHeight = 1080; 1310 break; 1311 case 'highres': 1312 that.video.videoWidth = 2560; 1313 that.video.videoHeight = 1440; 1314 break; 1315 } 1316 1317 return that; 1318 }; 1319 1320 VideoWrapper.prototype.play = function () { 1321 var that = this; 1322 1323 that.__manuallyStopped = false; 1324 1325 if (that.type === 'youtube') { 1326 if (that.ytReady) { 1327 that.$video.trigger('play'); 1328 that.ytPlayer.playVideo(); 1329 } 1330 } else { 1331 that.video.play(); 1332 } 1333 1334 return that; 1335 }; 1336 1337 VideoWrapper.prototype.pause = function () { 1338 var that = this; 1339 1340 that.__manuallyStopped = false; 1341 1342 if (that.type === 'youtube') { 1343 if (that.ytReady) { 1344 that.ytPlayer.pauseVideo(); 1345 } 1346 } else { 1347 that.video.pause(); 1348 } 1349 1350 return that; 1351 }; 1352 1353 VideoWrapper.prototype.stop = function () { 1354 var that = this; 1355 1356 that.__manuallyStopped = true; 1357 1358 if (that.type === 'youtube') { 1359 if (that.ytReady) { 1360 that.ytPlayer.pauseVideo(); 1361 that.ytPlayer.seekTo(0); 1362 } 1363 } else { 1364 that.video.pause(); 1365 that.video.currentTime = 0; 1366 } 1367 1368 return that; 1369 }; 1370 1371 VideoWrapper.prototype.destroy = function () { 1372 var that = this; 1373 1374 if (that.ytPlayer) { 1375 that.ytPlayer.destroy(); 1376 } 1377 1378 that.$video.remove(); 1379 1380 return that; 1381 }; 1382 1383 VideoWrapper.prototype.getCurrentTime = function (seconds) { 1384 var that = this; 1385 1386 if (that.type === 'youtube') { 1387 if (that.ytReady) { 1388 return that.ytPlayer.getCurrentTime(); 1389 } 1390 } else { 1391 return that.video.currentTime; 1392 } 1393 1394 return 0; 1395 }; 1396 1397 VideoWrapper.prototype.setCurrentTime = function (seconds) { 1398 var that = this; 1399 1400 if (that.type === 'youtube') { 1401 if (that.ytReady) { 1402 that.ytPlayer.seekTo(seconds, true); 1403 } 1404 } else { 1405 that.video.currentTime = seconds; 1406 } 1407 1408 return that; 1409 }; 1410 1411 VideoWrapper.prototype.getDuration = function () { 1412 var that = this; 1413 1414 if (that.type === 'youtube') { 1415 if (that.ytReady) { 1416 return that.ytPlayer.getDuration(); 1417 } 1418 } else { 1419 return that.video.duration; 1420 } 1421 1422 return 0; 1423 }; 1424 1425 /** 1426 * This will load the youtube API (if not loaded yet) 1427 * Use $(window).one('youtube_api_load', ...) to listen for API loaded event 1428 */ 1429 VideoWrapper.loadYoutubeAPI = function () { 1430 if (window['YT']) { 1431 return; 1432 } 1433 if (!$('script[src*=www\\.youtube\\.com\\/iframe_api]').length) { 1434 $('<script type="text/javascript" src="https://www.youtube.com/iframe_api">').appendTo('body'); 1435 } 1436 var ytAPILoadInt = setInterval(function () { 1437 if (window['YT'] && window['YT'].loaded) { 1438 $(window).trigger('youtube_api_load'); 1439 clearTimeout(ytAPILoadInt); 1440 } 1441 }, 50); 1442 }; 1443 1444 var getDeviceOrientation = function () { 1445 1446 if ('matchMedia' in window) { 1447 if (window.matchMedia("(orientation: portrait)").matches) { 1448 return 'portrait'; 1449 } else if (window.matchMedia("(orientation: landscape)").matches) { 1450 return 'landscape'; 1451 } 1452 } 1453 1454 if (screen.height > screen.width) { 1455 return 'portrait'; 1456 } 1457 1458 // Even square devices have orientation, 1459 // but a desktop browser may be too old for `matchMedia`. 1460 // Defaulting to `landscape` for the VERY rare case of a square desktop screen is good enough. 1461 return 'landscape'; 1462 }; 1463 1464 var getWindowOrientation = function () { 1465 if (window.innerHeight > window.innerWidth) { 1466 return 'portrait'; 1467 } 1468 if (window.innerWidth > window.innerHeight) { 1469 return 'landscape'; 1470 } 1471 1472 return 'square'; 1473 }; 1474 1475 /* SUPPORTS FIXED POSITION? 1476 * 1477 * Based on code from jQuery Mobile 1.1.0 1478 * http://jquerymobile.com/ 1479 * 1480 * In a nutshell, we need to figure out if fixed positioning is supported. 1481 * Unfortunately, this is very difficult to do on iOS, and usually involves 1482 * injecting content, scrolling the page, etc.. It's ugly. 1483 * jQuery Mobile uses this workaround. It's not ideal, but works. 1484 * 1485 * Modified to detect IE6 1486 * ========================= */ 1487 1488 var supportsFixedPosition = (function () { 1489 var ua = navigator.userAgent 1490 , platform = navigator.platform 1491 // Rendering engine is Webkit, and capture major version 1492 , wkmatch = ua.match( /AppleWebKit\/([0-9]+)/ ) 1493 , wkversion = !!wkmatch && wkmatch[ 1 ] 1494 , ffmatch = ua.match( /Fennec\/([0-9]+)/ ) 1495 , ffversion = !!ffmatch && ffmatch[ 1 ] 1496 , operammobilematch = ua.match( /Opera Mobi\/([0-9]+)/ ) 1497 , omversion = !!operammobilematch && operammobilematch[ 1 ] 1498 , iematch = ua.match( /MSIE ([0-9]+)/ ) 1499 , ieversion = !!iematch && iematch[ 1 ]; 1500 1501 return !( 1502 // iOS 4.3 and older : Platform is iPhone/Pad/Touch and Webkit version is less than 534 (ios5) 1503 ((platform.indexOf( "iPhone" ) > -1 || platform.indexOf( "iPad" ) > -1 || platform.indexOf( "iPod" ) > -1 ) && wkversion && wkversion < 534) || 1504 1505 // Opera Mini 1506 (window.operamini && ({}).toString.call( window.operamini ) === "[object OperaMini]") || 1507 (operammobilematch && omversion < 7458) || 1508 1509 //Android lte 2.1: Platform is Android and Webkit version is less than 533 (Android 2.2) 1510 (ua.indexOf( "Android" ) > -1 && wkversion && wkversion < 533) || 1511 1512 // Firefox Mobile before 6.0 - 1513 (ffversion && ffversion < 6) || 1514 1515 // WebOS less than 3 1516 ("palmGetResource" in window && wkversion && wkversion < 534) || 1517 1518 // MeeGo 1519 (ua.indexOf( "MeeGo" ) > -1 && ua.indexOf( "NokiaBrowser/8.5.0" ) > -1) || 1520 1521 // IE6 1522 (ieversion && ieversion <= 6) 1523 ); 1524 }()); 1525 1526 }(jQuery, window));