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
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
;
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);
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:
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) {
67 initialize
: 'deck.init'
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() {
95 $.each(slides
, function(i
, el
) {
99 options
.classes
.before
,
100 options
.classes
.previous
,
101 options
.classes
.current
,
102 options
.classes
.next
,
103 options
.classes
.after
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();
145 if (options
.setAriaHiddens
) {
150 var initSlidesArray = function(elements
) {
151 if ($.isArray(elements
)) {
152 $.each(elements
, function(i
, element
) {
153 slides
.push($(element
));
157 $(elements
).each(function(i
, element
) {
158 slides
.push($(element
));
163 var bindKeyEvents = function() {
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;
182 event
.preventDefault();
186 event
.preventDefault();
190 $document
.undelegate(editables
, 'keydown.deck', stopPropagation
);
191 $document
.delegate(editables
, 'keydown.deck', stopPropagation
);
194 var bindTouchEvents = function() {
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
) {
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
) {
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
) {
223 startTouch
= undefined;
225 else if (rightToLeft
|| bottomToTop
) {
227 startTouch
= undefined;
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
);
255 event
.lockInit = function() {
258 event
.releaseInit = function() {
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
) {
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
) {
305 assignSlideId(i
, $slide
);
306 hash
= '#' + $slide
.attr('id');
307 if (hash
=== window
.location
.hash
) {
308 setTimeout(function() {
312 $fragmentLinks
= $fragmentLinks
.add('a[href="' + hash
+ '"]');
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 */
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
356 init: function(opts
) {
357 var beforeInitEvent
= createBeforeInitEvent();
358 var overrides
= opts
;
360 if (!$.isPlainObject(opts
)) {
361 overrides
= arguments
[1] || {};
362 $.extend(true, overrides
, {
369 options
= $.extend(true, {}, $.deck
.defaults
, overrides
);
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
383 initSlidesArray(options
.selectors
.slides
);
384 setupHashBehaviors();
387 $container
.scrollLeft(0).scrollTop(0);
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
);
428 /* Number index, easy. */
429 if (indexInBounds(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
) {
441 if (typeof index
=== 'undefined') {
445 /* Trigger beforeChange. If nothing prevents the change, trigger
447 $document
.trigger(beforeChangeEvent
, [currentIndex
, index
]);
448 if (!beforeChangeEvent
.isDefaultPrevented()) {
449 $document
.trigger(events
.change
, [currentIndex
, index
]);
450 changeHash(currentIndex
, index
);
451 currentIndex
= index
;
459 Moves to the next slide. If the last slide is already active, the call
463 methods
.go(currentIndex
+1);
469 Moves to the previous slide. If the first slide is already active, the
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
)) {
489 return slides
[index
];
493 jQuery.deck('getSlides')
495 Returns all slides as an array of jQuery objects.
497 getSlides: function() {
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
) {
539 jQuery.deck('getContainer')
541 Returns a jQuery object containing the deck container as defined by the
544 getContainer: function() {
549 jQuery.deck('getOptions')
551 Returns the options object for the deck, including any overrides that
552 were defined at initialization.
554 getOptions: function() {
559 jQuery.deck('extend', name, method)
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) {
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
);
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'
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
620 This class is added to the slide immediately following the 'current'
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'
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
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.
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
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,
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.
678 before
: 'deck-before',
679 childCurrent
: 'deck-child-current',
680 current
: 'deck-current',
681 loading
: 'deck-loading',
683 onPrefix
: 'on-slide-',
684 previous
: 'deck-previous'
688 container
: '.deck-container',
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]
700 swipeDirection
: 'horizontal',
704 initLockTimeout
: 10000,
705 hashPrefix
: 'slide-',
706 preventFragmentScroll
: 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
);
719 goByHash(window
.location
.hash
);
723 $window
.bind('load.deck', function() {
724 if (options
.preventFragmentScroll
) {
725 $container
.scrollLeft(0).scrollTop(0);