// This file was automatically generated from files in src/ directory. /*! Licensed under MIT License - http://github.com/impress/impress.js */ /** * impress.js * * impress.js is a presentation tool based on the power of CSS3 transforms and transitions * in modern browsers and inspired by the idea behind prezi.com. * * * Copyright 2011-2012 Bartek Szopka (@bartaz), 2016-2023 Henrik Ingo (@henrikingo) * and 70+ other contributors * * Released under the MIT License. * * ------------------------------------------------ * author: Bartek Szopka, Henrik Ingo * version: 2.0.0 * url: http://impress.js.org * source: http://github.com/impress/impress.js/ */ // You are one of those who like to know how things work inside? // Let me show you the cogs that make impress.js run... ( function( document, window ) { "use strict"; var lib; // HELPER FUNCTIONS // `pfx` is a function that takes a standard CSS property name as a parameter // and returns it's prefixed version valid for current browser it runs in. // The code is heavily inspired by Modernizr http://www.modernizr.com/ var pfx = ( function() { var style = document.createElement( "dummy" ).style, prefixes = "Webkit Moz O ms Khtml".split( " " ), memory = {}; return function( prop ) { if ( typeof memory[ prop ] === "undefined" ) { var ucProp = prop.charAt( 0 ).toUpperCase() + prop.substr( 1 ), props = ( prop + " " + prefixes.join( ucProp + " " ) + ucProp ).split( " " ); memory[ prop ] = null; for ( var i in props ) { if ( style[ props[ i ] ] !== undefined ) { memory[ prop ] = props[ i ]; break; } } } return memory[ prop ]; }; } )(); var validateOrder = function( order, fallback ) { var validChars = "xyz"; var returnStr = ""; if ( typeof order === "string" ) { for ( var i in order.split( "" ) ) { if ( validChars.indexOf( order[ i ] ) >= 0 ) { returnStr += order[ i ]; // Each of x,y,z can be used only once. validChars = validChars.split( order[ i ] ).join( "" ); } } } if ( returnStr ) { return returnStr; } else if ( fallback !== undefined ) { return fallback; } else { return "xyz"; } }; // `css` function applies the styles given in `props` object to the element // given as `el`. It runs all property names through `pfx` function to make // sure proper prefixed version of the property is used. var css = function( el, props ) { var key, pkey; for ( key in props ) { if ( props.hasOwnProperty( key ) ) { pkey = pfx( key ); if ( pkey !== null ) { el.style[ pkey ] = props[ key ]; } } } return el; }; // `translate` builds a translate transform string for given data. var translate = function( t ) { return " translate3d(" + t.x + "px," + t.y + "px," + t.z + "px) "; }; // `rotate` builds a rotate transform string for given data. // By default the rotations are in X Y Z order that can be reverted by passing `true` // as second parameter. var rotate = function( r, revert ) { var order = r.order ? r.order : "xyz"; var css = ""; var axes = order.split( "" ); if ( revert ) { axes = axes.reverse(); } for ( var i = 0; i < axes.length; i++ ) { css += " rotate" + axes[ i ].toUpperCase() + "(" + r[ axes[ i ] ] + "deg)"; } return css; }; // `scale` builds a scale transform string for given data. var scale = function( s ) { return " scale(" + s + ") "; }; // `computeWindowScale` counts the scale factor between window size and size // defined for the presentation in the config. var computeWindowScale = function( config ) { var hScale = window.innerHeight / config.height, wScale = window.innerWidth / config.width, scale = hScale > wScale ? wScale : hScale; if ( config.maxScale && scale > config.maxScale ) { scale = config.maxScale; } if ( config.minScale && scale < config.minScale ) { scale = config.minScale; } return scale; }; // CHECK SUPPORT var body = document.body; var impressSupported = // Browser should support CSS 3D transtorms ( pfx( "perspective" ) !== null ) && // And `classList` and `dataset` APIs ( body.classList ) && ( body.dataset ); if ( !impressSupported ) { // We can't be sure that `classList` is supported body.className += " impress-not-supported "; } // GLOBALS AND DEFAULTS // This is where the root elements of all impress.js instances will be kept. // Yes, this means you can have more than one instance on a page, but I'm not // sure if it makes any sense in practice ;) var roots = {}; var preInitPlugins = []; var preStepLeavePlugins = []; // Some default config values. var defaults = { width: 1920, height: 1080, maxScale: 3, minScale: 0, perspective: 1000, transitionDuration: 1000 }; // Configuration options var config = null; // It's just an empty function ... and a useless comment. var empty = function() { return false; }; // IMPRESS.JS API // And that's where interesting things will start to happen. // It's the core `impress` function that returns the impress.js API // for a presentation based on the element with given id ("impress" // by default). var impress = window.impress = function( rootId ) { // If impress.js is not supported by the browser return a dummy API // it may not be a perfect solution but we return early and avoid // running code that may use features not implemented in the browser. if ( !impressSupported ) { return { init: empty, goto: empty, prev: empty, next: empty, swipe: empty, tear: empty, lib: {} }; } rootId = rootId || "impress"; // If given root is already initialized just return the API if ( roots[ "impress-root-" + rootId ] ) { return roots[ "impress-root-" + rootId ]; } // The gc library depends on being initialized before we do any changes to DOM. lib = initLibraries( rootId ); body.classList.remove( "impress-not-supported" ); body.classList.add( "impress-supported" ); // Data of all presentation steps var stepsData = {}; // Element of currently active step var activeStep = null; // Current state (position, rotation and scale) of the presentation var currentState = null; // Array of step elements var steps = null; // Scale factor of the browser window var windowScale = null; // Root presentation elements var root = lib.util.byId( rootId ); var canvas = document.createElement( "div" ); var initialized = false; // STEP EVENTS // // There are currently two step events triggered by impress.js // `impress:stepenter` is triggered when the step is shown on the // screen (the transition from the previous one is finished) and // `impress:stepleave` is triggered when the step is left (the // transition to next step just starts). // Reference to last entered step var lastEntered = null; // `onStepEnter` is called whenever the step element is entered // but the event is triggered only if the step is different than // last entered step. // We sometimes call `goto`, and therefore `onStepEnter`, just to redraw a step, such as // after screen resize. In this case - more precisely, in any case - we trigger a // `impress:steprefresh` event. var onStepEnter = function( step ) { if ( lastEntered !== step ) { lib.util.triggerEvent( step, "impress:stepenter" ); lastEntered = step; } lib.util.triggerEvent( step, "impress:steprefresh" ); }; // `onStepLeave` is called whenever the currentStep element is left // but the event is triggered only if the currentStep is the same as // lastEntered step. var onStepLeave = function( currentStep, nextStep ) { if ( lastEntered === currentStep ) { lib.util.triggerEvent( currentStep, "impress:stepleave", { next: nextStep } ); lastEntered = null; } }; // `initStep` initializes given step element by reading data from its // data attributes and setting correct styles. var initStep = function( el, idx ) { var data = el.dataset, step = { translate: { x: lib.util.toNumberAdvanced( data.x ), y: lib.util.toNumberAdvanced( data.y ), z: lib.util.toNumberAdvanced( data.z ) }, rotate: { x: lib.util.toNumber( data.rotateX ), y: lib.util.toNumber( data.rotateY ), z: lib.util.toNumber( data.rotateZ || data.rotate ), order: validateOrder( data.rotateOrder ) }, scale: lib.util.toNumber( data.scale, 1 ), transitionDuration: lib.util.toNumber( data.transitionDuration, config.transitionDuration ), el: el }; if ( !el.id ) { el.id = "step-" + ( idx + 1 ); } stepsData[ "impress-" + el.id ] = step; css( el, { position: "absolute", transform: "translate(-50%,-50%)" + translate( step.translate ) + rotate( step.rotate ) + scale( step.scale ), transformStyle: "preserve-3d" } ); }; // Initialize all steps. // Read the data-* attributes, store in internal stepsData, and render with CSS. var initAllSteps = function() { steps = lib.util.$$( ".step", root ); steps.forEach( initStep ); }; // Build configuration from root and defaults var buildConfig = function() { var rootData = root.dataset; return { width: lib.util.toNumber( rootData.width, defaults.width ), height: lib.util.toNumber( rootData.height, defaults.height ), maxScale: lib.util.toNumber( rootData.maxScale, defaults.maxScale ), minScale: lib.util.toNumber( rootData.minScale, defaults.minScale ), perspective: lib.util.toNumber( rootData.perspective, defaults.perspective ), transitionDuration: lib.util.toNumber( rootData.transitionDuration, defaults.transitionDuration ) }; }; // `init` API function that initializes (and runs) the presentation. var init = function() { if ( initialized ) { return; } // Initialize the configuration object, so it can be used by pre-init plugins. config = buildConfig(); execPreInitPlugins( root ); // First we set up the viewport for mobile devices. // For some reason iPad goes nuts when it is not done properly. var meta = lib.util.$( "meta[name='viewport']" ) || document.createElement( "meta" ); meta.content = "width=device-width, minimum-scale=1, maximum-scale=1, user-scalable=no"; if ( meta.parentNode !== document.head ) { meta.name = "viewport"; document.head.appendChild( meta ); } windowScale = computeWindowScale( config ); // Wrap steps with "canvas" element lib.util.arrayify( root.childNodes ).forEach( function( el ) { canvas.appendChild( el ); } ); root.appendChild( canvas ); // Set initial styles document.documentElement.style.height = "100%"; css( body, { height: "100%", overflow: "hidden" } ); var rootStyles = { position: "absolute", transformOrigin: "top left", transition: "all 0s ease-in-out", transformStyle: "preserve-3d" }; css( root, rootStyles ); css( root, { top: "50%", left: "50%", perspective: ( config.perspective / windowScale ) + "px", transform: scale( windowScale ) } ); css( canvas, rootStyles ); body.classList.remove( "impress-disabled" ); body.classList.add( "impress-enabled" ); // Get and init steps initAllSteps(); // Set a default initial state of the canvas currentState = { translate: { x: 0, y: 0, z: 0 }, rotate: { x: 0, y: 0, z: 0, order: "xyz" }, scale: 1 }; initialized = true; lib.util.triggerEvent( root, "impress:init", { api: roots[ "impress-root-" + rootId ] } ); }; // `getStep` is a helper function that returns a step element defined by parameter. // If a number is given, step with index given by the number is returned, if a string // is given step element with such id is returned, if DOM element is given it is returned // if it is a correct step element. var getStep = function( step ) { if ( typeof step === "number" ) { step = step < 0 ? steps[ steps.length + step ] : steps[ step ]; } else if ( typeof step === "string" ) { step = lib.util.byId( step ); } return ( step && step.id && stepsData[ "impress-" + step.id ] ) ? step : null; }; // Used to reset timeout for `impress:stepenter` event var stepEnterTimeout = null; // `goto` API function that moves to step given as `el` parameter (by index, id or element). // `duration` optionally given as second parameter, is the transition duration in css. // `reason` is the string "next", "prev" or "goto" (default) and will be made available to // preStepLeave plugins. // `origEvent` may contain event that caused the call to goto, such as a key press event var goto = function( el, duration, reason, origEvent ) { reason = reason || "goto"; origEvent = origEvent || null; if ( !initialized ) { return false; } // Re-execute initAllSteps for each transition. This allows to edit step attributes // dynamically, such as change their coordinates, or even remove or add steps, and have // that change apply when goto() is called. initAllSteps(); if ( !( el = getStep( el ) ) ) { return false; } // Sometimes it's possible to trigger focus on first link with some keyboard action. // Browser in such a case tries to scroll the page to make this element visible // (even that body overflow is set to hidden) and it breaks our careful positioning. // // So, as a lousy (and lazy) workaround we will make the page scroll back to the top // whenever slide is selected // // If you are reading this and know any better way to handle it, I'll be glad to hear // about it! window.scrollTo( 0, 0 ); var step = stepsData[ "impress-" + el.id ]; duration = ( duration !== undefined ? duration : step.transitionDuration ); // If we are in fact moving to another step, start with executing the registered // preStepLeave plugins. if ( activeStep && activeStep !== el ) { var event = { target: activeStep, detail: {} }; event.detail.next = el; event.detail.transitionDuration = duration; event.detail.reason = reason; if ( origEvent ) { event.origEvent = origEvent; } if ( execPreStepLeavePlugins( event ) === false ) { // PreStepLeave plugins are allowed to abort the transition altogether, by // returning false. // see stop and substep plugins for an example of doing just that return false; } // Plugins are allowed to change the detail values el = event.detail.next; step = stepsData[ "impress-" + el.id ]; duration = event.detail.transitionDuration; } if ( activeStep ) { activeStep.classList.remove( "active" ); body.classList.remove( "impress-on-" + activeStep.id ); } el.classList.add( "active" ); body.classList.add( "impress-on-" + el.id ); // Compute target state of the canvas based on given step var target = { rotate: { x: -step.rotate.x, y: -step.rotate.y, z: -step.rotate.z, order: step.rotate.order }, translate: { x: -step.translate.x, y: -step.translate.y, z: -step.translate.z }, scale: 1 / step.scale }; // Check if the transition is zooming in or not. // // This information is used to alter the transition style: // when we are zooming in - we start with move and rotate transition // and the scaling is delayed, but when we are zooming out we start // with scaling down and move and rotation are delayed. var zoomin = target.scale >= currentState.scale; duration = lib.util.toNumber( duration, config.transitionDuration ); var delay = ( duration / 2 ); // If the same step is re-selected, force computing window scaling, // because it is likely to be caused by window resize if ( el === activeStep ) { windowScale = computeWindowScale( config ); } var targetScale = target.scale * windowScale; // Trigger leave of currently active element (if it's not the same step again) if ( activeStep && activeStep !== el ) { onStepLeave( activeStep, el ); } // Now we alter transforms of `root` and `canvas` to trigger transitions. // // And here is why there are two elements: `root` and `canvas` - they are // being animated separately: // `root` is used for scaling and `canvas` for translate and rotations. // Transitions on them are triggered with different delays (to make // visually nice and "natural" looking transitions), so we need to know // that both of them are finished. css( root, { // To keep the perspective look similar for different scales // we need to "scale" the perspective, too // For IE 11 support we must specify perspective independent // of transform. perspective: ( config.perspective / targetScale ) + "px", transform: scale( targetScale ), transitionDuration: duration + "ms", transitionDelay: ( zoomin ? delay : 0 ) + "ms" } ); css( canvas, { transform: rotate( target.rotate, true ) + translate( target.translate ), transitionDuration: duration + "ms", transitionDelay: ( zoomin ? 0 : delay ) + "ms" } ); // Here is a tricky part... // // If there is no change in scale or no change in rotation and translation, it means // there was actually no delay - because there was no transition on `root` or `canvas` // elements. We want to trigger `impress:stepenter` event in the correct moment, so // here we compare the current and target values to check if delay should be taken into // account. // // I know that this `if` statement looks scary, but it's pretty simple when you know // what is going on - it's simply comparing all the values. if ( currentState.scale === target.scale || ( currentState.rotate.x === target.rotate.x && currentState.rotate.y === target.rotate.y && currentState.rotate.z === target.rotate.z && currentState.translate.x === target.translate.x && currentState.translate.y === target.translate.y && currentState.translate.z === target.translate.z ) ) { delay = 0; } // Store current state currentState = target; activeStep = el; // And here is where we trigger `impress:stepenter` event. // We simply set up a timeout to fire it taking transition duration (and possible delay) // into account. // // I really wanted to make it in more elegant way. The `transitionend` event seemed to // be the best way to do it, but the fact that I'm using transitions on two separate // elements and that the `transitionend` event is only triggered when there was a // transition (change in the values) caused some bugs and made the code really // complicated, cause I had to handle all the conditions separately. And it still // needed a `setTimeout` fallback for the situations when there is no transition at all. // So I decided that I'd rather make the code simpler than use shiny new // `transitionend`. // // If you want learn something interesting and see how it was done with `transitionend` // go back to version 0.5.2 of impress.js: // http://github.com/bartaz/impress.js/blob/0.5.2/js/impress.js window.clearTimeout( stepEnterTimeout ); stepEnterTimeout = window.setTimeout( function() { onStepEnter( activeStep ); }, duration + delay ); return el; }; // `prev` API function goes to previous step (in document order) // `event` is optional, may contain the event that caused the need to call prev() var prev = function( origEvent ) { var prev = steps.indexOf( activeStep ) - 1; prev = prev >= 0 ? steps[ prev ] : steps[ steps.length - 1 ]; return goto( prev, undefined, "prev", origEvent ); }; // `next` API function goes to next step (in document order) // `event` is optional, may contain the event that caused the need to call next() var next = function( origEvent ) { var next = steps.indexOf( activeStep ) + 1; next = next < steps.length ? steps[ next ] : steps[ 0 ]; return goto( next, undefined, "next", origEvent ); }; // Swipe for touch devices by @and3rson. // Below we extend the api to control the animation between the currently // active step and a presumed next/prev step. See touch plugin for // an example of using this api. // Helper function var interpolate = function( a, b, k ) { return a + ( b - a ) * k; }; // Animate a swipe. // // Pct is a value between -1.0 and +1.0, designating the current length // of the swipe. // // If pct is negative, swipe towards the next() step, if positive, // towards the prev() step. // // Note that pre-stepleave plugins such as goto can mess with what is a // next() and prev() step, so we need to trigger the pre-stepleave event // here, even if a swipe doesn't guarantee that the transition will // actually happen. // // Calling swipe(), with any value of pct, won't in itself cause a // transition to happen, this is just to animate the swipe. Once the // transition is committed - such as at a touchend event - caller is // responsible for also calling prev()/next() as appropriate. // // Note: For now, this function is made available to be used by the swipe plugin (which // is the UI counterpart to this). It is a semi-internal API and intentionally not // documented in DOCUMENTATION.md. var swipe = function( pct ) { if ( Math.abs( pct ) > 1 ) { return; } // Prepare & execute the preStepLeave event var event = { target: activeStep, detail: {} }; event.detail.swipe = pct; // Will be ignored within swipe animation, but just in case a plugin wants to read this, // humor them event.detail.transitionDuration = config.transitionDuration; var idx; // Needed by jshint if ( pct < 0 ) { idx = steps.indexOf( activeStep ) + 1; event.detail.next = idx < steps.length ? steps[ idx ] : steps[ 0 ]; event.detail.reason = "next"; } else if ( pct > 0 ) { idx = steps.indexOf( activeStep ) - 1; event.detail.next = idx >= 0 ? steps[ idx ] : steps[ steps.length - 1 ]; event.detail.reason = "prev"; } else { // No move return; } if ( execPreStepLeavePlugins( event ) === false ) { // If a preStepLeave plugin wants to abort the transition, don't animate a swipe // For stop, this is probably ok. For substep, the plugin it self might want to do // some animation, but that's not the current implementation. return false; } var nextElement = event.detail.next; var nextStep = stepsData[ "impress-" + nextElement.id ]; // If the same step is re-selected, force computing window scaling, var nextScale = nextStep.scale * windowScale; var k = Math.abs( pct ); var interpolatedStep = { translate: { x: interpolate( currentState.translate.x, -nextStep.translate.x, k ), y: interpolate( currentState.translate.y, -nextStep.translate.y, k ), z: interpolate( currentState.translate.z, -nextStep.translate.z, k ) }, rotate: { x: interpolate( currentState.rotate.x, -nextStep.rotate.x, k ), y: interpolate( currentState.rotate.y, -nextStep.rotate.y, k ), z: interpolate( currentState.rotate.z, -nextStep.rotate.z, k ), // Unfortunately there's a discontinuity if rotation order changes. Nothing I // can do about it? order: k < 0.7 ? currentState.rotate.order : nextStep.rotate.order }, scale: interpolate( currentState.scale * windowScale, nextScale, k ) }; css( root, { // To keep the perspective look similar for different scales // we need to 'scale' the perspective, too perspective: config.perspective / interpolatedStep.scale + "px", transform: scale( interpolatedStep.scale ), transitionDuration: "0ms", transitionDelay: "0ms" } ); css( canvas, { transform: rotate( interpolatedStep.rotate, true ) + translate( interpolatedStep.translate ), transitionDuration: "0ms", transitionDelay: "0ms" } ); }; // Teardown impress // Resets the DOM to the state it was before impress().init() was called. // (If you called impress(rootId).init() for multiple different rootId's, then you must // also call tear() once for each of them.) var tear = function() { lib.gc.teardown(); delete roots[ "impress-root-" + rootId ]; }; // Adding some useful classes to step elements. // // All the steps that have not been shown yet are given `future` class. // When the step is entered the `future` class is removed and the `present` // class is given. When the step is left `present` class is replaced with // `past` class. // // So every step element is always in one of three possible states: // `future`, `present` and `past`. // // There classes can be used in CSS to style different types of steps. // For example the `present` class can be used to trigger some custom // animations when step is shown. lib.gc.addEventListener( root, "impress:init", function() { // STEP CLASSES steps.forEach( function( step ) { step.classList.add( "future" ); } ); lib.gc.addEventListener( root, "impress:stepenter", function( event ) { event.target.classList.remove( "past" ); event.target.classList.remove( "future" ); event.target.classList.add( "present" ); }, false ); lib.gc.addEventListener( root, "impress:stepleave", function( event ) { event.target.classList.remove( "present" ); event.target.classList.add( "past" ); }, false ); }, false ); // Adding hash change support. lib.gc.addEventListener( root, "impress:init", function() { // Last hash detected var lastHash = ""; // `#/step-id` is used instead of `#step-id` to prevent default browser // scrolling to element in hash. // // And it has to be set after animation finishes, because in Chrome it // makes transition laggy. // BUG: http://code.google.com/p/chromium/issues/detail?id=62820 lib.gc.addEventListener( root, "impress:stepenter", function( event ) { window.location.hash = lastHash = "#/" + event.target.id; }, false ); lib.gc.addEventListener( window, "hashchange", function() { // When the step is entered hash in the location is updated // (just few lines above from here), so the hash change is // triggered and we would call `goto` again on the same element. // // To avoid this we store last entered hash and compare. if ( window.location.hash !== lastHash ) { goto( lib.util.getElementFromHash() ); } }, false ); // START // by selecting step defined in url or first step of the presentation goto( lib.util.getElementFromHash() || steps[ 0 ], 0 ); }, false ); body.classList.add( "impress-disabled" ); // Store and return API for given impress.js root element return ( roots[ "impress-root-" + rootId ] = { init: init, goto: goto, next: next, prev: prev, swipe: swipe, tear: tear, lib: lib } ); }; // Flag that can be used in JS to check if browser have passed the support test impress.supported = impressSupported; // ADD and INIT LIBRARIES // Library factories are defined in src/lib/*.js, and register themselves by calling // impress.addLibraryFactory(libraryFactoryObject). They're stored here, and used to augment // the API with library functions when client calls impress(rootId). // See src/lib/README.md for clearer example. // (Advanced usage: For different values of rootId, a different instance of the libaries are // generated, in case they need to hold different state for different root elements.) var libraryFactories = {}; impress.addLibraryFactory = function( obj ) { for ( var libname in obj ) { if ( obj.hasOwnProperty( libname ) ) { libraryFactories[ libname ] = obj[ libname ]; } } }; // Call each library factory, and return the lib object that is added to the api. var initLibraries = function( rootId ) { //jshint ignore:line var lib = {}; for ( var libname in libraryFactories ) { if ( libraryFactories.hasOwnProperty( libname ) ) { if ( lib[ libname ] !== undefined ) { throw "impress.js ERROR: Two libraries both tried to use libname: " + libname; } lib[ libname ] = libraryFactories[ libname ]( rootId ); } } return lib; }; // `addPreInitPlugin` allows plugins to register a function that should // be run (synchronously) at the beginning of init, before // impress().init() itself executes. impress.addPreInitPlugin = function( plugin, weight ) { weight = parseInt( weight ) || 10; if ( weight <= 0 ) { throw "addPreInitPlugin: weight must be a positive integer"; } if ( preInitPlugins[ weight ] === undefined ) { preInitPlugins[ weight ] = []; } preInitPlugins[ weight ].push( plugin ); }; // Called at beginning of init, to execute all pre-init plugins. var execPreInitPlugins = function( root ) { //jshint ignore:line for ( var i = 0; i < preInitPlugins.length; i++ ) { var thisLevel = preInitPlugins[ i ]; if ( thisLevel !== undefined ) { for ( var j = 0; j < thisLevel.length; j++ ) { thisLevel[ j ]( root, roots[ "impress-root-" + root.id ] ); } } } }; // `addPreStepLeavePlugin` allows plugins to register a function that should // be run (synchronously) at the beginning of goto() impress.addPreStepLeavePlugin = function( plugin, weight ) { //jshint ignore:line weight = parseInt( weight ) || 10; if ( weight <= 0 ) { throw "addPreStepLeavePlugin: weight must be a positive integer"; } if ( preStepLeavePlugins[ weight ] === undefined ) { preStepLeavePlugins[ weight ] = []; } preStepLeavePlugins[ weight ].push( plugin ); }; impress.getConfig = function() { return config; }; // Called at beginning of goto(), to execute all preStepLeave plugins. var execPreStepLeavePlugins = function( event ) { //jshint ignore:line for ( var i = 0; i < preStepLeavePlugins.length; i++ ) { var thisLevel = preStepLeavePlugins[ i ]; if ( thisLevel !== undefined ) { for ( var j = 0; j < thisLevel.length; j++ ) { if ( thisLevel[ j ]( event ) === false ) { // If a plugin returns false, the stepleave event (and related transition) // is aborted return false; } } } } }; } )( document, window ); // THAT'S ALL FOLKS! // // Thanks for reading it all. // Or thanks for scrolling down and reading the last part. // // I've learnt a lot when building impress.js and I hope this code and comments // will help somebody learn at least some part of it. /** * Garbage collection utility * * This library allows plugins to add elements and event listeners they add to the DOM. The user * can call `impress().lib.gc.teardown()` to cause all of them to be removed from DOM, so that * the document is in the state it was before calling `impress().init()`. * * In addition to just adding elements and event listeners to the garbage collector, plugins * can also register callback functions to do arbitrary cleanup upon teardown. * * Henrik Ingo (c) 2016 * MIT License */ ( function( document, window ) { "use strict"; var roots = []; var rootsCount = 0; var startingState = { roots: [] }; var libraryFactory = function( rootId ) { if ( roots[ rootId ] ) { return roots[ rootId ]; } // Per root global variables (instance variables?) var elementList = []; var eventListenerList = []; var callbackList = []; recordStartingState( rootId ); // LIBRARY FUNCTIONS // Definitions of the library functions we return as an object at the end // `pushElement` adds a DOM element to the gc stack var pushElement = function( element ) { elementList.push( element ); }; // `appendChild` is a convenience wrapper that combines DOM appendChild with gc.pushElement var appendChild = function( parent, element ) { parent.appendChild( element ); pushElement( element ); }; // `pushEventListener` adds an event listener to the gc stack var pushEventListener = function( target, type, listenerFunction ) { eventListenerList.push( { target:target, type:type, listener:listenerFunction } ); }; // `addEventListener` combines DOM addEventListener with gc.pushEventListener var addEventListener = function( target, type, listenerFunction ) { target.addEventListener( type, listenerFunction ); pushEventListener( target, type, listenerFunction ); }; // `pushCallback` If the above utilities are not enough, plugins can add their own callback // function to do arbitrary things. var pushCallback = function( callback ) { callbackList.push( callback ); }; pushCallback( function( rootId ) { resetStartingState( rootId ); } ); // `teardown` will // - execute all callbacks in LIFO order // - call `removeChild` on all DOM elements in LIFO order // - call `removeEventListener` on all event listeners in LIFO order // The goal of a teardown is to return to the same state that the DOM was before // `impress().init()` was called. var teardown = function() { // Execute the callbacks in LIFO order var i; // Needed by jshint for ( i = callbackList.length - 1; i >= 0; i-- ) { callbackList[ i ]( rootId ); } callbackList = []; for ( i = 0; i < elementList.length; i++ ) { elementList[ i ].parentElement.removeChild( elementList[ i ] ); } elementList = []; for ( i = 0; i < eventListenerList.length; i++ ) { var target = eventListenerList[ i ].target; var type = eventListenerList[ i ].type; var listener = eventListenerList[ i ].listener; target.removeEventListener( type, listener ); } }; var lib = { pushElement: pushElement, appendChild: appendChild, pushEventListener: pushEventListener, addEventListener: addEventListener, pushCallback: pushCallback, teardown: teardown }; roots[ rootId ] = lib; rootsCount++; return lib; }; // Let impress core know about the existence of this library window.impress.addLibraryFactory( { gc: libraryFactory } ); // CORE INIT // The library factory (gc(rootId)) is called at the beginning of impress(rootId).init() // For the purposes of teardown(), we can use this as an opportunity to save the state // of a few things in the DOM in their virgin state, before impress().init() did anything. // Note: These could also be recorded by the code in impress.js core as these values // are changed, but in an effort to not deviate too much from upstream, I'm adding // them here rather than the core itself. var recordStartingState = function( rootId ) { startingState.roots[ rootId ] = {}; startingState.roots[ rootId ].steps = []; // Record whether the steps have an id or not var steps = document.getElementById( rootId ).querySelectorAll( ".step" ); for ( var i = 0; i < steps.length; i++ ) { var el = steps[ i ]; startingState.roots[ rootId ].steps.push( { el: el, id: el.getAttribute( "id" ) } ); } // In the rare case of multiple roots, the following is changed on first init() and // reset at last tear(). if ( rootsCount === 0 ) { startingState.body = {}; // It is customary for authors to set body.class="impress-not-supported" as a starting // value, which can then be removed by impress().init(). But it is not required. // Remember whether it was there or not. if ( document.body.classList.contains( "impress-not-supported" ) ) { startingState.body.impressNotSupported = true; } else { startingState.body.impressNotSupported = false; } // If there's a element, its contents will be overwritten by init var metas = document.head.querySelectorAll( "meta" ); for ( i = 0; i < metas.length; i++ ) { var m = metas[ i ]; if ( m.name === "viewport" ) { startingState.meta = m.content; } } } }; // CORE TEARDOWN var resetStartingState = function( rootId ) { // Reset body element document.body.classList.remove( "impress-enabled" ); document.body.classList.remove( "impress-disabled" ); var root = document.getElementById( rootId ); var activeId = root.querySelector( ".active" ).id; document.body.classList.remove( "impress-on-" + activeId ); document.documentElement.style.height = ""; document.body.style.height = ""; document.body.style.overflow = ""; // Remove style values from the root and step elements // Note: We remove the ones set by impress.js core. Otoh, we didn't preserve any original // values. A more sophisticated implementation could keep track of original values and then // reset those. var steps = root.querySelectorAll( ".step" ); for ( var i = 0; i < steps.length; i++ ) { steps[ i ].classList.remove( "future" ); steps[ i ].classList.remove( "past" ); steps[ i ].classList.remove( "present" ); steps[ i ].classList.remove( "active" ); steps[ i ].style.position = ""; steps[ i ].style.transform = ""; steps[ i ].style[ "transform-style" ] = ""; } root.style.position = ""; root.style[ "transform-origin" ] = ""; root.style.transition = ""; root.style[ "transform-style" ] = ""; root.style.top = ""; root.style.left = ""; root.style.transform = ""; // Reset id of steps ("step-1" id's are auto generated) steps = startingState.roots[ rootId ].steps; var step; while ( step = steps.pop() ) { if ( step.id === null ) { step.el.removeAttribute( "id" ); } else { step.el.setAttribute( "id", step.id ); } } delete startingState.roots[ rootId ]; // Move step div elements away from canvas, then delete canvas // Note: There's an implicit assumption here that the canvas div is the only child element // of the root div. If there would be something else, it's gonna be lost. var canvas = root.firstChild; var canvasHTML = canvas.innerHTML; root.innerHTML = canvasHTML; if ( roots[ rootId ] !== undefined ) { delete roots[ rootId ]; rootsCount--; } if ( rootsCount === 0 ) { // In the rare case that more than one impress root elements were initialized, these // are only reset when all are uninitialized. document.body.classList.remove( "impress-supported" ); if ( startingState.body.impressNotSupported ) { document.body.classList.add( "impress-not-supported" ); } // We need to remove or reset the meta element inserted by impress.js var metas = document.head.querySelectorAll( "meta" ); for ( i = 0; i < metas.length; i++ ) { var m = metas[ i ]; if ( m.name === "viewport" ) { if ( startingState.meta !== undefined ) { m.content = startingState.meta; } else { m.parentElement.removeChild( m ); } } } } }; } )( document, window ); /** * Common utility functions * * Copyright 2011-2012 Bartek Szopka (@bartaz) * Henrik Ingo (c) 2016 * MIT License */ ( function( document, window ) { "use strict"; var roots = []; var libraryFactory = function( rootId ) { if ( roots[ rootId ] ) { return roots[ rootId ]; } // `$` returns first element for given CSS `selector` in the `context` of // the given element or whole document. var $ = function( selector, context ) { context = context || document; return context.querySelector( selector ); }; // `$$` return an array of elements for given CSS `selector` in the `context` of // the given element or whole document. var $$ = function( selector, context ) { context = context || document; return arrayify( context.querySelectorAll( selector ) ); }; // `arrayify` takes an array-like object and turns it into real Array // to make all the Array.prototype goodness available. var arrayify = function( a ) { return [].slice.call( a ); }; // `byId` returns element with given `id` - you probably have guessed that ;) var byId = function( id ) { return document.getElementById( id ); }; // `getElementFromHash` returns an element located by id from hash part of // window location. var getElementFromHash = function() { // Get id from url # by removing `#` or `#/` from the beginning, // so both "fallback" `#slide-id` and "enhanced" `#/slide-id` will work var encoded = window.location.hash.replace( /^#\/?/, "" ); return byId( decodeURIComponent( encoded ) ); }; // `getUrlParamValue` return a given URL parameter value if it exists // `undefined` if it doesn't exist var getUrlParamValue = function( parameter ) { var chunk = window.location.search.split( parameter + "=" )[ 1 ]; var value = chunk && chunk.split( "&" )[ 0 ]; if ( value !== "" ) { return value; } }; // Throttling function calls, by Remy Sharp // http://remysharp.com/2010/07/21/throttling-function-calls/ var throttle = function( fn, delay ) { var timer = null; return function() { var context = this, args = arguments; window.clearTimeout( timer ); timer = window.setTimeout( function() { fn.apply( context, args ); }, delay ); }; }; // `toNumber` takes a value given as `numeric` parameter and tries to turn // it into a number. If it is not possible it returns 0 (or other value // given as `fallback`). var toNumber = function( numeric, fallback ) { return isNaN( numeric ) ? ( fallback || 0 ) : Number( numeric ); }; /** * Extends toNumber() to correctly compute also relative-to-screen-size values 5w and 5h. * * Returns the computed value in pixels with w/h postfix removed. */ var toNumberAdvanced = function( numeric, fallback ) { if ( typeof numeric !== "string" ) { return toNumber( numeric, fallback ); } var ratio = numeric.match( /^([+-]*[\d\.]+)([wh])$/ ); if ( ratio == null ) { return toNumber( numeric, fallback ); } else { var value = parseFloat( ratio[ 1 ] ); var config = window.impress.getConfig(); var multiplier = ratio[ 2 ] === "w" ? config.width : config.height; return value * multiplier; } }; // `triggerEvent` builds a custom DOM event with given `eventName` and `detail` data // and triggers it on element given as `el`. var triggerEvent = function( el, eventName, detail ) { var event = document.createEvent( "CustomEvent" ); event.initCustomEvent( eventName, true, true, detail ); el.dispatchEvent( event ); }; var lib = { $: $, $$: $$, arrayify: arrayify, byId: byId, getElementFromHash: getElementFromHash, throttle: throttle, toNumber: toNumber, toNumberAdvanced: toNumberAdvanced, triggerEvent: triggerEvent, getUrlParamValue: getUrlParamValue }; roots[ rootId ] = lib; return lib; }; // Let impress core know about the existence of this library window.impress.addLibraryFactory( { util: libraryFactory } ); } )( document, window ); /** * Helper functions for rotation. * * Tommy Tam (c) 2021 * MIT License */ ( function( document, window ) { "use strict"; // Singleton library variables var roots = []; var libraryFactory = function( rootId ) { if ( roots[ "impress-root-" + rootId ] ) { return roots[ "impress-root-" + rootId ]; } /** * Round the number to 2 decimals, it's enough for use */ var roundNumber = function( num ) { return Math.round( ( num + Number.EPSILON ) * 100 ) / 100; }; /** * Get the length/norm of a vector. * * https://en.wikipedia.org/wiki/Norm_(mathematics) */ var vectorLength = function( vec ) { return Math.sqrt( vec.x * vec.x + vec.y * vec.y + vec.z * vec.z ); }; /** * Dot product of two vectors. * * https://en.wikipedia.org/wiki/Dot_product */ var vectorDotProd = function( vec1, vec2 ) { return vec1.x * vec2.x + vec1.y * vec2.y + vec1.z * vec2.z; }; /** * Cross product of two vectors. * * https://en.wikipedia.org/wiki/Cross_product */ var vectorCrossProd = function( vec1, vec2 ) { return { x: vec1.y * vec2.z - vec1.z * vec2.y, y: vec1.z * vec2.x - vec1.x * vec2.z, z: vec1.x * vec2.y - vec1.y * vec2.x }; }; /** * Determine wheter a vector is a zero vector */ var isZeroVector = function( vec ) { return !roundNumber( vec.x ) && !roundNumber( vec.y ) && !roundNumber( vec.z ); }; /** * Scalar triple product of three vectors. * * It can be used to determine the handness of vectors. * * https://en.wikipedia.org/wiki/Triple_product#Scalar_triple_product */ var tripleProduct = function( vec1, vec2, vec3 ) { return vectorDotProd( vectorCrossProd( vec1, vec2 ), vec3 ); }; /** * The world/absolute unit coordinates. * * This coordinate is used by browser to position objects. * It will not be affected by object rotations. * All relative positions will finally be converted to this * coordinate to be used. */ var worldUnitCoordinate = { x: { x:1, y:0, z:0 }, y: { x:0, y:1, z:0 }, z: { x:0, y:0, z:1 } }; /** * Make quaternion from rotation axis and angle. * * q = [ cos(½θ), sin(½θ) axis ] * * If the angle is zero, returns the corresponded quaternion * of axis. * * If the angle is not zero, returns the rotating quaternion * which corresponds to rotation about the axis, by the angle θ. * * https://en.wikipedia.org/wiki/Quaternion * https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation */ var makeQuaternion = function( axis, theta = 0 ) { var r = 0; var t = 1; if ( theta ) { var radians = theta * Math.PI / 180; r = Math.cos( radians / 2 ); t = Math.sin( radians / 2 ) / vectorLength( axis ); } var q = [ r, axis.x * t, axis.y * t, axis.z * t ]; return q; }; /** * Extract vector from quaternion */ var quaternionToVector = function( quaternion ) { return { x: roundNumber( quaternion[ 1 ] ), y: roundNumber( quaternion[ 2 ] ), z: roundNumber( quaternion[ 3 ] ) }; }; /** * Returns the conjugate quaternion of a quaternion * * https://en.wikipedia.org/wiki/Quaternion#Conjugation,_the_norm,_and_reciprocal */ var conjugateQuaternion = function( quaternion ) { return [ quaternion[ 0 ], -quaternion[ 1 ], -quaternion[ 2 ], -quaternion[ 3 ] ]; }; /** * Left multiple two quaternion. * * Is's used to combine two rotating quaternion into one. */ var leftMulQuaternion = function( q1, q2 ) { return [ ( q1[ 0 ] * q2[ 0 ] - q1[ 1 ] * q2[ 1 ] - q1[ 2 ] * q2[ 2 ] - q1[ 3 ] * q2[ 3 ] ), ( q1[ 1 ] * q2[ 0 ] + q1[ 0 ] * q2[ 1 ] - q1[ 3 ] * q2[ 2 ] + q1[ 2 ] * q2[ 3 ] ), ( q1[ 2 ] * q2[ 0 ] + q1[ 3 ] * q2[ 1 ] + q1[ 0 ] * q2[ 2 ] - q1[ 1 ] * q2[ 3 ] ), ( q1[ 3 ] * q2[ 0 ] - q1[ 2 ] * q2[ 1 ] + q1[ 1 ] * q2[ 2 ] + q1[ 0 ] * q2[ 3 ] ) ]; }; /** * Convert a rotation into a quaternion */ var rotationToQuaternion = function( baseCoordinate, rotation ) { var order = rotation.order ? rotation.order : "xyz"; var axes = order.split( "" ); var result = [ 1, 0, 0, 0 ]; for ( var i = 0; i < axes.length; i++ ) { var deg = rotation[ axes[ i ] ]; if ( !deg || ( Math.abs( deg ) < 0.0001 ) ) { continue; } // All CSS rotation is based on the rotated coordinate // So we need to calculate the rotated coordinate first var coordinate = baseCoordinate; if ( i > 0 ) { coordinate = { x: rotateByQuaternion( baseCoordinate.x, result ), y: rotateByQuaternion( baseCoordinate.y, result ), z: rotateByQuaternion( baseCoordinate.z, result ) }; } result = leftMulQuaternion( makeQuaternion( coordinate[ axes[ i ] ], deg ), result ); } return result; }; /** * Rotate a vector by a quaternion. */ var rotateByQuaternion = function( vec, quaternion ) { var q = makeQuaternion( vec ); q = leftMulQuaternion( leftMulQuaternion( quaternion, q ), conjugateQuaternion( quaternion ) ); return quaternionToVector( q ); }; /** * Rotate a vector by rotaion sequence. */ var rotateVector = function( baseCoordinate, vec, rotation ) { var quaternion = rotationToQuaternion( baseCoordinate, rotation ); return rotateByQuaternion( vec, quaternion ); }; /** * Given a rotation, return the rotationed coordinate */ var rotateCoordinate = function( coordinate, rotation ) { var quaternion = rotationToQuaternion( coordinate, rotation ); return { x: rotateByQuaternion( coordinate.x, quaternion ), y: rotateByQuaternion( coordinate.y, quaternion ), z: rotateByQuaternion( coordinate.z, quaternion ) }; }; /** * Return the angle between two vector. * * The axis is used to determine the rotation direction. */ var angleBetweenTwoVector = function( axis, vec1, vec2 ) { var vecLen1 = vectorLength( vec1 ); var vecLen2 = vectorLength( vec2 ); if ( !vecLen1 || !vecLen2 ) { return 0; } var cos = vectorDotProd( vec1, vec2 ) / vecLen1 / vecLen2 ; var angle = Math.acos( cos ) * 180 / Math.PI; if ( tripleProduct( vec1, vec2, axis ) > 0 ) { return angle; } else { return -angle; } }; /** * Return the angle between a vector and a plane. * * The plane is determined by an axis and a vector on the plane. */ var angleBetweenPlaneAndVector = function( axis, planeVec, rotatedVec ) { var norm = vectorCrossProd( axis, planeVec ); if ( isZeroVector( norm ) ) { return 0; } return 90 - angleBetweenTwoVector( axis, rotatedVec, norm ); }; /** * Calculated a order specified rotation sequence to * transform from the world coordinate to required coordinate. */ var coordinateToOrderedRotation = function( coordinate, order ) { var axis0 = order[ 0 ]; var axis1 = order[ 1 ]; var axis2 = order[ 2 ]; var reversedOrder = order.split( "" ).reverse().join( "" ); var rotate2 = angleBetweenPlaneAndVector( coordinate[ axis2 ], worldUnitCoordinate[ axis0 ], coordinate[ axis0 ] ); // The r2 is the reverse of rotate for axis2 // The coordinate1 is the coordinate before rotate of axis2 var r2 = { order: reversedOrder }; r2[ axis2 ] = -rotate2; var coordinate1 = rotateCoordinate( coordinate, r2 ); // Calculate the rotation for axis1 var rotate1 = angleBetweenTwoVector( coordinate1[ axis1 ], worldUnitCoordinate[ axis0 ], coordinate1[ axis0 ] ); // Calculate the rotation for axis0 var rotate0 = angleBetweenTwoVector( worldUnitCoordinate[ axis0 ], worldUnitCoordinate[ axis1 ], coordinate1[ axis1 ] ); var rotation = { }; rotation.order = order; rotation[ axis0 ] = roundNumber( rotate0 ); rotation[ axis1 ] = roundNumber( rotate1 ); rotation[ axis2 ] = roundNumber( rotate2 ); return rotation; }; /** * Returns the possible rotations from unit coordinate * to specified coordinate. */ var possibleRotations = function( coordinate ) { var orders = [ "xyz", "xzy", "yxz", "yzx", "zxy", "zyx" ]; var rotations = [ ]; for ( var i = 0; i < orders.length; ++i ) { rotations.push( coordinateToOrderedRotation( coordinate, orders[ i ] ) ); } return rotations; }; /** * Calculate a degree which in range (-180, 180] of baseDeg */ var nearestAngle = function( baseDeg, deg ) { while ( deg > baseDeg + 180 ) { deg -= 360; } while ( deg < baseDeg - 180 ) { deg += 360; } return deg; }; /** * Given a base rotation and multiple rotations, return the best one. * * The best one is the one has least rotate from base. */ var bestRotation = function( baseRotate, rotations ) { var bestScore; var bestRotation; for ( var i = 0; i < rotations.length; ++i ) { var rotation = { order: rotations[ i ].order, x: nearestAngle( baseRotate.x, rotations[ i ].x ), y: nearestAngle( baseRotate.y, rotations[ i ].y ), z: nearestAngle( baseRotate.z, rotations[ i ].z ) }; var score = Math.abs( rotation.x - baseRotate.x ) + Math.abs( rotation.y - baseRotate.y ) + Math.abs( rotation.z - baseRotate.z ); if ( !i || ( score < bestScore ) ) { bestScore = score; bestRotation = rotation; } } return bestRotation; }; /** * Given a coordinate, return the best rotation to achieve it. * * The baseRotate is used to select the near rotation from it. */ var coordinateToRotation = function( baseRotate, coordinate ) { var rotations = possibleRotations( coordinate ); return bestRotation( baseRotate, rotations ); }; /** * Apply a relative rotation to the base rotation. * * Calculate the coordinate after the rotation on each axis, * and finally find out a one step rotation has the effect * of two rotation. * * If there're multiple way to accomplish, select the one * that is nearest to the base. * * Return one rotation has the same effect. */ var combineRotations = function( rotations ) { // No rotation if ( rotations.length <= 0 ) { return { x:0, y:0, z:0, order:"xyz" }; } // Find out the base coordinate var coordinate = worldUnitCoordinate; // One by one apply rotations in order for ( var i = 0; i < rotations.length; i++ ) { coordinate = rotateCoordinate( coordinate, rotations[ i ] ); } // Calculate one rotation from unit coordinate to rotated // coordinate. Because there're multiple possibles, // select the one nearest to the base var rotate = coordinateToRotation( rotations[ 0 ], coordinate ); return rotate; }; var translateRelative = function( relative, prevRotation ) { var result = rotateVector( worldUnitCoordinate, relative, prevRotation ); result.rotate = combineRotations( [ prevRotation, relative.rotate ] ); return result; }; var lib = { translateRelative: translateRelative }; roots[ "impress-root-" + rootId ] = lib; return lib; }; // Let impress core know about the existence of this library window.impress.addLibraryFactory( { rotation: libraryFactory } ); } )( document, window ); /** * Autoplay plugin - Automatically advance slideshow after N seconds * * Copyright 2016 Henrik Ingo, henrik.ingo@avoinelama.fi * Released under the MIT license. */ /* global clearTimeout, setTimeout, document */ ( function( document ) { "use strict"; var autoplayDefault = 0; var currentStepTimeout = 0; var api = null; var timeoutHandle = null; var root = null; var util; // On impress:init, check whether there is a default setting, as well as // handle step-1. document.addEventListener( "impress:init", function( event ) { util = event.detail.api.lib.util; // Getting API from event data instead of global impress().init(). // You don't even need to know what is the id of the root element // or anything. `impress:init` event data gives you everything you // need to control the presentation that was just initialized. api = event.detail.api; root = event.target; // Element attributes starting with "data-", become available under // element.dataset. In addition hyphenized words become camelCased. var data = root.dataset; var autoplay = util.getUrlParamValue( "impress-autoplay" ) || data.autoplay; if ( autoplay ) { autoplayDefault = util.toNumber( autoplay, 0 ); } var toolbar = document.querySelector( "#impress-toolbar" ); if ( toolbar ) { addToolbarButton( toolbar ); } api.lib.gc.pushCallback( function() { clearTimeout( timeoutHandle ); } ); // Note that right after impress:init event, also impress:stepenter is // triggered for the first slide, so that's where code flow continues. }, false ); document.addEventListener( "impress:autoplay:pause", function( event ) { status = "paused"; reloadTimeout( event ); }, false ); document.addEventListener( "impress:autoplay:play", function( event ) { status = "playing"; reloadTimeout( event ); }, false ); // If default autoplay time was defined in the presentation root, or // in this step, set timeout. var reloadTimeout = function( event ) { var step = event.target; currentStepTimeout = util.toNumber( step.dataset.autoplay, autoplayDefault ); if ( status === "paused" ) { setAutoplayTimeout( 0 ); } else { setAutoplayTimeout( currentStepTimeout ); } }; document.addEventListener( "impress:stepenter", function( event ) { reloadTimeout( event ); }, false ); document.addEventListener( "impress:substep:enter", function( event ) { reloadTimeout( event ); }, false ); /** * Set timeout after which we move to next() step. */ var setAutoplayTimeout = function( timeout ) { if ( timeoutHandle ) { clearTimeout( timeoutHandle ); } if ( timeout > 0 ) { timeoutHandle = setTimeout( function() { api.next(); }, timeout * 1000 ); } setButtonText(); }; /*** Toolbar plugin integration *******************************************/ var status = "not clicked"; var toolbarButton = null; var makeDomElement = function( html ) { var tempDiv = document.createElement( "div" ); tempDiv.innerHTML = html; return tempDiv.firstChild; }; var toggleStatus = function() { if ( currentStepTimeout > 0 && status !== "paused" ) { status = "paused"; } else { status = "playing"; } }; var getButtonText = function() { if ( currentStepTimeout > 0 && status !== "paused" ) { return "||"; // Pause } else { return "▶"; // Play } }; var setButtonText = function() { if ( toolbarButton ) { // Keep button size the same even if label content is changing var buttonWidth = toolbarButton.offsetWidth; var buttonHeight = toolbarButton.offsetHeight; toolbarButton.innerHTML = getButtonText(); if ( !toolbarButton.style.width ) { toolbarButton.style.width = buttonWidth + "px"; } if ( !toolbarButton.style.height ) { toolbarButton.style.height = buttonHeight + "px"; } } }; var addToolbarButton = function( toolbar ) { var html = '"; // jshint ignore:line toolbarButton = makeDomElement( html ); toolbarButton.addEventListener( "click", function() { toggleStatus(); if ( status === "playing" ) { if ( autoplayDefault === 0 ) { autoplayDefault = 7; } if ( currentStepTimeout === 0 ) { currentStepTimeout = autoplayDefault; } setAutoplayTimeout( currentStepTimeout ); } else if ( status === "paused" ) { setAutoplayTimeout( 0 ); } } ); util.triggerEvent( toolbar, "impress:toolbar:appendChild", { group: 10, element: toolbarButton } ); }; } )( document ); /** * Blackout plugin * * Press b or . to hide all slides, and b or . again to show them. * Also navigating to a different slide will show them again (impress:stepleave). * * Copyright 2014 @Strikeskids * Released under the MIT license. */ /* global document */ ( function( document ) { "use strict"; var canvas = null; var blackedOut = false; var util = null; var root = null; var api = null; // While waiting for a shared library of utilities, copying these 2 from main impress.js var css = function( el, props ) { var key, pkey; for ( key in props ) { if ( props.hasOwnProperty( key ) ) { pkey = pfx( key ); if ( pkey !== null ) { el.style[ pkey ] = props[ key ]; } } } return el; }; var pfx = ( function() { var style = document.createElement( "dummy" ).style, prefixes = "Webkit Moz O ms Khtml".split( " " ), memory = {}; return function( prop ) { if ( typeof memory[ prop ] === "undefined" ) { var ucProp = prop.charAt( 0 ).toUpperCase() + prop.substr( 1 ), props = ( prop + " " + prefixes.join( ucProp + " " ) + ucProp ).split( " " ); memory[ prop ] = null; for ( var i in props ) { if ( style[ props[ i ] ] !== undefined ) { memory[ prop ] = props[ i ]; break; } } } return memory[ prop ]; }; } )(); var removeBlackout = function() { if ( blackedOut ) { css( canvas, { display: "block" } ); blackedOut = false; util.triggerEvent( root, "impress:autoplay:play", {} ); } }; var blackout = function() { if ( blackedOut ) { removeBlackout(); } else { css( canvas, { display: ( blackedOut = !blackedOut ) ? "none" : "block" } ); blackedOut = true; util.triggerEvent( root, "impress:autoplay:pause", {} ); } }; // Wait for impress.js to be initialized document.addEventListener( "impress:init", function( event ) { api = event.detail.api; util = api.lib.util; root = event.target; canvas = root.firstElementChild; var gc = api.lib.gc; gc.addEventListener( document, "keydown", function( event ) { // Accept b or . -> . is sent by presentation remote controllers if ( event.keyCode === 66 || event.keyCode === 190 ) { event.preventDefault(); if ( !blackedOut ) { blackout(); } else { removeBlackout(); } } }, false ); gc.addEventListener( document, "keyup", function( event ) { // Accept b or . -> . is sent by presentation remote controllers if ( event.keyCode === 66 || event.keyCode === 190 ) { event.preventDefault(); } }, false ); }, false ); document.addEventListener( "impress:stepleave", function() { removeBlackout(); }, false ); } )( document ); /** * Extras Plugin * * This plugin performs initialization (like calling mermaid.initialize()) * for the extras/ plugins if they are loaded into a presentation. * * See README.md for details. * * Copyright 2016 Henrik Ingo (@henrikingo) * Released under the MIT license. */ /* global markdown, marked, hljs, mermaid, impress */ ( function( document, window ) { "use strict"; const SLIDE_SEPARATOR = /^-----$/m; const getMarkdownParser = function( ) { if ( window.hasOwnProperty( "marked" ) ) { // Using marked return function( elem, src ) { return marked.parse( src ); }; } else if ( window.hasOwnProperty( "markdown" ) ) { // Using builtin markdown engine return function( elem, src ) { var dialect = elem.dataset.markdownDialect; return markdown.toHTML( src, dialect ); }; } return null; }; const getMarkdownSlides = function( elem ) { var text = elem.textContent; // Using first not blank line to detect leading whitespaces. // can't properly handle the mixing of space and tabs var m = text.match( /^([ \t]*)\S/m ); if ( m !== null ) { text = text.replace( new RegExp( "^" + m[ 1 ], "mg" ), "" ); } return text.split( SLIDE_SEPARATOR ); }; const convertMarkdowns = function( selector ) { // Detect markdown engine var parseMarkdown = getMarkdownParser(); if ( !parseMarkdown ) { return; } for ( var elem of document.querySelectorAll( selector ) ) { var id = null; if ( elem.id ) { id = elem.id; elem.id = ""; } var origTitle = null; if ( elem.title ) { origTitle = elem.title; elem.title = ""; } var slides = getMarkdownSlides( elem ); var slideElems = [ elem ]; for ( var j = 1; j < slides.length; ++j ) { var newElem = elem.cloneNode( false ); newElem.id = ""; elem.parentNode.insertBefore( newElem, slideElems[ 0 ] ); slideElems.splice( 0, 0, newElem ); } if ( id ) { slideElems[ 0 ].id = id; } for ( var i = 0; i < slides.length; ++i ) { slideElems[ i ].innerHTML = parseMarkdown( slideElems[ i ], slides[ i ] ); if ( origTitle && ( i === 0 ) ) { slideElems[ i ].title = origTitle; } } } }; var preInit = function() { // Query all .markdown elements and translate to HTML convertMarkdowns( ".markdown" ); if ( window.hljs ) { hljs.initHighlightingOnLoad(); } if ( window.mermaid ) { mermaid.initialize( { startOnLoad:true } ); } }; // Register the plugin to be called in pre-init phase // Note: Markdown.js should run early/first, because it creates new div elements. // So add this with a lower-than-default weight. impress.addPreInitPlugin( preInit, 1 ); } )( document, window ); /** * Form support * * Functionality to better support use of input, textarea, button... elements in a presentation. * * This plugin does two things: * * Set stopPropagation on any element that might take text input. This allows users to type, for * example, the letter 'P' into a form field, without causing the presenter console to spring up. * * On impress:stepleave, de-focus any potentially active * element. This is to prevent the focus from being left in a form element that is no longer visible * in the window, and user therefore typing garbage into the form. * * TODO: Currently it is not possible to use TAB to navigate between form elements. Impress.js, and * in particular the navigation plugin, unfortunately must fully take control of the tab key, * otherwise a user could cause the browser to scroll to a link or button that's not on the current * step. However, it could be possible to allow tab navigation between form elements, as long as * they are on the active step. This is a topic for further study. * * Copyright 2016 Henrik Ingo * MIT License */ /* global document */ ( function( document ) { "use strict"; var root; var api; document.addEventListener( "impress:init", function( event ) { root = event.target; api = event.detail.api; var gc = api.lib.gc; var selectors = [ "input", "textarea", "select", "[contenteditable=true]" ]; for ( var selector of selectors ) { var elements = document.querySelectorAll( selector ); if ( !elements ) { continue; } for ( var i = 0; i < elements.length; i++ ) { var e = elements[ i ]; gc.addEventListener( e, "keydown", function( event ) { event.stopPropagation(); } ); gc.addEventListener( e, "keyup", function( event ) { event.stopPropagation(); } ); } } }, false ); document.addEventListener( "impress:stepleave", function() { document.activeElement.blur(); }, false ); } )( document ); /** * Fullscreen plugin * * Press F5 to enter fullscreen and ESC to exit fullscreen mode. * * Copyright 2019 @giflw * Released under the MIT license. */ /* global document */ ( function( document ) { "use strict"; function enterFullscreen() { var elem = document.documentElement; if ( !document.fullscreenElement ) { elem.requestFullscreen(); } } function exitFullscreen() { if ( document.fullscreenElement ) { document.exitFullscreen(); } } // Wait for impress.js to be initialized document.addEventListener( "impress:init", function( event ) { var api = event.detail.api; var root = event.target; var gc = api.lib.gc; var util = api.lib.util; gc.addEventListener( document, "keydown", function( event ) { // 116 (F5) is sent by presentation remote controllers if ( event.code === "F5" ) { event.preventDefault(); enterFullscreen(); util.triggerEvent( root.querySelector( ".active" ), "impress:steprefresh" ); } // 27 (Escape) is sent by presentation remote controllers if ( event.key === "Escape" || event.key === "F5" ) { event.preventDefault(); exitFullscreen(); util.triggerEvent( root.querySelector( ".active" ), "impress:steprefresh" ); } }, false ); util.triggerEvent( document, "impress:help:add", { command: "F5 / ESC", text: "Fullscreen: Enter / Exit", row: 200 } ); }, false ); } )( document ); /** * Goto Plugin * * The goto plugin is a pre-stepleave plugin. It is executed before impress:stepleave, * and will alter the destination where to transition next. * * Example: * * *
* * *
* * *
* * See https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values for a table * of what strings to use for each key. * * Copyright 2016-2017 Henrik Ingo (@henrikingo) * Released under the MIT license. */ /* global window, document, impress */ ( function( document, window ) { "use strict"; var lib; document.addEventListener( "impress:init", function( event ) { lib = event.detail.api.lib; }, false ); var isNumber = function( numeric ) { return !isNaN( numeric ); }; var goto = function( event ) { if ( ( !event ) || ( !event.target ) ) { return; } var data = event.target.dataset; var steps = document.querySelectorAll( ".step" ); // Data-goto-key-list="" & data-goto-next-list="" ////////////////////////////////////////// if ( data.gotoKeyList !== undefined && data.gotoNextList !== undefined && event.origEvent !== undefined && event.origEvent.key !== undefined ) { var keylist = data.gotoKeyList.split( " " ); var nextlist = data.gotoNextList.split( " " ); if ( keylist.length !== nextlist.length ) { window.console.log( "impress goto plugin: data-goto-key-list and data-goto-next-list don't match:" ); window.console.log( keylist ); window.console.log( nextlist ); // Don't return, allow the other categories to work despite this error } else { var index = keylist.indexOf( event.origEvent.key ); if ( index >= 0 ) { var next = nextlist[ index ]; if ( isNumber( next ) ) { event.detail.next = steps[ next ]; // If the new next element has its own transitionDuration, we're responsible // for setting that on the event as well event.detail.transitionDuration = lib.util.toNumber( event.detail.next.dataset.transitionDuration, event.detail.transitionDuration ); return; } else { var newTarget = document.getElementById( next ); if ( newTarget && newTarget.classList.contains( "step" ) ) { event.detail.next = newTarget; event.detail.transitionDuration = lib.util.toNumber( event.detail.next.dataset.transitionDuration, event.detail.transitionDuration ); return; } else { window.console.log( "impress goto plugin: " + next + " is not a step in this impress presentation." ); } } } } } // Data-goto-next="" & data-goto-prev="" /////////////////////////////////////////////////// // Handle event.target data-goto-next attribute if ( isNumber( data.gotoNext ) && event.detail.reason === "next" ) { event.detail.next = steps[ data.gotoNext ]; // If the new next element has its own transitionDuration, we're responsible for setting // that on the event as well event.detail.transitionDuration = lib.util.toNumber( event.detail.next.dataset.transitionDuration, event.detail.transitionDuration ); return; } if ( data.gotoNext && event.detail.reason === "next" ) { var newTarget = document.getElementById( data.gotoNext ); // jshint ignore:line if ( newTarget && newTarget.classList.contains( "step" ) ) { event.detail.next = newTarget; event.detail.transitionDuration = lib.util.toNumber( event.detail.next.dataset.transitionDuration, event.detail.transitionDuration ); return; } else { window.console.log( "impress goto plugin: " + data.gotoNext + " is not a step in this impress presentation." ); } } // Handle event.target data-goto-prev attribute if ( isNumber( data.gotoPrev ) && event.detail.reason === "prev" ) { event.detail.next = steps[ data.gotoPrev ]; event.detail.transitionDuration = lib.util.toNumber( event.detail.next.dataset.transitionDuration, event.detail.transitionDuration ); return; } if ( data.gotoPrev && event.detail.reason === "prev" ) { var newTarget = document.getElementById( data.gotoPrev ); // jshint ignore:line if ( newTarget && newTarget.classList.contains( "step" ) ) { event.detail.next = newTarget; event.detail.transitionDuration = lib.util.toNumber( event.detail.next.dataset.transitionDuration, event.detail.transitionDuration ); return; } else { window.console.log( "impress goto plugin: " + data.gotoPrev + " is not a step in this impress presentation." ); } } // Data-goto="" /////////////////////////////////////////////////////////////////////////// // Handle event.target data-goto attribute if ( isNumber( data.goto ) ) { event.detail.next = steps[ data.goto ]; event.detail.transitionDuration = lib.util.toNumber( event.detail.next.dataset.transitionDuration, event.detail.transitionDuration ); return; } if ( data.goto ) { var newTarget = document.getElementById( data.goto ); // jshint ignore:line if ( newTarget && newTarget.classList.contains( "step" ) ) { event.detail.next = newTarget; event.detail.transitionDuration = lib.util.toNumber( event.detail.next.dataset.transitionDuration, event.detail.transitionDuration ); return; } else { window.console.log( "impress goto plugin: " + data.goto + " is not a step in this impress presentation." ); } } }; // Register the plugin to be called in pre-stepleave phase impress.addPreStepLeavePlugin( goto ); } )( document, window ); /** * Help popup plugin * * Example: * * *
* * For developers: * * Typical use for this plugin, is for plugins that support some keypress, to add a line * to the help popup produced by this plugin. For example "P: Presenter console". * * Copyright 2016 Henrik Ingo (@henrikingo) * Released under the MIT license. */ /* global window, document */ ( function( document, window ) { "use strict"; var rows = []; var timeoutHandle; var triggerEvent = function( el, eventName, detail ) { var event = document.createEvent( "CustomEvent" ); event.initCustomEvent( eventName, true, true, detail ); el.dispatchEvent( event ); }; var renderHelpDiv = function() { var helpDiv = document.getElementById( "impress-help" ); if ( helpDiv ) { var html = []; for ( var row in rows ) { for ( var arrayItem in row ) { html.push( rows[ row ][ arrayItem ] ); } } if ( html ) { helpDiv.innerHTML = "\n" + html.join( "\n" ) + "
\n"; } } }; var toggleHelp = function() { var helpDiv = document.getElementById( "impress-help" ); if ( !helpDiv ) { return; } if ( helpDiv.style.display === "block" ) { helpDiv.style.display = "none"; } else { helpDiv.style.display = "block"; window.clearTimeout( timeoutHandle ); } }; document.addEventListener( "keyup", function( event ) { if ( event.keyCode === 72 || event.keyCode === 191 ) { // "h" || "?" event.preventDefault(); toggleHelp(); } }, false ); // API // Other plugins can add help texts, typically if they support an action on a keypress. /** * Add a help text to the help popup. * * :param: e.detail.command Example: "H" * :param: e.detail.text Example: "Show this help." * :param: e.detail.row Row index from 0 to 9 where to place this help text. Example: 0 */ document.addEventListener( "impress:help:add", function( e ) { // The idea is for the sender of the event to supply a unique row index, used for sorting. // But just in case two plugins would ever use the same row index, we wrap each row into // its own array. If there are more than one entry for the same index, they are shown in // first come, first serve ordering. var rowIndex = e.detail.row; if ( typeof rows[ rowIndex ] !== "object" || !rows[ rowIndex ].isArray ) { rows[ rowIndex ] = []; } rows[ e.detail.row ].push( "" + e.detail.command + "" + e.detail.text + "" ); renderHelpDiv(); } ); document.addEventListener( "impress:init", function( e ) { renderHelpDiv(); // At start, show the help for 7 seconds. var helpDiv = document.getElementById( "impress-help" ); if ( helpDiv ) { helpDiv.style.display = "block"; timeoutHandle = window.setTimeout( function() { var helpDiv = document.getElementById( "impress-help" ); helpDiv.style.display = "none"; }, 7000 ); // Regster callback to empty the help div on teardown var api = e.detail.api; api.lib.gc.pushCallback( function() { window.clearTimeout( timeoutHandle ); helpDiv.style.display = ""; helpDiv.innerHTML = ""; rows = []; } ); } // Use our own API to register the help text for "h" triggerEvent( document, "impress:help:add", { command: "H", text: "Show this help", row: 0 } ); } ); } )( document, window ); /** * 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 'zh-CN': case 'zh-cn': lang = { 'noNotes': '
当前帧没有备注
', 'restart': '重新开始', 'clickToOpen': '点击以打开演讲者控制界面', 'prev': '上一帧', 'next': '下一帧', 'loading': '加载中', 'ready': '就绪', 'moving': '移动中', '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 when already set causes glitches 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 when already set causes glitches 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', 'slideView' ); preView.contentDocument.body.classList.add( 'impress-console', 'preView' ); 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', 'slideView' ); if ( cssFileIframe !== undefined ) { slideView.contentDocument.head.insertAdjacentHTML( 'beforeend', '' ); } } ); preView.addEventListener( 'load', function() { preView.contentDocument.body.classList.add( 'impress-console', 'preView' ); 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 = '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 clickStr = 'var x = document.getElementById(\'impress-console-button\');' + 'x.parentNode.removeChild(x);' + 'var r = document.getElementById(\'' + rootId + '\');' + 'impress(\'' + rootId + '\').lib.util.triggerEvent(r, \'impress:console:open\', {})'; var styleStr = 'margin: 25vh 25vw;width:50vw;height:50vh;'; 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' ) { 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 `; }; } )( document, window ); /** * Media Plugin * * This plugin will do the following things: * * - Add a special class when playing (body.impress-media-video-playing * and body.impress-media-video-playing) and pausing media (body.impress-media-video-paused * and body.impress-media-audio-paused) (removing them when ending). * This can be useful for example for darkening the background or fading out other elements * while a video is playing. * Only media at the current step are taken into account. All classes are removed when leaving * a step. * * - Introduce the following new data attributes: * * - data-media-autoplay="true": Autostart media when entering its step. * - data-media-autostop="true": Stop media (= pause and reset to start), when leaving its * step. * - data-media-autopause="true": Pause media but keep current time when leaving its step. * * When these attributes are added to a step they are inherited by all media on this step. * Of course this setting can be overwritten by adding different attributes to inidvidual * media. * * The same rule applies when this attributes is added to the root element. Settings can be * overwritten for individual steps and media. * * Examples: * - data-media-autoplay="true" data-media-autostop="true": start media on enter, stop on * leave, restart from beginning when re-entering the step. * * - data-media-autoplay="true" data-media-autopause="true": start media on enter, pause on * leave, resume on re-enter * * - data-media-autoplay="true" data-media-autostop="true" data-media-autopause="true": start * media on enter, stop on leave (stop overwrites pause). * * - data-media-autoplay="true" data-media-autopause="false": let media start automatically * when entering a step and let it play when leaving the step. * * -
...
* All media is startet automatically on all steps except the one that has the * data-media-autoplay="false" attribute. * * - Pro tip: Use