foundation.orbit.js 15 KB

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