79437aed511d15c4eddd85b07004b395c72df84b
[reveal.js.git] / js / reveal.js
1 /**
2  * Copyright (C) 2011 Hakim El Hattab, http://hakim.se
3  * 
4  * Permission is hereby granted, free of charge, to any person obtaining a copy
5  * of this software and associated documentation files (the "Software"), to deal
6  * in the Software without restriction, including without limitation the rights
7  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8  * copies of the Software, and to permit persons to whom the Software is
9  * furnished to do so, subject to the following conditions:
10  * 
11  * The above copyright notice and this permission notice shall be included in
12  * all copies or substantial portions of the Software.
13  * 
14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20  * THE SOFTWARE.
21  *
22  * 
23  * #############################################################################
24  * 
25  *
26  * Reveal.js is an easy to use HTML based slideshow enhanced by 
27  * sexy CSS 3D transforms.
28  * 
29  * Slides are given unique hash based URL's so that they can be 
30  * opened directly.
31  * 
32  * Public facing methods:
33  * - Reveal.initialize( { ... options ... } );
34  * - Reveal.navigateTo( indexh, indexv );
35  * - Reveal.navigateLeft();
36  * - Reveal.navigateRight();
37  * - Reveal.navigateUp();
38  * - Reveal.navigateDown();
39  * 
40  * 
41  * version 0.1:
42  * - First release
43  * 
44  * version 0.2:         
45  * - Refactored code and added inline documentation
46  * - Slides now have unique URL's
47  * - A basic API to invoke navigation was added
48  * 
49  * version 0.3:         
50  * - Added licensing terms
51  * - Fixed broken links on touch devices
52  * 
53  * version 1.0:
54  * - Added controls
55  * - Added initialization options
56  * - Reveal views in fragments
57  * - Revamped, darker, theme
58  * - Tweaked markup styles (a, em, strong, b, i, blockquote, q, pre, ul, ol)
59  * - Support for themes at initialization (default/linear/concave)
60  * - Code highlighting via highlight.js
61  * 
62  * version 1.1:
63  * - Optional progress bar UI element
64  * 
65  * TODO:
66  * - Touch/swipe interactions
67  * - Presentation overview via keyboard shortcut
68  *      
69  * @author Hakim El Hattab | http://hakim.se
70  * @version 1.1
71  */
72 var Reveal = (function(){
73         
74         var HORIZONTAL_SLIDES_SELECTOR = '#main>section',
75                 VERTICAL_SLIDES_SELECTOR = 'section.present>section',
76
77                 // The horizontal and verical index of the currently active slide
78                 indexh = 0,
79                 indexv = 0,
80
81                 // Configurations options, including;
82                 // > {Boolean} controls
83                 // > {Boolean} progress
84                 // > {String} theme
85                 // > {Boolean} rollingLinks
86                 config = {},
87
88                 // Cached references to DOM elements
89                 dom = {};
90         
91         /**
92          * Starts up the slideshow by applying configuration
93          * options and binding various events.
94          */
95         function initialize( options ) {
96                 // Cache references to DOM elements
97                 dom.progress = document.querySelector( 'body>.progress' );
98                 dom.progressbar = document.querySelector( 'body>.progress span' );
99                 dom.controls = document.querySelector( '.controls' );
100                 dom.controlsLeft = document.querySelector( '.controls .left' );
101                 dom.controlsRight = document.querySelector( '.controls .right' );
102                 dom.controlsUp = document.querySelector( '.controls .up' );
103                 dom.controlsDown = document.querySelector( '.controls .down' );
104
105                 // Bind all view events
106                 document.addEventListener('keydown', onDocumentKeyDown, false);
107                 document.addEventListener('touchstart', onDocumentTouchStart, false);
108                 window.addEventListener('hashchange', onWindowHashChange, false);
109                 dom.controlsLeft.addEventListener('click', preventAndForward( navigateLeft ), false);
110                 dom.controlsRight.addEventListener('click', preventAndForward( navigateRight ), false);
111                 dom.controlsUp.addEventListener('click', preventAndForward( navigateUp ), false);
112                 dom.controlsDown.addEventListener('click', preventAndForward( navigateDown ), false);
113
114                 // Fall back on default options
115                 config.rollingLinks = options.rollingLinks === undefined ? true : options.rollingLinks;
116                 config.controls = options.controls === undefined ? false : options.controls;
117                 config.progress = options.progress === undefined ? false : options.progress;
118                 config.theme = options.theme === undefined ? 'default' : options.theme;
119
120                 if( config.controls ) {
121                         dom.controls.style.display = 'block';
122                 }
123
124                 if( config.progress ) {
125                         dom.progress.style.display = 'block';
126                 }
127
128                 if( config.theme !== 'default' ) {
129                         document.body.classList.add( config.theme );
130                 }
131
132                 if( config.rollingLinks ) {
133                         // Add some 3D magic to our anchors
134                         linkify();
135                 }
136
137                 // Read the initial hash
138                 readURL();
139         }
140
141         /**
142          * Prevents an events defaults behavior calls the 
143          * specified delegate.
144          * 
145          * @param {Function} delegate The method to call 
146          * after the wrapper has been executed
147          */
148         function preventAndForward( delegate ) {
149                 return function( event ) {
150                         event.preventDefault();
151                         delegate.call();
152                 }
153         }
154         
155         /**
156          * Handler for the document level 'keydown' event.
157          * 
158          * @param {Object} event
159          */
160         function onDocumentKeyDown( event ) {
161                 
162                 // FFT: Use document.querySelector( ':focus' ) === null 
163                 // instead of checking contentEditable?
164
165                 if( event.keyCode >= 37 && event.keyCode <= 40 && event.target.contentEditable === 'inherit' ) {
166                         
167                         switch( event.keyCode ) {
168                                 case 37: navigateLeft(); break; // left
169                                 case 39: navigateRight(); break; // right
170                                 case 38: navigateUp(); break; // up
171                                 case 40: navigateDown(); break; // down
172                         }
173                         
174                         slide();
175                         
176                         event.preventDefault();
177                         
178                 }
179         }
180         
181         /**
182          * Handler for the document level 'touchstart' event.
183          * 
184          * This enables very basic tap interaction for touch
185          * devices. Added mainly for performance testing of 3D
186          * transforms on iOS but was so happily surprised with
187          * how smoothly it runs so I left it in here. Apple +1
188          * 
189          * @param {Object} event
190          */
191         function onDocumentTouchStart( event ) {
192                 // We're only interested in one point taps
193                 if (event.touches.length === 1) {
194                         // Never prevent taps on anchors and images
195                         if( event.target.tagName.toLowerCase() === 'a' || event.target.tagName.toLowerCase() === 'img' ) {
196                                 return;
197                         }
198                         
199                         event.preventDefault();
200                         
201                         var point = {
202                                 x: event.touches[0].clientX,
203                                 y: event.touches[0].clientY
204                         };
205                         
206                         // Define the extent of the areas that may be tapped
207                         // to navigate
208                         var wt = window.innerWidth * 0.3;
209                         var ht = window.innerHeight * 0.3;
210                         
211                         if( point.x < wt ) {
212                                 navigateLeft();
213                         }
214                         else if( point.x > window.innerWidth - wt ) {
215                                 navigateRight();
216                         }
217                         else if( point.y < ht ) {
218                                 navigateUp();
219                         }
220                         else if( point.y > window.innerHeight - ht ) {
221                                 navigateDown();
222                         }
223                         
224                         slide();
225                         
226                 }
227         }
228         
229         
230         /**
231          * Handler for the window level 'hashchange' event.
232          * 
233          * @param {Object} event
234          */
235         function onWindowHashChange( event ) {
236                 readURL();
237         }
238
239         /**
240          * Wrap all links in 3D goodness.
241          */
242         function linkify() {
243                 var supports3DTransforms =  document.body.style['webkitPerspective'] !== undefined || 
244                                         document.body.style['MozPerspective'] !== undefined ||
245                                         document.body.style['perspective'] !== undefined;
246
247         if( supports3DTransforms ) {
248                 var nodes = document.querySelectorAll( 'section a:not(.image)' );
249
250                 for( var i = 0, len = nodes.length; i < len; i++ ) {
251                     var node = nodes[i];
252                     
253                     if( node.textContent && ( !node.className || !node.className.match( /roll/g ) ) ) {
254                         node.className += ' roll';
255                         node.innerHTML = '<span data-title="'+ node.text +'">' + node.innerHTML + '</span>';
256                     }
257                 };
258         }
259         }
260         
261         /**
262          * Updates one dimension of slides by showing the slide
263          * with the specified index.
264          * 
265          * @param {String} selector A CSS selector that will fetch
266          * the group of slides we are working with
267          * @param {Number} index The index of the slide that should be
268          * shown
269          * 
270          * @return {Number} The index of the slide that is now shown,
271          * might differ from the passed in index if it was out of 
272          * bounds.
273          */
274         function updateSlides( selector, index ) {
275                 
276                 // Select all slides and convert the NodeList result to
277                 // an array
278                 var slides = Array.prototype.slice.call( document.querySelectorAll( selector ) );
279                 
280                 if( slides.length ) {
281                         // Enforce max and minimum index bounds
282                         index = Math.max(Math.min(index, slides.length - 1), 0);
283                         
284                         slides[index].setAttribute('class', 'present');
285                         
286                         // Any element previous to index is given the 'past' class
287                         slides.slice(0, index).map(function(element){
288                                 element.setAttribute('class', 'past');
289                         });
290                         
291                         // Any element subsequent to index is given the 'future' class
292                         slides.slice(index + 1).map(function(element){
293                                 element.setAttribute('class', 'future');
294                         });
295                 }
296                 else {
297                         // Since there are no slides we can't be anywhere beyond the 
298                         // zeroth index
299                         index = 0;
300                 }
301                 
302                 return index;
303                 
304         }
305         
306         /**
307          * Updates the visual slides to represent the currently
308          * set indices. 
309          */
310         function slide() {
311                 indexh = updateSlides( HORIZONTAL_SLIDES_SELECTOR, indexh );
312                 indexv = updateSlides( VERTICAL_SLIDES_SELECTOR, indexv );
313
314                 // Update progress if enabled
315                 if( config.progress ) {
316                         dom.progressbar.style.width = ( indexh / ( document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR ).length - 1 ) ) * window.innerWidth + 'px';
317                 }
318
319                 updateControls();
320                 
321                 writeURL();
322         }
323
324         /**
325          * Updates the state and link pointers of the controls.
326          */
327         function updateControls() {
328                 var routes = availableRoutes();
329
330                 // Remove the 'enabled' class from all directions
331                 [ dom.controlsLeft, dom.controlsRight, dom.controlsUp, dom.controlsDown ].forEach( function( node ) {
332                         node.classList.remove( 'enabled' );
333                 } )
334
335                 if( routes.left ) dom.controlsLeft.classList.add( 'enabled' );
336                 if( routes.right ) dom.controlsRight.classList.add( 'enabled' );
337                 if( routes.up ) dom.controlsUp.classList.add( 'enabled' );
338                 if( routes.down ) dom.controlsDown.classList.add( 'enabled' );
339         }
340
341         /**
342          * Determine what available routes there are for navigation.
343          * 
344          * @return {Object} containing four booleans: left/right/up/down
345          */
346         function availableRoutes() {
347                 var horizontalSlides = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR );
348                 var verticalSlides = document.querySelectorAll( VERTICAL_SLIDES_SELECTOR );
349
350                 return {
351                         left: indexh > 0,
352                         right: indexh < horizontalSlides.length - 1,
353                         up: indexv > 0,
354                         down: indexv < verticalSlides.length - 1
355                 };
356         }
357         
358         /**
359          * Reads the current URL (hash) and navigates accordingly.
360          */
361         function readURL() {
362                 // Break the hash down to separate components
363                 var bits = window.location.hash.slice(2).split('/');
364                 
365                 // Read the index components of the hash
366                 indexh = bits[0] ? parseInt( bits[0] ) : 0;
367                 indexv = bits[1] ? parseInt( bits[1] ) : 0;
368                 
369                 navigateTo( indexh, indexv );
370         }
371         
372         /**
373          * Updates the page URL (hash) to reflect the current
374          * state. 
375          */
376         function writeURL() {
377                 var url = '/';
378                 
379                 // Only include the minimum possible number of components in
380                 // the URL
381                 if( indexh > 0 || indexv > 0 ) url += indexh;
382                 if( indexv > 0 ) url += '/' + indexv;
383                 
384                 window.location.hash = url;
385         }
386
387         /**
388          * Navigate to the next slide fragment.
389          * 
390          * @return {Boolean} true if there was a next fragment,
391          * false otherwise
392          */
393         function nextFragment() {
394                 // Vertical slides:
395                 if( document.querySelector( VERTICAL_SLIDES_SELECTOR + '.present' ) ) {
396                         var verticalFragments = document.querySelectorAll( VERTICAL_SLIDES_SELECTOR + '.present .fragment:not(.visible)' );
397                         if( verticalFragments.length ) {
398                                 verticalFragments[0].classList.add( 'visible' );
399                                 return true;
400                         }
401                 }
402                 // Horizontal slides:
403                 else {
404                         var horizontalFragments = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.present .fragment:not(.visible)' );
405                         if( horizontalFragments.length ) {
406                                 horizontalFragments[0].classList.add( 'visible' );
407                                 return true;
408                         }
409                 }
410
411                 return false;
412         }
413
414         /**
415          * Navigate to the previous slide fragment.
416          * 
417          * @return {Boolean} true if there was a previous fragment,
418          * false otherwise
419          */
420         function previousFragment() {
421                 // Vertical slides:
422                 if( document.querySelector( VERTICAL_SLIDES_SELECTOR + '.present' ) ) {
423                         var verticalFragments = document.querySelectorAll( VERTICAL_SLIDES_SELECTOR + '.present .fragment.visible' );
424                         if( verticalFragments.length ) {
425                                 verticalFragments[ verticalFragments.length - 1 ].classList.remove( 'visible' );
426                                 return true;
427                         }
428                 }
429                 // Horizontal slides:
430                 else {
431                         var horizontalFragments = document.querySelectorAll( HORIZONTAL_SLIDES_SELECTOR + '.present .fragment.visible' );
432                         if( horizontalFragments.length ) {
433                                 horizontalFragments[ horizontalFragments.length - 1 ].classList.remove( 'visible' );
434                                 return true;
435                         }
436                 }
437                 
438                 return false;
439         }
440         
441         /**
442          * Triggers a navigation to the specified indices.
443          * 
444          * @param {Number} h The horizontal index of the slide to show
445          * @param {Number} v The vertical index of the slide to show
446          */
447         function navigateTo( h, v ) {
448                 indexh = h === undefined ? indexh : h;
449                 indexv = v === undefined ? indexv : v;
450                 
451                 slide();
452         }
453         
454         function navigateLeft() {
455                 // Prioritize hiding fragments
456                 if( previousFragment() === false ) {
457                         indexh --;
458                         indexv = 0;
459                         slide();
460                 }
461         }
462         function navigateRight() {
463                 // Prioritize revealing fragments
464                 if( nextFragment() === false ) {
465                         indexh ++;
466                         indexv = 0;
467                         slide();
468                 }
469         }
470         function navigateUp() {
471                 // Prioritize hiding fragments
472                 if( previousFragment() === false ) {
473                         indexv --;
474                         slide();
475                 }
476         }
477         function navigateDown() {
478                 // Prioritize revealing fragments
479                 if( nextFragment() === false ) {
480                         indexv ++;
481                         slide();
482                 }
483         }
484         
485         // Expose some methods publicly
486         return {
487                 initialize: initialize,
488                 navigateTo: navigateTo,
489                 navigateLeft: navigateLeft,
490                 navigateRight: navigateRight,
491                 navigateUp: navigateUp,
492                 navigateDown: navigateDown
493         };
494         
495 })();
496