PageSourceSearch

https://typemoon.com/js/jquery.sliderPro.js

js typemoon.com collected 2026-09-24 09:18:15 UTC 192,734 bytes, 5,955 lines download raw bytes

1/*!
2*  - v1.2.5
3* Homepage: http://bqworks.com/slider-pro/
4* Author: bqworks
5* Author URL: http://bqworks.com/
6*/
7;(function( window, $ ) {
8
9	"use strict";
10
11	// Static methods for Slider Pro
12	$.SliderPro = {
13
14		// List of added modules
15		modules: [],
16
17		// Add a module by extending the core prototype
18		addModule: function( name, module ) {
19			this.modules.push( name );
20			$.extend( SliderPro.prototype, module );
21		}
22	};
23
24	// namespace
25	var NS = $.SliderPro.namespace = 'SliderPro';
26
27	var SliderPro = function( instance, options ) {
28
29		// Reference to the slider instance
30		this.instance = instance;
31
32		// Reference to the slider jQuery element
33		this.$slider = $( this.instance );
34
35		// Reference to the slides (sp-slides) jQuery element
36		this.$slides = null;
37
38		// Reference to the mask (sp-mask) jQuery element
39		this.$slidesMask = null;
40
41		// Reference to the slides (sp-slides-container) jQuery element
42		this.$slidesContainer = null;
43
44		// Array of SliderProSlide objects, ordered by their DOM index
45		this.slides = [];
46
47		// Array of SliderProSlide objects, ordered by their left/top position in the slider.
48		// This will be updated continuously if the slider is loopable.
49		this.slidesOrder = [];
50
51		// Holds the options passed to the slider when it was instantiated
52		this.options = options;
53
54		// Holds the final settings of the slider after merging the specified
55		// ones with the default ones.
56		this.settings = {};
57
58		// Another reference to the settings which will not be altered by breakpoints or by other means
59		this.originalSettings = {};
60
61		// Reference to the original 'gotoSlide' method
62		this.originalGotoSlide = null;
63
64		// The index of the currently selected slide (starts with 0)
65		this.selectedSlideIndex = 0;
66
67		// The index of the previously selected slide
68		this.previousSlideIndex = 0;
69
70		// Indicates the position of the slide considered to be in the middle.
71		// If there are 5 slides (0, 1, 2, 3, 4) the middle position will be 2.
72		// If there are 6 slides (0, 1, 2, 3, 4, 5) the middle position will be approximated to 2.
73		this.middleSlidePosition = 0;
74
75		// Indicates the type of supported transition (CSS3 2D, CSS3 3D or JavaScript)
76		this.supportedAnimation = null;
77
78		// Indicates the required vendor prefix for CSS (i.e., -webkit, -moz, etc.)
79		this.vendorPrefix = null;
80
81		// Indicates the name of the CSS transition's complete event (i.e., transitionend, webkitTransitionEnd, etc.)
82		this.transitionEvent = null;
83
84		// Indicates the 'left' or 'top' position
85		this.positionProperty = null;
86
87		// Indicates if the current browser is IE
88		this.isIE = null;
89
90		// The position of the slides container
91		this.slidesPosition = 0;
92
93		// The width of the individual slide
94		this.slideWidth = 0;
95
96		// The height of the individual slide
97		this.slideHeight = 0;
98
99		// The width or height, depending on the orientation, of the individual slide
100		this.slideSize = 0;
101
102		// Reference to the old slide width, used to check if the width has changed
103		this.previousSlideWidth = 0;
104
105		// Reference to the old slide height, used to check if the height has changed
106		this.previousSlideHeight = 0;
107		
108		// Reference to the old window width, used to check if the window width has changed
109		this.previousWindowWidth = 0;
110		
111		// Reference to the old window height, used to check if the window height has changed
112		this.previousWindowHeight = 0;
113
114		// The distance from the margin of the slider to the left/top of the selected slide.
115		// This is useful in calculating the position of the selected slide when there are 
116		// more visible slides.
117		this.visibleOffset = 0;
118
119		// Property used for deferring the resizing of the slider
120		this.allowResize = true;
121
122		// Unique ID to be used for event listening
123		this.uniqueId = new Date().valueOf();
124
125		// Stores size breakpoints
126		this.breakpoints = [];
127
128		// Indicates the current size breakpoint
129		this.currentBreakpoint = -1;
130
131		// An array of shuffled indexes, based on which the slides will be shuffled
132		this.shuffledIndexes = [];
133
134		// Initialize the slider
135		this._init();
136	};
137
138	SliderPro.prototype = {
139
140		// The starting place for the slider
141		_init: function() {
142			var that = this;
143
144			this.supportedAnimation = SliderProUtils.getSupportedAnimation();
145			this.vendorPrefix = SliderProUtils.getVendorPrefix();
146			this.transitionEvent = SliderProUtils.getTransitionEvent();
147			this.isIE = SliderProUtils.checkIE();
148
149			// Remove the 'sp-no-js' when the slider's JavaScript code starts running
150			this.$slider.removeClass( 'sp-no-js' );
151
152			// Add the 'ios' class if it's an iOS device
153			if ( window.navigator.userAgent.match( /(iPad|iPhone|iPod)/g ) ) {
154				this.$slider.addClass( 'ios' );
155			}
156
157			// Check if IE (older than 11) is used and add the version number as a class to the slider since
158			// older IE versions might need CSS tweaks.
159			var rmsie = /(msie) ([\w.]+)/,
160				ieVersion = rmsie.exec( window.navigator.userAgent.toLowerCase() );
161			
162			if ( this.isIE ) {
163				this.$slider.addClass( 'ie' );
164			}
165
166			if ( ieVersion !== null ) {
167				this.$slider.addClass( 'ie' + parseInt( ieVersion[2], 10 ) );
168			}
169
170			// Set up the slides containers
171			// slider-pro > sp-slides-container > sp-mask > sp-slides > sp-slide
172			this.$slidesContainer = $( '<div class="sp-slides-container"></div>' ).appendTo( this.$slider );
173			this.$slidesMask = $( '<div class="sp-mask"></div>' ).appendTo( this.$slidesContainer );
174			this.$slides = this.$slider.find( '.sp-slides' ).appendTo( this.$slidesMask );
175			this.$slider.find( '.sp-slide' ).appendTo( this.$slides );
176			
177			var modules = $.SliderPro.modules;
178
179			// Merge the modules' default settings with the core's default settings
180			if ( typeof modules !== 'undefined' ) {
181				for ( var i = 0; i < modules.length; i++ ) {
182					var defaults = modules[ i ].substring( 0, 1 ).toLowerCase() + modules[ i ].substring( 1 ) + 'Defaults';
183
184					if ( typeof this[ defaults ] !== 'undefined' ) {
185						$.extend( this.defaults, this[ defaults ] );
186					}
187				}
188			}
189
190			// Merge the specified setting with the default ones
191			this.settings = $.extend( {}, this.defaults, this.options );
192
193			// Initialize the modules
194			if ( typeof modules !== 'undefined' ) {
195				for ( var j = 0; j < modules.length; j++ ) {
196					if ( typeof this[ 'init' + modules[ j ] ] !== 'undefined' ) {
197						this[ 'init' + modules[ j ] ]();
198					}
199				}
200			}
201
202			// Keep a reference of the original settings and use it
203			// to restore the settings when the breakpoints are used.
204			this.originalSettings = $.extend( {}, this.settings );
205
206			// Get the reference to the 'gotoSlide' method
207			this.originalGotoSlide = this.gotoSlide;
208
209			// Parse the breakpoints object and store the values into an array,
210			// sorting them in ascending order based on the specified size.
211			if ( this.settings.breakpoints !== null ) {
212				for ( var sizes in this.settings.breakpoints ) {
213					this.breakpoints.push({ size: parseInt( sizes, 10 ), properties:this.settings.breakpoints[ sizes ] });
214				}
215
216				this.breakpoints = this.breakpoints.sort(function( a, b ) {
217					return a.size >= b.size ? 1: -1;
218				});
219			}
220
221			// Set which slide should be selected initially
222			this.selectedSlideIndex = this.settings.startSlide;
223
224			// Shuffle/randomize the slides
225			if ( this.settings.shuffle === true ) {
226				var slides = this.$slides.find( '.sp-slide' ),
227					shuffledSlides = [];
228
229				// Populate the 'shuffledIndexes' with index numbers
230				slides.each(function( index ) {
231					that.shuffledIndexes.push( index );
232				});
233
234				for ( var k = this.shuffledIndexes.length - 1; k > 0; k-- ) {
235					var l = Math.floor( Math.random() * ( k + 1 ) ),
236						temp = this.shuffledIndexes[ k ];
237
238					this.shuffledIndexes[ k ] = this.shuffledIndexes[ l ];
239					this.shuffledIndexes[ l ] = temp;
240				}
241
242				// Reposition the slides based on the order of the indexes in the
243				// 'shuffledIndexes' array
244				$.each( this.shuffledIndexes, function( index, element ) {
245					shuffledSlides.push( slides[ element ] );
246				});
247				
248				// Append the sorted slides to the slider
249				this.$slides.empty().append( shuffledSlides ) ;
250			}
251			
252			// Resize the slider when the browser window resizes.
253			// Also, deffer the resizing in order to not allow multiple
254			// resizes in a 200 milliseconds interval.
255			$( window ).on( 'resize.' + this.uniqueId + '.' + NS, function() {
256			
257				// Get the current width and height of the window
258				var newWindowWidth = $( window ).width(),
259					newWindowHeight = $( window ).height();
260				
261				// If the resize is not allowed yet or if the window size hasn't changed (this needs to be verified
262				// because in IE8 and lower the resize event is triggered whenever an element from the page changes
263				// its size) return early.
264				if ( that.allowResize === false ||
265					( that.previousWindowWidth === newWindowWidth && that.previousWindowHeight === newWindowHeight ) ) {
266					return;
267				}
268				
269				// Asign the new values for the window width and height
270				that.previousWindowWidth = newWindowWidth;
271				that.previousWindowHeight = newWindowHeight;
272			
273				that.allowResize = false;
274
275				setTimeout(function() {
276					that.resize();
277					that.allowResize = true;
278				}, 200 );
279			});
280
281			// Resize the slider when the 'update' method is called.
282			this.on( 'update.' + NS, function() {
283
284				// Reset the previous slide width
285				that.previousSlideWidth = 0;
286
287				// Some updates might require a resize
288				that.resize();
289			});
290
291			this.update();
292
293			// add the 'sp-selected' class to the initially selected slide
294			this.$slides.find( '.sp-slide' ).eq( this.selectedSlideIndex ).addClass( 'sp-selected' );
295
296			// Fire the 'init' event
297			this.trigger({ type: 'init' });
298			if ( $.isFunction( this.settings.init ) ) {
299				this.settings.init.call( this, { type: 'init' });
300			}
301		},
302
303		// Update the slider by checking for setting changes and for slides
304		// that weren't initialized yet.
305		update: function() {
306			var that = this;
307
308			// Check the current slider orientation and reset CSS that might have been
309			// added for a different orientation, since the orientation can be changed
310			// at runtime.
311			if ( this.settings.orientation === 'horizontal' ) {
312				this.$slider.removeClass( 'sp-vertical' ).addClass( 'sp-horizontal' );
313				this.$slider.css({ 'height': '', 'max-height': '' });
314				this.$slides.find( '.sp-slide' ).css( 'top', '' );
315			} else if ( this.settings.orientation === 'vertical' ) {
316				this.$slider.removeClass( 'sp-horizontal' ).addClass( 'sp-vertical' );
317				this.$slides.find( '.sp-slide' ).css( 'left', '' );
318			}
319
320			// Set the position that will be used to arrange elements, like the slides,
321			// based on the orientation.
322			this.positionProperty = this.settings.orientation === 'horizontal' ? 'left' : 'top';
323
324			// Reset the 'gotoSlide' method
325			this.gotoSlide = this.originalGotoSlide;
326
327			// Loop through the array of SliderProSlide objects and if a stored slide is found
328			// which is not in the DOM anymore, destroy that slide.
329			for ( var i = this.slides.length - 1; i >= 0; i-- ) {
330				if ( this.$slider.find( '.sp-slide[data-index="' + i + '"]' ).length === 0 ) {
331					var slide = this.slides[ i ];
332
333					slide.destroy();
334					this.slides.splice( i, 1 );
335				}
336			}
337
338			this.slidesOrder.length = 0;
339
340			// Loop through the list of slides and initialize newly added slides if any,
341			// and reset the index of each slide.
342			this.$slider.find( '.sp-slide' ).each(function( index ) {
343				var $slide = $( this );
344
345				if ( typeof $slide.attr( 'data-init' ) === 'undefined' ) {
346					that._createSlide( index, $slide );
347				} else {
348					that.slides[ index ].setIndex( index );
349				}
350
351				that.slidesOrder.push( index );
352			});
353
354			// Calculate the position/index of the middle slide
355			this.middleSlidePosition = parseInt( ( that.slidesOrder.length - 1 ) / 2, 10 );
356
357			// Arrange the slides in a loop
358			if ( this.settings.loop === true ) {
359				this._updateSlidesOrder();
360			}
361
362			// Fire the 'update' event
363			this.trigger({ type: 'update' });
364			if ( $.isFunction( this.settings.update ) ) {
365				this.settings.update.call( this, { type: 'update' } );
366			}
367		},
368
369		// Create a SliderProSlide instance for the slide passed as a jQuery element
370		_createSlide: function( index, element ) {
371			var that = this,
372				slide = new SliderProSlide( $( element ), index, this.settings );
373
374			this.slides.splice( index, 0, slide );
375		},
376
377		// Arrange the slide elements in a loop inside the 'slidesOrder' array
378		_updateSlidesOrder: function() {
379			var	slicedItems,
380				i,
381
382				// Calculate the distance between the selected element and the middle position
383				distance = $.inArray( this.selectedSlideIndex, this.slidesOrder ) - this.middleSlidePosition;
384
385			// If the distance is negative it means that the selected slider is before the middle position, so
386			// slides from the end of the array will be added at the beginning, in order to shift the selected slide
387			// forward.
388			// 
389			// If the distance is positive, slides from the beginning of the array will be added at the end.
390			if ( distance < 0 ) {
391				slicedItems = this.slidesOrder.splice( distance, Math.abs( distance ) );
392
393				for ( i = slicedItems.length - 1; i >= 0; i-- ) {
394					this.slidesOrder.unshift( slicedItems[ i ] );
395				}
396			} else if ( distance > 0 ) {
397				slicedItems = this.slidesOrder.splice( 0, distance );
398
399				for ( i = 0; i <= slicedItems.length - 1; i++ ) {
400					this.slidesOrder.push( slicedItems[ i ] );
401				}
402			}
403		},
404
405		// Set the left/top position of the slides based on their position in the 'slidesOrder' array
406		_updateSlidesPosition: function() {
407			var selectedSlidePixelPosition = parseInt( this.$slides.find( '.sp-slide' ).eq( this.selectedSlideIndex ).css( this.positionProperty ), 10 );
408
409			for ( var slideIndex = 0; slideIndex < this.slidesOrder.length; slideIndex++ ) {
410				var slide = this.$slides.find( '.sp-slide' ).eq( this.slidesOrder[ slideIndex ] );
411				slide.css( this.positionProperty, selectedSlidePixelPosition + ( slideIndex - this.middleSlidePosition  ) * ( this.slideSize + this.settings.slideDistance ) );
412			}
413		},
414
415		// Set the left/top position of the slides based on their position in the 'slidesOrder' array,
416		// and also set the position of the slides container.
417		_resetSlidesPosition: function() {
418			for ( var slideIndex = 0; slideIndex < this.slidesOrder.length; slideIndex++ ) {
419				var slide = this.$slides.find( '.sp-slide' ).eq( this.slidesOrder[ slideIndex ] );
420				slide.css( this.positionProperty, slideIndex * ( this.slideSize + this.settings.slideDistance ) );
421			}
422
423			var newSlidesPosition = - parseInt( this.$slides.find( '.sp-slide' ).eq( this.selectedSlideIndex ).css( this.positionProperty ), 10 ) + this.visibleOffset;
424			this._moveTo( newSlidesPosition, true );
425		},
426
427		// Called when the slider needs to resize
428		resize: function() {
429			var that = this;
430
431			// Check if the current window width is bigger than the biggest breakpoint
432			// and if necessary reset the properties to the original settings.
433			// 
434			// If the window width is smaller than a certain breakpoint, apply the settings specified
435			// for that breakpoint but only after merging them with the original settings
436			// in order to make sure that only the specified settings for the breakpoint are applied
437			if ( this.settings.breakpoints !== null && this.breakpoints.length > 0 ) {
438				if ( $( window ).width() > this.breakpoints[ this.breakpoints.length - 1 ].size && this.currentBreakpoint !== -1 ) {
439					this.currentBreakpoint = -1;
440					this._setProperties( this.originalSettings, false );
441				} else {
442					for ( var i = 0, n = this.breakpoints.length; i < n; i++ ) {
443						if ( $( window ).width() <= this.breakpoints[ i ].size ) {
444							if ( this.currentBreakpoint !== this.breakpoints[ i ].size ) {
445								var eventObject = { type: 'breakpointReach', size: this.breakpoints[ i ].size, settings: this.breakpoints[ i ].properties };
446								this.trigger( eventObject );
447								if ( $.isFunction( this.settings.breakpointReach ) )
448									this.settings.breakpointReach.call( this, eventObject );
449
450								this.currentBreakpoint = this.breakpoints[ i ].size;
451								var settings = $.extend( {}, this.originalSettings, this.breakpoints[ i ].properties );
452								this._setProperties( settings, false );
453								
454								return;
455							}
456
457							break;
458						}
459					}
460				}
461			}
462
463			// Set the width of the main slider container based on whether or not the slider is responsive,
464			// full width or full size
465			if ( this.settings.responsive === true ) {
466				if ( ( this.settings.forceSize === 'fullWidth' || this.settings.forceSize === 'fullWindow' ) &&
467					( this.settings.visibleSize === 'auto' || this.settings.visibleSize !== 'auto' && this.settings.orientation === 'vertical' )
468				) {
469					this.$slider.css( 'margin', 0 );
470					this.$slider.css({ 'width': $( window ).width(), 'max-width': '', 'marginLeft': - this.$slider.offset().left });
471				} else {
472					this.$slider.css({ 'width': '100%', 'max-width': this.settings.width, 'marginLeft': '' });
473				}
474			} else {
475				this.$slider.css({ 'width': this.settings.width });
476			}
477
478			// Calculate the aspect ratio of the slider
479			if ( this.settings.aspectRatio === -1 ) {
480				this.settings.aspectRatio = this.settings.width / this.settings.height;
481			}
482			
483			// Initially set the slide width to the size of the slider.
484			// Later, this will be set to less if there are multiple visible slides.
485			this.slideWidth = this.$slider.width();
486
487			// Set the height to the same size as the browser window if the slider is set to be 'fullWindow',
488			// or calculate the height based on the width and the aspect ratio.
489			if ( this.settings.forceSize === 'fullWindow' ) {
490				this.slideHeight = $( window ).height();
491			} else {
492				this.slideHeight = isNaN( this.settings.aspectRatio ) ? this.settings.height : this.slideWidth / this.settings.aspectRatio;
493			}
494
495			// Resize the slider only if the size of the slider has changed
496			// If it hasn't, return.
497			if ( this.previousSlideWidth !== this.slideWidth ||
498				this.previousSlideHeight !== this.slideHeight ||
499				this.settings.visibleSize !== 'auto' ||
500				this.$slider.outerWidth() > this.$slider.parent().width() ||
501				this.$slider.width() !== this.$slidesMask.width()
502			) {
503				this.previousSlideWidth = this.slideWidth;
504				this.previousSlideHeight = this.slideHeight;
505			} else {
506				return;
507			}
508
509			// The slide width or slide height is needed for several calculation, so create a reference to it
510			// based on the current orientation.
511			this.slideSize = this.settings.orientation === 'horizontal' ? this.slideWidth : this.slideHeight;
512			
513			// Initially set the visible size of the slides and the offset of the selected slide as if there is only
514			// on visible slide.
515			// If there will be multiple visible slides (when 'visibleSize' is different than 'auto'), these will
516			// be updated accordingly.
517			this.visibleSlidesSize = this.slideSize;
518			this.visibleOffset = 0;
519
520			// Loop through the existing slides and reset their size.
521			$.each( this.slides, function( index, element ) {
522				element.setSize( that.slideWidth, that.slideHeight );
523			});
524
525			// Set the initial size of the mask container to the size of an individual slide
526			this.$slidesMask.css({ 'width': this.slideWidth, 'height': this.slideHeight });
527
528			// Adjust the height if it's set to 'auto'
529			if ( this.settings.autoHeight === true ) {
530
531				// Delay the resizing of the height to allow for other resize handlers
532				// to execute first before calculating the final height of the slide
533				setTimeout( function() {
534					that._resizeHeight();
535				}, 1 );
536			} else {
537				this.$slidesMask.css( this.vendorPrefix + 'transition', '' );
538			}
539
540			// The 'visibleSize' option can be set to fixed or percentage size to make more slides
541			// visible at a time.
542			// By default it's set to 'auto'.
543			if ( this.settings.visibleSize !== 'auto' ) {
544				if ( this.settings.orientation === 'horizontal' ) {
545
546					// If the size is forced to full width or full window, the 'visibleSize' option will be
547					// ignored and the slider will become as wide as the browser window.
548					if ( this.settings.forceSize === 'fullWidth' || this.settings.forceSize === 'fullWindow' ) {
549						this.$slider.css( 'margin', 0 );
550						this.$slider.css({ 'width': $( window ).width(), 'max-width': '', 'marginLeft': - this.$slider.offset().left });
551					} else {
552						this.$slider.css({ 'width': this.settings.visibleSize, 'max-width': '100%', 'marginLeft': 0 });
553					}
554					
555					this.$slidesMask.css( 'width', this.$slider.width() );
556
557					this.visibleSlidesSize = this.$slidesMask.width();
558					this.visibleOffset = Math.round( ( this.$slider.width() - this.slideWidth ) / 2 );
559				} else {
560
561					// If the size is forced to full window, the 'visibleSize' option will be
562					// ignored and the slider will become as high as the browser window.
563					if ( this.settings.forceSize === 'fullWindow' ) {
564						this.$slider.css({ 'height': $( window ).height(), 'max-height': '' });
565					} else {
566						this.$slider.css({ 'height': this.settings.visibleSize, 'max-height': '100%' });
567					}
568
569					this.$slidesMask.css( 'height', this.$slider.height() );
570
571					this.visibleSlidesSize = this.$slidesMask.height();
572					this.visibleOffset = Math.round( ( this.$slider.height() - this.slideHeight ) / 2 );
573				}
574			}
575
576			this._resetSlidesPosition();
577
578			// Fire the 'sliderResize' event
579			this.trigger({ type: 'sliderResize' });
580			if ( $.isFunction( this.settings.sliderResize ) ) {
581				this.settings.sliderResize.call( this, { type: 'sliderResize' });
582			}
583		},
584
585		// Resize the height of the slider to the height of the selected slide.
586		// It's used when the 'autoHeight' option is set to 'true'.
587		_resizeHeight: function() {
588			var that = this,
589				selectedSlide = this.getSlideAt( this.selectedSlideIndex ),
590				size = selectedSlide.getSize();
591
592			selectedSlide.off( 'imagesLoaded.' + NS );
593			selectedSlide.on( 'imagesLoaded.' + NS, function( event ) {
594				if ( event.index === that.selectedSlideIndex ) {
595					var size = selectedSlide.getSize();
596					that._resizeHeightTo( size.height );
597				}
598			});
599
600			// If the selected slide contains images which are still loading,
601			// wait for the loading to complete and then request the size again.
602			if ( size !== 'loading' ) {
603				this._resizeHeightTo( size.height );
604			}
605		},
606
607		// Open the slide at the specified index
608		gotoSlide: function( index ) {
609			if ( index === this.selectedSlideIndex || typeof this.slides[ index ] === 'undefined' ) {
610				return;
611			}
612
613			var that = this;
614
615			this.previousSlideIndex = this.selectedSlideIndex;
616			this.selectedSlideIndex = index;
617
618			// Re-assign the 'sp-selected' class to the currently selected slide
619			this.$slides.find( '.sp-selected' ).removeClass( 'sp-selected' );
620			this.$slides.find( '.sp-slide' ).eq( this.selectedSlideIndex ).addClass( 'sp-selected' );
621
622			// If the slider is loopable reorder the slides to have the selected slide in the middle
623			// and update the slides' position.
624			if ( this.settings.loop === true ) {
625				this._updateSlidesOrder();
626				this._updateSlidesPosition();
627			}
628
629			// Adjust the height of the slider
630			if ( this.settings.autoHeight === true ) {
631				this._resizeHeight();
632			}
633
634			// Calculate the new position that the slides container need to take
635			var newSlidesPosition = - parseInt( this.$slides.find( '.sp-slide' ).eq( this.selectedSlideIndex ).css( this.positionProperty ), 10 ) + this.visibleOffset;
636
637			// Move the slides container to the new position
638			this._moveTo( newSlidesPosition, false, function() {
639				if ( that.settings.loop === true ) {
640					that._resetSlidesPosition();
641				}
642
643				// Fire the 'gotoSlideComplete' event
644				that.trigger({ type: 'gotoSlideComplete', index: index, previousIndex: that.previousSlideIndex });
645				if ( $.isFunction( that.settings.gotoSlideComplete ) ) {
646					that.settings.gotoSlideComplete.call( that, { type: 'gotoSlideComplete', index: index, previousIndex: that.previousSlideIndex } );
647				}
648			});
649
650			// Fire the 'gotoSlide' event
651			this.trigger({ type: 'gotoSlide', index: index, previousIndex: this.previousSlideIndex });
652			if ( $.isFunction( this.settings.gotoSlide ) ) {
653				this.settings.gotoSlide.call( this, { type: 'gotoSlide', index: index, previousIndex: this.previousSlideIndex } );
654			}
655		},
656
657		// Open the next slide
658		nextSlide: function() {
659			var index = ( this.selectedSlideIndex >= this.getTotalSlides() - 1 ) ? 0 : ( this.selectedSlideIndex + 1 );
660			this.gotoSlide( index );
661		},
662
663		// Open the previous slide
664		previousSlide: function() {
665			var index = this.selectedSlideIndex <= 0 ? ( this.getTotalSlides() - 1 ) : ( this.selectedSlideIndex - 1 );
666			this.gotoSlide( index );
667		},
668
669		// Move the slides container to the specified position.
670		// The movement can be instant or animated.
671		_moveTo: function( position, instant, callback ) {
672			var that = this,
673				css = {};
674
675			if ( position === this.slidesPosition ) {
676				return;
677			}
678			
679			this.slidesPosition = position;
680			
681			if ( ( this.supportedAnimation === 'css-3d' || this.supportedAnimation === 'css-2d' ) && this.isIE === false ) {
682				var transition,
683					left = this.settings.orientation === 'horizontal' ? position : 0,
684					top = this.settings.orientation === 'horizontal' ? 0 : position;
685
686				if ( this.supportedAnimation === 'css-3d' ) {
687					css[ this.vendorPrefix + 'transform' ] = 'translate3d(' + left + 'px, ' + top + 'px, 0)';
688				} else {
689					css[ this.vendorPrefix + 'transform' ] = 'translate(' + left + 'px, ' + top + 'px)';
690				}
691
692				if ( typeof instant !== 'undefined' && instant === true ) {
693					transition = '';
694				} else {
695					this.$slides.addClass( 'sp-animated' );
696					transition = this.vendorPrefix + 'transform ' + this.settings.slideAnimationDuration / 1000 + 's';
697
698					this.$slides.on( this.transitionEvent, function( event ) {
699						if ( event.target !== event.currentTarget ) {
700							return;
701						}
702
703						that.$slides.off( that.transitionEvent );
704						that.$slides.removeClass( 'sp-animated' );
705						
706						if ( typeof callback === 'function' ) {
707							callback();
708						}
709					});
710				}
711
712				css[ this.vendorPrefix + 'transition' ] = transition;
713
714				this.$slides.css( css );
715			} else {
716				css[ 'margin-' + this.positionProperty ] = position;
717
718				if ( typeof instant !== 'undefined' && instant === true ) {
719					this.$slides.css( css );
720				} else {
721					this.$slides.addClass( 'sp-animated' );
722					this.$slides.animate( css, this.settings.slideAnimationDuration, function() {
723						that.$slides.removeClass( 'sp-animated' );
724
725						if ( typeof callback === 'function' ) {
726							callback();
727						}
728					});
729				}
730			}
731		},
732
733		// Stop the movement of the slides
734		_stopMovement: function() {
735			var css = {};
736
737			if ( ( this.supportedAnimation === 'css-3d' || this.supportedAnimation === 'css-2d' ) && this.isIE === false) {
738
739				// Get the current position of the slides by parsing the 'transform' property
740				var	matrixString = this.$slides.css( this.vendorPrefix + 'transform' ),
741					matrixType = matrixString.indexOf( 'matrix3d' ) !== -1 ? 'matrix3d' : 'matrix',
742					matrixArray = matrixString.replace( matrixType, '' ).match( /-?[0-9\.]+/g ),
743					left = matrixType === 'matrix3d' ? parseInt( matrixArray[ 12 ], 10 ) : parseInt( matrixArray[ 4 ], 10 ),
744					top = matrixType === 'matrix3d' ? parseInt( matrixArray[ 13 ], 10 ) : parseInt( matrixArray[ 5 ], 10 );
745					
746				// Set the transform property to the value that the transform had when the function was called
747				if ( this.supportedAnimation === 'css-3d' ) {
748					css[ this.vendorPrefix + 'transform' ] = 'translate3d(' + left + 'px, ' + top + 'px, 0)';
749				} else {
750					css[ this.vendorPrefix + 'transform' ] = 'translate(' + left + 'px, ' + top + 'px)';
751				}
752
753				css[ this.vendorPrefix + 'transition' ] = '';
754
755				this.$slides.css( css );
756				this.$slides.off( this.transitionEvent );
757				this.slidesPosition = this.settings.orientation === 'horizontal' ? left : top;
758			} else {
759				this.$slides.stop();
760				this.slidesPosition = parseInt( this.$slides.css( 'margin-' + this.positionProperty ), 10 );
761			}
762
763			this.$slides.removeClass( 'sp-animated' );
764		},
765
766		// Resize the height of the slider to the specified value
767		_resizeHeightTo: function( height ) {
768			var that = this,
769				css = { 'height': height };
770
771			if ( this.supportedAnimation === 'css-3d' || this.supportedAnimation === 'css-2d' ) {
772				css[ this.vendorPrefix + 'transition' ] = 'height ' + this.settings.heightAnimationDuration / 1000 + 's';
773
774				this.$slidesMask.off( this.transitionEvent );
775				this.$slidesMask.on( this.transitionEvent, function( event ) {
776					if ( event.target !== event.currentTarget ) {
777						return;
778					}
779
780					that.$slidesMask.off( that.transitionEvent );
781
782					// Fire the 'resizeHeightComplete' event
783					that.trigger({ type: 'resizeHeightComplete' });
784					if ( $.isFunction( that.settings.resizeHeightComplete ) ) {
785						that.settings.resizeHeightComplete.call( that, { type: 'resizeHeightComplete' } );
786					}
787				});
788
789				this.$slidesMask.css( css );
790			} else {
791				this.$slidesMask.stop().animate( css, this.settings.heightAnimationDuration, function( event ) {
792					// Fire the 'resizeHeightComplete' event
793					that.trigger({ type: 'resizeHeightComplete' });
794					if ( $.isFunction( that.settings.resizeHeightComplete ) ) {
795						that.settings.resizeHeightComplete.call( that, { type: 'resizeHeightComplete' } );
796					}
797				});
798			}
799		},
800
801		// Destroy the slider instance
802		destroy: function() {
803			// Remove the stored reference to this instance
804			this.$slider.removeData( 'sliderPro' );
805			
806			// Clean the CSS
807			this.$slider.removeAttr( 'style' );
808			this.$slides.removeAttr( 'style' );
809
810			// Remove event listeners
811			this.off( 'update.' + NS );
812			$( window ).off( 'resize.' + this.uniqueId + '.' + NS );
813
814			// Destroy modules
815			var modules = $.SliderPro.modules;
816
817			if ( typeof modules !== 'undefined' ) {
818				for ( var i = 0; i < modules.length; i++ ) {
819					if ( typeof this[ 'destroy' + modules[ i ] ] !== 'undefined' ) {
820						this[ 'destroy' + modules[ i ] ]();
821					}
822				}
823			}
824
825			// Destroy all slides
826			$.each( this.slides, function( index, element ) {
827				element.destroy();
828			});
829
830			this.slides.length = 0;
831
832			// Move the slides to their initial position in the DOM and 
833			// remove the container elements created dynamically.
834			this.$slides.prependTo( this.$slider );
835			this.$slidesContainer.remove();
836		},
837
838		// Set properties on runtime
839		_setProperties: function( properties, store ) {
840			// Parse the properties passed as an object
841			for ( var prop in properties ) {
842				this.settings[ prop ] = properties[ prop ];
843
844				// Alter the original settings as well unless 'false' is passed to the 'store' parameter
845				if ( store !== false ) {
846					this.originalSettings[ prop ] = properties[ prop ];
847				}
848			}
849
850			this.update();
851		},
852
853		// Attach an event handler to the slider
854		on: function( type, callback ) {
855			return this.$slider.on( type, callback );
856		},
857
858		// Detach an event handler
859		off: function( type ) {
860			return this.$slider.off( type );
861		},
862
863		// Trigger an event on the slider
864		trigger: function( data ) {
865			return this.$slider.triggerHandler( data );
866		},
867
868		// Return the slide at the specified index
869		getSlideAt: function( index ) {
870			return this.slides[ index ];
871		},
872
873		// Return the index of the currently opened slide
874		getSelectedSlide: function() {
875			return this.selectedSlideIndex;
876		},
877
878		// Return the total amount of slides
879		getTotalSlides: function() {
880			return this.slides.length;
881		},
882
883		// The default options of the slider
884		defaults: {
885			// Width of the slide
886			width: 500,
887
888			// Height of the slide
889			height: 300,
890
891			// Indicates if the slider is responsive
892			responsive: true,
893
894			// The aspect ratio of the slider (width/height)
895			aspectRatio: -1,
896
897			// The scale mode for images (cover, contain, exact and none)
898			imageScaleMode: 'cover',
899
900			// Indicates if the image will be centered
901			centerImage: true,
902
903			// Indicates if the image can be scaled up more than its original size
904			allowScaleUp: true,
905
906			// Indicates if height of the slider will be adjusted to the
907			// height of the selected slide
908			autoHeight: false,
909
910			// Indicates the initially selected slide
911			startSlide: 0,
912
913			// Indicates if the slides will be shuffled
914			shuffle: false,
915
916			// Indicates whether the slides will be arranged horizontally
917			// or vertically. Can be set to 'horizontal' or 'vertical'.
918			orientation: 'horizontal',
919
920			// Indicates if the size of the slider will be forced to 'fullWidth' or 'fullWindow'
921			forceSize: 'none',
922
923			// Indicates if the slider will be loopable
924			loop: true,
925
926			// The distance between slides
927			slideDistance: 10,
928
929			// The duration of the slide animation
930			slideAnimationDuration: 700,
931
932			// The duration of the height animation
933			heightAnimationDuration: 700,
934
935			// Sets the size of the visible area, allowing the increase of it in order
936			// to make more slides visible.
937			// By default, only the selected slide will be visible. 
938			visibleSize: 'auto',
939
940			// Breakpoints for allowing the slider's options to be changed
941			// based on the size of the window.
942			breakpoints: null,
943
944			// Called when the slider is initialized
945			init: function() {},
946
947			// Called when the slider is updates
948			update: function() {},
949
950			// Called when the slider is resized
951			sliderResize: function() {},
952
953			// Called when a new slide is selected
954			gotoSlide: function() {},
955
956			// Called when the navigation to the newly selected slide is complete
957			gotoSlideComplete: function() {},
958
959			// Called when the height animation of the slider is complete
960			resizeHeightComplete: function() {},
961
962			// Called when a breakpoint is reached
963			breakpointReach: function() {}
964		}
965	};
966
967	var SliderProSlide = function( slide, index, settings ) {
968
969		// Reference to the slide jQuery element
970		this.$slide = slide;
971
972		// Reference to the main slide image
973		this.$mainImage = null;
974
975		// Reference to the container that will hold the main image
976		this.$imageContainer = null;
977
978		// Indicates whether the slide has a main image
979		this.hasMainImage = false;
980
981		// Indicates whether the main image is loaded
982		this.isMainImageLoaded = false;
983
984		// Indicates whether the main image is in the process of being loaded
985		this.isMainImageLoading = false;
986
987		// Indicates whether the slide has any image. There could be other images (i.e., in layers)
988		// besides the main slide image.
989		this.hasImages = false;
990
991		// Indicates if all the images in the slide are loaded
992		this.areImagesLoaded = false;
993
994		// The width and height of the slide
995		this.width = 0;
996		this.height = 0;
997
998		// Reference to the global settings of the slider
999		this.settings = settings;
1000
1001		// Set the index of the slide
1002		this.setIndex( index );
1003
1004		// Initialize the slide
1005		this._init();
1006	};
1007
1008	SliderProSlide.prototype = {
1009
1010		// The starting point for the slide
1011		_init: function() {
1012			var that = this;
1013
1014			// Mark the slide as initialized
1015			this.$slide.attr( 'data-init', true );
1016
1017			// Get the main slide image if there is one
1018			this.$mainImage = this.$slide.find( '.sp-image' ).length !== 0 ? this.$slide.find( '.sp-image' ) : null;
1019
1020			// If there is a main slide image, create a container for it and add the image to this container.
1021			// The container will allow the isolation of the image from the rest of the slide's content. This is
1022			// helpful when you want to show some content below the image and not cover it.
1023			if ( this.$mainImage !== null ) {
1024				this.hasMainImage = true;
1025
1026				this.$imageContainer = $( '<div class="sp-image-container"></div>' ).prependTo( this.$slide );
1027
1028				if ( this.$mainImage.parent( 'a' ).length !== 0 ) {
1029					this.$mainImage.parent( 'a' ).appendTo( this.$imageContainer );
1030				} else {
1031					this.$mainImage.appendTo( this.$imageContainer );
1032				}
1033			}
1034
1035			this.hasImages = this.$slide.find( 'img' ).length !== 0 ? true : false;
1036		},
1037
1038		// Set the size of the slide
1039		setSize: function( width, height ) {
1040			var that = this;
1041			
1042			this.width = width;
1043			this.height = this.settings.autoHeight === true ? 'auto' : height;
1044
1045			this.$slide.css({
1046				'width': this.width,
1047				'height': this.height
1048			});
1049
1050			if ( this.hasMainImage === true ) {
1051				this.$imageContainer.css({
1052					'width': this.width,
1053					'height': this.height
1054				});
1055
1056				// Resize the main image if it's loaded. If the 'data-src' attribute is present it means
1057				// that the image will be lazy-loaded
1058				if ( typeof this.$mainImage.attr( 'data-src' ) === 'undefined' ) {
1059					this.resizeMainImage();
1060				}
1061			}
1062		},
1063
1064		// Get the size (width and height) of the slide
1065		getSize: function() {
1066			var that = this,
1067				size;
1068
1069			// Check if all images have loaded, and if they have, return the size, else, return 'loading'
1070			if ( this.hasImages === true && this.areImagesLoaded === false && typeof this.$slide.attr( 'data-loading' ) === 'undefined' ) {
1071				this.$slide.attr( 'data-loading', true );
1072
1073				var status = SliderProUtils.checkImagesComplete( this.$slide, function() {
1074					that.areImagesLoaded = true;
1075					that.$slide.removeAttr( 'data-loading' );
1076					that.trigger({ type: 'imagesLoaded.' + NS, index: that.index });
1077				});
1078
1079				if ( status === 'complete' ) {
1080					size = this.calculateSize();
1081
1082					return {
1083						'width': size.width,
1084						'height': size.height
1085					};
1086				} else {
1087					return 'loading';
1088				}
1089			} else {
1090				size = this.calculateSize();
1091
1092				return {
1093					'width': size.width,
1094					'height': size.height
1095				};
1096			}
1097		},
1098
1099		// Calculate the width and height of the slide by going
1100		// through all the child elements and measuring their 'bottom'
1101		// and 'right' properties. The element with the biggest
1102		// 'right'/'bottom' property will determine the slide's
1103		// width/height.
1104		calculateSize: function() {
1105			var width = this.$slide.width(),
1106				height = this.$slide.height();
1107
1108			this.$slide.children().each(function( index, element ) {
1109				var child = $( element );
1110
1111				if ( child.is( ':hidden' ) === true ) {
1112					return;
1113				}
1114
1115				var	rect = element.getBoundingClientRect(),
1116					bottom = child.position().top + ( rect.bottom - rect.top ),
1117					right = child.position().left + ( rect.right - rect.left );
1118
1119				if ( bottom > height ) {
1120					height = bottom;
1121				}
1122
1123				if ( right > width ) {
1124					width = right;
1125				}
1126			});
1127
1128			return { width: width, height: height };
1129		},
1130
1131		// Resize the main image.
1132		// 
1133		// Call this when the slide resizes or when the main image has changed to a different image.
1134		resizeMainImage: function( isNewImage ) {
1135			var that = this;
1136
1137			// If the main image has changed, reset the 'flags'
1138			if ( isNewImage === true ) {
1139				this.isMainImageLoaded = false;
1140				this.isMainImageLoading = false;
1141			}
1142
1143			// If the image was not loaded yet and it's not in the process of being loaded, load it
1144			if ( this.isMainImageLoaded === false && this.isMainImageLoading === false ) {
1145				this.isMainImageLoading = true;
1146
1147				SliderProUtils.checkImagesComplete( this.$mainImage, function() {
1148					that.isMainImageLoaded = true;
1149					that.isMainImageLoading = false;
1150					that.resizeMainImage();
1151					that.trigger({ type: 'imagesLoaded.' + NS, index: that.index });
1152				});
1153
1154				return;
1155			}
1156
1157			if ( this.settings.allowScaleUp === false ) {
1158				this.$mainImage.css({ 'width': '', 'height': '', 'maxWidth': '', 'maxHeight': '' });
1159
1160				var naturalWidth = this.$mainImage.width(),
1161					naturalHeight = this.$mainImage.height();
1162
1163				this.$mainImage.css({ 'maxWidth': naturalWidth, 'maxHeight': naturalHeight });
1164			}
1165
1166			// After the main image has loaded, resize it
1167			if ( this.settings.autoHeight === true ) {
1168				this.$mainImage.css({ width: '100%', height: 'auto' });
1169			} else {
1170				if ( this.settings.imageScaleMode === 'cover' ) {
1171					if ( this.$mainImage.width() / this.$mainImage.height() <= this.width / this.height ) {
1172						this.$mainImage.css({ width: '100%', height: 'auto' });
1173					} else {
1174						this.$mainImage.css({ width: 'auto', height: '100%' });
1175					}
1176				} else if ( this.settings.imageScaleMode === 'contain' ) {
1177					if ( this.$mainImage.width() / this.$mainImage.height() >= this.width / this.height ) {
1178						this.$mainImage.css({ width: '100%', height: 'auto' });
1179					} else {
1180						this.$mainImage.css({ width: 'auto', height: '100%' });
1181					}
1182				} else if ( this.settings.imageScaleMode === 'exact' ) {
1183					this.$mainImage.css({ width: '100%', height: '100%' });
1184				}
1185			}
1186
1187			if ( this.settings.centerImage === true ) {
1188				this.$mainImage.css({ 'marginLeft': ( this.$imageContainer.width() - this.$mainImage.width() ) * 0.5, 'marginTop': ( this.$imageContainer.height() - this.$mainImage.height() ) * 0.5 });
1189			}
1190		},
1191
1192		// Destroy the slide
1193		destroy: function() {
1194			// Clean the slide element from attached styles and data
1195			this.$slide.removeAttr( 'style' );
1196			this.$slide.removeAttr( 'data-init' );
1197			this.$slide.removeAttr( 'data-index' );
1198			this.$slide.removeAttr( 'data-loaded' );
1199
1200			// If there is a main image, remove its container
1201			if ( this.hasMainImage === true ) {
1202				this.$slide.find( '.sp-image' )
1203					.removeAttr( 'style' )
1204					.appendTo( this.$slide );
1205
1206				this.$slide.find( '.sp-image-container' ).remove();
1207			}
1208		},
1209
1210		// Return the index of the slide
1211		getIndex: function() {
1212			return this.index;
1213		},
1214
1215		// Set the index of the slide
1216		setIndex: function( index ) {
1217			this.index = index;
1218			this.$slide.attr( 'data-index', this.index );
1219		},
1220
1221		// Attach an event handler to the slide
1222		on: function( type, callback ) {
1223			return this.$slide.on( type, callback );
1224		},
1225
1226		// Detach an event handler to the slide
1227		off: function( type ) {
1228			return this.$slide.off( type );
1229		},
1230
1231		// Trigger an event on the slide
1232		trigger: function( data ) {
1233			return this.$slide.triggerHandler( data );
1234		}
1235	};
1236
1237	window.SliderPro = SliderPro;
1238	window.SliderProSlide = SliderProSlide;
1239
1240	$.fn.sliderPro = function( options ) {
1241		var args = Array.prototype.slice.call( arguments, 1 );
1242
1243		return this.each(function() {
1244			// Instantiate the slider or alter it
1245			if ( typeof $( this ).data( 'sliderPro' ) === 'undefined' ) {
1246				var newInstance = new SliderPro( this, options );
1247
1248				// Store a reference to the instance created
1249				$( this ).data( 'sliderPro', newInstance );
1250			} else if ( typeof options !== 'undefined' ) {
1251				var	currentInstance = $( this ).data( 'sliderPro' );
1252
1253				// Check the type of argument passed
1254				if ( typeof currentInstance[ options ] === 'function' ) {
1255					currentInstance[ options ].apply( currentInstance, args );
1256				} else if ( typeof currentInstance.settings[ options ] !== 'undefined' ) {
1257					var obj = {};
1258					obj[ options ] = args[ 0 ];
1259					currentInstance._setProperties( obj );
1260				} else if ( typeof options === 'object' ) {
1261					currentInstance._setProperties( options );
1262				} else {
1263					$.error( options + ' does not exist in sliderPro.' );
1264				}
1265			}
1266		});
1267	};
1268
1269	// Contains useful utility functions
1270	var SliderProUtils = {
1271
1272		// Indicates what type of animations are supported in the current browser
1273		// Can be CSS 3D, CSS 2D or JavaScript
1274		supportedAnimation: null,
1275
1276		// Indicates the required vendor prefix for the current browser
1277		vendorPrefix: null,
1278
1279		// Indicates the name of the transition's complete event for the current browser
1280		transitionEvent: null,
1281
1282		// Indicates if the current browser is Internet Explorer (any version)
1283		isIE: null,
1284
1285		// Check whether CSS3 3D or 2D transforms are supported. If they aren't, use JavaScript animations
1286		getSupportedAnimation: function() {
1287			if ( this.supportedAnimation !== null ) {
1288				return this.supportedAnimation;
1289			}
1290
1291			var element = document.body || document.documentElement,
1292				elementStyle = element.style,
1293				isCSSTransitions = typeof elementStyle.transition !== 'undefined' ||
1294									typeof elementStyle.WebkitTransition !== 'undefined' ||
1295									typeof elementStyle.MozTransition !== 'undefined' ||
1296									typeof elementStyle.OTransition !== 'undefined';
1297
1298			if ( isCSSTransitions === true ) {
1299				var div = document.createElement( 'div' );
1300
1301				// Check if 3D transforms are supported
1302				if ( typeof div.style.WebkitPerspective !== 'undefined' || typeof div.style.perspective !== 'undefined' ) {
1303					this.supportedAnimation = 'css-3d';
1304				}
1305
1306				// Additional checks for Webkit
1307				if ( this.supportedAnimation === 'css-3d' && typeof div.styleWebkitPerspective !== 'undefined' ) {
1308					var style = document.createElement( 'style' );
1309					style.textContent = '@media (transform-3d),(-webkit-transform-3d){#test-3d{left:9px;position:absolute;height:5px;margin:0;padding:0;border:0;}}';
1310					document.getElementsByTagName( 'head' )[0].appendChild( style );
1311
1312					div.id = 'test-3d';
1313					document.body.appendChild( div );
1314
1315					if ( ! ( div.offsetLeft === 9 && div.offsetHeight === 5 ) ) {
1316						this.supportedAnimation = null;
1317					}
1318
1319					style.parentNode.removeChild( style );
1320					div.parentNode.removeChild( div );
1321				}
1322
1323				// If CSS 3D transforms are not supported, check if 2D transforms are supported
1324				if ( this.supportedAnimation === null && ( typeof div.style['-webkit-transform'] !== 'undefined' || typeof div.style.transform !== 'undefined' ) ) {
1325					this.supportedAnimation = 'css-2d';
1326				}
1327			} else {
1328				this.supportedAnimation = 'javascript';
1329			}
1330			
1331			return this.supportedAnimation;
1332		},
1333
1334		// Check what vendor prefix should be used in the current browser
1335		getVendorPrefix: function() {
1336			if ( this.vendorPrefix !== null ) {
1337				return this.vendorPrefix;
1338			}
1339
1340			var div = document.createElement( 'div' ),
1341				prefixes = [ 'Webkit', 'Moz', 'ms', 'O' ];
1342			
1343			if ( 'transform' in div.style ) {
1344				this.vendorPrefix = '';
1345				return this.vendorPrefix;
1346			}
1347			
1348			for ( var i = 0; i < prefixes.length; i++ ) {
1349				if ( ( prefixes[ i ] + 'Transform' ) in div.style ) {
1350					this.vendorPrefix = '-' + prefixes[ i ].toLowerCase() + '-';
1351					break;
1352				}
1353			}
1354			
1355			return this.vendorPrefix;
1356		},
1357
1358		// Check the name of the transition's complete event in the current browser
1359		getTransitionEvent: function() {
1360			if ( this.transitionEvent !== null ) {
1361				return this.transitionEvent;
1362			}
1363
1364			var div = document.createElement( 'div' ),
1365				transitions = {
1366					'transition': 'transitionend',
1367					'WebkitTransition': 'webkitTransitionEnd',
1368					'MozTransition': 'transitionend',
1369					'OTransition': 'oTransitionEnd'
1370				};
1371
1372			for ( var transition in transitions ) {
1373				if ( transition in div.style ) {
1374					this.transitionEvent = transitions[ transition ];
1375					break;
1376				}
1377			}
1378
1379			return this.transitionEvent;
1380		},
1381
1382		// If a single image is passed, check if it's loaded.
1383		// If a different element is passed, check if there are images
1384		// inside it, and check if these images are loaded.
1385		checkImagesComplete: function( target, callback ) {
1386			var that = this,
1387
1388				// Check the initial status of the image(s)
1389				status = this.checkImagesStatus( target );
1390
1391			// If there are loading images, wait for them to load.
1392			// If the images are loaded, call the callback function directly.
1393			if ( status === 'loading' ) {
1394				var checkImages = setInterval(function() {
1395					status = that.checkImagesStatus( target );
1396
1397					if ( status === 'complete' ) {
1398						clearInterval( checkImages );
1399
1400						if ( typeof callback === 'function' ) {
1401							callback();
1402						}
1403					}
1404				}, 100 );
1405			} else if ( typeof callback === 'function' ) {
1406				callback();
1407			}
1408
1409			return status;
1410		},
1411
1412		checkImagesStatus: function( target ) {
1413			var status = 'complete';
1414
1415			if ( target.is( 'img' ) && target[0].complete === false ) {
1416				status = 'loading';
1417			} else {
1418				target.find( 'img' ).each(function( index ) {
1419					var image = $( this )[0];
1420
1421					if ( image.complete === false ) {
1422						status = 'loading';
1423					}
1424				});
1425			}
1426
1427			return status;
1428		},
1429
1430		checkIE: function() {
1431			if ( this.isIE !== null ) {
1432				return this.isIE;
1433			}
1434
1435			var userAgent = window.navigator.userAgent,
1436				msie = userAgent.indexOf( 'MSIE' );
1437
1438			if ( userAgent.indexOf( 'MSIE' ) !== -1 || userAgent.match( /Trident.*rv\:11\./ ) ) {
1439				this.isIE = true;
1440			} else {
1441				this.isIE = false;
1442			}
1443
1444			return this.isIE;
1445		}
1446	};
1447
1448	window.SliderProUtils = SliderProUtils;
1449
1450})( window, jQuery );
1451
1452// Thumbnails module for Slider Pro.
1453// 
1454// Adds the possibility to create a thumbnail scroller, each thumbnail
1455// corresponding to a slide.
1456;(function( window, $ ) {
1457
1458	"use strict";
1459
1460	var NS = 'Thumbnails.' + $.SliderPro.namespace;
1461
1462	var Thumbnails = {
1463
1464		// Reference to the thumbnail scroller 
1465		$thumbnails: null,
1466
1467		// Reference to the container of the thumbnail scroller
1468		$thumbnailsContainer: null,
1469
1470		// List of Thumbnail objects
1471		thumbnails: null,
1472
1473		// Index of the selected thumbnail
1474		selectedThumbnailIndex: 0,
1475
1476		// Total size (width or height, depending on the orientation) of the thumbnails
1477		thumbnailsSize: 0,
1478
1479		// Size of the thumbnail's container
1480		thumbnailsContainerSize: 0,
1481
1482		// The position of the thumbnail scroller inside its container
1483		thumbnailsPosition: 0,
1484
1485		// Orientation of the thumbnails
1486		thumbnailsOrientation: null,
1487
1488		// Indicates the 'left' or 'top' position based on the orientation of the thumbnails
1489		thumbnailsPositionProperty: null,
1490
1491		// Indicates if there are thumbnails in the slider
1492		isThumbnailScroller: false,
1493
1494		initThumbnails: function() {
1495			var that = this;
1496
1497			this.thumbnails = [];
1498
1499			this.on( 'update.' + NS, $.proxy( this._thumbnailsOnUpdate, this ) );
1500			this.on( 'sliderResize.' + NS, $.proxy( this._thumbnailsOnResize, this ) );
1501			this.on( 'gotoSlide.' + NS, function( event ) {
1502				that._gotoThumbnail( event.index );
1503			});
1504		},
1505
1506		// Called when the slider is updated
1507		_thumbnailsOnUpdate: function() {
1508			var that = this;
1509
1510			if ( this.$slider.find( '.sp-thumbnail' ).length === 0 && this.thumbnails.length === 0 ) {
1511				this.isThumbnailScroller = false;
1512				return;
1513			}
1514
1515			this.isThumbnailScroller = true;
1516
1517			// Create the container of the thumbnail scroller, if it wasn't created yet
1518			if ( this.$thumbnailsContainer === null ) {
1519				this.$thumbnailsContainer = $( '<div class="sp-thumbnails-container"></div>' ).insertAfter( this.$slidesContainer );
1520			}
1521
1522			// If the thumbnails' main container doesn't exist, create it, and get a reference to it
1523			if ( this.$thumbnails === null ) {
1524				if ( this.$slider.find( '.sp-thumbnails' ).length !== 0 ) {
1525					this.$thumbnails = this.$slider.find( '.sp-thumbnails' ).appendTo( this.$thumbnailsContainer );
1526
1527					// Shuffle/randomize the thumbnails
1528					if ( this.settings.shuffle === true ) {
1529						var thumbnails = this.$thumbnails.find( '.sp-thumbnail' ),
1530							shuffledThumbnails = [];
1531
1532						// Reposition the thumbnails based on the order of the indexes in the
1533						// 'shuffledIndexes' array
1534						$.each( this.shuffledIndexes, function( index, element ) {
1535							var $thumbnail = $( thumbnails[ element ] );
1536
1537							if ( $thumbnail.parent( 'a' ).length !== 0 ) {
1538								$thumbnail = $thumbnail.parent( 'a' );
1539							}
1540
1541							shuffledThumbnails.push( $thumbnail );
1542						});
1543						
1544						// Append the sorted thumbnails to the thumbnail scroller
1545						this.$thumbnails.empty().append( shuffledThumbnails ) ;
1546					}
1547				} else {
1548					this.$thumbnails = $( '<div class="sp-thumbnails"></div>' ).appendTo( this.$thumbnailsContainer );
1549				}
1550			}
1551
1552			// Check if there are thumbnails inside the slides and move them in the thumbnails container
1553			this.$slides.find( '.sp-thumbnail' ).each( function( index ) {
1554				var $thumbnail = $( this ),
1555					thumbnailIndex = $thumbnail.parents( '.sp-slide' ).index(),
1556					lastThumbnailIndex = that.$thumbnails.find( '.sp-thumbnail' ).length - 1;
1557
1558				if ( $thumbnail.parent( 'a' ).length !== 0 ) {
1559					$thumbnail = $thumbnail.parent( 'a' );
1560				}
1561
1562				// If the index of the slide that contains the thumbnail is greater than the total number
1563				// of thumbnails from the thumbnails container, position the thumbnail at the end.
1564				// Otherwise, add the thumbnails at the corresponding position.
1565				if ( thumbnailIndex > lastThumbnailIndex ) {
1566					$thumbnail.appendTo( that.$thumbnails );
1567				} else {
1568					$thumbnail.insertBefore( that.$thumbnails.find( '.sp-thumbnail' ).eq( thumbnailIndex ) );
1569				}
1570			});
1571
1572			// Loop through the Thumbnail objects and if a corresponding element is not found in the DOM,
1573			// it means that the thumbnail might have been removed. In this case, destroy that Thumbnail instance.
1574			for ( var i = this.thumbnails.length - 1; i >= 0; i-- ) {
1575				if ( this.$thumbnails.find( '.sp-thumbnail[data-index="' + i + '"]' ).length === 0 ) {
1576					var thumbnail = this.thumbnails[ i ];
1577
1578					thumbnail.destroy();
1579					this.thumbnails.splice( i, 1 );
1580				}
1581			}
1582
1583			// Loop through the thumbnails and if there is any uninitialized thumbnail,
1584			// initialize it, else update the thumbnail's index.
1585			this.$thumbnails.find( '.sp-thumbnail' ).each(function( index ) {
1586				var $thumbnail = $( this );
1587
1588				if ( typeof $thumbnail.attr( 'data-init' ) === 'undefined' ) {
1589					that._createThumbnail( $thumbnail, index );
1590				} else {
1591					that.thumbnails[ index ].setIndex( index );
1592				}
1593			});
1594
1595			// Remove the previous class that corresponds to the position of the thumbnail scroller
1596			this.$thumbnailsContainer.removeClass( 'sp-top-thumbnails sp-bottom-thumbnails sp-left-thumbnails sp-right-thumbnails' );
1597
1598			// Check the position of the thumbnail scroller and assign it the appropriate class and styling
1599			if ( this.settings.thumbnailsPosition === 'top' ) {
1600				this.$thumbnailsContainer.addClass( 'sp-top-thumbnails' );
1601				this.thumbnailsOrientation = 'horizontal';
1602			} else if ( this.settings.thumbnailsPosition === 'bottom' ) {
1603				this.$thumbnailsContainer.addClass( 'sp-bottom-thumbnails' );
1604				this.thumbnailsOrientation = 'horizontal';
1605			} else if ( this.settings.thumbnailsPosition === 'left' ) {
1606				this.$thumbnailsContainer.addClass( 'sp-left-thumbnails' );
1607				this.thumbnailsOrientation = 'vertical';
1608			} else if ( this.settings.thumbnailsPosition === 'right' ) {
1609				this.$thumbnailsContainer.addClass( 'sp-right-thumbnails' );
1610				this.thumbnailsOrientation = 'vertical';
1611			}
1612
1613			// Check if the pointer needs to be created
1614			if ( this.settings.thumbnailPointer === true ) {
1615				this.$thumbnailsContainer.addClass( 'sp-has-pointer' );
1616			} else {
1617				this.$thumbnailsContainer.removeClass( 'sp-has-pointer' );
1618			}
1619
1620			// Mark the thumbnail that corresponds to the selected slide
1621			this.selectedThumbnailIndex = this.selectedSlideIndex;
1622			this.$thumbnails.find( '.sp-thumbnail-container' ).eq( this.selectedThumbnailIndex ).addClass( 'sp-selected-thumbnail' );
1623			
1624			// Calculate the total size of the thumbnails
1625			this.thumbnailsSize = 0;
1626
1627			$.each( this.thumbnails, function( index, thumbnail ) {
1628				thumbnail.setSize( that.settings.thumbnailWidth, that.settings.thumbnailHeight );
1629				that.thumbnailsSize += that.thumbnailsOrientation === 'horizontal' ? thumbnail.getSize().width : thumbnail.getSize().height;
1630			});
1631
1632			// Set the size of the thumbnails
1633			if ( this.thumbnailsOrientation === 'horizontal' ) {
1634				this.$thumbnails.css({ 'width': this.thumbnailsSize, 'height': this.settings.thumbnailHeight });
1635				this.$thumbnailsContainer.css( 'height', '' );
1636				this.thumbnailsPositionProperty = 'left';
1637			} else {
1638				this.$thumbnails.css({ 'width': this.settings.thumbnailWidth, 'height': this.thumbnailsSize });
1639				this.$thumbnailsContainer.css( 'width', '' );
1640				this.thumbnailsPositionProperty = 'top';
1641			}
1642
1643			// Fire the 'thumbnailsUpdate' event
1644			this.trigger({ type: 'thumbnailsUpdate' });
1645			if ( $.isFunction( this.settings.thumbnailsUpdate ) ) {
1646				this.settings.thumbnailsUpdate.call( this, { type: 'thumbnailsUpdate' } );
1647			}
1648		},
1649
1650		// Create an individual thumbnail
1651		_createThumbnail: function( element, index ) {
1652			var that = this,
1653				thumbnail = new Thumbnail( element, this.$thumbnails, index );
1654
1655			// When the thumbnail is clicked, navigate to the corresponding slide
1656			thumbnail.on( 'thumbnailClick.' + NS, function( event ) {
1657				that.gotoSlide( event.index );
1658			});
1659
1660			// Add the thumbnail at the specified index
1661			this.thumbnails.splice( index, 0, thumbnail );
1662		},
1663
1664		// Called when the slider is resized.
1665		// Resets the size and position of the thumbnail scroller container.
1666		_thumbnailsOnResize: function() {
1667			if ( this.isThumbnailScroller === false ) {
1668				return;
1669			}
1670
1671			var that = this,
1672				newThumbnailsPosition;
1673
1674			if ( this.thumbnailsOrientation === 'horizontal' ) {
1675				this.thumbnailsContainerSize = Math.min( this.$slidesMask.width(), this.thumbnailsSize );
1676				this.$thumbnailsContainer.css( 'width', this.thumbnailsContainerSize );
1677
1678				// Reduce the slide mask's height, to make room for the thumbnails
1679				if ( this.settings.forceSize === 'fullWindow' ) {
1680					this.$slidesMask.css( 'height', this.$slidesMask.height() - this.$thumbnailsContainer.outerHeight( true ) );
1681
1682					// Resize the slide
1683					this.slideHeight = this.$slidesMask.height();
1684						
1685					$.each( this.slides, function( index, element ) {
1686						element.setSize( that.slideWidth, that.slideHeight );
1687					});
1688				}
1689			} else if ( this.thumbnailsOrientation === 'vertical' ) {
1690
1691				// Check if the width of the slide mask plus the width of the thumbnail scroller is greater than
1692				// the width of the slider's container and if that's the case, reduce the slides container width
1693				// in order to make the entire slider fit inside the slider's container.
1694				if ( this.$slidesMask.width() + this.$thumbnailsContainer.outerWidth( true ) > this.$slider.parent().width() ) {
1695					// Reduce the slider's width, to make room for the thumbnails
1696					if ( this.settings.forceSize === 'fullWidth' || this.settings.forceSize === 'fullWindow' ) {
1697						this.$slider.css( 'max-width', $( window ).width() - this.$thumbnailsContainer.outerWidth( true ) );
1698					} else {
1699						this.$slider.css( 'max-width', this.$slider.parent().width() - this.$thumbnailsContainer.outerWidth( true ) );
1700					}
1701					
1702					this.$slidesMask.css( 'width', this.$slider.width() );
1703
1704					// If the slides are horizontally oriented, update the visible size and the offset
1705					// of the selected slide, since the slider's size was reduced to make room for the thumbnails.
1706					// 
1707					// If the slides are vertically oriented, update the width and height (to maintain the aspect ratio)
1708					// of the slides.
1709					if ( this.settings.orientation === 'horizontal' ) {
1710						this.visibleOffset = Math.round( ( this.$slider.width() - this.slideSize ) / 2 );
1711						this.visibleSlidesSize = this.$slidesMask.width();
1712					} else if ( this.settings.orientation === 'vertical' ) {
1713						this.slideWidth = this.$slider.width();
1714
1715						$.each( this.slides, function( index, element ) {
1716							element.setSize( that.slideWidth, that.slideHeight );
1717						});
1718					}
1719
1720					// Re-arrange the slides
1721					this._resetSlidesPosition();
1722				}
1723
1724				this.thumbnailsContainerSize = Math.min( this.$slidesMask.height(), this.thumbnailsSize );
1725				this.$thumbnailsContainer.css( 'height', this.thumbnailsContainerSize );
1726			}
1727
1728			// If the total size of the thumbnails is smaller than the thumbnail scroller' container (which has
1729			// the same size as the slides container), it means that all the thumbnails will be visible, so set
1730			// the position of the thumbnail scroller to 0.
1731			// 
1732			// If that's not the case, the thumbnail scroller will be positioned based on which thumbnail is selected.
1733			if ( this.thumbnailsSize <= this.thumbnailsContainerSize || this.$thumbnails.find( '.sp-selected-thumbnail' ).length === 0 ) {
1734				newThumbnailsPosition = 0;
1735			} else {
1736				newThumbnailsPosition = Math.max( - this.thumbnails[ this.selectedThumbnailIndex ].getPosition()[ this.thumbnailsPositionProperty ], this.thumbnailsContainerSize - this.thumbnailsSize );
1737			}
1738
1739			// Add a padding to the slider, based on the thumbnail scroller's orientation, to make room
1740			// for the thumbnails.
1741			if ( this.settings.thumbnailsPosition === 'top' ) {
1742				this.$slider.css({ 'paddingTop': this.$thumbnailsContainer.outerHeight( true ), 'paddingLeft': '', 'paddingRight': '' });
1743			} else if ( this.settings.thumbnailsPosition === 'bottom' ) {
1744				this.$slider.css({ 'paddingTop': '', 'paddingLeft': '', 'paddingRight': '' });
1745			} else if ( this.settings.thumbnailsPosition === 'left' ) {
1746				this.$slider.css({ 'paddingTop': '', 'paddingLeft': this.$thumbnailsContainer.outerWidth( true ), 'paddingRight': '' });
1747			} else if ( this.settings.thumbnailsPosition === 'right' ) {
1748				this.$slider.css({ 'paddingTop': '', 'paddingLeft': '', 'paddingRight': this.$thumbnailsContainer.outerWidth( true ) });
1749			}
1750
1751			this._moveThumbnailsTo( newThumbnailsPosition, true );
1752		},
1753
1754		// Selects the thumbnail at the indicated index and moves the thumbnail scroller
1755		// accordingly.
1756		_gotoThumbnail: function( index ) {
1757			if ( this.isThumbnailScroller === false || typeof this.thumbnails[ index ] === 'undefined' ) {
1758				return;
1759			}
1760
1761			var previousIndex = this.selectedThumbnailIndex,
1762				newThumbnailsPosition = this.thumbnailsPosition;
1763
1764			this.selectedThumbnailIndex = index;
1765
1766			// Set the 'selected' class to the appropriate thumbnail
1767			this.$thumbnails.find( '.sp-selected-thumbnail' ).removeClass( 'sp-selected-thumbnail' );
1768			this.$thumbnails.find( '.sp-thumbnail-container' ).eq( this.selectedThumbnailIndex ).addClass( 'sp-selected-thumbnail' );
1769
1770			// Calculate the new position that the thumbnail scroller needs to go to.
1771			// 
1772			// If the selected thumbnail has a higher index than the previous one, make sure that the thumbnail
1773			// that comes after the selected thumbnail will be visible, if the selected thumbnail is not the
1774			// last thumbnail in the list.
1775			// 
1776			// If the selected thumbnail has a lower index than the previous one, make sure that the thumbnail
1777			// that's before the selected thumbnail will be visible, if the selected thumbnail is not the
1778			// first thumbnail in the list.
1779			if ( this.selectedThumbnailIndex >= previousIndex ) {
1780				var nextThumbnailIndex = this.selectedThumbnailIndex === this.thumbnails.length - 1 ? this.selectedThumbnailIndex : this.selectedThumbnailIndex + 1,
1781					nextThumbnail = this.thumbnails[ nextThumbnailIndex ],
1782					nextThumbnailPosition = this.thumbnailsOrientation === 'horizontal' ? nextThumbnail.getPosition().right : nextThumbnail.getPosition().bottom,
1783					thumbnailsRightPosition = - this.thumbnailsPosition + this.thumbnailsContainerSize;
1784
1785				if ( nextThumbnailPosition > thumbnailsRightPosition ) {
1786					newThumbnailsPosition = this.thumbnailsPosition - ( nextThumbnailPosition - thumbnailsRightPosition );
1787				}
1788			} else if ( this.selectedThumbnailIndex < previousIndex ) {
1789				var previousThumbnailIndex = this.selectedThumbnailIndex === 0 ? this.selectedThumbnailIndex : this.selectedThumbnailIndex - 1,
1790					previousThumbnail = this.thumbnails[ previousThumbnailIndex ],
1791					previousThumbnailPosition = this.thumbnailsOrientation === 'horizontal' ? previousThumbnail.getPosition().left : previousThumbnail.getPosition().top;
1792
1793				if ( previousThumbnailPosition < - this.thumbnailsPosition ) {
1794					newThumbnailsPosition = - previousThumbnailPosition;
1795				}
1796			}
1797
1798			// Move the thumbnail scroller to the calculated position
1799			this._moveThumbnailsTo( newThumbnailsPosition );
1800
1801			// Fire the 'gotoThumbnail' event
1802			this.trigger({ type: 'gotoThumbnail' });
1803			if ( $.isFunction( this.settings.gotoThumbnail ) ) {
1804				this.settings.gotoThumbnail.call( this, { type: 'gotoThumbnail' });
1805			}
1806		},
1807
1808		// Move the thumbnail scroller to the indicated position
1809		_moveThumbnailsTo: function( position, instant, callback ) {
1810			var that = this,
1811				css = {};
1812
1813			// Return if the position hasn't changed
1814			if ( position === this.thumbnailsPosition ) {
1815				return;
1816			}
1817
1818			this.thumbnailsPosition = position;
1819
1820			// Use CSS transitions if they are supported. If not, use JavaScript animation
1821			if ( this.supportedAnimation === 'css-3d' || this.supportedAnimation === 'css-2d' ) {
1822				var transition,
1823					left = this.thumbnailsOrientation === 'horizontal' ? position : 0,
1824					top = this.thumbnailsOrientation === 'horizontal' ? 0 : position;
1825
1826				if ( this.supportedAnimation === 'css-3d' ) {
1827					css[ this.vendorPrefix + 'transform' ] = 'translate3d(' + left + 'px, ' + top + 'px, 0)';
1828				} else {
1829					css[ this.vendorPrefix + 'transform' ] = 'translate(' + left + 'px, ' + top + 'px)';
1830				}
1831
1832				if ( typeof instant !== 'undefined' && instant === true ) {
1833					transition = '';
1834				} else {
1835					this.$thumbnails.addClass( 'sp-animated' );
1836					transition = this.vendorPrefix + 'transform ' + 700 / 1000 + 's';
1837
1838					this.$thumbnails.on( this.transitionEvent, function( event ) {
1839						if ( event.target !== event.currentTarget ) {
1840							return;
1841						}
1842
1843						that.$thumbnails.off( that.transitionEvent );
1844						that.$thumbnails.removeClass( 'sp-animated' );
1845
1846						if ( typeof callback === 'function' ) {
1847							callback();
1848						}
1849
1850						// Fire the 'thumbnailsMoveComplete' event
1851						that.trigger({ type: 'thumbnailsMoveComplete' });
1852						if ( $.isFunction( that.settings.thumbnailsMoveComplete ) ) {
1853							that.settings.thumbnailsMoveComplete.call( that, { type: 'thumbnailsMoveComplete' });
1854						}
1855					});
1856				}
1857
1858				css[ this.vendorPrefix + 'transition' ] = transition;
1859
1860				this.$thumbnails.css( css );
1861			} else {
1862				css[ 'margin-' + this.thumbnailsPositionProperty ] = position;
1863
1864				if ( typeof instant !== 'undefined' && instant === true ) {
1865					this.$thumbnails.css( css );
1866				} else {
1867					this.$thumbnails
1868						.addClass( 'sp-animated' )
1869						.animate( css, 700, function() {
1870							that.$thumbnails.removeClass( 'sp-animated' );
1871
1872							if ( typeof callback === 'function' ) {
1873								callback();
1874							}
1875
1876							// Fire the 'thumbnailsMoveComplete' event
1877							that.trigger({ type: 'thumbnailsMoveComplete' });
1878							if ( $.isFunction( that.settings.thumbnailsMoveComplete ) ) {
1879								that.settings.thumbnailsMoveComplete.call( that, { type: 'thumbnailsMoveComplete' });
1880							}
1881						});
1882				}
1883			}
1884		},
1885
1886		// Stop the movement of the thumbnail scroller
1887		_stopThumbnailsMovement: function() {
1888			var css = {};
1889
1890			if ( this.supportedAnimation === 'css-3d' || this.supportedAnimation === 'css-2d' ) {
1891				var	matrixString = this.$thumbnails.css( this.vendorPrefix + 'transform' ),
1892					matrixType = matrixString.indexOf( 'matrix3d' ) !== -1 ? 'matrix3d' : 'matrix',
1893					matrixArray = matrixString.replace( matrixType, '' ).match( /-?[0-9\.]+/g ),
1894					left = matrixType === 'matrix3d' ? parseInt( matrixArray[ 12 ], 10 ) : parseInt( matrixArray[ 4 ], 10 ),
1895					top = matrixType === 'matrix3d' ? parseInt( matrixArray[ 13 ], 10 ) : parseInt( matrixArray[ 5 ], 10 );
1896
1897				if ( this.supportedAnimation === 'css-3d' ) {
1898					css[ this.vendorPrefix + 'transform' ] = 'translate3d(' + left + 'px, ' + top + 'px, 0)';
1899				} else {
1900					css[ this.vendorPrefix + 'transform' ] = 'translate(' + left + 'px, ' + top + 'px)';
1901				}
1902
1903				css[ this.vendorPrefix + 'transition' ] = '';
1904
1905				this.$thumbnails.css( css );
1906				this.$thumbnails.off( this.transitionEvent );
1907				this.thumbnailsPosition = this.thumbnailsOrientation === 'horizontal' ? parseInt( matrixArray[ 4 ] , 10 ) : parseInt( matrixArray[ 5 ] , 10 );
1908			} else {
1909				this.$thumbnails.stop();
1910				this.thumbnailsPosition = parseInt( this.$thumbnails.css( 'margin-' + this.thumbnailsPositionProperty ), 10 );
1911			}
1912
1913			this.$thumbnails.removeClass( 'sp-animated' );
1914		},
1915
1916		// Destroy the module
1917		destroyThumbnails: function() {
1918			var that = this;
1919
1920			// Remove event listeners
1921			this.off( 'update.' + NS );
1922
1923			if ( this.isThumbnailScroller === false ) {
1924				return;
1925			}
1926			
1927			this.off( 'sliderResize.' + NS );
1928			this.off( 'gotoSlide.' + NS );
1929			$( window ).off( 'resize.' + this.uniqueId + '.' + NS );
1930
1931			// Destroy the individual thumbnails
1932			this.$thumbnails.find( '.sp-thumbnail' ).each( function() {
1933				var $thumbnail = $( this ),
1934					index = parseInt( $thumbnail.attr( 'data-index' ), 10 ),
1935					thumbnail = that.thumbnails[ index ];
1936
1937				thumbnail.off( 'thumbnailClick.' + NS );
1938				thumbnail.destroy();
1939			});
1940
1941			this.thumbnails.length = 0;
1942
1943			// Add the thumbnail scroller directly in the slider and
1944			// remove the thumbnail scroller container
1945			this.$thumbnails.appendTo( this.$slider );
1946			this.$thumbnailsContainer.remove();
1947			
1948			// Remove any created padding
1949			this.$slider.css({ 'paddingTop': '', 'paddingLeft': '', 'paddingRight': '' });
1950		},
1951
1952		thumbnailsDefaults: {
1953
1954			// Sets the width of the thumbnail
1955			thumbnailWidth: 100,
1956
1957			// Sets the height of the thumbnail
1958			thumbnailHeight: 80,
1959
1960			// Sets the position of the thumbnail scroller (top, bottom, right, left)
1961			thumbnailsPosition: 'bottom',
1962
1963			// Indicates if a pointer will be displayed for the selected thumbnail
1964			thumbnailPointer: false,
1965
1966			// Called when the thumbnails are updated
1967			thumbnailsUpdate: function() {},
1968
1969			// Called when a new thumbnail is selected
1970			gotoThumbnail: function() {},
1971
1972			// Called when the thumbnail scroller has moved
1973			thumbnailsMoveComplete: function() {}
1974		}
1975	};
1976
1977	var Thumbnail = function( thumbnail, thumbnails, index ) {
1978
1979		// Reference to the thumbnail jQuery element
1980		this.$thumbnail = thumbnail;
1981
1982		// Reference to the thumbnail scroller
1983		this.$thumbnails = thumbnails;
1984
1985		// Reference to the thumbnail's container, which will be 
1986		// created dynamically.
1987		this.$thumbnailContainer = null;
1988
1989		// The width and height of the thumbnail
1990		this.width = 0;
1991		this.height = 0;
1992
1993		// Indicates whether the thumbnail's image is loaded
1994		this.isImageLoaded = false;
1995
1996		// Set the index of the slide
1997		this.setIndex( index );
1998
1999		// Initialize the thumbnail
2000		this._init();
2001	};
2002
2003	Thumbnail.prototype = {
2004
2005		_init: function() {
2006			var that = this;
2007
2008			// Mark the thumbnail as initialized
2009			this.$thumbnail.attr( 'data-init', true );
2010
2011			// Create a container for the thumbnail and add the original thumbnail to this container.
2012			// Having a container will help crop the thumbnail image if it's too large.
2013			this.$thumbnailContainer = $( '<div class="sp-thumbnail-container"></div>' ).appendTo( this.$thumbnails );
2014
2015			if ( this.$thumbnail.parent( 'a' ).length !== 0 ) {
2016				this.$thumbnail.parent( 'a' ).appendTo( this.$thumbnailContainer );
2017			} else {
2018				this.$thumbnail.appendTo( this.$thumbnailContainer );
2019			}
2020
2021			// When the thumbnail container is clicked, fire an event
2022			this.$thumbnailContainer.on( 'click.' + NS, function() {
2023				that.trigger({ type: 'thumbnailClick.' + NS, index: that.index });
2024			});
2025		},
2026
2027		// Set the width and height of the thumbnail
2028		setSize: function( width, height ) {
2029			this.width = width;
2030			this.height = height;
2031
2032			// Apply the width and height to the thumbnail's container
2033			this.$thumbnailContainer.css({ 'width': this.width, 'height': this.height });
2034
2035			// If there is an image, resize it to fit the thumbnail container
2036			if ( this.$thumbnail.is( 'img' ) && typeof this.$thumbnail.attr( 'data-src' ) === 'undefined' ) {
2037				this.resizeImage();
2038			}
2039		},
2040
2041		// Return the width and height of the thumbnail
2042		getSize: function() {
2043			return {
2044				width: this.$thumbnailContainer.outerWidth( true ),
2045				height: this.$thumbnailContainer.outerHeight( true )
2046			};
2047		},
2048
2049		// Return the top, bottom, left and right position of the thumbnail
2050		getPosition: function() {
2051			return {
2052				left: this.$thumbnailContainer.position().left + parseInt( this.$thumbnailContainer.css( 'marginLeft' ) , 10 ),
2053				right: this.$thumbnailContainer.position().left + parseInt( this.$thumbnailContainer.css( 'marginLeft' ) , 10 ) + this.$thumbnailContainer.outerWidth(),
2054				top: this.$thumbnailContainer.position().top + parseInt( this.$thumbnailContainer.css( 'marginTop' ) , 10 ),
2055				bottom: this.$thumbnailContainer.position().top + parseInt( this.$thumbnailContainer.css( 'marginTop' ) , 10 ) + this.$thumbnailContainer.outerHeight()
2056			};
2057		},
2058
2059		// Set the index of the thumbnail
2060		setIndex: function( index ) {
2061			this.index = index;
2062			this.$thumbnail.attr( 'data-index', this.index );
2063		},
2064
2065		// Resize the thumbnail's image
2066		resizeImage: function() {
2067			var that = this;
2068
2069			// If the image is not loaded yet, load it
2070			if ( this.isImageLoaded === false ) {
2071				SliderProUtils.checkImagesComplete( this.$thumbnailContainer , function() {
2072					that.isImageLoaded = true;
2073					that.resizeImage();
2074				});
2075
2076				return;
2077			}
2078
2079			// Get the reference to the thumbnail image again because it was replaced by
2080			// another img element during the loading process
2081			this.$thumbnail = this.$thumbnailContainer.find( '.sp-thumbnail' );
2082
2083			// Calculate whether the image should stretch horizontally or vertically
2084			var imageWidth = this.$thumbnail.width(),
2085				imageHeight = this.$thumbnail.height();
2086
2087			if ( imageWidth / imageHeight <= this.width / this.height ) {
2088				this.$thumbnail.css({ width: '100%', height: 'auto' });
2089			} else {
2090				this.$thumbnail.css({ width: 'auto', height: '100%' });
2091			}
2092
2093			this.$thumbnail.css({ 'marginLeft': ( this.$thumbnailContainer.width() - this.$thumbnail.width() ) * 0.5, 'marginTop': ( this.$thumbnailContainer.height() - this.$thumbnail.height() ) * 0.5 });
2094		},
2095
2096		// Destroy the thumbnail
2097		destroy: function() {
2098			this.$thumbnailContainer.off( 'click.' + NS );
2099
2100			// Remove added attributes
2101			this.$thumbnail.removeAttr( 'data-init' );
2102			this.$thumbnail.removeAttr( 'data-index' );
2103
2104			// Remove the thumbnail's container and add the thumbnail
2105			// back to the thumbnail scroller container
2106			if ( this.$thumbnail.parent( 'a' ).length !== 0 ) {
2107				this.$thumbnail.parent( 'a' ).insertBefore( this.$thumbnailContainer );
2108			} else {
2109				this.$thumbnail.insertBefore( this.$thumbnailContainer );
2110			}
2111			
2112			this.$thumbnailContainer.remove();
2113		},
2114
2115		// Attach an event handler to the slide
2116		on: function( type, callback ) {
2117			return this.$thumbnailContainer.on( type, callback );
2118		},
2119
2120		// Detach an event handler to the slide
2121		off: function( type ) {
2122			return this.$thumbnailContainer.off( type );
2123		},
2124
2125		// Trigger an event on the slide
2126		trigger: function( data ) {
2127			return this.$thumbnailContainer.triggerHandler( data );
2128		}
2129	};
2130
2131	$.SliderPro.addModule( 'Thumbnails', Thumbnails );
2132
2133})( window, jQuery );
2134
2135// ConditionalImages module for Slider Pro.
2136// 
2137// Adds the possibility to specify multiple sources for each image and
2138// load the image that's the most appropriate for the size of the slider.
2139// For example, instead of loading a large image even if the slider will be small
2140// you can specify a smaller image that will be loaded instead.
2141;(function( window, $ ) {
2142
2143	"use strict";
2144
2145	var NS = 'ConditionalImages.' + $.SliderPro.namespace;
2146
2147	var ConditionalImages = {
2148
2149		// Reference to the previous size
2150		previousImageSize: null,
2151
2152		// Reference to the current size
2153		currentImageSize: null,
2154
2155		// Indicates if the current display supports high PPI
2156		isRetinaScreen: false,
2157
2158		initConditionalImages: function() {
2159			this.currentImageSize = this.previousImageSize = 'default';
2160			this.isRetinaScreen = ( typeof this._isRetina !== 'undefined' ) && ( this._isRetina() === true );
2161
2162			this.on( 'update.' + NS, $.proxy( this._conditionalImagesOnUpdate, this ) );
2163			this.on( 'sliderResize.' + NS, $.proxy( this._conditionalImagesOnResize, this ) );
2164		},
2165
2166		// Loop through all the existing images and specify the original path of the image
2167		// inside the 'data-default' attribute.
2168		_conditionalImagesOnUpdate: function() {
2169			$.each( this.slides, function( index, element ) {
2170				var $slide = element.$slide;
2171
2172				$slide.find( 'img:not([ data-default ])' ).each(function() {
2173					var $image = $( this );
2174
2175					if ( typeof $image.attr( 'data-src' ) !== 'undefined' ) {
2176						$image.attr( 'data-default', $image.attr( 'data-src' ) );
2177					} else {
2178						$image.attr( 'data-default', $image.attr( 'src' ) );
2179					}
2180				});
2181			});
2182		},
2183
2184		// When the window resizes, identify the applyable image size based on the current size of the slider
2185		// and apply it to all images that have a version of the image specified for this size.
2186		_conditionalImagesOnResize: function() {
2187			if ( this.slideWidth <= this.settings.smallSize ) {
2188				this.currentImageSize = 'small';
2189			} else if ( this.slideWidth <= this.settings.mediumSize ) {
2190				this.currentImageSize = 'medium';
2191			} else if ( this.slideWidth <= this.settings.largeSize ) {
2192				this.currentImageSize = 'large';
2193			} else {
2194				this.currentImageSize = 'default';
2195			}
2196
2197			if ( this.previousImageSize !== this.currentImageSize ) {
2198				var that = this;
2199
2200				$.each( this.slides, function( index, element ) {
2201					var $slide = element.$slide;
2202
2203					$slide.find( 'img' ).each(function() {
2204						var $image = $( this ),
2205							imageSource = '';
2206
2207						// Check if the current display supports high PPI and if a retina version of the current size was specified
2208						if ( that.isRetinaScreen === true && typeof $image.attr( 'data-retina' + that.currentImageSize ) !== 'undefined' ) {
2209							imageSource = $image.attr( 'data-retina' + that.currentImageSize );
2210
2211							// If the retina image was not loaded yet, replace the default image source with the one
2212							// that corresponds to the current slider size
2213							if ( typeof $image.attr( 'data-retina' ) !== 'undefined' && $image.attr( 'data-retina' ) !== imageSource ) {
2214								$image.attr( 'data-retina', imageSource );
2215							}
2216						} else if ( ( that.isRetinaScreen === false || that.isRetinaScreen === true && typeof $image.attr( 'data-retina' ) === 'undefined' ) && typeof $image.attr( 'data-' + that.currentImageSize ) !== 'undefined' ) {
2217							imageSource = $image.attr( 'data-' + that.currentImageSize );
2218
2219							// If the image is set to lazy load, replace the image source with the one
2220							// that corresponds to the current slider size
2221							if ( typeof $image.attr( 'data-src' ) !== 'undefined' && $image.attr( 'data-src' ) !== imageSource ) {
2222								$image.attr( 'data-src', imageSource );
2223							}
2224						}
2225
2226						// If a new image was found
2227						if ( imageSource !== '' ) {
2228
2229							// The existence of the 'data-src' attribute indicates that the image
2230							// will be lazy loaded, so don't load the new image yet
2231							if ( typeof $image.attr( 'data-src' ) === 'undefined' && $image.attr( 'src' ) !== imageSource  ) {
2232								that._loadConditionalImage( $image, imageSource, function( newImage ) {
2233									if ( newImage.hasClass( 'sp-image' ) ) {
2234										element.$mainImage = newImage;
2235										element.resizeMainImage( true );
2236									}
2237								});
2238							}
2239						}
2240					});
2241				});
2242
2243				this.previousImageSize = this.currentImageSize;
2244			}
2245		},
2246
2247		// Replace the target image with a new image
2248		_loadConditionalImage: function( image, source, callback ) {
2249
2250			// Create a new image element
2251			var newImage = $( new Image() );
2252
2253			// Copy the class(es) and inline style
2254			newImage.attr( 'class', image.attr( 'class' ) );
2255			newImage.attr( 'style', image.attr( 'style' ) );
2256
2257			// Copy the data attributes
2258			$.each( image.data(), function( name, value ) {
2259				newImage.attr( 'data-' + name, value );
2260			});
2261
2262			// Copy the width and height attributes if they exist
2263			if ( typeof image.attr( 'width' ) !== 'undefined') {
2264				newImage.attr( 'width', image.attr( 'width' ) );
2265			}
2266
2267			if ( typeof image.attr( 'height' ) !== 'undefined') {
2268				newImage.attr( 'height', image.attr( 'height' ) );
2269			}
2270
2271			if ( typeof image.attr( 'alt' ) !== 'undefined' ) {
2272				newImage.attr( 'alt', image.attr( 'alt' ) );
2273			}
2274
2275			if ( typeof image.attr( 'title' ) !== 'undefined' ) {
2276				newImage.attr( 'title', image.attr( 'title' ) );
2277			}
2278
2279			newImage.attr( 'src', source );
2280
2281			// Add the new image in the same container and remove the older image
2282			newImage.insertAfter( image );
2283			image.remove();
2284			image = null;
2285				
2286			if ( typeof callback === 'function' ) {
2287				callback( newImage );
2288			}
2289		},
2290
2291		// Destroy the module
2292		destroyConditionalImages: function() {
2293			this.off( 'update.' + NS );
2294			this.off( 'sliderResize.' + NS );
2295		},
2296
2297		conditionalImagesDefaults: {
2298
2299			// If the slider size is below this size, the small version of the images will be used
2300			smallSize: 480,
2301
2302			// If the slider size is below this size, the small version of the images will be used
2303			mediumSize: 768,
2304
2305			// If the slider size is below this size, the small version of the images will be used
2306			largeSize: 1024
2307		}
2308	};
2309
2310	$.SliderPro.addModule( 'ConditionalImages', ConditionalImages );
2311
2312})( window, jQuery );
2313
2314// Retina module for Slider Pro.
2315// 
2316// Adds the possibility to load a different image when the slider is
2317// viewed on a retina screen.
2318;(function( window, $ ) {
2319
2320	"use strict";
2321	
2322	var NS = 'Retina.' + $.SliderPro.namespace;
2323
2324	var Retina = {
2325
2326		initRetina: function() {
2327			var that = this;
2328
2329			// Return if it's not a retina screen
2330			if ( this._isRetina() === false ) {
2331				return;
2332			}
2333			
2334			this.on( 'update.' + NS, $.proxy( this._checkRetinaImages, this ) );
2335
2336			if ( this.$slider.find( '.sp-thumbnail' ).length !== 0 ) {
2337				this.on( 'update.Thumbnails.' + NS, $.proxy( this._checkRetinaThumbnailImages, this ) );
2338			}
2339		},
2340
2341		// Checks if the current display supports high PPI
2342		_isRetina: function() {
2343			if ( window.devicePixelRatio >= 2 ) {
2344				return true;
2345			}
2346
2347			if ( window.matchMedia && ( window.matchMedia( "(-webkit-min-device-pixel-ratio: 2),(min-resolution: 2dppx)" ).matches ) ) {
2348				return true;
2349			}
2350
2351			return false;
2352		},
2353
2354		// Loop through the slides and replace the images with their retina version
2355		_checkRetinaImages: function() {
2356			var that = this;
2357
2358			$.each( this.slides, function( index, element ) {
2359				var $slide = element.$slide;
2360
2361				if ( typeof $slide.attr( 'data-retina-loaded' ) === 'undefined' ) {
2362					$slide.attr( 'data-retina-loaded', true );
2363
2364					$slide.find( 'img[data-retina]' ).each(function() {
2365						var $image = $( this );
2366
2367						if ( typeof $image.attr( 'data-src' ) !== 'undefined' ) {
2368							$image.attr( 'data-src', $image.attr( 'data-retina' ) );
2369						} else {
2370							that._loadRetinaImage( $image, function( newImage ) {
2371								if ( newImage.hasClass( 'sp-image' ) ) {
2372									element.$mainImage = newImage;
2373									element.resizeMainImage( true );
2374								}
2375							});
2376						}
2377					});
2378				}
2379			});
2380		},
2381
2382		// Loop through the thumbnails and replace the images with their retina version
2383		_checkRetinaThumbnailImages: function() {
2384			var that = this;
2385
2386			$.each( this.thumbnails, function( index, element ) {
2387				var $thumbnail = element.$thumbnailContainer;
2388
2389				if ( typeof $thumbnail.attr( 'data-retina-loaded' ) === 'undefined' ) {
2390					$thumbnail.attr( 'data-retina-loaded', true );
2391
2392					$thumbnail.find( 'img[data-retina]' ).each(function() {
2393						var $image = $( this );
2394
2395						if ( typeof $image.attr( 'data-src' ) !== 'undefined' ) {
2396							$image.attr( 'data-src', $image.attr( 'data-retina' ) );
2397						} else {
2398							that._loadRetinaImage( $image, function( newImage ) {
2399								if ( newImage.hasClass( 'sp-thumbnail' ) ) {
2400									element.resizeImage();
2401								}
2402							});
2403						}
2404					});
2405				}
2406			});
2407		},
2408
2409		// Load the retina image
2410		_loadRetinaImage: function( image, callback ) {
2411			var retinaFound = false,
2412				newImagePath = '';
2413
2414			// Check if there is a retina image specified
2415			if ( typeof image.attr( 'data-retina' ) !== 'undefined' ) {
2416				retinaFound = true;
2417
2418				newImagePath = image.attr( 'data-retina' );
2419			}
2420
2421			// Check if there is a lazy loaded, non-retina, image specified
2422			if ( typeof image.attr( 'data-src' ) !== 'undefined' ) {
2423				if ( retinaFound === false ) {
2424					newImagePath = image.attr( 'data-src') ;
2425				}
2426
2427				image.removeAttr('data-src');
2428			}
2429
2430			// Return if there isn't a retina or lazy loaded image
2431			if ( newImagePath === '' ) {
2432				return;
2433			}
2434
2435			// Create a new image element
2436			var newImage = $( new Image() );
2437
2438			// Copy the class(es) and inline style
2439			newImage.attr( 'class', image.attr('class') );
2440			newImage.attr( 'style', image.attr('style') );
2441
2442			// Copy the data attributes
2443			$.each( image.data(), function( name, value ) {
2444				newImage.attr( 'data-' + name, value );
2445			});
2446
2447			// Copy the width and height attributes if they exist
2448			if ( typeof image.attr( 'width' ) !== 'undefined' ) {
2449				newImage.attr( 'width', image.attr( 'width' ) );
2450			}
2451
2452			if ( typeof image.attr( 'height' ) !== 'undefined' ) {
2453				newImage.attr( 'height', image.attr( 'height' ) );
2454			}
2455
2456			if ( typeof image.attr( 'alt' ) !== 'undefined' ) {
2457				newImage.attr( 'alt', image.attr( 'alt' ) );
2458			}
2459
2460			if ( typeof image.attr( 'title' ) !== 'undefined' ) {
2461				newImage.attr( 'title', image.attr( 'title' ) );
2462			}
2463
2464			// Add the new image in the same container and remove the older image
2465			newImage.insertAfter( image );
2466			image.remove();
2467			image = null;
2468
2469			// Assign the source of the image
2470			newImage.attr( 'src', newImagePath );
2471
2472			if ( typeof callback === 'function' ) {
2473				callback( newImage );
2474			}
2475		},
2476
2477		// Destroy the module
2478		destroyRetina: function() {
2479			this.off( 'update.' + NS );
2480			this.off( 'update.Thumbnails.' + NS );
2481		}
2482	};
2483
2484	$.SliderPro.addModule( 'Retina', Retina );
2485	
2486})( window, jQuery );
2487
2488// Lazy Loading module for Slider Pro.
2489// 
2490// Adds the possibility to delay the loading of the images until the slides/thumbnails
2491// that contain them become visible. This technique improves the initial loading
2492// performance.
2493;(function( window, $ ) {
2494
2495	"use strict";
2496
2497	var NS = 'LazyLoading.' + $.SliderPro.namespace;
2498
2499	var LazyLoading = {
2500
2501		allowLazyLoadingCheck: true,
2502
2503		initLazyLoading: function() {
2504			var that = this;
2505
2506			// The 'resize' event is fired after every update, so it's possible to use it for checking
2507			// if the update made new slides become visible
2508			// 
2509			// Also, resizing the slider might make new slides or thumbnails visible
2510			this.on( 'sliderResize.' + NS, $.proxy( this._lazyLoadingOnResize, this ) );
2511
2512			// Check visible images when a new slide is selected
2513			this.on( 'gotoSlide.' + NS, $.proxy( this._checkAndLoadVisibleImages, this ) );
2514
2515			// Check visible thumbnail images when the thumbnails are updated because new thumbnail
2516			// might have been added or the settings might have been changed so that more thumbnail
2517			// images become visible
2518			// 
2519			// Also, check visible thumbnail images after the thumbnails have moved because new thumbnails might
2520			// have become visible
2521			this.on( 'thumbnailsUpdate.' + NS + ' ' + 'thumbnailsMoveComplete.' + NS, $.proxy( this._checkAndLoadVisibleThumbnailImages, this ) );
2522		},
2523
2524		_lazyLoadingOnResize: function() {
2525			var that = this;
2526
2527			if ( this.allowLazyLoadingCheck === false ) {
2528				return;
2529			}
2530
2531			this.allowLazyLoadingCheck = false;
2532			
2533			this._checkAndLoadVisibleImages();
2534
2535			if ( this.$slider.find( '.sp-thumbnail' ).length !== 0 ) {
2536				this._checkAndLoadVisibleThumbnailImages();
2537			}
2538
2539			// Use a timer to deffer the loading of images in order to prevent too many
2540			// checking attempts
2541			setTimeout(function() {
2542				that.allowLazyLoadingCheck = true;
2543			}, 500 );
2544		},
2545
2546		// Check visible slides and load their images
2547		_checkAndLoadVisibleImages: function() {
2548			if ( this.$slider.find( '.sp-slide:not([ data-loaded ])' ).length === 0 ) {
2549				return;
2550			}
2551
2552			var that = this,
2553
2554				// Use either the middle position or the index of the selected slide as a reference, depending on
2555				// whether the slider is loopable
2556				referencePosition = this.settings.loop === true ? this.middleSlidePosition : this.selectedSlideIndex,
2557
2558				// Calculate how many slides are visible at the sides of the selected slide
2559				visibleOnSides = Math.ceil( ( this.visibleSlidesSize - this.slideSize ) / 2 / this.slideSize ),
2560
2561				// Calculate the indexes of the first and last slide that will be checked
2562				from = referencePosition - visibleOnSides - 1 > 0 ? referencePosition - visibleOnSides - 1 : 0,
2563				to = referencePosition + visibleOnSides + 1 < this.getTotalSlides() - 1 ? referencePosition + visibleOnSides + 1 : this.getTotalSlides() - 1,
2564				
2565				// Get all the slides that need to be checked
2566				slidesToCheck = this.slidesOrder.slice( from, to + 1 );
2567
2568			// Loop through the selected slides and if the slide is not marked as having
2569			// been loaded yet, loop through its images and load them.
2570			$.each( slidesToCheck, function( index, element ) {
2571				var slide = that.slides[ element ],
2572					$slide = slide.$slide;
2573
2574				if ( typeof $slide.attr( 'data-loaded' ) === 'undefined' ) {
2575					$slide.attr( 'data-loaded', true );
2576
2577					$slide.find( 'img[ data-src ]' ).each(function() {
2578						var image = $( this );
2579						that._loadImage( image, function( newImage ) {
2580							if ( newImage.hasClass( 'sp-image' ) ) {
2581								slide.$mainImage = newImage;
2582								slide.resizeMainImage( true );
2583							}
2584						});
2585					});
2586				}
2587			});
2588		},
2589
2590		// Check visible thumbnails and load their images
2591		_checkAndLoadVisibleThumbnailImages: function() {
2592			if ( this.$slider.find( '.sp-thumbnail-container:not([ data-loaded ])' ).length === 0 ) {
2593				return;
2594			}
2595
2596			var that = this,
2597				thumbnailSize = this.thumbnailsSize / this.thumbnails.length,
2598
2599				// Calculate the indexes of the first and last thumbnail that will be checked
2600				from = Math.floor( Math.abs( this.thumbnailsPosition / thumbnailSize ) ),
2601				to = Math.floor( ( - this.thumbnailsPosition + this.thumbnailsContainerSize ) / thumbnailSize ),
2602
2603				// Get all the thumbnails that need to be checked
2604				thumbnailsToCheck = this.thumbnails.slice( from, to + 1 );
2605
2606			// Loop through the selected thumbnails and if the thumbnail is not marked as having
2607			// been loaded yet, load its image.
2608			$.each( thumbnailsToCheck, function( index, element ) {
2609				var $thumbnailContainer = element.$thumbnailContainer;
2610
2611				if ( typeof $thumbnailContainer.attr( 'data-loaded' ) === 'undefined' ) {
2612					$thumbnailContainer.attr( 'data-loaded', true );
2613
2614					$thumbnailContainer.find( 'img[ data-src ]' ).each(function() {
2615						var image = $( this );
2616
2617						that._loadImage( image, function() {
2618							element.resizeImage();
2619						});
2620					});
2621				}
2622			});
2623		},
2624
2625		// Load an image
2626		_loadImage: function( image, callback ) {
2627			// Create a new image element
2628			var newImage = $( new Image() );
2629
2630			// Copy the class(es) and inline style
2631			newImage.attr( 'class', image.attr( 'class' ) );
2632			newImage.attr( 'style', image.attr( 'style' ) );
2633
2634			// Copy the data attributes
2635			$.each( image.data(), function( name, value ) {
2636				newImage.attr( 'data-' + name, value );
2637			});
2638
2639			// Copy the width and height attributes if they exist
2640			if ( typeof image.attr( 'width' ) !== 'undefined') {
2641				newImage.attr( 'width', image.attr( 'width' ) );
2642			}
2643
2644			if ( typeof image.attr( 'height' ) !== 'undefined') {
2645				newImage.attr( 'height', image.attr( 'height' ) );
2646			}
2647
2648			if ( typeof image.attr( 'alt' ) !== 'undefined' ) {
2649				newImage.attr( 'alt', image.attr( 'alt' ) );
2650			}
2651
2652			if ( typeof image.attr( 'title' ) !== 'undefined' ) {
2653				newImage.attr( 'title', image.attr( 'title' ) );
2654			}
2655
2656			// Assign the source of the image
2657			newImage.attr( 'src', image.attr( 'data-src' ) );
2658			newImage.removeAttr( 'data-src' );
2659
2660			// Add the new image in the same container and remove the older image
2661			newImage.insertAfter( image );
2662			image.remove();
2663			image = null;
2664			
2665			if ( typeof callback === 'function' ) {
2666				callback( newImage );
2667			}
2668		},
2669
2670		// Destroy the module
2671		destroyLazyLoading: function() {
2672			this.off( 'update.' + NS );
2673			this.off( 'gotoSlide.' + NS );
2674			this.off( 'sliderResize.' + NS );
2675			this.off( 'thumbnailsUpdate.' + NS );
2676			this.off( 'thumbnailsMoveComplete.' + NS );
2677		}
2678	};
2679
2680	$.SliderPro.addModule( 'LazyLoading', LazyLoading );
2681
2682})( window, jQuery );
2683
2684// Layers module for Slider Pro.
2685// 
2686// Adds support for animated and static layers. The layers can contain any content,
2687// from simple text for video elements.
2688;(function( window, $ ) {
2689
2690	"use strict";
2691
2692	var NS = 'Layers.' +  $.SliderPro.namespace;
2693
2694	var Layers = {
2695
2696		// Reference to the original 'gotoSlide' method
2697		layersGotoSlideReference: null,
2698
2699		// Reference to the timer that will delay the overriding
2700		// of the 'gotoSlide' method
2701		waitForLayersTimer: null,
2702
2703		initLayers: function() {
2704			this.on( 'update.' + NS, $.proxy( this._layersOnUpdate, this ) );
2705			this.on( 'sliderResize.' + NS, $.proxy( this._layersOnResize, this ) );
2706			this.on( 'gotoSlide.' + NS, $.proxy( this._layersOnGotoSlide, this ) );
2707		},
2708
2709		// Loop through the slides and initialize all layers
2710		_layersOnUpdate: function( event ) {
2711			var that = this;
2712
2713			$.each( this.slides, function( index, element ) {
2714				var $slide = element.$slide;
2715
2716				// Initialize the layers
2717				this.$slide.find( '.sp-layer:not([ data-layer-init ])' ).each(function() {
2718					var layer = new Layer( $( this ) );
2719
2720					// Add the 'layers' array to the slide objects (instance of SliderProSlide)
2721					if ( typeof element.layers === 'undefined' ) {
2722						element.layers = [];
2723					}
2724
2725					element.layers.push( layer );
2726
2727					if ( $( this ).hasClass( 'sp-static' ) === false ) {
2728
2729						// Add the 'animatedLayers' array to the slide objects (instance of SliderProSlide)
2730						if ( typeof element.animatedLayers === 'undefined' ) {
2731							element.animatedLayers = [];
2732						}
2733
2734						element.animatedLayers.push( layer );
2735					}
2736				});
2737			});
2738
2739			// If the 'waitForLayers' option is enabled, the slider will not move to another slide
2740			// until all the layers from the previous slide will be hidden. To achieve this,
2741			// replace the current 'gotoSlide' function with another function that will include the 
2742			// required functionality.
2743			// 
2744			// Since the 'gotoSlide' method might be overridden by other modules as well, delay this
2745			// override to make sure it's the last override.
2746			if ( this.settings.waitForLayers === true ) {
2747				clearTimeout( this.waitForLayersTimer );
2748
2749				this.waitForLayersTimer = setTimeout(function() {
2750					that.layersGotoSlideReference = that.gotoSlide;
2751					that.gotoSlide = that._layersGotoSlide;
2752				}, 1 );
2753			}
2754
2755			// Show the layers for the initial slide
2756			// Delay the call in order to make sure the layers
2757			// are scaled properly before displaying them
2758			setTimeout(function() {
2759				that.showLayers( that.selectedSlideIndex );
2760			}, 1);
2761		},
2762
2763		// When the slider resizes, try to scale down the layers proportionally. The automatic scaling
2764		// will make use of an option, 'autoScaleReference', by comparing the current width of the slider
2765		// with the reference width. So, if the reference width is 1000 pixels and the current width is
2766		// 500 pixels, it means that the layers will be scaled down to 50% of their size.
2767		_layersOnResize: function() {
2768			var that = this,
2769				autoScaleReference,
2770				useAutoScale = this.settings.autoScaleLayers,
2771				scaleRatio;
2772
2773			if ( this.settings.autoScaleLayers === false ) {
2774				return;
2775			}
2776
2777			// If there isn't a reference for how the layers should scale down automatically, use the 'width'
2778			// option as a reference, unless the width was set to a percentage. If there isn't a set reference and
2779			// the width was set to a percentage, auto scaling will not be used because it's not possible to
2780			// calculate how much should the layers scale.
2781			if ( this.settings.autoScaleReference === -1 ) {
2782				if ( typeof this.settings.width === 'string' && this.settings.width.indexOf( '%' ) !== -1 ) {
2783					useAutoScale = false;
2784				} else {
2785					autoScaleReference = parseInt( this.settings.width, 10 );
2786				}
2787			} else {
2788				autoScaleReference = this.settings.autoScaleReference;
2789			}
2790
2791			if ( useAutoScale === true && this.slideWidth < autoScaleReference ) {
2792				scaleRatio = that.slideWidth / autoScaleReference;
2793			} else {
2794				scaleRatio = 1;
2795			}
2796
2797			$.each( this.slides, function( index, slide ) {
2798				if ( typeof slide.layers !== 'undefined' ) {
2799					$.each( slide.layers, function( index, layer ) {
2800						layer.scale( scaleRatio );
2801					});
2802				}
2803			});
2804		},
2805
2806		// Replace the 'gotoSlide' method with this one, which makes it possible to 
2807		// change the slide only after the layers from the previous slide are hidden.
2808		_layersGotoSlide: function( index ) {
2809			var that = this,
2810				animatedLayers = this.slides[ this.selectedSlideIndex ].animatedLayers;
2811
2812			// If the slider is dragged, don't wait for the layer to hide
2813			if ( this.$slider.hasClass( 'sp-swiping' ) || typeof animatedLayers === 'undefined' || animatedLayers.length === 0  ) {
2814				this.layersGotoSlideReference( index );
2815			} else {
2816				this.on( 'hideLayersComplete.' + NS, function() {
2817					that.off( 'hideLayersComplete.' + NS );
2818					that.layersGotoSlideReference( index );
2819				});
2820
2821				this.hideLayers( this.selectedSlideIndex );
2822			}
2823		},
2824
2825		// When a new slide is selected, hide the layers from the previous slide
2826		// and show the layers from the current slide.
2827		_layersOnGotoSlide: function( event ) {
2828			if ( this.previousSlideIndex !== this.selectedSlideIndex &&  this.settings.waitForLayers === false ) {
2829				this.hideLayers( this.previousSlideIndex );
2830			}
2831
2832			this.showLayers( this.selectedSlideIndex );
2833		},
2834
2835		// Show the animated layers from the slide at the specified index,
2836		// and fire an event when all the layers from the slide become visible.
2837		showLayers: function( index ) {
2838			var that = this,
2839				animatedLayers = this.slides[ index ].animatedLayers,
2840				layerCounter = 0;
2841
2842			if ( typeof animatedLayers === 'undefined' ) {
2843				return;
2844			}
2845
2846			$.each( animatedLayers, function( index, element ) {
2847
2848				// If the layer is already visible, increment the counter directly, else wait 
2849				// for the layer's showing animation to complete.
2850				if ( element.isVisible() === true ) {
2851					layerCounter++;
2852
2853					if ( layerCounter === animatedLayers.length ) {
2854						that.trigger({ type: 'showLayersComplete', index: index });
2855						if ( $.isFunction( that.settings.showLayersComplete ) ) {
2856							that.settings.showLayersComplete.call( that, { type: 'showLayersComplete', index: index });
2857						}
2858					}
2859				} else {
2860					element.show(function() {
2861						layerCounter++;
2862
2863						if ( layerCounter === animatedLayers.length ) {
2864							that.trigger({ type: 'showLayersComplete', index: index });
2865							if ( $.isFunction( that.settings.showLayersComplete ) ) {
2866								that.settings.showLayersComplete.call( that, { type: 'showLayersComplete', index: index });
2867							}
2868						}
2869					});
2870				}
2871			});
2872		},
2873
2874		// Hide the animated layers from the slide at the specified index,
2875		// and fire an event when all the layers from the slide become invisible.
2876		hideLayers: function( index ) {
2877			var that = this,
2878				animatedLayers = this.slides[ index ].animatedLayers,
2879				layerCounter = 0;
2880
2881			if ( typeof animatedLayers === 'undefined' ) {
2882				return;
2883			}
2884
2885			$.each( animatedLayers, function( index, element ) {
2886
2887				// If the layer is already invisible, increment the counter directly, else wait 
2888				// for the layer's hiding animation to complete.
2889				if ( element.isVisible() === false ) {
2890					layerCounter++;
2891
2892					if ( layerCounter === animatedLayers.length ) {
2893						that.trigger({ type: 'hideLayersComplete', index: index });
2894						if ( $.isFunction( that.settings.hideLayersComplete ) ) {
2895							that.settings.hideLayersComplete.call( that, { type: 'hideLayersComplete', index: index });
2896						}
2897					}
2898				} else {
2899					element.hide(function() {
2900						layerCounter++;
2901
2902						if ( layerCounter === animatedLayers.length ) {
2903							that.trigger({ type: 'hideLayersComplete', index: index });
2904							if ( $.isFunction( that.settings.hideLayersComplete ) ) {
2905								that.settings.hideLayersComplete.call( that, { type: 'hideLayersComplete', index: index });
2906							}
2907						}
2908					});
2909				}
2910			});
2911		},
2912
2913		// Destroy the module
2914		destroyLayers: function() {
2915			this.off( 'update.' + NS );
2916			this.off( 'resize.' + NS );
2917			this.off( 'gotoSlide.' + NS );
2918			this.off( 'hideLayersComplete.' + NS );
2919		},
2920
2921		layersDefaults: {
2922
2923			// Indicates whether the slider will wait for the layers to disappear before
2924			// going to a new slide
2925			waitForLayers: false,
2926
2927			// Indicates whether the layers will be scaled automatically
2928			autoScaleLayers: true,
2929
2930			// Sets a reference width which will be compared to the current slider width
2931			// in order to determine how much the layers need to scale down. By default,
2932			// the reference width will be equal to the slide width. However, if the slide width
2933			// is set to a percentage value, then it's necessary to set a specific value for 'autoScaleReference'.
2934			autoScaleReference: -1,
2935
2936			// Called when all animated layers become visible
2937			showLayersComplete: function() {},
2938
2939			// Called when all animated layers become invisible
2940			hideLayersComplete: function() {}
2941		}
2942	};
2943
2944	// Override the slide's 'destroy' method in order to destroy the 
2945	// layers that where added to the slide as well.
2946	var slideDestroy = window.SliderProSlide.prototype.destroy;
2947
2948	window.SliderProSlide.prototype.destroy = function() {
2949		if ( typeof this.layers !== 'undefined' ) {
2950			$.each( this.layers, function( index, element ) {
2951				element.destroy();
2952			});
2953
2954			this.layers.length = 0;
2955		}
2956
2957		if ( typeof this.animatedLayers !== 'undefined' ) {
2958			this.animatedLayers.length = 0;
2959		}
2960
2961		slideDestroy.apply( this );
2962	};
2963
2964	var Layer = function( layer ) {
2965
2966		// Reference to the layer jQuery element
2967		this.$layer = layer;
2968
2969		// Indicates whether a layer is currently visible or hidden
2970		this.visible = false;
2971
2972		// Indicates whether the layer was styled
2973		this.styled = false;
2974
2975		// Holds the data attributes added to the layer
2976		this.data = null;
2977
2978		// Indicates the layer's reference point (topLeft, bottomLeft, topRight or bottomRight)
2979		this.position = null;
2980		
2981		// Indicates which CSS property (left or right) will be used for positioning the layer 
2982		this.horizontalProperty = null;
2983		
2984		// Indicates which CSS property (top or bottom) will be used for positioning the layer 
2985		this.verticalProperty = null;
2986
2987		// Indicates the value of the horizontal position
2988		this.horizontalPosition = null;
2989		
2990		// Indicates the value of the vertical position
2991		this.verticalPosition = null;
2992
2993		// Indicates how much the layers needs to be scaled
2994		this.scaleRatio = 1;
2995
2996		// Indicates the type of supported transition (CSS3 2D, CSS3 3D or JavaScript)
2997		this.supportedAnimation = SliderProUtils.getSupportedAnimation();
2998
2999		// Indicates the required vendor prefix for CSS (i.e., -webkit, -moz, etc.)
3000		this.vendorPrefix = SliderProUtils.getVendorPrefix();
3001
3002		// Indicates the name of the CSS transition's complete event (i.e., transitionend, webkitTransitionEnd, etc.)
3003		this.transitionEvent = SliderProUtils.getTransitionEvent();
3004
3005		// Reference to the timer that will be used to hide the layers automatically after a given time interval
3006		this.stayTimer = null;
3007
3008		this._init();
3009	};
3010
3011	Layer.prototype = {
3012
3013		// Initialize the layers
3014		_init: function() {
3015			this.$layer.attr( 'data-layer-init', true );
3016
3017			if ( this.$layer.hasClass( 'sp-static' ) ) {
3018				this._setStyle();
3019			} else {
3020				this.$layer.css({ 'visibility': 'hidden' });
3021			}
3022		},
3023
3024		// Set the size and position of the layer
3025		_setStyle: function() {
3026			this.styled = true;
3027
3028			// Get the data attributes specified in HTML
3029			this.data = this.$layer.data();
3030			
3031			if ( typeof this.data.width !== 'undefined' ) {
3032				this.$layer.css( 'width', this.data.width );
3033			}
3034
3035			if ( typeof this.data.height !== 'undefined' ) {
3036				this.$layer.css( 'height', this.data.height );
3037			}
3038
3039			if ( typeof this.data.depth !== 'undefined' ) {
3040				this.$layer.css( 'z-index', this.data.depth );
3041			}
3042
3043			this.position = this.data.position ? ( this.data.position ).toLowerCase() : 'topleft';
3044
3045			if ( this.position.indexOf( 'right' ) !== -1 ) {
3046				this.horizontalProperty = 'right';
3047			} else if ( this.position.indexOf( 'left' ) !== -1 ) {
3048				this.horizontalProperty = 'left';
3049			} else {
3050				this.horizontalProperty = 'center';
3051			}
3052
3053			if ( this.position.indexOf( 'bottom' ) !== -1 ) {
3054				this.verticalProperty = 'bottom';
3055			} else if ( this.position.indexOf( 'top' ) !== -1 ) {
3056				this.verticalProperty = 'top';
3057			} else {
3058				this.verticalProperty = 'center';
3059			}
3060
3061			this._setPosition();
3062
3063			this.scale( this.scaleRatio );
3064		},
3065
3066		// Set the position of the layer
3067		_setPosition: function() {
3068			var inlineStyle = this.$layer.attr( 'style' );
3069
3070			this.horizontalPosition = typeof this.data.horizontal !== 'undefined' ? this.data.horizontal : 0;
3071			this.verticalPosition = typeof this.data.vertical !== 'undefined' ? this.data.vertical : 0;
3072
3073			// Set the horizontal position of the layer based on the data set
3074			if ( this.horizontalProperty === 'center' ) {
3075				
3076				// prevent content wrapping while setting the width
3077				if ( this.$layer.is( 'img' ) === false && ( typeof inlineStyle === 'undefined' || ( typeof inlineStyle !== 'undefined' && inlineStyle.indexOf( 'width' ) === -1 ) ) ) {
3078					this.$layer.css( 'white-space', 'nowrap' );
3079					this.$layer.css( 'width', this.$layer.outerWidth( true ) );
3080				}
3081
3082				this.$layer.css({ 'marginLeft': 'auto', 'marginRight': 'auto', 'left': this.horizontalPosition, 'right': 0 });
3083			} else {
3084				this.$layer.css( this.horizontalProperty, this.horizontalPosition );
3085			}
3086
3087			// Set the vertical position of the layer based on the data set
3088			if ( this.verticalProperty === 'center' ) {
3089
3090				// prevent content wrapping while setting the height
3091				if ( this.$layer.is( 'img' ) === false && ( typeof inlineStyle === 'undefined' || ( typeof inlineStyle !== 'undefined' && inlineStyle.indexOf( 'height' ) === -1 ) ) ) {
3092					this.$layer.css( 'white-space', 'nowrap' );
3093					this.$layer.css( 'height', this.$layer.outerHeight( true ) );
3094				}
3095
3096				this.$layer.css({ 'marginTop': 'auto', 'marginBottom': 'auto', 'top': this.verticalPosition, 'bottom': 0 });
3097			} else {
3098				this.$layer.css( this.verticalProperty, this.verticalPosition );
3099			}
3100		},
3101
3102		// Scale the layer
3103		scale: function( ratio ) {
3104
3105			// Return if the layer is set to be unscalable
3106			if ( this.$layer.hasClass( 'sp-no-scale' ) ) {
3107				return;
3108			}
3109
3110			// Store the ratio (even if the layer is not ready to be scaled yet)
3111			this.scaleRatio = ratio;
3112
3113			// Return if the layer is not styled yet
3114			if ( this.styled === false ) {
3115				return;
3116			}
3117
3118			var horizontalProperty = this.horizontalProperty === 'center' ? 'left' : this.horizontalProperty,
3119				verticalProperty = this.verticalProperty === 'center' ? 'top' : this.verticalProperty,
3120				css = {};
3121
3122			// Apply the scaling
3123			css[ this.vendorPrefix + 'transform-origin' ] = this.horizontalProperty + ' ' + this.verticalProperty;
3124			css[ this.vendorPrefix + 'transform' ] = 'scale(' + this.scaleRatio + ')';
3125
3126			// If the position is not set to a percentage value, apply the scaling to the position
3127			if ( typeof this.horizontalPosition !== 'string' ) {
3128				css[ horizontalProperty ] = this.horizontalPosition * this.scaleRatio;
3129			}
3130
3131			// If the position is not set to a percentage value, apply the scaling to the position
3132			if ( typeof this.verticalPosition !== 'string' ) {
3133				css[ verticalProperty ] = this.verticalPosition * this.scaleRatio;
3134			}
3135
3136			// If the width or height is set to a percentage value, increase the percentage in order to
3137			// maintain the same layer to slide proportions. This is necessary because otherwise the scaling
3138			// transform would minimize the layers more than intended.
3139			if ( typeof this.data.width === 'string' && this.data.width.indexOf( '%' ) !== -1 ) {
3140				css.width = ( parseInt( this.data.width, 10 ) / this.scaleRatio ).toString() + '%';
3141			}
3142
3143			if ( typeof this.data.height === 'string' && this.data.height.indexOf( '%' ) !== -1 ) {
3144				css.height = ( parseInt( this.data.height, 10 ) / this.scaleRatio ).toString() + '%';
3145			}
3146
3147			this.$layer.css( css );
3148		},
3149
3150		// Show the layer
3151		show: function( callback ) {
3152			if ( this.visible === true ) {
3153				return;
3154			}
3155
3156			this.visible = true;
3157
3158			// First, style the layer if it's not already styled
3159			if ( this.styled === false ) {
3160				this._setStyle();
3161			}
3162
3163			var that = this,
3164				offset = typeof this.data.showOffset !== 'undefined' ? this.data.showOffset : 50,
3165				duration = typeof this.data.showDuration !== 'undefined' ? this.data.showDuration / 1000 : 0.4,
3166				delay = typeof this.data.showDelay !== 'undefined' ? this.data.showDelay : 10,
3167				stayDuration = typeof that.data.stayDuration !== 'undefined' ? parseInt( that.data.stayDuration, 10 ) : -1;
3168
3169			// Animate the layers with CSS3 or with JavaScript
3170			if ( this.supportedAnimation === 'javascript' ) {
3171				this.$layer
3172					.stop()
3173					.delay( delay )
3174					.css({ 'opacity': 0, 'visibility': 'visible' })
3175					.animate( { 'opacity': 1 }, duration * 1000, function() {
3176
3177						// Hide the layer after a given time interval
3178						if ( stayDuration !== -1 ) {
3179							that.stayTimer = setTimeout(function() {
3180								that.hide();
3181								that.stayTimer = null;
3182							}, stayDuration );
3183						}
3184
3185						if ( typeof callback !== 'undefined' ) {
3186							callback();
3187						}
3188					});
3189			} else {
3190				var start = { 'opacity': 0, 'visibility': 'visible' },
3191					target = { 'opacity': 1 },
3192					transformValues = '';
3193
3194				start[ this.vendorPrefix + 'transform' ] = 'scale(' + this.scaleRatio + ')';
3195				target[ this.vendorPrefix + 'transform' ] = 'scale(' + this.scaleRatio + ')';
3196				target[ this.vendorPrefix + 'transition' ] = 'opacity ' + duration + 's';
3197
3198				if ( typeof this.data.showTransition !== 'undefined' ) {
3199					if ( this.data.showTransition === 'left' ) {
3200						transformValues = offset + 'px, 0';
3201					} else if ( this.data.showTransition === 'right' ) {
3202						transformValues = '-' + offset + 'px, 0';
3203					} else if ( this.data.showTransition === 'up' ) {
3204						transformValues = '0, ' + offset + 'px';
3205					} else if ( this.data.showTransition === 'down') {
3206						transformValues = '0, -' + offset + 'px';
3207					}
3208
3209					start[ this.vendorPrefix + 'transform' ] += this.supportedAnimation === 'css-3d' ? ' translate3d(' + transformValues + ', 0)' : ' translate(' + transformValues + ')';
3210					target[ this.vendorPrefix + 'transform' ] += this.supportedAnimation === 'css-3d' ? ' translate3d(0, 0, 0)' : ' translate(0, 0)';
3211					target[ this.vendorPrefix + 'transition' ] += ', ' + this.vendorPrefix + 'transform ' + duration + 's';
3212				}
3213
3214				// Listen when the layer animation is complete
3215				this.$layer.on( this.transitionEvent, function( event ) {
3216					if ( event.target !== event.currentTarget ) {
3217						return;
3218					}
3219
3220					that.$layer
3221						.off( that.transitionEvent )
3222						.css( that.vendorPrefix + 'transition', '' );
3223
3224					// Hide the layer after a given time interval
3225					if ( stayDuration !== -1 ) {
3226						that.stayTimer = setTimeout(function() {
3227							that.hide();
3228							that.stayTimer = null;
3229						}, stayDuration );
3230					}
3231
3232					if ( typeof callback !== 'undefined' ) {
3233						callback();
3234					}
3235				});
3236
3237				this.$layer.css( start );
3238
3239				setTimeout( function() {
3240					that.$layer.css( target );
3241				}, delay );
3242			}
3243		},
3244
3245		// Hide the layer
3246		hide: function( callback ) {
3247			if ( this.visible === false ) {
3248				return;
3249			}
3250
3251			var that = this,
3252				offset = typeof this.data.hideOffset !== 'undefined' ? this.data.hideOffset : 50,
3253				duration = typeof this.data.hideDuration !== 'undefined' ? this.data.hideDuration / 1000 : 0.4,
3254				delay = typeof this.data.hideDelay !== 'undefined' ? this.data.hideDelay : 10;
3255
3256			this.visible = false;
3257
3258			// If the layer is hidden before it hides automatically, clear the timer
3259			if ( this.stayTimer !== null ) {
3260				clearTimeout( this.stayTimer );
3261			}
3262
3263			// Animate the layers with CSS3 or with JavaScript
3264			if ( this.supportedAnimation === 'javascript' ) {
3265				this.$layer
3266					.stop()
3267					.delay( delay )
3268					.animate({ 'opacity': 0 }, duration * 1000, function() {
3269						$( this ).css( 'visibility', 'hidden' );
3270
3271						if ( typeof callback !== 'undefined' ) {
3272							callback();
3273						}
3274					});
3275			} else {
3276				var transformValues = '',
3277					target = { 'opacity': 0 };
3278
3279				target[ this.vendorPrefix + 'transform' ] = 'scale(' + this.scaleRatio + ')';
3280				target[ this.vendorPrefix + 'transition' ] = 'opacity ' + duration + 's';
3281
3282				if ( typeof this.data.hideTransition !== 'undefined' ) {
3283					if ( this.data.hideTransition === 'left' ) {
3284						transformValues = '-' + offset + 'px, 0';
3285					} else if ( this.data.hideTransition === 'right' ) {
3286						transformValues = offset + 'px, 0';
3287					} else if ( this.data.hideTransition === 'up' ) {
3288						transformValues = '0, -' + offset + 'px';
3289					} else if ( this.data.hideTransition === 'down' ) {
3290						transformValues = '0, ' + offset + 'px';
3291					}
3292
3293					target[ this.vendorPrefix + 'transform' ] += this.supportedAnimation === 'css-3d' ? ' translate3d(' + transformValues + ', 0)' : ' translate(' + transformValues + ')';
3294					target[ this.vendorPrefix + 'transition' ] += ', ' + this.vendorPrefix + 'transform ' + duration + 's';
3295				}
3296
3297				// Listen when the layer animation is complete
3298				this.$layer.on( this.transitionEvent, function( event ) {
3299					if ( event.target !== event.currentTarget ) {
3300						return;
3301					}
3302
3303					that.$layer
3304						.off( that.transitionEvent )
3305						.css( that.vendorPrefix + 'transition', '' );
3306
3307					// Hide the layer after transition
3308					if ( that.visible === false ) {
3309						that.$layer.css( 'visibility', 'hidden' );
3310					}
3311
3312					if ( typeof callback !== 'undefined' ) {
3313						callback();
3314					}
3315				});
3316
3317				setTimeout( function() {
3318					that.$layer.css( target );
3319				}, delay );
3320			}
3321		},
3322
3323		isVisible: function() {
3324			if ( this.visible === false || this.$layer.is( ':hidden' ) ) {
3325				return false;
3326			}
3327
3328			return true;
3329		},
3330
3331		// Destroy the layer
3332		destroy: function() {
3333			this.$layer.removeAttr( 'style' );
3334			this.$layer.removeAttr( 'data-layer-init' );
3335		}
3336	};
3337
3338	$.SliderPro.addModule( 'Layers', Layers );
3339	
3340})( window, jQuery );
3341
3342// Fade module for Slider Pro.
3343// 
3344// Adds the possibility to navigate through slides using a cross-fade effect.
3345;(function( window, $ ) {
3346
3347	"use strict";
3348
3349	var NS = 'Fade.' + $.SliderPro.namespace;
3350
3351	var Fade = {
3352
3353		// Reference to the original 'gotoSlide' method
3354		fadeGotoSlideReference: null,
3355
3356		initFade: function() {
3357			this.on( 'update.' + NS, $.proxy( this._fadeOnUpdate, this ) );
3358		},
3359
3360		// If fade is enabled, store a reference to the original 'gotoSlide' method
3361		// and then assign a new function to 'gotoSlide'.
3362		_fadeOnUpdate: function() {
3363			if ( this.settings.fade === true ) {
3364				this.fadeGotoSlideReference = this.gotoSlide;
3365				this.gotoSlide = this._fadeGotoSlide;
3366			}
3367		},
3368
3369		// Will replace the original 'gotoSlide' function by adding a cross-fade effect
3370		// between the previous and the next slide.
3371		_fadeGotoSlide: function( index ) {
3372			if ( index === this.selectedSlideIndex ) {
3373				return;
3374			}
3375			
3376			// If the slides are being swiped/dragged, don't use fade, but call the original method instead.
3377			// If not, which means that a new slide was selected through a button, arrows or direct call, then
3378			// use fade.
3379			if ( this.$slider.hasClass( 'sp-swiping' ) ) {
3380				this.fadeGotoSlideReference( index );
3381			} else {
3382				var that = this,
3383					$nextSlide,
3384					$previousSlide,
3385					newIndex = index;
3386
3387				// Loop through all the slides and overlap the the previous and next slide,
3388				// and hide the other slides.
3389				$.each( this.slides, function( index, element ) {
3390					var slideIndex = element.getIndex(),
3391						$slide = element.$slide;
3392
3393					if ( slideIndex === newIndex ) {
3394						$slide.css({ 'opacity': 0, 'left': 0, 'top': 0, 'z-index': 20 });
3395						$nextSlide = $slide;
3396					} else if ( slideIndex === that.selectedSlideIndex ) {
3397						$slide.css({ 'opacity': 1, 'left': 0, 'top': 0, 'z-index': 10 });
3398						$previousSlide = $slide;
3399					} else {
3400						$slide.css( 'visibility', 'hidden' );
3401					}
3402				});
3403
3404				// Set the new indexes for the previous and selected slides
3405				this.previousSlideIndex = this.selectedSlideIndex;
3406				this.selectedSlideIndex = index;
3407
3408				// Re-assign the 'sp-selected' class to the currently selected slide
3409				this.$slides.find( '.sp-selected' ).removeClass( 'sp-selected' );
3410				this.$slides.find( '.sp-slide' ).eq( this.selectedSlideIndex ).addClass( 'sp-selected' );
3411			
3412				// Rearrange the slides if the slider is loopable
3413				if ( that.settings.loop === true ) {
3414					that._updateSlidesOrder();
3415				}
3416
3417				// Move the slides container so that the cross-fading slides (which now have the top and left
3418				// position set to 0) become visible and in the center of the slider.
3419				this._moveTo( this.visibleOffset, true );
3420
3421				// Fade out the previous slide, if indicated, in addition to fading in the next slide
3422				if ( this.settings.fadeOutPreviousSlide === true ) {
3423					this._fadeSlideTo( $previousSlide, 0 );
3424				}
3425
3426				// Fade in the selected slide
3427				this._fadeSlideTo( $nextSlide, 1, function() {
3428
3429					// After the animation is over, make all the slides visible again
3430					$.each( that.slides, function( index, element ) {
3431						var $slide = element.$slide;
3432						$slide.css({ 'visibility': '', 'opacity': '', 'z-index': '' });
3433					});
3434					
3435					// Reset the position of the slides and slides container
3436					that._resetSlidesPosition();
3437
3438					// Fire the 'gotoSlideComplete' event
3439					that.trigger({ type: 'gotoSlideComplete', index: index, previousIndex: that.previousSlideIndex });
3440					if ( $.isFunction( that.settings.gotoSlideComplete ) ) {
3441						that.settings.gotoSlideComplete.call( that, { type: 'gotoSlideComplete', index: index, previousIndex: that.previousSlideIndex } );
3442					}
3443				});
3444
3445				if ( this.settings.autoHeight === true ) {
3446					this._resizeHeight();
3447				}
3448
3449				// Fire the 'gotoSlide' event
3450				this.trigger({ type: 'gotoSlide', index: index, previousIndex: this.previousSlideIndex });
3451				if ( $.isFunction( this.settings.gotoSlide ) ) {
3452					this.settings.gotoSlide.call( this, { type: 'gotoSlide', index: index, previousIndex: this.previousSlideIndex });
3453				}
3454			}
3455		},
3456
3457		// Fade the target slide to the specified opacity (0 or 1)
3458		_fadeSlideTo: function( target, opacity, callback ) {
3459			var that = this;
3460
3461			// Use CSS transitions if they are supported. If not, use JavaScript animation.
3462			if ( this.supportedAnimation === 'css-3d' || this.supportedAnimation === 'css-2d' ) {
3463
3464				// There needs to be a delay between the moment the opacity is set
3465				// and the moment the transitions starts.
3466				setTimeout(function(){
3467					var css = { 'opacity': opacity };
3468					css[ that.vendorPrefix + 'transition' ] = 'opacity ' + that.settings.fadeDuration / 1000 + 's';
3469					target.css( css );
3470				}, 100 );
3471
3472				target.on( this.transitionEvent, function( event ) {
3473					if ( event.target !== event.currentTarget ) {
3474						return;
3475					}
3476					
3477					target.off( that.transitionEvent );
3478					target.css( that.vendorPrefix + 'transition', '' );
3479
3480					if ( typeof callback === 'function' ) {
3481						callback();
3482					}
3483				});
3484			} else {
3485				target.stop().animate({ 'opacity': opacity }, this.settings.fadeDuration, function() {
3486					if ( typeof callback === 'function' ) {
3487						callback();
3488					}
3489				});
3490			}
3491		},
3492
3493		// Destroy the module
3494		destroyFade: function() {
3495			this.off( 'update.' + NS );
3496
3497			if ( this.fadeGotoSlideReference !== null ) {
3498				this.gotoSlide = this.fadeGotoSlideReference;
3499			}
3500		},
3501
3502		fadeDefaults: {
3503
3504			// Indicates if fade will be used
3505			fade: false,
3506
3507			// Indicates if the previous slide will be faded out (in addition to the next slide being faded in)
3508			fadeOutPreviousSlide: true,
3509
3510			// Sets the duration of the fade effect
3511			fadeDuration: 500
3512		}
3513	};
3514
3515	$.SliderPro.addModule( 'Fade', Fade );
3516
3517})( window, jQuery );
3518
3519// Touch Swipe module for Slider Pro.
3520// 
3521// Adds touch-swipe functionality for slides.
3522;(function( window, $ ) {
3523
3524	"use strict";
3525	
3526	var NS = 'TouchSwipe.' + $.SliderPro.namespace;
3527
3528	var TouchSwipe = {
3529
3530		// The x and y coordinates of the pointer/finger's starting position
3531		touchStartPoint: {x: 0, y: 0},
3532
3533		// The x and y coordinates of the pointer/finger's end position
3534		touchEndPoint: {x: 0, y: 0},
3535
3536		// The distance from the starting to the end position on the x and y axis
3537		touchDistance: {x: 0, y: 0},
3538
3539		// The position of the slides when the touch swipe starts
3540		touchStartPosition: 0,
3541
3542		// Indicates if the slides are being swiped
3543		isTouchMoving: false,
3544
3545		// Stores the names of the events
3546		touchSwipeEvents: { startEvent: '', moveEvent: '', endEvent: '' },
3547
3548		initTouchSwipe: function() {
3549			var that = this;
3550
3551			// check if touch swipe is enabled
3552			if ( this.settings.touchSwipe === false ) {
3553				return;
3554			}
3555
3556			this.touchSwipeEvents.startEvent = 'touchstart' + '.' + NS + ' mousedown' + '.' + NS;
3557			this.touchSwipeEvents.moveEvent = 'touchmove' + '.' + NS + ' mousemove' + '.' + NS;
3558			this.touchSwipeEvents.endEvent = 'touchend' + '.' + this.uniqueId + '.' + NS + ' mouseup' + '.' + this.uniqueId + '.' + NS;
3559
3560			// Listen for touch swipe/mouse move events
3561			this.$slidesMask.on( this.touchSwipeEvents.startEvent, $.proxy( this._onTouchStart, this ) );
3562			this.$slidesMask.on( 'dragstart.' + NS, function( event ) {
3563				event.preventDefault();
3564			});
3565
3566			// Add the grabbing icon
3567			this.$slidesMask.addClass( 'sp-grab' );
3568		},
3569
3570		// Called when the slides starts being dragged
3571		_onTouchStart: function( event ) {
3572		
3573			// Disable dragging if the element is set to allow selections
3574			if ( $( event.target ).closest( '.sp-selectable' ).length >= 1 ) {
3575				return;
3576			}
3577
3578			var that = this,
3579				eventObject = typeof event.originalEvent.touches !== 'undefined' ? event.originalEvent.touches[0] : event.originalEvent;
3580
3581			// Prevent default behavior only for mouse events
3582			if ( typeof event.originalEvent.touches === 'undefined' ) {
3583				event.preventDefault();
3584			}
3585
3586			// Disable click events on links
3587			$( event.target ).parents( '.sp-slide' ).find( 'a' ).one( 'click.' + NS, function( event ) {
3588				event.preventDefault();
3589			});
3590
3591			// Get the initial position of the mouse pointer and the initial position
3592			// of the slides' container
3593			this.touchStartPoint.x = eventObject.pageX || eventObject.clientX;
3594			this.touchStartPoint.y = eventObject.pageY || eventObject.clientY;
3595			this.touchStartPosition = this.slidesPosition;
3596
3597			// Clear the previous distance values
3598			this.touchDistance.x = this.touchDistance.y = 0;
3599
3600			// If the slides are being grabbed while they're still animating, stop the
3601			// current movement
3602			if ( this.$slides.hasClass( 'sp-animated' ) ) {
3603				this.isTouchMoving = true;
3604				this._stopMovement();
3605				this.touchStartPosition = this.slidesPosition;
3606			}
3607
3608			// Listen for move and end events
3609			this.$slidesMask.on( this.touchSwipeEvents.moveEvent, $.proxy( this._onTouchMove, this ) );
3610			$( document ).on( this.touchSwipeEvents.endEvent, $.proxy( this._onTouchEnd, this ) );
3611
3612			// Swap grabbing icons
3613			this.$slidesMask.removeClass( 'sp-grab' ).addClass( 'sp-grabbing' );
3614
3615			// Add 'sp-swiping' class to indicate that the slides are being swiped
3616			this.$slider.addClass( 'sp-swiping' );
3617		},
3618
3619		// Called during the slides' dragging
3620		_onTouchMove: function( event ) {
3621			var eventObject = typeof event.originalEvent.touches !== 'undefined' ? event.originalEvent.touches[0] : event.originalEvent;
3622
3623			// Indicate that the move event is being fired
3624			this.isTouchMoving = true;
3625
3626			// Get the current position of the mouse pointer
3627			this.touchEndPoint.x = eventObject.pageX || eventObject.clientX;
3628			this.touchEndPoint.y = eventObject.pageY || eventObject.clientY;
3629
3630			// Calculate the distance of the movement on both axis
3631			this.touchDistance.x = this.touchEndPoint.x - this.touchStartPoint.x;
3632			this.touchDistance.y = this.touchEndPoint.y - this.touchStartPoint.y;
3633			
3634			// Calculate the distance of the swipe that takes place in the same direction as the orientation of the slides
3635			// and calculate the distance from the opposite direction.
3636			// 
3637			// For a swipe to be valid there should more distance in the same direction as the orientation of the slides.
3638			var distance = this.settings.orientation === 'horizontal' ? this.touchDistance.x : this.touchDistance.y,
3639				oppositeDistance = this.settings.orientation === 'horizontal' ? this.touchDistance.y : this.touchDistance.x;
3640
3641			// If the movement is in the same direction as the orientation of the slides, the swipe is valid
3642			if ( Math.abs( distance ) > Math.abs( oppositeDistance ) ) {
3643				event.preventDefault();
3644			} else {
3645				return;
3646			}
3647
3648			if ( this.settings.loop === false ) {
3649				// Make the slides move slower if they're dragged outside its bounds
3650				if ( ( this.slidesPosition > this.touchStartPosition && this.selectedSlideIndex === 0 ) ||
3651					( this.slidesPosition < this.touchStartPosition && this.selectedSlideIndex === this.getTotalSlides() - 1 )
3652				) {
3653					distance = distance * 0.2;
3654				}
3655			}
3656
3657			this._moveTo( this.touchStartPosition + distance, true );
3658		},
3659
3660		// Called when the slides are released
3661		_onTouchEnd: function( event ) {
3662			var that = this,
3663				touchDistance = this.settings.orientation === 'horizontal' ? this.touchDistance.x : this.touchDistance.y;
3664
3665			// Remove the move and end listeners
3666			this.$slidesMask.off( this.touchSwipeEvents.moveEvent );
3667			$( document ).off( this.touchSwipeEvents.endEvent );
3668
3669			// Swap grabbing icons
3670			this.$slidesMask.removeClass( 'sp-grabbing' ).addClass( 'sp-grab' );
3671
3672			// Check if there is intention for a tap
3673			if ( this.isTouchMoving === false || this.isTouchMoving === true && Math.abs( this.touchDistance.x ) < 10 && Math.abs( this.touchDistance.y ) < 10 ) {
3674				// Re-enable click events on links
3675				$( event.target ).parents( '.sp-slide' ).find( 'a' ).off( 'click.' + NS );
3676				this.$slider.removeClass( 'sp-swiping' );
3677			}
3678
3679			// Remove the 'sp-swiping' class but with a delay
3680			// because there might be other event listeners that check
3681			// the existence of this class, and this class should still be 
3682			// applied for those listeners, since there was a swipe event
3683			setTimeout(function() {
3684				that.$slider.removeClass( 'sp-swiping' );
3685			}, 1 );
3686
3687			// Return if the slides didn't move
3688			if ( this.isTouchMoving === false ) {
3689				return;
3690			}
3691
3692			this.isTouchMoving = false;
3693
3694			$( event.target ).parents( '.sp-slide' ).one( 'click', function( event ) {
3695				event.preventDefault();
3696			});
3697
3698			// Calculate the old position of the slides in order to return to it if the swipe
3699			// is below the threshold
3700			var oldSlidesPosition = - parseInt( this.$slides.find( '.sp-slide' ).eq( this.selectedSlideIndex ).css( this.positionProperty ), 10 ) + this.visibleOffset;
3701
3702			if ( Math.abs( touchDistance ) < this.settings.touchSwipeThreshold ) {
3703				this._moveTo( oldSlidesPosition );
3704			} else {
3705
3706				// Calculate by how many slides the slides container has moved
3707				var slideArrayDistance = touchDistance / ( this.slideSize + this.settings.slideDistance );
3708
3709				// Floor the obtained value and add or subtract 1, depending on the direction of the swipe
3710				slideArrayDistance = parseInt( slideArrayDistance, 10 ) + ( slideArrayDistance > 0 ? 1 : - 1 );
3711
3712				// Get the index of the currently selected slide and subtract the position index in order to obtain
3713				// the new index of the selected slide. 
3714				var nextSlideIndex = this.slidesOrder[ $.inArray( this.selectedSlideIndex, this.slidesOrder ) - slideArrayDistance ];
3715
3716				if ( this.settings.loop === true ) {
3717					this.gotoSlide( nextSlideIndex );
3718				} else {
3719					if ( typeof nextSlideIndex !== 'undefined' ) {
3720						this.gotoSlide( nextSlideIndex );
3721					} else {
3722						this._moveTo( oldSlidesPosition );
3723					}
3724				}
3725			}
3726		},
3727
3728		// Destroy the module
3729		destroyTouchSwipe: function() {
3730			this.$slidesMask.off( this.touchSwipeEvents.startEvent );
3731			this.$slidesMask.off( this.touchSwipeEvents.moveEvent );
3732			this.$slidesMask.off( 'dragstart.' + NS );
3733			$( document ).off( this.touchSwipeEvents.endEvent );
3734			this.$slidesMask.removeClass( 'sp-grab' );
3735		},
3736
3737		touchSwipeDefaults: {
3738			
3739			// Indicates whether the touch swipe will be enabled
3740			touchSwipe: true,
3741
3742			// Sets the minimum amount that the slides should move
3743			touchSwipeThreshold: 50
3744		}
3745	};
3746
3747	$.SliderPro.addModule( 'TouchSwipe', TouchSwipe );
3748	
3749})( window, jQuery );
3750
3751// Caption module for Slider Pro.
3752// 
3753// Adds a corresponding caption for each slide. The caption
3754// will appear and disappear with the slide.
3755;(function( window, $ ) {
3756
3757	"use strict";
3758	
3759	var NS = 'Caption.' + $.SliderPro.namespace;
3760
3761	var Caption = {
3762
3763		// Reference to the container element that will hold the caption
3764		$captionContainer: null,
3765
3766		// The caption content/text
3767		captionContent: '',
3768
3769		initCaption: function() {
3770			this.on( 'update.' + NS, $.proxy( this._captionOnUpdate, this ) );
3771			this.on( 'gotoSlide.' + NS, $.proxy( this._updateCaptionContent, this ) );
3772		},
3773
3774		// Create the caption container and hide the captions inside the slides
3775		_captionOnUpdate: function() {
3776			this.$captionContainer = this.$slider.find( '.sp-caption-container' );
3777
3778			if ( this.$slider.find( '.sp-caption' ).length && this.$captionContainer.length === 0 ) {
3779				this.$captionContainer = $( '<div class="sp-caption-container"></div>' ).appendTo( this.$slider );
3780
3781				// Show the caption for the selected slide
3782				this._updateCaptionContent();
3783			}
3784
3785			// Hide the captions inside the slides
3786			this.$slides.find( '.sp-caption' ).each(function() {
3787				$( this ).css( 'display', 'none' );
3788			});
3789		},
3790
3791		// Show the caption content for the selected slide
3792		_updateCaptionContent: function() {
3793			var that = this,
3794				newCaptionField = this.$slider.find( '.sp-slide' ).eq( this.selectedSlideIndex ).find( '.sp-caption' ),
3795				newCaptionContent = newCaptionField.length !== 0 ? newCaptionField.html() : '';
3796
3797			// Either use a fade effect for swapping the captions or use an instant change
3798			if ( this.settings.fadeCaption === true ) {
3799				
3800				// If the previous slide had a caption, fade out that caption first and when the animation is over
3801				// fade in the current caption.
3802				// If the previous slide didn't have a caption, fade in the current caption directly.
3803				if ( this.captionContent !== '' ) {
3804
3805					// If the caption container has 0 opacity when the fade out transition starts, set it
3806					// to 1 because the transition wouldn't work if the initial and final values are the same,
3807					// and the callback functions wouldn't fire in this case.
3808					if ( parseFloat( this.$captionContainer.css( 'opacity' ), 10 ) === 0 ) {
3809						this.$captionContainer.css( this.vendorPrefix + 'transition', '' );
3810						this.$captionContainer.css( 'opacity', 1 );
3811					}
3812
3813					this._fadeCaptionTo( 0, function() {
3814						that.captionContent = newCaptionContent;
3815
3816						if ( newCaptionContent !== '' ) {
3817							that.$captionContainer.html( that.captionContent );
3818							that._fadeCaptionTo( 1 );
3819						} else {
3820							that.$captionContainer.empty();
3821						}
3822					});
3823				} else {
3824					this.captionContent = newCaptionContent;
3825					this.$captionContainer.html( this.captionContent );
3826					this.$captionContainer.css( 'opacity', 0 );
3827					this._fadeCaptionTo( 1 );
3828				}
3829			} else {
3830				this.captionContent = newCaptionContent;
3831				this.$captionContainer.html( this.captionContent );
3832			}
3833		},
3834
3835		// Fade the caption container to the specified opacity
3836		_fadeCaptionTo: function( opacity, callback ) {
3837			var that = this;
3838
3839			// Use CSS transitions if they are supported. If not, use JavaScript animation.
3840			if ( this.supportedAnimation === 'css-3d' || this.supportedAnimation === 'css-2d' ) {
3841				
3842				// There needs to be a delay between the moment the opacity is set
3843				// and the moment the transitions starts.
3844				setTimeout(function(){
3845					var css = { 'opacity': opacity };
3846					css[ that.vendorPrefix + 'transition' ] = 'opacity ' + that.settings.captionFadeDuration / 1000 + 's';
3847					that.$captionContainer.css( css );
3848				}, 1 );
3849
3850				this.$captionContainer.on( this.transitionEvent, function( event ) {
3851					if ( event.target !== event.currentTarget ) {
3852						return;
3853					}
3854
3855					that.$captionContainer.off( that.transitionEvent );
3856					that.$captionContainer.css( that.vendorPrefix + 'transition', '' );
3857
3858					if ( typeof callback === 'function' ) {
3859						callback();
3860					}
3861				});
3862			} else {
3863				this.$captionContainer.stop().animate({ 'opacity': opacity }, this.settings.captionFadeDuration, function() {
3864					if ( typeof callback === 'function' ) {
3865						callback();
3866					}
3867				});
3868			}
3869		},
3870
3871		// Destroy the module
3872		destroyCaption: function() {
3873			this.off( 'update.' + NS );
3874			this.off( 'gotoSlide.' + NS );
3875
3876			this.$captionContainer.remove();
3877
3878			this.$slider.find( '.sp-caption' ).each(function() {
3879				$( this ).css( 'display', '' );
3880			});
3881		},
3882
3883		captionDefaults: {
3884
3885			// Indicates whether or not the captions will be faded
3886			fadeCaption: true,
3887
3888			// Sets the duration of the fade animation
3889			captionFadeDuration: 500
3890		}
3891	};
3892
3893	$.SliderPro.addModule( 'Caption', Caption );
3894	
3895})( window, jQuery );
3896
3897// Deep Linking module for Slider Pro.
3898// 
3899// Updates the hash of the URL as the user navigates through the slides.
3900// Also, allows navigating to a specific slide by indicating it in the hash.
3901;(function( window, $ ) {
3902
3903	"use strict";
3904
3905	var NS = 'DeepLinking.' + $.SliderPro.namespace;
3906
3907	var DeepLinking = {
3908
3909		initDeepLinking: function() {
3910			var that = this;
3911
3912			// Parse the initial hash
3913			this.on( 'init.' + NS, function() {
3914				that._gotoHash( window.location.hash );
3915			});
3916
3917			// Update the hash when a new slide is selected
3918			this.on( 'gotoSlide.' + NS, function( event ) {
3919				if ( that.settings.updateHash === true ) {
3920
3921					// get the 'id' attribute of the slide
3922					var slideId = that.$slider.find( '.sp-slide' ).eq( event.index ).attr( 'id' );
3923
3924					// if the slide doesn't have an 'id' attribute, use the slide index
3925					if ( typeof slideId === 'undefined' ) {
3926						slideId = event.index;
3927					}
3928
3929					window.location.hash = that.$slider.attr( 'id' ) + '/' + slideId;
3930				}
3931			});
3932
3933			// Check when the hash changes and navigate to the indicated slide
3934			$( window ).on( 'hashchange.' + this.uniqueId + '.' + NS, function() {
3935				that._gotoHash( window.location.hash );
3936			});
3937		},
3938
3939		// Parse the hash and return the slider id and the slide id
3940		_parseHash: function( hash ) {
3941			if ( hash !== '' ) {
3942				// Eliminate the # symbol
3943				hash = hash.substring(1);
3944
3945				// Get the specified slider id and slide id
3946				var values = hash.split( '/' ),
3947					slideId = values.pop(),
3948					sliderId = hash.slice( 0, - slideId.toString().length - 1 );
3949
3950				if ( this.$slider.attr( 'id' ) === sliderId ) {
3951					return { 'sliderID': sliderId, 'slideId': slideId };
3952				}
3953			}
3954
3955			return false;
3956		},
3957
3958		// Navigate to the appropriate slide, based on the specified hash
3959		_gotoHash: function( hash ) {
3960			var result = this._parseHash( hash );
3961
3962			if ( result === false ) {
3963				return;
3964			}
3965
3966			var slideId = result.slideId,
3967				slideIdNumber = parseInt( slideId, 10 );
3968
3969			// check if the specified slide id is a number or string
3970			if ( isNaN( slideIdNumber ) ) {
3971				// get the index of the slide based on the specified id
3972				var slideIndex = this.$slider.find( '.sp-slide#' + slideId ).index();
3973
3974				if ( slideIndex !== -1 && slideIndex !== this.selectedSlideIndex ) {
3975					this.gotoSlide( slideIndex );
3976				}
3977			} else if ( slideIdNumber !== this.selectedSlideIndex ) {
3978				this.gotoSlide( slideIdNumber );
3979			}
3980		},
3981
3982		// Destroy the module
3983		destroyDeepLinking: function() {
3984			this.off( 'init.' + NS );
3985			this.off( 'gotoSlide.' + NS );
3986			$( window ).off( 'hashchange.' + this.uniqueId + '.' + NS );
3987		},
3988
3989		deepLinkingDefaults: {
3990
3991			// Indicates whether the hash will be updated when a new slide is selected
3992			updateHash: false
3993		}
3994	};
3995
3996	$.SliderPro.addModule( 'DeepLinking', DeepLinking );
3997	
3998})( window, jQuery );
3999
4000// Autoplay module for Slider Pro.
4001// 
4002// Adds automatic navigation through the slides by calling the
4003// 'nextSlide' or 'previousSlide' methods at certain time intervals.
4004;(function( window, $ ) {
4005
4006	"use strict";
4007	
4008	var NS = 'Autoplay.' + $.SliderPro.namespace;
4009
4010	var Autoplay = {
4011
4012		autoplayTimer: null,
4013
4014		isTimerRunning: false,
4015
4016		isTimerPaused: false,
4017
4018		initAutoplay: function() {
4019			this.on( 'update.' + NS, $.proxy( this._autoplayOnUpdate, this ) );
4020		},
4021
4022		// Start the autoplay if it's enabled, or stop it if it's disabled but running 
4023		_autoplayOnUpdate: function( event ) {
4024			if ( this.settings.autoplay === true ) {
4025				this.on( 'gotoSlide.' + NS, $.proxy( this._autoplayOnGotoSlide, this ) );
4026				this.on( 'mouseenter.' + NS, $.proxy( this._autoplayOnMouseEnter, this ) );
4027				this.on( 'mouseleave.' + NS, $.proxy( this._autoplayOnMouseLeave, this ) );
4028
4029				this.startAutoplay();
4030			} else {
4031				this.off( 'gotoSlide.' + NS );
4032				this.off( 'mouseenter.' + NS );
4033				this.off( 'mouseleave.' + NS );
4034
4035				this.stopAutoplay();
4036			}
4037		},
4038
4039		// Restart the autoplay timer when a new slide is selected
4040		_autoplayOnGotoSlide: function( event ) {
4041			// stop previous timers before starting a new one
4042			if ( this.isTimerRunning === true ) {
4043				this.stopAutoplay();
4044			}
4045			
4046			if ( this.isTimerPaused === false ) {
4047				this.startAutoplay();
4048			}
4049		},
4050
4051		// Pause the autoplay when the slider is hovered
4052		_autoplayOnMouseEnter: function( event ) {
4053			if ( this.isTimerRunning && ( this.settings.autoplayOnHover === 'pause' || this.settings.autoplayOnHover === 'stop' ) ) {
4054				this.stopAutoplay();
4055				this.isTimerPaused = true;
4056			}
4057		},
4058
4059		// Start the autoplay when the mouse moves away from the slider
4060		_autoplayOnMouseLeave: function( event ) {
4061			if ( this.settings.autoplay === true && this.isTimerRunning === false && this.settings.autoplayOnHover !== 'stop' ) {
4062				this.startAutoplay();
4063				this.isTimerPaused = false;
4064			}
4065		},
4066
4067		// Starts the autoplay
4068		startAutoplay: function() {
4069			var that = this;
4070			
4071			this.isTimerRunning = true;
4072
4073			this.autoplayTimer = setTimeout(function() {
4074				if ( that.settings.autoplayDirection === 'normal' ) {
4075					that.nextSlide();
4076				} else if ( that.settings.autoplayDirection === 'backwards' ) {
4077					that.previousSlide();
4078				}
4079			}, this.settings.autoplayDelay );
4080		},
4081
4082		// Stops the autoplay
4083		stopAutoplay: function() {
4084			this.isTimerRunning = false;
4085			this.isTimerPaused = false;
4086
4087			clearTimeout( this.autoplayTimer );
4088		},
4089
4090		// Destroy the module
4091		destroyAutoplay: function() {
4092			clearTimeout( this.autoplayTimer );
4093
4094			this.off( 'update.' + NS );
4095			this.off( 'gotoSlide.' + NS );
4096			this.off( 'mouseenter.' + NS );
4097			this.off( 'mouseleave.' + NS );
4098		},
4099
4100		autoplayDefaults: {
4101			// Indicates whether or not autoplay will be enabled
4102			autoplay: true,
4103
4104			// Sets the delay/interval at which the autoplay will run
4105			autoplayDelay: 5000,
4106
4107			// Indicates whether autoplay will navigate to the next slide or previous slide
4108			autoplayDirection: 'normal',
4109
4110			// Indicates if the autoplay will be paused or stopped when the slider is hovered.
4111			// Possible values are 'pause', 'stop' or 'none'.
4112			autoplayOnHover: 'pause'
4113		}
4114	};
4115
4116	$.SliderPro.addModule( 'Autoplay', Autoplay );
4117	
4118})(window, jQuery);
4119
4120// Keyboard module for Slider Pro.
4121// 
4122// Adds the possibility to navigate through slides using the keyboard arrow keys, or
4123// open the link attached to the main slide image by using the Enter key.
4124;(function( window, $ ) {
4125
4126	"use strict";
4127	
4128	var NS = 'Keyboard.' + $.SliderPro.namespace;
4129
4130	var Keyboard = {
4131
4132		initKeyboard: function() {
4133			var that = this,
4134				hasFocus = false;
4135
4136			if ( this.settings.keyboard === false ) {
4137				return;
4138			}
4139
4140			// Detect when the slide is in focus and when it's not, and, optionally, make it
4141			// responsive to keyboard input only when it's in focus
4142			this.$slider.on( 'focus.' + NS, function() {
4143				hasFocus = true;
4144			});
4145
4146			this.$slider.on( 'blur.' + NS, function() {
4147				hasFocus = false;
4148			});
4149
4150			$( document ).on( 'keydown.' + this.uniqueId + '.' + NS, function( event ) {
4151				if ( that.settings.keyboardOnlyOnFocus === true && hasFocus === false ) {
4152					return;
4153				}
4154
4155				// If the left arrow key is pressed, go to the previous slide.
4156				// If the right arrow key is pressed, go to the next slide.
4157				// If the Enter key is pressed, open the link attached to the main slide image.
4158				if ( event.which === 37 ) {
4159					that.previousSlide();
4160				} else if ( event.which === 39 ) {
4161					that.nextSlide();
4162				} else if ( event.which === 13 ) {
4163					that.$slider.find( '.sp-slide' ).eq( that.selectedSlideIndex ).find( '.sp-image-container a' )[0].click();
4164				}
4165			});
4166		},
4167
4168		// Destroy the module
4169		destroyKeyboard: function() {
4170			this.$slider.off( 'focus.' + NS );
4171			this.$slider.off( 'blur.' + NS );
4172			$( document ).off( 'keydown.' + this.uniqueId + '.' + NS );
4173		},
4174
4175		keyboardDefaults: {
4176
4177			// Indicates whether keyboard navigation will be enabled
4178			keyboard: true,
4179
4180			// Indicates whether the slider will respond to keyboard input only when
4181			// the slider is in focus.
4182			keyboardOnlyOnFocus: false
4183		}
4184	};
4185
4186	$.SliderPro.addModule( 'Keyboard', Keyboard );
4187	
4188})( window, jQuery );
4189
4190// Full Screen module for Slider Pro.
4191// 
4192// Adds the possibility to open the slider full-screen, using the HMTL5 FullScreen API.
4193;(function( window, $ ) {
4194
4195	"use strict";
4196
4197	var NS = 'FullScreen.' + $.SliderPro.namespace;
4198
4199	var FullScreen = {
4200
4201		// Indicates whether the slider is currently in full-screen mode
4202		isFullScreen: false,
4203
4204		// Reference to the full-screen button
4205		$fullScreenButton: null,
4206
4207		// Reference to a set of settings that influence the slider's size
4208		// before it goes full-screen
4209		sizeBeforeFullScreen: {},
4210
4211		initFullScreen: function() {
4212			if ( ! ( document.fullscreenEnabled ||
4213				document.webkitFullscreenEnabled ||
4214				document.mozFullScreenEnabled ||
4215				document.msFullscreenEnabled ) ) {
4216				return;
4217			}
4218		
4219			this.on( 'update.' + NS, $.proxy( this._fullScreenOnUpdate, this ) );
4220		},
4221
4222		// Create or remove the full-screen button depending on the value of the 'fullScreen' option
4223		_fullScreenOnUpdate: function() {
4224			if ( this.settings.fullScreen === true && this.$fullScreenButton === null ) {
4225				this._addFullScreen();
4226			} else if ( this.settings.fullScreen === false && this.$fullScreenButton !== null ) {
4227				this._removeFullScreen();
4228			}
4229
4230			if ( this.settings.fullScreen === true ) {
4231				if ( this.settings.fadeFullScreen === true ) {
4232					this.$fullScreenButton.addClass( 'sp-fade-full-screen' );
4233				} else if ( this.settings.fadeFullScreen === false ) {
4234					this.$fullScreenButton.removeClass( 'sp-fade-full-screen' );
4235				}
4236			}
4237		},
4238
4239		// Create the full-screen button
4240		_addFullScreen: function() {
4241			this.$fullScreenButton = $('<div class="sp-full-screen-button"></div>').appendTo( this.$slider );
4242			this.$fullScreenButton.on( 'click.' + NS, $.proxy( this._onFullScreenButtonClick, this ) );
4243
4244			document.addEventListener( 'fullscreenchange', $.proxy( this._onFullScreenChange, this ) );
4245			document.addEventListener( 'mozfullscreenchange', $.proxy( this._onFullScreenChange, this ) );
4246			document.addEventListener( 'webkitfullscreenchange', $.proxy( this._onFullScreenChange, this ) );
4247			document.addEventListener( 'MSFullscreenChange', $.proxy( this._onFullScreenChange, this ) );
4248		},
4249
4250		// Remove the full-screen button
4251		_removeFullScreen: function() {
4252			if ( this.$fullScreenButton !== null ) {
4253				this.$fullScreenButton.off( 'click.' + NS );
4254				this.$fullScreenButton.remove();
4255				this.$fullScreenButton = null;
4256				document.removeEventListener( 'fullscreenchange', this._onFullScreenChange );
4257				document.removeEventListener( 'mozfullscreenchange', this._onFullScreenChange );
4258				document.removeEventListener( 'webkitfullscreenchange', this._onFullScreenChange );
4259				document.removeEventListener( 'MSFullscreenChange', this._onFullScreenChange );
4260			}
4261		},
4262
4263		// When the full-screen button is clicked, put the slider into full-screen mode, and
4264		// take it out of the full-screen mode when it's clicked again.
4265		_onFullScreenButtonClick: function() {
4266			if ( this.isFullScreen === false ) {
4267				if ( this.instance.requestFullScreen ) {
4268					this.instance.requestFullScreen();
4269				} else if ( this.instance.mozRequestFullScreen ) {
4270					this.instance.mozRequestFullScreen();
4271				} else if ( this.instance.webkitRequestFullScreen ) {
4272					this.instance.webkitRequestFullScreen();
4273				} else if ( this.instance.msRequestFullscreen ) {
4274					this.instance.msRequestFullscreen();
4275				}
4276			} else {
4277				if ( document.exitFullScreen ) {
4278					document.exitFullScreen();
4279				} else if ( document.mozCancelFullScreen ) {
4280					document.mozCancelFullScreen();
4281				} else if ( document.webkitCancelFullScreen ) {
4282					document.webkitCancelFullScreen();
4283				} else if ( document.msExitFullscreen ) {
4284					document.msExitFullscreen();
4285				}
4286			}
4287		},
4288
4289		// This will be called whenever the full-screen mode changes.
4290		// If the slider is in full-screen mode, set it to 'full window', and if it's
4291		// not in full-screen mode anymore, set it back to the original size.
4292		_onFullScreenChange: function() {
4293			this.isFullScreen = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement ? true : false;
4294
4295			if ( this.isFullScreen === true ) {
4296				this.sizeBeforeFullScreen = { forceSize: this.settings.forceSize, autoHeight: this.settings.autoHeight };
4297				this.$slider.addClass( 'sp-full-screen' );
4298				this.settings.forceSize = 'fullWindow';
4299				this.settings.autoHeight = false;
4300			} else {
4301				this.$slider.css( 'margin', '' );
4302				this.$slider.removeClass( 'sp-full-screen' );
4303				this.settings.forceSize = this.sizeBeforeFullScreen.forceSize;
4304				this.settings.autoHeight = this.sizeBeforeFullScreen.autoHeight;
4305			}
4306
4307			this.resize();
4308		},
4309
4310		// Destroy the module
4311		destroyFullScreen: function() {
4312			this.off( 'update.' + NS );
4313			this._removeFullScreen();
4314		},
4315
4316		fullScreenDefaults: {
4317
4318			// Indicates whether the full-screen button is enabled
4319			fullScreen: false,
4320
4321			// Indicates whether the button will fade in only on hover
4322			fadeFullScreen: true
4323		}
4324	};
4325
4326	$.SliderPro.addModule( 'FullScreen', FullScreen );
4327
4328})( window, jQuery );
4329
4330// Buttons module for Slider Pro.
4331// 
4332// Adds navigation buttons at the bottom of the slider.
4333;(function( window, $ ) {
4334
4335	"use strict";
4336	
4337	var NS = 'Buttons.' + $.SliderPro.namespace;
4338
4339	var Buttons = {
4340
4341		// Reference to the buttons container
4342		$buttons: null,
4343
4344		initButtons: function() {
4345			this.on( 'update.' + NS, $.proxy( this._buttonsOnUpdate, this ) );
4346		},
4347
4348		_buttonsOnUpdate: function() {
4349			this.$buttons = this.$slider.find('.sp-buttons');
4350			
4351			// If there is more that one slide but the buttons weren't created yet, create the buttons.
4352			// If the buttons were created but their number differs from the total number of slides, re-create the buttons.
4353			// If the buttons were created but there are less than one slide, remove the buttons.s
4354			if ( this.settings.buttons === true && this.getTotalSlides() > 1 && this.$buttons.length === 0 ) {
4355				this._createButtons();
4356			} else if ( this.settings.buttons === true && this.getTotalSlides() !== this.$buttons.find( '.sp-button' ).length && this.$buttons.length !== 0 ) {
4357				this._adjustButtons();
4358			} else if ( this.settings.buttons === false || ( this.getTotalSlides() <= 1 && this.$buttons.length !== 0 ) ) {
4359				this._removeButtons();
4360			}
4361		},
4362
4363		// Create the buttons
4364		_createButtons: function() {
4365			var that = this;
4366
4367			// Create the buttons' container
4368			this.$buttons = $( '<div class="sp-buttons"></div>' ).appendTo( this.$slider );
4369
4370			// Create the buttons
4371			for ( var i = 0; i < this.getTotalSlides(); i++ ) {
4372				$( '<div class="sp-button"></div>' ).appendTo( this.$buttons );
4373			}
4374
4375			// Listen for button clicks 
4376			this.$buttons.on( 'click.' + NS, '.sp-button', function() {
4377				that.gotoSlide( $( this ).index() );
4378			});
4379
4380			// Set the initially selected button
4381			this.$buttons.find( '.sp-button' ).eq( this.selectedSlideIndex ).addClass( 'sp-selected-button' );
4382
4383			// Select the corresponding button when the slide changes
4384			this.on( 'gotoSlide.' + NS, function( event ) {
4385				that.$buttons.find( '.sp-selected-button' ).removeClass( 'sp-selected-button' );
4386				that.$buttons.find( '.sp-button' ).eq( event.index ).addClass( 'sp-selected-button' );
4387			});
4388
4389			// Indicate that the slider has buttons 
4390			this.$slider.addClass( 'sp-has-buttons' );
4391		},
4392
4393		// Re-create the buttons. This is calles when the number of slides changes.
4394		_adjustButtons: function() {
4395			this.$buttons.empty();
4396
4397			// Create the buttons
4398			for ( var i = 0; i < this.getTotalSlides(); i++ ) {
4399				$( '<div class="sp-button"></div>' ).appendTo( this.$buttons );
4400			}
4401
4402			// Change the selected the buttons
4403			this.$buttons.find( '.sp-selected-button' ).removeClass( 'sp-selected-button' );
4404			this.$buttons.find( '.sp-button' ).eq( this.selectedSlideIndex ).addClass( 'sp-selected-button' );
4405		},
4406
4407		// Remove the buttons
4408		_removeButtons: function() {
4409			this.$buttons.off( 'click.' + NS, '.sp-button' );
4410			this.off( 'gotoSlide.' + NS );
4411			this.$buttons.remove();
4412			this.$slider.removeClass( 'sp-has-buttons' );
4413		},
4414
4415		destroyButtons: function() {
4416			this._removeButtons();
4417			this.off( 'update.' + NS );
4418		},
4419
4420		buttonsDefaults: {
4421			
4422			// Indicates whether the buttons will be created
4423			buttons: true
4424		}
4425	};
4426
4427	$.SliderPro.addModule( 'Buttons', Buttons );
4428
4429})( window, jQuery );
4430
4431// Arrows module for Slider Pro.
4432// 
4433// Adds arrows for navigating to the next or previous slide.
4434;(function( window, $ ) {
4435
4436	"use strict";
4437
4438	var NS = 'Arrows.' + $.SliderPro.namespace;
4439
4440	var Arrows = {
4441
4442		// Reference to the arrows container
4443		$arrows: null,
4444
4445		// Reference to the previous arrow
4446		$previousArrow: null,
4447
4448		// Reference to the next arrow
4449		$nextArrow: null,
4450
4451		initArrows: function() {
4452			this.on( 'update.' + NS, $.proxy( this._arrowsOnUpdate, this ) );
4453			this.on( 'gotoSlide.' + NS, $.proxy( this._checkArrowsVisibility, this ) );
4454		},
4455
4456		_arrowsOnUpdate: function() {
4457			var that = this;
4458
4459			// Create the arrows if the 'arrows' option is set to true
4460			if ( this.settings.arrows === true && this.$arrows === null ) {
4461				this.$arrows = $( '<div class="sp-arrows"></div>' ).appendTo( this.$slidesContainer );
4462				
4463				this.$previousArrow = $( '<div class="sp-arrow sp-previous-arrow"></div>' ).appendTo( this.$arrows );
4464				this.$nextArrow = $( '<div class="sp-arrow sp-next-arrow"></div>' ).appendTo( this.$arrows );
4465
4466				this.$previousArrow.on( 'click.' + NS, function() {
4467					that.previousSlide();
4468				});
4469
4470				this.$nextArrow.on( 'click.' + NS, function() {
4471					that.nextSlide();
4472				});
4473
4474				this._checkArrowsVisibility();
4475			} else if ( this.settings.arrows === false && this.$arrows !== null ) {
4476				this._removeArrows();
4477			}
4478
4479			if ( this.settings.arrows === true ) {
4480				if ( this.settings.fadeArrows === true ) {
4481					this.$arrows.addClass( 'sp-fade-arrows' );
4482				} else if ( this.settings.fadeArrows === false ) {
4483					this.$arrows.removeClass( 'sp-fade-arrows' );
4484				}
4485			}
4486		},
4487
4488		// Show or hide the arrows depending on the position of the selected slide
4489		_checkArrowsVisibility: function() {
4490			if ( this.settings.arrows === false || this.settings.loop === true ) {
4491				return;
4492			}
4493
4494			if ( this.selectedSlideIndex === 0 ) {
4495				this.$previousArrow.css( 'display', 'none' );
4496			} else {
4497				this.$previousArrow.css( 'display', 'block' );
4498			}
4499
4500			if ( this.selectedSlideIndex === this.getTotalSlides() - 1 ) {
4501				this.$nextArrow.css( 'display', 'none' );
4502			} else {
4503				this.$nextArrow.css( 'display', 'block' );
4504			}
4505		},
4506		
4507		_removeArrows: function() {
4508			if ( this.$arrows !== null ) {
4509				this.$previousArrow.off( 'click.' + NS );
4510				this.$nextArrow.off( 'click.' + NS );
4511				this.$arrows.remove();
4512				this.$arrows = null;
4513			}
4514		},
4515
4516		destroyArrows: function() {
4517			this._removeArrows();
4518			this.off( 'update.' + NS );
4519			this.off( 'gotoSlide.' + NS );
4520		},
4521
4522		arrowsDefaults: {
4523
4524			// Indicates whether the arrow buttons will be created
4525			arrows: false,
4526
4527			// Indicates whether the arrows will fade in only on hover
4528			fadeArrows: true
4529		}
4530	};
4531
4532	$.SliderPro.addModule( 'Arrows', Arrows );
4533
4534})( window, jQuery );
4535
4536// Thumbnail Touch Swipe module for Slider Pro.
4537// 
4538// Adds touch-swipe functionality for thumbnails.
4539;(function( window, $ ) {
4540
4541	"use strict";
4542	
4543	var NS = 'ThumbnailTouchSwipe.' + $.SliderPro.namespace;
4544
4545	var ThumbnailTouchSwipe = {
4546
4547		// The x and y coordinates of the pointer/finger's starting position
4548		thumbnailTouchStartPoint: { x: 0, y: 0 },
4549
4550		// The x and y coordinates of the pointer/finger's end position
4551		thumbnailTouchEndPoint: { x: 0, y: 0 },
4552
4553		// The distance from the starting to the end position on the x and y axis
4554		thumbnailTouchDistance: { x: 0, y: 0 },
4555
4556		// The position of the thumbnail scroller when the touch swipe starts
4557		thumbnailTouchStartPosition: 0,
4558
4559		// Indicates if the thumbnail scroller is being swiped
4560		isThumbnailTouchMoving: false,
4561
4562		// Indicates if the touch swipe was initialized
4563		isThumbnailTouchSwipe: false,
4564
4565		// Stores the names of the events
4566		thumbnailTouchSwipeEvents: { startEvent: '', moveEvent: '', endEvent: '' },
4567
4568		initThumbnailTouchSwipe: function() {
4569			this.on( 'update.' + NS, $.proxy( this._thumbnailTouchSwipeOnUpdate, this ) );
4570		},
4571
4572		_thumbnailTouchSwipeOnUpdate: function() {
4573
4574			// Return if there are no thumbnails
4575			if ( this.isThumbnailScroller === false ) {
4576				return;
4577			}
4578
4579			// Initialize the touch swipe functionality if it wasn't initialized yet
4580			if ( this.settings.thumbnailTouchSwipe === true && this.isThumbnailTouchSwipe === false ) {
4581				this.isThumbnailTouchSwipe = true;
4582
4583				this.thumbnailTouchSwipeEvents.startEvent = 'touchstart' + '.' + NS + ' mousedown' + '.' + NS;
4584				this.thumbnailTouchSwipeEvents.moveEvent = 'touchmove' + '.' + NS + ' mousemove' + '.' + NS;
4585				this.thumbnailTouchSwipeEvents.endEvent = 'touchend' + '.' + this.uniqueId + '.' + NS + ' mouseup' + '.' + this.uniqueId + '.' + NS;
4586				
4587				// Listen for touch swipe/mouse move events
4588				this.$thumbnails.on( this.thumbnailTouchSwipeEvents.startEvent, $.proxy( this._onThumbnailTouchStart, this ) );
4589				this.$thumbnails.on( 'dragstart.' + NS, function( event ) {
4590					event.preventDefault();
4591				});
4592			
4593				// Add the grabbing icon
4594				this.$thumbnails.addClass( 'sp-grab' );
4595			}
4596
4597			// Remove the default thumbnailClick
4598			$.each( this.thumbnails, function( index, thumbnail ) {
4599				thumbnail.off( 'thumbnailClick' );
4600			});
4601		},
4602
4603		// Called when the thumbnail scroller starts being dragged
4604		_onThumbnailTouchStart: function( event ) {
4605			// Disable dragging if the element is set to allow selections
4606			if ( $( event.target ).closest( '.sp-selectable' ).length >= 1 ) {
4607				return;
4608			}
4609
4610			var that = this,
4611				eventObject = typeof event.originalEvent.touches !== 'undefined' ? event.originalEvent.touches[0] : event.originalEvent;
4612
4613			// Prevent default behavior for mouse events
4614			if ( typeof event.originalEvent.touches === 'undefined' ) {
4615				event.preventDefault();
4616			}
4617
4618			// Disable click events on links
4619			$( event.target ).parents( '.sp-thumbnail-container' ).find( 'a' ).one( 'click.' + NS, function( event ) {
4620				event.preventDefault();
4621			});
4622
4623			// Get the initial position of the mouse pointer and the initial position
4624			// of the thumbnail scroller
4625			this.thumbnailTouchStartPoint.x = eventObject.pageX || eventObject.clientX;
4626			this.thumbnailTouchStartPoint.y = eventObject.pageY || eventObject.clientY;
4627			this.thumbnailTouchStartPosition = this.thumbnailsPosition;
4628
4629			// Clear the previous distance values
4630			this.thumbnailTouchDistance.x = this.thumbnailTouchDistance.y = 0;
4631
4632			// If the thumbnail scroller is being grabbed while it's still animating, stop the
4633			// current movement
4634			if ( this.$thumbnails.hasClass( 'sp-animated' ) ) {
4635				this.isThumbnailTouchMoving = true;
4636				this._stopThumbnailsMovement();
4637				this.thumbnailTouchStartPosition = this.thumbnailsPosition;
4638			}
4639
4640			// Listen for move and end events
4641			this.$thumbnails.on( this.thumbnailTouchSwipeEvents.moveEvent, $.proxy( this._onThumbnailTouchMove, this ) );
4642			$( document ).on( this.thumbnailTouchSwipeEvents.endEvent, $.proxy( this._onThumbnailTouchEnd, this ) );
4643
4644			// Swap grabbing icons
4645			this.$thumbnails.removeClass( 'sp-grab' ).addClass( 'sp-grabbing' );
4646
4647			// Add 'sp-swiping' class to indicate that the thumbnail scroller is being swiped
4648			this.$thumbnailsContainer.addClass( 'sp-swiping' );
4649		},
4650
4651		// Called during the thumbnail scroller's dragging
4652		_onThumbnailTouchMove: function( event ) {
4653			var eventObject = typeof event.originalEvent.touches !== 'undefined' ? event.originalEvent.touches[0] : event.originalEvent;
4654
4655			// Indicate that the move event is being fired
4656			this.isThumbnailTouchMoving = true;
4657
4658			// Get the current position of the mouse pointer
4659			this.thumbnailTouchEndPoint.x = eventObject.pageX || eventObject.clientX;
4660			this.thumbnailTouchEndPoint.y = eventObject.pageY || eventObject.clientY;
4661
4662			// Calculate the distance of the movement on both axis
4663			this.thumbnailTouchDistance.x = this.thumbnailTouchEndPoint.x - this.thumbnailTouchStartPoint.x;
4664			this.thumbnailTouchDistance.y = this.thumbnailTouchEndPoint.y - this.thumbnailTouchStartPoint.y;
4665			
4666			// Calculate the distance of the swipe that takes place in the same direction as the orientation of the thumbnails
4667			// and calculate the distance from the opposite direction.
4668			// 
4669			// For a swipe to be valid there should more distance in the same direction as the orientation of the thumbnails.
4670			var distance = this.thumbnailsOrientation === 'horizontal' ? this.thumbnailTouchDistance.x : this.thumbnailTouchDistance.y,
4671				oppositeDistance = this.thumbnailsOrientation === 'horizontal' ? this.thumbnailTouchDistance.y : this.thumbnailTouchDistance.x;
4672
4673			// If the movement is in the same direction as the orientation of the thumbnails, the swipe is valid
4674			if ( Math.abs( distance ) > Math.abs( oppositeDistance ) ) {
4675				event.preventDefault();
4676			} else {
4677				return;
4678			}
4679
4680			// Make the thumbnail scroller move slower if it's dragged outside its bounds
4681			if ( this.thumbnailsPosition >= 0 ) {
4682				var infOffset = - this.thumbnailTouchStartPosition;
4683				distance = infOffset + ( distance - infOffset ) * 0.2;
4684			} else if ( this.thumbnailsPosition <= - this.thumbnailsSize + this.thumbnailsContainerSize ) {
4685				var supOffset = this.thumbnailsSize - this.thumbnailsContainerSize + this.thumbnailTouchStartPosition;
4686				distance = - supOffset + ( distance + supOffset ) * 0.2;
4687			}
4688			
4689			this._moveThumbnailsTo( this.thumbnailTouchStartPosition + distance, true );
4690		},
4691
4692		// Called when the thumbnail scroller is released
4693		_onThumbnailTouchEnd: function( event ) {
4694			var that = this,
4695				thumbnailTouchDistance = this.thumbnailsOrientation === 'horizontal' ? this.thumbnailTouchDistance.x : this.thumbnailTouchDistance.y;
4696
4697			// Remove the move and end listeners
4698			this.$thumbnails.off( this.thumbnailTouchSwipeEvents.moveEvent );
4699			$( document ).off( this.thumbnailTouchSwipeEvents.endEvent );
4700
4701			// Swap grabbing icons
4702			this.$thumbnails.removeClass( 'sp-grabbing' ).addClass( 'sp-grab' );
4703
4704			// Check if there is intention for a tap/click
4705			if ( this.isThumbnailTouchMoving === false ||
4706				this.isThumbnailTouchMoving === true &&
4707				Math.abs( this.thumbnailTouchDistance.x ) < 10 &&
4708				Math.abs( this.thumbnailTouchDistance.y ) < 10
4709			) {
4710				var targetThumbnail = $( event.target ).hasClass( 'sp-thumbnail-container' ) ? $( event.target ) : $( event.target ).parents( '.sp-thumbnail-container' ),
4711					index = targetThumbnail.index();
4712
4713				// If a link is cliked, navigate to that link, else navigate to the slide that corresponds to the thumbnail
4714				if ( $( event.target ).parents( 'a' ).length !== 0 ) {
4715					$( event.target ).parents( 'a' ).off( 'click.' + NS );
4716					this.$thumbnailsContainer.removeClass( 'sp-swiping' );
4717				} else if ( index !== this.selectedThumbnailIndex && index !== -1 ) {
4718					this.gotoSlide( index );
4719				}
4720
4721				return;
4722			}
4723
4724			this.isThumbnailTouchMoving = false;
4725
4726			$( event.target ).parents( '.sp-thumbnail' ).one( 'click', function( event ) {
4727				event.preventDefault();
4728			});
4729
4730			// Remove the 'sp-swiping' class but with a delay
4731			// because there might be other event listeners that check
4732			// the existence of this class, and this class should still be 
4733			// applied for those listeners, since there was a swipe event
4734			setTimeout(function() {
4735				that.$thumbnailsContainer.removeClass( 'sp-swiping' );
4736			}, 1 );
4737
4738			// Keep the thumbnail scroller inside the bounds
4739			if ( this.thumbnailsPosition > 0 ) {
4740				this._moveThumbnailsTo( 0 );
4741			} else if ( this.thumbnailsPosition < this.thumbnailsContainerSize - this.thumbnailsSize ) {
4742				this._moveThumbnailsTo( this.thumbnailsContainerSize - this.thumbnailsSize );
4743			}
4744
4745			// Fire the 'thumbnailsMoveComplete' event
4746			this.trigger({ type: 'thumbnailsMoveComplete' });
4747			if ( $.isFunction( this.settings.thumbnailsMoveComplete ) ) {
4748				this.settings.thumbnailsMoveComplete.call( this, { type: 'thumbnailsMoveComplete' });
4749			}
4750		},
4751
4752		// Destroy the module
4753		destroyThumbnailTouchSwipe: function() {
4754			this.off( 'update.' + NS );
4755
4756			if ( this.isThumbnailScroller === false ) {
4757				return;
4758			}
4759
4760			this.$thumbnails.off( this.thumbnailTouchSwipeEvents.startEvent );
4761			this.$thumbnails.off( this.thumbnailTouchSwipeEvents.moveEvent );
4762			this.$thumbnails.off( 'dragstart.' + NS );
4763			$( document ).off( this.thumbnailTouchSwipeEvents.endEvent );
4764			this.$thumbnails.removeClass( 'sp-grab' );
4765		},
4766
4767		thumbnailTouchSwipeDefaults: {
4768
4769			// Indicates whether the touch swipe will be enabled for thumbnails
4770			thumbnailTouchSwipe: true
4771		}
4772	};
4773
4774	$.SliderPro.addModule( 'ThumbnailTouchSwipe', ThumbnailTouchSwipe );
4775
4776})( window, jQuery );
4777
4778// Thumbnail Arrows module for Slider Pro.
4779// 
4780// Adds thumbnail arrows for moving the thumbnail scroller.
4781;(function( window, $ ) {
4782
4783	"use strict";
4784	
4785	var NS = 'ThumbnailArrows.' + $.SliderPro.namespace;
4786
4787	var ThumbnailArrows = {
4788
4789		// Reference to the arrows container
4790		$thumbnailArrows: null,
4791
4792		// Reference to the 'previous' thumbnail arrow
4793		$previousThumbnailArrow: null,
4794
4795		// Reference to the 'next' thumbnail arrow
4796		$nextThumbnailArrow: null,
4797
4798		initThumbnailArrows: function() {
4799			var that = this;
4800
4801			this.on( 'update.' + NS, $.proxy( this._thumbnailArrowsOnUpdate, this ) );
4802			
4803			// Check if the arrows need to be visible or invisible when the thumbnail scroller
4804			// resizes and when the thumbnail scroller moves.
4805			this.on( 'sliderResize.' + NS + ' ' + 'thumbnailsMoveComplete.' + NS, function() {
4806				if ( that.isThumbnailScroller === true && that.settings.thumbnailArrows === true ) {
4807					that._checkThumbnailArrowsVisibility();
4808				}
4809			});
4810		},
4811		
4812		// Called when the slider is updated
4813		_thumbnailArrowsOnUpdate: function() {
4814			var that = this;
4815			
4816			if ( this.isThumbnailScroller === false ) {
4817				return;
4818			}
4819
4820			// Create or remove the thumbnail scroller arrows
4821			if ( this.settings.thumbnailArrows === true && this.$thumbnailArrows === null ) {
4822				this.$thumbnailArrows = $( '<div class="sp-thumbnail-arrows"></div>' ).appendTo( this.$thumbnailsContainer );
4823				
4824				this.$previousThumbnailArrow = $( '<div class="sp-thumbnail-arrow sp-previous-thumbnail-arrow"></div>' ).appendTo( this.$thumbnailArrows );
4825				this.$nextThumbnailArrow = $( '<div class="sp-thumbnail-arrow sp-next-thumbnail-arrow"></div>' ).appendTo( this.$thumbnailArrows );
4826
4827				this.$previousThumbnailArrow.on( 'click.' + NS, function() {
4828					var previousPosition = Math.min( 0, that.thumbnailsPosition + that.thumbnailsContainerSize );
4829					that._moveThumbnailsTo( previousPosition );
4830				});
4831
4832				this.$nextThumbnailArrow.on( 'click.' + NS, function() {
4833					var nextPosition = Math.max( that.thumbnailsContainerSize - that.thumbnailsSize, that.thumbnailsPosition - that.thumbnailsContainerSize );
4834					that._moveThumbnailsTo( nextPosition );
4835				});
4836			} else if ( this.settings.thumbnailArrows === false && this.$thumbnailArrows !== null ) {
4837				this._removeThumbnailArrows();
4838			}
4839
4840			// Add fading functionality and check if the arrows need to be visible or not
4841			if ( this.settings.thumbnailArrows === true ) {
4842				if ( this.settings.fadeThumbnailArrows === true ) {
4843					this.$thumbnailArrows.addClass( 'sp-fade-thumbnail-arrows' );
4844				} else if ( this.settings.fadeThumbnailArrows === false ) {
4845					this.$thumbnailArrows.removeClass( 'sp-fade-thumbnail-arrows' );
4846				}
4847
4848				this._checkThumbnailArrowsVisibility();
4849			}
4850		},
4851
4852		// Checks if the 'next' or 'previous' arrows need to be visible or hidden,
4853		// based on the position of the thumbnail scroller
4854		_checkThumbnailArrowsVisibility: function() {
4855			if ( this.thumbnailsPosition === 0 ) {
4856				this.$previousThumbnailArrow.css( 'display', 'none' );
4857			} else {
4858				this.$previousThumbnailArrow.css( 'display', 'block' );
4859			}
4860
4861			if ( this.thumbnailsPosition === this.thumbnailsContainerSize - this.thumbnailsSize ) {
4862				this.$nextThumbnailArrow.css( 'display', 'none' );
4863			} else {
4864				this.$nextThumbnailArrow.css( 'display', 'block' );
4865			}
4866		},
4867
4868		// Remove the thumbnail arrows
4869		_removeThumbnailArrows: function() {
4870			if ( this.$thumbnailArrows !== null ) {
4871				this.$previousThumbnailArrow.off( 'click.' + NS );
4872				this.$nextThumbnailArrow.off( 'click.' + NS );
4873				this.$thumbnailArrows.remove();
4874				this.$thumbnailArrows = null;
4875			}
4876		},
4877
4878		// Destroy the module
4879		destroyThumbnailArrows: function() {
4880			this._removeThumbnailArrows();
4881			this.off( 'update.' + NS );
4882			this.off( 'sliderResize.' + NS );
4883			this.off( 'thumbnailsMoveComplete.' + NS );
4884		},
4885
4886		thumbnailArrowsDefaults: {
4887
4888			// Indicates whether the thumbnail arrows will be enabled
4889			thumbnailArrows: false,
4890
4891			// Indicates whether the thumbnail arrows will be faded
4892			fadeThumbnailArrows: true
4893		}
4894	};
4895
4896	$.SliderPro.addModule( 'ThumbnailArrows', ThumbnailArrows );
4897
4898})( window, jQuery );
4899
4900// Video module for Slider Pro
4901//
4902// Adds automatic control for several video players and providers
4903;(function( window, $ ) {
4904
4905	"use strict";
4906
4907	var NS = 'Video.' + $.SliderPro.namespace;
4908	
4909	var Video = {
4910
4911		firstInit: false,
4912
4913		initVideo: function() {
4914			this.on( 'update.' + NS, $.proxy( this._videoOnUpdate, this ) );
4915			this.on( 'gotoSlideComplete.' + NS, $.proxy( this._videoOnGotoSlideComplete, this ) );
4916		},
4917
4918		_videoOnUpdate: function() {
4919			var that = this;
4920
4921			// Find all the inline videos and initialize them
4922			this.$slider.find( '.sp-video' ).not( 'a, [data-video-init]' ).each(function() {
4923				var video = $( this );
4924				that._initVideo( video );
4925			});
4926
4927			// Find all the lazy-loaded videos and preinitialize them. They will be initialized
4928			// only when their play button is clicked.
4929			this.$slider.find( 'a.sp-video' ).not( '[data-video-preinit]' ).each(function() {
4930				var video = $( this );
4931				that._preinitVideo( video );
4932			});
4933
4934			// call the 'gotoSlideComplete' method in case the first slide contains a video that
4935			// needs to play automatically
4936			if ( this.firstInit === false ) {
4937				this.firstInit = true;
4938				this._videoOnGotoSlideComplete({ index: this.selectedSlideIndex, previousIndex: -1 });
4939			}
4940		},
4941
4942		// Initialize the target video
4943		_initVideo: function( video ) {
4944			var that = this;
4945
4946			video.attr( 'data-video-init', true )
4947				.videoController();
4948
4949			// When the video starts playing, pause the autoplay if it's running
4950			video.on( 'videoPlay.' + NS, function() {
4951				if ( that.settings.playVideoAction === 'stopAutoplay' && typeof that.stopAutoplay !== 'undefined' ) {
4952					that.stopAutoplay();
4953					that.settings.autoplay = false;
4954				}
4955
4956				// Fire the 'videoPlay' event
4957				var eventObject = { type: 'videoPlay', video: video };
4958				that.trigger( eventObject );
4959				if ( $.isFunction( that.settings.videoPlay ) ) {
4960					that.settings.videoPlay.call( that, eventObject );
4961				}
4962			});
4963
4964			// When the video is paused, restart the autoplay
4965			video.on( 'videoPause.' + NS, function() {
4966				if ( that.settings.pauseVideoAction === 'startAutoplay' && typeof that.startAutoplay !== 'undefined' ) {
4967					that.startAutoplay();
4968					that.settings.autoplay = true;
4969				}
4970
4971				// Fire the 'videoPause' event
4972				var eventObject = { type: 'videoPause', video: video };
4973				that.trigger( eventObject );
4974				if ( $.isFunction( that.settings.videoPause ) ) {
4975					that.settings.videoPause.call( that, eventObject );
4976				}
4977			});
4978
4979			// When the video ends, restart the autoplay (which was paused during the playback), or
4980			// go to the next slide, or replay the video
4981			video.on( 'videoEnded.' + NS, function() {
4982				if ( that.settings.endVideoAction === 'startAutoplay' && typeof that.startAutoplay !== 'undefined' ) {
4983					that.startAutoplay();
4984					that.settings.autoplay = true;
4985				} else if ( that.settings.endVideoAction === 'nextSlide' ) {
4986					that.nextSlide();
4987				} else if ( that.settings.endVideoAction === 'replayVideo' ) {
4988					video.videoController( 'replay' );
4989				}
4990
4991				// Fire the 'videoEnd' event
4992				var eventObject = { type: 'videoEnd', video: video };
4993				that.trigger( eventObject );
4994				if ( $.isFunction(that.settings.videoEnd ) ) {
4995					that.settings.videoEnd.call( that, eventObject );
4996				}
4997			});
4998		},
4999
5000		// Pre-initialize the video. This is for lazy loaded videos.
5001		_preinitVideo: function( video ) {
5002			var that = this;
5003
5004			video.attr( 'data-video-preinit', true );
5005
5006			// When the video poster is clicked, remove the poster and create
5007			// the inline video
5008			video.on( 'click.' + NS, function( event ) {
5009
5010				// If the video is being dragged, don't start the video
5011				if ( that.$slider.hasClass( 'sp-swiping' ) ) {
5012					return;
5013				}
5014
5015				event.preventDefault();
5016
5017				var href = video.attr( 'href' ),
5018					iframe,
5019					provider,
5020					regExp,
5021					match,
5022					id,
5023					src,
5024					videoAttributes,
5025					videoWidth = video.children( 'img' ).attr( 'width' ) || video.children( 'img' ).width(),
5026					videoHeight = video.children( 'img' ).attr( 'height') || video.children( 'img' ).height();
5027
5028				// Check if it's a youtube or vimeo video
5029				if ( href.indexOf( 'youtube' ) !== -1 || href.indexOf( 'youtu.be' ) !== -1 ) {
5030					provider = 'youtube';
5031				} else if ( href.indexOf( 'vimeo' ) !== -1 ) {
5032					provider = 'vimeo';
5033				}
5034
5035				// Get the id of the video
5036				regExp = provider === 'youtube' ? /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/ : /http:\/\/(www\.)?vimeo.com\/(\d+)/;
5037				match = href.match( regExp );
5038				id = match[2];
5039
5040				// Get the source of the iframe that will be created
5041				src = provider === 'youtube' ? '//www.youtube.com/embed/' + id + '?enablejsapi=1&wmode=opaque' : '//player.vimeo.com/video/'+ id +'?api=1';
5042				
5043				// Get the attributes passed to the video link and then pass them to the iframe's src
5044				videoAttributes = href.split( '?' )[ 1 ];
5045
5046				if ( typeof videoAttributes !== 'undefined' ) {
5047					videoAttributes = videoAttributes.split( '&' );
5048
5049					$.each( videoAttributes, function( index, value ) {
5050						if ( value.indexOf( id ) === -1 ) {
5051							src += '&' + value;
5052						}
5053					});
5054				}
5055
5056				// Create the iframe
5057				iframe = $( '<iframe></iframe>' )
5058					.attr({
5059						'src': src,
5060						'width': videoWidth,
5061						'height': videoHeight,
5062						'class': video.attr( 'class' ),
5063						'frameborder': 0,
5064						'allowfullscreen': 'allowfullscreen'
5065					}).insertBefore( video );
5066
5067				// Initialize the video and play it
5068				that._initVideo( iframe );
5069				iframe.videoController( 'play' );
5070
5071				// Hide the video poster
5072				video.css( 'display', 'none' );
5073			});
5074		},
5075
5076		// Called when a new slide is selected
5077		_videoOnGotoSlideComplete: function( event ) {
5078
5079			// Get the video from the previous slide
5080			var previousVideo = this.$slides.find( '.sp-slide' ).eq( event.previousIndex ).find( '.sp-video[data-video-init]' );
5081
5082			// Handle the video from the previous slide by stopping it, or pausing it,
5083			// or remove it, depending on the value of the 'leaveVideoAction' option.
5084			if ( event.previousIndex !== -1 && previousVideo.length !== 0 ) {
5085				if ( this.settings.leaveVideoAction === 'stopVideo' ) {
5086					previousVideo.videoController( 'stop' );
5087				} else if ( this.settings.leaveVideoAction === 'pauseVideo' ) {
5088					previousVideo.videoController( 'pause' );
5089				} else if ( this.settings.leaveVideoAction === 'removeVideo'  ) {
5090					// If the video was lazy-loaded, remove it and show the poster again. If the video
5091					// was not lazy-loaded, but inline, stop the video.
5092					if ( previousVideo.siblings( 'a.sp-video' ).length !== 0 ) {
5093						previousVideo.siblings( 'a.sp-video' ).css( 'display', '' );
5094						previousVideo.videoController( 'destroy' );
5095						previousVideo.remove();
5096					} else {
5097						previousVideo.videoController( 'stop' );
5098					}
5099				}
5100			}
5101
5102			// Handle the video from the selected slide
5103			if ( this.settings.reachVideoAction === 'playVideo' ) {
5104				var loadedVideo = this.$slides.find( '.sp-slide' ).eq( event.index ).find( '.sp-video[data-video-init]' ),
5105					unloadedVideo = this.$slides.find( '.sp-slide' ).eq( event.index ).find( '.sp-video[data-video-preinit]' );
5106
5107				// If the video was already initialized, play it. If it's not initialized (because
5108				// it's lazy loaded) initialize it and play it.
5109				if ( loadedVideo.length !== 0 ) {
5110					loadedVideo.videoController( 'play' );
5111				} else if ( unloadedVideo.length !== 0 ) {
5112					unloadedVideo.trigger( 'click.' + NS );
5113				}
5114			}
5115		},
5116
5117		// Destroy the module
5118		destroyVideo: function() {
5119			this.$slider.find( '.sp-video[ data-video-preinit ]' ).each(function() {
5120				var video = $( this );
5121				video.removeAttr( 'data-video-preinit' );
5122				video.off( 'click.' + NS );
5123			});
5124
5125			// Loop through the all the videos and destroy them
5126			this.$slider.find( '.sp-video[ data-video-init ]' ).each(function() {
5127				var video = $( this );
5128				video.removeAttr( 'data-video-init' );
5129				video.off( 'Video' );
5130				video.videoController( 'destroy' );
5131			});
5132
5133			this.off( 'update.' + NS );
5134			this.off( 'gotoSlideComplete.' + NS );
5135		},
5136
5137		videoDefaults: {
5138
5139			// Sets the action that the video will perform when its slide container is selected
5140			// ( 'playVideo' and 'none' )
5141			reachVideoAction: 'none',
5142
5143			// Sets the action that the video will perform when another slide is selected
5144			// ( 'stopVideo', 'pauseVideo', 'removeVideo' and 'none' )
5145			leaveVideoAction: 'pauseVideo',
5146
5147			// Sets the action that the slider will perform when the video starts playing
5148			// ( 'stopAutoplay' and 'none' )
5149			playVideoAction: 'stopAutoplay',
5150
5151			// Sets the action that the slider will perform when the video is paused
5152			// ( 'startAutoplay' and 'none' )
5153			pauseVideoAction: 'none',
5154
5155			// Sets the action that the slider will perform when the video ends
5156			// ( 'startAutoplay', 'nextSlide', 'replayVideo' and 'none' )
5157			endVideoAction: 'none',
5158
5159			// Called when the video starts playing
5160			videoPlay: function() {},
5161
5162			// Called when the video is paused
5163			videoPause: function() {},
5164
5165			// Called when the video ends
5166			videoEnd: function() {}
5167		}
5168	};
5169
5170	$.SliderPro.addModule( 'Video', Video );
5171	
5172})( window, jQuery );
5173
5174// Video Controller jQuery plugin
5175// Creates a universal controller for multiple video types and providers
5176;(function( $ ) {
5177
5178	"use strict";
5179
5180// Check if an iOS device is used.
5181// This information is important because a video can not be
5182// controlled programmatically unless the user has started the video manually.
5183var	isIOS = window.navigator.userAgent.match( /(iPad|iPhone|iPod)/g ) ? true : false;
5184
5185var VideoController = function( instance, options ) {
5186	this.$video = $( instance );
5187	this.options = options;
5188	this.settings = {};
5189	this.player = null;
5190
5191	this._init();
5192};
5193
5194VideoController.prototype = {
5195
5196	_init: function() {
5197		this.settings = $.extend( {}, this.defaults, this.options );
5198
5199		var that = this,
5200			players = $.VideoController.players,
5201			videoID = this.$video.attr( 'id' );
5202
5203		// Loop through the available video players
5204		// and check if the targeted video element is supported by one of the players.
5205		// If a compatible type is found, store the video type.
5206		for ( var name in players ) {
5207			if ( typeof players[ name ] !== 'undefined' && players[ name ].isType( this.$video ) ) {
5208				this.player = new players[ name ]( this.$video );
5209				break;
5210			}
5211		}
5212
5213		// Return if the player could not be instantiated
5214		if ( this.player === null ) {
5215			return;
5216		}
5217
5218		// Add event listeners
5219		var events = [ 'ready', 'start', 'play', 'pause', 'ended' ];
5220		
5221		$.each( events, function( index, element ) {
5222			var event = 'video' + element.charAt( 0 ).toUpperCase() + element.slice( 1 );
5223
5224			that.player.on( element, function() {
5225				that.trigger({ type: event, video: videoID });
5226				if ( $.isFunction( that.settings[ event ] ) ) {
5227					that.settings[ event ].call( that, { type: event, video: videoID } );
5228				}
5229			});
5230		});
5231	},
5232	
5233	play: function() {
5234		if ( isIOS === true && this.player.isStarted() === false || this.player.getState() === 'playing' ) {
5235			return;
5236		}
5237
5238		this.player.play();
5239	},
5240	
5241	stop: function() {
5242		if ( isIOS === true && this.player.isStarted() === false || this.player.getState() === 'stopped' ) {
5243			return;
5244		}
5245
5246		this.player.stop();
5247	},
5248	
5249	pause: function() {
5250		if ( isIOS === true && this.player.isStarted() === false || this.player.getState() === 'paused' ) {
5251			return;
5252		}
5253
5254		this.player.pause();
5255	},
5256
5257	replay: function() {
5258		if ( isIOS === true && this.player.isStarted() === false ) {
5259			return;
5260		}
5261		
5262		this.player.replay();
5263	},
5264
5265	on: function( type, callback ) {
5266		return this.$video.on( type, callback );
5267	},
5268	
5269	off: function( type ) {
5270		return this.$video.off( type );
5271	},
5272
5273	trigger: function( data ) {
5274		return this.$video.triggerHandler( data );
5275	},
5276
5277	destroy: function() {
5278		if ( this.player.isStarted() === true ) {
5279			this.stop();
5280		}
5281
5282		this.player.off( 'ready' );
5283		this.player.off( 'start' );
5284		this.player.off( 'play' );
5285		this.player.off( 'pause' );
5286		this.player.off( 'ended' );
5287
5288		this.$video.removeData( 'videoController' );
5289	},
5290
5291	defaults: {
5292		videoReady: function() {},
5293		videoStart: function() {},
5294		videoPlay: function() {},
5295		videoPause: function() {},
5296		videoEnded: function() {}
5297	}
5298};
5299
5300$.VideoController = {
5301	players: {},
5302
5303	addPlayer: function( name, player ) {
5304		this.players[ name ] = player;
5305	}
5306};
5307
5308$.fn.videoController = function( options ) {
5309	var args = Array.prototype.slice.call( arguments, 1 );
5310
5311	return this.each(function() {
5312		// Instantiate the video controller or call a function on the current instance
5313		if ( typeof $( this ).data( 'videoController' ) === 'undefined' ) {
5314			var newInstance = new VideoController( this, options );
5315
5316			// Store a reference to the instance created
5317			$( this ).data( 'videoController', newInstance );
5318		} else if ( typeof options !== 'undefined' ) {
5319			var	currentInstance = $( this ).data( 'videoController' );
5320
5321			// Check the type of argument passed
5322			if ( typeof currentInstance[ options ] === 'function' ) {
5323				currentInstance[ options ].apply( currentInstance, args );
5324			} else {
5325				$.error( options + ' does not exist in videoController.' );
5326			}
5327		}
5328	});
5329};
5330
5331// Base object for the video players
5332var Video = function( video ) {
5333	this.$video = video;
5334	this.player = null;
5335	this.ready = false;
5336	this.started = false;
5337	this.state = '';
5338	this.events = $({});
5339
5340	this._init();
5341};
5342
5343Video.prototype = {
5344	_init: function() {},
5345
5346	play: function() {},
5347
5348	pause: function() {},
5349
5350	stop: function() {},
5351
5352	replay: function() {},
5353
5354	isType: function() {},
5355
5356	isReady: function() {
5357		return this.ready;
5358	},
5359
5360	isStarted: function() {
5361		return this.started;
5362	},
5363
5364	getState: function() {
5365		return this.state;
5366	},
5367
5368	on: function( type, callback ) {
5369		return this.events.on( type, callback );
5370	},
5371	
5372	off: function( type ) {
5373		return this.events.off( type );
5374	},
5375
5376	trigger: function( data ) {
5377		return this.events.triggerHandler( data );
5378	}
5379};
5380
5381// YouTube video
5382var YoutubeVideoHelper = {
5383	youtubeAPIAdded: false,
5384	youtubeVideos: []
5385};
5386
5387var YoutubeVideo = function( video ) {
5388	this.init = false;
5389	var youtubeAPILoaded = window.YT && window.YT.Player;
5390
5391	if ( typeof youtubeAPILoaded !== 'undefined' ) {
5392		Video.call( this, video );
5393	} else {
5394		YoutubeVideoHelper.youtubeVideos.push({ 'video': video, 'scope': this });
5395		
5396		if ( YoutubeVideoHelper.youtubeAPIAdded === false ) {
5397			YoutubeVideoHelper.youtubeAPIAdded = true;
5398
5399			var tag = document.createElement( 'script' );
5400			tag.src = "//www.youtube.com/player_api";
5401			var firstScriptTag = document.getElementsByTagName( 'script' )[0];
5402			firstScriptTag.parentNode.insertBefore( tag, firstScriptTag );
5403
5404			window.onYouTubePlayerAPIReady = function() {
5405				$.each( YoutubeVideoHelper.youtubeVideos, function( index, element ) {
5406					Video.call( element.scope, element.video );
5407				});
5408			};
5409		}
5410	}
5411};
5412
5413YoutubeVideo.prototype = new Video();
5414YoutubeVideo.prototype.constructor = YoutubeVideo;
5415$.VideoController.addPlayer( 'YoutubeVideo', YoutubeVideo );
5416
5417YoutubeVideo.isType = function( video ) {
5418	if ( video.is( 'iframe' ) ) {
5419		var src = video.attr( 'src' );
5420
5421		if ( src.indexOf( 'youtube.com' ) !== -1 || src.indexOf( 'youtu.be' ) !== -1 ) {
5422			return true;
5423		}
5424	}
5425
5426	return false;
5427};
5428
5429YoutubeVideo.prototype._init = function() {
5430	this.init = true;
5431	this._setup();
5432};
5433	
5434YoutubeVideo.prototype._setup = function() {
5435	var that = this;
5436
5437	// Get a reference to the player
5438	this.player = new YT.Player( this.$video[0], {
5439		events: {
5440			'onReady': function() {
5441				that.trigger({ type: 'ready' });
5442				that.ready = true;
5443			},
5444			
5445			'onStateChange': function( event ) {
5446				switch ( event.data ) {
5447					case YT.PlayerState.PLAYING:
5448						if (that.started === false) {
5449							that.started = true;
5450							that.trigger({ type: 'start' });
5451						}
5452
5453						that.state = 'playing';
5454						that.trigger({ type: 'play' });
5455						break;
5456					
5457					case YT.PlayerState.PAUSED:
5458						that.state = 'paused';
5459						that.trigger({ type: 'pause' });
5460						break;
5461					
5462					case YT.PlayerState.ENDED:
5463						that.state = 'ended';
5464						that.trigger({ type: 'ended' });
5465						break;
5466				}
5467			}
5468		}
5469	});
5470};
5471
5472YoutubeVideo.prototype.play = function() {
5473	var that = this;
5474
5475	if ( this.ready === true ) {
5476		this.player.playVideo();
5477	} else {
5478		var timer = setInterval(function() {
5479			if ( that.ready === true ) {
5480				clearInterval( timer );
5481				that.player.playVideo();
5482			}
5483		}, 100 );
5484	}
5485};
5486
5487YoutubeVideo.prototype.pause = function() {
5488	// On iOS, simply pausing the video can make other videos unresponsive
5489	// so we stop the video instead.
5490	if ( isIOS === true ) {
5491		this.stop();
5492	} else {
5493		this.player.pauseVideo();
5494	}
5495};
5496
5497YoutubeVideo.prototype.stop = function() {
5498	this.player.seekTo( 1 );
5499	this.player.stopVideo();
5500	this.state = 'stopped';
5501};
5502
5503YoutubeVideo.prototype.replay = function() {
5504	this.player.seekTo( 1 );
5505	this.player.playVideo();
5506};
5507
5508YoutubeVideo.prototype.on = function( type, callback ) {
5509	var that = this;
5510
5511	if ( this.init === true ) {
5512		Video.prototype.on.call( this, type, callback );
5513	} else {
5514		var timer = setInterval(function() {
5515			if ( that.init === true ) {
5516				clearInterval( timer );
5517				Video.prototype.on.call( that, type, callback );
5518			}
5519		}, 100 );
5520	}
5521};
5522
5523// Vimeo video
5524var VimeoVideoHelper = {
5525	vimeoAPIAdded: false,
5526	vimeoVideos: []
5527};
5528
5529var VimeoVideo = function( video ) {
5530	this.init = false;
5531
5532	if ( typeof window.Froogaloop !== 'undefined' ) {
5533		Video.call( this, video );
5534	} else {
5535		VimeoVideoHelper.vimeoVideos.push({ 'video': video, 'scope': this });
5536
5537		if ( VimeoVideoHelper.vimeoAPIAdded === false ) {
5538			VimeoVideoHelper.vimeoAPIAdded = true;
5539
5540			var tag = document.createElement('script');
5541			tag.src = "//a.vimeocdn.com/js/froogaloop2.min.js";
5542			var firstScriptTag = document.getElementsByTagName( 'script' )[0];
5543			firstScriptTag.parentNode.insertBefore( tag, firstScriptTag );
5544		
5545			var checkVimeoAPITimer = setInterval(function() {
5546				if ( typeof window.Froogaloop !== 'undefined' ) {
5547					clearInterval( checkVimeoAPITimer );
5548					
5549					$.each( VimeoVideoHelper.vimeoVideos, function( index, element ) {
5550						Video.call( element.scope, element.video );
5551					});
5552				}
5553			}, 100 );
5554		}
5555	}
5556};
5557
5558VimeoVideo.prototype = new Video();
5559VimeoVideo.prototype.constructor = VimeoVideo;
5560$.VideoController.addPlayer( 'VimeoVideo', VimeoVideo );
5561
5562VimeoVideo.isType = function( video ) {
5563	if ( video.is( 'iframe' ) ) {
5564		var src = video.attr('src');
5565
5566		if ( src.indexOf( 'vimeo.com' ) !== -1 ) {
5567			return true;
5568		}
5569	}
5570
5571	return false;
5572};
5573
5574VimeoVideo.prototype._init = function() {
5575	this.init = true;
5576	this._setup();
5577};
5578
5579VimeoVideo.prototype._setup = function() {
5580	var that = this;
5581
5582	// Get a reference to the player
5583	this.player = $f( this.$video[0] );
5584	
5585	this.player.addEvent( 'ready', function() {
5586		that.ready = true;
5587		that.trigger({ type: 'ready' });
5588		
5589		that.player.addEvent( 'play', function() {
5590			if ( that.started === false ) {
5591				that.started = true;
5592				that.trigger({ type: 'start' });
5593			}
5594
5595			that.state = 'playing';
5596			that.trigger({ type: 'play' });
5597		});
5598		
5599		that.player.addEvent( 'pause', function() {
5600			that.state = 'paused';
5601			that.trigger({ type: 'pause' });
5602		});
5603		
5604		that.player.addEvent( 'finish', function() {
5605			that.state = 'ended';
5606			that.trigger({ type: 'ended' });
5607		});
5608	});
5609};
5610
5611VimeoVideo.prototype.play = function() {
5612	var that = this;
5613
5614	if ( this.ready === true ) {
5615		this.player.api( 'play' );
5616	} else {
5617		var timer = setInterval(function() {
5618			if ( that.ready === true ) {
5619				clearInterval( timer );
5620				that.player.api( 'play' );
5621			}
5622		}, 100 );
5623	}
5624};
5625
5626VimeoVideo.prototype.pause = function() {
5627	this.player.api( 'pause' );
5628};
5629
5630VimeoVideo.prototype.stop = function() {
5631	this.player.api( 'seekTo', 0 );
5632	this.player.api( 'pause' );
5633	this.state = 'stopped';
5634};
5635
5636VimeoVideo.prototype.replay = function() {
5637	this.player.api( 'seekTo', 0 );
5638	this.player.api( 'play' );
5639};
5640
5641VimeoVideo.prototype.on = function( type, callback ) {
5642	var that = this;
5643
5644	if ( this.init === true ) {
5645		Video.prototype.on.call( this, type, callback );
5646	} else {
5647		var timer = setInterval(function() {
5648			if ( that.init === true ) {
5649				clearInterval( timer );
5650				Video.prototype.on.call( that, type, callback );
5651			}
5652		}, 100 );
5653	}
5654};
5655
5656// HTML5 video
5657var HTML5Video = function( video ) {
5658	Video.call( this, video );
5659};
5660
5661HTML5Video.prototype = new Video();
5662HTML5Video.prototype.constructor = HTML5Video;
5663$.VideoController.addPlayer( 'HTML5Video', HTML5Video );
5664
5665HTML5Video.isType = function( video ) {
5666	if ( video.is( 'video' ) && video.hasClass( 'video-js' ) === false && video.hasClass( 'sublime' ) === false ) {
5667		return true;
5668	}
5669
5670	return false;
5671};
5672
5673HTML5Video.prototype._init = function() {
5674	var that = this;
5675
5676	// Get a reference to the player
5677	this.player = this.$video[0];
5678	
5679	var checkVideoReady = setInterval(function() {
5680		if ( that.player.readyState === 4 ) {
5681			clearInterval( checkVideoReady );
5682
5683			that.ready = true;
5684			that.trigger({ type: 'ready' });
5685
5686			that.player.addEventListener( 'play', function() {
5687				if ( that.started === false ) {
5688					that.started = true;
5689					that.trigger({ type: 'start' });
5690				}
5691
5692				that.state = 'playing';
5693				that.trigger({ type: 'play' });
5694			});
5695			
5696			that.player.addEventListener( 'pause', function() {
5697				that.state = 'paused';
5698				that.trigger({ type: 'pause' });
5699			});
5700			
5701			that.player.addEventListener( 'ended', function() {
5702				that.state = 'ended';
5703				that.trigger({ type: 'ended' });
5704			});
5705		}
5706	}, 100 );
5707};
5708
5709HTML5Video.prototype.play = function() {
5710	var that = this;
5711
5712	if ( this.ready === true ) {
5713		this.player.play();
5714	} else {
5715		var timer = setInterval(function() {
5716			if ( that.ready === true ) {
5717				clearInterval( timer );
5718				that.player.play();
5719			}
5720		}, 100 );
5721	}
5722};
5723
5724HTML5Video.prototype.pause = function() {
5725	this.player.pause();
5726};
5727
5728HTML5Video.prototype.stop = function() {
5729	this.player.currentTime = 0;
5730	this.player.pause();
5731	this.state = 'stopped';
5732};
5733
5734HTML5Video.prototype.replay = function() {
5735	this.player.currentTime = 0;
5736	this.player.play();
5737};
5738
5739// VideoJS video
5740var VideoJSVideo = function( video ) {
5741	Video.call( this, video );
5742};
5743
5744VideoJSVideo.prototype = new Video();
5745VideoJSVideo.prototype.constructor = VideoJSVideo;
5746$.VideoController.addPlayer( 'VideoJSVideo', VideoJSVideo );
5747
5748VideoJSVideo.isType = function( video ) {
5749	if ( ( typeof video.attr( 'data-videojs-id' ) !== 'undefined' || video.hasClass( 'video-js' ) ) && typeof videojs !== 'undefined' ) {
5750		return true;
5751	}
5752
5753	return false;
5754};
5755
5756VideoJSVideo.prototype._init = function() {
5757	var that = this,
5758		videoID = this.$video.hasClass( 'video-js' ) ? this.$video.attr( 'id' ) : this.$video.attr( 'data-vi
5758deojs-id' );
5759	
5760	this.player = videojs( videoID );
5761
5762	this.player.ready(function() {
5763		that.ready = true;
5764		that.trigger({ type: 'ready' });
5765
5766		that.player.on( 'play', function() {
5767			if ( that.started === false ) {
5768				that.started = true;
5769				that.trigger({ type: 'start' });
5770			}
5771
5772			that.state = 'playing';
5773			that.trigger({ type: 'play' });
5774		});
5775		
5776		that.player.on( 'pause', function() {
5777			that.state = 'paused';
5778			that.trigger({ type: 'pause' });
5779		});
5780		
5781		that.player.on( 'ended', function() {
5782			that.state = 'ended';
5783			that.trigger({ type: 'ended' });
5784		});
5785	});
5786};
5787
5788VideoJSVideo.prototype.play = function() {
5789	this.player.play();
5790};
5791
5792VideoJSVideo.prototype.pause = function() {
5793	this.player.pause();
5794};
5795
5796VideoJSVideo.prototype.stop = function() {
5797	this.player.currentTime( 0 );
5798	this.player.pause();
5799	this.state = 'stopped';
5800};
5801
5802VideoJSVideo.prototype.replay = function() {
5803	this.player.currentTime( 0 );
5804	this.player.play();
5805};
5806
5807// Sublime video
5808var SublimeVideo = function( video ) {
5809	Video.call( this, video );
5810};
5811
5812SublimeVideo.prototype = new Video();
5813SublimeVideo.prototype.constructor = SublimeVideo;
5814$.VideoController.addPlayer( 'SublimeVideo', SublimeVideo );
5815
5816SublimeVideo.isType = function( video ) {
5817	if ( video.hasClass( 'sublime' ) && typeof sublime !== 'undefined' ) {
5818		return true;
5819	}
5820
5821	return false;
5822};
5823
5824SublimeVideo.prototype._init = function() {
5825	var that = this;
5826
5827	sublime.ready(function() {
5828		// Get a reference to the player
5829		that.player = sublime.player( that.$video.attr( 'id' ) );
5830
5831		that.ready = true;
5832		that.trigger({ type: 'ready' });
5833
5834		that.player.on( 'play', function() {
5835			if ( that.started === false ) {
5836				that.started = true;
5837				that.trigger({ type: 'start' });
5838			}
5839
5840			that.state = 'playing';
5841			that.trigger({ type: 'play' });
5842		});
5843
5844		that.player.on( 'pause', function() {
5845			that.state = 'paused';
5846			that.trigger({ type: 'pause' });
5847		});
5848
5849		that.player.on( 'stop', function() {
5850			that.state = 'stopped';
5851			that.trigger({ type: 'stop' });
5852		});
5853
5854		that.player.on( 'end', function() {
5855			that.state = 'ended';
5856			that.trigger({ type: 'ended' });
5857		});
5858	});
5859};
5860
5861SublimeVideo.prototype.play = function() {
5862	this.player.play();
5863};
5864
5865SublimeVideo.prototype.pause = function() {
5866	this.player.pause();
5867};
5868
5869SublimeVideo.prototype.stop = function() {
5870	this.player.stop();
5871};
5872
5873SublimeVideo.prototype.replay = function() {
5874	this.player.stop();
5875	this.player.play();
5876};
5877
5878// JWPlayer video
5879var JWPlayerVideo = function( video ) {
5880	Video.call( this, video );
5881};
5882
5883JWPlayerVideo.prototype = new Video();
5884JWPlayerVideo.prototype.constructor = JWPlayerVideo;
5885$.VideoController.addPlayer( 'JWPlayerVideo', JWPlayerVideo );
5886
5887JWPlayerVideo.isType = function( video ) {
5888	if ( ( typeof video.attr( 'data-jwplayer-id' ) !== 'undefined' || video.hasClass( 'jwplayer' ) || video.find( "object[data*='jwplayer']" ).length !== 0 ) &&
5889		typeof jwplayer !== 'undefined') {
5890		return true;
5891	}
5892
5893	return false;
5894};
5895
5896JWPlayerVideo.prototype._init = function() {
5897	var that = this,
5898		videoID;
5899
5900	if ( this.$video.hasClass( 'jwplayer' ) ) {
5901		videoID = this.$video.attr( 'id' );
5902	} else if ( typeof this.$video.attr( 'data-jwplayer-id' ) !== 'undefined' ) {
5903		videoID = this.$video.attr( 'data-jwplayer-id');
5904	} else if ( this.$video.find( "object[data*='jwplayer']" ).length !== 0 ) {
5905		videoID = this.$video.find( 'object' ).attr( 'id' );
5906	}
5907
5908	// Get a reference to the player
5909	this.player = jwplayer( videoID );
5910
5911	this.player.onReady(function() {
5912		that.ready = true;
5913		that.trigger({ type: 'ready' });
5914	
5915		that.player.onPlay(function() {
5916			if ( that.started === false ) {
5917				that.started = true;
5918				that.trigger({ type: 'start' });
5919			}
5920
5921			that.state = 'playing';
5922			that.trigger({ type: 'play' });
5923		});
5924
5925		that.player.onPause(function() {
5926			that.state = 'paused';
5927			that.trigger({ type: 'pause' });
5928		});
5929		
5930		that.player.onComplete(function() {
5931			that.state = 'ended';
5932			that.trigger({ type: 'ended' });
5933		});
5934	});
5935};
5936
5937JWPlayerVideo.prototype.play = function() {
5938	this.player.play( true );
5939};
5940
5941JWPlayerVideo.prototype.pause = function() {
5942	this.player.pause( true );
5943};
5944
5945JWPlayerVideo.prototype.stop = function() {
5946	this.player.stop();
5947	this.state = 'stopped';
5948};
5949
5950JWPlayerVideo.prototype.replay = function() {
5951	this.player.seek( 0 );
5952	this.player.play( true );
5953};
5954
5955})( jQuery );

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.