UNPKG

15.4 kBJavaScriptView Raw
1'use strict';
2
3import $ from 'jquery';
4import { Keyboard } from './foundation.util.keyboard';
5import { Motion } from './foundation.util.motion';
6import { Timer } from './foundation.util.timer';
7import { onImagesLoaded } from './foundation.util.imageLoader';
8import { GetYoDigits } from './foundation.util.core';
9import { Plugin } from './foundation.plugin';
10import { Touch } from './foundation.util.touch'
11
12
13/**
14 * Orbit module.
15 * @module foundation.orbit
16 * @requires foundation.util.keyboard
17 * @requires foundation.util.motion
18 * @requires foundation.util.timer
19 * @requires foundation.util.imageLoader
20 * @requires foundation.util.touch
21 */
22
23class Orbit extends Plugin {
24 /**
25 * Creates a new instance of an orbit carousel.
26 * @class
27 * @name Orbit
28 * @param {jQuery} element - jQuery object to make into an Orbit Carousel.
29 * @param {Object} options - Overrides to the default plugin settings.
30 */
31 _setup(element, options){
32 this.$element = element;
33 this.options = $.extend({}, Orbit.defaults, this.$element.data(), options);
34 this.className = 'Orbit'; // ie9 back compat
35
36 Touch.init($); // Touch init is idempotent, we just need to make sure it's initialied.
37
38 this._init();
39
40 Keyboard.register('Orbit', {
41 'ltr': {
42 'ARROW_RIGHT': 'next',
43 'ARROW_LEFT': 'previous'
44 },
45 'rtl': {
46 'ARROW_LEFT': 'next',
47 'ARROW_RIGHT': 'previous'
48 }
49 });
50 }
51
52 /**
53 * Initializes the plugin by creating jQuery collections, setting attributes, and starting the animation.
54 * @function
55 * @private
56 */
57 _init() {
58 // @TODO: consider discussion on PR #9278 about DOM pollution by changeSlide
59 this._reset();
60
61 this.$wrapper = this.$element.find(`.${this.options.containerClass}`);
62 this.$slides = this.$element.find(`.${this.options.slideClass}`);
63
64 var $images = this.$element.find('img'),
65 initActive = this.$slides.filter('.is-active'),
66 id = this.$element[0].id || GetYoDigits(6, 'orbit');
67
68 this.$element.attr({
69 'data-resize': id,
70 'id': id
71 });
72
73 if (!initActive.length) {
74 this.$slides.eq(0).addClass('is-active');
75 }
76
77 if (!this.options.useMUI) {
78 this.$slides.addClass('no-motionui');
79 }
80
81 if ($images.length) {
82 onImagesLoaded($images, this._prepareForOrbit.bind(this));
83 } else {
84 this._prepareForOrbit();//hehe
85 }
86
87 if (this.options.bullets) {
88 this._loadBullets();
89 }
90
91 this._events();
92
93 if (this.options.autoPlay && this.$slides.length > 1) {
94 this.geoSync();
95 }
96
97 if (this.options.accessible) { // allow wrapper to be focusable to enable arrow navigation
98 this.$wrapper.attr('tabindex', 0);
99 }
100 }
101
102 /**
103 * Creates a jQuery collection of bullets, if they are being used.
104 * @function
105 * @private
106 */
107 _loadBullets() {
108 this.$bullets = this.$element.find(`.${this.options.boxOfBullets}`).find('button');
109 }
110
111 /**
112 * Sets a `timer` object on the orbit, and starts the counter for the next slide.
113 * @function
114 */
115 geoSync() {
116 var _this = this;
117 this.timer = new Timer(
118 this.$element,
119 {
120 duration: this.options.timerDelay,
121 infinite: false
122 },
123 function() {
124 _this.changeSlide(true);
125 });
126 this.timer.start();
127 }
128
129 /**
130 * Sets wrapper and slide heights for the orbit.
131 * @function
132 * @private
133 */
134 _prepareForOrbit() {
135 var _this = this;
136 this._setWrapperHeight();
137 }
138
139 /**
140 * Calulates the height of each slide in the collection, and uses the tallest one for the wrapper height.
141 * @function
142 * @private
143 * @param {Function} cb - a callback function to fire when complete.
144 */
145 _setWrapperHeight(cb) {//rewrite this to `for` loop
146 var max = 0, temp, counter = 0, _this = this;
147
148 this.$slides.each(function() {
149 temp = this.getBoundingClientRect().height;
150 $(this).attr('data-slide', counter);
151
152 if (_this.$slides.filter('.is-active')[0] !== _this.$slides.eq(counter)[0]) {//if not the active slide, set css position and display property
153 $(this).css({'position': 'relative', 'display': 'none'});
154 }
155 max = temp > max ? temp : max;
156 counter++;
157 });
158
159 if (counter === this.$slides.length) {
160 this.$wrapper.css({'height': max}); //only change the wrapper height property once.
161 if(cb) {cb(max);} //fire callback with max height dimension.
162 }
163 }
164
165 /**
166 * Sets the max-height of each slide.
167 * @function
168 * @private
169 */
170 _setSlideHeight(height) {
171 this.$slides.each(function() {
172 $(this).css('max-height', height);
173 });
174 }
175
176 /**
177 * Adds event listeners to basically everything within the element.
178 * @function
179 * @private
180 */
181 _events() {
182 var _this = this;
183
184 //***************************************
185 //**Now using custom event - thanks to:**
186 //** Yohai Ararat of Toronto **
187 //***************************************
188 //
189 this.$element.off('.resizeme.zf.trigger').on({
190 'resizeme.zf.trigger': this._prepareForOrbit.bind(this)
191 })
192 if (this.$slides.length > 1) {
193
194 if (this.options.swipe) {
195 this.$slides.off('swipeleft.zf.orbit swiperight.zf.orbit')
196 .on('swipeleft.zf.orbit', function(e){
197 e.preventDefault();
198 _this.changeSlide(true);
199 }).on('swiperight.zf.orbit', function(e){
200 e.preventDefault();
201 _this.changeSlide(false);
202 });
203 }
204 //***************************************
205
206 if (this.options.autoPlay) {
207 this.$slides.on('click.zf.orbit', function() {
208 _this.$element.data('clickedOn', _this.$element.data('clickedOn') ? false : true);
209 _this.timer[_this.$element.data('clickedOn') ? 'pause' : 'start']();
210 });
211
212 if (this.options.pauseOnHover) {
213 this.$element.on('mouseenter.zf.orbit', function() {
214 _this.timer.pause();
215 }).on('mouseleave.zf.orbit', function() {
216 if (!_this.$element.data('clickedOn')) {
217 _this.timer.start();
218 }
219 });
220 }
221 }
222
223 if (this.options.navButtons) {
224 var $controls = this.$element.find(`.${this.options.nextClass}, .${this.options.prevClass}`);
225 $controls.attr('tabindex', 0)
226 //also need to handle enter/return and spacebar key presses
227 .on('click.zf.orbit touchend.zf.orbit', function(e){
228 e.preventDefault();
229 _this.changeSlide($(this).hasClass(_this.options.nextClass));
230 });
231 }
232
233 if (this.options.bullets) {
234 this.$bullets.on('click.zf.orbit touchend.zf.orbit', function() {
235 if (/is-active/g.test(this.className)) { return false; }//if this is active, kick out of function.
236 var idx = $(this).data('slide'),
237 ltr = idx > _this.$slides.filter('.is-active').data('slide'),
238 $slide = _this.$slides.eq(idx);
239
240 _this.changeSlide(ltr, $slide, idx);
241 });
242 }
243
244 if (this.options.accessible) {
245 this.$wrapper.add(this.$bullets).on('keydown.zf.orbit', function(e) {
246 // handle keyboard event with keyboard util
247 Keyboard.handleKey(e, 'Orbit', {
248 next: function() {
249 _this.changeSlide(true);
250 },
251 previous: function() {
252 _this.changeSlide(false);
253 },
254 handled: function() { // if bullet is focused, make sure focus moves
255 if ($(e.target).is(_this.$bullets)) {
256 _this.$bullets.filter('.is-active').focus();
257 }
258 }
259 });
260 });
261 }
262 }
263 }
264
265 /**
266 * Resets Orbit so it can be reinitialized
267 */
268 _reset() {
269 // Don't do anything if there are no slides (first run)
270 if (typeof this.$slides == 'undefined') {
271 return;
272 }
273
274 if (this.$slides.length > 1) {
275 // Remove old events
276 this.$element.off('.zf.orbit').find('*').off('.zf.orbit')
277
278 // Restart timer if autoPlay is enabled
279 if (this.options.autoPlay) {
280 this.timer.restart();
281 }
282
283 // Reset all sliddes
284 this.$slides.each(function(el) {
285 $(el).removeClass('is-active is-active is-in')
286 .removeAttr('aria-live')
287 .hide();
288 });
289
290 // Show the first slide
291 this.$slides.first().addClass('is-active').show();
292
293 // Triggers when the slide has finished animating
294 this.$element.trigger('slidechange.zf.orbit', [this.$slides.first()]);
295
296 // Select first bullet if bullets are present
297 if (this.options.bullets) {
298 this._updateBullets(0);
299 }
300 }
301 }
302
303 /**
304 * Changes the current slide to a new one.
305 * @function
306 * @param {Boolean} isLTR - flag if the slide should move left to right.
307 * @param {jQuery} chosenSlide - the jQuery element of the slide to show next, if one is selected.
308 * @param {Number} idx - the index of the new slide in its collection, if one chosen.
309 * @fires Orbit#slidechange
310 */
311 changeSlide(isLTR, chosenSlide, idx) {
312 if (!this.$slides) {return; } // Don't freak out if we're in the middle of cleanup
313 var $curSlide = this.$slides.filter('.is-active').eq(0);
314
315 if (/mui/g.test($curSlide[0].className)) { return false; } //if the slide is currently animating, kick out of the function
316
317 var $firstSlide = this.$slides.first(),
318 $lastSlide = this.$slides.last(),
319 dirIn = isLTR ? 'Right' : 'Left',
320 dirOut = isLTR ? 'Left' : 'Right',
321 _this = this,
322 $newSlide;
323
324 if (!chosenSlide) { //most of the time, this will be auto played or clicked from the navButtons.
325 $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!!!!!
326 (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
327 :
328 (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
329 } else {
330 $newSlide = chosenSlide;
331 }
332
333 if ($newSlide.length) {
334 /**
335 * Triggers before the next slide starts animating in and only if a next slide has been found.
336 * @event Orbit#beforeslidechange
337 */
338 this.$element.trigger('beforeslidechange.zf.orbit', [$curSlide, $newSlide]);
339
340 if (this.options.bullets) {
341 idx = idx || this.$slides.index($newSlide); //grab index to update bullets
342 this._updateBullets(idx);
343 }
344
345 if (this.options.useMUI && !this.$element.is(':hidden')) {
346 Motion.animateIn(
347 $newSlide.addClass('is-active').css({'position': 'absolute', 'top': 0}),
348 this.options[`animInFrom${dirIn}`],
349 function(){
350 $newSlide.css({'position': 'relative', 'display': 'block'})
351 .attr('aria-live', 'polite');
352 });
353
354 Motion.animateOut(
355 $curSlide.removeClass('is-active'),
356 this.options[`animOutTo${dirOut}`],
357 function(){
358 $curSlide.removeAttr('aria-live');
359 if(_this.options.autoPlay && !_this.timer.isPaused){
360 _this.timer.restart();
361 }
362 //do stuff?
363 });
364 } else {
365 $curSlide.removeClass('is-active is-in').removeAttr('aria-live').hide();
366 $newSlide.addClass('is-active is-in').attr('aria-live', 'polite').show();
367 if (this.options.autoPlay && !this.timer.isPaused) {
368 this.timer.restart();
369 }
370 }
371 /**
372 * Triggers when the slide has finished animating in.
373 * @event Orbit#slidechange
374 */
375 this.$element.trigger('slidechange.zf.orbit', [$newSlide]);
376 }
377 }
378
379 /**
380 * Updates the active state of the bullets, if displayed.
381 * @function
382 * @private
383 * @param {Number} idx - the index of the current slide.
384 */
385 _updateBullets(idx) {
386 var $oldBullet = this.$element.find(`.${this.options.boxOfBullets}`)
387 .find('.is-active').removeClass('is-active').blur(),
388 span = $oldBullet.find('span:last').detach(),
389 $newBullet = this.$bullets.eq(idx).addClass('is-active').append(span);
390 }
391
392 /**
393 * Destroys the carousel and hides the element.
394 * @function
395 */
396 _destroy() {
397 this.$element.off('.zf.orbit').find('*').off('.zf.orbit').end().hide();
398 }
399}
400
401Orbit.defaults = {
402 /**
403 * Tells the JS to look for and loadBullets.
404 * @option
405 * @type {boolean}
406 * @default true
407 */
408 bullets: true,
409 /**
410 * Tells the JS to apply event listeners to nav buttons
411 * @option
412 * @type {boolean}
413 * @default true
414 */
415 navButtons: true,
416 /**
417 * motion-ui animation class to apply
418 * @option
419 * @type {string}
420 * @default 'slide-in-right'
421 */
422 animInFromRight: 'slide-in-right',
423 /**
424 * motion-ui animation class to apply
425 * @option
426 * @type {string}
427 * @default 'slide-out-right'
428 */
429 animOutToRight: 'slide-out-right',
430 /**
431 * motion-ui animation class to apply
432 * @option
433 * @type {string}
434 * @default 'slide-in-left'
435 *
436 */
437 animInFromLeft: 'slide-in-left',
438 /**
439 * motion-ui animation class to apply
440 * @option
441 * @type {string}
442 * @default 'slide-out-left'
443 */
444 animOutToLeft: 'slide-out-left',
445 /**
446 * Allows Orbit to automatically animate on page load.
447 * @option
448 * @type {boolean}
449 * @default true
450 */
451 autoPlay: true,
452 /**
453 * Amount of time, in ms, between slide transitions
454 * @option
455 * @type {number}
456 * @default 5000
457 */
458 timerDelay: 5000,
459 /**
460 * Allows Orbit to infinitely loop through the slides
461 * @option
462 * @type {boolean}
463 * @default true
464 */
465 infiniteWrap: true,
466 /**
467 * Allows the Orbit slides to bind to swipe events for mobile, requires an additional util library
468 * @option
469 * @type {boolean}
470 * @default true
471 */
472 swipe: true,
473 /**
474 * Allows the timing function to pause animation on hover.
475 * @option
476 * @type {boolean}
477 * @default true
478 */
479 pauseOnHover: true,
480 /**
481 * Allows Orbit to bind keyboard events to the slider, to animate frames with arrow keys
482 * @option
483 * @type {boolean}
484 * @default true
485 */
486 accessible: true,
487 /**
488 * Class applied to the container of Orbit
489 * @option
490 * @type {string}
491 * @default 'orbit-container'
492 */
493 containerClass: 'orbit-container',
494 /**
495 * Class applied to individual slides.
496 * @option
497 * @type {string}
498 * @default 'orbit-slide'
499 */
500 slideClass: 'orbit-slide',
501 /**
502 * Class applied to the bullet container. You're welcome.
503 * @option
504 * @type {string}
505 * @default 'orbit-bullets'
506 */
507 boxOfBullets: 'orbit-bullets',
508 /**
509 * Class applied to the `next` navigation button.
510 * @option
511 * @type {string}
512 * @default 'orbit-next'
513 */
514 nextClass: 'orbit-next',
515 /**
516 * Class applied to the `previous` navigation button.
517 * @option
518 * @type {string}
519 * @default 'orbit-previous'
520 */
521 prevClass: 'orbit-previous',
522 /**
523 * Boolean to flag the js to use motion ui classes or not. Default to true for backwards compatability.
524 * @option
525 * @type {boolean}
526 * @default true
527 */
528 useMUI: true
529};
530
531export {Orbit};