wrapper to contain thumbnails
this.$thumbWrap = $('
').appendTo(this.$sliderWrap);
// Loop to apply thumbnail widths/margins to
wraps, appending an image clone to each
for (var i = 0; i < this.totalSlides; i++) {
var $thumb = $('').css({'width' : width, 'marginLeft' : this.settings['thumbMargin'] + '%'}).attr({'href' : '#'});
$(clones[i]).removeAttr('style').appendTo(this.$thumbWrap).wrap($thumb);
}
// Safety margin to stop IE7 wrapping the thumbnails (no visual effect in other browsers)
this.$thumbWrap.children().last().css('margin-right', -10);
// Add active class to starting slide's respective thumb
$(this.$thumbWrap.find('a')[this.settings['startSlide']]).addClass('active');
// Listen for click events on thumnails
this.$thumbWrap.on('click', 'a', function (e) {
e.preventDefault();
// Get identifier from thumb class
var cl = parseInt($(this).attr('class').split('-')[3]);
// Call transition
_this.transition(cl);
});
}
,captions: function() {
var _this = this,
$captions = this.$slides.find('.rs-caption');
// User-defined caption width
$captions.css({'width' : _this.settings['captionWidth'] + '%', 'opacity' : 0});
// Display starting slide's caption
this.$currentSlide.find('.rs-caption').css({'opacity' : 1});
// Add CSS3 transition vendor prefixes
$captions.each(function() {
$(this).prefixes({
'transition':'opacity ' + _this.settings['transitionDuration'] + 'ms ease-in-out',
'transform' : 'translateZ(0)' // Necessary for some reason to stop caption opacity jumping when translateZ is also applied to '.rs-slide-bg' (RS.$sliderBG)
});
});
}
,transition: function (slideNum, forward) {
// If inProgress flag is not set (i.e. if not mid-transition)
if (!this.inProgress) {
// If not already on requested slide
if (slideNum !== this.currentPlace) {
// Check whether the requested slide index is ahead or behind in the array (if not passed in as param)
if(forward === undefined) {
forward = slideNum > this.currentPlace ? true : false;
}
// Assign next slide prop (elem)
this.$nextSlide = $(this.$slides[slideNum]);
// Assign next slide index prop (int)
this.currentPlace = slideNum;
// User-defined function, fires with transition
this.settings['onChange']();
// Instantiate new Transition object, passing in self (RS obj), transition type (string), direction (bool)
new Transition(this, this.settings['transition'], forward);
// If thumbnails exist, revise active class states
if (this.settings['controls'] === 'thumbs') {
this.$thumbWrap.find('a').removeClass('active');
$(this.$thumbWrap.find('a')[slideNum]).addClass('active');
}
}
}
}
};
// Transition object constructor
function Transition(RS, transition, forward) {
this.RS = RS; // RS (RefineSlide) object
this.RS.inProgress = true; // Set RS inProgress flag to prevent additional Transition objects being instantiated until transition end
this.forward = forward; // Bool: true for forward, false for backward
this.transition = transition; // String: name of transition requested
this.fallback3d = this.RS.settings['fallback3d']; // String: fallback to use when 3D transforms aren't supported
this.init(); // Call Transition initialisation method
}
// Transition object Prototype
Transition.prototype = {
// Fallback to use if CSS transitions are unsupported
fallback: 'fade'
// Array of possible animations
,anims: [ 'fade', 'sliceH', 'sliceV', 'slideH', 'slideV', 'scale', 'blockScale', 'kaleidoscope', 'fan', 'blindH', 'blindV']
,init: function () {
// Call requested transition method
this[this.transition]();
}
,before: function (callback) {
var _this = this;
// Prepare slide opacity & z-index
this.RS.$currentSlide.css('z-index', 2);
this.RS.$nextSlide.css({'opacity' : 1, 'z-index' : 1});
// Fade out/in captions with CSS/JS depending on browser capability
if (this.RS.cssTransitions) {
this.RS.$currentSlide.find('.rs-caption').css('opacity', 0);
this.RS.$nextSlide.find('.rs-caption').css('opacity', 1);
} else {
this.RS.$currentSlide.find('.rs-caption').animate({'opacity' : 0}, _this.RS.settings['transitionDuration']);
this.RS.$nextSlide.find('.rs-caption').animate({'opacity' : 1}, _this.RS.settings['transitionDuration']);
}
// Check if transition describes a setup method
if (typeof this.setup === 'function') {
// Setup required by transition
var transition = this.setup();
setTimeout(function () {
callback(transition);
}, 40);
} else {
// Transition execution
this.execute();
}
// Listen for CSS transition end on elem (set by transition)
if (this.RS.cssTransitions) {
$(this.listenTo).one('webkitTransitionEnd transitionend oTransitionEnd msTransitionend MSTransitionEnd', function () {
// Post-transition reset
_this.after();
});
}
}
,after: function () {
// Reset transition CSS
this.RS.$slider.removeAttr('style');
this.RS.$currentSlide.removeAttr('style').css('opacity', 0);
this.RS.$nextSlide.removeAttr('style').css({'z-index': 2,'opacity' : 1});
// Additional reset steps required by transition (if any exist)
if (typeof this.reset === 'function') this.reset();
// If slideshow is active, reset the timeout
if (this.RS.settings['autoPlay']) {
clearTimeout(this.RS.cycling);
this.RS.setAutoPlay();
}
// Assign new slide position
this.RS.$currentSlide = this.RS.$nextSlide;
// Remove RS obj inProgress flag (i.e. allow new Transition to be instantiated)
this.RS.inProgress = false;
// User-defined function, fires after transition has ended
this.RS.settings['afterChange']();
}
,fade: function () {
var _this = this;
// If CSS transitions are supported by browser
if (this.RS.cssTransitions) {
// Setup steps
this.setup = function () {
// Set event listener to next slide elem
_this.listenTo = _this.RS.$currentSlide;
// Add CSS3 transition vendor prefixes
_this.RS.$currentSlide.prefixes({'transition' : 'opacity ' + _this.RS.settings['transitionDuration'] + 'ms ease-in-out'});
};
// Execution steps
this.execute = function () {
// Display next slide over current slide
_this.RS.$currentSlide.css({'opacity' : 0});
}
} else { // JS animation fallback
this.execute = function () {
_this.RS.$currentSlide.animate({'opacity' : 0}, _this.RS.settings['transitionDuration'], function () {
// Reset steps
_this.after();
});
}
}
this.before(function () {
// Fire on setup callback
_this.execute();
});
}
// cube() method is used by cubeH() & cubeV() - not for calling directly
,cube: function (tz, ntx, nty, nrx, nry, wrx, wry) { // Args: translateZ, (next slide) translateX, (next slide) translateY, (next slide) rotateX, (next slide) rotateY, (wrap) rotateX, (wrap) rotateY
// Fallback if browser does not support 3d transforms/CSS transitions
if (!this.RS.cssTransitions || !this.RS.cssTransforms3d) {
return this[this['fallback3d']](); // User-defined transition
}
var _this = this;
// Setup steps
this.setup = function () {
// Set event listener to '.rs-slider'
_this.listenTo = _this.RS.$slider;
// Set CSS3 perspective vendor prefixes on '.rs-slide-bg' elem
// see http://desandro.github.com/3dtransforms/docs/perspective.html
this.RS.$sliderBG.prefixes({'perspective' : 1000});
var slideProps = {
'transform' : 'translateZ(' + tz + 'px)',
'backface-visibility' : 'hidden'
};
// CSS3 props for slide - s
_this.RS.$currentSlide.prefixes(slideProps);
_this.RS.$nextSlide.prefixes(slideProps);
// CSS3 props for slider
_this.RS.$slider.prefixes({
'transition' : 'none',
'transform-style' : 'preserve-3d',
'transform' : 'translateZ(-' + tz + 'px)'
});
// CSS3 prop for next slide -
_this.RS.$nextSlide.css({'opacity':1}).prefixes({
'transform' : 'translateZ(0px) translateY(' + nty + 'px) translateX(' + ntx + 'px) rotateY('+ nry +'deg) rotateX('+ nrx +'deg)'
});
};
// Execution steps
this.execute = function () {
var output = [];
// Loop through vendor prefixes to make CSS3 transform rules (more involved than just calling prefixes() func in this case as need prefix twice: e.g. '-moz-transition: -moz-transition...')
for (var i = 0; i < testBrowser.browserVendors.length; i++) {
output[testBrowser.browserVendors[i] + 'transition'] = testBrowser.browserVendors[i] + 'transform ' + _this.RS.settings['transitionDuration'] + 'ms ease-in-out';
}
// Add CSS3 props to elem
_this.RS.$slider.prefixes(output);
// Additional CSS3 prop
_this.RS.$slider.prefixes({'transform' : 'translateZ(-' + tz + 'px) rotateX('+ wrx +'deg) rotateY('+ wry +'deg)'});
};
this.before(function () {
// Fire on setup callback
_this.execute();
});
}
,cubeH: function () {
// Set to half of slide width
var dimension = $(this.RS.$slides).width() / 2;
// If next slide is ahead in array
if (this.forward) {
this.cube(dimension, dimension, 0, 0, 90, 0, -90);
} else {
this.cube(dimension, -dimension, 0, 0, -90, 0, 90);
}
}
,cubeV: function () {
// Set to half of slide height
var dimension = $(this.RS.$slides).height() / 2;
// If next slide is ahead in array
if (this.forward) {
this.cube(dimension, 0, -dimension, 90, 0, -90, 0);
} else {
this.cube(dimension, 0, dimension, -90, 0, 90, 0);
}
}
// grid() method is used by many transitions - not for calling directly
// Grid calculations are based on those in the awesome flux slider (joelambert.co.uk/flux)
,grid: function (cols, rows, ro, tx, ty, sc, op) {// Args: columns, rows, rotate, translateX, translateY, scale, opacity
// Fallback if browser does not support CSS transitions
if (!this.RS.cssTransitions) {
return this[this['fallback']]();
}
var _this = this;
// Setup steps
this.setup = function () {
// The time (in ms) added to/subtracted from the delay total for each new gridlet
var count = (_this.RS.settings['transitionDuration']) / (cols + rows);
// Gridlet creator (divisions of the image grid, positioned with background-images to replicate the look of an entire slide image when assembled)
function gridlet(width, height, top, left, top_bk, left_bk, mar_left, src, imgWidth, imgHeight, c, r) {
var delay = (c + r) * count;
// Return a gridlet elem with styles for specific transition
return $('').css({
'width' : width,
'height' : height,
'top' : top,
'left' : left,
'background-image' : 'url(' + src + ')',
'background-position' : '-' + left_bk + 'px -' + top_bk + 'px',
'background-size' : imgWidth + 'px ' + imgHeight + 'px'
}).prefixes({
'transition' : 'all ' + _this.RS.settings['transitionDuration'] + 'ms ease-in-out ' + delay + 'ms',
'transform' : 'none'
});
}
// Get the next slide's image
_this.$img = _this.RS.$currentSlide.find('img.rs-slide-image');
// Create a grid to hold the gridlets
_this.$grid = $('').addClass('rs-grid');
// Prepend the grid to the next slide (i.e. so it's above the slide image)
_this.RS.$currentSlide.prepend(_this.$grid);
// vars to calculate positioning/size of gridlets
var imgWidth = _this.$img.width(),
imgHeight = _this.$img.height(),
imgSrc = _this.$img.attr('src'),
colWidth = Math.floor(imgWidth / cols),
rowHeight = Math.floor(imgHeight / rows),
colRemainder = imgWidth - (cols * colWidth),
colAdd = Math.ceil(colRemainder / cols),
rowRemainder = imgHeight - (rows * rowHeight),
rowAdd = Math.ceil(rowRemainder / rows);//default is 0
// tx/ty args can be passed as 'auto'/'min-auto' (meaning use slide width/height or negative slide width/height)
tx = tx === 'auto' ? imgWidth : tx;
tx = tx === 'min-auto' ? - imgWidth : tx;
ty = ty === 'auto' ? imgHeight : ty;
ty = ty === 'min-auto' ? - imgHeight : ty;
// Loop through cols
top_init=parseInt(_this.$img.css('margin-top'));
mar_left=0;
switch (_this.$img.css('text-align')){
case 'left':
left_init=parseInt(_this.$img.css('margin-left'));
var leftDist = parseInt(_this.$img.css('margin-left'));
break;
case 'center':
//left_init=0;
left_pos=(_this.$grid.width() / 2);
get_margin=-(parseInt(_this.$img.css('margin-left')));
mar_half=_this.$img.width()/2
var leftDist = left_pos-mar_half-get_margin/2;
left_init=leftDist;
break;
case 'right':
var leftDist = parseInt(_this.$grid.width())-parseInt(_this.$img.width());
left_init=leftDist;
break;
}
for (var i = 0; i < cols; i++) {
var topDist = top_init,// parseInt(_this.$img.css('margin-top')),
newColWidth = colWidth;//defult topDist=0;
// If imgWidth (px) does not divide cleanly into the specified number of cols, adjust individual col widths to create correct total
if (colRemainder > 0) {
var add = colRemainder >= colAdd ? colAdd : colRemainder;
newColWidth += add;
colRemainder -= add;
}
// Nested loop to create row gridlets for each col
for (var j = 0; j < rows; j++) {
var newRowHeight = rowHeight,
newRowRemainder = rowRemainder;
// If imgHeight (px) does not divide cleanly into the specified number of rows, adjust individual row heights to create correct total
if (newRowRemainder > 0) {
add = newRowRemainder >= rowAdd ? rowAdd : rowRemainder;
newRowHeight += add;
newRowRemainder -= add;
}
// console.log(left_init);
// Create & append gridlet to grid
_this.$grid.append(gridlet(newColWidth, newRowHeight, topDist, leftDist, topDist-top_init,leftDist-left_init,mar_left, imgSrc, imgWidth, imgHeight, i, j));
topDist += newRowHeight;
}
leftDist += newColWidth;
}
// Set event listener on last gridlet to finish transitioning
_this.listenTo = _this.$grid.children().last();
// Show grid & hide the image it replaces
_this.$grid.show();
_this.$img.css('opacity', 0);
// Add identifying classes to corner gridlets (useful if applying border radius)
_this.$grid.children().first().addClass('rs-top-left');
_this.$grid.children().last().addClass('rs-bottom-right');
_this.$grid.children().eq(rows - 1).addClass('rs-bottom-left');
_this.$grid.children().eq(- rows).addClass('rs-top-right');
};
// Execution steps
this.execute = function () {
_this.$grid.children().css('opacity', op).prefixes({'transform' : 'rotate('+ ro +'deg) translateX('+ tx +'px) translateY('+ ty +'px) scale('+ sc +')'});
};
this.before(function () {
// Fire on setup callback
_this.execute();
});
// Reset steps
this.reset = function () {
_this.$img.css('opacity', 1);
_this.$grid.remove();
}
}
,sliceH: function () {
this.grid(1, 8, 0, 'min-auto', 10, 1, 0);
}
,sliceV: function () {
this.grid(10, 1, 0, 0, 'auto', 1, 0);
}
,slideV: function () {
this.grid(1, 1, 0, 0, 'auto', 1, 1);
}
,slideH: function () {
this.grid(1, 1, 0, 'min-auto', 0, 1, 1);
}
,scale: function () {
this.grid(1, 1, 0, 0, 0, 1.5, 0);
}
,blockScale: function () {
this.grid(8, 6, 0, 0, 0, .6, 0);
}
,kaleidoscope: function () {
this.grid(10, 8, 0, 0, 0, 1, 0);
}
,fan: function () {
this.grid(1, 10, 45, 100, 0, 1, 0);
}
,blindV: function () {
this.grid(1, 8, 0, 0, 0, .7, 0);
}
,blindH: function () {
this.grid(10, 1, 0, 0, 0, .7, 0);
}
,random: function () {
// Pick a random transition from the anims array (obj prop)
this[this.anims[Math.floor(Math.random() * this.anims.length)]]();
}
};
// Obj to check browser capabilities
var testBrowser = {
// Browser vendor CSS prefixes
browserVendors: ['', '-webkit-', '-moz-', '-ms-', '-o-', '-khtml-']
// Browser vendor DOM prefixes
,domPrefixes: ['', 'Webkit', 'Moz', 'ms', 'O', 'Khtml']
// Method to iterate over a property (using all DOM prefixes)
// Returns true if prop is recognised by browser (else returns false)
,testDom: function (prop) {
var i = this.domPrefixes.length;
while (i--) {
if (document.body.style[this.domPrefixes[i] + prop] !== undefined) {
return true;
}
}
return false;
}
,cssTransitions: function () {
// Use Modernizr if available & implements csstransitions test
if (window.Modernizr && Modernizr.csstransitions !== undefined) {
return Modernizr.csstransitions;
}
// Use testDom method to check prop (returns bool)
return this.testDom('Transition');
}
,cssTransforms3d: function () {
// Use Modernizr if available & implements csstransforms3d test
if (window.Modernizr && Modernizr.csstransforms3d !== undefined) {
return Modernizr.csstransforms3d;
}
// Check for vendor-less prop
if (document.body.style['perspectiveProperty'] !== undefined) {
return true;
}
// Use testDom method to check prop (returns bool)
return this.testDom('Perspective');
}
};
// CSS vendor prefix generator
$.fn['prefixes'] = function (props) {
var output = [];
// Loop through props, add with each vendor prefix to output array
for (var prop in props) {
if(props.hasOwnProperty(prop)) {
var i = testBrowser.browserVendors.length;
while (i--) {
output[testBrowser.browserVendors[i] + prop] = props[prop];
}
}
}
// Add output array of vendor-ised props to elem
this.css(output);
return this;
};
/*!
* David Desandro's imagesloaded plugin is included here as a cross-browser way to ensure all images have loaded before slider setup (e.g. testing for image dimensions)
* Another reliable method would be to wait until the window.load event before setup - though that could cause considerable delays on certain pages
*
* jQuery imagesLoaded plugin v2.0.1
* http://github.com/desandro/imagesloaded
*
* MIT License. by Paul Irish et al.
*/
// blank image data-uri bypasses webkit log warning (thx doug jones)
var BLANK = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==';
$.fn.imagesLoaded = function( callback ) {
var $this = this,
deferred = $.isFunction($.Deferred) ? $.Deferred() : 0,
hasNotify = $.isFunction(deferred.notify),
$images = $this.find('img').add( $this.filter('img') ),
loaded = [],
proper = [],
broken = [];
function doneLoading() {
var $proper = $(proper),
$broken = $(broken);
if ( deferred ) {
if ( broken.length ) {
deferred.reject( $images, $proper, $broken );
} else {
deferred.resolve( $images );
}
}
if ( $.isFunction( callback ) ) {
callback.call( $this, $images, $proper, $broken );
}
}
function imgLoaded( img, isBroken ) {
// don't proceed if BLANK image, or image is already loaded
if ( img.src === BLANK || $.inArray( img, loaded ) !== -1 ) {
return;
}
// store element in loaded images array
loaded.push( img );
// keep track of broken and properly loaded images
if ( isBroken ) {
broken.push( img );
} else {
proper.push( img );
}
// cache image and its state for future calls
$.data( img, 'imagesLoaded', { isBroken: isBroken, src: img.src } );
// trigger deferred progress method if present
if ( hasNotify ) {
deferred.notifyWith( $(img), [ isBroken, $images, $(proper), $(broken) ] );
}
// call doneLoading and clean listeners if all images are loaded
if ( $images.length === loaded.length ){
setTimeout( doneLoading );
$images.unbind( '.imagesLoaded' );
}
}
// if no images, trigger immediately
if ( !$images.length ) {
doneLoading();
} else {
$images.bind( 'load.imagesLoaded error.imagesLoaded', function( event ){
// trigger imgLoaded
imgLoaded( event.target, event.type === 'error' );
}).each( function( i, el ) {
var src = el.src;
// find out if this image has been already checked for status
// if it was, and src has not changed, call imgLoaded on it
var cached = $.data( el, 'imagesLoaded' );
if ( cached && cached.src === src ) {
imgLoaded( el, cached.isBroken );
return;
}
// if complete is true and browser supports natural sizes, try
// to check for image status manually
if ( el.complete && el.naturalWidth !== undefined ) {
imgLoaded( el, el.naturalWidth === 0 || el.naturalHeight === 0 );
return;
}
// cached images don't fire load sometimes, so we reset src, but only when
// dealing with IE, or image is complete (loaded) and failed manual check
// webkit hack from http://groups.google.com/group/jquery-dev/browse_thread/thread/eee6ab7b2da50e1f
if ( el.readyState || el.complete ) {
el.src = BLANK;
el.src = src;
}
});
}
return deferred ? deferred.promise( $this ) : $this;
};
// jQuery plugin wrapper
$.fn['refineSlide'] = function (settings) {
return this.each(function () {
// Check if already instantiated on this elem
if (!$.data(this, 'refineSlide')) {
// Instantiate & store elem + string
$.data(this, 'refineSlide', new RS(this, settings));
}
});
}
})(jQuery, window, document);