From 19c2ab042a80921dcba430d996ddfa7c0a53224c Mon Sep 17 00:00:00 2001 From: Audrey Lim Date: Sun, 3 Apr 2016 16:36:34 -0700 Subject: [PATCH] x/tools/present: display presenter notes and synchronize browser windows Change-Id: If7d5cc52f7594c141060d40e8393ac69cb7ba9ad Reviewed-on: https://go-review.googlesource.com/21488 Reviewed-by: Andrew Gerrand --- cmd/present/static/notes.css | 32 ++++++ cmd/present/static/notes.js | 158 ++++++++++++++++++++++++++++++ cmd/present/static/slides.js | 94 +++++++++++++++--- cmd/present/templates/slides.tmpl | 11 +++ godoc/static/play.js | 51 ++++++---- godoc/static/static.go | 51 ++++++---- 6 files changed, 341 insertions(+), 56 deletions(-) create mode 100644 cmd/present/static/notes.css create mode 100644 cmd/present/static/notes.js diff --git a/cmd/present/static/notes.css b/cmd/present/static/notes.css new file mode 100644 index 0000000000..0b44583795 --- /dev/null +++ b/cmd/present/static/notes.css @@ -0,0 +1,32 @@ +p { + margin: 10px; +} + +#presenter-slides { + display: block; + margin-top: -10px; + margin-left: -17px; + position: fixed; + border: 0; + width : 146%; + height: 750px; + + transform: scale(0.7, 0.7); + transform-origin: top left; + -moz-transform: scale(0.7); + -moz-transform-origin: top left; + -o-transform: scale(0.7); + -o-transform-origin: top left; + -webkit-transform: scale(0.7); + -webkit-transform-origin: top left; +} + +#presenter-notes { + margin-top: -180px; + font-family: 'Open Sans', Arial, sans-serif; + height: 30%; + width: 100%; + overflow: scroll; + position: fixed; + top: 706px; +} diff --git a/cmd/present/static/notes.js b/cmd/present/static/notes.js new file mode 100644 index 0000000000..b584b708bb --- /dev/null +++ b/cmd/present/static/notes.js @@ -0,0 +1,158 @@ +// Copyright 2016 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. + +// Store child window object which will display slides with notes +var notesWindow = null; + +var isParentWindow = window.parent == window; + +// When parent window closes, clear storage and close child window +if (isParentWindow) { + window.onbeforeunload = function() { + localStorage.clear(); + if (notesWindow) notesWindow.close(); + } +}; + +function toggleNotesWindow() { + if (!isParentWindow) return; + if (notesWindow) { + notesWindow.close(); + notesWindow = null; + return; + } + + initNotes(); +}; + +function initNotes() { + notesWindow = window.open('', '', 'width=1000,height=700'); + var w = notesWindow; + var slidesUrl = window.location.href; + + var curSlide = parseInt(localStorage.getItem('destSlide'), 10); + var formattedNotes; + var section = sections[curSlide - 1]; + // curSlide is 0 when initialized from the first page of slides. + // Check if section is valid before retrieving Notes. + if (section) { + formattedNotes = formatNotes(section.Notes); + } + + // Hack to apply css. Requires existing html on notesWindow. + w.document.write("
"); + + w.document.title = window.document.title; + + var slides = w.document.createElement('iframe'); + slides.id = 'presenter-slides'; + slides.src = slidesUrl; + w.document.body.appendChild(slides); + // setTimeout needed for Firefox + setTimeout(function() { + slides.focus(); + }, 100); + + var notes = w.document.createElement('div'); + notes.id = 'presenter-notes'; + notes.innerHTML = formattedNotes; + w.document.body.appendChild(notes); + + w.document.close(); + + function addPresenterNotesStyle() { + var el = w.document.createElement('link'); + el.rel = 'stylesheet'; + el.type = 'text/css'; + el.href = PERMANENT_URL_PREFIX + 'notes.css'; + w.document.body.appendChild(el); + w.document.querySelector('head').appendChild(el); + } + + addPresenterNotesStyle(); + + // Add listener on notesWindow to update notes when triggered from + // parent window + w.addEventListener('storage', updateNotes, false); +}; + + +function formatNotes(notes) { + var formattedNotes = ''; + if (notes) { + for (var i = 0; i < notes.length; i++) { + formattedNotes = formattedNotes + '

' + notes[i] + '

'; + } + } + return formattedNotes; +}; + +function updateNotes() { + // When triggered from parent window, notesWindow is null + // The storage event listener on notesWindow will update notes + if (!notesWindow) return; + var destSlide = parseInt(localStorage.getItem('destSlide'), 10); + var section = sections[destSlide - 1]; + var el = notesWindow.document.getElementById('presenter-notes'); + + if (!el) return; + + if (section && section.Notes) { + el.innerHTML = formatNotes(section.Notes); + } else { + el.innerHTML = ''; + } +}; + +/* Playground syncing */ + +// When presenter notes are enabled, playground click handlers are +// stored here to sync click events on the correct playground +var playgroundHandlers = {onRun: [], onKill: [], onClose: []}; + +function updatePlay(e) { + var i = localStorage.getItem('play-index'); + + switch (e.key) { + case 'play-index': + return; + case 'play-action': + // Sync 'run', 'kill', 'close' actions + var action = localStorage.getItem('play-action'); + playgroundHandlers[action][i](e); + return; + case 'play-code': + // Sync code editing + var play = document.querySelectorAll('div.playground')[i]; + play.innerHTML = localStorage.getItem('play-code'); + return; + case 'output-style': + // Sync resizing of playground output + var out = document.querySelectorAll('.output')[i]; + out.style = localStorage.getItem('output-style'); + return; + } +}; + +// Reset 'run', 'kill', 'close' storage items when synced +// so that successive actions can be synced correctly +function updatePlayStorage(action, index, e) { + localStorage.setItem('play-index', index); + + if (localStorage.getItem('play-action') === action) { + // We're the receiving window, and the message has been received + localStorage.removeItem('play-action'); + } else { + // We're the triggering window, send the message + localStorage.setItem('play-action', action); + } + + if (action === 'onRun') { + if (localStorage.getItem('play-shiftKey') === 'true') { + localStorage.removeItem('play-shiftKey'); + } else if (e.shiftKey) { + localStorage.setItem('play-shiftKey', e.shiftKey); + } + } +}; diff --git a/cmd/present/static/slides.js b/cmd/present/static/slides.js index f5e7861882..ceb651bd47 100644 --- a/cmd/present/static/slides.js +++ b/cmd/present/static/slides.js @@ -14,17 +14,17 @@ 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"))) { +if (typeof document !== 'undefined' && !('classList' in document.createElement('a'))) { (function (view) { var - classListProp = "classList" - , protoProp = "prototype" + classListProp = 'classList' + , protoProp = 'prototype' , elemCtrProto = (view.HTMLElement || view.Element)[protoProp] , objCtr = Object strTrim = String[protoProp].trim || function () { - return this.replace(/^\s+|\s+$/g, ""); + return this.replace(/^\s+|\s+$/g, ''); } , arrIndexOf = Array[protoProp].indexOf || function (item) { for (var i = 0, len = this.length; i < len; i++) { @@ -41,16 +41,16 @@ var this.message = message; } , checkTokenAndGetIndex = function (classList, token) { - if (token === "") { + if (token === '') { throw new DOMEx( - "SYNTAX_ERR" - , "An invalid or illegal string was specified" + 'SYNTAX_ERR' + , 'An invalid or illegal string was specified' ); } if (/\s/.test(token)) { throw new DOMEx( - "INVALID_CHARACTER_ERR" - , "String contains an invalid character" + 'INVALID_CHARACTER_ERR' + , 'String contains an invalid character' ); } return arrIndexOf.call(classList, token); @@ -79,18 +79,18 @@ classListProto.item = function (i) { return this[i] || null; }; classListProto.contains = function (token) { - token += ""; + token += ''; return checkTokenAndGetIndex(this, token) !== -1; }; classListProto.add = function (token) { - token += ""; + token += ''; if (checkTokenAndGetIndex(this, token) === -1) { this.push(token); this._updateClassName(); } }; classListProto.remove = function (token) { - token += ""; + token += ''; var index = checkTokenAndGetIndex(this, token); if (index !== -1) { this.splice(index, 1); @@ -98,7 +98,7 @@ classListProto.remove = function (token) { } }; classListProto.toggle = function (token) { - token += ""; + token += ''; if (checkTokenAndGetIndex(this, token) === -1) { this.add(token); } else { @@ -106,7 +106,7 @@ classListProto.toggle = function (token) { } }; classListProto.toString = function () { - return this.join(" "); + return this.join(' '); }; if (objCtr.defineProperty) { @@ -211,6 +211,8 @@ function prevSlide() { updateSlides(); } + + if (notesEnabled) localStorage.setItem('destSlide', curSlide); }; function nextSlide() { @@ -220,6 +222,8 @@ function nextSlide() { updateSlides(); } + + if (notesEnabled) localStorage.setItem('destSlide', curSlide); }; /* Slide events */ @@ -395,9 +399,12 @@ function updateHash() { function handleBodyKeyDown(event) { // If we're in a code element, only handle pgup/down. - var inCode = event.target.classList.contains("code"); + 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(); @@ -481,11 +488,13 @@ function handleDomLoaded() { setupInteraction(); - if (window.location.hostname == "localhost" || window.location.hostname == "127.0.0.1" || window.location.hostname == "::1") { + 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() { @@ -521,3 +530,56 @@ if (!window['_DEBUG'] && document.location.href.indexOf('?debug') !== -1) { } else { initialize(); } + +/* Synchronize windows when notes are enabled */ + +function setupNotesSync() { + if (!notesEnabled) return; + + function setupPlayResizeSync() { + var out = document.getElementsByClassName('output'); + for (let 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 (let 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('destSlide', 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('destSlide'); + while (destSlide > curSlide) { + nextSlide(); + } + while (destSlide < curSlide) { + prevSlide(); + } + + updatePlay(e); + updateNotes(); +} diff --git a/cmd/present/templates/slides.tmpl b/cmd/present/templates/slides.tmpl index 2b4ec988f5..bc2fa80581 100644 --- a/cmd/present/templates/slides.tmpl +++ b/cmd/present/templates/slides.tmpl @@ -6,7 +6,18 @@ {{.Title}} + + + {{if .NotesEnabled}} + + + {{end}} +