'use strict'; import $ from 'jquery'; import { Keyboard } from './foundation.util.keyboard'; import { Motion } from './foundation.util.motion'; import { Timer } from './foundation.util.timer'; import { onImagesLoaded } from './foundation.util.imageLoader'; import { GetYoDigits } from './foundation.util.core'; import { Plugin } from './foundation.plugin'; import { Touch } from './foundation.util.touch' /** * Orbit module. * @module foundation.orbit * @requires foundation.util.keyboard * @requires foundation.util.motion * @requires foundation.util.timer * @requires foundation.util.imageLoader * @requires foundation.util.touch */ class Orbit extends Plugin { /** * Creates a new instance of an orbit carousel. * @class * @name Orbit * @param {jQuery} element - jQuery object to make into an Orbit Carousel. * @param {Object} options - Overrides to the default plugin settings. */ _setup(element, options){ this.$element = element; this.options = $.extend({}, Orbit.defaults, this.$element.data(), options); this.className = 'Orbit'; // ie9 back compat Touch.init($); // Touch init is idempotent, we just need to make sure it's initialied. this._init(); Keyboard.register('Orbit', { 'ltr': { 'ARROW_RIGHT': 'next', 'ARROW_LEFT': 'previous' }, 'rtl': { 'ARROW_LEFT': 'next', 'ARROW_RIGHT': 'previous' } }); } /** * Initializes the plugin by creating jQuery collections, setting attributes, and starting the animation. * @function * @private */ _init() { // @TODO: consider discussion on PR #9278 about DOM pollution by changeSlide this._reset(); 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'), id = this.$element[0].id || GetYoDigits(6, 'orbit'); this.$element.attr({ 'data-resize': id, 'id': id }); if (!initActive.length) { this.$slides.eq(0).addClass('is-active'); } if (!this.options.useMUI) { this.$slides.addClass('no-motionui'); } if ($images.length) { onImagesLoaded($images, this._prepareForOrbit.bind(this)); } else { this._prepareForOrbit();//hehe } if (this.options.bullets) { this._loadBullets(); } this._events(); if (this.options.autoPlay && this.$slides.length > 1) { 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 */ _loadBullets() { 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 */ geoSync() { var _this = this; this.timer = new 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 */ _prepareForOrbit() { var _this = this; this._setWrapperHeight(); } /** * 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. */ _setWrapperHeight(cb) {//rewrite this to `for` loop var max = 0, temp, counter = 0, _this = this; this.$slides.each(function() { temp = this.getBoundingClientRect().height; $(this).attr('data-slide', counter); if (_this.$slides.filter('.is-active')[0] !== _this.$slides.eq(counter)[0]) {//if not the active 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. if(cb) {cb(max);} //fire callback with max height dimension. } } /** * Sets the max-height of each slide. * @function * @private */ _setSlideHeight(height) { this.$slides.each(function() { $(this).css('max-height', height); }); } /** * Adds event listeners to basically everything within the element. * @function * @private */ _events() { var _this = this; //*************************************** //**Now using custom event - thanks to:** //** Yohai Ararat of Toronto ** //*************************************** // this.$element.off('.resizeme.zf.trigger').on({ 'resizeme.zf.trigger': this._prepareForOrbit.bind(this) }) if (this.$slides.length > 1) { 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(e){ e.preventDefault(); _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); }); } if (this.options.accessible) { this.$wrapper.add(this.$bullets).on('keydown.zf.orbit', function(e) { // handle keyboard event with keyboard util 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(); } } }); }); } } } /** * Resets Orbit so it can be reinitialized */ _reset() { // Don't do anything if there are no slides (first run) if (typeof this.$slides == 'undefined') { return; } if (this.$slides.length > 1) { // Remove old events this.$element.off('.zf.orbit').find('*').off('.zf.orbit') // Restart timer if autoPlay is enabled if (this.options.autoPlay) { this.timer.restart(); } // Reset all sliddes this.$slides.each(function(el) { $(el).removeClass('is-active is-active is-in') .removeAttr('aria-live') .hide(); }); // Show the first slide this.$slides.first().addClass('is-active').show(); // Triggers when the slide has finished animating this.$element.trigger('slidechange.zf.orbit', [this.$slides.first()]); // Select first bullet if bullets are present if (this.options.bullets) { this._updateBullets(0); } } } /** * 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 */ changeSlide(isLTR, chosenSlide, idx) { if (!this.$slides) {return; } // Don't freak out if we're in the middle of cleanup 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) { /** * Triggers before the next slide starts animating in and only if a next slide has been found. * @event Orbit#beforeslidechange */ this.$element.trigger('beforeslidechange.zf.orbit', [$curSlide, $newSlide]); if (this.options.bullets) { idx = idx || this.$slides.index($newSlide); //grab index to update bullets this._updateBullets(idx); } if (this.options.useMUI && !this.$element.is(':hidden')) { 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'); }); Motion.animateOut( $curSlide.removeClass('is-active'), this.options[`animOutTo${dirOut}`], function(){ $curSlide.removeAttr('aria-live'); if(_this.options.autoPlay && !_this.timer.isPaused){ _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.isPaused) { 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. */ _updateBullets(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 */ _destroy() { this.$element.off('.zf.orbit').find('*').off('.zf.orbit').end().hide(); } } Orbit.defaults = { /** * Tells the JS to look for and loadBullets. * @option * @type {boolean} * @default true */ bullets: true, /** * Tells the JS to apply event listeners to nav buttons * @option * @type {boolean} * @default true */ navButtons: true, /** * motion-ui animation class to apply * @option * @type {string} * @default 'slide-in-right' */ animInFromRight: 'slide-in-right', /** * motion-ui animation class to apply * @option * @type {string} * @default 'slide-out-right' */ animOutToRight: 'slide-out-right', /** * motion-ui animation class to apply * @option * @type {string} * @default 'slide-in-left' * */ animInFromLeft: 'slide-in-left', /** * motion-ui animation class to apply * @option * @type {string} * @default 'slide-out-left' */ animOutToLeft: 'slide-out-left', /** * Allows Orbit to automatically animate on page load. * @option * @type {boolean} * @default true */ autoPlay: true, /** * Amount of time, in ms, between slide transitions * @option * @type {number} * @default 5000 */ timerDelay: 5000, /** * Allows Orbit to infinitely loop through the slides * @option * @type {boolean} * @default true */ infiniteWrap: true, /** * Allows the Orbit slides to bind to swipe events for mobile, requires an additional util library * @option * @type {boolean} * @default true */ swipe: true, /** * Allows the timing function to pause animation on hover. * @option * @type {boolean} * @default true */ pauseOnHover: true, /** * Allows Orbit to bind keyboard events to the slider, to animate frames with arrow keys * @option * @type {boolean} * @default true */ accessible: true, /** * Class applied to the container of Orbit * @option * @type {string} * @default 'orbit-container' */ containerClass: 'orbit-container', /** * Class applied to individual slides. * @option * @type {string} * @default 'orbit-slide' */ slideClass: 'orbit-slide', /** * Class applied to the bullet container. You're welcome. * @option * @type {string} * @default 'orbit-bullets' */ boxOfBullets: 'orbit-bullets', /** * Class applied to the `next` navigation button. * @option * @type {string} * @default 'orbit-next' */ nextClass: 'orbit-next', /** * Class applied to the `previous` navigation button. * @option * @type {string} * @default 'orbit-previous' */ prevClass: 'orbit-previous', /** * Boolean to flag the js to use motion ui classes or not. Default to true for backwards compatability. * @option * @type {boolean} * @default true */ useMUI: true }; export {Orbit};