/** * Orbit module. * @module foundation.orbit * @requires foundation.util.keyboard * @requires foundation.util.motion * @requires foundation.util.timerAndImageLoader * @requires foundation.util.touch */ !function($, Foundation){ 'use strict'; /** * Creates a new instance of an orbit carousel. * @class * @param {jQuery} element - jQuery object to make into an Orbit Carousel. * @param {Object} options - Overrides to the default plugin settings. */ function Orbit(element, options){ this.$element = element; this.options = $.extend({}, Orbit.defaults, this.$element.data(), options); this._init(); Foundation.registerPlugin(this, 'Orbit'); Foundation.Keyboard.register('Orbit', { 'ltr': { 'ARROW_RIGHT': 'next', 'ARROW_LEFT': 'previous' }, 'rtl': { 'ARROW_LEFT': 'next', 'ARROW_RIGHT': 'previous' } }); } Orbit.defaults = { /** * Tells the JS to loadBullets. * @option * @example true */ bullets: true, /** * Tells the JS to apply event listeners to nav buttons * @option * @example true */ navButtons: true, /** * motion-ui animation class to apply * @option * @example 'slide-in-right' */ animInFromRight: 'slide-in-right', /** * motion-ui animation class to apply * @option * @example 'slide-out-right' */ animOutToRight: 'slide-out-right', /** * motion-ui animation class to apply * @option * @example 'slide-in-left' * */ animInFromLeft: 'slide-in-left', /** * motion-ui animation class to apply * @option * @example 'slide-out-left' */ animOutToLeft: 'slide-out-left', /** * Allows Orbit to automatically animate on page load. * @option * @example true */ autoPlay: true, /** * Amount of time, in ms, between slide transitions * @option * @example 5000 */ timerDelay: 5000, /** * Allows Orbit to infinitely loop through the slides * @option * @example true */ infiniteWrap: true, /** * Allows the Orbit slides to bind to swipe events for mobile, requires an additional util library * @option * @example true */ swipe: true, /** * Allows the timing function to pause animation on hover. * @option * @example true */ pauseOnHover: true, /** * Allows Orbit to bind keyboard events to the slider, to animate frames with arrow keys * @option * @example true */ accessible: true, /** * Class applied to the container of Orbit * @option * @example 'orbit-container' */ containerClass: 'orbit-container', /** * Class applied to individual slides. * @option * @example 'orbit-slide' */ slideClass: 'orbit-slide', /** * Class applied to the bullet container. You're welcome. * @option * @example 'orbit-bullets' */ boxOfBullets: 'orbit-bullets', /** * Class applied to the `next` navigation button. * @option * @example 'orbit-next' */ nextClass: 'orbit-next', /** * Class applied to the `previous` navigation button. * @option * @example 'orbit-previous' */ prevClass: 'orbit-previous', /** * Boolean to flag the js to use motion ui classes or not. Default to true for backwards compatability. * @option * @example true */ useMUI: true }; /** * Initializes the plugin by creating jQuery collections, setting attributes, and starting the animation. * @function * @private */ Orbit.prototype._init = function(){ this.$wrapper = this.$element.find('.' + this.options.containerClass); this.$slides = this.$element.find('.' + this.options.slideClass); var $images = this.$element.find('img'), initActive = this.$slides.filter('.is-active'); if(!initActive.length){ this.$slides.eq(0).addClass('is-active'); } if(!this.options.useMUI){ this.$slides.addClass('no-motionui'); } if($images.length){ Foundation.onImagesLoaded($images, this._prepareForOrbit.bind(this)); }else{ this._prepareForOrbit();//hehe } if(this.options.bullets){ this._loadBullets(); } this._events(); if(this.options.autoPlay){ this.geoSync(); } if(this.options.accessible){ // allow wrapper to be focusable to enable arrow navigation this.$wrapper.attr('tabindex', 0); } }; /** * Creates a jQuery collection of bullets, if they are being used. * @function * @private */ Orbit.prototype._loadBullets = function(){ this.$bullets = this.$element.find('.' + this.options.boxOfBullets).find('button'); }; /** * Sets a `timer` object on the orbit, and starts the counter for the next slide. * @function */ Orbit.prototype.geoSync = function(){ var _this = this; this.timer = new Foundation.Timer( this.$element, {duration: this.options.timerDelay, infinite: false}, function(){ _this.changeSlide(true); }); this.timer.start(); }; /** * Sets wrapper and slide heights for the orbit. * @function * @private */ Orbit.prototype._prepareForOrbit = function(){ var _this = this; this._setWrapperHeight(function(max){ _this._setSlideHeight(max); }); }; /** * Calulates the height of each slide in the collection, and uses the tallest one for the wrapper height. * @function * @private * @param {Function} cb - a callback function to fire when complete. */ Orbit.prototype._setWrapperHeight = function(cb){//rewrite this to `for` loop var max = 0, temp, counter = 0; this.$slides.each(function(){ temp = this.getBoundingClientRect().height; $(this).attr('data-slide', counter); if(counter){//if not the first slide, set css position and display property $(this).css({'position': 'relative', 'display': 'none'}); } max = temp > max ? temp : max; counter++; }); if(counter === this.$slides.length){ this.$wrapper.css({'height': max});//only change the wrapper height property once. cb(max);//fire callback with max height dimension. } }; /** * Sets the max-height of each slide. * @function * @private */ Orbit.prototype._setSlideHeight = function(height){ this.$slides.each(function(){ $(this).css('max-height', height); }); }; /** * Adds event listeners to basically everything within the element. * @function * @private */ Orbit.prototype._events = function(){ var _this = this; //*************************************** //**Now using custom event - thanks to:** //** Yohai Ararat of Toronto ** //*************************************** if(this.options.swipe){ this.$slides.off('swipeleft.zf.orbit swiperight.zf.orbit') .on('swipeleft.zf.orbit', function(e){ e.preventDefault(); _this.changeSlide(true); }).on('swiperight.zf.orbit', function(e){ e.preventDefault(); _this.changeSlide(false); }); } //*************************************** if(this.options.autoPlay){ this.$slides.on('click.zf.orbit', function(){ _this.$element.data('clickedOn', _this.$element.data('clickedOn') ? false : true); _this.timer[_this.$element.data('clickedOn') ? 'pause' : 'start'](); }); if(this.options.pauseOnHover){ this.$element.on('mouseenter.zf.orbit', function(){ _this.timer.pause(); }).on('mouseleave.zf.orbit', function(){ if(!_this.$element.data('clickedOn')){ _this.timer.start(); } }); } } if(this.options.navButtons){ var $controls = this.$element.find('.' + this.options.nextClass + ', .' + this.options.prevClass); $controls.attr('tabindex', 0) //also need to handle enter/return and spacebar key presses .on('click.zf.orbit touchend.zf.orbit', function(){ _this.changeSlide($(this).hasClass(_this.options.nextClass)); }); } if(this.options.bullets){ this.$bullets.on('click.zf.orbit touchend.zf.orbit', function(){ if(/is-active/g.test(this.className)){ return false; }//if this is active, kick out of function. var idx = $(this).data('slide'), ltr = idx > _this.$slides.filter('.is-active').data('slide'), $slide = _this.$slides.eq(idx); _this.changeSlide(ltr, $slide, idx); }); } this.$wrapper.add(this.$bullets).on('keydown.zf.orbit', function(e){ // handle keyboard event with keyboard util Foundation.Keyboard.handleKey(e, 'Orbit', { next: function() { _this.changeSlide(true); }, previous: function() { _this.changeSlide(false); }, handled: function() { // if bullet is focused, make sure focus moves if ($(e.target).is(_this.$bullets)) { _this.$bullets.filter('.is-active').focus(); } } }); }); }; /** * Changes the current slide to a new one. * @function * @param {Boolean} isLTR - flag if the slide should move left to right. * @param {jQuery} chosenSlide - the jQuery element of the slide to show next, if one is selected. * @param {Number} idx - the index of the new slide in its collection, if one chosen. * @fires Orbit#slidechange */ Orbit.prototype.changeSlide = function(isLTR, chosenSlide, idx){ var $curSlide = this.$slides.filter('.is-active').eq(0); if(/mui/g.test($curSlide[0].className)){ return false; }//if the slide is currently animating, kick out of the function var $firstSlide = this.$slides.first(), $lastSlide = this.$slides.last(), dirIn = isLTR ? 'Right' : 'Left', dirOut = isLTR ? 'Left' : 'Right', _this = this, $newSlide; if(!chosenSlide){//most of the time, this will be auto played or clicked from the navButtons. $newSlide = isLTR ? //if wrapping enabled, check to see if there is a `next` or `prev` sibling, if not, select the first or last slide to fill in. if wrapping not enabled, attempt to select `next` or `prev`, if there's nothing there, the function will kick out on next step. CRAZY NESTED TERNARIES!!!!! (this.options.infiniteWrap ? $curSlide.next('.' + this.options.slideClass).length ? $curSlide.next('.' + this.options.slideClass) : $firstSlide : $curSlide.next('.' + this.options.slideClass))//pick next slide if moving left to right : (this.options.infiniteWrap ? $curSlide.prev('.' + this.options.slideClass).length ? $curSlide.prev('.' + this.options.slideClass) : $lastSlide : $curSlide.prev('.' + this.options.slideClass));//pick prev slide if moving right to left }else{ $newSlide = chosenSlide; } if($newSlide.length){ if(this.options.bullets){ idx = idx || this.$slides.index($newSlide);//grab index to update bullets this._updateBullets(idx); } if(this.options.useMUI){ Foundation.Motion.animateIn( $newSlide.addClass('is-active').css({'position': 'absolute', 'top': 0}), this.options['animInFrom' + dirIn], function(){ $newSlide.css({'position': 'relative', 'display': 'block'}) .attr('aria-live', 'polite'); }); Foundation.Motion.animateOut( $curSlide.removeClass('is-active'), this.options['animOutTo' + dirOut], function(){ $curSlide.removeAttr('aria-live'); if(_this.options.autoPlay){ _this.timer.restart(); } //do stuff? }); }else{ $curSlide.removeClass('is-active is-in').removeAttr('aria-live').hide(); $newSlide.addClass('is-active is-in').attr('aria-live', 'polite').show(); if(this.options.autoPlay){ this.timer.restart(); } } /** * Triggers when the slide has finished animating in. * @event Orbit#slidechange */ this.$element.trigger('slidechange.zf.orbit', [$newSlide]); } }; /** * Updates the active state of the bullets, if displayed. * @function * @private * @param {Number} idx - the index of the current slide. */ Orbit.prototype._updateBullets = function(idx){ var $oldBullet = this.$element.find('.' + this.options.boxOfBullets) .find('.is-active').removeClass('is-active').blur(), span = $oldBullet.find('span:last').detach(), $newBullet = this.$bullets.eq(idx).addClass('is-active').append(span); }; /** * Destroys the carousel and hides the element. * @function */ Orbit.prototype.destroy = function(){ delete this.timer; this.$element.off('.zf.orbit').find('*').off('.zf.orbit').end().hide(); Foundation.unregisterPlugin(this); }; Foundation.plugin(Orbit, 'Orbit'); }(jQuery, window.Foundation);