comparison seminar/io2012slides/js/slide-deck.js @ 3:fea44bb81877 default tip

many change
author taiki
date Tue, 10 Dec 2013 15:31:33 +0900
parents
children
comparison
equal deleted inserted replaced
2:7f4128b2369a 3:fea44bb81877
1 /**
2 * @authors Luke Mahe
3 * @authors Eric Bidelman
4 * @fileoverview TODO
5 */
6 document.cancelFullScreen = document.webkitCancelFullScreen ||
7 document.mozCancelFullScreen;
8
9 /**
10 * @constructor
11 */
12 function SlideDeck(el) {
13 this.curSlide_ = 0;
14 this.prevSlide_ = 0;
15 this.config_ = null;
16 this.container = el || document.querySelector('slides');
17 this.slides = [];
18 this.controller = null;
19
20 this.getCurrentSlideFromHash_();
21
22 // Call this explicitly. Modernizr.load won't be done until after DOM load.
23 this.onDomLoaded_.bind(this)();
24 }
25
26 /**
27 * @const
28 * @private
29 */
30 SlideDeck.prototype.SLIDE_CLASSES_ = [
31 'far-past', 'past', 'current', 'next', 'far-next'];
32
33 /**
34 * @const
35 * @private
36 */
37 SlideDeck.prototype.CSS_DIR_ = 'theme/css/';
38
39 /**
40 * @private
41 */
42 SlideDeck.prototype.getCurrentSlideFromHash_ = function() {
43 var slideNo = parseInt(document.location.hash.substr(1));
44
45 if (slideNo) {
46 this.curSlide_ = slideNo - 1;
47 } else {
48 this.curSlide_ = 0;
49 }
50 };
51
52 /**
53 * @param {number} slideNo
54 */
55 SlideDeck.prototype.loadSlide = function(slideNo) {
56 if (slideNo) {
57 this.curSlide_ = slideNo - 1;
58 this.updateSlides_();
59 }
60 };
61
62 /**
63 * @private
64 */
65 SlideDeck.prototype.onDomLoaded_ = function(e) {
66 document.body.classList.add('loaded'); // Add loaded class for templates to use.
67
68 this.slides = this.container.querySelectorAll('slide:not([hidden]):not(.backdrop)');
69
70 // If we're on a smartphone, apply special sauce.
71 if (Modernizr.mq('only screen and (max-device-width: 480px)')) {
72 // var style = document.createElement('link');
73 // style.rel = 'stylesheet';
74 // style.type = 'text/css';
75 // style.href = this.CSS_DIR_ + 'phone.css';
76 // document.querySelector('head').appendChild(style);
77
78 // No need for widescreen layout on a phone.
79 this.container.classList.remove('layout-widescreen');
80 }
81
82 this.loadConfig_(SLIDE_CONFIG);
83 this.addEventListeners_();
84 this.updateSlides_();
85
86 // Add slide numbers and total slide count metadata to each slide.
87 var that = this;
88 for (var i = 0, slide; slide = this.slides[i]; ++i) {
89 slide.dataset.slideNum = i + 1;
90 slide.dataset.totalSlides = this.slides.length;
91
92 slide.addEventListener('click', function(e) {
93 if (document.body.classList.contains('overview')) {
94 that.loadSlide(this.dataset.slideNum);
95 e.preventDefault();
96 window.setTimeout(function() {
97 that.toggleOverview();
98 }, 500);
99 }
100 }, false);
101 }
102
103 // Note: this needs to come after addEventListeners_(), which adds a
104 // 'keydown' listener that this controller relies on.
105 // Also, no need to set this up if we're on mobile.
106 if (!Modernizr.touch) {
107 this.controller = new SlideController(this);
108 if (this.controller.isPopup) {
109 document.body.classList.add('popup');
110 }
111 }
112 };
113
114 /**
115 * @private
116 */
117 SlideDeck.prototype.addEventListeners_ = function() {
118 document.addEventListener('keydown', this.onBodyKeyDown_.bind(this), false);
119 window.addEventListener('popstate', this.onPopState_.bind(this), false);
120
121 // var transEndEventNames = {
122 // 'WebkitTransition': 'webkitTransitionEnd',
123 // 'MozTransition': 'transitionend',
124 // 'OTransition': 'oTransitionEnd',
125 // 'msTransition': 'MSTransitionEnd',
126 // 'transition': 'transitionend'
127 // };
128 //
129 // // Find the correct transitionEnd vendor prefix.
130 // window.transEndEventName = transEndEventNames[
131 // Modernizr.prefixed('transition')];
132 //
133 // // When slides are done transitioning, kickoff loading iframes.
134 // // Note: we're only looking at a single transition (on the slide). This
135 // // doesn't include autobuilds the slides may have. Also, if the slide
136 // // transitions on multiple properties (e.g. not just 'all'), this doesn't
137 // // handle that case.
138 // this.container.addEventListener(transEndEventName, function(e) {
139 // this.enableSlideFrames_(this.curSlide_);
140 // }.bind(this), false);
141
142 // document.addEventListener('slideenter', function(e) {
143 // var slide = e.target;
144 // window.setTimeout(function() {
145 // this.enableSlideFrames_(e.slideNumber);
146 // this.enableSlideFrames_(e.slideNumber + 1);
147 // }.bind(this), 300);
148 // }.bind(this), false);
149 };
150
151 /**
152 * @private
153 * @param {Event} e The pop event.
154 */
155 SlideDeck.prototype.onPopState_ = function(e) {
156 if (e.state != null) {
157 this.curSlide_ = e.state;
158 this.updateSlides_(true);
159 }
160 };
161
162 /**
163 * @param {Event} e
164 */
165 SlideDeck.prototype.onBodyKeyDown_ = function(e) {
166 if (/^(input|textarea)$/i.test(e.target.nodeName) ||
167 e.target.isContentEditable) {
168 return;
169 }
170
171 // Forward keydowns to the main slides if we're the popup.
172 if (this.controller && this.controller.isPopup) {
173 this.controller.sendMsg({keyCode: e.keyCode});
174 }
175
176 switch (e.keyCode) {
177 case 13: // Enter
178 if (document.body.classList.contains('overview')) {
179 this.toggleOverview();
180 }
181 break;
182
183 case 39: // right arrow
184 case 32: // space
185 case 34: // PgDn
186 this.nextSlide();
187 e.preventDefault();
188 break;
189
190 case 37: // left arrow
191 case 8: // Backspace
192 case 33: // PgUp
193 this.prevSlide();
194 e.preventDefault();
195 break;
196
197 case 40: // down arrow
198 this.nextSlide();
199 e.preventDefault();
200 break;
201
202 case 38: // up arrow
203 this.prevSlide();
204 e.preventDefault();
205 break;
206
207 case 72: // H: Toggle code highlighting
208 document.body.classList.toggle('highlight-code');
209 break;
210
211 case 79: // O: Toggle overview
212 this.toggleOverview();
213 break;
214
215 case 80: // P
216 if (this.controller && this.controller.isPopup) {
217 document.body.classList.toggle('with-notes');
218 } else if (this.controller && !this.controller.popup) {
219 document.body.classList.toggle('with-notes');
220 }
221 break;
222
223 case 82: // R
224 // TODO: implement refresh on main slides when popup is refreshed.
225 break;
226
227 case 27: // ESC: Hide notes and highlighting
228 document.body.classList.remove('with-notes');
229 document.body.classList.remove('highlight-code');
230
231 if (document.body.classList.contains('overview')) {
232 this.toggleOverview();
233 }
234 break;
235
236 case 70: // F: Toggle fullscreen
237 // Only respect 'f' on body. Don't want to capture keys from an <input>.
238 // Also, ignore browser's fullscreen shortcut (cmd+shift+f) so we don't
239 // get trapped in fullscreen!
240 if (e.target == document.body && !(e.shiftKey && e.metaKey)) {
241 if (document.mozFullScreen !== undefined && !document.mozFullScreen) {
242 document.body.mozRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
243 } else if (document.webkitIsFullScreen !== undefined && !document.webkitIsFullScreen) {
244 document.body.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
245 } else {
246 document.cancelFullScreen();
247 }
248 }
249 break;
250
251 case 87: // W: Toggle widescreen
252 // Only respect 'w' on body. Don't want to capture keys from an <input>.
253 if (e.target == document.body && !(e.shiftKey && e.metaKey)) {
254 this.container.classList.toggle('layout-widescreen');
255 }
256 break;
257 }
258 };
259
260 /**
261 *
262 */
263 SlideDeck.prototype.focusOverview_ = function() {
264 var overview = document.body.classList.contains('overview');
265
266 for (var i = 0, slide; slide = this.slides[i]; i++) {
267 slide.style[Modernizr.prefixed('transform')] = overview ?
268 'translateZ(-2500px) translate(' + (( i - this.curSlide_ ) * 105) +
269 '%, 0%)' : '';
270 }
271 };
272
273 /**
274 */
275 SlideDeck.prototype.toggleOverview = function() {
276 document.body.classList.toggle('overview');
277
278 this.focusOverview_();
279 };
280
281 /**
282 * @private
283 */
284 SlideDeck.prototype.loadConfig_ = function(config) {
285 if (!config) {
286 return;
287 }
288
289 this.config_ = config;
290
291 var settings = this.config_.settings;
292
293 this.loadTheme_(settings.theme || []);
294
295 if (settings.favIcon) {
296 this.addFavIcon_(settings.favIcon);
297 }
298
299 // Prettyprint. Default to on.
300 if (!!!('usePrettify' in settings) || settings.usePrettify) {
301 prettyPrint();
302 }
303
304 if (settings.analytics) {
305 this.loadAnalytics_();
306 }
307
308 if (settings.fonts) {
309 this.addFonts_(settings.fonts);
310 }
311
312 // Builds. Default to on.
313 if (!!!('useBuilds' in settings) || settings.useBuilds) {
314 this.makeBuildLists_();
315 }
316
317 if (settings.title) {
318 document.title = settings.title.replace(/<br\/?>/, ' ') + ' - Google IO 2012';
319 document.querySelector('[data-config-title]').innerHTML = settings.title;
320 }
321
322 if (settings.subtitle) {
323 document.querySelector('[data-config-subtitle]').innerHTML = settings.subtitle;
324 }
325
326 if (this.config_.presenters) {
327 var presenters = this.config_.presenters;
328 var dataConfigContact = document.querySelector('[data-config-contact]');
329
330 var html = [];
331 if (presenters.length == 1) {
332 var p = presenters[0];
333
334 html = [p.name, p.company].join('<br>');
335
336 var gplus = p.gplus ? '<span>g+</span><a href="' + p.gplus +
337 '">' + p.gplus.replace(/https?:\/\//, '') + '</a>' : '';
338
339 var twitter = p.twitter ? '<span>twitter</span>' +
340 '<a href="http://twitter.com/' + p.twitter + '">' +
341 p.twitter + '</a>' : '';
342
343 var www = p.www ? '<span>www</span><a href="' + p.www +
344 '">' + p.www.replace(/https?:\/\//, '') + '</a>' : '';
345
346 var github = p.github ? '<span>github</span><a href="' + p.github +
347 '">' + p.github.replace(/https?:\/\//, '') + '</a>' : '';
348
349 var html2 = [gplus, twitter, www, github].join('<br>');
350
351 if (dataConfigContact) {
352 dataConfigContact.innerHTML = html2;
353 }
354 } else {
355 for (var i = 0, p; p = presenters[i]; ++i) {
356 html.push(p.name + ' - ' + p.company);
357 }
358 html = html.join('<br>');
359 if (dataConfigContact) {
360 dataConfigContact.innerHTML = html;
361 }
362 }
363
364 var dataConfigPresenter = document.querySelector('[data-config-presenter]');
365 if (dataConfigPresenter) {
366 document.querySelector('[data-config-presenter]').innerHTML = html;
367 }
368 }
369
370 /* Left/Right tap areas. Default to including. */
371 if (!!!('enableSlideAreas' in settings) || settings.enableSlideAreas) {
372 var el = document.createElement('div');
373 el.classList.add('slide-area');
374 el.id = 'prev-slide-area';
375 el.addEventListener('click', this.prevSlide.bind(this), false);
376 this.container.appendChild(el);
377
378 var el = document.createElement('div');
379 el.classList.add('slide-area');
380 el.id = 'next-slide-area';
381 el.addEventListener('click', this.nextSlide.bind(this), false);
382 this.container.appendChild(el);
383 }
384
385 if (Modernizr.touch && (!!!('enableTouch' in settings) ||
386 settings.enableTouch)) {
387 var self = this;
388
389 // Note: this prevents mobile zoom in/out but prevents iOS from doing
390 // it's crazy scroll over effect and disaligning the slides.
391 window.addEventListener('touchstart', function(e) {
392 e.preventDefault();
393 }, false);
394
395 var hammer = new Hammer(this.container);
396 hammer.ondragend = function(e) {
397 if (e.direction == 'right' || e.direction == 'down') {
398 self.prevSlide();
399 } else if (e.direction == 'left' || e.direction == 'up') {
400 self.nextSlide();
401 }
402 };
403 }
404 };
405
406 /**
407 * @private
408 * @param {Array.<string>} fonts
409 */
410 SlideDeck.prototype.addFonts_ = function(fonts) {
411 var el = document.createElement('link');
412 el.rel = 'stylesheet';
413 el.href = ('https:' == document.location.protocol ? 'https' : 'http') +
414 '://fonts.googleapis.com/css?family=' + fonts.join('|') + '&v2';
415 document.querySelector('head').appendChild(el);
416 };
417
418 /**
419 * @private
420 */
421 SlideDeck.prototype.buildNextItem_ = function() {
422 var slide = this.slides[this.curSlide_];
423 var toBuild = slide.querySelector('.to-build');
424 var built = slide.querySelector('.build-current');
425
426 if (built) {
427 built.classList.remove('build-current');
428 if (built.classList.contains('fade')) {
429 built.classList.add('build-fade');
430 }
431 }
432
433 if (!toBuild) {
434 var items = slide.querySelectorAll('.build-fade');
435 for (var j = 0, item; item = items[j]; j++) {
436 item.classList.remove('build-fade');
437 }
438 return false;
439 }
440
441 toBuild.classList.remove('to-build');
442 toBuild.classList.add('build-current');
443
444 return true;
445 };
446
447 /**
448 * @param {boolean=} opt_dontPush
449 */
450 SlideDeck.prototype.prevSlide = function(opt_dontPush) {
451 if (this.curSlide_ > 0) {
452 var bodyClassList = document.body.classList;
453 bodyClassList.remove('highlight-code');
454
455 // Toggle off speaker notes if they're showing when we move backwards on the
456 // main slides. If we're the speaker notes popup, leave them up.
457 if (this.controller && !this.controller.isPopup) {
458 bodyClassList.remove('with-notes');
459 } else if (!this.controller) {
460 bodyClassList.remove('with-notes');
461 }
462
463 this.prevSlide_ = this.curSlide_--;
464
465 this.updateSlides_(opt_dontPush);
466 }
467 };
468
469 /**
470 * @param {boolean=} opt_dontPush
471 */
472 SlideDeck.prototype.nextSlide = function(opt_dontPush) {
473 if (!document.body.classList.contains('overview') && this.buildNextItem_()) {
474 return;
475 }
476
477 if (this.curSlide_ < this.slides.length - 1) {
478 var bodyClassList = document.body.classList;
479 bodyClassList.remove('highlight-code');
480
481 // Toggle off speaker notes if they're showing when we advanced on the main
482 // slides. If we're the speaker notes popup, leave them up.
483 if (this.controller && !this.controller.isPopup) {
484 bodyClassList.remove('with-notes');
485 } else if (!this.controller) {
486 bodyClassList.remove('with-notes');
487 }
488
489 this.prevSlide_ = this.curSlide_++;
490
491 this.updateSlides_(opt_dontPush);
492 }
493 };
494
495 /* Slide events */
496
497 /**
498 * Triggered when a slide enter/leave event should be dispatched.
499 *
500 * @param {string} type The type of event to trigger
501 * (e.g. 'slideenter', 'slideleave').
502 * @param {number} slideNo The index of the slide that is being left.
503 */
504 SlideDeck.prototype.triggerSlideEvent = function(type, slideNo) {
505 var el = this.getSlideEl_(slideNo);
506 if (!el) {
507 return;
508 }
509
510 // Call onslideenter/onslideleave if the attribute is defined on this slide.
511 var func = el.getAttribute(type);
512 if (func) {
513 new Function(func).call(el); // TODO: Don't use new Function() :(
514 }
515
516 // Dispatch event to listeners setup using addEventListener.
517 var evt = document.createEvent('Event');
518 evt.initEvent(type, true, true);
519 evt.slideNumber = slideNo + 1; // Make it readable
520 evt.slide = el;
521
522 el.dispatchEvent(evt);
523 };
524
525 /**
526 * @private
527 */
528 SlideDeck.prototype.updateSlides_ = function(opt_dontPush) {
529 var dontPush = opt_dontPush || false;
530
531 var curSlide = this.curSlide_;
532 for (var i = 0; i < this.slides.length; ++i) {
533 switch (i) {
534 case curSlide - 2:
535 this.updateSlideClass_(i, 'far-past');
536 break;
537 case curSlide - 1:
538 this.updateSlideClass_(i, 'past');
539 break;
540 case curSlide:
541 this.updateSlideClass_(i, 'current');
542 break;
543 case curSlide + 1:
544 this.updateSlideClass_(i, 'next');
545 break;
546 case curSlide + 2:
547 this.updateSlideClass_(i, 'far-next');
548 break;
549 default:
550 this.updateSlideClass_(i);
551 break;
552 }
553 };
554
555 this.triggerSlideEvent('slideleave', this.prevSlide_);
556 this.triggerSlideEvent('slideenter', curSlide);
557
558 // window.setTimeout(this.disableSlideFrames_.bind(this, curSlide - 2), 301);
559 //
560 // this.enableSlideFrames_(curSlide - 1); // Previous slide.
561 // this.enableSlideFrames_(curSlide + 1); // Current slide.
562 // this.enableSlideFrames_(curSlide + 2); // Next slide.
563
564 // Enable current slide's iframes (needed for page loat at current slide).
565 this.enableSlideFrames_(curSlide + 1);
566
567 // No way to tell when all slide transitions + auto builds are done.
568 // Give ourselves a good buffer to preload the next slide's iframes.
569 window.setTimeout(this.enableSlideFrames_.bind(this, curSlide + 2), 1000);
570
571 this.updateHash_(dontPush);
572
573 if (document.body.classList.contains('overview')) {
574 this.focusOverview_();
575 return;
576 }
577
578 };
579
580 /**
581 * @private
582 * @param {number} slideNo
583 */
584 SlideDeck.prototype.enableSlideFrames_ = function(slideNo) {
585 var el = this.slides[slideNo - 1];
586 if (!el) {
587 return;
588 }
589
590 var frames = el.querySelectorAll('iframe');
591 for (var i = 0, frame; frame = frames[i]; i++) {
592 this.enableFrame_(frame);
593 }
594 };
595
596 /**
597 * @private
598 * @param {number} slideNo
599 */
600 SlideDeck.prototype.enableFrame_ = function(frame) {
601 var src = frame.dataset.src;
602 if (src && frame.src != src) {
603 frame.src = src;
604 }
605 };
606
607 /**
608 * @private
609 * @param {number} slideNo
610 */
611 SlideDeck.prototype.disableSlideFrames_ = function(slideNo) {
612 var el = this.slides[slideNo - 1];
613 if (!el) {
614 return;
615 }
616
617 var frames = el.querySelectorAll('iframe');
618 for (var i = 0, frame; frame = frames[i]; i++) {
619 this.disableFrame_(frame);
620 }
621 };
622
623 /**
624 * @private
625 * @param {Node} frame
626 */
627 SlideDeck.prototype.disableFrame_ = function(frame) {
628 frame.src = 'about:blank';
629 };
630
631 /**
632 * @private
633 * @param {number} slideNo
634 */
635 SlideDeck.prototype.getSlideEl_ = function(no) {
636 if ((no < 0) || (no >= this.slides.length)) {
637 return null;
638 } else {
639 return this.slides[no];
640 }
641 };
642
643 /**
644 * @private
645 * @param {number} slideNo
646 * @param {string} className
647 */
648 SlideDeck.prototype.updateSlideClass_ = function(slideNo, className) {
649 var el = this.getSlideEl_(slideNo);
650
651 if (!el) {
652 return;
653 }
654
655 if (className) {
656 el.classList.add(className);
657 }
658
659 for (var i = 0, slideClass; slideClass = this.SLIDE_CLASSES_[i]; ++i) {
660 if (className != slideClass) {
661 el.classList.remove(slideClass);
662 }
663 }
664 };
665
666 /**
667 * @private
668 */
669 SlideDeck.prototype.makeBuildLists_ = function () {
670 for (var i = this.curSlide_, slide; slide = this.slides[i]; ++i) {
671 var items = slide.querySelectorAll('.build > *');
672 for (var j = 0, item; item = items[j]; ++j) {
673 if (item.classList) {
674 item.classList.add('to-build');
675 if (item.parentNode.classList.contains('fade')) {
676 item.classList.add('fade');
677 }
678 }
679 }
680 }
681 };
682
683 /**
684 * @private
685 * @param {boolean} dontPush
686 */
687 SlideDeck.prototype.updateHash_ = function(dontPush) {
688 if (!dontPush) {
689 var slideNo = this.curSlide_ + 1;
690 var hash = '#' + slideNo;
691 if (window.history.pushState) {
692 window.history.pushState(this.curSlide_, 'Slide ' + slideNo, hash);
693 } else {
694 window.location.replace(hash);
695 }
696
697 // Record GA hit on this slide.
698 window['_gaq'] && window['_gaq'].push(['_trackPageview',
699 document.location.href]);
700 }
701 };
702
703
704 /**
705 * @private
706 * @param {string} favIcon
707 */
708 SlideDeck.prototype.addFavIcon_ = function(favIcon) {
709 var el = document.createElement('link');
710 el.rel = 'icon';
711 el.type = 'image/png';
712 el.href = favIcon;
713 document.querySelector('head').appendChild(el);
714 };
715
716 /**
717 * @private
718 * @param {string} theme
719 */
720 SlideDeck.prototype.loadTheme_ = function(theme) {
721 var styles = [];
722 if (theme.constructor.name === 'String') {
723 styles.push(theme);
724 } else {
725 styles = theme;
726 }
727
728 for (var i = 0, style; themeUrl = styles[i]; i++) {
729 var style = document.createElement('link');
730 style.rel = 'stylesheet';
731 style.type = 'text/css';
732 if (themeUrl.indexOf('http') == -1) {
733 style.href = this.CSS_DIR_ + themeUrl + '.css';
734 } else {
735 style.href = themeUrl;
736 }
737 document.querySelector('head').appendChild(style);
738 }
739 };
740
741 /**
742 * @private
743 */
744 SlideDeck.prototype.loadAnalytics_ = function() {
745 var _gaq = window['_gaq'] || [];
746 _gaq.push(['_setAccount', this.config_.settings.analytics]);
747 _gaq.push(['_trackPageview']);
748
749 (function() {
750 var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
751 ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
752 var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
753 })();
754 };
755
756
757 // Polyfill missing APIs (if we need to), then create the slide deck.
758 // iOS < 5 needs classList, dataset, and window.matchMedia. Modernizr contains
759 // the last one.
760 (function() {
761 Modernizr.load({
762 test: !!document.body.classList && !!document.body.dataset,
763 nope: ['js/polyfills/classList.min.js', 'js/polyfills/dataset.min.js'],
764 complete: function() {
765 window.slidedeck = new SlideDeck();
766 }
767 });
768 })();