diff --git a/src/plugins/impressConsole/README.md b/src/plugins/impressConsole/README.md new file mode 100644 index 0000000..8fe53d5 --- /dev/null +++ b/src/plugins/impressConsole/README.md @@ -0,0 +1,33 @@ +Impress Console Plugin +====================== + +Press 'P' to show a speaker console window. + +* View of current slide +* Preview of next slide +* Speaker notes (contents of a
element on current slide) +* Navigation + +For speaker notes, add the following anywhere inside a step + +
Speaker notes text...
+ +Example CSS: + + /* Hide notes from the actual presentation. This will not affect the visibility of notes in the impress console window. */ + + .notes { + display: none; + } + + + +Credits +------- + +* Henrik Ingo, henrik.ingo@avoinelama.fi, impress.js (plugin) integration +* Heiko Richler, Aico.Richler@gmx.net, major changes in rev. 1.3 +* Lennart Regebro, regebro@gmail.com, main author of impressConsole +* David Souther, davidsouther@gmail.com, author of the original notes.js + +MIT License diff --git a/src/plugins/impressConsole/impressConsole.js b/src/plugins/impressConsole/impressConsole.js new file mode 100644 index 0000000..4a0b207 --- /dev/null +++ b/src/plugins/impressConsole/impressConsole.js @@ -0,0 +1,747 @@ +/** + * Adds a presenter console to impress.js + * + * MIT Licensed, see license.txt. + * + * Copyright 2012, 2013, 2015 impress-console contributors (see README.txt) + * + * version: 1.3-dev + * + */ + +// This file contains so much HTML, that we will just respectfully disagree about js +/* jshint quotmark:single */ +/* global navigator, top, setInterval, clearInterval, document, window */ + +( function( document, window ) { + 'use strict'; + + // TODO: Move this to src/lib/util.js + var triggerEvent = function( el, eventName, detail ) { + var event = document.createEvent( 'CustomEvent' ); + event.initCustomEvent( eventName, true, true, detail ); + el.dispatchEvent( event ); + }; + + // Create Language object depending on browsers language setting + var lang; + switch ( navigator.language ) { + case 'de': + lang = { + 'noNotes': '
Keine Notizen hierzu
', + 'restart': 'Neustart', + 'clickToOpen': 'Klicken um Sprecherkonsole zu öffnen', + 'prev': 'zurück', + 'next': 'weiter', + 'loading': 'initalisiere', + 'ready': 'Bereit', + 'moving': 'in Bewegung', + 'useAMPM': false + }; + break; + case 'en': // jshint ignore:line + default : // jshint ignore:line + lang = { + 'noNotes': '
No notes for this step
', + 'restart': 'Restart', + 'clickToOpen': 'Click to open speaker console', + 'prev': 'Prev', + 'next': 'Next', + 'loading': 'Loading', + 'ready': 'Ready', + 'moving': 'Moving', + 'useAMPM': false + }; + break; + } + + // Settings to set iframe in speaker console + const preViewDefaultFactor = 0.7; + const preViewMinimumFactor = 0.5; + const preViewGap = 4; + + // This is the default template for the speaker console window + const consoleTemplate = '' + + '' + + + // Order is important: If user provides a cssFile, those will win, because they're later + '{{cssStyle}}' + + '{{cssLink}}' + + '' + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '
' + + '
' + + '
' + + '' + + '' + + '
--:--
' + + '
00m 00s
' + + '
{{loading}}
' + + '
' + + ''; + + // Default css location + var cssFileOldDefault = 'css/impressConsole.css'; + var cssFile = undefined; // jshint ignore:line + + // Css for styling iframs on the console + var cssFileIframeOldDefault = 'css/iframe.css'; + var cssFileIframe = undefined; // jshint ignore:line + + // All console windows, so that you can call impressConsole() repeatedly. + var allConsoles = {}; + + // Zero padding helper function: + var zeroPad = function( i ) { + return ( i < 10 ? '0' : '' ) + i; + }; + + // The console object + var impressConsole = window.impressConsole = function( rootId ) { + + rootId = rootId || 'impress'; + + if ( allConsoles[ rootId ] ) { + return allConsoles[ rootId ]; + } + + // Root presentation elements + var root = document.getElementById( rootId ); + + var consoleWindow = null; + + var nextStep = function() { + var classes = ''; + var nextElement = document.querySelector( '.active' ); + + // Return to parents as long as there is no next sibling + while ( !nextElement.nextElementSibling && nextElement.parentNode ) { + nextElement = nextElement.parentNode; + } + nextElement = nextElement.nextElementSibling; + while ( nextElement ) { + classes = nextElement.attributes[ 'class' ]; + if ( classes && classes.value.indexOf( 'step' ) !== -1 ) { + consoleWindow.document.getElementById( 'blocker' ).innerHTML = lang.next; + return nextElement; + } + + if ( nextElement.firstElementChild ) { // First go into deep + nextElement = nextElement.firstElementChild; + } else { + + // Go to next sibling or through parents until there is a next sibling + while ( !nextElement.nextElementSibling && nextElement.parentNode ) { + nextElement = nextElement.parentNode; + } + nextElement = nextElement.nextElementSibling; + } + } + + // No next element. Pick the first + consoleWindow.document.getElementById( 'blocker' ).innerHTML = lang.restart; + return document.querySelector( '.step' ); + }; + + // Sync the notes to the step + var onStepLeave = function() { + if ( consoleWindow ) { + + // Set notes to next steps notes. + var newNotes = document.querySelector( '.active' ).querySelector( '.notes' ); + if ( newNotes ) { + newNotes = newNotes.innerHTML; + } else { + newNotes = lang.noNotes; + } + consoleWindow.document.getElementById( 'notes' ).innerHTML = newNotes; + + // Set the views + var baseURL = document.URL.substring( 0, document.URL.search( '#/' ) ); + var slideSrc = baseURL + '#' + document.querySelector( '.active' ).id; + var preSrc = baseURL + '#' + nextStep().id; + var slideView = consoleWindow.document.getElementById( 'slideView' ); + + // Setting them when they are already set causes glithes in Firefox, so check first: + if ( slideView.src !== slideSrc ) { + slideView.src = slideSrc; + } + var preView = consoleWindow.document.getElementById( 'preView' ); + if ( preView.src !== preSrc ) { + preView.src = preSrc; + } + + consoleWindow.document.getElementById( 'status' ).innerHTML = + '' + lang.moving + ''; + } + }; + + // Sync the previews to the step + var onStepEnter = function() { + if ( consoleWindow ) { + + // We do everything here again, because if you stopped the previos step to + // early, the onstepleave trigger is not called for that step, so + // we need this to sync things. + var newNotes = document.querySelector( '.active' ).querySelector( '.notes' ); + if ( newNotes ) { + newNotes = newNotes.innerHTML; + } else { + newNotes = lang.noNotes; + } + var notes = consoleWindow.document.getElementById( 'notes' ); + notes.innerHTML = newNotes; + notes.scrollTop = 0; + + // Set the views + var baseURL = document.URL.substring( 0, document.URL.search( '#/' ) ); + var slideSrc = baseURL + '#' + document.querySelector( '.active' ).id; + var preSrc = baseURL + '#' + nextStep().id; + var slideView = consoleWindow.document.getElementById( 'slideView' ); + + // Setting them when they are already set causes glithes in Firefox, so check first: + if ( slideView.src !== slideSrc ) { + slideView.src = slideSrc; + } + var preView = consoleWindow.document.getElementById( 'preView' ); + if ( preView.src !== preSrc ) { + preView.src = preSrc; + } + + consoleWindow.document.getElementById( 'status' ).innerHTML = + '' + lang.ready + ''; + } + }; + + // Sync substeps + var onSubstep = function( event ) { + if ( consoleWindow ) { + if ( event.detail.reason === 'next' ) { + onSubstepShow(); + } + if ( event.detail.reason === 'prev' ) { + onSubstepHide(); + } + } + }; + + var onSubstepShow = function() { + var slideView = consoleWindow.document.getElementById( 'slideView' ); + triggerEventInView( slideView, 'impress:substep:show' ); + }; + + var onSubstepHide = function() { + var slideView = consoleWindow.document.getElementById( 'slideView' ); + triggerEventInView( slideView, 'impress:substep:hide' ); + }; + + var triggerEventInView = function( frame, eventName, detail ) { + + // Note: Unfortunately Chrome does not allow createEvent on file:// URLs, so this won't + // work. This does work on Firefox, and should work if viewing the presentation on a + // http:// URL on Chrome. + var event = frame.contentDocument.createEvent( 'CustomEvent' ); + event.initCustomEvent( eventName, true, true, detail ); + frame.contentDocument.dispatchEvent( event ); + }; + + var spaceHandler = function() { + var notes = consoleWindow.document.getElementById( 'notes' ); + if ( notes.scrollTopMax - notes.scrollTop > 20 ) { + notes.scrollTop = notes.scrollTop + notes.clientHeight * 0.8; + } else { + window.impress().next(); + } + }; + + var timerReset = function() { + consoleWindow.timerStart = new Date(); + }; + + // Show a clock + var clockTick = function() { + var now = new Date(); + var hours = now.getHours(); + var minutes = now.getMinutes(); + var seconds = now.getSeconds(); + var ampm = ''; + + if ( lang.useAMPM ) { + ampm = ( hours < 12 ) ? 'AM' : 'PM'; + hours = ( hours > 12 ) ? hours - 12 : hours; + hours = ( hours === 0 ) ? 12 : hours; + } + + // Clock + var clockStr = zeroPad( hours ) + ':' + zeroPad( minutes ) + ':' + zeroPad( seconds ) + + ' ' + ampm; + consoleWindow.document.getElementById( 'clock' ).firstChild.nodeValue = clockStr; + + // Timer + seconds = Math.floor( ( now - consoleWindow.timerStart ) / 1000 ); + minutes = Math.floor( seconds / 60 ); + seconds = Math.floor( seconds % 60 ); + consoleWindow.document.getElementById( 'timer' ).firstChild.nodeValue = + zeroPad( minutes ) + 'm ' + zeroPad( seconds ) + 's'; + + if ( !consoleWindow.initialized ) { + + // Nudge the slide windows after load, or they will scrolled wrong on Firefox. + consoleWindow.document.getElementById( 'slideView' ).contentWindow.scrollTo( 0, 0 ); + consoleWindow.document.getElementById( 'preView' ).contentWindow.scrollTo( 0, 0 ); + consoleWindow.initialized = true; + } + }; + + var registerKeyEvent = function( keyCodes, handler, window ) { + if ( window === undefined ) { + window = consoleWindow; + } + + // Prevent default keydown action when one of supported key is pressed + window.document.addEventListener( 'keydown', function( event ) { + if ( !event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey && + keyCodes.indexOf( event.keyCode ) !== -1 ) { + event.preventDefault(); + } + }, false ); + + // Trigger impress action on keyup + window.document.addEventListener( 'keyup', function( event ) { + if ( !event.ctrlKey && !event.altKey && !event.shiftKey && !event.metaKey && + keyCodes.indexOf( event.keyCode ) !== -1 ) { + handler(); + event.preventDefault(); + } + }, false ); + }; + + var consoleOnLoad = function() { + var slideView = consoleWindow.document.getElementById( 'slideView' ); + var preView = consoleWindow.document.getElementById( 'preView' ); + + // Firefox: + slideView.contentDocument.body.classList.add( 'impress-console' ); + preView.contentDocument.body.classList.add( 'impress-console' ); + if ( cssFileIframe !== undefined ) { + slideView.contentDocument.head.insertAdjacentHTML( + 'beforeend', + '' + ); + preView.contentDocument.head.insertAdjacentHTML( + 'beforeend', + '' + ); + } + + // Chrome: + slideView.addEventListener( 'load', function() { + slideView.contentDocument.body.classList.add( 'impress-console' ); + if ( cssFileIframe !== undefined ) { + slideView.contentDocument.head.insertAdjacentHTML( + 'beforeend', + '' + ); + } + } ); + preView.addEventListener( 'load', function() { + preView.contentDocument.body.classList.add( 'impress-console' ); + if ( cssFileIframe !== undefined ) { + preView.contentDocument.head.insertAdjacentHTML( + 'beforeend', + '' ); + } + } ); + }; + + var open = function() { + if ( top.isconsoleWindow ) { + return; + } + + if ( consoleWindow && !consoleWindow.closed ) { + consoleWindow.focus(); + } else { + consoleWindow = window.open( '', 'impressConsole' ); + + // If opening failes this may be because the browser prevents this from + // not (or less) interactive JavaScript... + if ( consoleWindow == null ) { + + // ... so I add a button to klick. + // workaround on firefox + var message = document.createElement( 'div' ); + message.id = 'consoleWindowError'; + message.style.position = 'fixed'; + message.style.left = 0; + message.style.top = 0; + message.style.right = 0; + message.style.bottom = 0; + message.style.backgroundColor = 'rgba(255, 255, 255, 0.9)'; + var onClickStr = 'var x = document.getElementById(\'consoleWindowError\');' + + 'x.parentNode.removeChild(x);impressConsole().open();'; + message.innerHTML = ''; + document.body.appendChild( message ); + return; + } + + var cssLink = ''; + if ( cssFile !== undefined ) { + cssLink = ''; + } + + // This sets the window location to the main window location, so css can be loaded: + consoleWindow.document.open(); + + // Write the template: + consoleWindow.document.write( + + // CssStyleStr is lots of inline defined at the end of this file + consoleTemplate.replace( '{{cssStyle}}', cssStyleStr() ) + .replace( '{{cssLink}}', cssLink ) + .replace( /{{.*?}}/gi, function( x ) { + return lang[ x.substring( 2, x.length - 2 ) ]; } + ) + ); + consoleWindow.document.title = 'Speaker Console (' + document.title + ')'; + consoleWindow.impress = window.impress; + + // We set this flag so we can detect it later, to prevent infinite popups. + consoleWindow.isconsoleWindow = true; + + // Set the onload function: + consoleWindow.onload = consoleOnLoad; + + // Add clock tick + consoleWindow.timerStart = new Date(); + consoleWindow.timerReset = timerReset; + consoleWindow.clockInterval = setInterval( allConsoles[ rootId ].clockTick, 1000 ); + + // Keyboard navigation handlers + // 33: pg up, 37: left, 38: up + registerKeyEvent( [ 33, 37, 38 ], window.impress().prev ); + + // 34: pg down, 39: right, 40: down + registerKeyEvent( [ 34, 39, 40 ], window.impress().next ); + + // 32: space + registerKeyEvent( [ 32 ], spaceHandler ); + + // 82: R + registerKeyEvent( [ 82 ], timerReset ); + + // Cleanup + consoleWindow.onbeforeunload = function() { + + // I don't know why onunload doesn't work here. + clearInterval( consoleWindow.clockInterval ); + }; + + // It will need a little nudge on Firefox, but only after loading: + onStepEnter(); + consoleWindow.initialized = false; + consoleWindow.document.close(); + + //Catch any window resize to pass size on + window.onresize = resize; + consoleWindow.onresize = resize; + + return consoleWindow; + } + }; + + var resize = function() { + var slideView = consoleWindow.document.getElementById( 'slideView' ); + var preView = consoleWindow.document.getElementById( 'preView' ); + + // Get ratio of presentation + var ratio = window.innerHeight / window.innerWidth; + + // Get size available for views + var views = consoleWindow.document.getElementById( 'views' ); + + // SlideView may have a border or some padding: + // asuming same border width on both direktions + var delta = slideView.offsetWidth - slideView.clientWidth; + + // Set views + var slideViewWidth = ( views.clientWidth - delta ); + var slideViewHeight = Math.floor( slideViewWidth * ratio ); + + var preViewTop = slideViewHeight + preViewGap; + + var preViewWidth = Math.floor( slideViewWidth * preViewDefaultFactor ); + var preViewHeight = Math.floor( slideViewHeight * preViewDefaultFactor ); + + // Shrink preview to fit into space available + if ( views.clientHeight - delta < preViewTop + preViewHeight ) { + preViewHeight = views.clientHeight - delta - preViewTop; + preViewWidth = Math.floor( preViewHeight / ratio ); + } + + // If preview is not high enough forget ratios! + if ( preViewWidth <= Math.floor( slideViewWidth * preViewMinimumFactor ) ) { + slideViewWidth = ( views.clientWidth - delta ); + slideViewHeight = Math.floor( ( views.clientHeight - delta - preViewGap ) / + ( 1 + preViewMinimumFactor ) ); + + preViewTop = slideViewHeight + preViewGap; + + preViewWidth = Math.floor( slideViewWidth * preViewMinimumFactor ); + preViewHeight = views.clientHeight - delta - preViewTop; + } + + // Set the calculated into styles + slideView.style.width = slideViewWidth + 'px'; + slideView.style.height = slideViewHeight + 'px'; + + preView.style.top = preViewTop + 'px'; + + preView.style.width = preViewWidth + 'px'; + preView.style.height = preViewHeight + 'px'; + }; + + var _init = function( cssConsole, cssIframe ) { + if ( cssConsole !== undefined ) { + cssFile = cssConsole; + } + + // You can also specify the css in the presentation root div: + //
+ else if ( root.dataset.consoleCss !== undefined ) { + cssFile = root.dataset.consoleCss; + } + + if ( cssIframe !== undefined ) { + cssFileIframe = cssIframe; + } else if ( root.dataset.consoleCssIframe !== undefined ) { + cssFileIframe = root.dataset.consoleCssIframe; + } + + // Register the event + root.addEventListener( 'impress:stepleave', onStepLeave ); + root.addEventListener( 'impress:stepenter', onStepEnter ); + root.addEventListener( 'impress:substep:stepleaveaborted', onSubstep ); + root.addEventListener( 'impress:substep:show', onSubstepShow ); + root.addEventListener( 'impress:substep:hide', onSubstepHide ); + + //When the window closes, clean up after ourselves. + window.onunload = function() { + if ( consoleWindow && !consoleWindow.closed ) { + consoleWindow.close(); + } + }; + + //Open speaker console when they press 'p' + registerKeyEvent( [ 80 ], open, window ); + + //Btw, you can also launch console automatically: + //
+ if ( root.dataset.consoleAutolaunch === 'true' ) { + window.open(); + } + }; + + var init = function( cssConsole, cssIframe ) { + if ( ( cssConsole === undefined || cssConsole === cssFileOldDefault ) && + ( cssIframe === undefined || cssIframe === cssFileIframeOldDefault ) ) { + window.console.log( 'impressConsole.init() is deprecated. ' + + 'impressConsole is now initialized automatically when you ' + + 'call impress().init().' ); + } + _init( cssConsole, cssIframe ); + }; + + document.addEventListener( 'impress:init', function() { + _init(); + + // Add 'P' to the help popup + triggerEvent( document, 'impress:help:add', + { command: 'P', text: 'Presenter console', row: 10 } ); + } ); + + // New API for impress.js plugins is based on using events + root.addEventListener( 'impress:console:open', function() { + window.open(); + } ); + + /** + * Register a key code to an event handler + * + * :param: event.detail.keyCodes List of key codes + * :param: event.detail.handler A function registered as the event handler + * :param: event.detail.window The console window to register the keycode in + */ + root.addEventListener( 'impress:console:registerKeyEvent', function( event ) { + registerKeyEvent( event.detail.keyCodes, event.detail.handler, event.detail.window ); + } ); + + // Return the object + allConsoles[ rootId ] = { init: init, open: open, clockTick: clockTick, + registerKeyEvent: registerKeyEvent }; + return allConsoles[ rootId ]; + + }; + + // Returns a string to be used inline as a css `; + }; + + impressConsole(); + +} )( document, window );