Created
January 12, 2018 10:45
-
-
Save jonathanarbely/434761da7703afdf721036e0de25ad34 to your computer and use it in GitHub Desktop.
unslider.js from release v.2.0.3 (1e6558f) - Contains a critical bugfix in the calculateSlides function
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// unslider.js from release v.2.0.3 (1e6558f) - Contains a critical bugfix, PR pending | |
// See this issue for more info on this custom version: https://github.com/idiot/unslider/issues/511 | |
/** | |
* Unslider | |
* version 2.0 | |
* by @idiot and friends | |
*/ | |
(function($) { | |
// Don't throw any errors when jQuery | |
if(!$) { | |
return console.warn('Unslider needs jQuery'); | |
} | |
$.Unslider = function(context, options) { | |
var self = this; | |
// Create an Unslider reference we can use everywhere | |
self._ = 'unslider'; | |
// Store our default options in here | |
// Everything will be overwritten by the jQuery plugin though | |
self.defaults = { | |
// Should the slider move on its own or only when | |
// you interact with the nav/arrows? | |
// Only accepts boolean true/false. | |
autoplay: false, | |
// 3 second delay between slides moving, pass | |
// as a number in milliseconds. | |
delay: 3000, | |
// Animation speed in millseconds | |
speed: 750, | |
// An easing string to use. If you're using Velocity, use a | |
// Velocity string otherwise you can use jQuery/jQ UI options. | |
easing: 'swing', // [.42, 0, .58, 1], | |
// Does it support keyboard arrows? | |
// Can pass either true or false - | |
// or an object with the keycodes, like so: | |
// { | |
// prev: 37, | |
// next: 39 | |
// } | |
// You can call any internal method name | |
// before the keycode and it'll be called. | |
keys: { | |
prev: 37, | |
next: 39 | |
}, | |
// Do you want to generate clickable navigation | |
// to skip to each slide? Accepts boolean true/false or | |
// a callback function per item to generate. | |
nav: true, | |
// Should there be left/right arrows to go back/forth? | |
// -> This isn't keyboard support. | |
// Either set true/false, or an object with the HTML | |
// elements for each arrow like below: | |
arrows: { | |
prev: '<a class="' + self._ + '-arrow prev">Prev</a>', | |
next: '<a class="' + self._ + '-arrow next">Next</a>' | |
}, | |
// How should Unslider animate? | |
// It can do one of the following types: | |
// "fade": each slide fades in to each other | |
// "horizontal": each slide moves from left to right | |
// "vertical": each slide moves from top to bottom | |
animation: 'horizontal', | |
// If you don't want to use a list to display your slides, | |
// you can change it here. Not recommended and you'll need | |
// to adjust the CSS accordingly. | |
selectors: { | |
container: 'ul:first', | |
slides: 'li' | |
}, | |
// Do you want to animate the heights of each slide as | |
// it moves | |
animateHeight: false, | |
// Active class for the nav | |
activeClass: self._ + '-active', | |
// Have swipe support? | |
// You can set this here with a boolean and always use | |
// initSwipe/destroySwipe later on. | |
swipe: true | |
}; | |
// Set defaults | |
self.$context = context; | |
self.options = {}; | |
// Leave our elements blank for now | |
// Since they get changed by the options, we'll need to | |
// set them in the init method. | |
self.$parent = null; | |
self.$container = null; | |
self.$slides = null; | |
self.$nav = null; | |
self.$arrows = []; | |
// Set our indexes and totals | |
self.total = 0; | |
self.current = 0; | |
// Generate a specific random ID so we don't dupe events | |
self.prefix = self._ + '-'; | |
self.eventSuffix = '.' + self.prefix + ~~(Math.random() * 2e3); | |
// In case we're going to use the autoplay | |
self.interval = null; | |
// Get everything set up innit | |
self.init = function(options) { | |
// Set up our options inside here so we can re-init at | |
// any time | |
self.options = $.extend({}, self.defaults, options); | |
// Our elements | |
self.$container = self.$context.find(self.options.selectors.container).addClass(self.prefix + 'wrap'); | |
self.$slides = self.$container.children(self.options.selectors.slides); | |
// We'll manually init the container | |
self.setup(); | |
// We want to keep this script as small as possible | |
// so we'll optimise some checks | |
$.each(['nav', 'arrows', 'keys', 'infinite'], function(index, module) { | |
self.options[module] && self['init' + $._ucfirst(module)](); | |
}); | |
// Add swipe support | |
if(jQuery.event.special.swipe && self.options.swipe) { | |
self.initSwipe(); | |
} | |
// If autoplay is set to true, call self.start() | |
// to start calling our timeouts | |
self.options.autoplay && self.start(); | |
// We should be able to recalculate slides at will | |
self.calculateSlides(); | |
// Listen to a ready event | |
self.$context.trigger(self._ + '.ready'); | |
// Everyday I'm chainin' | |
return self.animate(self.options.index || self.current, 'init'); | |
}; | |
self.setup = function() { | |
// Add a CSS hook to the main element | |
self.$context.addClass(self.prefix + self.options.animation).wrap('<div class="' + self._ + '" />'); | |
self.$parent = self.$context.parent('.' + self._); | |
// We need to manually check if the container is absolutely | |
// or relatively positioned | |
var position = self.$context.css('position'); | |
// If we don't already have a position set, we'll | |
// automatically set it ourselves | |
if(position === 'static') { | |
self.$context.css('position', 'relative'); | |
} | |
self.$context.css('overflow', 'hidden'); | |
}; | |
// Set up the slide widths to animate with | |
// so the box doesn't float over | |
self.calculateSlides = function() { | |
// Check how many children the slider contains | |
self.$slides = self.$container.children(); // this is the new line, the rest is release v.2.0.3 | |
self.total = self.$slides.length; | |
// Set the total width | |
if(self.options.animation !== 'fade') { | |
var prop = 'width'; | |
if(self.options.animation === 'vertical') { | |
prop = 'height'; | |
} | |
self.$container.css(prop, (self.total * 100) + '%').addClass(self.prefix + 'carousel'); | |
self.$slides.css(prop, (100 / self.total) + '%'); | |
} | |
}; | |
// Start our autoplay | |
self.start = function() { | |
self.interval = setTimeout(function() { | |
// Move on to the next slide | |
self.next(); | |
// If we've got autoplay set up | |
// we don't need to keep starting | |
// the slider from within our timeout | |
// as .animate() calls it for us | |
}, self.options.delay); | |
return self; | |
}; | |
// And pause our timeouts | |
// and force stop the slider if needed | |
self.stop = function() { | |
clearTimeout(self.interval); | |
return self; | |
}; | |
// Set up our navigation | |
self.initNav = function() { | |
var $nav = $('<nav class="' + self.prefix + 'nav"><ol /></nav>'); | |
// Build our click navigation item-by-item | |
self.$slides.each(function(key) { | |
// If we've already set a label, let's use that | |
// instead of generating one | |
var label = this.getAttribute('data-nav') || key + 1; | |
// Listen to any callback functions | |
if($.isFunction(self.options.nav)) { | |
label = self.options.nav.call(self.$slides.eq(key), key, label); | |
} | |
// And add it to our navigation item | |
$nav.children('ol').append('<li data-slide="' + key + '">' + label + '</li>'); | |
}); | |
// Keep a copy of the nav everywhere so we can use it | |
self.$nav = $nav.insertAfter(self.$context); | |
// Now our nav is built, let's add it to the slider and bind | |
// for any click events on the generated links | |
self.$nav.find('li').on('click' + self.eventSuffix, function() { | |
// Cache our link and set it to be active | |
var $me = $(this).addClass(self.options.activeClass); | |
// Set the right active class, remove any other ones | |
$me.siblings().removeClass(self.options.activeClass); | |
// Move the slide | |
self.animate($me.attr('data-slide')); | |
}); | |
}; | |
// Set up our left-right arrow navigation | |
// (Not keyboard arrows, prev/next buttons) | |
self.initArrows = function() { | |
if(self.options.arrows === true) { | |
self.options.arrows = self.defaults.arrows; | |
} | |
// Loop our options object and bind our events | |
$.each(self.options.arrows, function(key, val) { | |
// Add our arrow HTML and bind it | |
self.$arrows.push( | |
$(val).insertAfter(self.$context).on('click' + self.eventSuffix, self[key]) | |
); | |
}); | |
}; | |
// Set up our keyboad navigation | |
// Allow binding to multiple keycodes | |
self.initKeys = function() { | |
if(self.options.keys === true) { | |
self.options.keys = self.defaults.keys; | |
} | |
$(document).on('keyup' + self.eventSuffix, function(e) { | |
$.each(self.options.keys, function(key, val) { | |
if(e.which === val) { | |
$.isFunction(self[key]) && self[key].call(self); | |
} | |
}); | |
}); | |
}; | |
// Requires jQuery.event.swipe | |
// -> stephband.info/jquery.event.swipe | |
self.initSwipe = function() { | |
var width = self.$slides.width(); | |
self.$container.on({ | |
swipeleft: self.next, | |
swiperight: self.prev, | |
movestart: function(e) { | |
// If the movestart heads off in a upwards or downwards | |
// direction, prevent it so that the browser scrolls normally. | |
if((e.distX > e.distY && e.distX < -e.distY) || (e.distX < e.distY && e.distX > -e.distY)) { | |
return !!e.preventDefault(); | |
} | |
self.$container.css('position', 'relative'); | |
} | |
}); | |
// We don't want to have a tactile swipe in the slider | |
// in the fade animation, as it can cause some problems | |
// with layout, so we'll just disable it. | |
if(self.options.animation !== 'fade') { | |
self.$container.on({ | |
move: function(e) { | |
self.$container.css('left', -(100 * self.current) + (100 * e.distX / width) + '%'); | |
}, | |
moveend: function(e) { | |
if((Math.abs(e.distX) / width) < $.event.special.swipe.settings.threshold) { | |
return self._move(self.$container, {left: -(100 * self.current) + '%'}, false, 200); | |
} | |
} | |
}); | |
} | |
}; | |
// Infinite scrolling is a massive pain in the arse | |
// so we need to create a whole bloody function to set | |
// it up. Argh. | |
self.initInfinite = function() { | |
var pos = ['first', 'last']; | |
$.each(pos, function(index, item) { | |
self.$slides.push.apply( | |
self.$slides, | |
// Exclude all cloned slides and call .first() or .last() | |
// depending on what `item` is. | |
self.$slides.filter(':not(".' + self._ + '-clone")')[item]() | |
// Make a copy of it and identify it as a clone | |
.clone().addClass(self._ + '-clone') | |
// Either insert before or after depending on whether we're | |
// the first or last clone | |
['insert' + (index === 0 ? 'After' : 'Before')]( | |
// Return the other element in the position array | |
// if item = first, return "last" | |
self.$slides[pos[~~!index]]() | |
) | |
); | |
}); | |
}; | |
// Remove any trace of arrows | |
// Loop our array of arrows and use jQuery to remove | |
// It'll unbind any event handlers for us | |
self.destroyArrows = function() { | |
$.each(self.$arrows, function($arrow) { | |
$arrow.remove(); | |
}); | |
}; | |
// Remove any swipe events and reset the position | |
self.destroySwipe = function() { | |
// We bind to 4 events, so we'll unbind those | |
self.$container.off('movestart move moveend'); | |
}; | |
// Unset the keyboard navigation | |
// Remove the handler | |
self.destroyKeys = function() { | |
// Remove the event handler | |
$(document).off('keyup' + self.eventSuffix); | |
}; | |
self.setIndex = function(to) { | |
if(to < 0) { | |
to = self.total - 1; | |
} | |
self.current = Math.min(Math.max(0, to), self.total - 1); | |
if(self.options.nav) { | |
self.$nav.find('[data-slide="' + self.current + '"]')._active(self.options.activeClass); | |
} | |
self.$slides.eq(self.current)._active(self.options.activeClass); | |
return self; | |
}; | |
// Despite the name, this doesn't do any animation - since there's | |
// now three different types of animation, we let this method delegate | |
// to the right type, keeping the name for backwards compat. | |
self.animate = function(to, dir) { | |
// Animation shortcuts | |
// Instead of passing a number index, we can now | |
// use .data('unslider').animate('last'); | |
// or .unslider('animate:last') | |
// to go to the very last slide | |
if(to === 'first') to = 0; | |
if(to === 'last') to = self.total; | |
// Don't animate if it's not a valid index | |
if(isNaN(to)) { | |
return self; | |
} | |
if(self.options.autoplay) { | |
self.stop().start(); | |
} | |
self.setIndex(to); | |
// Add a callback method to do stuff with | |
self.$context.trigger(self._ + '.change', [to, self.$slides.eq(to)]); | |
// Delegate the right method - everything's named consistently | |
// so we can assume it'll be called "animate" + | |
var fn = 'animate' + $._ucfirst(self.options.animation); | |
// Make sure it's a valid animation method, otherwise we'll get | |
// a load of bug reports that'll be really hard to report | |
if($.isFunction(self[fn])) { | |
self[fn](self.current, dir); | |
} | |
return self; | |
}; | |
// Shortcuts for animating if we don't know what the current | |
// index is (i.e back/forward) | |
// For moving forward we need to make sure we don't overshoot. | |
self.next = function() { | |
var target = self.current + 1; | |
// If we're at the end, we need to move back to the start | |
if(target >= self.total) { | |
target = 0; | |
} | |
return self.animate(target, 'next'); | |
}; | |
// Previous is a bit simpler, we can just decrease the index | |
// by one and check if it's over 0. | |
self.prev = function() { | |
return self.animate(self.current - 1, 'prev'); | |
}; | |
// Our default animation method, the old-school left-to-right | |
// horizontal animation | |
self.animateHorizontal = function(to) { | |
var prop = 'left'; | |
// Add RTL support, slide the slider | |
// the other way if the site is right-to-left | |
if(self.$context.attr('dir') === 'rtl') { | |
prop = 'right'; | |
} | |
if(self.options.infinite) { | |
// So then we need to hide the first slide | |
self.$container.css('margin-' + prop, '-100%'); | |
} | |
return self.slide(prop, to); | |
}; | |
// The same animation methods, but vertical support | |
// RTL doesn't affect the vertical direction so we | |
// can just call as is | |
self.animateVertical = function(to) { | |
self.options.animateHeight = true; | |
// Normal infinite CSS fix doesn't work for | |
// vertical animation so we need to manually set it | |
// with pixels. Ah well. | |
if(self.options.infinite) { | |
self.$container.css('margin-top', -self.$slides.outerHeight()); | |
} | |
return self.slide('top', to); | |
}; | |
// Actually move the slide now | |
// We have to pass a property to animate as there's | |
// a few different directions it can now move, but it's | |
// otherwise unchanged from before. | |
self.slide = function(prop, to) { | |
// If we want to change the height of the slider | |
// to match the current slide, you can set | |
// {animateHeight: true} | |
if(self.options.animateHeight) { | |
self._move(self.$context, {height: self.$slides.eq(to).outerHeight()}, false); | |
} | |
// For infinite sliding we add a dummy slide at the end and start | |
// of each slider to give the appearance of being infinite | |
if(self.options.infinite) { | |
var dummy; | |
// Going backwards to last slide | |
if(to === self.total - 1) { | |
// We're setting a dummy position and an actual one | |
// the dummy is what the index looks like | |
// (and what we'll silently update to afterwards), | |
// and the actual is what makes it not go backwards | |
dummy = self.total - 3; | |
to = -1; | |
} | |
// Going forwards to first slide | |
if(to === self.total - 2) { | |
dummy = 0; | |
to = self.total - 2; | |
} | |
// If it's a number we can safely set it | |
if(typeof dummy === 'number') { | |
self.setIndex(dummy); | |
// Listen for when the slide's finished transitioning so | |
// we can silently move it into the right place and clear | |
// this whole mess up. | |
self.$context.on(self._ + '.moved', function() { | |
if(self.current === dummy) { | |
self.$container.css(prop, -(100 * dummy) + '%').off(self._ + '.moved'); | |
} | |
}); | |
} | |
} | |
// We need to create an object to store our property in | |
// since we don't know what it'll be. | |
var obj = {}; | |
// Manually create it here | |
obj[prop] = -(100 * to) + '%'; | |
// And animate using our newly-created object | |
return self._move(self.$container, obj); | |
}; | |
// Fade between slides rather than, uh, sliding it | |
self.animateFade = function(to) { | |
var $active = self.$slides.eq(to).addClass(self.options.activeClass); | |
// Toggle our classes | |
self._move($active.siblings().removeClass(self.options.activeClass), {opacity: 0}); | |
self._move($active, {opacity: 1}, false); | |
}; | |
self._move = function($el, obj, callback, speed) { | |
if(callback !== false) { | |
callback = function() { | |
self.$context.trigger(self._ + '.moved'); | |
}; | |
} | |
return $el._move(obj, speed || self.options.speed, self.options.easing, callback); | |
}; | |
// Allow daisy-chaining of methods | |
return self.init(options); | |
}; | |
// Internal (but global) jQuery methods | |
// They're both just helpful types of shorthand for | |
// anything that might take too long to write out or | |
// something that might be used more than once. | |
$.fn._active = function(className) { | |
return this.addClass(className).siblings().removeClass(className); | |
}; | |
// The equivalent to PHP's ucfirst(). Take the first | |
// character of a string and make it uppercase. | |
// Simples. | |
$._ucfirst = function(str) { | |
// Take our variable, run a regex on the first letter | |
return (str + '').toLowerCase().replace(/^./, function(match) { | |
// And uppercase it. Simples. | |
return match.toUpperCase(); | |
}); | |
}; | |
$.fn._move = function() { | |
this.stop(true, true); | |
return $.fn[$.fn.velocity ? 'velocity' : 'animate'].apply(this, arguments); | |
}; | |
// And set up our jQuery plugin | |
$.fn.unslider = function(opts) { | |
return this.each(function() { | |
var $this = $(this); | |
// Allow usage of .unslider('function_name') | |
// as well as using .data('unslider') to access the | |
// main Unslider object | |
if(typeof opts === 'string' && $this.data('unslider')) { | |
opts = opts.split(':'); | |
var call = $this.data('unslider')[opts[0]]; | |
// Do we have arguments to pass to the string-function? | |
if($.isFunction(call)) { | |
return call.apply($this, opts[1] ? opts[1].split(',') : null); | |
} | |
} | |
return $this.data('unslider', new $.Unslider($this, opts)); | |
}); | |
}; | |
})(window.jQuery); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment