notes plugin now operates entirely through window.postMessage, adding support for file protocol

This commit is contained in:
Hakim El Hattab 2014-04-19 10:54:14 +02:00
parent ce31184bf3
commit 5b18c1f308
2 changed files with 168 additions and 96 deletions

View file

@ -82,6 +82,7 @@
left: 3px; left: 3px;
font-weight: bold; font-weight: bold;
font-size: 14px; font-size: 14px;
z-index: 2;
color: rgba( 255, 255, 255, 0.9 ); color: rgba( 255, 255, 255, 0.9 );
} }
@ -138,22 +139,8 @@
<body> <body>
<script> <div id="wrap-current-slide" class="slides"></div>
function getNotesURL( controls ) { <div id="wrap-next-slide" class="slides"><span>UPCOMING:</span></div>
return window.opener.location.protocol + '//' + window.opener.location.host + window.opener.location.pathname + '?receiver&controls='+ ( controls || 'false' ) +'&progress=false&overview=false' + window.opener.location.hash;
}
var notesCurrentSlideURL = getNotesURL( true );
var notesNextSlideURL = getNotesURL( false );
</script>
<div id="wrap-current-slide" class="slides">
<script>document.write( '<iframe width="1280" height="1024" id="current-slide" src="'+ notesCurrentSlideURL +'"></iframe>' );</script>
</div>
<div id="wrap-next-slide" class="slides">
<script>document.write( '<iframe width="640" height="512" id="next-slide" src="'+ notesNextSlideURL +'"></iframe>' );</script>
<span>UPCOMING:</span>
</div>
<div class="time"> <div class="time">
<div class="clock"> <div class="clock">
@ -171,18 +158,64 @@
<script src="../../plugin/markdown/marked.js"></script> <script src="../../plugin/markdown/marked.js"></script>
<script> <script>
window.addEventListener( 'load', function() { (function() {
if( window.opener && window.opener.location && window.opener.location.href ) { var notes,
currentState,
var notes = document.getElementById( 'notes' ), currentSlide,
currentSlide = document.getElementById( 'current-slide' ), nextSlide,
nextSlide = document.getElementById( 'next-slide' ), connected = false;
silenced = false;
window.addEventListener( 'message', function( event ) { window.addEventListener( 'message', function( event ) {
var data = JSON.parse( event.data ); var data = JSON.parse( event.data );
// Messages sent by the notes plugin inside of the main window
if( data && data.namespace === 'reveal-notes' ) {
if( data.type === 'connect' ) {
handleConnectMessage( data );
}
else if( data.type === 'state' ) {
handleStateMessage( data );
}
}
// Messages sent by the reveal.js inside of the current slide preview
else if( data && data.namespace === 'reveal' ) {
if( /ready/.test( data.eventName ) ) {
// Send a message back to notify that the handshake is complete
window.opener.postMessage( JSON.stringify({ namespace: 'reveal-notes', type: 'connected'} ), '*' );
}
else if( /slidechanged|fragmentshown|fragmenthidden|overviewshown|overviewhidden|paused|resumed/.test( data.eventName ) && currentState !== JSON.stringify( data.state ) ) {
window.opener.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ]} ), '*' );
}
}
} );
/**
* Called when the main window is trying to establish a
* connection.
*/
function handleConnectMessage( data ) {
if( connected === false ) {
connected = true;
setupIframes( data );
setupTimer();
}
}
/**
* Called when the main window sends an updated state.
*/
function handleStateMessage( data ) {
// Store the most recently set state to avoid circular loops
// applying the same state
currentState = JSON.stringify( data.state );
// No need for updating the notes in case of fragment changes // No need for updating the notes in case of fragment changes
if ( data.notes !== undefined) { if ( data.notes !== undefined) {
if( data.markdown ) { if( data.markdown ) {
@ -193,15 +226,44 @@
} }
} }
silenced = true;
// Update the note slides // Update the note slides
currentSlide.contentWindow.Reveal.slide( data.indexh, data.indexv, data.indexf ); currentSlide.contentWindow.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ] }), '*' );
nextSlide.contentWindow.Reveal.slide( data.nextindexh, data.nextindexv ); nextSlide.contentWindow.postMessage( JSON.stringify({ method: 'setState', args: [ data.state ] }), '*' );
nextSlide.contentWindow.postMessage( JSON.stringify({ method: 'next' }), '*' );
silenced = false; }
}, false ); /**
* Creates the preview iframes.
*/
function setupIframes( data ) {
notes = document.getElementById( 'notes' );
var url = data.url + '?receiver&progress=false&overview=false&history=false';
var hash = '#/' + data.state.indexh + '/' + data.state.indexv;
currentSlide = document.createElement( 'iframe' );
currentSlide.setAttribute( 'id', 'current-slide' );
currentSlide.setAttribute( 'width', 1280 );
currentSlide.setAttribute( 'height', 1024 );
currentSlide.setAttribute( 'src', url + '&postMessageEvents=true' + hash );
document.querySelector( '#wrap-current-slide' ).appendChild( currentSlide );
nextSlide = document.createElement( 'iframe' );
nextSlide.setAttribute( 'id', 'next-slide' );
nextSlide.setAttribute( 'width', 640 );
nextSlide.setAttribute( 'height', 512 );
nextSlide.setAttribute( 'src', url + '&controls=false' + hash );
document.querySelector( '#wrap-next-slide' ).appendChild( nextSlide );
}
/**
* Create the timer and clock and start updating them
* at an interval.
*/
function setupTimer() {
var start = new Date(), var start = new Date(),
timeEl = document.querySelector( '.time' ), timeEl = document.querySelector( '.time' ),
@ -224,44 +286,24 @@
clockEl.innerHTML = now.toLocaleTimeString(); clockEl.innerHTML = now.toLocaleTimeString();
hoursEl.innerHTML = zeroPadInteger( hours ); hoursEl.innerHTML = zeroPadInteger( hours );
hoursEl.className = hours > 0 ? "" : "mute"; hoursEl.className = hours > 0 ? '' : 'mute';
minutesEl.innerHTML = ":" + zeroPadInteger( minutes ); minutesEl.innerHTML = ':' + zeroPadInteger( minutes );
minutesEl.className = minutes > 0 ? "" : "mute"; minutesEl.className = minutes > 0 ? '' : 'mute';
secondsEl.innerHTML = ":" + zeroPadInteger( seconds ); secondsEl.innerHTML = ':' + zeroPadInteger( seconds );
}, 1000 ); }, 1000 );
// Broadcasts the state of the notes window to synchronize
// the main window
function synchronizeMainWindow() {
if( !silenced ) {
var indices = currentSlide.contentWindow.Reveal.getIndices();
window.opener.Reveal.slide( indices.h, indices.v, indices.f );
} }
}
// Navigate the main window when the notes slide changes
currentSlide.contentWindow.Reveal.addEventListener( 'slidechanged', synchronizeMainWindow );
currentSlide.contentWindow.Reveal.addEventListener( 'fragmentshown', synchronizeMainWindow );
currentSlide.contentWindow.Reveal.addEventListener( 'fragmenthidden', synchronizeMainWindow );
}
else {
document.body.innerHTML = '<p class="error">Unable to access <code>window.opener.location</code>.<br>Make sure the presentation is running on a web server.</p>';
}
}, false );
function zeroPadInteger( num ) { function zeroPadInteger( num ) {
var str = "00" + parseInt( num );
var str = '00' + parseInt( num );
return str.substring( str.length - 2 ); return str.substring( str.length - 2 );
} }
})();
</script> </script>
</body> </body>
</html> </html>

