#149, move slide selector to an option, alias old init signature
[deck.js.git] / core / deck.core.js
bloba13854a99a3179b56b5e04085c534e139942b1da
1 /*!
2 Deck JS - deck.core
3 Copyright (c) 2011-2013 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;
20   var events = {
21     /*
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.
27     */
28     beforeChange: 'deck.beforeChange',
30     /*
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     });
40     */
41     change: 'deck.change',
43     /*
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.
47     */
48     beforeInitialize: 'deck.beforeInit',
50     /*
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
63           }
64        });
65     });
66     */
67     initialize: 'deck.init'
68   };
70   var options = {};
71   var $document = $(document);
72   var stopPropagation = function(event) {
73     event.stopPropagation();
74   };
76   var updateContainerState = function() {
77     var oldIndex = $container.data('onSlide');
78     $container.removeClass(options.classes.onPrefix + oldIndex);
79     $container.addClass(options.classes.onPrefix + currentIndex);
80     $container.data('onSlide', currentIndex);
81   };
83   var updateChildCurrent = function() {
84     var $oldCurrent = $('.' + options.classes.current);
85     var $oldParents = $oldCurrent.parentsUntil(options.selectors.container);
86     var $newCurrent = slides[currentIndex];
87     var $newParents = $newCurrent.parentsUntil(options.selectors.container);
88     $oldParents.removeClass(options.classes.childCurrent);
89     $newParents.addClass(options.classes.childCurrent);
90   };
92   var removeOldSlideStates = function() {
93     var $all = $();
94     $.each(slides, function(i, el) {
95       $all = $all.add(el);
96     });
97     $all.removeClass([
98       options.classes.before,
99       options.classes.previous,
100       options.classes.current,
101       options.classes.next,
102       options.classes.after
103     ].join(' '));
104   };
106   var addNewSlideStates = function() {
107     slides[currentIndex].addClass(options.classes.current);
108     if (currentIndex > 0) {
109       slides[currentIndex-1].addClass(options.classes.previous);
110     }
111     if (currentIndex + 1 < slides.length) {
112       slides[currentIndex+1].addClass(options.classes.next);
113     }
114     if (currentIndex > 1) {
115       $.each(slides.slice(0, currentIndex - 1), function(i, $slide) {
116         $slide.addClass(options.classes.before);
117       });
118     }
119     if (currentIndex + 2 < slides.length) {
120       $.each(slides.slice(currentIndex+2), function(i, $slide) {
121         $slide.addClass(options.classes.after);
122       });
123     }
124   };
126   var updateStates = function() {
127     updateContainerState();
128     updateChildCurrent();
129     removeOldSlideStates();
130     addNewSlideStates();
131   };
133   var initSlidesArray = function(elements) {
134     if ($.isArray(elements)) {
135       $.each(elements, function(i, element) {
136         slides.push($(element));
137       });
138     }
139     else {
140       $(elements).each(function(i, element) {
141         slides.push($(element));
142       });
143     }
144   };
146   var bindKeyEvents = function() {
147     var editables = [
148       'input',
149       'textarea',
150       'select',
151       'button',
152       'meter',
153       'progress',
154       '[contentEditable]'
155     ].join(', ');
157     $document.unbind('keydown.deck').bind('keydown.deck', function(event) {
158       var isNext = event.which === options.keys.next;
159       var isPrev = event.which === options.keys.previous;
160       isNext = isNext || $.inArray(event.which, options.keys.next) > -1;
161       isPrev = isPrev || $.inArray(event.which, options.keys.previous) > -1;
163       if (isNext) {
164         methods.next();
165         event.preventDefault();
166       }
167       else if (isPrev) {
168         methods.prev();
169         event.preventDefault();
170       }
171     });
173     $document.undelegate(editables, 'keydown.deck', stopPropagation);
174     $document.delegate(editables, 'keydown.deck', stopPropagation);
175   };
177   var bindTouchEvents = function() {
178     var startTouch;
179     var direction = options.touch.swipeDirection;
180     var tolerance = options.touch.swipeTolerance;
181     var listenToHorizontal = ({ both: true, horizontal: true })[direction];
182     var listenToVertical = ({ both: true, vertical: true })[direction];
184     $container.unbind('touchstart.deck');
185     $container.bind('touchstart.deck', function(event) {
186       if (!startTouch) {
187         startTouch = $.extend({}, event.originalEvent.targetTouches[0]);
188       }
189     });
191     $container.unbind('touchmove.deck');
192     $container.bind('touchmove.deck', function(event) {
193       $.each(event.originalEvent.changedTouches, function(i, touch) {
194         if (!startTouch || touch.identifier !== startTouch.identifier) {
195           return true;
196         }
197         var xDistance = touch.screenX - startTouch.screenX;
198         var yDistance = touch.screenY - startTouch.screenY;
199         var leftToRight = xDistance > tolerance && listenToHorizontal;
200         var rightToLeft = xDistance < -tolerance && listenToHorizontal;
201         var topToBottom = yDistance > tolerance && listenToVertical;
202         var bottomToTop = yDistance < -tolerance && listenToVertical;
204         if (leftToRight || topToBottom) {
205           $.deck('prev');
206           startTouch = undefined;
207         }
208         else if (rightToLeft || bottomToTop) {
209           $.deck('next');
210           startTouch = undefined;
211         }
212         return false;
213       });
215       if (listenToVertical) {
216         event.preventDefault();
217       }
218     });
220     $container.unbind('touchend.deck');
221     $container.bind('touchend.deck', function(event) {
222       $.each(event.originalEvent.changedTouches, function(i, touch) {
223         if (startTouch && touch.identifier === startTouch.identifier) {
224           startTouch = undefined;
225         }
226       });
227     });
228   };
230   var indexInBounds = function(index) {
231     return typeof index === 'number' && index >=0 && index < slides.length;
232   };
234   var createBeforeInitEvent = function() {
235     var event = $.Event(events.beforeInitialize);
236     event.locks = 0;
237     event.done = $.noop;
238     event.lockInit = function() {
239       ++event.locks;
240     };
241     event.releaseInit = function() {
242       --event.locks;
243       if (!event.locks) {
244         event.done();
245       }
246     };
247     return event;
248   };
250   /* Methods exposed in the jQuery.deck namespace */
251   var methods = {
253     /*
254     jQuery.deck(selector, options)
256     selector: string | jQuery | array
257     options: object, optional
259     Initializes the deck, using each element matched by selector as a slide.
260     May also be passed an array of string selectors or jQuery objects, in
261     which case each selector in the array is considered a slide. The second
262     parameter is an optional options object which will extend the default
263     values.
265     $.deck('.slide');
267     or
269     $.deck([
270        '#first-slide',
271        '#second-slide',
272        '#etc'
273     ]);
274     */
275     init: function(opts) {
276       var beforeInitEvent = createBeforeInitEvent();
277       var overrides = opts;
279       if (!$.isPlainObject(opts)) {
280         overrides = arguments[1] || {};
281         $.extend(true, overrides, {
282           selectors: {
283             slides: arguments[0]
284           }
285         });
286       }
288       options = $.extend(true, {}, $.deck.defaults, overrides);
289       slides = [];
290       currentIndex = 0;
291       $container = $(options.selectors.container);
293       // Hide the deck while states are being applied to kill transitions
294       $container.addClass(options.classes.loading);
296       // populate the array of slides for pre-init
297       initSlidesArray(options.selectors.slides);
298       // Pre init event for preprocessing hooks
299       beforeInitEvent.done = function() {
300         // re-populate the array of slides
301         slides = [];
302         initSlidesArray(options.selectors.slides);
303         bindKeyEvents();
304         bindTouchEvents();
305         $container.scrollLeft(0).scrollTop(0);
307         if (slides.length) {
308           updateStates();
309         }
311         // Show deck again now that slides are in place
312         $container.removeClass(options.classes.loading);
313         $document.trigger(events.initialize);
314       };
316       $document.trigger(beforeInitEvent);
317       if (!beforeInitEvent.locks) {
318         beforeInitEvent.done();
319       }
320       window.setTimeout(function() {
321         if (beforeInitEvent.locks) {
322           if (window.console) {
323             window.console.warn('Something locked deck initialization\
324               without releasing it before the timeout. Proceeding with\
325               initialization anyway.');
326           }
327           beforeInitEvent.done();
328         }
329       }, options.initLockTimeout);
330     },
332     /*
333     jQuery.deck('go', index)
335     index: integer | string
337     Moves to the slide at the specified index if index is a number. Index is
338     0-based, so $.deck('go', 0); will move to the first slide. If index is a
339     string this will move to the slide with the specified id. If index is out
340     of bounds or doesn't match a slide id the call is ignored.
341     */
342     go: function(indexOrId) {
343       var beforeChangeEvent = $.Event(events.beforeChange);
344       var index;
346       /* Number index, easy. */
347       if (indexInBounds(indexOrId)) {
348         index = indexOrId;
349       }
350       /* Id string index, search for it and set integer index */
351       else if (typeof indexOrId === 'string') {
352         $.each(slides, function(i, $slide) {
353           if ($slide.attr('id') === indexOrId) {
354             index = i;
355             return false;
356           }
357         });
358       }
359       if (typeof index === 'undefined') {
360         return;
361       }
363       /* Trigger beforeChange. If nothing prevents the change, trigger
364       the slide change. */
365       $document.trigger(beforeChangeEvent, [currentIndex, index]);
366       if (!beforeChangeEvent.isDefaultPrevented()) {
367         $document.trigger(events.change, [currentIndex, index]);
368         currentIndex = index;
369         updateStates();
370       }
371     },
373     /*
374     jQuery.deck('next')
376     Moves to the next slide. If the last slide is already active, the call
377     is ignored.
378     */
379     next: function() {
380       methods.go(currentIndex+1);
381     },
383     /*
384     jQuery.deck('prev')
386     Moves to the previous slide. If the first slide is already active, the
387     call is ignored.
388     */
389     prev: function() {
390       methods.go(currentIndex-1);
391     },
393     /*
394     jQuery.deck('getSlide', index)
396     index: integer, optional
398     Returns a jQuery object containing the slide at index. If index is not
399     specified, the current slide is returned.
400     */
401     getSlide: function(index) {
402       index = typeof index !== 'undefined' ? index : currentIndex;
403       if (!indexInBounds(index)) {
404         return null;
405       }
406       return slides[index];
407     },
409     /*
410     jQuery.deck('getSlides')
412     Returns all slides as an array of jQuery objects.
413     */
414     getSlides: function() {
415       return slides;
416     },
418     /*
419     jQuery.deck('getContainer')
421     Returns a jQuery object containing the deck container as defined by the
422     container option.
423     */
424     getContainer: function() {
425       return $container;
426     },
428     /*
429     jQuery.deck('getOptions')
431     Returns the options object for the deck, including any overrides that
432     were defined at initialization.
433     */
434     getOptions: function() {
435       return options;
436     },
438     /*
439     jQuery.deck('extend', name, method)
441     name: string
442     method: function
444     Adds method to the deck namespace with the key of name. This doesn’t
445     give access to any private member data — public methods must still be
446     used within method — but lets extension authors piggyback on the deck
447     namespace rather than pollute jQuery.
449     $.deck('extend', 'alert', function(msg) {
450        alert(msg);
451     });
453     // Alerts 'boom'
454     $.deck('alert', 'boom');
455     */
456     extend: function(name, method) {
457       methods[name] = method;
458     }
459   };
461   /* jQuery extension */
462   $.deck = function(method, arg) {
463     var args = Array.prototype.slice.call(arguments, 1);
464     if (methods[method]) {
465       return methods[method].apply(this, args);
466     }
467     else {
468       return methods.init(method, arg);
469     }
470   };
472   /*
473   The default settings object for a deck. All deck extensions should extend
474   this object to add defaults for any of their options.
476   options.classes.after
477     This class is added to all slides that appear after the 'next' slide.
479   options.classes.before
480     This class is added to all slides that appear before the 'previous'
481     slide.
483   options.classes.childCurrent
484     This class is added to all elements in the DOM tree between the
485     'current' slide and the deck container. For standard slides, this is
486     mostly seen and used for nested slides.
488   options.classes.current
489     This class is added to the current slide.
491   options.classes.loading
492     This class is applied to the deck container during loading phases and is
493     primarily used as a way to short circuit transitions between states
494     where such transitions are distracting or unwanted.  For example, this
495     class is applied during deck initialization and then removed to prevent
496     all the slides from appearing stacked and transitioning into place
497     on load.
499   options.classes.next
500     This class is added to the slide immediately following the 'current'
501     slide.
503   options.classes.onPrefix
504     This prefix, concatenated with the current slide index, is added to the
505     deck container as you change slides.
507   options.classes.previous
508     This class is added to the slide immediately preceding the 'current'
509     slide.
511   options.selectors.container
512     Elements matched by this CSS selector will be considered the deck
513     container. The deck container is used to scope certain states of the
514     deck, as with the onPrefix option, or with extensions such as deck.goto
515     and deck.menu.
517   options.selectors.slides
518     Elements matched by this selector make up the individual deck slides.
519     If a user chooses to pass the slide selector as the first argument to
520     $.deck() on initialization it does the same thing as passing in this
521     option and this option value will be set to the value of that parameter.
523   options.keys.next
524     The numeric keycode used to go to the next slide.
526   options.keys.previous
527     The numeric keycode used to go to the previous slide.
529   options.touch.swipeDirection
530     The direction swipes occur to cause slide changes. Can be 'horizontal',
531     'vertical', or 'both'. Any other value or a falsy value will disable
532     swipe gestures for navigation.
534   options.touch.swipeTolerance
535     The number of pixels the users finger must travel to produce a swipe
536     gesture.
537   */
538   $.deck.defaults = {
539     classes: {
540       after: 'deck-after',
541       before: 'deck-before',
542       childCurrent: 'deck-child-current',
543       current: 'deck-current',
544       loading: 'deck-loading',
545       next: 'deck-next',
546       onPrefix: 'on-slide-',
547       previous: 'deck-previous'
548     },
550     selectors: {
551       container: '.deck-container',
552       slides: '.slide'
553     },
555     keys: {
556       // enter, space, page down, right arrow, down arrow,
557       next: [13, 32, 34, 39, 40],
558       // backspace, page up, left arrow, up arrow
559       previous: [8, 33, 37, 38]
560     },
562     touch: {
563       swipeDirection: 'horizontal',
564       swipeTolerance: 60
565     },
567     initLockTimeout: 10000
568   };
570   $document.ready(function() {
571     $('html').addClass('ready');
572   });
573 })(jQuery);