| // Copyright 2012 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| var PERMANENT_URL_PREFIX = '/static/'; | |
| var SLIDE_CLASSES = ['far-past', 'past', 'current', 'next', 'far-next']; | |
| var PM_TOUCH_SENSITIVITY = 15; | |
| var curSlide; | |
| /* ---------------------------------------------------------------------- */ | |
| /* classList polyfill by Eli Grey | |
| * (http://purl.eligrey.com/github/classList.js/blob/master/classList.js) */ | |
| if ( | |
| typeof document !== 'undefined' && | |
| !('classList' in document.createElement('a')) | |
| ) { | |
| (function(view) { | |
| var classListProp = 'classList', | |
| protoProp = 'prototype', | |
| elemCtrProto = (view.HTMLElement || view.Element)[protoProp], | |
| objCtr = Object; | |
| (strTrim = | |
| String[protoProp].trim || | |
| function() { | |
| return this.replace(/^\s+|\s+$/g, ''); | |
| }), | |
| (arrIndexOf = | |
| Array[protoProp].indexOf || | |
| function(item) { | |
| for (var i = 0, len = this.length; i < len; i++) { | |
| if (i in this && this[i] === item) { | |
| return i; | |
| } | |
| } | |
| return -1; | |
| }), | |
| // Vendors: please allow content code to instantiate DOMExceptions | |
| (DOMEx = function(type, message) { | |
| this.name = type; | |
| this.code = DOMException[type]; | |
| this.message = message; | |
| }), | |
| (checkTokenAndGetIndex = function(classList, token) { | |
| if (token === '') { | |
| throw new DOMEx( | |
| 'SYNTAX_ERR', | |
| 'An invalid or illegal string was specified' | |
| ); | |
| } | |
| if (/\s/.test(token)) { | |
| throw new DOMEx( | |
| 'INVALID_CHARACTER_ERR', | |
| 'String contains an invalid character' | |
| ); | |
| } | |
| return arrIndexOf.call(classList, token); | |
| }), | |
| (ClassList = function(elem) { | |
| var trimmedClasses = strTrim.call(elem.className), | |
| classes = trimmedClasses ? trimmedClasses.split(/\s+/) : []; | |
| for (var i = 0, len = classes.length; i < len; i++) { | |
| this.push(classes[i]); | |
| } | |
| this._updateClassName = function() { | |
| elem.className = this.toString(); | |
| }; | |
| }), | |
| (classListProto = ClassList[protoProp] = []), | |
| (classListGetter = function() { | |
| return new ClassList(this); | |
| }); | |
| // Most DOMException implementations don't allow calling DOMException's toString() | |
| // on non-DOMExceptions. Error's toString() is sufficient here. | |
| DOMEx[protoProp] = Error[protoProp]; | |
| classListProto.item = function(i) { | |
| return this[i] || null; | |
| }; | |
| classListProto.contains = function(token) { | |
| token += ''; | |
| return checkTokenAndGetIndex(this, token) !== -1; | |
| }; | |
| classListProto.add = function(token) { | |
| token += ''; | |
| if (checkTokenAndGetIndex(this, token) === -1) { | |
| this.push(token); | |
| this._updateClassName(); | |
| } | |
| }; | |
| classListProto.remove = function(token) { | |
| token += ''; | |
| var index = checkTokenAndGetIndex(this, token); | |
| if (index !== -1) { | |
| this.splice(index, 1); | |
| this._updateClassName(); | |
| } | |
| }; | |
| classListProto.toggle = function(token) { | |
| token += ''; | |
| if (checkTokenAndGetIndex(this, token) === -1) { | |
| this.add(token); | |
| } else { | |
| this.remove(token); | |
| } | |
| }; | |
| classListProto.toString = function() { | |
| return this.join(' '); | |
| }; | |
| if (objCtr.defineProperty) { | |
| var classListPropDesc = { | |
| get: classListGetter, | |
| enumerable: true, | |
| configurable: true, | |
| }; | |
| try { | |
| objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); | |
| } catch (ex) { | |
| // IE 8 doesn't support enumerable:true | |
| if (ex.number === -0x7ff5ec54) { | |
| classListPropDesc.enumerable = false; | |
| objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); | |
| } | |
| } | |
| } else if (objCtr[protoProp].__defineGetter__) { | |
| elemCtrProto.__defineGetter__(classListProp, classListGetter); | |
| } | |
| })(self); | |
| } | |
| /* ---------------------------------------------------------------------- */ | |
| /* Slide movement */ | |
| function hideHelpText() { | |
| document.getElementById('help').style.display = 'none'; | |
| } | |
| function getSlideEl(no) { | |
| if (no < 0 || no >= slideEls.length) { | |
| return null; | |
| } else { | |
| return slideEls[no]; | |
| } | |
| } | |
| function updateSlideClass(slideNo, className) { | |
| var el = getSlideEl(slideNo); | |
| if (!el) { | |
| return; | |
| } | |
| if (className) { | |
| el.classList.add(className); | |
| } | |
| for (var i in SLIDE_CLASSES) { | |
| if (className != SLIDE_CLASSES[i]) { | |
| el.classList.remove(SLIDE_CLASSES[i]); | |
| } | |
| } | |
| } | |
| function updateSlides() { | |
| if (window.trackPageview) window.trackPageview(); | |
| for (var i = 0; i < slideEls.length; i++) { | |
| switch (i) { | |
| case curSlide - 2: | |
| updateSlideClass(i, 'far-past'); | |
| break; | |
| case curSlide - 1: | |
| updateSlideClass(i, 'past'); | |
| break; | |
| case curSlide: | |
| updateSlideClass(i, 'current'); | |
| break; | |
| case curSlide + 1: | |
| updateSlideClass(i, 'next'); | |
| break; | |
| case curSlide + 2: | |
| updateSlideClass(i, 'far-next'); | |
| break; | |
| default: | |
| updateSlideClass(i); | |
| break; | |
| } | |
| } | |
| triggerLeaveEvent(curSlide - 1); | |
| triggerEnterEvent(curSlide); | |
| window.setTimeout(function() { | |
| // Hide after the slide | |
| disableSlideFrames(curSlide - 2); | |
| }, 301); | |
| enableSlideFrames(curSlide - 1); | |
| enableSlideFrames(curSlide + 2); | |
| updateHash(); | |
| } | |
| function prevSlide() { | |
| hideHelpText(); | |
| if (curSlide > 0) { | |
| curSlide--; | |
| updateSlides(); | |
| } | |
| if (notesEnabled) localStorage.setItem(destSlideKey(), curSlide); | |
| } | |
| function nextSlide() { | |
| hideHelpText(); | |
| if (curSlide < slideEls.length - 1) { | |
| curSlide++; | |
| updateSlides(); | |
| } | |
| if (notesEnabled) localStorage.setItem(destSlideKey(), curSlide); | |
| } | |
| /* Slide events */ | |
| function triggerEnterEvent(no) { | |
| var el = getSlideEl(no); | |
| if (!el) { | |
| return; | |
| } | |
| var onEnter = el.getAttribute('onslideenter'); | |
| if (onEnter) { | |
| new Function(onEnter).call(el); | |
| } | |
| var evt = document.createEvent('Event'); | |
| evt.initEvent('slideenter', true, true); | |
| evt.slideNumber = no + 1; // Make it readable | |
| el.dispatchEvent(evt); | |
| } | |
| function triggerLeaveEvent(no) { | |
| var el = getSlideEl(no); | |
| if (!el) { | |
| return; | |
| } | |
| var onLeave = el.getAttribute('onslideleave'); | |
| if (onLeave) { | |
| new Function(onLeave).call(el); | |
| } | |
| var evt = document.createEvent('Event'); | |
| evt.initEvent('slideleave', true, true); | |
| evt.slideNumber = no + 1; // Make it readable | |
| el.dispatchEvent(evt); | |
| } | |
| /* Touch events */ | |
| function handleTouchStart(event) { | |
| if (event.touches.length == 1) { | |
| touchDX = 0; | |
| touchDY = 0; | |
| touchStartX = event.touches[0].pageX; | |
| touchStartY = event.touches[0].pageY; | |
| document.body.addEventListener('touchmove', handleTouchMove, true); | |
| document.body.addEventListener('touchend', handleTouchEnd, true); | |
| } | |
| } | |
| function handleTouchMove(event) { | |
| if (event.touches.length > 1) { | |
| cancelTouch(); | |
| } else { | |
| touchDX = event.touches[0].pageX - touchStartX; | |
| touchDY = event.touches[0].pageY - touchStartY; | |
| event.preventDefault(); | |
| } | |
| } | |
| function handleTouchEnd(event) { | |
| var dx = Math.abs(touchDX); | |
| var dy = Math.abs(touchDY); | |
| if (dx > PM_TOUCH_SENSITIVITY && dy < (dx * 2) / 3) { | |
| if (touchDX > 0) { | |
| prevSlide(); | |
| } else { | |
| nextSlide(); | |
| } | |
| } | |
| cancelTouch(); | |
| } | |
| function cancelTouch() { | |
| document.body.removeEventListener('touchmove', handleTouchMove, true); | |
| document.body.removeEventListener('touchend', handleTouchEnd, true); | |
| } | |
| /* Preloading frames */ | |
| function disableSlideFrames(no) { | |
| var el = getSlideEl(no); | |
| if (!el) { | |
| return; | |
| } | |
| var frames = el.getElementsByTagName('iframe'); | |
| for (var i = 0, frame; (frame = frames[i]); i++) { | |
| disableFrame(frame); | |
| } | |
| } | |
| function enableSlideFrames(no) { | |
| var el = getSlideEl(no); | |
| if (!el) { | |
| return; | |
| } | |
| var frames = el.getElementsByTagName('iframe'); | |
| for (var i = 0, frame; (frame = frames[i]); i++) { | |
| enableFrame(frame); | |
| } | |
| } | |
| function disableFrame(frame) { | |
| frame.src = 'about:blank'; | |
| } | |
| function enableFrame(frame) { | |
| var src = frame._src; | |
| if (frame.src != src && src != 'about:blank') { | |
| frame.src = src; | |
| } | |
| } | |
| function setupFrames() { | |
| var frames = document.querySelectorAll('iframe'); | |
| for (var i = 0, frame; (frame = frames[i]); i++) { | |
| frame._src = frame.src; | |
| disableFrame(frame); | |
| } | |
| enableSlideFrames(curSlide); | |
| enableSlideFrames(curSlide + 1); | |
| enableSlideFrames(curSlide + 2); | |
| } | |
| function setupInteraction() { | |
| /* Clicking and tapping */ | |
| var el = document.createElement('div'); | |
| el.className = 'slide-area'; | |
| el.id = 'prev-slide-area'; | |
| el.addEventListener('click', prevSlide, false); | |
| document.querySelector('section.slides').appendChild(el); | |
| var el = document.createElement('div'); | |
| el.className = 'slide-area'; | |
| el.id = 'next-slide-area'; | |
| el.addEventListener('click', nextSlide, false); | |
| document.querySelector('section.slides').appendChild(el); | |
| /* Swiping */ | |
| document.body.addEventListener('touchstart', handleTouchStart, false); | |
| } | |
| /* Hash functions */ | |
| function getCurSlideFromHash() { | |
| var slideNo = parseInt(location.hash.substr(1)); | |
| if (slideNo) { | |
| curSlide = slideNo - 1; | |
| } else { | |
| curSlide = 0; | |
| } | |
| } | |
| function updateHash() { | |
| location.replace('#' + (curSlide + 1)); | |
| } | |
| /* Event listeners */ | |
| function handleBodyKeyDown(event) { | |
| // If we're in a code element, only handle pgup/down. | |
| var inCode = event.target.classList.contains('code'); | |
| switch (event.keyCode) { | |
| case 78: // 'N' opens presenter notes window | |
| if (!inCode && notesEnabled) toggleNotesWindow(); | |
| break; | |
| case 72: // 'H' hides the help text | |
| case 27: // escape key | |
| if (!inCode) hideHelpText(); | |
| break; | |
| case 39: // right arrow | |
| case 13: // Enter | |
| case 32: // space | |
| if (inCode) break; | |
| case 34: // PgDn | |
| nextSlide(); | |
| event.preventDefault(); | |
| break; | |
| case 37: // left arrow | |
| case 8: // Backspace | |
| if (inCode) break; | |
| case 33: // PgUp | |
| prevSlide(); | |
| event.preventDefault(); | |
| break; | |
| case 40: // down arrow | |
| if (inCode) break; | |
| nextSlide(); | |
| event.preventDefault(); | |
| break; | |
| case 38: // up arrow | |
| if (inCode) break; | |
| prevSlide(); | |
| event.preventDefault(); | |
| break; | |
| } | |
| } | |
| function scaleSmallViewports() { | |
| var el = document.querySelector('section.slides'); | |
| var transform = ''; | |
| var sWidthPx = 1250; | |
| var sHeightPx = 750; | |
| var sAspectRatio = sWidthPx / sHeightPx; | |
| var wAspectRatio = window.innerWidth / window.innerHeight; | |
| if (wAspectRatio <= sAspectRatio && window.innerWidth < sWidthPx) { | |
| transform = 'scale(' + window.innerWidth / sWidthPx + ')'; | |
| } else if (window.innerHeight < sHeightPx) { | |
| transform = 'scale(' + window.innerHeight / sHeightPx + ')'; | |
| } | |
| el.style.transform = transform; | |
| } | |
| function addEventListeners() { | |
| document.addEventListener('keydown', handleBodyKeyDown, false); | |
| var resizeTimeout; | |
| window.addEventListener('resize', function() { | |
| // throttle resize events | |
| window.clearTimeout(resizeTimeout); | |
| resizeTimeout = window.setTimeout(function() { | |
| resizeTimeout = null; | |
| scaleSmallViewports(); | |
| }, 50); | |
| }); | |
| // Force reset transform property of section.slides when printing page. | |
| // Use both onbeforeprint and matchMedia for compatibility with different browsers. | |
| var beforePrint = function() { | |
| var el = document.querySelector('section.slides'); | |
| el.style.transform = ''; | |
| }; | |
| window.onbeforeprint = beforePrint; | |
| if (window.matchMedia) { | |
| var mediaQueryList = window.matchMedia('print'); | |
| mediaQueryList.addListener(function(mql) { | |
| if (mql.matches) beforePrint(); | |
| }); | |
| } | |
| } | |
| /* Initialization */ | |
| function addFontStyle() { | |
| var el = document.createElement('link'); | |
| el.rel = 'stylesheet'; | |
| el.type = 'text/css'; | |
| el.href = | |
| '//fonts.googleapis.com/css?family=' + | |
| 'Open+Sans:regular,semibold,italic,italicsemibold|Droid+Sans+Mono'; | |
| document.body.appendChild(el); | |
| } | |
| function addGeneralStyle() { | |
| var el = document.createElement('link'); | |
| el.rel = 'stylesheet'; | |
| el.type = 'text/css'; | |
| el.href = PERMANENT_URL_PREFIX + 'styles.css'; | |
| document.body.appendChild(el); | |
| var el = document.createElement('meta'); | |
| el.name = 'viewport'; | |
| el.content = 'width=device-width,height=device-height,initial-scale=1'; | |
| document.querySelector('head').appendChild(el); | |
| var el = document.createElement('meta'); | |
| el.name = 'apple-mobile-web-app-capable'; | |
| el.content = 'yes'; | |
| document.querySelector('head').appendChild(el); | |
| scaleSmallViewports(); | |
| } | |
| function handleDomLoaded() { | |
| slideEls = document.querySelectorAll('section.slides > article'); | |
| setupFrames(); | |
| addFontStyle(); | |
| addGeneralStyle(); | |
| addEventListeners(); | |
| updateSlides(); | |
| setupInteraction(); | |
| if ( | |
| window.location.hostname == 'localhost' || | |
| window.location.hostname == '127.0.0.1' || | |
| window.location.hostname == '::1' | |
| ) { | |
| hideHelpText(); | |
| } | |
| document.body.classList.add('loaded'); | |
| setupNotesSync(); | |
| } | |
| function initialize() { | |
| getCurSlideFromHash(); | |
| if (window['_DEBUG']) { | |
| PERMANENT_URL_PREFIX = '../'; | |
| } | |
| if (window['_DCL']) { | |
| handleDomLoaded(); | |
| } else { | |
| document.addEventListener('DOMContentLoaded', handleDomLoaded, false); | |
| } | |
| } | |
| // If ?debug exists then load the script relative instead of absolute | |
| if (!window['_DEBUG'] && document.location.href.indexOf('?debug') !== -1) { | |
| document.addEventListener( | |
| 'DOMContentLoaded', | |
| function() { | |
| // Avoid missing the DomContentLoaded event | |
| window['_DCL'] = true; | |
| }, | |
| false | |
| ); | |
| window['_DEBUG'] = true; | |
| var script = document.createElement('script'); | |
| script.type = 'text/javascript'; | |
| script.src = '../slides.js'; | |
| var s = document.getElementsByTagName('script')[0]; | |
| s.parentNode.insertBefore(script, s); | |
| // Remove this script | |
| s.parentNode.removeChild(s); | |
| } else { | |
| initialize(); | |
| } | |
| /* Synchronize windows when notes are enabled */ | |
| function setupNotesSync() { | |
| if (!notesEnabled) return; | |
| function setupPlayResizeSync() { | |
| var out = document.getElementsByClassName('output'); | |
| for (var i = 0; i < out.length; i++) { | |
| $(out[i]).bind('resize', function(event) { | |
| if ($(event.target).hasClass('ui-resizable')) { | |
| localStorage.setItem('play-index', i); | |
| localStorage.setItem('output-style', out[i].style.cssText); | |
| } | |
| }); | |
| } | |
| } | |
| function setupPlayCodeSync() { | |
| var play = document.querySelectorAll('div.playground'); | |
| for (var i = 0; i < play.length; i++) { | |
| play[i].addEventListener('input', inputHandler, false); | |
| function inputHandler(e) { | |
| localStorage.setItem('play-index', i); | |
| localStorage.setItem('play-code', e.target.innerHTML); | |
| } | |
| } | |
| } | |
| setupPlayCodeSync(); | |
| setupPlayResizeSync(); | |
| localStorage.setItem(destSlideKey(), curSlide); | |
| window.addEventListener('storage', updateOtherWindow, false); | |
| } | |
| // An update to local storage is caught only by the other window | |
| // The triggering window does not handle any sync actions | |
| function updateOtherWindow(e) { | |
| // Ignore remove storage events which are not meant to update the other window | |
| var isRemoveStorageEvent = !e.newValue; | |
| if (isRemoveStorageEvent) return; | |
| var destSlide = localStorage.getItem(destSlideKey()); | |
| while (destSlide > curSlide) { | |
| nextSlide(); | |
| } | |
| while (destSlide < curSlide) { | |
| prevSlide(); | |
| } | |
| updatePlay(e); | |
| updateNotes(); | |
| } |