diff presen/js/slide-deck.js @ 10:5c57e35e19b6

add presen
author sugi
date Tue, 23 Apr 2013 23:31:26 +0900
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/presen/js/slide-deck.js	Tue Apr 23 23:31:26 2013 +0900
@@ -0,0 +1,775 @@
+/**
+ * @authors Luke Mahe
+ * @authors Eric Bidelman
+ * @fileoverview TODO
+ */
+document.cancelFullScreen = document.webkitCancelFullScreen ||
+                            document.mozCancelFullScreen;
+
+/**
+ * @constructor
+ */
+function SlideDeck(el) {
+  this.curSlide_ = 0;
+  this.prevSlide_ = 0;
+  this.config_ = null;
+  this.container = el || document.querySelector('slides');
+  this.slides = [];
+  this.controller = null;
+
+  this.getCurrentSlideFromHash_();
+
+  // Call this explicitly. Modernizr.load won't be done until after DOM load.
+  this.onDomLoaded_.bind(this)();
+}
+
+/**
+ * @const
+ * @private
+ */
+SlideDeck.prototype.SLIDE_CLASSES_ = [
+  'far-past', 'past', 'current', 'next', 'far-next'];
+
+/**
+ * @const
+ * @private
+ */
+SlideDeck.prototype.CSS_DIR_ = 'theme/css/';
+
+/**
+ * @private
+ */
+SlideDeck.prototype.getCurrentSlideFromHash_ = function() {
+  var slideNo = parseInt(document.location.hash.substr(1));
+
+  if (slideNo) {
+    this.curSlide_ = slideNo - 1;
+  } else {
+    this.curSlide_ = 0;
+  }
+};
+
+/**
+ * @param {number} slideNo
+ */
+SlideDeck.prototype.loadSlide = function(slideNo) {
+  if (slideNo) {
+    this.curSlide_ = slideNo - 1;
+    this.updateSlides_();
+  }
+};
+
+/**
+ * @private
+ */
+SlideDeck.prototype.onDomLoaded_ = function(e) {
+  document.body.classList.add('loaded'); // Add loaded class for templates to use.
+
+  this.slides = this.container.querySelectorAll('slide:not([hidden]):not(.backdrop)');
+
+  // If we're on a smartphone, apply special sauce.
+  if (Modernizr.mq('only screen and (max-device-width: 480px)')) {
+    // var style = document.createElement('link');
+    // style.rel = 'stylesheet';
+    // style.type = 'text/css';
+    // style.href = this.CSS_DIR_ + 'phone.css';
+    // document.querySelector('head').appendChild(style);
+
+    // No need for widescreen layout on a phone.
+    this.container.classList.remove('layout-widescreen');
+  }
+
+  this.loadConfig_(SLIDE_CONFIG);
+  this.addEventListeners_();
+  this.updateSlides_();
+
+  // Add slide numbers and total slide count metadata to each slide.
+  var that = this;
+  for (var i = 0, slide; slide = this.slides[i]; ++i) {
+    slide.dataset.slideNum = i + 1;
+    slide.dataset.totalSlides = this.slides.length;
+
+    slide.addEventListener('click', function(e) {
+      if (document.body.classList.contains('overview')) {
+        that.loadSlide(this.dataset.slideNum);
+        e.preventDefault();
+        window.setTimeout(function() {
+          that.toggleOverview();
+        }, 500);
+      }
+    }, false);
+  }
+
+  // Note: this needs to come after addEventListeners_(), which adds a
+  // 'keydown' listener that this controller relies on.
+  // Also, no need to set this up if we're on mobile.
+  if (!Modernizr.touch) {
+    this.controller = new SlideController(this);
+    if (this.controller.isPopup) {
+      document.body.classList.add('popup');
+    }
+  }
+};
+
+/**
+ * @private
+ */
+SlideDeck.prototype.addEventListeners_ = function() {
+  document.addEventListener('keydown', this.onBodyKeyDown_.bind(this), false);
+  window.addEventListener('popstate', this.onPopState_.bind(this), false);
+
+  // var transEndEventNames = {
+  //   'WebkitTransition': 'webkitTransitionEnd',
+  //   'MozTransition': 'transitionend',
+  //   'OTransition': 'oTransitionEnd',
+  //   'msTransition': 'MSTransitionEnd',
+  //   'transition': 'transitionend'
+  // };
+  // 
+  // // Find the correct transitionEnd vendor prefix.
+  // window.transEndEventName = transEndEventNames[
+  //     Modernizr.prefixed('transition')];
+  // 
+  // // When slides are done transitioning, kickoff loading iframes.
+  // // Note: we're only looking at a single transition (on the slide). This
+  // // doesn't include autobuilds the slides may have. Also, if the slide
+  // // transitions on multiple properties (e.g. not just 'all'), this doesn't
+  // // handle that case.
+  // this.container.addEventListener(transEndEventName, function(e) {
+  //     this.enableSlideFrames_(this.curSlide_);
+  // }.bind(this), false);
+
+  // document.addEventListener('slideenter', function(e) {
+  //   var slide = e.target;
+  //   window.setTimeout(function() {
+  //     this.enableSlideFrames_(e.slideNumber);
+  //     this.enableSlideFrames_(e.slideNumber + 1);
+  //   }.bind(this), 300);
+  // }.bind(this), false);
+};
+
+/**
+ * @private
+ * @param {Event} e The pop event.
+ */
+SlideDeck.prototype.onPopState_ = function(e) {
+  if (e.state != null) {
+    this.curSlide_ = e.state;
+    this.updateSlides_(true);
+  }
+};
+
+/**
+ * @param {Event} e
+ */
+SlideDeck.prototype.onBodyKeyDown_ = function(e) {
+  if (/^(input|textarea)$/i.test(e.target.nodeName) ||
+      e.target.isContentEditable) {
+    return;
+  }
+
+  // Forward keydowns to the main slides if we're the popup.
+  if (this.controller && this.controller.isPopup) {
+    this.controller.sendMsg({keyCode: e.keyCode});
+  }
+
+  switch (e.keyCode) {
+    case 13: // Enter
+      if (document.body.classList.contains('overview')) {
+        this.toggleOverview();
+      }
+      break;
+
+    case 39: // right arrow
+    case 32: // space
+    case 34: // PgDn
+      this.nextSlide();
+      e.preventDefault();
+      break;
+
+    case 37: // left arrow
+    case 8: // Backspace
+    case 33: // PgUp
+      this.prevSlide();
+      e.preventDefault();
+      break;
+
+    case 40: // down arrow
+      this.nextSlide();
+      e.preventDefault();
+      break;
+
+    case 38: // up arrow
+      this.prevSlide();
+      e.preventDefault();
+      break;
+
+    case 72: // H: Toggle code highlighting
+      document.body.classList.toggle('highlight-code');
+      break;
+
+    case 79: // O: Toggle overview
+      this.toggleOverview();
+      break;
+
+    case 80: // P
+      if (this.controller && this.controller.isPopup) {
+        document.body.classList.toggle('with-notes');
+      } else if (this.controller && !this.controller.popup) {
+        document.body.classList.toggle('with-notes');
+      }
+      break;
+
+    case 82: // R
+      // TODO: implement refresh on main slides when popup is refreshed.
+      break;
+
+    case 27: // ESC: Hide notes and highlighting
+      document.body.classList.remove('with-notes');
+      document.body.classList.remove('highlight-code');
+
+      if (document.body.classList.contains('overview')) {
+        this.toggleOverview();
+      }
+      break;
+
+    case 70: // F: Toggle fullscreen
+       // Only respect 'f' on body. Don't want to capture keys from an <input>.
+       // Also, ignore browser's fullscreen shortcut (cmd+shift+f) so we don't
+       // get trapped in fullscreen!
+      if (e.target == document.body && !(e.shiftKey && e.metaKey)) {
+        if (document.mozFullScreen !== undefined && !document.mozFullScreen) {
+          document.body.mozRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
+        } else if (document.webkitIsFullScreen !== undefined && !document.webkitIsFullScreen) {
+          document.body.webkitRequestFullScreen(Element.ALLOW_KEYBOARD_INPUT);
+        } else {
+          document.cancelFullScreen();
+        }
+      }
+      break;
+
+    case 87: // W: Toggle widescreen
+      // Only respect 'w' on body. Don't want to capture keys from an <input>.
+      if (e.target == document.body && !(e.shiftKey && e.metaKey)) {
+        this.container.classList.toggle('layout-widescreen');
+      }
+      break;
+  }
+};
+
+/**
+ *
+ */
+SlideDeck.prototype.focusOverview_ = function() {
+  var overview = document.body.classList.contains('overview');
+
+  for (var i = 0, slide; slide = this.slides[i]; i++) {
+    slide.style[Modernizr.prefixed('transform')] = overview ?
+        'translateZ(-2500px) translate(' + (( i - this.curSlide_ ) * 105) +
+                                       '%, 0%)' : '';
+  }
+};
+
+/**
+ */
+SlideDeck.prototype.toggleOverview = function() {
+  document.body.classList.toggle('overview');
+
+  this.focusOverview_();
+};
+
+/**
+ * @private
+ */
+SlideDeck.prototype.loadConfig_ = function(config) {
+  if (!config) {
+    return;
+  }
+
+  this.config_ = config;
+
+  var settings = this.config_.settings;
+
+  this.loadTheme_(settings.theme || []);
+
+  if (settings.favIcon) {
+    this.addFavIcon_(settings.favIcon);
+  }
+
+  // Prettyprint. Default to on.
+  if (!!!('usePrettify' in settings) || settings.usePrettify) {
+    prettyPrint();
+  }
+
+  if (settings.analytics) {
+    this.loadAnalytics_();
+  }
+
+  if (settings.fonts) {
+    this.addFonts_(settings.fonts);
+  }
+
+  // Builds. Default to on.
+  if (!!!('useBuilds' in settings) || settings.useBuilds) {
+    this.makeBuildLists_();
+  }
+
+  if (settings.title) {
+    document.title = settings.title.replace(/<br\/?>/, ' ');
+    if (settings.eventTitle) {
+      document.title +=  ' - ' + settings.eventTitle;
+    }
+    document.querySelector('[data-config-title]').innerHTML = settings.title;
+  }
+
+  if (settings.subtitle) {
+    document.querySelector('[data-config-subtitle]').innerHTML = settings.subtitle;
+  }
+
+  if (this.config_.presenters) {
+    var presenters = this.config_.presenters;
+    var dataConfigContact = document.querySelector('[data-config-contact]');
+
+    var html = [];
+    if (presenters.length == 1) {
+      var p = presenters[0];
+
+      html = [p.name, p.company].join('<br>');
+
+      var gplus = p.gplus ? '<span>g+</span><a href="' + p.gplus +
+          '">' + p.gplus.replace(/https?:\/\//, '') + '</a>' : '';
+
+      var twitter = p.twitter ? '<span>twitter</span>' +
+          '<a href="http://twitter.com/' + p.twitter + '">' +
+          p.twitter + '</a>' : '';
+
+      var www = p.www ? '<span>www</span><a href="' + p.www +
+                        '">' + p.www.replace(/https?:\/\//, '') + '</a>' : '';
+
+      var github = p.github ? '<span>github</span><a href="' + p.github +
+          '">' + p.github.replace(/https?:\/\//, '') + '</a>' : '';
+
+      var html2 = [gplus, twitter, www, github].join('<br>');
+
+      if (dataConfigContact) {
+        dataConfigContact.innerHTML = html2;
+      }
+    } else {
+      for (var i = 0, p; p = presenters[i]; ++i) {
+        html.push(p.name + ' - ' + p.company);
+      }
+      html = html.join('<br>');
+      if (dataConfigContact) {
+        dataConfigContact.innerHTML = html;
+      }
+    }
+
+    var dataConfigPresenter = document.querySelector('[data-config-presenter]');
+    if (dataConfigPresenter) {
+      dataConfigPresenter.innerHTML = html;
+      if (settings.eventTitle) {
+        dataConfigPresenter.innerHTML = dataConfigPresenter.innerHTML + '<br>' +
+                                        settings.eventTitle;
+      }
+    }
+  }
+
+  /* Left/Right tap areas. Default to including. */
+  if (!!!('enableSlideAreas' in settings) || settings.enableSlideAreas) {
+    var el = document.createElement('div');
+    el.classList.add('slide-area');
+    el.id = 'prev-slide-area';
+    el.addEventListener('click', this.prevSlide.bind(this), false);
+    this.container.appendChild(el);
+
+    var el = document.createElement('div');
+    el.classList.add('slide-area');
+    el.id = 'next-slide-area';
+    el.addEventListener('click', this.nextSlide.bind(this), false);
+    this.container.appendChild(el);
+  }
+
+  if (Modernizr.touch && (!!!('enableTouch' in settings) ||
+      settings.enableTouch)) {
+    var self = this;
+
+    // Note: this prevents mobile zoom in/out but prevents iOS from doing
+    // it's crazy scroll over effect and disaligning the slides.
+    window.addEventListener('touchstart', function(e) {
+      e.preventDefault();
+    }, false);
+
+    var hammer = new Hammer(this.container);
+    hammer.ondragend = function(e) {
+      if (e.direction == 'right' || e.direction == 'down') {
+        self.prevSlide();
+      } else if (e.direction == 'left' || e.direction == 'up') {
+        self.nextSlide();
+      }
+    };
+  }
+};
+
+/**
+ * @private
+ * @param {Array.<string>} fonts
+ */
+SlideDeck.prototype.addFonts_ = function(fonts) {
+  var el = document.createElement('link');
+  el.rel = 'stylesheet';
+  el.href = ('https:' == document.location.protocol ? 'https' : 'http') +
+      '://fonts.googleapis.com/css?family=' + fonts.join('|') + '&v2';
+  document.querySelector('head').appendChild(el);
+};
+
+/**
+ * @private
+ */
+SlideDeck.prototype.buildNextItem_ = function() {
+  var slide = this.slides[this.curSlide_];
+  var toBuild = slide.querySelector('.to-build');
+  var built = slide.querySelector('.build-current');
+
+  if (built) {
+    built.classList.remove('build-current');
+    if (built.classList.contains('fade')) {
+      built.classList.add('build-fade');
+    }
+  }
+
+  if (!toBuild) {
+    var items = slide.querySelectorAll('.build-fade');
+    for (var j = 0, item; item = items[j]; j++) {
+      item.classList.remove('build-fade');
+    }
+    return false;
+  }
+
+  toBuild.classList.remove('to-build');
+  toBuild.classList.add('build-current');
+
+  return true;
+};
+
+/**
+ * @param {boolean=} opt_dontPush
+ */
+SlideDeck.prototype.prevSlide = function(opt_dontPush) {
+  if (this.curSlide_ > 0) {
+    var bodyClassList = document.body.classList;
+    bodyClassList.remove('highlight-code');
+
+    // Toggle off speaker notes if they're showing when we move backwards on the
+    // main slides. If we're the speaker notes popup, leave them up.
+    if (this.controller && !this.controller.isPopup) {
+      bodyClassList.remove('with-notes');
+    } else if (!this.controller) {
+      bodyClassList.remove('with-notes');
+    }
+
+    this.prevSlide_ = this.curSlide_--;
+
+    this.updateSlides_(opt_dontPush);
+  }
+};
+
+/**
+ * @param {boolean=} opt_dontPush
+ */
+SlideDeck.prototype.nextSlide = function(opt_dontPush) {
+  if (!document.body.classList.contains('overview') && this.buildNextItem_()) {
+    return;
+  }
+
+  if (this.curSlide_ < this.slides.length - 1) {
+    var bodyClassList = document.body.classList;
+    bodyClassList.remove('highlight-code');
+
+    // Toggle off speaker notes if they're showing when we advanced on the main
+    // slides. If we're the speaker notes popup, leave them up.
+    if (this.controller && !this.controller.isPopup) {
+      bodyClassList.remove('with-notes');
+    } else if (!this.controller) {
+      bodyClassList.remove('with-notes');
+    }
+
+    this.prevSlide_ = this.curSlide_++;
+
+    this.updateSlides_(opt_dontPush);
+  }
+};
+
+/* Slide events */
+
+/**
+ * Triggered when a slide enter/leave event should be dispatched.
+ *
+ * @param {string} type The type of event to trigger
+ *     (e.g. 'slideenter', 'slideleave').
+ * @param {number} slideNo The index of the slide that is being left.
+ */
+SlideDeck.prototype.triggerSlideEvent = function(type, slideNo) {
+  var el = this.getSlideEl_(slideNo);
+  if (!el) {
+    return;
+  }
+
+  // Call onslideenter/onslideleave if the attribute is defined on this slide.
+  var func = el.getAttribute(type);
+  if (func) {
+    new Function(func).call(el); // TODO: Don't use new Function() :(
+  }
+
+  // Dispatch event to listeners setup using addEventListener.
+  var evt = document.createEvent('Event');
+  evt.initEvent(type, true, true);
+  evt.slideNumber = slideNo + 1; // Make it readable
+  evt.slide = el;
+
+  el.dispatchEvent(evt);
+};
+
+/**
+ * @private
+ */
+SlideDeck.prototype.updateSlides_ = function(opt_dontPush) {
+  var dontPush = opt_dontPush || false;
+
+  var curSlide = this.curSlide_;
+  for (var i = 0; i < this.slides.length; ++i) {
+    switch (i) {
+      case curSlide - 2:
+        this.updateSlideClass_(i, 'far-past');
+        break;
+      case curSlide - 1:
+        this.updateSlideClass_(i, 'past');
+        break;
+      case curSlide:
+        this.updateSlideClass_(i, 'current');
+        break;
+      case curSlide + 1:
+        this.updateSlideClass_(i, 'next');
+        break;
+      case curSlide + 2:
+        this.updateSlideClass_(i, 'far-next');
+        break;
+      default:
+        this.updateSlideClass_(i);
+        break;
+    }
+  };
+
+  this.triggerSlideEvent('slideleave', this.prevSlide_);
+  this.triggerSlideEvent('slideenter', curSlide);
+
+// window.setTimeout(this.disableSlideFrames_.bind(this, curSlide - 2), 301);
+// 
+// this.enableSlideFrames_(curSlide - 1); // Previous slide.
+// this.enableSlideFrames_(curSlide + 1); // Current slide.
+// this.enableSlideFrames_(curSlide + 2); // Next slide.
+
+   // Enable current slide's iframes (needed for page loat at current slide).
+   this.enableSlideFrames_(curSlide + 1);
+
+   // No way to tell when all slide transitions + auto builds are done.
+   // Give ourselves a good buffer to preload the next slide's iframes.
+   window.setTimeout(this.enableSlideFrames_.bind(this, curSlide + 2), 1000);
+
+  this.updateHash_(dontPush);
+
+  if (document.body.classList.contains('overview')) {
+    this.focusOverview_();
+    return;
+  }
+
+};
+
+/**
+ * @private
+ * @param {number} slideNo
+ */
+SlideDeck.prototype.enableSlideFrames_ = function(slideNo) {
+  var el = this.slides[slideNo - 1];
+  if (!el) {
+    return;
+  }
+
+  var frames = el.querySelectorAll('iframe');
+  for (var i = 0, frame; frame = frames[i]; i++) {
+    this.enableFrame_(frame);
+  }
+};
+
+/**
+ * @private
+ * @param {number} slideNo
+ */
+SlideDeck.prototype.enableFrame_ = function(frame) {
+  var src = frame.dataset.src;
+  if (src && frame.src != src) {
+    frame.src = src;
+  }
+};
+
+/**
+ * @private
+ * @param {number} slideNo
+ */
+SlideDeck.prototype.disableSlideFrames_ = function(slideNo) {
+  var el = this.slides[slideNo - 1];
+  if (!el) {
+    return;
+  }
+
+  var frames = el.querySelectorAll('iframe');
+  for (var i = 0, frame; frame = frames[i]; i++) {
+    this.disableFrame_(frame);
+  }
+};
+
+/**
+ * @private
+ * @param {Node} frame
+ */
+SlideDeck.prototype.disableFrame_ = function(frame) {
+  frame.src = 'about:blank';
+};
+
+/**
+ * @private
+ * @param {number} slideNo
+ */
+SlideDeck.prototype.getSlideEl_ = function(no) {
+  if ((no < 0) || (no >= this.slides.length)) {
+    return null;
+  } else {
+    return this.slides[no];
+  }
+};
+
+/**
+ * @private
+ * @param {number} slideNo
+ * @param {string} className
+ */
+SlideDeck.prototype.updateSlideClass_ = function(slideNo, className) {
+  var el = this.getSlideEl_(slideNo);
+
+  if (!el) {
+    return;
+  }
+
+  if (className) {
+    el.classList.add(className);
+  }
+
+  for (var i = 0, slideClass; slideClass = this.SLIDE_CLASSES_[i]; ++i) {
+    if (className != slideClass) {
+      el.classList.remove(slideClass);
+    }
+  }
+};
+
+/**
+ * @private
+ */
+SlideDeck.prototype.makeBuildLists_ = function () {
+  for (var i = this.curSlide_, slide; slide = this.slides[i]; ++i) {
+    var items = slide.querySelectorAll('.build > *');
+    for (var j = 0, item; item = items[j]; ++j) {
+      if (item.classList) {
+        item.classList.add('to-build');
+        if (item.parentNode.classList.contains('fade')) {
+          item.classList.add('fade');
+        }
+      }
+    }
+  }
+};
+
+/**
+ * @private
+ * @param {boolean} dontPush
+ */
+SlideDeck.prototype.updateHash_ = function(dontPush) {
+  if (!dontPush) {
+    var slideNo = this.curSlide_ + 1;
+    var hash = '#' + slideNo;
+    if (window.history.pushState) {
+      window.history.pushState(this.curSlide_, 'Slide ' + slideNo, hash);
+    } else {
+      window.location.replace(hash);
+    }
+
+    // Record GA hit on this slide.
+    window['_gaq'] && window['_gaq'].push(['_trackPageview',
+                                          document.location.href]);
+  }
+};
+
+
+/**
+ * @private
+ * @param {string} favIcon
+ */
+SlideDeck.prototype.addFavIcon_ = function(favIcon) {
+  var el = document.createElement('link');
+  el.rel = 'icon';
+  el.type = 'image/png';
+  el.href = favIcon;
+  document.querySelector('head').appendChild(el);
+};
+
+/**
+ * @private
+ * @param {string} theme
+ */
+SlideDeck.prototype.loadTheme_ = function(theme) {
+  var styles = [];
+  if (theme.constructor.name === 'String') {
+    styles.push(theme);
+  } else {
+    styles = theme;
+  }
+
+  for (var i = 0, style; themeUrl = styles[i]; i++) {
+    var style = document.createElement('link');
+    style.rel = 'stylesheet';
+    style.type = 'text/css';
+    if (themeUrl.indexOf('http') == -1) {
+      style.href = this.CSS_DIR_ + themeUrl + '.css';
+    } else {
+      style.href = themeUrl;
+    }
+    document.querySelector('head').appendChild(style);
+  }
+};
+
+/**
+ * @private
+ */
+SlideDeck.prototype.loadAnalytics_ = function() {
+  var _gaq = window['_gaq'] || [];
+  _gaq.push(['_setAccount', this.config_.settings.analytics]);
+  _gaq.push(['_trackPageview']);
+
+  (function() {
+    var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
+    ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
+    var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
+  })();
+};
+
+
+// Polyfill missing APIs (if we need to), then create the slide deck.
+// iOS < 5 needs classList, dataset, and window.matchMedia. Modernizr contains
+// the last one.
+(function() {
+  Modernizr.load({
+    test: !!document.body.classList && !!document.body.dataset,
+    nope: ['js/polyfills/classList.min.js', 'js/polyfills/dataset.min.js'],
+    complete: function() {
+      window.slidedeck = new SlideDeck();
+    }
+  });
+})();