Files
impress.js/src/plugins/impressConsole/impressConsole.js
Henrik Ingo e92e02ccb4 Make impress-console-button actually clickable (#651)
Changed the onclick handler to trigger the impress:console:open event
and not use the impressConsole() global function any more. The latter
is considered deprecated now that impressConsole is integrated into
impress.js itself.

Also catch some errors that appear in event handlers when the target
for the click event was immediately removed from DOM.

Fixes #651
2018-01-01 15:05:55 +02:00

750 lines
29 KiB
JavaScript

/**
* 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': '<div class="noNotes">Keine Notizen hierzu</div>',
'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': '<div class="noNotes">No notes for this step</div>',
'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 = '<!DOCTYPE html>' +
'<html id="impressconsole"><head>' +
// Order is important: If user provides a cssFile, those will win, because they're later
'{{cssStyle}}' +
'{{cssLink}}' +
'</head><body>' +
'<div id="console">' +
'<div id="views">' +
'<iframe id="slideView" scrolling="no"></iframe>' +
'<iframe id="preView" scrolling="no"></iframe>' +
'<div id="blocker"></div>' +
'</div>' +
'<div id="notes"></div>' +
'</div>' +
'<div id="controls"> ' +
'<div id="prev"><a href="#" onclick="impress().prev(); return false;" />' +
'{{prev}}</a></div>' +
'<div id="next"><a href="#" onclick="impress().next(); return false;" />' +
'{{next}}</a></div>' +
'<div id="clock">--:--</div>' +
'<div id="timer" onclick="timerReset()">00m 00s</div>' +
'<div id="status">{{loading}}</div>' +
'</div>' +
'</body></html>';
// 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 =
'<span class="moving">' + lang.moving + '</span>';
}
};
// 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 =
'<span class="ready">' + lang.ready + '</span>';
}
};
// 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',
'<link rel="stylesheet" type="text/css" href="' + cssFileIframe + '">'
);
preView.contentDocument.head.insertAdjacentHTML(
'beforeend',
'<link rel="stylesheet" type="text/css" href="' + cssFileIframe + '">'
);
}
// Chrome:
slideView.addEventListener( 'load', function() {
slideView.contentDocument.body.classList.add( 'impress-console' );
if ( cssFileIframe !== undefined ) {
slideView.contentDocument.head.insertAdjacentHTML(
'beforeend',
'<link rel="stylesheet" type="text/css" href="' +
cssFileIframe + '">'
);
}
} );
preView.addEventListener( 'load', function() {
preView.contentDocument.body.classList.add( 'impress-console' );
if ( cssFileIframe !== undefined ) {
preView.contentDocument.head.insertAdjacentHTML(
'beforeend',
'<link rel="stylesheet" type="text/css" href="' +
cssFileIframe + '">' );
}
} );
};
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 = 'impress-console-button';
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(\'impress-console-button\');' +
'x.parentNode.removeChild(x);var root = document.getElementById(\'' + rootId + '\');' +
'impress(\'' + rootId + '\').lib.util.triggerEvent(root, \'impress:console:open\', {})';
message.innerHTML = '<button style="margin: 25vh 25vw;width:50vw;height:50vh;" ' +
'onclick="' + onClickStr + '">' +
lang.clickToOpen +
'</button>';
document.body.appendChild( message );
return;
}
var cssLink = '';
if ( cssFile !== undefined ) {
cssLink = '<link rel="stylesheet" type="text/css" media="screen" href="' +
cssFile + '">';
}
// 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 <style></style> 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:
// <div id="impress" data-console-css=..." data-console-css-iframe="...">
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:
//<div id="impress" data-console-autolaunch="true">
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 );
};
// New API for impress.js plugins is based on using events
root.addEventListener( 'impress:console:open', function() {
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, _init: _init };
return allConsoles[ rootId ];
};
// This initializes impressConsole automatically when initializing impress itself
document.addEventListener( 'impress:init', function( event ) {
// Note: impressConsole wants the id string, not the DOM element directly
impressConsole( event.target.id )._init();
// Add 'P' to the help popup
triggerEvent( document, 'impress:help:add',
{ command: 'P', text: 'Presenter console', row: 10 } );
} );
// Returns a string to be used inline as a css <style> element in the console window.
// Apologies for length, but hiding it here at the end to keep it away from rest of the code.
var cssStyleStr = function() {
return `<style>
#impressconsole body {
background-color: rgb(255, 255, 255);
padding: 0;
margin: 0;
font-family: verdana, arial, sans-serif;
font-size: 2vw;
}
#impressconsole div#console {
position: absolute;
top: 0.5vw;
left: 0.5vw;
right: 0.5vw;
bottom: 3vw;
margin: 0;
}
#impressconsole div#views, #impressconsole div#notes {
position: absolute;
top: 0;
bottom: 0;
}
#impressconsole div#views {
left: 0;
right: 50vw;
overflow: hidden;
}
#impressconsole div#blocker {
position: absolute;
right: 0;
bottom: 0;
}
#impressconsole div#notes {
left: 50vw;
right: 0;
overflow-x: hidden;
overflow-y: auto;
padding: 0.3ex;
background-color: rgb(255, 255, 255);
border: solid 1px rgb(120, 120, 120);
}
#impressconsole div#notes .noNotes {
color: rgb(200, 200, 200);
}
#impressconsole div#notes p {
margin-top: 0;
}
#impressconsole iframe {
position: absolute;
margin: 0;
padding: 0;
left: 0;
border: solid 1px rgb(120, 120, 120);
}
#impressconsole iframe#slideView {
top: 0;
width: 49vw;
height: 49vh;
}
#impressconsole iframe#preView {
opacity: 0.7;
top: 50vh;
width: 30vw;
height: 30vh;
}
#impressconsole div#controls {
margin: 0;
position: absolute;
bottom: 0.25vw;
left: 0.5vw;
right: 0.5vw;
height: 2.5vw;
background-color: rgb(255, 255, 255);
background-color: rgba(255, 255, 255, 0.6);
}
#impressconsole div#prev, div#next {
}
#impressconsole div#prev a, #impressconsole div#next a {
display: block;
border: solid 1px rgb(70, 70, 70);
border-radius: 0.5vw;
font-size: 1.5vw;
padding: 0.25vw;
text-decoration: none;
background-color: rgb(220, 220, 220);
color: rgb(0, 0, 0);
}
#impressconsole div#prev a:hover, #impressconsole div#next a:hover {
background-color: rgb(245, 245, 245);
}
#impressconsole div#prev {
float: left;
}
#impressconsole div#next {
float: right;
}
#impressconsole div#status {
margin-left: 2em;
margin-right: 2em;
text-align: center;
float: right;
}
#impressconsole div#clock {
margin-left: 2em;
margin-right: 2em;
text-align: center;
float: left;
}
#impressconsole div#timer {
margin-left: 2em;
margin-right: 2em;
text-align: center;
float: left;
}
#impressconsole span.moving {
color: rgb(255, 0, 0);
}
#impressconsole span.ready {
color: rgb(0, 128, 0);
}
</style>`;
};
} )( document, window );