Prep for 1.1 release
[deck.js.git] / core / deck.core.js
blobfa5c1e90fc244063b6fb7bd3a81bdce8e397745c
1 /*!
2 Deck JS - deck.core
3 Copyright (c) 2011-2014 Caleb Troughton
4 Dual licensed under the MIT license.
5 https://github.com/imakewebthings/deck.js/blob/master/MIT-license.txt
6 */
8 /*
9 The deck.core module provides all the basic functionality for creating and
10 moving through a deck. It does so by applying classes to indicate the state of
11 the deck and its slides, allowing CSS to take care of the visual representation
12 of each state. It also provides methods for navigating the deck and inspecting
13 its state, as well as basic key bindings for going to the next and previous
14 slides. More functionality is provided by wholly separate extension modules
15 that use the API provided by core.
17 (function($, undefined) {
18 var slides, currentIndex, $container, $fragmentLinks;
20 var events = {
22 This event fires at the beginning of a slide change, before the actual
23 change occurs. Its purpose is to give extension authors a way to prevent
24 the slide change from occuring. This is done by calling preventDefault
25 on the event object within this event. If that is done, the deck.change
26 event will never be fired and the slide will not change.
28 beforeChange: 'deck.beforeChange',
31 This event fires whenever the current slide changes, whether by way of
32 next, prev, or go. The callback function is passed two parameters, from
33 and to, equal to the indices of the old slide and the new slide
34 respectively. If preventDefault is called on the event within this handler
35 the slide change does not occur.
37 $(document).bind('deck.change', function(event, from, to) {
38 alert('Moving from slide ' + from + ' to ' + to);
39 });
41 change: 'deck.change',
44 This event fires at the beginning of deck initialization, after the options
45 are set but before the slides array is created. This event makes a good hook
46 for preprocessing extensions looking to modify the deck.
48 beforeInitialize: 'deck.beforeInit',
51 This event fires at the end of deck initialization. Extensions should
52 implement any code that relies on user extensible options (key bindings,
53 element selectors, classes) within a handler for this event. Native
54 events associated with Deck JS should be scoped under a .deck event
55 namespace, as with the example below:
57 var $d = $(document);
58 $.deck.defaults.keys.myExtensionKeycode = 70; // 'h'
59 $d.bind('deck.init', function() {
60 $d.bind('keydown.deck', function(event) {
61 if (event.which === $.deck.getOptions().keys.myExtensionKeycode) {
62 // Rock out
64 });
65 });
67 initialize: 'deck.init'
70 var options = {};
71 var $document = $(document);
72 var $window = $(window);
73 var stopPropagation = function(event) {
74 event.stopPropagation();
77 var updateContainerState = function() {
78 var oldIndex = $container.data('onSlide');
79 $container.removeClass(options.classes.onPrefix + oldIndex);
80 $container.addClass(options.classes.onPrefix + currentIndex);
81 $container.data('onSlide', currentIndex);
84 var updateChildCurrent = function() {
85 var $oldCurrent = $('.' + options.classes.current);
86 var $oldParents = $oldCurrent.parentsUntil(options.selectors.container);
87 var $newCurrent = slides[currentIndex];
88 var $newParents = $newCurrent.parentsUntil(options.selectors.container);
89 $oldParents.removeClass(options.classes.childCurrent);
90 $newParents.addClass(options.classes.childCurrent);
93 var removeOldSlideStates = function() {
94 var $all = $();
95 $.each(slides, function(i, el) {
96 $all = $all.add(el);
97 });
98 $all.removeClass([
99 options.classes.before,
100 options.classes.previous,
101 options.classes.current,
102 options.classes.next,
103 options.classes.after
104 ].join(' '));
107 var addNewSlideStates = function() {
108 slides[currentIndex].addClass(options.classes.current);
109 if (currentIndex > 0) {
110 slides[currentIndex-1].addClass(options.classes.previous);
112 if (currentIndex + 1 < slides.length) {
113 slides[currentIndex+1].addClass(options.classes.next);
115 if (currentIndex > 1) {
116 $.each(slides.slice(0, currentIndex - 1), function(i, $slide) {
117 $slide.addClass(options.classes.before);
120 if (currentIndex + 2 < slides.length) {
121 $.each(slides.slice(currentIndex+2), function(i, $slide) {
122 $slide.addClass(options.classes.after);
127 var setAriaHiddens = function() {
128 $(options.selectors.slides).each(function() {
129 var $slide = $(this);
130 var isSub = $slide.closest('.' + options.classes.childCurrent).length;
131 var isBefore = $slide.hasClass(options.classes.before) && !isSub;
132 var isPrevious = $slide.hasClass(options.classes.previous) && !isSub;
133 var isNext = $slide.hasClass(options.classes.next);
134 var isAfter = $slide.hasClass(options.classes.after);
135 var ariaHiddenValue = isBefore || isPrevious || isNext || isAfter;
136 $slide.attr('aria-hidden', ariaHiddenValue);
140 var updateStates = function() {
141 updateContainerState();
142 updateChildCurrent();
143 removeOldSlideStates();
144 addNewSlideStates();
145 if (options.setAriaHiddens) {
146 setAriaHiddens();
150 var initSlidesArray = function(elements) {
151 if ($.isArray(elements)) {
152 $.each(elements, function(i, element) {
153 slides.push($(element));
156 else {
157 $(elements).each(function(i, element) {
158 slides.push($(element));
163 var bindKeyEvents = function() {
164 var editables = [
165 'input',
166 'textarea',
167 'select',
168 'button',
169 'meter',
170 'progress',
171 '[contentEditable]'
172 ].join(', ');
174 $document.unbind('keydown.deck').bind('keydown.deck', function(event) {
175 var isNext = event.which === options.keys.next;
176 var isPrev = event.which === options.keys.previous;
177 isNext = isNext || $.inArray(event.which, options.keys.next) > -1;
178 isPrev = isPrev || $.inArray(event.which, options.keys.previous) > -1;
180 if (isNext) {
181 methods.next();
182 event.preventDefault();
184 else if (isPrev) {
185 methods.prev();
186 event.preventDefault();
190 $document.undelegate(editables, 'keydown.deck', stopPropagation);
191 $document.delegate(editables, 'keydown.deck', stopPropagation);
194 var bindTouchEvents = function() {
195 var startTouch;
196 var direction = options.touch.swipeDirection;
197 var tolerance = options.touch.swipeTolerance;
198 var listenToHorizontal = ({ both: true, horizontal: true })[direction];
199 var listenToVertical = ({ both: true, vertical: true })[direction];
201 $container.unbind('touchstart.deck');
202 $container.bind('touchstart.deck', function(event) {
203 if (!startTouch) {
204 startTouch = $.extend({}, event.originalEvent.targetTouches[0]);
208 $container.unbind('touchmove.deck');
209 $container.bind('touchmove.deck', function(event) {
210 $.each(event.originalEvent.changedTouches, function(i, touch) {
211 if (!startTouch || touch.identifier !== startTouch.identifier) {
212 return true;
214 var xDistance = touch.screenX - startTouch.screenX;
215 var yDistance = touch.screenY - startTouch.screenY;
216 var leftToRight = xDistance > tolerance && listenToHorizontal;
217 var rightToLeft = xDistance < -tolerance && listenToHorizontal;
218 var topToBottom = yDistance > tolerance && listenToVertical;
219 var bottomToTop = yDistance < -tolerance && listenToVertical;
221 if (leftToRight || topToBottom) {
222 $.deck('prev');
223 startTouch = undefined;
225 else if (rightToLeft || bottomToTop) {
226 $.deck('next');
227 startTouch = undefined;
229 return false;
232 if (listenToVertical) {
233 event.preventDefault();
237 $container.unbind('touchend.deck');
238 $container.bind('touchend.deck', function(event) {
239 $.each(event.originalEvent.changedTouches, function(i, touch) {
240 if (startTouch && touch.identifier === startTouch.identifier) {
241 startTouch = undefined;
247 var indexInBounds = function(index) {
248 return typeof index === 'number' && index >=0 && index < slides.length;
251 var createBeforeInitEvent = function() {
252 var event = $.Event(events.beforeInitialize);
253 event.locks = 0;
254 event.done = $.noop;
255 event.lockInit = function() {
256 ++event.locks;
258 event.releaseInit = function() {
259 --event.locks;
260 if (!event.locks) {
261 event.done();
264 return event;
267 var goByHash = function(str) {
268 var id = str.substr(str.indexOf("#") + 1);
270 $.each(slides, function(i, $slide) {
271 if ($slide.attr('id') === id) {
272 $.deck('go', i);
273 return false;
277 // If we don't set these to 0 the container scrolls due to hashchange
278 if (options.preventFragmentScroll) {
279 $.deck('getContainer').scrollLeft(0).scrollTop(0);
283 var assignSlideId = function(i, $slide) {
284 var currentId = $slide.attr('id');
285 var previouslyAssigned = $slide.data('deckAssignedId') === currentId;
286 if (!currentId || previouslyAssigned) {
287 $slide.attr('id', options.hashPrefix + i);
288 $slide.data('deckAssignedId', options.hashPrefix + i);
292 var removeContainerHashClass = function(id) {
293 $container.removeClass(options.classes.onPrefix + id);
296 var addContainerHashClass = function(id) {
297 $container.addClass(options.classes.onPrefix + id);
300 var setupHashBehaviors = function() {
301 $fragmentLinks = $();
302 $.each(slides, function(i, $slide) {
303 var hash;
305 assignSlideId(i, $slide);
306 hash = '#' + $slide.attr('id');
307 if (hash === window.location.hash) {
308 setTimeout(function() {
309 $.deck('go', i);
310 }, 1);
312 $fragmentLinks = $fragmentLinks.add('a[href="' + hash + '"]');
315 if (slides.length) {
316 addContainerHashClass($.deck('getSlide').attr('id'));
320 var changeHash = function(from, to) {
321 var hash = '#' + $.deck('getSlide', to).attr('id');
322 var hashPath = window.location.href.replace(/#.*/, '') + hash;
324 removeContainerHashClass($.deck('getSlide', from).attr('id'));
325 addContainerHashClass($.deck('getSlide', to).attr('id'));
326 if (Modernizr.history) {
327 window.history.replaceState({}, "", hashPath);
331 /* Methods exposed in the jQuery.deck namespace */
332 var methods = {
335 jQuery.deck(selector, options)
337 selector: string | jQuery | array
338 options: object, optional
340 Initializes the deck, using each element matched by selector as a slide.
341 May also be passed an array of string selectors or jQuery objects, in
342 which case each selector in the array is considered a slide. The second
343 parameter is an optional options object which will extend the default
344 values.
346 $.deck('.slide');
350 $.deck([
351 '#first-slide',
352 '#second-slide',
353 '#etc'
356 init: function(opts) {
357 var beforeInitEvent = createBeforeInitEvent();
358 var overrides = opts;
360 if (!$.isPlainObject(opts)) {
361 overrides = arguments[1] || {};
362 $.extend(true, overrides, {
363 selectors: {
364 slides: arguments[0]
369 options = $.extend(true, {}, $.deck.defaults, overrides);
370 slides = [];
371 currentIndex = 0;
372 $container = $(options.selectors.container);
374 // Hide the deck while states are being applied to kill transitions
375 $container.addClass(options.classes.loading);
377 // populate the array of slides for pre-init
378 initSlidesArray(options.selectors.slides);
379 // Pre init event for preprocessing hooks
380 beforeInitEvent.done = function() {
381 // re-populate the array of slides
382 slides = [];
383 initSlidesArray(options.selectors.slides);
384 setupHashBehaviors();
385 bindKeyEvents();
386 bindTouchEvents();
387 $container.scrollLeft(0).scrollTop(0);
389 if (slides.length) {
390 updateStates();
393 // Show deck again now that slides are in place
394 $container.removeClass(options.classes.loading);
395 $document.trigger(events.initialize);
398 $document.trigger(beforeInitEvent);
399 if (!beforeInitEvent.locks) {
400 beforeInitEvent.done();
402 window.setTimeout(function() {
403 if (beforeInitEvent.locks) {
404 if (window.console) {
405 window.console.warn('Something locked deck initialization\
406 without releasing it before the timeout. Proceeding with\
407 initialization anyway.');
409 beforeInitEvent.done();
411 }, options.initLockTimeout);
415 jQuery.deck('go', index)
417 index: integer | string
419 Moves to the slide at the specified index if index is a number. Index is
420 0-based, so $.deck('go', 0); will move to the first slide. If index is a
421 string this will move to the slide with the specified id. If index is out
422 of bounds or doesn't match a slide id the call is ignored.
424 go: function(indexOrId) {
425 var beforeChangeEvent = $.Event(events.beforeChange);
426 var index;
428 /* Number index, easy. */
429 if (indexInBounds(indexOrId)) {
430 index = indexOrId;
432 /* Id string index, search for it and set integer index */
433 else if (typeof indexOrId === 'string') {
434 $.each(slides, function(i, $slide) {
435 if ($slide.attr('id') === indexOrId) {
436 index = i;
437 return false;
441 if (typeof index === 'undefined') {
442 return;
445 /* Trigger beforeChange. If nothing prevents the change, trigger
446 the slide change. */
447 $document.trigger(beforeChangeEvent, [currentIndex, index]);
448 if (!beforeChangeEvent.isDefaultPrevented()) {
449 $document.trigger(events.change, [currentIndex, index]);
450 changeHash(currentIndex, index);
451 currentIndex = index;
452 updateStates();
457 jQuery.deck('next')
459 Moves to the next slide. If the last slide is already active, the call
460 is ignored.
462 next: function() {
463 methods.go(currentIndex+1);
467 jQuery.deck('prev')
469 Moves to the previous slide. If the first slide is already active, the
470 call is ignored.
472 prev: function() {
473 methods.go(currentIndex-1);
477 jQuery.deck('getSlide', index)
479 index: integer, optional
481 Returns a jQuery object containing the slide at index. If index is not
482 specified, the current slide is returned.
484 getSlide: function(index) {
485 index = typeof index !== 'undefined' ? index : currentIndex;
486 if (!indexInBounds(index)) {
487 return null;
489 return slides[index];
493 jQuery.deck('getSlides')
495 Returns all slides as an array of jQuery objects.
497 getSlides: function() {
498 return slides;
502 jQuery.deck('getTopLevelSlides')
504 Returns all slides that are not subslides.
506 getTopLevelSlides: function() {
507 var topLevelSlides = [];
508 var slideSelector = options.selectors.slides;
509 var subSelector = [slideSelector, slideSelector].join(' ');
510 $.each(slides, function(i, $slide) {
511 if (!$slide.is(subSelector)) {
512 topLevelSlides.push($slide);
515 return topLevelSlides;
519 jQuery.deck('getNestedSlides', index)
521 index: integer, optional
523 Returns all the nested slides of the current slide. If index is
524 specified it returns the nested slides of the slide at that index.
525 If there are no nested slides this will return an empty array.
527 getNestedSlides: function(index) {
528 var targetIndex = index == null ? currentIndex : index;
529 var $targetSlide = $.deck('getSlide', targetIndex);
530 var $nesteds = $targetSlide.find(options.selectors.slides);
531 var nesteds = $nesteds.get();
532 return $.map(nesteds, function(slide, i) {
533 return $(slide);
539 jQuery.deck('getContainer')
541 Returns a jQuery object containing the deck container as defined by the
542 container option.
544 getContainer: function() {
545 return $container;
549 jQuery.deck('getOptions')
551 Returns the options object for the deck, including any overrides that
552 were defined at initialization.
554 getOptions: function() {
555 return options;
559 jQuery.deck('extend', name, method)
561 name: string
562 method: function
564 Adds method to the deck namespace with the key of name. This doesn’t
565 give access to any private member data — public methods must still be
566 used within method — but lets extension authors piggyback on the deck
567 namespace rather than pollute jQuery.
569 $.deck('extend', 'alert', function(msg) {
570 alert(msg);
573 // Alerts 'boom'
574 $.deck('alert', 'boom');
576 extend: function(name, method) {
577 methods[name] = method;
581 /* jQuery extension */
582 $.deck = function(method, arg) {
583 var args = Array.prototype.slice.call(arguments, 1);
584 if (methods[method]) {
585 return methods[method].apply(this, args);
587 else {
588 return methods.init(method, arg);
593 The default settings object for a deck. All deck extensions should extend
594 this object to add defaults for any of their options.
596 options.classes.after
597 This class is added to all slides that appear after the 'next' slide.
599 options.classes.before
600 This class is added to all slides that appear before the 'previous'
601 slide.
603 options.classes.childCurrent
604 This class is added to all elements in the DOM tree between the
605 'current' slide and the deck container. For standard slides, this is
606 mostly seen and used for nested slides.
608 options.classes.current
609 This class is added to the current slide.
611 options.classes.loading
612 This class is applied to the deck container during loading phases and is
613 primarily used as a way to short circuit transitions between states
614 where such transitions are distracting or unwanted. For example, this
615 class is applied during deck initialization and then removed to prevent
616 all the slides from appearing stacked and transitioning into place
617 on load.
619 options.classes.next
620 This class is added to the slide immediately following the 'current'
621 slide.
623 options.classes.onPrefix
624 This prefix, concatenated with the current slide index, is added to the
625 deck container as you change slides.
627 options.classes.previous
628 This class is added to the slide immediately preceding the 'current'
629 slide.
631 options.selectors.container
632 Elements matched by this CSS selector will be considered the deck
633 container. The deck container is used to scope certain states of the
634 deck, as with the onPrefix option, or with extensions such as deck.goto
635 and deck.menu.
637 options.selectors.slides
638 Elements matched by this selector make up the individual deck slides.
639 If a user chooses to pass the slide selector as the first argument to
640 $.deck() on initialization it does the same thing as passing in this
641 option and this option value will be set to the value of that parameter.
643 options.keys.next
644 The numeric keycode used to go to the next slide.
646 options.keys.previous
647 The numeric keycode used to go to the previous slide.
649 options.touch.swipeDirection
650 The direction swipes occur to cause slide changes. Can be 'horizontal',
651 'vertical', or 'both'. Any other value or a falsy value will disable
652 swipe gestures for navigation.
654 options.touch.swipeTolerance
655 The number of pixels the users finger must travel to produce a swipe
656 gesture.
658 options.hashPrefix
659 Every slide that does not have an id is assigned one at initialization.
660 Assigned ids take the form of hashPrefix + slideIndex, e.g., slide-0,
661 slide-12, etc.
663 options.preventFragmentScroll
664 When deep linking to a hash of a nested slide, this scrolls the deck
665 container to the top, undoing the natural browser behavior of scrolling
666 to the document fragment on load.
668 options.setAriaHiddens
669 When set to true, deck.js will set aria hidden attributes for slides
670 that do not appear offscreen according to a typical heirarchical
671 deck structure. You may want to turn this off if you are using a theme
672 where slides besides the current slide are visible on screen and should
673 be accessible to screenreaders.
675 $.deck.defaults = {
676 classes: {
677 after: 'deck-after',
678 before: 'deck-before',
679 childCurrent: 'deck-child-current',
680 current: 'deck-current',
681 loading: 'deck-loading',
682 next: 'deck-next',
683 onPrefix: 'on-slide-',
684 previous: 'deck-previous'
687 selectors: {
688 container: '.deck-container',
689 slides: '.slide'
692 keys: {
693 // enter, space, page down, right arrow, down arrow,
694 next: [13, 32, 34, 39, 40],
695 // backspace, page up, left arrow, up arrow
696 previous: [8, 33, 37, 38]
699 touch: {
700 swipeDirection: 'horizontal',
701 swipeTolerance: 60
704 initLockTimeout: 10000,
705 hashPrefix: 'slide-',
706 preventFragmentScroll: true,
707 setAriaHiddens: true
710 $document.ready(function() {
711 $('html').addClass('ready');
714 $window.bind('hashchange.deck', function(event) {
715 if (event.originalEvent && event.originalEvent.newURL) {
716 goByHash(event.originalEvent.newURL);
718 else {
719 goByHash(window.location.hash);
723 $window.bind('load.deck', function() {
724 if (options.preventFragmentScroll) {
725 $container.scrollLeft(0).scrollTop(0);
728 })(jQuery);