View file

@ -1,6 +1,13 @@
/** /**
* Handles opening of and synchronization with the reveal.js * Handles opening of and synchronization with the reveal.js
* notes window. * notes window.
*
* Handshake process:
* 1. This window posts 'connect' to notes window
* - Includes URL of presentation to show
* 2. Notes window responds with 'connected' when it is available
* 3. This window proceeds to send the current presentation state
* to the notes window
*/ */
var RevealNotes = (function() { var RevealNotes = (function() {
@ -9,41 +16,46 @@ var RevealNotes = (function() {
jsFileLocation = jsFileLocation.replace(/notes\.js(\?.*)?$/, ''); // the js folder path jsFileLocation = jsFileLocation.replace(/notes\.js(\?.*)?$/, ''); // the js folder path
var notesPopup = window.open( jsFileLocation + 'notes.html', 'reveal.js - Notes', 'width=1120,height=850' ); var notesPopup = window.open( jsFileLocation + 'notes.html', 'reveal.js - Notes', 'width=1120,height=850' );
// Fires when slide is changed /**
Reveal.addEventListener( 'slidechanged', post ); * Connect to the notes window through a postmessage handshake.
* Using postmessage enables us to work in situations where the
* origins differ, such as a presentation being opened from the
* file system.
*/
function connect() {
// Keep trying to connect until we get a 'connected' message back
var connectInterval = setInterval( function() {
notesPopup.postMessage( JSON.stringify( {
namespace: 'reveal-notes',
type: 'connect',
url: window.location.protocol + '//' + window.location.host + window.location.pathname,
state: Reveal.getState()
} ), '*' );
}, 500 );
// Fires when a fragment is shown window.addEventListener( 'message', function( event ) {
Reveal.addEventListener( 'fragmentshown', post ); var data = JSON.parse( event.data );
if( data && data.namespace === 'reveal-notes' && data.type === 'connected' ) {
// Fires when a fragment is hidden clearInterval( connectInterval );
Reveal.addEventListener( 'fragmenthidden', post ); onConnected();
}
} );
}
/** /**
* Posts the current slide data to the notes window * Posts the current slide data to the notes window
*/ */
function post() { function post() {
var slideElement = Reveal.getCurrentSlide(),
slideIndices = Reveal.getIndices(),
notesElement = slideElement.querySelector( 'aside.notes' ),
nextindexh,
nextindexv;
if( slideElement.nextElementSibling && slideElement.parentNode.nodeName == 'SECTION' ) { var slideElement = Reveal.getCurrentSlide(),
nextindexh = slideIndices.h; notesElement = slideElement.querySelector( 'aside.notes' );
nextindexv = slideIndices.v + 1;
} else {
nextindexh = slideIndices.h + 1;
nextindexv = 0;
}
var messageData = { var messageData = {
notes : '', namespace: 'reveal-notes',
indexh : slideIndices.h, type: 'state',
indexv : slideIndices.v, notes: '',
indexf : slideIndices.f, markdown: false,
nextindexh : nextindexh, state: Reveal.getState()
nextindexv : nextindexv,
markdown : false
}; };
// Look for notes defined in a slide attribute // Look for notes defined in a slide attribute
@ -58,12 +70,30 @@ var RevealNotes = (function() {
} }
notesPopup.postMessage( JSON.stringify( messageData ), '*' ); notesPopup.postMessage( JSON.stringify( messageData ), '*' );
} }
// Navigate to the current slide when the notes are loaded /**
notesPopup.addEventListener( 'load', function( event ) { * Called once we have established a connection to the notes
* window.
*/
function onConnected() {
// Monitor events that trigger a change in state
Reveal.addEventListener( 'slidechanged', post );
Reveal.addEventListener( 'fragmentshown', post );
Reveal.addEventListener( 'fragmenthidden', post );
Reveal.addEventListener( 'overviewhidden', post );
Reveal.addEventListener( 'overviewshown', post );
Reveal.addEventListener( 'paused', post );
Reveal.addEventListener( 'resumed', post );
// Post the initial state
post(); post();
}, false );
}
connect();
} }
// If the there's a 'notes' query set, open directly // If the there's a 'notes' query set, open directly