File: /home/joyfejor/public_html/wp-content/themes/onepress/assets/js/jquery.backstretch.js
/*
* Backstretch
* http://srobbin.com/jquery-plugins/backstretch/
*
* Copyright (c) 2013 Scott Robbin
* Licensed under the MIT license.
*/
;(function ($, window, undefined) {
'use strict';
/** @const */
var YOUTUBE_REGEXP = /^.*(youtu\.be\/|youtube\.com\/v\/|youtube\.com\/embed\/|youtube\.com\/watch\?v=|youtube\.com\/watch\?.*\&v=)([^#\&\?]*).*/i;
/* PLUGIN DEFINITION
* ========================= */
$.fn.backstretch = function (images, options) {
var args = arguments;
/*
* Scroll the page one pixel to get the right window height on iOS
* Pretty harmless for everyone else
*/
if ($(window).scrollTop() === 0 ) {
window.scrollTo(0, 0);
}
var returnValues;
this.each(function (eachIndex) {
var $this = $(this)
, obj = $this.data('backstretch');
// Do we already have an instance attached to this element?
if (obj) {
// Is this a method they're trying to execute?
if (typeof args[0] === 'string' &&
typeof obj[args[0]] === 'function') {
// Call the method
var returnValue = obj[args[0]].apply(obj, Array.prototype.slice.call(args, 1));
if (returnValue === obj) { // If a method is chaining
returnValue = undefined;
}
if (returnValue !== undefined) {
returnValues = returnValues || [];
returnValues[eachIndex] = returnValue;
}
return; // Nothing further to do
}
// Merge the old options with the new
options = $.extend(obj.options, options);
// Remove the old instance
if ( obj.hasOwnProperty('destroy') ) {
obj.destroy(true);
}
}
// We need at least one image
if (!images || (images && images.length === 0)) {
var cssBackgroundImage = $this.css('background-image');
if (cssBackgroundImage && cssBackgroundImage !== 'none') {
images = [ { url: $this.css('backgroundImage').replace(/url\(|\)|"|'/g,"") } ];
} else {
$.error('No images were supplied for Backstretch, or element must have a CSS-defined background image.');
}
}
obj = new Backstretch(this, images, options || {});
$this.data('backstretch', obj);
});
return returnValues ? returnValues.length === 1 ? returnValues[0] : returnValues : this;
};
// If no element is supplied, we'll attach to body
$.backstretch = function (images, options) {
// Return the instance
return $('body')
.backstretch(images, options)
.data('backstretch');
};
// Custom selector
$.expr[':'].backstretch = function(elem) {
return $(elem).data('backstretch') !== undefined;
};
/* DEFAULTS
* ========================= */
$.fn.backstretch.defaults = {
duration: 5000 // Amount of time in between slides (if slideshow)
, transition: 'fade' // Type of transition between slides
, transitionDuration: 0 // Duration of transition between slides
, animateFirst: true // Animate the transition of first image of slideshow in?
, alignX: 0.5 // The x-alignment for the image, can be 'left'|'center'|'right' or any number between 0.0 and 1.0
, alignY: 0.5 // The y-alignment for the image, can be 'top'|'center'|'bottom' or any number between 0.0 and 1.0
, paused: false // Whether the images should slide after given duration
, start: 0 // Index of the first image to show
, preload: 2 // How many images preload at a time?
, preloadSize: 1 // How many images can we preload in parallel?
, resolutionRefreshRate: 2500 // How long to wait before switching resolution?
, resolutionChangeRatioThreshold: 0.1 // How much a change should it be before switching resolution?
};
/* STYLES
*
* Baked-in styles that we'll apply to our elements.
* In an effort to keep the plugin simple, these are not exposed as options.
* That said, anyone can override these in their own stylesheet.
* ========================= */
var styles = {
wrap: {
left: 0
, top: 0
, overflow: 'hidden'
, margin: 0
, padding: 0
, height: '100%'
, width: '100%'
, zIndex: -999999
}
, itemWrapper: {
position: 'absolute'
, display: 'none'
, margin: 0
, padding: 0
, border: 'none'
, width: '100%'
, height: '100%'
, zIndex: -999999
}
, item: {
position: 'absolute'
, margin: 0
, padding: 0
, border: 'none'
, width: '100%'
, height: '100%'
, maxWidth: 'none'
}
};
/* Given an array of different options for an image,
* choose the optimal image for the container size.
*
* Given an image template (a string with {{ width }} and/or
* {{height}} inside) and a container object, returns the
* image url with the exact values for the size of that
* container.
*
* Returns an array of urls optimized for the specified resolution.
*
*/
var optimalSizeImages = (function () {
/* Sorts the array of image sizes based on width */
var widthInsertSort = function (arr) {
for (var i = 1; i < arr.length; i++) {
var tmp = arr[i],
j = i;
while (arr[j - 1] && parseInt(arr[j - 1].width, 10) > parseInt(tmp.width, 10)) {
arr[j] = arr[j - 1];
--j;
}
arr[j] = tmp;
}
return arr;
};
/* Given an array of various sizes of the same image and a container width,
* return the best image.
*/
var selectBest = function (containerWidth, containerHeight, imageSizes) {
var devicePixelRatio = window.devicePixelRatio || 1;
var deviceOrientation = getDeviceOrientation();
var windowOrientation = getWindowOrientation();
var wrapperOrientation = (containerHeight > containerWidth) ?
'portrait' :
(containerWidth > containerHeight ? 'landscape' : 'square');
var lastAllowedImage = 0;
var testWidth;
for (var j = 0, image; j < imageSizes.length; j++) {
image = imageSizes[j];
// In case a new image was pushed in, process it:
if (typeof image === 'string') {
image = imageSizes[j] = { url: image };
}
if (image.pixelRatio && image.pixelRatio !== 'auto' && parseFloat(image.pixelRatio) !== devicePixelRatio) {
// We disallowed choosing this image for current device pixel ratio,
// So skip this one.
continue;
}
if (image.deviceOrientation && image.deviceOrientation !== deviceOrientation) {
// We disallowed choosing this image for current device orientation,
// So skip this one.
continue;
}
if (image.windowOrientation && image.windowOrientation !== deviceOrientation) {
// We disallowed choosing this image for current window orientation,
// So skip this one.
continue;
}
if (image.orientation && image.orientation !== wrapperOrientation) {
// We disallowed choosing this image for current element's orientation,
// So skip this one.
continue;
}
// Mark this one as the last one we investigated
// which does not violate device pixel ratio rules.
// We may choose this one later if there's no match.
lastAllowedImage = j;
// For most images, we match the specified width against element width,
// And enforcing a limit depending on the "pixelRatio" property if specified.
// But if a pixelRatio="auto", then we consider the width as the physical width of the image,
// And match it while considering the device's pixel ratio.
testWidth = containerWidth;
if (image.pixelRatio === 'auto') {
containerWidth *= devicePixelRatio;
}
// Stop when the width of the image is larger or equal to the container width
if (image.width >= testWidth) {
break;
}
}
// Use the image located at where we stopped
return imageSizes[Math.min(j, lastAllowedImage)];
};
var replaceTagsInUrl = function (url, templateReplacer) {
if (typeof url === 'string') {
url = url.replace(/{{(width|height)}}/g, templateReplacer);
} else if (url instanceof Array) {
for (var i = 0; i < url.length; i++) {
if (url[i].src) {
url[i].src = replaceTagsInUrl(url[i].src, templateReplacer);
} else {
url[i] = replaceTagsInUrl(url[i], templateReplacer);
}
}
}
return url;
};
return function ($container, images) {
var containerWidth = $container.width(),
containerHeight = $container.height();
var chosenImages = [];
var templateReplacer = function (match, key) {
if (key === 'width') {
return containerWidth;
}
if (key === 'height') {
return containerHeight;
}
return match;
};
for (var i = 0; i < images.length; i++) {
if ($.isArray(images[i])) {
images[i] = widthInsertSort(images[i]);
var chosen = selectBest(containerWidth, containerHeight, images[i]);
chosenImages.push(chosen);
} else {
// In case a new image was pushed in, process it:
if (typeof images[i] === 'string') {
images[i] = { url: images[i] };
}
var item = $.extend({}, images[i]);
item.url = replaceTagsInUrl(item.url, templateReplacer);
chosenImages.push(item);
}
}
return chosenImages;
};
})();
var isVideoSource = function (source) {
return YOUTUBE_REGEXP.test(source.url) || source.isVideo;
};
/* Preload images */
var preload = (function (sources, startAt, count, batchSize, callback) {
// Plugin cache
var cache = [];
// Wrapper for cache
var caching = function(image){
for (var i = 0; i < cache.length; i++) {
if (cache[i].src === image.src) {
return cache[i];
}
}
cache.push(image);
return image;
};
// Execute callback
var exec = function(sources, callback, last){
if (typeof callback === 'function') {
callback.call(sources, last);
}
};
// Closure to hide cache
return function preload (sources, startAt, count, batchSize, callback){
// Check input data
if (typeof sources === 'undefined') {
return;
}
if (!$.isArray(sources)) {
sources = [sources];
}
if (arguments.length < 5 && typeof arguments[arguments.length - 1] === 'function') {
callback = arguments[arguments.length - 1];
}
startAt = (typeof startAt === 'function' || !startAt) ? 0 : startAt;
count = (typeof count === 'function' || !count || count < 0) ? sources.length : Math.min(count, sources.length);
batchSize = (typeof batchSize === 'function' || !batchSize) ? 1 : batchSize;
if (startAt >= sources.length) {
startAt = 0;
count = 0;
}
if (batchSize < 0) {
batchSize = count;
}
batchSize = Math.min(batchSize, count);
var next = sources.slice(startAt + batchSize, count - batchSize);
sources = sources.slice(startAt, batchSize);
count = sources.length;
// If sources array is empty
if (!count) {
exec(sources, callback, true);
return;
}
// Image loading callback
var countLoaded = 0;
var loaded = function() {
countLoaded++;
if (countLoaded !== count) {
return;
}
exec(sources, callback, !next);
preload(next, 0, 0, batchSize, callback);
};
// Loop sources to preload
var image;
for (var i = 0; i < sources.length; i++) {
if (isVideoSource(sources[i])) {
// Do not preload videos. There are issues with that.
// First - we need to keep an instance of the preloaded and use that exactly, not a copy.
// Second - there are memory issues.
// If there will be a requirement from users - I'll try to implement this.
continue;
} else {
image = new Image();
image.src = sources[i].url;
image = caching(image);
if (image.complete) {
loaded();
} else {
$(image).on('load error', loaded);
}
}
}
};
})();
/* Process images array */
var processImagesArray = function (images) {
var processed = [];
for (var i = 0; i < images.length; i++) {
if (typeof images[i] === 'string') {
processed.push({ url: images[i] });
}
else if ($.isArray(images[i])) {
processed.push(processImagesArray(images[i]));
}
else {
processed.push(processOptions(images[i]));
}
}
return processed;
};
/* Process options */
var processOptions = function (options, required) {
// Convert old options
// centeredX/centeredY are deprecated
if (options.centeredX || options.centeredY) {
if (window.console && window.console.log) {
window.console.log('jquery.backstretch: `centeredX`/`centeredY` is deprecated, please use `alignX`/`alignY`');
}
if (options.centeredX) {
options.alignX = 0.5;
}
if (options.centeredY) {
options.alignY = 0.5;
}
}
// Deprecated spec
if (options.speed !== undefined) {
if (window.console && window.console.log) {
window.console.log('jquery.backstretch: `speed` is deprecated, please use `transitionDuration`');
}
options.transitionDuration = options.speed;
options.transition = 'fade';
}
// Typo
if (options.resolutionChangeRatioTreshold !== undefined) {
window.console.log('jquery.backstretch: `treshold` is a typo!');
options.resolutionChangeRatioThreshold = options.resolutionChangeRatioTreshold;
}
// Current spec that needs processing
if (options.fadeFirst !== undefined) {
options.animateFirst = options.fadeFirst;
}
if (options.fade !== undefined) {
options.transitionDuration = options.fade;
options.transition = 'fade';
}
if (options.scale) {
options.scale = validScale(options.scale);
}
return processAlignOptions(options);
};
/* Process align options */
var processAlignOptions = function (options, required) {
if (options.alignX === 'left') {
options.alignX = 0.0;
}
else if (options.alignX === 'center') {
options.alignX = 0.5;
}
else if (options.alignX === 'right') {
options.alignX = 1.0;
}
else {
if (options.alignX !== undefined || required) {
options.alignX = parseFloat(options.alignX);
if (isNaN(options.alignX)) {
options.alignX = 0.5;
}
}
}
if (options.alignY === 'top') {
options.alignY = 0.0;
}
else if (options.alignY === 'center') {
options.alignY = 0.5;
}
else if (options.alignY === 'bottom') {
options.alignY = 1.0;
}
else {
if (options.alignX !== undefined || required) {
options.alignY = parseFloat(options.alignY);
if (isNaN(options.alignY)) {
options.alignY = 0.5;
}
}
}
return options;
};
var SUPPORTED_SCALE_OPTIONS = {
'cover': 'cover',
'fit': 'fit',
'fit-smaller': 'fit-smaller',
'fill': 'fill'
};
function validScale(scale) {
if (!SUPPORTED_SCALE_OPTIONS.hasOwnProperty(scale)) {
return 'cover';
}
return scale;
}
/* CLASS DEFINITION
* ========================= */
var Backstretch = function (container, images, options) {
this.options = $.extend({}, $.fn.backstretch.defaults, options || {});
this.firstShow = true;
// Process options
processOptions(this.options, true);
/* In its simplest form, we allow Backstretch to be called on an image path.
* e.g. $.backstretch('/path/to/image.jpg')
* So, we need to turn this back into an array.
*/
this.images = processImagesArray($.isArray(images) ? images : [images]);
/**
* Paused-Option
*/
if (this.options.paused) {
this.paused = true;
}
/**
* Start-Option (Index)
*/
if (this.options.start >= this.images.length)
{
this.options.start = this.images.length - 1;
}
if (this.options.start < 0)
{
this.options.start = 0;
}
// Convenience reference to know if the container is body.
this.isBody = container === document.body;
/* We're keeping track of a few different elements
*
* Container: the element that Backstretch was called on.
* Wrap: a DIV that we place the image into, so we can hide the overflow.
* Root: Convenience reference to help calculate the correct height.
*/
var $window = $(window);
this.$container = $(container);
this.$root = this.isBody ? supportsFixedPosition ? $window : $(document) : this.$container;
this.originalImages = this.images;
this.images = optimalSizeImages(
this.options.alwaysTestWindowResolution ? $window : this.$root,
this.originalImages);
/**
* Pre-Loading.
* This is the first image, so we will preload a minimum of 1 images.
*/
preload(this.images, this.options.start || 0, this.options.preload || 1);
// Don't create a new wrap if one already exists (from a previous instance of Backstretch)
var $existing = this.$container.children(".backstretch").first();
this.$wrap = $existing.length ? $existing :
$('<div class="backstretch"></div>')
.css(this.options.bypassCss ? {} : styles.wrap)
.appendTo(this.$container);
if (!this.options.bypassCss) {
// Non-body elements need some style adjustments
if (!this.isBody) {
// If the container is statically positioned, we need to make it relative,
// and if no zIndex is defined, we should set it to zero.
var position = this.$container.css('position')
, zIndex = this.$container.css('zIndex');
this.$container.css({
position: position === 'static' ? 'relative' : position
, zIndex: zIndex === 'auto' ? 0 : zIndex
});
// Needs a higher z-index
this.$wrap.css({zIndex: -999998});
}
// Fixed or absolute positioning?
this.$wrap.css({
position: this.isBody && supportsFixedPosition ? 'fixed' : 'absolute'
});
}
// Set the first image
this.index = this.options.start;
this.show(this.index);
// Listen for resize
$window.on('resize.backstretch', $.proxy(this.resize, this))
.on('orientationchange.backstretch', $.proxy(function () {
// Need to do this in order to get the right window height
if (this.isBody && window.pageYOffset === 0) {
window.scrollTo(0, 1);
this.resize();
}
}, this));
};
var performTransition = function (options) {
var transition = options.transition || 'fade';
// Look for multiple options
if (typeof transition === 'string' && transition.indexOf('|') > -1) {
transition = transition.split('|');
}
if (transition instanceof Array) {
transition = transition[Math.round(Math.random() * (transition.length - 1))];
}
var $new = options['new'];
var $old = options['old'] ? options['old'] : $([]);
switch (transition.toString().toLowerCase()) {
default:
case 'fade':
$new.fadeIn({
duration: options.duration,
complete: options.complete,
easing: options.easing || undefined
});
break;
case 'fadeinout':
case 'fade_in_out':
var fadeInNew = function () {
$new.fadeIn({
duration: options.duration / 2,
complete: options.complete,
easing: options.easing || undefined
});
};
if ($old.length) {
$old.fadeOut({
duration: options.duration / 2,
complete: fadeInNew,
easing: options.easing || undefined
});
} else {
fadeInNew();
}
break;
case 'pushleft':
case 'push_left':
case 'pushright':
case 'push_right':
case 'pushup':
case 'push_up':
case 'pushdown':
case 'push_down':
case 'coverleft':
case 'cover_left':
case 'coverright':
case 'cover_right':
case 'coverup':
case 'cover_up':
case 'coverdown':
case 'cover_down':
var transitionParts = transition.match(/^(cover|push)_?(.*)$/);
var animProp = transitionParts[2] === 'left' ? 'right' :
transitionParts[2] === 'right' ? 'left' :
transitionParts[2] === 'down' ? 'top' :
transitionParts[2] === 'up' ? 'bottom' :
'right';
var newCssStart = {
'display': ''
}, newCssAnim = {};
newCssStart[animProp] = '-100%';
newCssAnim[animProp] = 0;
$new
.css(newCssStart)
.animate(newCssAnim, {
duration: options.duration,
complete: function () {
$new.css(animProp, '');
options.complete.apply(this, arguments);
},
easing: options.easing || undefined
});
if (transitionParts[1] === 'push' && $old.length) {
var oldCssAnim = {};
oldCssAnim[animProp] = '100%';
$old
.animate(oldCssAnim, {
duration: options.duration,
complete: function () {
$old.css('display', 'none');
},
easing: options.easing || undefined
});
}
break;
}
};
/* PUBLIC METHODS
* ========================= */
Backstretch.prototype = {
resize: function () {
try {
// Check for a better suited image after the resize
var $resTest = this.options.alwaysTestWindowResolution ? $(window) : this.$root;
var newContainerWidth = $resTest.width();
var newContainerHeight = $resTest.height();
var changeRatioW = newContainerWidth / (this._lastResizeContainerWidth || 0);
var changeRatioH = newContainerHeight / (this._lastResizeContainerHeight || 0);
var resolutionChangeRatioThreshold = this.options.resolutionChangeRatioThreshold || 0.0;
// check for big changes in container size
if ((newContainerWidth !== this._lastResizeContainerWidth ||
newContainerHeight !== this._lastResizeContainerHeight) &&
((Math.abs(changeRatioW - 1) >= resolutionChangeRatioThreshold || isNaN(changeRatioW)) ||
(Math.abs(changeRatioH - 1) >= resolutionChangeRatioThreshold || isNaN(changeRatioH)))) {
this._lastResizeContainerWidth = newContainerWidth;
this._lastResizeContainerHeight = newContainerHeight;
// Big change: rebuild the entire images array
this.images = optimalSizeImages($resTest, this.originalImages);
// Preload them (they will be automatically inserted on the next cycle)
if (this.options.preload) {
preload(this.images, (this.index + 1) % this.images.length, this.options.preload);
}
// In case there is no cycle and the new source is different than the current
if (this.images.length === 1 &&
this._currentImage.url !== this.images[0].url) {
// Wait a little an update the image being showed
var that = this;
clearTimeout(that._selectAnotherResolutionTimeout);
that._selectAnotherResolutionTimeout = setTimeout(function () {
that.show(0);
}, this.options.resolutionRefreshRate);
}
}
var bgCSS = {left: 0, top: 0, right: 'auto', bottom: 'auto'}
, boxWidth = this.isBody ? this.$root.width() : this.$root.innerWidth()
, boxHeight = this.isBody ? ( window.innerHeight ? window.innerHeight : this.$root.height() ) : this.$root.innerHeight()
, naturalWidth = this.$itemWrapper.data('width')
, naturalHeight = this.$itemWrapper.data('height')
, ratio = (naturalWidth / naturalHeight) || 1
, alignX = this._currentImage.alignX === undefined ? this.options.alignX : this._currentImage.alignX
, alignY = this._currentImage.alignY === undefined ? this.options.alignY : this._currentImage.alignY
, scale = validScale(this._currentImage.scale || this.options.scale);
var width, height;
if (scale === 'fit' || scale === 'fit-smaller') {
width = naturalWidth;
height = naturalHeight;
if (width > boxWidth ||
height > boxHeight ||
scale === 'fit-smaller') {
var boxRatio = boxWidth / boxHeight;
if (boxRatio > ratio) {
width = Math.floor(boxHeight * ratio);
height = boxHeight;
} else if (boxRatio < ratio) {
width = boxWidth;
height = Math.floor(boxWidth / ratio);
} else {
width = boxWidth;
height = boxHeight;
}
}
} else if (scale === 'fill') {
width = boxWidth;
height = boxHeight;
} else { // 'cover'
width = Math.max(boxHeight * ratio, boxWidth);
height = Math.max(width / ratio, boxHeight);
}
// Make adjustments based on image ratio
bgCSS.top = -(height - boxHeight) * alignY;
bgCSS.left = -(width - boxWidth) * alignX;
bgCSS.width = width;
bgCSS.height = height;
if (!this.options.bypassCss) {
this.$wrap
.css({width: boxWidth, height: boxHeight})
.find('>.backstretch-item').not('.deleteable')
.each(function () {
var $wrapper = $(this);
$wrapper.find('img,video,iframe')
.css(bgCSS);
});
}
var evt = $.Event('backstretch.resize', {
relatedTarget: this.$container[0]
});
this.$container.trigger(evt, this);
} catch(err) {
// IE7 seems to trigger resize before the image is loaded.
// This try/catch block is a hack to let it fail gracefully.
}
return this;
}
// Show the slide at a certain position
, show: function (newIndex, overrideOptions) {
// Validate index
if (Math.abs(newIndex) > this.images.length - 1) {
return;
}
// Vars
var that = this
, $oldItemWrapper = that.$wrap.find('>.backstretch-item').addClass('deleteable')
, oldVideoWrapper = that.videoWrapper
, evtOptions = { relatedTarget: that.$container[0] };
// Trigger the "before" event
that.$container.trigger($.Event('backstretch.before', evtOptions), [that, newIndex]);
// Set the new frame index
this.index = newIndex;
var selectedImage = that.images[newIndex];
// Pause the slideshow
clearTimeout(that._cycleTimeout);
// New image
delete that.videoWrapper; // Current item may not be a video
var isVideo = isVideoSource(selectedImage);
if (isVideo) {
that.videoWrapper = new VideoWrapper(selectedImage);
that.$item = that.videoWrapper.$video.css('pointer-events', 'none');
} else {
that.$item = $('<img />');
}
that.$itemWrapper = $('<div class="backstretch-item">')
.append(that.$item);
if (this.options.bypassCss) {
that.$itemWrapper.css({
'display': 'none'
});
} else {
that.$itemWrapper.css(styles.itemWrapper);
that.$item.css(styles.item);
}
that.$item.bind(isVideo ? 'canplay' : 'load', function (e) {
var $this = $(this)
, $wrapper = $this.parent()
, options = $wrapper.data('options');
if (overrideOptions) {
options = $.extend({}, options, overrideOptions);
}
var imgWidth = this.naturalWidth || this.videoWidth || this.width
, imgHeight = this.naturalHeight || this.videoHeight || this.height;
// Save the natural dimensions
$wrapper
.data('width', imgWidth)
.data('height', imgHeight);
var getOption = function (opt) {
return options[opt] !== undefined ?
options[opt] :
that.options[opt];
};
var transition = getOption('transition');
var transitionEasing = getOption('transitionEasing');
var transitionDuration = getOption('transitionDuration');
// Show the image, then delete the old one
var bringInNextImage = function () {
if (oldVideoWrapper) {
oldVideoWrapper.stop();
oldVideoWrapper.destroy();
}
$oldItemWrapper.remove();
// Resume the slideshow
if (!that.paused && that.images.length > 1) {
that.cycle();
}
// Now we can clear the background on the element, to spare memory
if (!that.options.bypassCss && !that.isBody) {
that.$container.css('background-image', 'none');
}
// Trigger the "after" and "show" events
// "show" is being deprecated
$(['after', 'show']).each(function () {
that.$container.trigger($.Event('backstretch.' + this, evtOptions), [that, newIndex]);
});
if (isVideo) {
that.videoWrapper.play();
}
};
if ((that.firstShow && !that.options.animateFirst) || !transitionDuration || !transition) {
// Avoid transition on first show or if there's no transitionDuration value
$wrapper.show();
bringInNextImage();
} else {
performTransition({
'new': $wrapper,
old: $oldItemWrapper,
transition: transition,
duration: transitionDuration,
easing: transitionEasing,
complete: bringInNextImage
});
}
that.firstShow = false;
// Resize
that.resize();
});
that.$itemWrapper.appendTo(that.$wrap);
that.$item.attr('alt', selectedImage.alt || '');
that.$itemWrapper.data('options', selectedImage);
if (!isVideo) {
that.$item.attr('src', selectedImage.url);
}
that._currentImage = selectedImage;
return that;
}
, current: function () {
return this.index;
}
, next: function () {
var args = Array.prototype.slice.call(arguments, 0);
args.unshift(this.index < this.images.length - 1 ? this.index + 1 : 0);
return this.show.apply(this, args);
}
, prev: function () {
var args = Array.prototype.slice.call(arguments, 0);
args.unshift(this.index === 0 ? this.images.length - 1 : this.index - 1);
return this.show.apply(this, args);
}
, pause: function () {
// Pause the slideshow
this.paused = true;
if (this.videoWrapper) {
this.videoWrapper.pause();
}
return this;
}
, resume: function () {
// Resume the slideshow
this.paused = false;
if (this.videoWrapper) {
this.videoWrapper.play();
}
this.cycle();
return this;
}
, cycle: function () {
// Start/resume the slideshow
if(this.images.length > 1) {
// Clear the timeout, just in case
clearTimeout(this._cycleTimeout);
var duration = (this._currentImage && this._currentImage.duration) || this.options.duration;
var isVideo = isVideoSource(this._currentImage);
var callNext = function () {
this.$item.off('.cycle');
// Check for paused slideshow
if (!this.paused) {
this.next();
}
};
// Special video handling
if (isVideo) {
// Leave video at last frame
if (!this._currentImage.loop) {
var lastFrameTimeout = 0;
this.$item
.on('playing.cycle', function () {
var player = $(this).data('player');
clearTimeout(lastFrameTimeout);
lastFrameTimeout = setTimeout(function () {
player.pause();
player.$video.trigger('ended');
}, (player.getDuration() - player.getCurrentTime()) * 1000);
})
.on('ended.cycle', function () {
clearTimeout(lastFrameTimeout);
});
}
// On error go to next
this.$item.on('error.cycle initerror.cycle', $.proxy(callNext, this));
}
if (isVideo && !this._currentImage.duration) {
// It's a video - playing until end
this.$item.on('ended.cycle', $.proxy(callNext, this));
} else {
// Cycling according to specified duration
this._cycleTimeout = setTimeout($.proxy(callNext, this), duration);
}
}
return this;
}
, destroy: function (preserveBackground) {
// Stop the resize events
$(window).off('resize.backstretch orientationchange.backstretch');
// Stop any videos
if (this.videoWrapper) {
this.videoWrapper.destroy();
}
// Clear the timeout
clearTimeout(this._cycleTimeout);
// Remove Backstretch
if(!preserveBackground) {
this.$wrap.remove();
}
this.$container.removeData('backstretch');
}
};
/**
* Video Abstraction Layer
*
* Static methods:
* > VideoWrapper.loadYoutubeAPI() -> Call in order to load the Youtube API.
* An 'youtube_api_load' event will be triggered on $(window) when the API is loaded.
*
* Generic:
* > player.type -> type of the video
* > player.video / player.$video -> contains the element holding the video
* > player.play() -> plays the video
* > player.pause() -> pauses the video
* > player.setCurrentTime(position) -> seeks to a position by seconds
*
* Youtube:
* > player.ytId will contain the youtube ID if the source is a youtube url
* > player.ytReady is a flag telling whether the youtube source is ready for playback
* */
var VideoWrapper = function () { this.init.apply(this, arguments); };
/**
* @param {Object} options
* @param {String|Array<String>|Array<{{src: String, type: String?}}>} options.url
* @param {Boolean} options.loop=false
* @param {Boolean?} options.mute=true
* @param {String?} options.poster
* loop, mute, poster
*/
VideoWrapper.prototype.init = function (options) {
var that = this;
var $video;
var setVideoElement = function () {
that.$video = $video;
that.video = $video[0];
};
// Determine video type
var videoType = 'video';
if (!(options.url instanceof Array) &&
YOUTUBE_REGEXP.test(options.url)) {
videoType = 'youtube';
}
that.type = videoType;
if (videoType === 'youtube') {
// Try to load the API in the meantime
VideoWrapper.loadYoutubeAPI();
that.ytId = options.url.match(YOUTUBE_REGEXP)[2];
var src = 'https://www.youtube.com/embed/' + that.ytId +
'?rel=0&autoplay=0&showinfo=0&controls=0&modestbranding=1' +
'&cc_load_policy=0&disablekb=1&iv_load_policy=3&loop=0' +
'&enablejsapi=1&origin=' + encodeURIComponent(window.location.origin);
that.__ytStartMuted = !!options.mute || options.mute === undefined;
$video = $('<iframe />')
.attr({ 'src_to_load': src })
.css({ 'border': 0, 'margin': 0, 'padding': 0 })
.data('player', that);
if (options.loop) {
$video.on('ended.loop', function () {
if (!that.__manuallyStopped) {
that.play();
}
});
}
that.ytReady = false;
setVideoElement();
if (window['YT']) {
that._initYoutube();
$video.trigger('initsuccess');
} else {
$(window).one('youtube_api_load', function () {
that._initYoutube();
$video.trigger('initsuccess');
});
}
}
else {
// Traditional <video> tag with multiple sources
$video = $('<video>')
.prop('autoplay', false)
.prop('controls', false)
.prop('loop', !!options.loop)
.prop('muted', !!options.mute || options.mute === undefined)
// Let the first frames be available before playback, as we do transitions
.prop('preload', 'auto')
.prop('poster', options.poster || '');
var sources = (options.url instanceof Array) ? options.url : [options.url];
for (var i = 0; i < sources.length; i++) {
var sourceItem = sources[i];
if (typeof(sourceItem) === 'string') {
sourceItem = { src: sourceItem };
}
$('<source>')
.attr('src', sourceItem.src)
// Make sure to not specify type if unknown -
// so the browser will try to autodetect.
.attr('type', sourceItem.type || null)
.appendTo($video);
}
if (!$video[0].canPlayType || !sources.length) {
$video.trigger('initerror');
} else {
$video.trigger('initsuccess');
}
setVideoElement();
}
};
VideoWrapper.prototype._initYoutube = function () {
var that = this;
var YT = window['YT'];
that.$video
.attr('src', that.$video.attr('src_to_load'))
.removeAttr('src_to_load');
// It won't init if it's not in the DOM, so we emulate that
var hasParent = !!that.$video[0].parentNode;
if (!hasParent) {
var $tmpParent = $('<div>').css('display', 'none !important').appendTo(document.body);
that.$video.appendTo($tmpParent);
}
var player = new YT.Player(that.video, {
events: {
'onReady': function () {
if (that.__ytStartMuted) {
player.mute();
}
if (!hasParent) {
// Restore parent to old state - without interrupting any changes
if (that.$video[0].parentNode === $tmpParent[0]) {
that.$video.detach();
}
$tmpParent.remove();
}
that.ytReady = true;
that._updateYoutubeSize();
that.$video.trigger('canplay');
},
'onStateChange': function (event) {
switch (event.data) {
case YT.PlayerState.PLAYING:
that.$video.trigger('playing');
break;
case YT.PlayerState.ENDED:
that.$video.trigger('ended');
break;
case YT.PlayerState.PAUSED:
that.$video.trigger('pause');
break;
case YT.PlayerState.BUFFERING:
that.$video.trigger('waiting');
break;
case YT.PlayerState.CUED:
that.$video.trigger('canplay');
break;
}
},
'onPlaybackQualityChange': function () {
that._updateYoutubeSize();
that.$video.trigger('resize');
},
'onError': function (err) {
that.hasError = true;
that.$video.trigger({ 'type': 'error', 'error': err });
}
}
});
that.ytPlayer = player;
return that;
};
VideoWrapper.prototype._updateYoutubeSize = function () {
var that = this;
switch (that.ytPlayer.getPlaybackQuality() || 'medium') {
case 'small':
that.video.videoWidth = 426;
that.video.videoHeight = 240;
break;
case 'medium':
that.video.videoWidth = 640;
that.video.videoHeight = 360;
break;
default:
case 'large':
that.video.videoWidth = 854;
that.video.videoHeight = 480;
break;
case 'hd720':
that.video.videoWidth = 1280;
that.video.videoHeight = 720;
break;
case 'hd1080':
that.video.videoWidth = 1920;
that.video.videoHeight = 1080;
break;
case 'highres':
that.video.videoWidth = 2560;
that.video.videoHeight = 1440;
break;
}
return that;
};
VideoWrapper.prototype.play = function () {
var that = this;
that.__manuallyStopped = false;
if (that.type === 'youtube') {
if (that.ytReady) {
that.$video.trigger('play');
that.ytPlayer.playVideo();
}
} else {
that.video.play();
}
return that;
};
VideoWrapper.prototype.pause = function () {
var that = this;
that.__manuallyStopped = false;
if (that.type === 'youtube') {
if (that.ytReady) {
that.ytPlayer.pauseVideo();
}
} else {
that.video.pause();
}
return that;
};
VideoWrapper.prototype.stop = function () {
var that = this;
that.__manuallyStopped = true;
if (that.type === 'youtube') {
if (that.ytReady) {
that.ytPlayer.pauseVideo();
that.ytPlayer.seekTo(0);
}
} else {
that.video.pause();
that.video.currentTime = 0;
}
return that;
};
VideoWrapper.prototype.destroy = function () {
var that = this;
if (that.ytPlayer) {
that.ytPlayer.destroy();
}
that.$video.remove();
return that;
};
VideoWrapper.prototype.getCurrentTime = function (seconds) {
var that = this;
if (that.type === 'youtube') {
if (that.ytReady) {
return that.ytPlayer.getCurrentTime();
}
} else {
return that.video.currentTime;
}
return 0;
};
VideoWrapper.prototype.setCurrentTime = function (seconds) {
var that = this;
if (that.type === 'youtube') {
if (that.ytReady) {
that.ytPlayer.seekTo(seconds, true);
}
} else {
that.video.currentTime = seconds;
}
return that;
};
VideoWrapper.prototype.getDuration = function () {
var that = this;
if (that.type === 'youtube') {
if (that.ytReady) {
return that.ytPlayer.getDuration();
}
} else {
return that.video.duration;
}
return 0;
};
/**
* This will load the youtube API (if not loaded yet)
* Use $(window).one('youtube_api_load', ...) to listen for API loaded event
*/
VideoWrapper.loadYoutubeAPI = function () {
if (window['YT']) {
return;
}
if (!$('script[src*=www\\.youtube\\.com\\/iframe_api]').length) {
$('<script type="text/javascript" src="https://www.youtube.com/iframe_api">').appendTo('body');
}
var ytAPILoadInt = setInterval(function () {
if (window['YT'] && window['YT'].loaded) {
$(window).trigger('youtube_api_load');
clearTimeout(ytAPILoadInt);
}
}, 50);
};
var getDeviceOrientation = function () {
if ('matchMedia' in window) {
if (window.matchMedia("(orientation: portrait)").matches) {
return 'portrait';
} else if (window.matchMedia("(orientation: landscape)").matches) {
return 'landscape';
}
}
if (screen.height > screen.width) {
return 'portrait';
}
// Even square devices have orientation,
// but a desktop browser may be too old for `matchMedia`.
// Defaulting to `landscape` for the VERY rare case of a square desktop screen is good enough.
return 'landscape';
};
var getWindowOrientation = function () {
if (window.innerHeight > window.innerWidth) {
return 'portrait';
}
if (window.innerWidth > window.innerHeight) {
return 'landscape';
}
return 'square';
};
/* SUPPORTS FIXED POSITION?
*
* Based on code from jQuery Mobile 1.1.0
* http://jquerymobile.com/
*
* In a nutshell, we need to figure out if fixed positioning is supported.
* Unfortunately, this is very difficult to do on iOS, and usually involves
* injecting content, scrolling the page, etc.. It's ugly.
* jQuery Mobile uses this workaround. It's not ideal, but works.
*
* Modified to detect IE6
* ========================= */
var supportsFixedPosition = (function () {
var ua = navigator.userAgent
, platform = navigator.platform
// Rendering engine is Webkit, and capture major version
, wkmatch = ua.match( /AppleWebKit\/([0-9]+)/ )
, wkversion = !!wkmatch && wkmatch[ 1 ]
, ffmatch = ua.match( /Fennec\/([0-9]+)/ )
, ffversion = !!ffmatch && ffmatch[ 1 ]
, operammobilematch = ua.match( /Opera Mobi\/([0-9]+)/ )
, omversion = !!operammobilematch && operammobilematch[ 1 ]
, iematch = ua.match( /MSIE ([0-9]+)/ )
, ieversion = !!iematch && iematch[ 1 ];
return !(
// iOS 4.3 and older : Platform is iPhone/Pad/Touch and Webkit version is less than 534 (ios5)
((platform.indexOf( "iPhone" ) > -1 || platform.indexOf( "iPad" ) > -1 || platform.indexOf( "iPod" ) > -1 ) && wkversion && wkversion < 534) ||
// Opera Mini
(window.operamini && ({}).toString.call( window.operamini ) === "[object OperaMini]") ||
(operammobilematch && omversion < 7458) ||
//Android lte 2.1: Platform is Android and Webkit version is less than 533 (Android 2.2)
(ua.indexOf( "Android" ) > -1 && wkversion && wkversion < 533) ||
// Firefox Mobile before 6.0 -
(ffversion && ffversion < 6) ||
// WebOS less than 3
("palmGetResource" in window && wkversion && wkversion < 534) ||
// MeeGo
(ua.indexOf( "MeeGo" ) > -1 && ua.indexOf( "NokiaBrowser/8.5.0" ) > -1) ||
// IE6
(ieversion && ieversion <= 6)
);
}());
}(jQuery, window));