Add deck.js slide deck with basic layout
[kai/lca12.git] / core / deck.core.js
1 /*!
2 Deck JS - deck.core
3 Copyright (c) 2011 Caleb Troughton
4 Dual licensed under the MIT license and GPL license.
5 https://github.com/imakewebthings/deck.js/blob/master/MIT-license.txt
6 https://github.com/imakewebthings/deck.js/blob/master/GPL-license.txt
7 */
8
9 /*
10 The deck.core module provides all the basic functionality for creating and
11 moving through a deck.  It does so by applying classes to indicate the state of
12 the deck and its slides, allowing CSS to take care of the visual representation
13 of each state.  It also provides methods for navigating the deck and inspecting
14 its state, as well as basic key bindings for going to the next and previous
15 slides.  More functionality is provided by wholly separate extension modules
16 that use the API provided by core.
17 */
18 (function($, deck, document, undefined) {
19         var slides, // Array of all the uh, slides...
20         current, // Array index of the current slide
21         $container, // Keeping this cached
22         
23         events = {
24                 /*
25                 This event fires whenever the current slide changes, whether by way of
26                 next, prev, or go. The callback function is passed two parameters, from
27                 and to, equal to the indices of the old slide and the new slide
28                 respectively. If preventDefault is called on the event within this handler
29                 the slide change does not occur.
30                 
31                 $(document).bind('deck.change', function(event, from, to) {
32                    alert('Moving from slide ' + from + ' to ' + to);
33                 });
34                 */
35                 change: 'deck.change',
36                 
37                 /*
38                 This event fires at the beginning of deck initialization, after the options
39                 are set but before the slides array is created.  This event makes a good hook
40                 for preprocessing extensions looking to modify the deck.
41                 */
42                 beforeInitialize: 'deck.beforeInit',
43                 
44                 /*
45                 This event fires at the end of deck initialization. Extensions should
46                 implement any code that relies on user extensible options (key bindings,
47                 element selectors, classes) within a handler for this event. Native
48                 events associated with Deck JS should be scoped under a .deck event
49                 namespace, as with the example below:
50                 
51                 var $d = $(document);
52                 $.deck.defaults.keys.myExtensionKeycode = 70; // 'h'
53                 $d.bind('deck.init', function() {
54                    $d.bind('keydown.deck', function(event) {
55                       if (event.which === $.deck.getOptions().keys.myExtensionKeycode) {
56                          // Rock out
57                       }
58                    });
59                 });
60                 */
61                 initialize: 'deck.init' 
62         },
63         
64         options = {},
65         $d = $(document),
66         
67         /*
68         Internal function. Updates slide and container classes based on which
69         slide is the current slide.
70         */
71         updateStates = function() {
72                 var oc = options.classes,
73                 osc = options.selectors.container,
74                 old = $container.data('onSlide'),
75                 $all = $();
76                 
77                 // Container state
78                 $container.removeClass(oc.onPrefix + old)
79                         .addClass(oc.onPrefix + current)
80                         .data('onSlide', current);
81                 
82                 // Remove and re-add child-current classes for nesting
83                 $('.' + oc.current).parentsUntil(osc).removeClass(oc.childCurrent);
84                 slides[current].parentsUntil(osc).addClass(oc.childCurrent);
85                 
86                 // Remove previous states
87                 $.each(slides, function(i, el) {
88                         $all = $all.add(el);
89                 });
90                 $all.removeClass([
91                         oc.before,
92                         oc.previous,
93                         oc.current,
94                         oc.next,
95                         oc.after
96                 ].join(" "));
97                 
98                 // Add new states back in
99                 slides[current].addClass(oc.current);
100                 if (current > 0) {
101                         slides[current-1].addClass(oc.previous);
102                 }
103                 if (current + 1 < slides.length) {
104                         slides[current+1].addClass(oc.next);
105                 }
106                 if (current > 1) {
107                         $.each(slides.slice(0, current - 1), function(i, el) {
108                                 el.addClass(oc.before);
109                         });
110                 }
111                 if (current + 2 < slides.length) {
112                         $.each(slides.slice(current+2), function(i, el) {
113                                 el.addClass(oc.after);
114                         });
115                 }
116         },
117         
118         /* Methods exposed in the jQuery.deck namespace */
119         methods = {
120                 
121                 /*
122                 jQuery.deck(selector, options)
123                 
124                 selector: string | jQuery | array
125                 options: object, optional
126                                 
127                 Initializes the deck, using each element matched by selector as a slide.
128                 May also be passed an array of string selectors or jQuery objects, in
129                 which case each selector in the array is considered a slide. The second
130                 parameter is an optional options object which will extend the default
131                 values.
132                 
133                 $.deck('.slide');
134                 
135                 or
136                 
137                 $.deck([
138                    '#first-slide',
139                    '#second-slide',
140                    '#etc'
141                 ]);
142                 */      
143                 init: function(elements, opts) {
144                         var startTouch,
145                         tolerance,
146                         esp = function(e) {
147                                 e.stopPropagation();
148                         };
149                         
150                         options = $.extend(true, {}, $[deck].defaults, opts);
151                         slides = [];
152                         current = 0;
153                         $container = $(options.selectors.container);
154                         tolerance = options.touch.swipeTolerance;
155                         
156                         // Pre init event for preprocessing hooks
157                         $d.trigger(events.beforeInitialize);
158                         
159                         // Hide the deck while states are being applied to kill transitions
160                         $container.addClass(options.classes.loading);
161                         
162                         // Fill slides array depending on parameter type
163                         if ($.isArray(elements)) {
164                                 $.each(elements, function(i, e) {
165                                         slides.push($(e));
166                                 });
167                         }
168                         else {
169                                 $(elements).each(function(i, e) {
170                                         slides.push($(e));
171                                 });
172                         }
173                         
174                         /* Remove any previous bindings, and rebind key events */
175                         $d.unbind('keydown.deck').bind('keydown.deck', function(e) {
176                                 if (e.which === options.keys.next || $.inArray(e.which, options.keys.next) > -1) {
177                                         methods.next();
178                                         e.preventDefault();
179                                 }
180                                 else if (e.which === options.keys.previous || $.inArray(e.which, options.keys.previous) > -1) {
181                                         methods.prev();
182                                         e.preventDefault();
183                                 }
184                         });
185                         
186                         /* Bind touch events for swiping between slides on touch devices */
187                         $container.unbind('touchstart.deck').bind('touchstart.deck', function(e) {
188                                 if (!startTouch) {
189                                         startTouch = $.extend({}, e.originalEvent.targetTouches[0]);
190                                 }
191                         })
192                         .unbind('touchmove.deck').bind('touchmove.deck', function(e) {
193                                 $.each(e.originalEvent.changedTouches, function(i, t) {
194                                         if (startTouch && t.identifier === startTouch.identifier) {
195                                                 if (t.screenX - startTouch.screenX > tolerance || t.screenY - startTouch.screenY > tolerance) {
196                                                         $[deck]('prev');
197                                                         startTouch = undefined;
198                                                 }
199                                                 else if (t.screenX - startTouch.screenX < -1 * tolerance || t.screenY - startTouch.screenY < -1 * tolerance) {
200                                                         $[deck]('next');
201                                                         startTouch = undefined;
202                                                 }
203                                                 return false;
204                                         }
205                                 });
206                                 e.preventDefault();
207                         })
208                         .unbind('touchend.deck').bind('touchend.deck', function(t) {
209                                 $.each(t.originalEvent.changedTouches, function(i, t) {
210                                         if (startTouch && t.identifier === startTouch.identifier) {
211                                                 startTouch = undefined;
212                                         }
213                                 });
214                         })
215                         .scrollLeft(0).scrollTop(0)
216                         /* Stop propagation of key events within editable elements of slides */
217                         .undelegate('input, textarea, select, button, meter, progress, [contentEditable]', 'keydown', esp)
218                         .delegate('input, textarea, select, button, meter, progress, [contentEditable]', 'keydown', esp);
219                         
220                         /*
221                         Kick iframe videos, which dont like to redraw w/ transforms.
222                         Remove this if Webkit ever fixes it.
223                          */
224                         $.each(slides, function(i, $el) {
225                                 $el.unbind('webkitTransitionEnd.deck').bind('webkitTransitionEnd.deck',
226                                 function(event) {
227                                         if ($el.hasClass($[deck]('getOptions').classes.current)) {
228                                                 var embeds = $(this).find('iframe').css('opacity', 0);
229                                                 window.setTimeout(function() {
230                                                         embeds.css('opacity', 1);
231                                                 }, 100);
232                                         }
233                                 });
234                         });
235                         
236                         if (slides.length) {
237                                 updateStates();
238                         }
239                         
240                         // Show deck again now that slides are in place
241                         $container.removeClass(options.classes.loading);
242                         $d.trigger(events.initialize);
243                 },
244                 
245                 /*
246                 jQuery.deck('go', index)
247                 
248                 index: integer | string
249                 
250                 Moves to the slide at the specified index if index is a number. Index is
251                 0-based, so $.deck('go', 0); will move to the first slide. If index is a
252                 string this will move to the slide with the specified id. If index is out
253                 of bounds or doesn't match a slide id the call is ignored.
254                 */
255                 go: function(index) {
256                         var e = $.Event(events.change),
257                         ndx;
258                         
259                         /* Number index, easy. */
260                         if (typeof index === 'number' && index >= 0 && index < slides.length) {
261                                 ndx = index;
262                         }
263                         /* Id string index, search for it and set integer index */
264                         else if (typeof index === 'string') {
265                                 $.each(slides, function(i, $slide) {
266                                         if ($slide.attr('id') === index) {
267                                                 ndx = i;
268                                                 return false;
269                                         }
270                                 });
271                         };
272                         
273                         /* Out of bounds, id doesn't exist, illegal input, eject */
274                         if (typeof ndx === 'undefined') return;
275                         
276                         $d.trigger(e, [current, ndx]);
277                         if (e.isDefaultPrevented()) {
278                                 /* Trigger the event again and undo the damage done by extensions. */
279                                 $d.trigger(events.change, [ndx, current]);
280                         }
281                         else {
282                                 current = ndx;
283                                 updateStates();
284                         }
285                 },
286                 
287                 /*
288                 jQuery.deck('next')
289                 
290                 Moves to the next slide. If the last slide is already active, the call
291                 is ignored.
292                 */
293                 next: function() {
294                         methods.go(current+1);
295                 },
296                 
297                 /*
298                 jQuery.deck('prev')
299                 
300                 Moves to the previous slide. If the first slide is already active, the
301                 call is ignored.
302                 */
303                 prev: function() {
304                         methods.go(current-1);
305                 },
306                 
307                 /*
308                 jQuery.deck('getSlide', index)
309                 
310                 index: integer, optional
311                 
312                 Returns a jQuery object containing the slide at index. If index is not
313                 specified, the current slide is returned.
314                 */
315                 getSlide: function(index) {
316                         var i = typeof index !== 'undefined' ? index : current;
317                         if (typeof i != 'number' || i < 0 || i >= slides.length) return null;
318                         return slides[i];
319                 },
320                 
321                 /*
322                 jQuery.deck('getSlides')
323                 
324                 Returns all slides as an array of jQuery objects.
325                 */
326                 getSlides: function() {
327                         return slides;
328                 },
329                 
330                 /*
331                 jQuery.deck('getContainer')
332                 
333                 Returns a jQuery object containing the deck container as defined by the
334                 container option.
335                 */
336                 getContainer: function() {
337                         return $container;
338                 },
339                 
340                 /*
341                 jQuery.deck('getOptions')
342                 
343                 Returns the options object for the deck, including any overrides that
344                 were defined at initialization.
345                 */
346                 getOptions: function() {
347                         return options;
348                 },
349                 
350                 /*
351                 jQuery.deck('extend', name, method)
352                 
353                 name: string
354                 method: function
355                 
356                 Adds method to the deck namespace with the key of name. This doesn’t
357                 give access to any private member data — public methods must still be
358                 used within method — but lets extension authors piggyback on the deck
359                 namespace rather than pollute jQuery.
360                 
361                 $.deck('extend', 'alert', function(msg) {
362                    alert(msg);
363                 });
364
365                 // Alerts 'boom'
366                 $.deck('alert', 'boom');
367                 */
368                 extend: function(name, method) {
369                         methods[name] = method;
370                 }
371         };
372         
373         /* jQuery extension */
374         $[deck] = function(method, arg) {
375                 if (methods[method]) {
376                         return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
377                 }
378                 else {
379                         return methods.init(method, arg);
380                 }
381         };
382         
383         /*
384         The default settings object for a deck. All deck extensions should extend
385         this object to add defaults for any of their options.
386         
387         options.classes.after
388                 This class is added to all slides that appear after the 'next' slide.
389         
390         options.classes.before
391                 This class is added to all slides that appear before the 'previous'
392                 slide.
393                 
394         options.classes.childCurrent
395                 This class is added to all elements in the DOM tree between the
396                 'current' slide and the deck container. For standard slides, this is
397                 mostly seen and used for nested slides.
398                 
399         options.classes.current
400                 This class is added to the current slide.
401                 
402         options.classes.loading
403                 This class is applied to the deck container during loading phases and is
404                 primarily used as a way to short circuit transitions between states
405                 where such transitions are distracting or unwanted.  For example, this
406                 class is applied during deck initialization and then removed to prevent
407                 all the slides from appearing stacked and transitioning into place
408                 on load.
409                 
410         options.classes.next
411                 This class is added to the slide immediately following the 'current'
412                 slide.
413                 
414         options.classes.onPrefix
415                 This prefix, concatenated with the current slide index, is added to the
416                 deck container as you change slides.
417                 
418         options.classes.previous
419                 This class is added to the slide immediately preceding the 'current'
420                 slide.
421                 
422         options.selectors.container
423                 Elements matched by this CSS selector will be considered the deck
424                 container. The deck container is used to scope certain states of the
425                 deck, as with the onPrefix option, or with extensions such as deck.goto
426                 and deck.menu.
427                 
428         options.keys.next
429                 The numeric keycode used to go to the next slide.
430                 
431         options.keys.previous
432                 The numeric keycode used to go to the previous slide.
433                 
434         options.touch.swipeTolerance
435                 The number of pixels the users finger must travel to produce a swipe
436                 gesture.
437         */
438         $[deck].defaults = {
439                 classes: {
440                         after: 'deck-after',
441                         before: 'deck-before',
442                         childCurrent: 'deck-child-current',
443                         current: 'deck-current',
444                         loading: 'deck-loading',
445                         next: 'deck-next',
446                         onPrefix: 'on-slide-',
447                         previous: 'deck-previous'
448                 },
449                 
450                 selectors: {
451                         container: '.deck-container'
452                 },
453                 
454                 keys: {
455                         // enter, space, page down, right arrow, down arrow,
456                         next: [13, 32, 34, 39, 40],
457                         // backspace, page up, left arrow, up arrow
458                         previous: [8, 33, 37, 38]
459                 },
460                 
461                 touch: {
462                         swipeTolerance: 60
463                 }
464         };
465         
466         $d.ready(function() {
467                 $('html').addClass('ready');
468         });
469         
470         /*
471         FF + Transforms + Flash video don't get along...
472         Firefox will reload and start playing certain videos after a
473         transform.  Blanking the src when a previously shown slide goes out
474         of view prevents this.
475         */
476         $d.bind('deck.change', function(e, from, to) {
477                 var oldFrames = $[deck]('getSlide', from).find('iframe'),
478                 newFrames = $[deck]('getSlide', to).find('iframe');
479                 
480                 oldFrames.each(function() {
481                 var $this = $(this),
482                 curSrc = $this.attr('src');
483             
484             if(curSrc) {
485                 $this.data('deck-src', curSrc).attr('src', '');
486             }
487                 });
488                 
489                 newFrames.each(function() {
490                         var $this = $(this),
491                         originalSrc = $this.data('deck-src');
492                         
493                         if (originalSrc) {
494                                 $this.attr('src', originalSrc);
495                         }
496                 });
497         });
498 })(jQuery, 'deck', document);